優秀的判斷力來自經驗,但經驗來自於錯誤的判斷
一、查詢的基本概念
查詢又稱檢索,是資訊處理領域的一項十分重要的功能,它同人們的日常工作和生活有著密切的聯絡。
對於一個查詢演算法的時間複雜度,既可以採用數量級的形式表示,也可以採用平均查詢長度來表示。平均查詢長度就是在整個查詢過程中需要訪問元素的平均個數,即用來衡量在查詢路徑上需要同多少元素進行比較後才能查詢成功。平均查詢長度計算公式為:
ASL=∑(下標是i=1,上面是n)PiCi
其中,n為查詢表的長度,即表中所含元素的個數,Pi為查詢第i個元素的概率,查詢全部n個元素的概率之和恆定值1,若不特別指明,均認為查詢每個元素概率相同,即P1=P2=......=Pn=1/n,Ci是查詢第i個元素時與給定值X所需比較元素的個數。若查詢每個元素的概率相同,則平均查詢長度的計算公式可簡化為:
ASL=1/n∑(下標是i=1,上面是n)Ci
二、順序表查詢
1、順序查詢
順序查詢是一種最簡單和最基本的查詢方法。它從順序表的一端開始,依次將每個元素值同給定值x進行比較,若某個元素值等於給定值x,則表明查詢成功,返回該元素所在的下標;若直到所有n個元素都比較完畢,仍找不到與x相等的元素,則表明查詢失敗,返回特定值,假定用-1表示。
順序查詢的演算法比較簡單,用Java語言描述為:
public static int sequenceSearch(Object [] a, Object x,int n) { //從陣列a的前n個元素中順序查找出值為x的元素 int i; //從表頭元素a[0]開始順序向後查詢,查詢成功則退出迴圈 for(i=0;i<n;i++) { if(a[i].equals(x)) { break; } } if(i<n) { return i; } else { return -1; } }
對順序查詢演算法的一個改進,可在表的尾端設定一個“崗哨”,即在查詢之前把給定值x賦給陣列a中下標為n的元素位置,這樣每迴圈一次只需要進行元素比較,不需要進行下標是否越界的檢查,當比較到下標為n的元素位置時,由於a[n]等於x必然成立,將自動退出迴圈。改進後的演算法描述為:
public static int sequenceSearch(Object [] a, Object x,int n) { //從陣列a的前n個元素中順序查找出值為x的元素 a[n]=x; int i; //從表頭元素a[0]開始順序向後查詢,查詢成功則退出迴圈 for(i=0;i<n;i++) { if(a[i].equals(x)) { break; } } if(i<n) { return i; } else { return -1; } }
由於改進後的演算法省略了對下標越界的檢查,所以必定能夠提高演算法的實際執行速度。當然在改進後的演算法中,陣列a的長度a.length要大於等於n+1.
順序查詢的缺點是速度較慢,查詢成功最多需要比較全部n個元素,平均查詢長度為(1+2+3+4+。。。+n)/n=(n+1)/2次,約為表長度n的一半,查詢失敗也需比較n+1次(即i從0取值到n),所以順序查詢的時間複雜度為O(n)。
2、二分查詢
二分查詢又稱折半查詢。作為二分查詢物件的資料表必須是順序儲存的有序表,通常假定有序表是按元素的關鍵字從小到大有序,即若關鍵字為數值,則按數值有序,若關鍵字為字元或字串資料,則按照國際雙位元組編碼有序。二分查詢的過程是:首先取整個有序表a[0]~a[n-1]的中點元素a[min](其中min=(n-1)/2)同給定值x比較,若相等,則查詢成功,返回該元素的下標mid;否則,若a[mid]>x成立,表明若存在對應的元素,則該元素只可能落在左子表a[0]~a[mid-1]中,接著只要在左子表中繼續進行二分查詢即可;若a[mid]<x成立,表明若存在對應的元素,則該元素只可能落在右子表a[mid+1]~a[n-1]中,接著只要在右子表中繼續進行二分查詢即可;這樣,經過一次比較,就將縮小一半的查詢空間,如此進行下去,直到找到與給定值x相等的元素,或者當前查詢區間為空(即表明查詢失敗)為止。
二分查詢的演算法描述為:
public static int binarySearch(Object [] a,Object x,int n)
{
//從陣列a的前n個元素中二分查詢給定值為x的元素
int low=0,high=n-1; //給表示待查區間上界和下界的變數賦值
while(low<=high)
{
int mid=(low+high)/2; //求出待查區間內中點元素的下標
if(((Comparable)a[mid]).compareTo(x)==0)
{
return mid; //查詢成功返回元素的下標
}
else if(((Comparable)a[mid]).compareTo(x)>0)
{
high=mid-1; //修改區間下界,將在右子表上繼續查詢
}
else
{
low=mid+1; //修改區間下界,將在右子表上繼續查詢
}
}
return -1; //查詢失敗返回-1
}
二分查詢過程可用一棵二叉樹來描述,樹中的每個根結點對應當前查詢區間的中點元素a[mid],它的左子樹和右子樹分別對應該區間在左子表和右子表,通常把此二叉樹稱為二分查詢的判定樹。由於二分查詢是在有序表上進行的,所以其對應的判定樹必然是一棵二叉搜尋樹。
進行二分查詢的判定樹不僅是一棵二叉搜尋樹,而且是一棵理性平衡樹,因為除最後一層外,其餘所有層的結點數都是滿的,所以判定樹的高度h和結點數n之間的關係為:
h=log2n(向下取整)+1或h=log2(n+1)(向上取整)
這就告訴我們,二分查詢成功時,同元素進行比較的次數最多為判定樹的高度h,在查詢每個元素等概率的情況下,同元素的平均比較次數要略低於h,所以二分查詢演算法的時間複雜度為O(log2n)。顯然二分查詢比順序查詢的速度要快得多。
二分查詢的優點是比較次數少,查詢速度快,但在查詢之前要為建立有序表付出代價,同時對有序表的插入和刪除都需要平均比較和移動表中的一半元素,是很浪費時間的操作,所以,二分查詢適用於資料相對穩定、很少進行插入和刪除運算的情況。另外,二分查詢只適用於順序儲存的有序表,不適用於連結儲存的有序表。
三、索引查詢
1、索引的概念
索引查詢又稱分級查詢。索引查詢是在具有索引儲存結構的資料表上進行的查詢。索引儲存結構的方法是:首先把一個集合或線性表按照一定的函式關係或條件劃分成若干個不同的子表,為每個子表對應建立一個索引項,由所有這些索引構成項構成對主表(即原集合或線性表)的一個索引表,然後,可採用順序或連結的方法來儲存索引表和主表中的每個子表。索引表中的每個索引項通常包含3個域:一是索引值域,用來儲存對應子表的索引值,它相當於記錄元素的關鍵字,在索引表中由此索引值來唯一標識一個索引項,亦即唯一標識一個子表;二是子表的開始位置域,用來儲存對應子表的第一個元素的儲存位置;三是子表長度域,用來儲存對應子表的長度,即所包含的元素個數,此域不是必須的,可以根據情況取捨。索引項的型別可定義為:
public class IndexItem {
Object index; //索引值的定義
int start; //子表中第一個元素所在的下標位置
int length; //子表的長度域
public IndexItem(Object ind,int sta,int len)
{
index=ind;
start=sta;
length=len;
}
}
這裡假定所有子表(合稱為主表)被順序或連結儲存在同一個陣列中,每個元素的儲存位置就是其元素的下標值,所以子表的開始位置域start的型別被定義為int。
2、索引的引用舉例
例如,一個學校的教師登記表如表10-1所示,若以每個教師記錄的職工號作為關鍵字,則此線性表(假定用LA表示)可簡記為:
LA=(JS001,JS002,JS003,JS004,DZ001,DZ002,DZ003,JJ001,JJ002,HG001,HG002,HG003)
若按照單位資料項的值(或關鍵字中的前兩位字元)對錶LA進行劃分,使得具有相同值的元素分類到同一個子表中,則得到的4個子表分別為:
JS=(JS001,JS002,JS003,JS004)
DZ=(DZ001,DZ002,DZ003)
JJ=(JJ001,JJ002)
HG=(HG001,HG002,HG003)
若使用一維陣列a來順序儲存這4個子表,在實際儲存時,可以在每個子表的後面預留一些空閒位置,待向子表中插入新元素之用,在這裡假定預留兩個空閒位置,則JS,DZ,JJ,HG子表在陣列a中的開始下標位置應依次為0,6,11和15。根據這種劃分所建立的索引表如表10-2所示。
還可以按照職稱資料項的值進行劃分,使得具有相同職稱的教師記錄被組織在同一個子表中,則得到的4個子表分別為:
JSH=(JS001,HG001)
FJS=(JS004,DZ003,HG002)
JIA=(JS002,JS003,DZ002,JJ001,JJ002)
ZHU=(DZ001,HG003)
。。。。。。
在索引儲存結構中,若索引表中每個索引項對應主表中的多條記錄,則稱為稀疏索引;若每個索引項唯一對應主表中的一條記錄,則稱為稠密索引。
在計算機儲存系統中,若儲存原始資料記錄的主檔案時無序的,即記錄不是按照關鍵字有序排列的,則一級索引(即對主檔案進行的索引)必須使用稠密索引,並且通常使索引表按關鍵字有序;若主檔案是有序的,則一級索引應稀疏索引,每個索引項對應主表中若干條記錄,每個索引項中的索引值要大於等於對應一組記錄的最大關鍵字,同時要小於下一個索引項所對應一組記錄的最小關鍵字,顯然這種稀疏索引也是按索引值有序的。若在檔案儲存中使用二級或二級以上索引,則相應的索引表均應為稀疏索引。
在訪問一個採用索引儲存結構的檔案時,首先是把整個索引表讀入到記憶體中,以便能夠利用順序或二分查詢方法快速地查找出給定索引值對應的一組記錄的開始儲存位置,然後再從主檔案的相應子表中查找出給定關鍵字的記錄。
3、索引查詢演算法
索引查詢是在索引表和主表上進行的查詢。索引查詢的過程是:首先根據給定的索引值K1,在索引表上查找出索引值等於K1的索引項,以確定對應子表在主表中的開始位置和長度,然後再根據給定值K2,在對應的子表中查找出等於K2的元素(結點)。對索引表或子表進行查詢時,若表示順序儲存的有序表,則既可進行順序查詢,也可進行二分查詢,否則只能進行順序查詢。
設陣列a是一個索引儲存結構中的主表,儲存著所有子表中的元素,陣列b是建立在主表a上的一個索引表,m為索引表的實際長度,即所含的索引項的個數,它要小於等於陣列b的長度b.length,k1和k2分別為給定待查詢的索引值和元素值,當然它們的實際型別分別為索引表中索引值域的型別和主表的型別,並假定每個子表採用順序儲存,則索引查詢演算法描述為:
public static int indexSearch(Object [] a, IndexItem[] b,int m,Object k1,Object k2)
{
//利用主表a和大小為m的索引表b索引查詢值為k1,元素值為k2的記錄
int i,j;
//在索引表中順序查詢索引值為k1的索引項
for(i=0;i<m;i++)
{
if(k1.equals(b[i].index))
{
break;
}
}
//若i等於m則表明查詢失敗,返回-1
if(i==m)
{
return -1;
}
//在已經查詢到的第i個子表中順序查詢元素為k2的記錄
j=b[i].start;
while(j<b[i].start+b[i].length)
{
if(k2.equals(a[j]))
{
break;
}
else
{
j++;
}
}
//若查詢成功則返回元素在主表中的下標位置,否則返回-1
if(j<b[i].start+b[i].length)
{
return j;
}
else
{
return -1;
}
}
若每個子表在主表a中採用的是以元素下標為地址的連結儲存,則只要把上面演算法中的while迴圈和其後的if語句替換為如下的while迴圈和返回語句即可:
如索引表b為稠密索引,則演算法更為簡單,只要在引數表中給出索引表引數b、索引表長度引數m和待查的元素值引數k即可,而在演算法中只需要利用k的關鍵字值查詢索引表b,並當查詢成功時返回b[i].start的值,失敗時返回-1即可。
索引查詢的比較次數等於演算法中查詢索引表的比較次數和查詢相應子表的比較次數之和。假定索引表的長度為m,每個子表的平均長度為s,則索引查詢的平均長度為:
ASL=(1+m)/2+(1+s)/2=1+(m+s)/2
索引查詢的速度快於順序查詢,而慢於二分查詢。在主表被等分為√n(根號n)個子表的條件下,其時間複雜度為O(√n)。
若在主表中的每個儲存子表後都預留空閒位置,則索引儲存也便於進行插入和刪除運算,因為其運算過程只涉及索引表和相應的子表,只需要對相應子表中的元素進行比較和移動,與其他任何子表無關,不像順序表那樣需要涉及整個表中的所有元素,即牽一髮而動全身。
4、分塊查詢
分塊查詢屬於索引查詢。它要求主表中每個子表(子表又稱為塊)之間是遞增有序的,即前塊中的最大關鍵字必須小於後塊中的最小關鍵字,或者說後塊中的最小關鍵字必須大於前塊中的最大關鍵字,但每個塊中元素的排列次序可以是任意的;它還要求索引表中的每個索引項的索引值域用來儲存對應塊中的最大關鍵字。由分塊查詢對主表和索引表的要求可知:索引表是按索引值遞增有序的,即索引表是一個有序表;主表中的關鍵字域和索引表中的索引值域具有相同的資料型別,即為關鍵字所屬的型別。
圖10-4 就是一個分塊查詢的示例,主表被分為3塊,每塊都佔有5個記錄位置,第1塊中含有4個記錄,第2塊中含有5個記錄,第3塊中含有3個記錄。第1塊中的最大關鍵字為34,它小於第2塊中的最小關鍵字36,第2塊中的最大關鍵字為72,它小於第3塊中的最小關鍵字86,所以,主表中塊與塊之間是遞增有序的。從圖中的索引表可以看出:每個索引項中的索引值域儲存著對應塊中的最大關鍵字,索引表是按照索引值遞增有序的。
當進行分塊查詢時,應根據所給的關鍵字首先查詢索引表,從中查找出剛好大於等於所給關鍵字的那個索引項,從而找到待查塊,然後再查詢這個塊,從中順序查詢到相應的記錄(若存在的話)。由於索引表是有序的,所以在索引表上既可以採用順序查詢,也可以採用二分查詢,而每個塊中的記錄排列是任意的,所以在塊內只能採用順序查詢。
例如,根據圖10-4 查詢關鍵字為40的記錄時,假定採用順序的方法查詢索引表,首先用40同第1項索引值34比較,因40>34,則接著同第2項索引值72比較,因40<=72,所以查詢索引表結束,轉而順序查詢主表中從下標5開始的塊,因關鍵字為40的記錄位於該塊的第3個位置,所以經過3次比較後查詢成功。
分塊查詢的演算法同上面已經給出的索引查詢演算法類似,其演算法描述為:
//分塊查詢
public static int blockSearch(Object [] a,IndexItem [] b,int m,Object k)
{
//利用主表a和大小為m的索引表b分塊查詢元素值為k的記錄
int i=0,j=0;
//在索引表中順序查詢索引值為k所對應的索引項
for(i=0;i<m;i++)
{
if(((Comparable)k).compareTo(b[i].index)<=0)
{
break;
}
}
//若i等於m,則表明查詢失敗,返回-1
if(i==m)
{
return -1;
}
//在已經查詢到的第i個子表中順序查詢元素值為k的記錄
j=b[i].start;
while(j<b[i].start+b[i].length)
{
if(((Comparable)k).compareTo(a[j])==0)
{
break;
}
}
//若查詢成功則返回元素的下標位置,否則返回-1
if(j<b[i].start+b[i].length)
{
return j;
}
else
{
return -1;
}
}
若在索引表上不是順序查詢,而是二分查詢相應的索引項,則需要把演算法中的for迴圈語句更換為如下的程式段:
int low=0,high=m-1;
while(low<=high)
{
int mid=(low+high)/2;
if(((Comparable)k).compareTo(b[mid].index)==0)
{
i=mid;
break;
}
else if(((Comparable)k).compareTo(b[mid].index)<0)
{
high=mid-1;
}
else
{
low=mid+1;
}
}
if(low>high)
{
i=low;
}
在這裡當而二分查詢失敗時,應把low的值賦給i,此時b[i].index是剛大於k的索引值。當然如low的值為m,則表示真正的查詢失敗。