第四章:樹與二叉樹(樹與二叉樹的應用:二叉排序樹、二叉平衡樹、哈夫曼樹)
1.二叉排序樹
二叉排序樹:BST,也稱二叉查找樹
二叉排序樹或者爲空樹,或爲非空樹,當爲非空樹時有如下特點:
- 若左子樹非空,則左子樹上所有結點關鍵字值
均小於
根結點的關鍵字 - 若右子樹非空,則右子樹上所有結點關鍵字值
均大於
根結點的關鍵字 - 左、右子樹本身也分別是一棵二叉排序樹。
注意這裏是小於和大於而沒有等於,就是說二叉排序樹中不存在值相同的結點。
二叉排序樹中序遍歷:1 2 3 4 5 6 8 10 16
這裏可以發現,二叉排序樹的中序遍歷結果的時遞增的,這符合所有的二叉排序樹。
二叉排序樹的中序遍歷序列時一個遞增的有序序列
1.1二叉排序樹的查找
- 二叉樹非空時,查找根結點,若相等則查找成功;
- 若不等,則當小於根結點值時,查找左子樹;當大於根結點的值時,查找右子樹。
- 當查找到葉節點仍沒查找到相應的值,則查找失敗。
練習:
查找5:首先8>5查找左子樹,5>4查找右子樹,5=5查找成功
查找6:首先8>6查找左子樹,6>4查找右子樹,6>5查找右子樹,6<7查找左子樹爲空,查找失敗。
我們根據這個過程其實很容易發現整個查找過程可以使用遞歸進行完成,可以自行嘗試,這裏使用非遞歸查詢,代碼編寫:
參1 二叉樹,參2 關鍵字 參3 保存查找到的結點的雙親結點,是一個指針引用型的變量,這裏函數體內對該指針進行修改時,不僅僅會對形參進行修改,而且會對我們傳入的變量指針進行修改。
BSTNode *BST_Search(BiTree T,ElemType key,BSTNode *&p){
p = NULL; //雙親結點置爲空(根結點沒有雙親結點)
while(T != NULL && key != T->data){//樹非空且關鍵字不匹配
p = T; //p指向改結點
if(key < T->data){
T = T->lchild; //循環查找左子樹
}else{
T = T->rchild; //循環查找右子樹
}
}
return T;
}
時間複雜度:O(h) (h爲二叉排序樹的高度)
1.2二叉排序樹的插入
- 若二叉排序樹爲空,則直接插入結點;
- 若二叉排序樹非空,當值小於根結點時,插入左子樹;當值大於根結點時,插入右子樹;當值等於根結點時不進行插入
練習:
插入6:6<8插入左子樹,6>4插入右子樹,6>5插入左子樹。
代碼編寫:
//參1 二叉樹(注意是引用),參2 插入值
int BST_Insert(BiTree &T,KeyType k){
if(T==NULL){ //樹爲空
T = (BiTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1;
}else if(k == T->key){ //相同不插入
return 0;
}else if(k < T->key){//小於插入左子樹中,遞歸調用
return BST_Insert(T->lchild,k);
}else{//大於插入右子樹中,遞歸調用
return BST_Insert(T->rchild,k);
}
}
1.3構造二叉排序樹
讀入一個元素並建立結點,若二叉樹爲空將其作爲根結點;若二叉排序樹非空,當值小於根結點時,插入左子樹;當值大於根結點時,插入右子樹;當值等於根結點時不進行插入。
//參3 插入結點的數量
void Create_BST(BiTree &T,KeyType str[],int n){
T = NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
二叉排序樹的構造過程就算兩個數組的值完全相同,但是構造順序不同,所生成的二叉排序樹就是不同的。
1.4二叉排序樹的刪除
假設我們刪除4結點,則剩下的4結點的左子樹和右子樹該怎麼形成結點8的左子樹呢?
爲了維護二叉樹的基本性質,其實刪除操作比較複雜的,我們需要分三種情況:
- 1.若被刪除的結點z是葉子結點則可以直接刪除,不影響
- 2.如果被刪除的結點z只有一個子樹,則讓z的子樹成爲z父結點的子樹,帶題z結點。
例如我們刪除結點5
- 3.若被刪除的結點z有兩顆子樹,則讓z的中序序列直接後繼代替z,並刪去直接後繼結點。
例如我們刪除結點4,我們知道結點4的直接後繼結點爲4的右子樹的最左邊的那一個結點,這裏是5,我們直接將結點4換成結點5,然後直接刪除結點5即可,這裏爲什麼能夠直接刪除?因爲這個結點一定是最左側那個結點,要麼沒有子樹(葉子結點),要麼就是隻有右子樹(如果有左子樹他就不是最左側的那個結點),所以刪除的時候直接參考上面的兩種情況就可以。
思考:
在二叉排序樹中刪除並插入某節點,得到的二叉排序樹是否與原來相同?
首先我們刪除一個結點7:
接着插入結點7
此時發現刪除並插入後二叉排序樹相同,這是刪除葉子結點的情況,出現了刪除並插入後二叉排序樹相同,有沒有不同的情況?
如果我們刪除的是一個雙親結點的5
然後插入結點5
此時我們發現刪除並插入後二叉排序樹不相同了。
故: 在二叉排序樹中刪除並插入某節點,根據刪除並插入的結點類型不同,得到的二叉排序樹可能相同,也可能不同。
1.5查找效率
查找長度:查找該節點時所經歷的結點的數量。
平均查找長度(ASL):所有結點查找長度求和取平均值,它取決於樹的高度。
例如:
查找效率:O(log2n)
查找效率:O(n)
2.平衡二叉樹
平衡二叉樹:AVL,任意結點的平衡因子
的絕對值
不超過一。
平衡因子:左子樹高度
-
右子樹高度
如上圖二叉樹,是否是平衡二叉樹?
可以把所有結點的平衡因子計算出來,進行判斷,可以看出此二叉樹是平衡二叉樹。
高度爲h的最小(結點數最少)平衡二叉樹
的結點數Nh?
根據平衡二叉樹 右側那個結點的值可以選擇:h、h-1和h-2,如果選擇h,加上根節點1層,總層數爲h+1,不符合題意。其中h-1和h-2都可以選擇但是爲什麼選擇h-2?是因爲這裏說的是最小平衡二叉樹,h-1層結點數肯定比h-2層結點數多。N0=0(0層結點數爲0)N1=1(結點數爲1)
比如我們現在要計算高度爲3的最小平衡二叉樹的結點數。則:
N3=N2+N1+1; =》N2=N1+N0+1=2; =》N3=2+1+1=4;
2.1平衡二叉樹的判斷
利用遞歸的後序遍歷過程:
- 判斷左子樹是一棵平衡二叉樹
- 判斷右子樹是一棵平衡二叉樹
- 判斷以該結點爲根的二叉樹爲平衡二叉樹
判斷條件
若左子樹和右子樹均爲平衡二叉樹且左子樹與右子樹高度差的絕對值小於等於1,則平衡。
根據判斷條件我們知道,每個結點要保存兩個變量:一個是該節點的平衡性(b:1平衡 0不平衡)另一個是該節點的高度(h)。
//參1 該棵樹的根節點
void Judge_AVL(BiTree bt,int &balance,int &h){
//左子樹平衡性左子樹高度 右子樹平衡性右子樹高度
int bl=0,br=0,hl=0,hr=0;
if(bt==NULL){//如果根節點爲空
h=0; //高度設爲0
balance=1; //並且是平衡的
}else if(bt->lchild==NULL&&bt->rchild==NULL){
//左子樹和右子樹都爲空
h=1; //高度爲1
balance=1; //平衡的
}else{
Judge_AVL(bt->lchild,bl,hl); //判斷左子樹
Judge_AVL(bt->rchild,br,hr); //判斷右子樹
//下面計算該節點爲根二叉樹的高度
//首先判斷哪個子樹的高度高,然後加1即可
if(hl>hr){
h=hl+1;
}else{
h=hr+1;
}
//判斷平衡性
// abs 是取絕對值
if(abs(hl-hr)<2&&bl==1&&br==1){
balance=1;
}else{
balance=0;
}
}
}
3.平衡二叉樹的插入
平衡二叉樹的插入過程其實和二叉排序樹的插入過程相比多了一步,如果按照二叉排序樹的插入過程,所形成的二叉樹不一定是平衡二叉樹,所以我們需要先插入之後進行調整。即:先插入後調整
。
調整原則:每次調整最小不平衡子樹
例如,如上圖插入完成之後我們需要調整,我們調整是從插入結點開始,向上依次調整。首先4結點平衡因子爲-1,符合平衡二叉樹,然後向上6結點平衡因子爲2,不符合平衡二叉樹,則需要調整。
3.1LL平衡旋轉(右單旋轉)
出現不平衡的原因:在結點A的左孩子的左子樹上插入了新結點。
調整方法:右旋操作:用A的左孩子B代替A,將A結點稱爲B的右子樹根結點,而B的原右子樹則作爲A的左子樹。
出現了不平衡的情況,下面調整成平衡二叉樹:
3.2RR平衡旋轉(左單旋轉)
出現不平衡的原因:在結點A的右孩子的左子樹上插入了新結點。
調整方法:左旋操作:用A的右孩子B代替A,將A結點稱爲B的左子樹根結點,而B的原左子樹則作爲A的右子樹。
3.3LR平衡旋轉(先左後右雙旋轉)
出現不平衡的原因:在結點A的左孩子的右子樹上插入了新結點。
調整方法:先左旋後右旋轉操作:將A的左孩子B的右孩子結點C代替B,然後再將C結點向上代替A的位置。
注意:其中Cl和Cr也可能都爲空,因爲B的右子樹Br可能爲空。
3.4RL平衡旋轉(先右後左雙旋轉)
出現不平衡的原因:在結點A的左孩子的左子樹上插入了新結點。
調整方法:先右旋後左旋轉操作:將A的右孩子B的左孩子結點C代替B,然後再將C結點向上代替A的位置。
4.帶權路徑長度
在學習哈夫曼樹之前首先需要了解 帶權路徑長度
路徑長度:路徑上所經歷 邊
的個數
結點的權:結點被賦予的數值
樹的帶權路徑長度:WPL,樹中所有 葉結點
的帶權路徑長度之和
如上二叉樹它的帶權路徑長度爲:WPL=7*2+2*2+3*2=24
如上二叉樹它的帶權路徑長度爲:WPL=7*1+2*2+3*2=17
我們可以看出,雖然兩個二叉樹的結點的權重相同,但是他們的帶權路徑長度缺不一樣,由此我們引出了哈夫曼樹的定義
5.哈夫曼樹
哈夫曼樹:也稱最優二叉樹,含有n個帶權葉子結點帶權路徑長度最小的二叉樹。
5.1哈夫曼樹的構造
哈夫曼樹的構造算法
- 將n個結點作爲n棵僅含有一個根結點的二叉樹,構成森林F
- 生成一個新結點,並從F中找出根結點權值最小的兩棵樹作爲它的左右子樹,且新結點的權值爲兩棵子樹根結點的權值之和
- 從F中刪除這兩個樹,並將新生成的樹加入到F中
- 重複2,3步驟,直到F中只有一棵樹爲止
這裏需要注意,哈夫曼樹的構造過程並未要求那棵樹作爲左子樹那顆樹作爲右子樹,所以哈夫曼樹是不唯一的
例如上面的例子:
首先是三個帶有權重的結點A B C
接着挑選兩個根節點權重最小的樹 B、C,形成一棵二叉樹,該二叉樹的根結點的權重爲兩個結點權重之和,然後放回森林中
然後依舊挑選兩個根節點權重最小的樹…
5.2哈夫曼樹的性質
- 每個初始結點都會成爲葉節點,雙支結點都爲新生成的結點
- 權值越大離根結點越近,反之權值越小離根結點越遠
- 哈夫曼樹中沒有結點的度爲1
- n個葉子結點的哈夫曼樹的結點總數爲2n-1,其中度爲2的結點數爲n-1
5.3哈夫曼編碼
編碼:對於一個字符串序列,用二進制來表示字符
固定長度編碼:
例如:HelloWorld,其中每一個字符使用三位的二進制表示,由此可以得出該字符串對應的二進制序列:000001010010011100011101010110
由上面的編碼表示我們可能有兩點疑慮:1.爲什麼要用三位的二進制表示,而不用比較短的二進制表示,比如兩位的?2.其中l
字符出現的次數比較多,那麼這些出現次數比較多的字符是不是可以使用比較短的編碼從而得到比較短的二進制序列?由此引出了第二種編碼方式:可變長度編碼
可變長度編碼:
例如:HelloWorld,我們用比較短的二進制表示,由此可以得出該字符串對應的二進制序列:0001001101110000
由此可以看到這樣的序列比之前的序列短了很多,但是這樣的序列是不可以應用的,因爲我們之前使用固定長度編碼每三位代表一個字符,依次遍歷就可以得到對應的字符串,但是現在可變長度的編碼比如:00,可以代表字符H或者兩個字符l,這樣就產生了歧義。其實前綴編碼纔是可用的。
前綴編碼:沒有一個編碼是另一個編碼的前綴
我們修改上面的對應表格爲:
我們將 l 修改爲 11,它不是任何一個編碼的前綴,這樣形成的二進制序列就可以逆置成字符串了。
那麼這樣的前綴編碼是怎麼得到的呢?其實就是利用了哈夫曼樹的特點。舉個栗子:五個字母以及出現的次數:A:5 B:3 C:6 D:9 E:13,我們利用哈夫曼樹的構造算法構造出一個哈夫曼樹,首先每一個字母作爲一個結點,並且它的權重就是它的出現次數
然後構造哈夫曼樹
接着我們只需要將樹中所有左邊的邊賦值爲0,右邊的邊賦值爲1
然後我們利用從根節點到某一個結點所以經歷的邊,就可以得到結點對應的前綴編碼了:D 00 - E 01 - C 10 - A 110 - B 111,而且我們可以看出出現次數越多的結點,它的前綴編碼越短,出現次數越少的結點它的編碼越長,也達到了縮短二進制序列的目的。
最後我們需要注意:
哈夫曼樹並不唯一,所以每個字符對應的哈夫曼編碼也不唯一,但帶權路徑長度相同且最優
關於數據結構的知識公衆號 理木客同步更新中,下次將會講解:數據結構 圖,歡迎大家的關注