二叉查詢樹
二叉查詢樹(BST:Binary Search Tree)是一種特殊的二叉樹,它改善了二叉樹節點查詢的效率。二叉查詢樹有以下性質:
二叉查詢樹節點的定義:
1 typedef struct BSTreeNode
2 {
3 int data;
4 struct BSTreeNode *left;//左子樹
5 struct BSTreeNode *right;//右子樹
6 }BSTree;
跟普通二叉樹的節點定義相同
因為查詢節點和插入節點都是在已經構建好二叉查詢樹的前提下才能進行的,在刪除節點的時候才涉及到調整二叉樹的操作,所以這裡先以前序遍歷的順序直接輸入一個二叉查詢樹,程式碼如下
1 /* 建立二叉查詢樹(資料以前序遍歷順序輸入)*/ 2 BSTree *Create_BSTreeNode(BSTree *nod) 3 { 4 int num; 5 6 scanf_s("%d", &num, 1); 7 if (num == 0) /* 假定輸入的資料都為正數,以0作為NULL的標誌 */ 8 { 9 return NULL; 10 } 11 else 12 { 13 if ((nod = (BSTree *)malloc(sizeof(BSTree))) == NULL) 14 { 15 printf("記憶體空間不足"); 16 exit(0); 17 } 18 nod->data = num; 19 nod->left = Create_BSTreeNode(nod->left); 20 nod->right = Create_BSTreeNode(nod->right); 21 22 return nod; 23 } 24 } 25 26 /* 前序遍歷二叉樹,並列印 */ 27 void PreOrder_Traverse(BSTree *nod, int level) 28 { 29 if (nod == NULL) 30 { 31 return ; 32 } 33 34 printf("data = %d level = %d\n", nod->data, level); 35 PreOrder_Traverse(nod->left, level + 1); 36 PreOrder_Traverse(nod->right, level + 1); 37 }
1、查詢節點(遞迴實現)
若根結點的關鍵字等於查詢的關鍵字,查詢成功,若小於根結點的關鍵字的值,遞迴查詢左子樹,若大於根結點的關鍵字的值,遞迴查詢右子樹,若子樹為空,則查詢失敗,查詢的操作較為簡單,實現程式碼如下
1 /* 查詢特定值 */
2 void SearchData(int targ, BSTree *nod)
3 {
4 if (nod != NULL)
5 {
6 if (nod->data == targ)
7 {
8 printf("查詢值存在,值為%d\n", nod->data);
9 }
10 else if (nod->data > targ)
11 {
12 SearchData(targ, nod->left); //遞迴查詢左子樹
13 }
14 else if (nod->data < targ)
15 {
16 SearchData(targ, nod->right); //遞迴查詢右子樹
17 }
18 }
19 else if (nod == NULL)
20 {
21 printf("查詢值不存在\n");
22 }
23 }
通過 BST 查詢節點,理想情況下我們需要檢查的節點數可以減半。如下圖中的 BST 樹,包含了 15 個節點。從根節點開始執行查詢演算法,第一次比較決定我們是移向左子樹還是右子樹,對於任意一種情況,一旦執行這一步,我們需要訪問的節點數就減少了一半,從 15 降到了 7。同樣,下一步訪問的節點也減少了一半,從 7 降到了 3,以此類推,根據這一特點,查詢演算法的時間複雜度應該是 O(log2n)
(圖片來源:https://www.cnblogs.com/gaochundong/p/binary_search_tree.html)
對於 BST 查詢演算法來說,其十分依賴於樹中節點的拓撲結構,也就是節點間的佈局關係,當 BST 樹中的節點以扇形結構散開時,對它的插入、刪除和查詢操作最優的情況下可以達到亞線性的執行時間 O(log2n),
因為當在 BST 中查詢一個節點時,每一步比較操作後都會將節點的數量減少一半。儘管如此,如果拓撲結構像下圖中的樣子時,執行時間就會退減到線性時間 O(n)。因為每一步比較操作後還是需要逐個比較其餘的節點,
也就是說,在這種情況下,在 BST 中查詢節點與在陣列(Array)中查詢就基本類似了。
因此,BST 演算法查詢時間依賴於樹的拓撲結構。最佳情況是 O(log2n),而最壞情況是 O(n)
測試用例:
以前序遍歷順序輸入二叉查詢樹
2、插入節點(遞迴實現)
新插入的結點一定是一個新新增的葉子結點,如下圖
雖然上面兩種插入結果得到的二叉樹都符合二叉查詢樹的性質,但是不滿足“新插入的結點一定是一個新新增的葉子結點”,因為有了這個特點插入的操作變得相對簡單,實現程式碼如下
1 /* 新增新節點 */
2 BSTree *AddNewNode(BSTree *cur, int NewData)
3 {
4 if (cur == NULL)
5 {
6 if ((cur = (BSTree *)malloc(sizeof(BSTree))) == NULL) //建立新節點
7 {
8 printf("記憶體不足");
9 exit(0);
10 }
11 cur->data = NewData;
12 cur->left = NULL;
13 cur->right = NULL;
14
15 return cur;
16 }
17 if (NewData > cur->data)
18 {
19 cur->right = AddNewNode(cur->right, NewData);
20 }
21 else if (NewData < cur->data)
22 {
23 cur->left = AddNewNode(cur->left, NewData);
24 }
25 else if (NewData == cur->data)
26 {
27 printf("不允許插入重複值\n");
28 exit(0);
29 }
30
31 return cur;
32 }
實際執行效果(測試查詢與插入):
查詢:以前序遍歷順序輸入一個二叉查詢樹(0作為NULL標誌)
插入:以前序遍歷順序輸入一個二叉查詢樹
3、刪除節點
刪除節點的操作相對查詢和插入要相對複雜一些,主要考慮以下三種情況(前兩種情況操作較為簡單,第三種較為複雜)
在刪除操作前先用要找到待刪除節點的位置(這裡使用的遞迴,也可以改成迭代)
情形一:刪除葉子節點
因為刪除葉子節點不會破壞BST的結構,刪除葉子節點的操作較為簡單,步驟如下
1、判斷待刪除節點的左右子樹是否為空,如果都為空那麼就是葉子節點
2、判斷待刪除節點是待刪除節點父節點的右子樹還是左子樹,將對應的指標賦值NULL
3、free待刪除節點
實現程式碼:
1 /* 刪除節點 */
2 void DeletNode(BSTree *parent, BSTree *cur, int DelData)
3 {
4 BSTree *SNode = NULL; //後繼節點
5 BSTree *PSNode = NULL; //後繼節點的父節點
6
7 if (DelData > cur->data)
8 {
9 DeletNode(cur, cur->right, DelData);
10 }
11 else if (DelData < cur->data)
12 {
13 DeletNode(cur, cur->left, DelData);
14 }
15 else if(DelData == cur->data)
16 {
17 if (cur->left == NULL && cur->right == NULL) //刪除節點為葉子節點
18 {
19 if (parent->left == cur) //如果該節點是父節點的左子樹
20 {
21 parent->left = NULL;
22 }
23 else if (parent->right == cur) //如果該節點是父節點的右子樹
24 {
25 parent->right = NULL;
26 }
27 free(cur); //釋放刪除節點
28 }
情形二:刪除帶有一個子節點的節點
(圖片來源:https://www.cnblogs.com/songwenjie/p/8973217.html)
上圖寫了四種,但對待刪除節點來說只有兩種,只有左子樹,或只有右子樹,兩種情況的處理方式基本相同,都是將待刪除節點的左/右子樹 賦值給 待刪除節點的父節點的左/右子樹
實現程式碼:
1 else if(cur->left != NULL && cur->right == NULL) //待刪除節點只有左子樹
2 {
3 if (parent->left == cur)
4 {
5 parent->left = cur->left;
6 }
7 else if (parent->right == cur)
8 {
9 parent->right = cur->left;
10 }
11 free(cur); //釋放待刪除節點
12 }
13 else if(cur->left == NULL && cur->right != NULL) //待刪除節點只有右子樹
14 {
15 if (parent->left == cur)
16 {
17 parent->left = cur->right;
18 }
19 else if (parent->right == cur)
20 {
21 parent->right = cur->right;
22 }
23 free(cur); //釋放待刪除節點
24 }
情形三:刪除帶兩個節點的節點
因為刪除節點會有破壞 BST 正確結構的風險,刪除帶兩個節點的節點操作顯得較為複雜,首先需要找到待刪除節點的 後繼節點 和 該後繼節點的父節點,(一個節點的後繼節點是指,這個節點在中序遍歷序列中的下一個節點,相應的,前驅節點是指這個節點在中序遍歷序列中的上一個節點),刪除節點的後繼節點一定是刪除節點右子樹的最左側節點,這篇隨筆採用的方式是後繼節點替代待刪除節點的方式而不是前驅節點替代刪除節點,需要考慮的情況如下
1、後繼節點為待刪除節點的子節點
在後繼節點為待刪除節點的子節點的前提下,該後繼節點有右子樹和沒有右子樹的操作是相同的,都是將 後繼節點 替代 待刪除節點,並將待刪除節點的左子樹 賦值給 後繼節點的左子樹
實現程式碼:
1 else if(cur->left != NULL && cur->right != NULL) //待刪除節點既有左子樹也有右子樹
2 {
3 SNode = SearchSuccessorNode(cur->right); //搜尋後繼節點
4 PSNode = SearchParentofSNode(cur->right, cur->right); //搜尋後繼節點的父節點
5
6 if (cur->right == SNode) //後繼節點為待刪除節點的右子樹(後繼節點有右子樹和沒有右子樹的操作相同)
7 {
8 if (parent->left == cur)
9 {
10 parent->left = SNode;
11 SNode->left = cur->left;
12
13 free(cur);
14 }
15 else if (parent->right == cur)
16 {
17 parent->right = SNode;
18 SNode->left = cur->left;
19
20 free(cur);
21 }
22 }
2、後繼節點不為待刪除節點的子節點
這裡後繼節點還要在分為後繼節點有子節點和沒有子節點的情況
(1)後繼節點沒有右子節點
根據實現程式碼來標註上面的節點
刪除後:
實現程式碼:
1 else if (cur->right != SNode && SNode->right == NULL) //後繼節點不為待刪除節點的右子樹,並且該後繼節點沒有右子樹
2 {
3 if (parent->left == cur)
4 {
5 parent->left = SNode;
6 SNode->left = cur->left;
7 SNode->right = cur->right;
8 PSNode->left = NULL;
9
10 free(cur);
11 }
12 else if (parent->right == cur)
13 {
14 parent->right = SNode;
15 SNode->left = cur->left;
16 SNode->right = cur->right;
17 PSNode->left = NULL;
18
19 free(cur);
20 }
21 }
(2)後繼節點有右子節點
刪除後:
與上面的後繼節點沒有右子節點相比需要增加一個操作,需要將後繼節點的右子樹 賦值給 後繼節點的父節點的左子樹
實現程式碼:
1 else if (cur->right != SNode && SNode->right != NULL) //後繼節點不為待刪除節點的右子樹,並且該後繼節點有右子樹
2 {
3 if (parent->left == cur)
4 {
5 parent->left = SNode;
6 PSNode->left = SNode->right; //後繼節點的右子樹作為後繼節點父節點的左子樹
7 SNode->left = cur->left;
8 SNode->right = cur->right;
9
10 free(cur);
11 }
12 else if (parent->right == cur)
13 {
14 parent->right = SNode;
15 PSNode->left = SNode->right; //後繼節點的右子樹作為後繼節點父節點的左子樹
16 SNode->left = cur->left;
17 SNode->right = cur->right;
18
19 free(cur);
20 }
21 }
測試資料:
一、“後繼節點是刪除節點的子節點”(因為後繼節點有無子樹的操作相同,這裡只測試沒有子樹的情況)
二、“後繼節點不是刪除節點的子節點,且後繼節點沒有右子樹”
三、“後繼節點不是刪除節點的子節點,且後繼節點有右子樹”
完整程式碼:(注:對free(cur)的位置進行了調整)
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <conio.h> 4 5 #define LEFT 1 6 #define RIGHT 2 7 8 typedef struct BSTreeNode 9 { 10 int data; 11 struct BSTreeNode *left;//左子樹 12 struct BSTreeNode *right;//右子樹 13 }BSTree; 14 15 BSTree *Create_BSTreeNode(BSTree *nod); //建立二叉查詢樹 16 void PreOrder_Traverse(BSTree *nod, int level); //前序遍歷二叉樹,並列印 17 void SearchData(int targ, BSTree *nod); //查詢特定值 18 BSTree *AddNewNode(BSTree *cur, int NewData); //新增新的節點 19 void DeletNode(BSTree *parent, BSTree *cur, int DelData); //刪除節點 20 BSTree *SearchSuccessorNode(BSTree *nod); //搜尋後繼節點 21 BSTree *SearchParentofSNode(BSTree *Pnod, BSTree *nod); //搜尋後繼節點的父節點 22 23 int main() 24 { 25 BSTree *nod = NULL; 26 //int num; 27 //int key; 28 int del; 29 30 nod = Create_BSTreeNode(nod); 31 PreOrder_Traverse(nod, 1); 32 //printf("輸出查詢資料\n"); 33 //scanf_s("%d", &num, 1); 34 //SearchData(num, nod); 35 //printf("輸出新插入資料\n"); 36 //scanf_s("%d", &key, 1); 37 38 printf("輸出刪除的資料\n"); 39 scanf_s("%d", &del, 1); 40 DeletNode(nod, nod, del); 41 42 //AddNewNode(nod, key); 43 PreOrder_Traverse(nod, 1); 44 45 return 0; 46 } 47 48 /* 搜尋後繼節點的父節點 */ 49 BSTree *SearchParentofSNode(BSTree *Pnod, BSTree *nod) 50 { 51 while (1) 52 { 53 if (nod->left != NULL) 54 { 55 Pnod = nod; 56 nod = nod->left; 57 } 58 else 59 { 60 break; 61 } 62 } 63 64 return Pnod; 65 } 66 67 /* 搜尋後繼節點 */ 68 BSTree *SearchSuccessorNode(BSTree *nod) 69 { 70 while (1) 71 { 72 if (nod->left != NULL) 73 { 74 nod = nod->left; 75 } 76 else 77 { 78 break; 79 } 80 } 81 82 return nod; 83 } 84 85 /* 刪除節點 */ 86 /* cur為待刪除節點, parent為待刪除節點的父節點 */ 87 void DeletNode(BSTree *parent, BSTree *cur, int DelData) 88 { 89 BSTree *SNode = NULL; //後繼節點 90 BSTree *PSNode = NULL; //後繼節點的父節點 91 92 if (DelData > cur->data) 93 { 94 DeletNode(cur, cur->right, DelData); 95 } 96 else if (DelData < cur->data) 97 { 98 DeletNode(cur, cur->left, DelData); 99 } 100 else if(DelData == cur->data) 101 { 102 if (cur->left == NULL && cur->right == NULL) //待刪除節點為葉子節點 103 { 104 if (parent->left == cur) //如果該節點是父節點的左子樹 105 { 106 parent->left = NULL; 107 } 108 else if (parent->right == cur) //如果該節點是父節點的右子樹 109 { 110 parent->right = NULL; 111 } 112 } 113 else if(cur->left != NULL && cur->right == NULL) //待刪除節點只有左子樹 114 { 115 if (parent->left == cur) 116 { 117 parent->left = cur->left; 118 } 119 else if (parent->right == cur) 120 { 121 parent->right = cur->left; 122 } 123 } 124 else if(cur->left == NULL && cur->right != NULL) //待刪除節點只有右子樹 125 { 126 if (parent->left == cur) 127 { 128 parent->left = cur->right; 129 } 130 else if (parent->right == cur) 131 { 132 parent->right = cur->right; 133 } 134 } 135 else if(cur->left != NULL && cur->right != NULL) //待刪除節點既有左子樹也有右子樹 136 { 137 SNode = SearchSuccessorNode(cur->right); //搜尋後繼節點 138 PSNode = SearchParentofSNode(cur->right, cur->right); //搜尋後繼節點的父節點 139 140 if (cur->right == SNode) //後繼節點為待刪除節點的右子樹(後繼節點有右子樹和沒有右子樹的操作相同) 141 { 142 if (parent->left == cur) 143 { 144 parent->left = SNode; 145 SNode->left = cur->left; 146 } 147 else if (parent->right == cur) 148 { 149 parent->right = SNode; 150 SNode->left = cur->left; 151 } 152 } 153 else if (cur->right != SNode && SNode->right == NULL) //後繼節點不為待刪除節點的右子樹,並且該後繼節點沒有右子樹 154 { 155 if (parent->left == cur) 156 { 157 parent->left = SNode; 158 SNode->left = cur->left; 159 SNode->right = cur->right; 160 PSNode->left = NULL; 161 } 162 else if (parent->right == cur) 163 { 164 parent->right = SNode; 165 SNode->left = cur->left; 166 SNode->right = cur->right; 167 PSNode->left = NULL; 168 } 169 } 170 else if (cur->right != SNode && SNode->right != NULL) //後繼節點不為待刪除節點的右子樹,並且該後繼節點有右子樹 171 { 172 if (parent->left == cur) 173 { 174 parent->left = SNode; 175 PSNode->left = SNode->right; //後繼節點的右子樹作為後繼節點父節點的左子樹 176 SNode->left = cur->left; 177 SNode->right = cur->right; 178 } 179 else if (parent->right == cur) 180 { 181 parent->right = SNode; 182 PSNode->left = SNode->right; //後繼節點的右子樹作為後繼節點父節點的左子樹 183 SNode->left = cur->left; 184 SNode->right = cur->right; 185 } 186 } 187 } 188 free(cur); //釋放待刪除節點 189 } 190 } 191 192 /* 新增新節點 */ 193 BSTree *AddNewNode(BSTree *cur, int NewData) 194 { 195 if (cur == NULL) 196 { 197 if ((cur = (BSTree *)malloc(sizeof(BSTree))) == NULL) //建立新節點 198 { 199 printf("記憶體不足"); 200 exit(0); 201 } 202 cur->data = NewData; 203 cur->left = NULL; 204 cur->right = NULL; 205 206 return cur; 207 } 208 if (NewData > cur->data) 209 { 210 cur->right = AddNewNode(cur->right, NewData); 211 } 212 else if (NewData < cur->data) 213 { 214 cur->left = AddNewNode(cur->left, NewData); 215 } 216 else if (NewData == cur->data) 217 { 218 printf("不允許插入重複值\n"); 219 exit(0); 220 } 221 222 return cur; 223 } 224 225 /* 查詢特定值 */ 226 void SearchData(int targ, BSTree *nod) 227 { 228 if (nod != NULL) 229 { 230 if (nod->data == targ) 231 { 232 printf("查詢值存在,值為%d\n", nod->data); 233 } 234 else if (nod->data > targ) 235 { 236 SearchData(targ, nod->left); //遞迴查詢左子樹 237 } 238 else if (nod->data < targ) 239 { 240 SearchData(targ, nod->right); //遞迴查詢右子樹 241 } 242 } 243 else if (nod == NULL) 244 { 245 printf("查詢值不存在\n"); 246 } 247 } 248 249 /* 建立二叉查詢樹(資料以前序遍歷順序輸入)*/ 250 BSTree *Create_BSTreeNode(BSTree *nod) 251 { 252 int num; 253 254 scanf_s("%d", &num, 1); 255 if (num == 0) /* 假定輸入的資料都為正數,以0作為NULL的標誌 */ 256 { 257 return NULL; 258 } 259 else 260 { 261 if ((nod = (BSTree *)malloc(sizeof(BSTree))) == NULL) 262 { 263 printf("記憶體空間不足"); 264 exit(0); 265 } 266 nod->data = num; 267 nod->left = Create_BSTreeNode(nod->left); 268 nod->right = Create_BSTreeNode(nod->right); 269 270 return nod; 271 } 272 } 273 274 /* 前序遍歷二叉樹,並列印 */ 275 void PreOrder_Traverse(BSTree *nod, int level) 276 { 277 if (nod == NULL) 278 { 279 return ; 280 } 281 282 printf("data = %d level = %d\n", nod->data, level); 283 PreOrder_Traverse(nod->left, level + 1); 284 PreOrder_Traverse(nod->right, level + 1); 285 }
因為主要是分析二叉查詢樹的查詢、插入、刪除操作,沒有考慮二叉樹為空 或 待刪除節點為根節點的情況