前面章節所介紹的有關在靜態查找表中對特定關鍵字進行順序查找、折半查找或者分塊查找,都是在查找表中各關鍵字被查找概率相同的前提下進行的。
例如查找表中有 n 個關鍵字,表中每個關鍵字被查找的概率都是 1/n。在等概率的情況,使用折半查找算法的性能最優。
而在某些情況下,查找表中各關鍵字被查找的概率是不同的。例如水果商店中有很多種水果,對於不同的顧客來說,由於口味不同,各種水果可能被選擇的概率是不同的。假設該顧客喜吃酸,那麼相對於蘋果和橘子,選擇橘子的概率肯定要更高一些。
在查找表中各關鍵字查找概率不相同的情況下,對於使用折半查找算法,按照之前的方式進行,其查找的效率並不一定是最優的。例如,某查找表中有 5 個關鍵字,各關鍵字被查找到的概率分別爲:0.1,0.2,0.1,0.4,0.2(全部關鍵字被查找概率和爲 1 ),則根據之前介紹的折半查找算法,建立相應的判定樹爲(樹中各關鍵字用概率表示):
折半查找查找成功時的平均查找長度的計算方式爲:
ASL = 判定樹中各結點的查找概率*所在層次
所以該平均查找長度爲:
ASL=0.1*1 + 0.1*2 + 0.4*2 + 0.2*3 + 0.2*3 = 2.3
由於各關鍵字被查找的概率是不相同的,所以若在查找時遵循被查找關鍵字先和查找概率大的關鍵字進行比對,建立的判定樹爲:
相應的平均查找長度爲:
ASL=0.4*1 + 0.2*2 + 0.2*2 + 0.1*3 + 0.1*3=1.8
後者折半查找的效率要比前者高,所以在查找表中各關鍵字查找概率不同時,要考慮建立一棵查找性能最佳的判定樹。若在只考慮查找成功的情況下,描述查找過程的判定樹其帶權路徑長度之和(用 PH 表示)最小時,查找性能最優,稱該二叉樹爲靜態最優查找樹
。
帶權路徑之和的計算公式爲:PH = 所有結點所在的層次數 * 每個結點對應的概率值。
但是由於構造最優查找樹花費的時間代價較高,而且有一種構造方式創建的判定樹的查找性能同最優查找樹僅差 1% - 2%,稱這種極度接近於最優查找樹的二叉樹爲次優查找樹
。
次優查找樹的構建方法
次優查找樹的算法描述如下:
已知一個序列:(rl,rl+1,……,rh),遞增有序。它對應的權值爲:(wl,wl+1,……,wh)。
定義:
取 △Pi 最小的那個元素 i 作爲根,然後分別對子序列(rl,rl+1,……,ri-1)和(ri+1,ri+2,……,rh)同樣構造次優查找樹,並分別作爲 i 的左子樹和右子樹。
在計算 △Pi 時,實際上就是計算元素 i 前面的元素的權值之和與元素i後面的元素的權值之和的差值。如果對每一個元素都要這樣計算就有很多重複計算,爲了提高效率,我們引入“累計權值和”:
並設 wl-1 = 0 和 swl-1 = 0,則
從上面這個公式可以看出,我們只要一次性地求出所有元素的 swi 值並保存起來,以後每次求 △Pi 就只要查表中對應的四個 sw 值進行計算就可以了。
我們先來看看下面這個例子:
關鍵字 | A | B | C | D | E | F | G | H | I | |
---|---|---|---|---|---|---|---|---|---|---|
權值 | 0 | 1 | 1 | 2 | 5 | 3 | 4 | 4 | 3 | 5 |
j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
swj | 0 | 1 |
2 |
4 |
9 |
12 |
16 |
20 |
23 |
28 |
△Pj | 27 | 25 | 22 | 15 | 7 | 0 |
8 | 15 | 23 | |
(根) | ↑i |
|||||||||
△Pj | 11 | 9 | 6 | 1 |
9 | 8 | 1 |
7 | ||
(根) | ↑i |
↑i |
||||||||
△Pj | 3 | 1 |
2 | 0 |
0 |
0 |
||||
(根) | ↑i |
↑i |
↑i |
↑i |
||||||
△Pj | 0 |
0 |
||||||||
(根) | ↑i |
↑i |
最終構造的次優二叉樹如下圖:
代碼實現爲:
typedef int KeyType;//定義關鍵字類型
typedef struct{
KeyType key;
}ElemType;//定義元素類型
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//定義變量
int i;
int min;
int dw;
//創建次優查找樹,R數組爲查找表,sw數組爲存儲的各關鍵字的概率(權值),low和high表示的sw數組中的權值的範圍
void SecondOptimal(BiTree T, ElemType R[], float sw[], int low, int high){
//由有序表R[low...high]及其累計權值表sw(其中sw[0]==0)遞歸構造次優查找樹
i = low;
min = abs(sw[high] - sw[low]);
dw = sw[high] + sw[low - 1];
//選擇最小的△Pi值
for (int j = low+1; j <=high; j++){
if (abs(dw-sw[j]-sw[j-1])<min){
i = j;
min = abs(dw - sw[j] - sw[j - 1]);
}
}
T = (BiTree)malloc(sizeof(BiTNode));
T->data = R[i];//生成結點(第一次生成根)
if (i == low) T->lchild = NULL;//左子樹空
else SecondOptimal(T->lchild, R, sw, low, i - 1);//構造左子樹
if (i == high) T->rchild = NULL;//右子樹空
else SecondOptimal(T->rchild, R, sw, i + 1, high);//構造右子樹
}
完整事例演示
例如,一含有 9 個關鍵字的查找表及其相應權值如下表所示:
則構建次優查找樹的過程如下:
首先求出查找表中所有的 △P 的值,找出整棵查找表的根結點:
例如,關鍵字 F 的 △P 的計算方式爲:從 G 到 I 的權值和 - 從 A 到 E 的權值和 = 4+3+5-1-1-2-5-3 = 0。
通過上圖左側表格得知,根結點爲 F,以 F 爲分界線,左側子表爲 F 結點的左子樹,右側子表爲 F 結點的右子樹(如上圖右側所示),繼續查找左右子樹的根結點:
通過重新分別計算左右兩查找子表的 △P 的值,得知左子樹的根結點爲 D,右子樹的根結點爲 H (如上圖右側所示),以兩結點爲分界線,繼續判斷兩根結點的左右子樹:
通過計算,構建的次優查找樹如上圖右側二叉樹所示。
後邊還有一步,判斷關鍵字 A 和 C 在樹中的位置,最後一步兩個關鍵字的權值爲 0 ,分別作爲結點 B 的左孩子和右孩子,這裏不再用圖表示。
注意:在建立次優查找樹的過程中,由於只根據的各關鍵字的 P 的值進行構建,沒有考慮單個關鍵字的相應權值的大小,有時會出現根結點的權值比孩子結點的權值還小,此時就需要適當調整兩者的位置。
總結
由於使用次優查找樹和最優查找樹的性能差距很小,構造次優查找樹的算法的時間複雜度爲 O(nlogn),因此可以使用次優查找樹表示概率不等的查找表對應的靜態查找表(又稱爲靜態樹表)。