中根遍历二叉查找树所得序列一定是有序序列_二叉搜索树(BST)
前面我們介紹了樹的基本概念,并引出了二叉樹。值得注意的是,無特征的二叉樹在工程上幾乎沒啥用處,一般都是使用bst、avl,trie,rbtree等具有特殊特征的二叉樹。
下面,我們先來看一看二叉搜索樹。
BST(Binary?Search?Tree,二叉搜索樹)可以提高查找的性能,二叉搜索樹相比于其他數據結構的優勢在于查找、插入的時間復雜度較低,平均為O(log n),最差為O(n),此時相當于二叉搜索樹變成了鏈表。從這一個特點我們應該不難發現,樹,是鏈表和數組之間的一種平衡,為了獲得更好性能的查找、插入和刪除方法。
BST的特點是:
1.若任意節點的左子樹不空,則左子樹上所有節點的值均小于它的根節點的值;
2.若任意節點的右子樹不空,則右子樹上所有節點的值均大于或等于它的根節點的值;
3.任意節點的左、右子樹也分別為二叉查找樹;?
中序遍歷二叉查找樹可得到一個關鍵字的有序序列,一個無序序列可以通過建構一棵二叉查找樹變成一個有序序列,建構樹的過程即為對無序序列進行查找的過程。每次插入的新的結點都是二叉查找樹上新的葉子結點(作為葉子節點插入),在進行插入操作時,不必移動其它結點,只需改動某個結點的指針,由空變為非空即可。
如圖1-1所示,即為一顆二叉搜索樹:
寫在前面:注意,此時我們實現的二叉搜索樹,不考慮有關鍵字相同的情況,即不考慮不同節點的數據元素相同時的場景,這就好比加減乘除沒有學會之前,我們不考慮微積分怎么求解。在前面的文章中,我們手動使用指針搭建了一個樹形結構,目的是為了演示二叉樹的先序,中序和后序遍歷。在今天這篇文章中,我們將實現BST的插入,刪除和查找工作。為了搭建出一顆二叉搜索樹,那我們先實現插入函數。基本數據定義:
有一個BST根節點中節點初始化的宏,用于初始化根節點。還是使用Linux內核設計者們的設計思路,如同內核鏈表的基本套路一樣,但是,插入操作的函數接口如何定義是一個值得思考的問題。插入,可以按照值插入,也可以按照節點插入。下面我們演示節點插入的方式,值插入讀者可以當做練習自行實現。API:struct crystal_bst_tree { struct bst_node my_node; int num;};int bst_insert(struct bst_root *root, struct crystal_bst_tree *tree)int bst_insert(struct bst_root *root, struct bst_node *tree)bst_insert函數有兩個參數,第一個是根節點,第二個是需要插入的節點,插入節點的定義,可以使用struct crystal_bst_tree *也可以使用struct bst_node *。這兩種類型的差別無非是是否需要使用container_of宏處理而已。按照Linux內核設計者們的慣例,我們使用后者,來實現插入操作:
static inline void bst_link_node(struct bst_node * node, struct bst_node * parent, struct bst_node ** bst_link){ node->bst_left = node->bst_right = NULL; *bst_link = node;}int bst_insert(struct bst_root *root, struct bst_node *tree){ struct bst_node **new = &(root->bst_node), *parent = NULL; /* Figure out where to put new node */ while (*new) { struct crystal_bst_tree *this = bst_entry(*new, struct crystal_bst_tree, my_node); struct crystal_bst_tree *pri_tree = bst_entry(tree, struct crystal_bst_tree, my_node); parent = *new; if (pri_tree->num < this->num) new= &((*new)->bst_left); else if (pri_tree->num > this->num) new = &((*new)->bst_right); else return -1; } /* Add new node */ bst_link_node(tree, parent, new); return 1;}其中parent變量,在這里是沒有意義的,但是,我們后面的擴展例子rbtree中會使用到。所以這里暫且保留。插入使用循環版本,想使用遞歸方式的讀者可以自己實踐。
有了插入之后,我們實現一下遍歷,因為前面的文章已經實現過了,這里直接貼出代碼:
void PreOrderTraverse(struct bst_node *T)//先序遍歷{ if(T == NULL) { return ; } struct crystal_bst_tree *obj; obj = bst_entry(T,struct crystal_bst_tree,my_node); printf("%d ",obj->num); PreOrderTraverse(T->bst_left); PreOrderTraverse(T->bst_right);}void InOrderTraverse(struct bst_node *T)//中序遍歷{ if(T == NULL) { return ; } InOrderTraverse(T->bst_left); struct crystal_bst_tree *obj; obj = bst_entry(T,struct crystal_bst_tree,my_node); printf("%d ",obj->num); InOrderTraverse(T->bst_right);}void PostOrderTraverse(struct bst_node * T)//后序遍歷{ if(T == NULL) { return; } PostOrderTraverse(T->bst_left); PostOrderTraverse(T->bst_right); struct crystal_bst_tree *obj; obj = bst_entry(T,struct crystal_bst_tree,my_node); printf("%d ",obj->num);}有了插入,遍歷之后,我們再實現一個查找函數,為什么Linux內核的紅黑樹把查找函數交給用戶自己實現呢?因為此時必須要要根據具體的數據類型來進行操作了,查找可以是根據節點查找,也可以是根據值查找,這里我們選取值查找的方式:
struct crystal_bst_tree *my_search(struct bst_root *root, int num){ struct bst_node *node = root->bst_node; while (node) { struct crystal_bst_tree *data = bst_entry(node, struct crystal_bst_tree, my_node); if (num < data->num) node = node->bst_left; else if (num > data->num) node = node->bst_right; else return data; } return NULL;}然后,我們再實現兩個函數,查找最小和最大值即查找first和last:
struct bst_node *bst_first(struct bst_root *root){ struct bst_node *n; n = root->bst_node; if (!n) { return NULL; } while (n->bst_left) { n = n->bst_left; } return n;}struct bst_node *bst_last(struct bst_root *root){ struct bst_node *n; n = root->bst_node; if (!n) { return NULL; } while (n->bst_right) { n = n->bst_right; } return n;}最后,我們實現刪除操作。刪除操作相比較于其他操作較為復雜一點,其代碼邏輯是:
1.若刪除結點為葉子結點,即PL(左子樹)和PR(右子樹)均為空樹。由于刪去葉子結點不破壞整棵樹的結構,則只需修改其雙親結點的指針即可。
2.若刪除結點只有左子樹PL或右子樹PR,此時只要令PL或PR直接成為其雙親結點的左子樹(當刪除節點是左子樹)或右子樹(當刪除節點是右子樹)即可,作此修改也不破壞二叉查找樹的特性。
3.若刪除結點的左子樹和右子樹均不空。在刪去節點之后,為保持其它元素之間的相對位置不變,可按中序遍歷保持有序進行調整,可以有兩種做法:其一是令刪除節點的左子樹為*f的左/右(依刪除節點是*f的左子樹還是右子樹而定)子樹,*s為刪除節點左子樹的最右下的結點,而刪除節點的右子樹為*s的右子樹;其二是令刪除節點的直接前驅(in-order predecessor)或直接后繼(in-order successor)替代刪除節點,然后再從二叉查找樹中刪去它的直接前驅(或直接后繼)。我們按照方法二,使用直接前驅或直接后繼來刪除節點。
直接前驅:尋找左子樹里面的最大值;直接后繼:尋找右子樹里面的最小值。這兩個最大最小值無論是使用遞歸還是循環,都是非常容易獲得的。
以直接前驅法為例子,找到直接前驅之后,將直接前驅替換成需要刪除的節點,并刪除直接前驅節點即可。如果是直接后繼,找到直接后繼之后,將直接后繼替換成需要刪除的節點,并刪除直接后繼節點即可。
本例使用直接前驅替換的方法:
bool Delete(struct bst_node **node){ struct bst_node *q, *s; if (!(*node)->bst_right && !(*node)->bst_left) { free(*node); *node = NULL; //該節點為葉子節點,直接刪除 } else if (!(*node)->bst_right) { // 右子樹空則只需重接它的左子樹 q = (*node)->bst_left; bst_entry(*node, struct crystal_bst_tree, my_node)->num = bst_entry(q, struct crystal_bst_tree, my_node)->num; (*node)->bst_left = q->bst_left; (*node)->bst_right = q->bst_right; free (q); } else if (!(*node)->bst_left) { // 左子樹空只需重接它的右子樹 q = (*node)->bst_right;; bst_entry(*node, struct crystal_bst_tree, my_node)->num = bst_entry(q, struct crystal_bst_tree, my_node)->num; (*node)->bst_left = q->bst_left; (*node)->bst_right = q->bst_right; free (q); } else { // 左右子樹均不空 q = *node; s = (*node)->bst_left; while (s->bst_right) {//(尋找直接前驅) q = s; s = s->bst_right; } bst_entry(*node, struct crystal_bst_tree, my_node)->num = bst_entry(s, struct crystal_bst_tree, my_node)->num; // s指向被刪結點的“前驅” if (q != *node) { q->bst_right = s->bst_left; // 重接q的右子樹 } else { q->bst_left = s->bst_left; // 重接q的左子樹 } free(s); } return true;}bool DeleteBST(struct bst_node **node, int key) { // 若二叉查找樹中存在關鍵字等于key的數據元素時,則刪除該數據元素,并返回TRUE;否則返回FALSE if (!*node) { return false; //不存在關鍵字等于key的數據元素 } else { if (key == bst_entry(*node, struct crystal_bst_tree, my_node)->num) {//找到關鍵字等于key的數據元素 return Delete(node); } else if (key < bst_entry(*node, struct crystal_bst_tree, my_node)->num) { return DeleteBST(&(*node)->bst_left, key); } else { return DeleteBST(&(*node)->bst_right, key); } }}實現上面的代碼可以很好的訓練自己的代碼能力,有遞歸,有二級指針,有樹的知識,一定要自己動手寫一寫,否則你覺得都是if else,會給你一種很簡單的感覺。
最后,再來看看main函數測試用例:
int main(void){ struct bst_root root = BST_ROOT; #define SZIE (10) struct crystal_bst_tree *tmp[SZIE]; int i; for (i = 0;i < SZIE;i++) { tmp[i] = malloc(sizeof(struct crystal_bst_tree)); } tmp[0]->num = 8; bst_insert(&root,&tmp[0]->my_node); tmp[1]->num = 3; bst_insert(&root,&tmp[1]->my_node); tmp[2]->num = 10; bst_insert(&root,&tmp[2]->my_node); tmp[3]->num = 1; bst_insert(&root,&tmp[3]->my_node); tmp[4]->num = 6; bst_insert(&root,&tmp[4]->my_node); tmp[5]->num = 14; bst_insert(&root,&tmp[5]->my_node); tmp[6]->num = 4; bst_insert(&root,&tmp[6]->my_node); tmp[7]->num = 7; bst_insert(&root,&tmp[7]->my_node); tmp[8]->num = 13; bst_insert(&root,&tmp[8]->my_node); //tmp[9]->num = 5; //bst_insert(&root,&tmp[9]->my_node); //printf("%p %p\n",tmp[6]->my_node.bst_right,&tmp[9]->my_node); //DeleteBST(&root.bst_node,7); printf("\r\nInOrderTraverse:\r\n"); InOrderTraverse(root.bst_node); printf("\r\n"); struct crystal_bst_tree *first,*last; first = bst_entry(bst_first(&root),struct crystal_bst_tree,my_node); printf("first->num = %d\r\n",first->num); last = bst_entry(bst_last(&root),struct crystal_bst_tree,my_node); printf("last->num = %d\r\n",last->num); int num = 7; struct crystal_bst_tree * t = my_search(&root,num); if (t) { printf("finded : %d\n",t->num); } else { printf("%d was not find\n",num); } return 0;}運行輸出:
使用中序遍歷,二叉搜索樹輸出是一個有序序列,這也是二叉搜索樹的應用場景之一。圖1-1所示的二叉搜索樹,如果我要再插入一個元素為5,你知道應該插入在哪個地址嗎?取消main函數中29-31行注釋的代碼,你可以發現:tmp[6]的右節點,即元素5,插入在4的右節點處。再打開32行屏蔽的代碼,測試刪除節點的操作:至此,我們就完成了BST的操作和練習。前面說到,BST最壞的情況下,算法時間復雜度為O(n),這是因為,當二叉樹剛好是一個鏈表的時候,此時退化成了線性結構,所以,人們又提出了AVL(平衡二叉樹),這也是我們下期文章講解的內容,讓我們下期再見,謝謝大家的觀看。不要以為工作了學習就與你無關,我們一直都是學生。B站(C語言教程,強烈推薦的視頻教程):https://space.bilibili.com/5782182總結
以上是生活随笔為你收集整理的中根遍历二叉查找树所得序列一定是有序序列_二叉搜索树(BST)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python工作不好找吗_如何更好的找到
- 下一篇: 今日起实施4月新规!摩托车加完油能直接启