4. 二叉查找樹(BST)
4.1 BST數據結構定義
使用C++語言,如果需要使用BST,那么不用重新造輪子了,C++語言里的map, set等STL容器應該可以滿足需求了(雖然STL里這些容器大多是以紅黑樹作為其底層實現),如果你需要使用小/大根堆(也叫優先隊列,特殊的、自平衡的BST),STL也能滿足你的需求(可以參考這里:http://www.cnblogs.com/dskit/archive/2009/12/13/1623152.html)。
先來看下BST的定義,BST是滿足如下3個條件的二叉樹:
1. 節點的左子樹包含的節點的值小於該節點的值
2. 節點的右子樹包含的節點的值大於等於該節點的值
3. 節點的左子樹和右子樹都是BST
BST的數據結構包含指向左右孩子的指針,以及一個指向節點父節點的指針(該指針在刪除節點的時候可以用於快速獲取其父節點,從而簡化操作)。BST的初始構建可以利用插入操作完成,BST最常使用的操作是查找和遍歷,還有刪除操作但相對較少使用;刪除操作是BST支持的幾種操作中實現難度最大的,下面我們依次介紹這BST的插入、查詢、遍歷和刪除操作。
1: #ifndef _BINARY_SEARCH_TREE_H 2: #define _BINARY_SEARCH_TREE_H 3: #include <stdio.h> 4: 5: /* 關鍵值比較函數 */ 6: typedef int (*bstCmp)(void *left, void *right); 7: 8: /* 遍歷樹時的處理函數 */ 9: typedef void (*bstKeyHandler)(void *key, int key_len); 10: 11: typedef struct bst { 12: struct bst *left; 13: struct bst *right; 14: /* 使用parent域的原因:在刪除節點時可以快速獲得被刪除節點的父節點 */ 15: struct bst *parent; 16: /* 關鍵值,可以是包含了豐富內容的結構 */ 17: void *key; 18: /* key所指向空間的長度 */ 19: int key_len; 20: } bst; 21: 22: typedef enum TraverseType { 23: TRAVERSE_TYPE_MID, /* 中序遍歷 */ 24: TRAVERSE_TYPE_PRE, /* 前序遍歷 */ 25: TRAVERSE_TYPE_SUF /* 后序遍歷 */ 26: } TraverseType; 27: 28: bst *bstSearch(bst *root, void *key, bstCmp cmp); 29: bst *bstInsert(bst *root, void *key, int key_len, bstCmp cmp); 30: int bstDelete(bst *root, void *key, bstCmp cmp); 31: void bstTraverse(bst *root, bstKeyHandler handler, TraverseType type); 32: #endif 33:
4.2 BST的插入
插入操作類似於查找,是個遞歸過程,只不過插入操作在查找不到的時候創建一個新節點並將其加入樹,需考慮下面4種情形:
1. 當前節點的關鍵值等於待插入節點關鍵值,則不做任何處理(若需要可更新該節點),返回;
2. 當前節點的關鍵值小於待插入節點關鍵值,根據BST的定義,待插入節點應插入當前節點的左子樹:
a) 若當前節點的左子樹為空,則待插入節點應為當前節點的左孩子,新建節點並插入,
b) 若當前節點的左子樹非空,則遞歸插入;
3. 當前節點的關鍵值大於待插入節點關鍵值,根據BST的定義,待插入節點應插入當前節點的右子樹:
a) 若當前節點的右子樹為空,則待插入節點應為當前節點的右孩子,新建節點並插入,
b) 若當前節點的右子樹非空,則遞歸插入;
4. 若當前節點為空,則說明當前為空樹,待插入節點應為樹根。
1: bst *bstNewNode(void *key, int key_len, bst *parent) 2: { 3: bst *new = (bst *)calloc(1, sizeof(bst)); 4: if (NULL == new) { 5: abort(); 6: } 7: new->key = calloc(1, key_len); 8: if (NULL == new->key) { 9: abort(); 10: } 11: new->key = key; 12: new->key_len = key_len; 13: new->parent = parent; 14: memmove(new->key, key, key_len); 15: 16: return new; 17: } 18: 19: bst *bstInsert(bst *root, void *key, int key_len, bstCmp cmp) 20: { 21: if (NULL == root) { /* 該分支處理根節點插入 */ 22: return bstNewNode(key, key_len, NULL); 23: } 24: 25: int ret = cmp(root->key, key); 26: if (0 == ret) { 27: return root; /* 關鍵值相同,不更新該元素,如需要可更新該節點 */ 28: } else if (0 < ret) { 29: if (NULL == root->right) { 30: root->right = bstNewNode(key, key_len, root); 31: return root->right; 32: } else { 33: return bstInsert(root->right, key, key_len, cmp); 34: } 35: } else /* 0 >= ret */ { 36: if (NULL == root->left) { 37: root->left = bstNewNode(key, key_len, root); 38: return root->right; 39: } else { 40: return bstInsert(root->left, key, key_len, cmp); 41: } 42: } 43: }
4.3 BST的查找
BST的查找實現利用遞歸相對簡單,具體實現如下:
1: bst *bstSearch(bst *root, void *key, bstCmp cmp) 2: { 3: if (NULL == root) { 4: return NULL; /* 被查找關鍵值不存在於樹中 */ 5: } 6: 7: int ret = cmp(root->key, key); 8: if (0 == ret) { 9: return root; /* 找到! */ 10: } else if (0 < ret) { 11: return bstSearch(root->right, key, cmp); /* 待查找關鍵值大於當前節點關鍵值,則在當前節點的右子樹中查找 */ 12: } else /* 0 >= ret */ { 13: return bstSearch(root->left, key, cmp); /* 待查找關鍵值小於當前節點關鍵值,則在當前節點的左子樹中查找 */ 14: } 15: }
4.4 BST的遍歷
遍歷實現也是利用遞歸的思路進行;可在實現中攜帶type參數,用於支持的遍歷方式:中序、前序或后序。下面的實現是中序遍歷,若需要可實現另外兩種遍歷方式。
1: void bstTraverse(bst *root, bstKeyHandler handler, TraverseType type) 2: { 3: handler(root->key, root->key_len); /* handler為節點的訪問處理函數 */ 4: 5: if (NULL != root->left) { 6: //printf("%d's left: ", *(int *)root->key); 7: bstTraverse(root->left, handler, type); 8: } 9: if (NULL != root->right) { 10: //printf("%d's right: ", *(int *)root->key); 11: bstTraverse(root->right, handler, type); 12: } 13: 14: return ; 15: }
4.5 BST的刪除
插入操作也類似於查找,是個遞歸過程,只不過刪除操作在找到被刪除節點后的處理要復雜些,需考慮下面4種情形:
1. 當前節點的關鍵值等於待刪除關鍵值,則進入刪除處理過程;
2. 當前節點的關鍵值小於待插入節點關鍵值,根據BST的定義,應在當前節點的左子樹上遞歸刪除操作;
3. 當前節點的關鍵值大於待插入節點關鍵值,根據BST的定義,應在當前節點的右子樹上遞歸刪除操作;
4. 若當前節點為空,則說明查找不到待刪除關鍵值的節點,返回-1指示刪除失敗。
刪除處理過程又需要考慮以前幾種情形:
1. 待刪除節點為葉子節點(左右孩子均為空);
a) 將待刪除節點的父節點指向該待刪除節點的指針置為空,
b) 刪除待刪除節點。
2. 待刪除節點(10)為左孩子為空,右孩子非空;
a) 將待刪除節點(10)的父節點(8)原來指向待刪除節點(10)的指針重新指向待刪除節點(10)的右孩子(14),
b) 將待刪除節點(10)的右孩子節點(14)原來指向待刪除節點(10)的父指針重新指向待刪除節點(10)的父節點(8),
b) 刪除待刪除節點(10)。
上面展示了待刪除節點(10)為(8)右孩子節點的情況,由於待刪除節點(10)的右孩子節點必定大於等於(10),而(10)又為(8)的右孩子,所以待刪除節點(10)的右孩子節點必定大於(8),待刪除節點(10)的右孩子節點可以直接取代(10)的位置作為(8)的右孩子。那么等待刪除節點本身為左孩子的情況呢?請看下圖,節點(3)滿足本身待刪除節點本身為左孩子的情況,根據BST的定義,若待刪除節點(3)為左孩子,則待刪除節點的所有孩子節點均小於其父節點(8),所以也可以將其右孩子節點直接作為(8)的左孩子。
3. 待刪除節點(14)為左孩子非空,右孩子為空;
a) 將待刪除節點(14)的父節點(10)原來指向待刪除節點(14)的指針重新指向待刪除節點(10)的左孩子(13),
b) 將待刪除節點(14)的左孩子節點(13)原來指向待刪除節點(14)的父指針重新指向待刪除節點(14)的父節點(10),
c) 刪除待刪除節點(14)。
可以這樣操作的原因分析類似2中的分析,不再贅述。
4. 待刪除節點(3)為左孩子非空,右孩子非空。
a) 將待刪除節點(3)的關鍵值與其右子樹上值最小節點(4)的值交換,原節點(4)轉換為待刪除節點,准備被刪除,
注1:待刪除節點右子樹上最小值節點的左孩子必為空, 否則,根據BST定義,最小值節點應在該節點的左子樹上;
注2:待刪除節點右子樹上最小值節點的右孩子可為空,也可不為空;
注3:待刪除節點與其右子樹上最小值節點交換后,刪除原右子樹最小值節點后,仍為BST。
b) 根據注1,刪除新待刪除節點轉換為刪除葉子節點或刪除只有右孩子節點的情況,本例為刪除葉子節點。
另外,也可以選擇待刪除節點左子樹上的最大值節點進行交換,處理方式與上述方式類似,讀者可以自行分析;有文獻稱總是選擇與右子樹上最小值節點交換或總是選擇與左子樹上最大值節點交換,可能造成樹的不平衡,從而使對BST的操作效率降低。
1: int bstDelete(bst *root, void *key, bstCmp cmp) 2: { 3: if (NULL == root) { /* 查找待刪除關鍵值失敗 */ 4: return -1; 5: } 6: 7: int ret = cmp(root->key, key); 8: if (0 == ret) { /* 查找到待刪除關鍵值,進入刪除處理程序 */ 9: if ((NULL != root->left) && (NULL != root->right)) { 10: bst *right_min = bstSearchMin(root->right); 11: bstSwap(root, right_min); /* 交換待刪除關鍵值與該待刪節點右子樹上關鍵值最小的節點的關鍵值交換 */ 12: if (right_min->parent->left == right_min) { 13: right_min->parent->left = right_min->right; /* 將指向當前待刪除節點的指針置為當前待刪除節點的右子樹(這里的右子樹可以為空) */ 14: } else { 15: right_min->parent->right = right_min->right; /* 將指向當前待刪除節點的指針置為當前待刪除節點的右子樹(這里的右子樹可以為空) */ 16: } 17: if (NULL != right_min->right) { /* 注意:這里需更新當前待刪除節點右子樹的父節點指針 */ 18: right_min->right->parent = right_min->parent; 19: } 20: free(right_min); /* 刪除待刪除節點右子樹上值最小的節點 */ 21: } else { 22: if (NULL != root->left) { 23: if (root->parent->left == root) { 24: root->parent->left = root->left; 25: } else { 26: root->parent->right = root->left; 27: } 28: root->left->parent = root->parent; 29: } else if (NULL != root->right) { 30: if (root->parent->left == root) { 31: root->parent->left = root->right; 32: } else { 33: root->parent->right = root->right; 34: } 35: root->right->parent = root->parent; 36: } else { 37: if (root->parent->left == root) { 38: root->parent->left = NULL; 39: } else { 40: root->parent->right = NULL; 41: } 42: } 43: free(root); 44: } 45: return 0; 46: } else if(0 < ret) { /* 當前節點的關鍵值大於待插入節點關鍵值,根據BST的定義,應在當前節點的右子樹上遞歸刪除操作; */ 47: return bstDelete(root->right, key, cmp); 48: } else /* 0 >= ret */ { /* 當前節點的關鍵值小於待插入節點關鍵值,根據BST的定義,應在當前節點的左子樹上遞歸刪除操作 */ 49: return bstDelete(root->left, key, cmp); 50: } 51: 52: return 0; 53: }
4.6 性能分析
平均復雜度 最壞情況復雜度
插入操作 O(logN) O(N)
查詢操作 O(logN) O(N)
刪除操作 O(logN) O(N)
當插入節點為有序序列時,構建的樹上的節點只有左孩子或右孩子,有最大復雜度O(N)。如插入有序序列(1, 2, 3, 4, 5),插入操作完成后的BST如下圖:
4.7 二叉查找樹應用
1. 如何合並兩顆BST?
法一:遍歷其中一顆BST,將其插入另一顆BST。
法二:根據兩顆樹的根節點選取一個虛擬的根節點,將兩顆BST作為虛擬根節點的左右子樹,然后對虛擬根節點進行刪除操作即可。
法一的時間復雜度為O(MlogN)或O(NlogM),法二的時間復雜度為O(logN)或O(logM)。可見法二的合並效率更高。
2. 有上百萬個電話號碼,需要頻繁的進行查找操作,怎樣設計數據結構使其效果最高?
該類問題使用BST可以很好的解決,當然使用其他改進的數據結構如紅黑樹、字典樹也是高效的解決方案。
4.8 參考文獻
http://en.wikipedia.org/wiki/Binary_search_tree
from:http://www.cnblogs.com/dskit/archive/2012/08/18/2645927.html