二叉搜索樹詳解

寫在前面

       本文主要分爲三個部分。

       第一部分介紹了二叉搜索樹的基本性質。

       第二部分全面詳細地講述了二叉搜索樹的各種基本操作。包括WALK/遍歷、SEARCH/查找、MINIMUM/最小關鍵字、MAXIMUM/最大關鍵字、SUCCESSOR/後繼、PREDECESSOR/前驅、INSERT/插入、DELETE/刪除等。主要參考《算法導論》(中文第3版)中有關二叉搜索樹的相關介紹說明。

       對於每一種基本操作,都至少分三個重點進行講解。它們分別是基本操作過程及原理(包含僞代碼及C++實現)、時間複雜度分析以及舉例分析(配圖)。力求讓每位讀者可以直觀地理解。

       第三部分則是完整的代碼實現及實例分析。

       本文斷斷續續寫了幾天,各部分的舉例分析也十分明瞭,爲了講得更清楚一些,所有的配圖都是自行製作的。請尊重勞動成果,供人供己查閱,如有錯誤之處,歡迎指正。

       原創文章,轉載請註明出處。http://blog.csdn.net/qq_21396469/article/details/78419609


一、二叉搜索樹簡介與基本性質

1、定義

       二叉搜索樹(BST)又稱二叉查找樹或二叉排序樹。一棵二叉搜索樹是以二叉樹來組織的,可以使用一個鏈表數據結構來表示,其中每一個結點就是一個對象。一般地,除了key和衛星數據(文末附註1)之外,每個結點還包含屬性lchild、rchild和parent,分別指向結點的左孩子、右孩子和雙親(父結點)。如果某個孩子結點或父結點不存在,則相應屬性的值爲空(NIL)。根結點是樹中唯一父指針爲NIL的結點,而葉子結點的孩子結點指針也爲NIL。


2、基本性質

       根據《算法導論》(中文第3版)的相關介紹,二叉搜索樹中的關鍵字總是以滿足二叉搜索樹性質的方式來存儲:

設x是二叉搜索樹中的一個結點。如果y是x左子樹中的一個結點,那麼y.key≤x.key。如果y是x右子樹中的一個結點,那麼y.key≥x.key。

       在二叉搜索樹中:

       ① 若任意結點的左子樹不空,則左子樹上所有結點的值均不大於它的根結點的值;

       ② 若任意結點的右子樹不空,則右子樹上所有結點的值均不小於它的根結點的值;

       ③ 任意結點的左、右子樹也分別爲二叉搜索樹。

       一棵典型的二叉搜索樹如下:



二、二叉搜索樹的基本操作與代碼實現

1、二叉搜索樹的結點

       正如前面所說,每個二叉搜索樹的結點,包含關鍵字key、左孩子指針lchild、右孩子指針rchild以及父結點指針parent。在C++實現中,我們定義一個結點類BSTNode來表示一個結點,並初始化結點的關鍵字等於0,左右孩子指針和父結點指針爲NIL。

  1. /* 二叉搜索樹節點 */
  2. class BSTNode
  3. {
  4. private:
  5. double key; // 關鍵字
  6. BSTNode *lchild; // 左孩子
  7. BSTNode *rchild; // 右孩子
  8. BSTNode *parent; // 父節點
  9. friend class BSTree;
  10. public:
  11. BSTNode(double k = 0.0, BSTNode *l = NULL, BSTNode *r = NULL, BSTNode *p = NULL) :key(k), lchild(l), rchild(r), parent(p){}
  12. };


2、二叉搜索樹的基本操作

       對於一棵二叉搜索樹來說,它支持許多動態集合操作,包括WALK(遍歷)、SEARCH(查找)、MINIMUM(最小關鍵字)、MAXIMUM(最大關鍵字)、SUCCESSOR(後繼)、PREDECESSOR(前驅)、INSERT(插入)、DELETE(刪除)等。下面將依次講解這些操作的具體過程及實現。


2.1 WALK(遍歷)

2.1.1 中序遍歷/INORDER-TREE-WALK

       二叉搜索樹的性質允許我們通過一個簡單的遞歸算法來按序輸出二叉搜索樹中的所有關鍵字,這種算法稱爲中序遍歷(inorder tree walk)算法。對於中序遍歷來說,輸出的子樹根的關鍵字位於其左子樹的關鍵字值和右子樹的關鍵字值之間。其僞代碼如下:


       根據上述僞代碼,我們可以很容易地寫出中序遍歷二叉搜索樹的實現。
  1. // 中序遍歷
  2. void inOrder_Tree_Walk(BSTNode *x)
  3. {
  4. if (x != NULL)
  5. {
  6. inOrder_Tree_Walk(x->lchild);
  7. cout << x->key << " ";
  8. inOrder_Tree_Walk(x->rchild);
  9. }
  10. }

       得益於二叉搜索樹的性質,當使用中序遍歷來訪問一棵二叉搜索樹上的所有結點時,最後得到的訪問序列恰好是所有結點關鍵字的升序序列。

 

2.1.2 先序遍歷/PREORDER-TREE-WALK

       跟中序遍歷類似,先序遍歷(preorder tree walk)算法也是通過遞歸來實現的。區別在於先序遍歷輸出的子樹根的關鍵字在其左右子樹的關鍵字值之前。我們同樣可以寫出先序遍歷的僞代碼:


       同樣,根據上述僞代碼,我們可以很容易地寫出先序遍歷二叉搜索樹的實現。

  1. // 先序遍歷
  2. void preOrder_Tree_Walk(BSTNode *x)
  3. {
  4. if (x != NULL)
  5. {
  6. cout << x->key << " ";
  7. preOrder_Tree_Walk(x->lchild);
  8. preOrder_Tree_Walk(x->rchild);
  9. }
  10. }


2.1.3 後序遍歷/POSTORDER-TREE-WALK

       跟中序遍歷和先序遍歷類似,後序遍歷(postorder tree walk)算法也是通過遞歸來實現的。區別在於後序遍歷輸出的子樹根的關鍵字在其左右子樹的關鍵字值之後。同樣地,我們可以寫出後序遍歷的僞代碼:


       根據上述僞代碼,我們可以很容易地寫出後序遍歷二叉搜索樹的實現。

  1. // 後序遍歷
  2. void postOrder_Tree_Walk(BSTNode *x)
  3. {
  4. if (x != NULL)
  5. {
  6. postOrder_Tree_Walk(x->lchild);
  7. postOrder_Tree_Walk(x->rchild);
  8. cout << x->key << " ";
  9. }
  10. }


2.1.4 遍歷的時間複雜度

       遍歷一棵有n個結點的二叉搜索樹需要耗費θ(n)(文末附註2)的時間,因爲初次調用之後,對於樹中的每個節點這個過程恰好要自己調用兩次:一次是它的左孩子,一次是它的右孩子。

       根據《算法導論》(中文第3版)定理12.1:

       如果x是一棵具有n個結點子樹的根,那麼調用INORDER-TREE-WALK(x)需要θ(n)的時間。

       類似地,PREORDER-TREE-WALK(x)和POSTORDER-TREE-WALK(x)也只需要θ(n)的時間。


2.1.5 遍歷訪問序列舉例

       基於上述中序遍歷、先序遍歷、後序遍歷的算法,對於下圖典型的二叉搜索樹來說,其三種遍歷所得到的關鍵字訪問序列分別爲:


       ① 中序遍歷:2、5、5(葉子)、6、7、8

       ② 先序遍歷:6、5、2、5(葉子)、7、8

       ③ 後序遍歷:2、5(葉子)、5、8、7、6

       這裏也驗證了我們前面提到的一句話:使用中序遍歷來訪問一棵二叉搜索樹上的所有結點時,最後得到的訪問序列恰好是所有結點關鍵字的升序序列。


2.2 查找(SEARCH)

2.2.1 查找過程

       在二叉搜索樹中查找一個具有給定關鍵字key的結點,需要輸入一個指向樹根的指針x和一個關鍵字k,如果這個結點存在,則TREE-SEARCH返回一個指向關鍵字爲k的結點的指針;否則返回NIL。

       具體查找過程爲:

       ① 從樹根開始查找,並沿着這棵樹中的一條簡單路徑向下進行;

       ② 若樹爲空樹,則查找失敗,返回NIL;

       ③ 對於遇到的每個結點x,若關鍵字k等於結點x的關鍵字,查找終止,返回指向結點x的指針;

       ④ 若關鍵字k小於結點x的關鍵字,則查找在x的左子樹中繼續(根據二叉搜索樹的性質,k此時不可能在右子樹中);

       ⑤ 對稱地,若關鍵字k大於結點x的關鍵字,則查找在x的右子樹中繼續(k此時不可能在左子樹中);

         ⑥ 若查找至葉子結點後仍未匹配到相等的關鍵字,則關鍵字爲k的結點不存在,返回NIL。

 

2.2.2 遞歸查找/TREE-SEARCH

       基於上述具體過程,可以寫出二叉搜索樹遞歸查找(TREE-SEARCH)的僞代碼:


       其相應的C++實現爲:

  1. /**
  2. * 查找(遞歸實現)
  3. * 輸入:一個指向根節點的指針x,和待查找的關鍵字k
  4. * 輸出:指向關鍵字爲k的節點的指針(若存在,否則輸出NIL)
  5. */
  6. BSTNode* tree_Search(BSTNode *x, double k)
  7. {
  8. if (x == NULL || k == x->key) // 如果找不着就返回NIL,找到了則返回對應節點的指針
  9. return x;
  10. if (k < x->key) // 關鍵字小於當前節點的關鍵字,查找就在左子樹中繼續
  11. return tree_Search(x->lchild, k);
  12. else // 關鍵字大於當前節點的關鍵字,查找就在右子樹中繼續
  13. return tree_Search(x->rchild, k);
  14. }


2.2.3 迭代實現/ITERATIVE-TREE-SEARCH

       我們知道,遞歸調用和退出時需要頻繁的壓棧和出棧操作。若問題規模比較大,則會在遞歸調用時耗費不少時間。因此,爲了進一步解決這個問題,可以考慮使用非遞歸的結構,節省遞歸導致的額外開銷,從而提升算法的時間效率。

       我們可以採用while循環來展開遞歸,用一種迭代方式重寫上述過程。對於大多數計算機,迭代版本的效率要高得多。

       二叉搜索樹的迭代查找(ITERATIVE-TREE-SEARCH)的僞代碼爲:


       相應的C++實現爲:

  1. /**
  2. * 查找(迭代實現)
  3. * 輸入:一個指向根節點的指針x,和待查找的關鍵字k
  4. * 輸出:指向關鍵字爲k的節點的指針(若存在,否則輸出NIL)
  5. */
  6. BSTNode* iterative_Tree_Search(BSTNode *x, double k)
  7. {
  8. while (x != NULL && k != x->key)
  9. {
  10. if (k < x->key) // 關鍵字小於當前節點的關鍵字,查找就在左子樹中繼續
  11. x = x->lchild;
  12. else // 關鍵字大於當前節點的關鍵字,查找就在右子樹中繼續
  13. x = x->rchild;
  14. }
  15. return x; // 如果找不着就返回NIL,找到了則返回對應節點的指針
  16. }


2.2.4 查找的時間複雜度

       對於二叉搜索樹的查找,從樹根開始遞歸/迭代期間遇到的結點形成了一條向下的簡單路徑。在最壞情況下,關鍵字k從樹根開始直至葉子結點都沒有匹配或者在葉子結點才匹配到,這樣比較的結點個數等於樹的高度h。

       因此,二叉搜索樹的查找TREE-SEARCH/ITERATIVE-TREE-SEARCH的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。

 

2.2.5 查找舉例

       對於以下的二叉搜索樹,查找關鍵字爲13的結點。

       查找首先從根結點開始,13<15,查找左子樹;13>6,查找右子樹;13>7,繼續查找右子樹;13==13,關鍵字相等,查找結束。成功找到了關鍵字爲13的結點,並返回指向它的指針。並且,查找過程中遇到的結點形成了一條向下的簡單路徑。



2.3 最小關鍵字(MINIMUM)

2.3.1 最小關鍵字查詢過程

       根據二叉搜索樹的性質,對於非葉子結點來說,其左子樹的關鍵字總是不大於該結點的關鍵字。從一棵子樹的樹根開始,沿着lchild指針直到遇到一個NIL,我們總能在一棵二叉搜索樹中找到一個指針,這個指針指向該子樹中的最小元素。

       二叉搜索樹中查詢最小關鍵字(MINIMUM)的僞代碼如下:


       根據上述僞代碼,可以直接寫出其C++實現。

  1. // 查找以節點x爲根的子樹的最小關鍵字並返回其節點指針
  2. BSTNode* tree_Minimum(BSTNode *x)
  3. {
  4. while (x->lchild != NULL)
  5. {
  6. x = x->lchild; // 從樹根x沿着lchild指針一直往下找,直到遇到一個NIL
  7. }
  8. return x;
  9. }

         二叉搜索樹性質保證了TREE-MINIMUM是正確的。如果結點x沒有左子樹,那麼由於x右子樹中的每個關鍵字都至少大於或等於x.key,則以x爲根的子樹中的最小關鍵字是x.key。如果結點x有左子樹,那麼由於其右子樹中沒有關鍵字小於x.key,且在左子樹中的每個關鍵字不大於x.key,則以x爲根的子樹中的最小關鍵字一定在以x.lchild爲根的子樹中。

 

2.3.2 最小關鍵字的時間複雜度

       在一棵二叉搜索樹上查詢最小關鍵字時,從樹根開始,沿着lchild指針直到遇到一個NIL,途中遇到的結點形成了一條向下的簡單路徑。最小關鍵字在二叉搜索樹最左結點找到(不一定是最左邊的葉子結點)。

       因此,查詢二叉搜索樹的最小關鍵字的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。


2.3.3 最小關鍵字舉例

       如下圖,查詢一棵二叉搜索樹的最小關鍵字。

       首先從樹根結點15開始,沿着lchild指針一路向下,路徑爲15->6->3->2。因此,該二叉搜索樹的最小關鍵字爲2。


2.4 最大關鍵字(MAXIMUM)

2.4.1 最大關鍵字查詢過程

       最大關鍵字的查詢過程與最小關鍵字的十分類似。根據二叉搜索樹的性質,對於非葉子結點來說,其右子樹的關鍵字總是不小於該結點的關鍵字。從一棵子樹的樹根開始,沿着rchild指針直到遇到一個NIL,我們總能在一棵二叉搜索樹中找到一個指針,這個指針指向該子樹中的最大元素。

       二叉搜索樹中查詢最大關鍵字(MAXIMUM)的僞代碼如下:


       根據上述僞代碼,可以直接寫出其C++實現。

  1. // 查找以節點x爲根的子樹的最大關鍵字並返回其節點指針
  2. BSTNode* tree_Maximum(BSTNode *x)
  3. {
  4. while (x->rchild != NULL)
  5. {
  6. x = x->rchild; // 從樹根x沿着rchild指針一直往下找,直到遇到一個NIL
  7. }
  8. return x;
  9. }

       二叉搜索樹性質保證了TREE-MAXIMUM是正確的。如果結點x沒有右子樹,那麼由於x左子樹中的每個關鍵字都至多小於或等於x.key,則以x爲根的子樹中的最大關鍵字是x.key。如果結點x有右子樹,那麼由於其左子樹中沒有關鍵字大於x.key,且在右子樹中的每個關鍵字不小於x.key,則以x爲根的子樹中的最大關鍵字一定在以x.rchild爲根的子樹中。

 

2.4.2 最大關鍵字的時間複雜度

       在一棵二叉搜索樹上查詢最小關鍵字時,從樹根開始,沿着rchild指針直到遇到一個NIL,途中遇到的結點形成了一條向下的簡單路徑。最大關鍵字在二叉搜索樹最右結點找到(不一定是最右邊的葉子結點)。

       因此,查詢二叉搜索樹的最大關鍵字的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。


2.4.3 最大關鍵字舉例

       如下圖,查詢一棵二叉搜索樹的最大關鍵字。

       首先從樹根結點15開始,沿着rchild指針一路向下,路徑爲15->18->20。因此,該二叉搜索樹的最大關鍵字爲20。


2.5 SUCCESSOR(後繼)

2.5.1 查詢某個結點的後繼的過程

       給定一棵二叉搜索樹中的一個結點,有時候需要按中序遍歷的次序查找它的後繼。一棵二叉搜索樹的結構允許我們通過沒有任何關鍵字的比較來確定一個結點的後繼。如果後繼存在,則返回指向結點x的後繼的指針。倘若結點x的關鍵字是這棵樹的最大關鍵字,則說明它沒有後繼了,返回NIL。

       求後繼的過程可以分以下兩種情況討論:

① 如果結點x的右子樹非空,那麼x的後繼恰是x右子樹中的最左結點(右子樹中的最小關鍵字);

② 如果結點x的右子樹爲空,則有以下兩種可能:

     a. 結點x是其父結點的左孩子,則結點x的後繼結點爲它的父結點;

     b. 結點x是其父結點的右孩子,則結點x的後繼結點爲x的最底層祖先,同時滿足“這個最底層祖先的左孩子也是結點x的祖先”的條件。(可以結合2.5.3的例子進行理解)

       在二叉搜索樹中查詢某個結點後繼(SUCCESSOR)的僞代碼如下:


       根據上述僞代碼,我們可以寫出它的C++實現。

  1. // 查找節點x的後繼節點並返回
  2. BSTNode* tree_Successor(BSTNode *x)
  3. {
  4. BSTNode *y = NULL;
  5. if (x->rchild != NULL) // 若節點x的右孩子不爲空,則x的後繼節點就是其右子樹中的最小關鍵字節點
  6. return tree_Minimum(x->rchild);
  7. // 若節點x的右孩子爲空,則有以下兩種情況
  8. // a.結點x是其父結點的左孩子,則結點x的後繼結點爲它的父結點;
  9. // b.結點x是其父結點的右孩子,則結點x的後繼結點爲x的最底層祖先,同時滿足“這個最底層祖先的左孩子也是結點x的祖先”的條件。
  10. y = x->parent; // 若節點x的右孩子爲空,先令y爲x的父節點
  11. while (y != NULL && x == y->rchild) // 從x開始沿樹而上直到遇到這樣一個節點x,這個節點x是它的雙親y的左孩子,此時雙親即爲後繼節點
  12. {
  13. x = y;
  14. y = y->parent;
  15. }
  16. return y;
  17. }


2.5.2 查詢結點後繼的時間複雜度

       當給定一棵二叉搜索樹的某個結點x,要求該結點的後繼結點時,這個過程或者遵從一條簡單路徑沿樹向下(結點x的右子樹非空時的情況,只需找右子樹中的最小關鍵字即可);或者遵從簡單路徑沿樹向上(結點x的右子樹爲空時的情況,只需沿樹向上查找符合條件的最底層祖先即可)。

       因此,不管是哪種情況,這個過程最壞情況下都是一條從根到葉子結點或者從葉子結點到根的完整路徑。所以,當給定一棵二叉搜索樹的某個結點,求該結點的後繼結點的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。

 

2.5.3 查詢結點後繼舉例

       如下圖,爲一棵二叉搜索樹。前面我們說過,求後繼的過程可以分爲以下兩種情況:

① 如果結點x的右子樹非空,那麼x的後繼恰是x右子樹中的最左結點(右子樹中的最小關鍵字);

② 如果結點x的右子樹爲空,則有以下兩種可能:

    a. 結點x是其父結點的左孩子,則結點x的後繼結點爲它的父結點;

    b. 結點x是其父結點的右孩子,則結點x的後繼結點爲x的最底層祖先,同時滿足“這個最底層祖先的左孩子也是結點x的祖先”的條件。


       對於情況①,假設我們要求結點15的後繼。因爲結點15的右子樹非空,所以15的後繼就是它的右子樹中的最小關鍵字,即爲17。

       對於情況②a,假設我們要求結點9的後繼。因爲結點9的右子樹爲空,且結點9是其父結點13的左孩子,所以9的後繼就是它的父結點,即爲13。

       對於情況②b,假設我們要求結點13的後繼。因爲結點13的右子樹爲空,且結點13是其父結點7的右孩子。此時我們要找的後繼是13的最底層祖先,而且這個最底層祖先的左孩子也是結點13的祖先之一。

       我們可以這樣看,首先結點13的祖先有7、6、15,;按理說最底層結點應該是7。但是我們需要注意,這個最底層是有前提條件的。前提條件就是這個祖先要有左孩子,並且這個左孩子也是結點13的祖先之一。

       在祖先7、6、15中,有左孩子的有6和15,但是結點6的左孩子3並不是結點13的祖先之一;結點15的左孩子6正是結點13的另一個祖先。因此,符合前提條件的最底層祖先爲15。所以13的後繼就是15。


2.6 PREDECESSOR(前驅)

2.6.1 查詢某個結點的前驅的過程

       給定一棵二叉搜索樹中的一個結點查找它的前驅的情況跟求後繼的情況是對稱的。一棵二叉搜索樹的結構也允許我們通過沒有任何關鍵字的比較來確定一個結點的前驅。如果前驅存在,則返回指向結點x的前驅的指針。倘若結點x的關鍵字是這棵樹的最小關鍵字,則說明它沒有前驅了,返回NIL。

       求前驅的過程同樣可以分以下兩種情況討論:

① 如果結點x的左子樹非空,那麼x的前驅恰是x左子樹中的最右結點(左子樹中的最大關鍵字);

② 如果結點x的左子樹爲空,則有以下兩種可能:

    a. 結點x是其父結點的右孩子,則結點x的前驅結點爲它的父結點;

    b. 結點x是其父結點的左孩子,則結點x的前驅結點爲x的最底層祖先,同時滿足“這個最底層祖先的右孩子也是結點x的祖先”的條件。(可以結合2.6.3的例子進行理解)

     在二叉搜索樹中查詢某個結點前驅(PREDECESSOR)的僞代碼如下:


       根據上述僞代碼,我們可以寫出它的C++實現。

  1. // 查找節點x的前驅節點並返回
  2. BSTNode* tree_Predecessor(BSTNode *x)
  3. {
  4. BSTNode *y = NULL;
  5. if (x->lchild != NULL) // 若節點的左孩子不爲空,則x的前驅節點就是其左子樹中的最大關鍵字節點
  6. return tree_Maximum(x->lchild);
  7. // 若節點x的左孩子爲空,則有以下兩種情況
  8. // a.結點x是其父結點的右孩子,則結點x的前驅結點爲它的父結點;
  9. // b.結點x是其父結點的左孩子,則結點x的前驅結點爲x的最底層祖先,同時滿足“這個最底層祖先的右孩子也是結點x的祖先”的條件。
  10. y = x->parent; // 若節點的左孩子爲空,先令y爲x的父節點
  11. while (y != NULL && x == y->lchild) // 從x開始沿樹而上直到遇到這樣一個節點x,這個節點x是它的雙親y的右孩子,此時雙親即爲前驅節點
  12. {
  13. x = y;
  14. y = y->parent;
  15. }
  16. return y;
  17. }


2.6.2 查詢結點前驅的時間複雜度

       當給定一棵二叉搜索樹的某個結點x,要求該結點的前驅結點時,這個過程或者遵從一條簡單路徑沿樹向下(結點x的左子樹非空時的情況,只需找左子樹中的最大關鍵字即可);或者遵從簡單路徑沿樹向上(結點x的左子樹爲空時的情況,只需沿樹向上查找符合條件的最底層祖先即可)。

       因此,不管是哪種情況,這個過程最壞情況下都是一條從根到葉子結點或者從葉子結點到根的完整路徑。所以,當給定一棵二叉搜索樹的某個結點,求該結點的前驅結點的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。

 

2.6.3 查詢結點前驅舉例

      如下圖,爲一棵二叉搜索樹。前面我們說過,求前驅的過程可以分爲以下兩種情況:

① 如果結點x的左子樹非空,那麼x的前驅恰是x左子樹中的最右結點(左子樹中的最大關鍵字);

② 如果結點x的左子樹爲空,則有以下兩種可能:

    a. 結點x是其父結點的右孩子,則結點x的前驅結點爲它的父結點;

    b. 結點x是其父結點的左孩子,則結點x的前驅結點爲x的最底層祖先,同時滿足“這個最底層祖先的右孩子也是結點x的祖先”的條件。


       對於情況①,假設我們要求結點15的前驅。因爲結點15的左子樹非空,所以15的前驅就是它的左子樹中的最大關鍵字,即爲13。

       對於情況②a,假設我們要求結點7的前驅。因爲結點7的左子樹爲空,且結點7是其父結點6的右孩子,所以7的前驅就是它的父結點,即爲6。

       對於情況②b,假設我們要求結點17的前驅。因爲結點17的左子樹爲空,且結點17是其父結點18的左孩子。此時我們要找的前驅是17的最底層祖先,而且這個最底層祖先的右孩子也是結點17的祖先之一。

       我們可以這樣看,首先結點17的祖先有18、15,;按理說最底層結點應該是18。但是我們需要注意,這個最底層是有前提條件的。前提條件就是這個祖先要有右孩子,並且這個右孩子也是結點17的祖先之一。

       在祖先18、15中,兩個結點都有右孩子,但是結點18的右孩子20並不是結點17的祖先之一;結點15的右孩子18正是結點17的另一個祖先。因此,符合前提條件的最底層祖先爲15。所以17的前驅就是15。


       到這裏,我們可以結合最小關鍵字、最大關鍵字、後繼、前驅的查找過程來證明一下《算法導論》(中文第3版)課後題12.2-5:如果有一棵二叉搜索樹中的一個結點有兩個孩子,那麼它的後繼沒有左孩子,它的前驅沒有右孩子。

證明:

       倘若一棵二叉搜索樹的結點有兩個孩子。則:

       當求該結點x的後繼時,因爲它的右孩子非空,符合求後繼的情況①,結點x的後繼y1就是它右子樹中的最小關鍵字。假設該最小關鍵字y1有左孩子z1,則根據二叉搜索樹的性質,z1.key≤y1.key,這與y1是最小關鍵字相矛盾。所以後繼y1沒有左孩子。

       另外,當求該結點x的前驅時,因爲它的左孩子非空,符合求前驅的情況①,結點x的前驅y2就是它的左子樹中的最大關鍵字。假設該最大關鍵字y2有右孩子z2,則根據二叉搜索樹的性質,z2.key≥y2.key,這與y2是最大關鍵字相矛盾。所以前驅y2沒有右孩子。

       因此,如果有一棵二叉搜索樹中的一個結點有兩個孩子,那麼它的後繼沒有左孩子,它的前驅沒有右孩子。

       證畢。


2.7 INSERT(插入)

2.7.1 插入過程

       插入操作會引起由二叉搜索樹表示的動態集合的變化。我們需要修改數據結構來反映這個變化,但要保證修改後二叉搜索樹的性質不被破壞。

       要將一個新值v插入到一棵二叉搜索樹T中,需要新建一個結點z,並且初始化z.key = v,z.lchild= NIL,z.rchild = NIL。現在問題轉化爲將結點z插入到二叉搜索樹T中的合適位置,並且保持二叉搜索樹的性質不變。

       插入的過程首先從樹根開始遍歷,沿樹向下移動。指針x記錄了一條向下的簡單路徑,並查找要替換的輸入項z的NIL。同時,保持遍歷指針y指向x的雙親。兩個指針沿樹向下移動時,通過比較當前結點x的關鍵字與待插入結點z的關鍵字大小,來決定向左或向右移動。直到x指向NIL時,這個NIL佔據的位置就是輸入項z要放置的位置。前面我們提到在x移動過程中還需要保持y指向x的父結點,原因是當我們找到可插入的NIL位置時,我們需要知道z屬於哪個結點。

       在二叉搜索樹中插入(INSERT)新結點的相應僞代碼如下:

       根據上述僞代碼,其相應的C++實現爲:

  1. // 將節點z插入到以T爲根節點的二叉搜索樹中
  2. void tree_Insert(BSTNode *&T, BSTNode *z)
  3. {
  4. BSTNode *x = T;
  5. BSTNode *y = NULL;
  6. while (x != NULL) // 使得指針沿樹向下移動,向左或向右移動取決於z->key和x->key的比較
  7. {
  8. y = x;
  9. if (z->key < x->key) // 關鍵字小於當前節點的關鍵字,向左子樹移動
  10. x = x->lchild;
  11. else // 關鍵字大於或等於當前節點的關鍵字,向右子樹移動
  12. x = x->rchild;
  13. }
  14. z->parent = y; // 節點z的父節點指針指向遍歷到的節點y
  15. if (y == NULL) // 若y爲NIL,說明原樹爲空
  16. T = z; // 將節點z作爲根節點插入
  17. else if (z->key < y->key) // 若y不爲NIL,且z的關鍵字小於y的關鍵字
  18. y->lchild = z; // 將y的左孩子指針指向節點z
  19. else // 若y不爲NIL,且z的關鍵字大於等於y的關鍵字
  20. y->rchild = z; // 將y的右孩子指針指向節點z
  21. }

       值得注意的是,在插入新結點後,新結點總是作爲一個新葉子結點而存在的。這是二叉搜索樹的另一個重要性質。

 

2.7.2 插入的時間複雜度

       在一棵二叉搜索樹中插入新結點,跟TREE-SEARCH、MINIMUM等操作類似,都是從樹根開始,遇到的結點形成了一條向下的簡單路徑。在插入過程中,需要從樹根開始向下移動,一直比較到葉子結點。

       所以,當給定一棵二叉搜索樹,向其中插入新結點的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。

 

2.7.3 插入新結點舉例

       如下圖,是一棵二叉搜索樹,現有新結點13要插入到這棵樹中。

       首先從樹根開始,關鍵字13>12,因此指針向右子樹移動。而13<18,故指針向左子樹移動。13<15,指針繼續向左子樹移動。但因爲此刻指針指向的是NIL了,因此這個位置就是新結點13所要插入的位置。修改相應的左右孩子指針及父結點指針,同時父結點也修改相應的孩子指針即可。整條搜索至完成插入的路徑爲12->18->15->13。


2.8 DELETE(刪除)

2.8.1 刪除過程

       跟插入操作一樣,刪除操作也會引起二叉搜索樹表示的動態集合的變化。我們也需要修改數據結構來反映這個變化。但是仍然需要保證刪除後二叉搜索樹的性質不變。但是,相對於插入操作,刪除操作會更加複雜一些。

       從一棵二叉搜索樹中刪除某個特定結點z可以分爲以下三種情況,其中前兩種情況較爲簡單,最後一種情況則複雜一點。

① 如果z沒有孩子結點,那麼只是簡單地將它刪除,並修改它的父結點,用NIL作爲孩子來替換z;

② 如果z只有一個孩子,那麼將這個孩子提升到樹中z的位置上,並修改z的父結點,用z的孩子來替換z;

     z只有一個孩子,且爲右孩子,用z的右子樹替換z:


     z只有一個孩子,且爲左孩子,用z的左子樹替換z:


如果z有兩個孩子,那麼找z的後繼y,並讓y佔據樹中z的位置。z的原來右子樹部分成爲y的新的右子樹,z的原來左子樹部分成爲y新的左子樹。這裏要注意,z的後繼y一定在z的右子樹中,並且沒有左孩子(詳情見上文2.7前的證明)。利用z的後繼y替換z,又細分爲以下兩種情況:

   a. 如果y是z的右孩子,那麼直接用y替換z,並保留y的右子樹(y沒有左子樹);


   b. 如果y不是z的右孩子,那麼先用y的右孩子替換y(y沒有左孩子),然後再用y替換z。


       刪除操作也需要從樹根開始,搜索待刪除的關鍵字的結點是否在樹中。若不存在,則刪除失敗;若存在則根據上述的三種情況執行刪除操作。

       爲了在二叉搜索樹中移動子樹,我們首先定義一個子過程TRANSPLANT,它是用一棵以v爲根的子樹來替換一棵以u爲根的子樹,將結點u的父結點變爲結點v的父結點,同時修改原u的父結點的孩子指針,使其指向v。簡單來說,這個子過程的主要工作就是修改v的父結點指針,同時修改原u父結點的孩子指針,這樣以v爲根的子樹就替換上去了。

       該TRANSPLANT的僞代碼如下:

       結合上面的TRANSPLANT子過程,我們可以進一步寫出從二叉搜索樹中刪除(DELETE)結點的僞代碼如下:


       根據上述僞代碼,可以寫出子過程TRANSPLANT以及DELETE操作的C++實現爲:

  1. // 用一棵以v爲根的子樹來替換一棵以u爲根的子樹,節點u的雙親變成節點v的雙親,並且v成爲u的雙親的相應孩子
  2. void transplant(BSTNode *&T, BSTNode *u, BSTNode *v)
  3. {
  4. if (u->parent == NULL) // 節點u爲根節點時
  5. T = v; // 節點v直接替換u作爲根節點
  6. else if (u == u->parent->lchild) // 節點u是其父節點的左孩子
  7. u->parent->lchild = v; // 父節點的左孩子指向節點v
  8. else // 節點u是其父節點的右孩子
  9. u->parent->rchild = v; // 父節點的右孩子指向節點v
  10. if (v != NULL)
  11. v->parent = u->parent; // 更新節點v的父節點指針
  12. }
  1. // 將節點z從以T爲根節點的二叉搜索樹中刪除
  2. BSTNode* tree_Delete(BSTNode *&T, BSTNode *z)
  3. {
  4. BSTNode *y = NULL;
  5. if (z->lchild == NULL) // 若z的左孩子爲NIL,直接用z的右孩子替換z
  6. transplant(T, z, z->rchild);
  7. else if (z->rchild == NULL) // 若z的右孩子爲NIL,直接用z的左孩子替換z
  8. transplant(T, z, z->lchild);
  9. else // 若z有兩個孩子
  10. {
  11. // 先讓y爲z的後繼,也即z的右子樹中的最小關鍵字
  12. // y肯定沒有左孩子,否則最小關鍵字就是那個左孩子而不是y了
  13. y = tree_Minimum(z->rchild);
  14. if (y->parent != z) // 若y不是z的右孩子
  15. {
  16. transplant(T, y, y->rchild); // 先用y的右孩子替換y
  17. y->rchild = z->rchild; // y的右子樹指針指向原來z的右子樹指針
  18. y->rchild->parent = y; // 原來z的右子樹(現爲y的右子樹)的父節點指針更新爲y
  19. }
  20. transplant(T, z, y); // 然後再用y替換z
  21. y->lchild = z->lchild; // y的左子樹指針指向原來z的左子樹指針
  22. y->lchild->parent = y; // 原來z的左子樹(現爲y的左子樹)的父節點指針更新爲y
  23. }
  24. return z;
  25. }

2.8.2 刪除的時間複雜度

       在一棵二叉搜索樹中刪除結點,我們可以看到DELETE操作實現的每一步,都只需要耗費常數時間。子過程TRANSPLANT的每一步實現也只需要常數時間。時間效率主要取決於在二叉搜索樹中查找要刪除的結點關鍵字是否存在。因此,刪除操作的時間複雜度是由查找操作的時間複雜度來決定的。

由於查找(SEARCH)的時間複雜度爲O(h),因此,當給定一棵二叉搜索樹,刪除結點的時間複雜度爲O(h),其中h爲二叉搜索樹的高度。

 

2.8.3 刪除結點舉例

       其實這部分已在2.8.1描述刪除過程時針對不同情況均已舉出了相關例子。詳情請見2.8.1的文字描述及配圖示例。


三、完整代碼及測試實例

1、測試實例

       利用完整代碼,假如我們想構建下圖這樣一棵二叉搜索樹。由於程序中使用插入操作來完成一棵二叉搜索樹的構建,因此,我們在輸入關鍵字序列時要注意一點:就是每棵子樹的根結點關鍵字要先於它的孩子結點的關鍵字輸入。

       舉個例子來說,下圖的二叉搜索樹,輸入序列爲:15 6 18 3 7 17 20 2 4 13 9 或者 15 6 18 3 2 47 13 9 17 20 等都是可以的,因爲這些序列都確保了子樹的根結點關鍵字先於其孩子結點關鍵字輸入。但是,類似於:15 6 2 4 3 7 13 9 18 17 20 的序列則是不合法的,無法構建出下圖的二叉搜索樹,原因是子樹根3後於其孩子結點2 4輸入了。


       接下來我們開始進行測試,首先輸入結點/關鍵字個數。就上圖而言,結點/關鍵字個數爲11個。接下來按照合法序列輸入關鍵字序列:15 6 18 3 7 17 20 2 413 9。


      可以看到當構建完二叉搜索樹後,輸出了先序、中序、後序遍歷的訪問序列,我們可以自行根據上圖的二叉搜索樹進行驗證。同時,因爲先序+中序(或者中序+後序)可以唯一確定一棵二叉樹,所以此處可以驗證我們構建的二叉搜索樹是否就是上圖的二叉搜索樹。

      接下來我們查找一下結點。比如我們查找結點13,由於上圖樹中存在結點13,因此查找成功。當我們查找不存在的結點時,比如結點14,就會查找失敗。


       最後我們來測試一下刪除的操作。刪除根節點15,因爲結點15有兩個孩子結點,並且其後繼17不是它的右孩子,屬於刪除情況中最複雜的一種情況。

       在這種情況下,刪除操作應該首先用後繼17的右孩子替換17。此例中右孩子爲NIL,也就是用NIL替換17。然後用後繼17來替換結點15,原15的左右子樹變爲17的左右子樹了。

       刪除操作完成後,我們可以繼續輸出先序+中序(或者中序+後序)的訪問序列。因爲先序+中序(或者中序+後序)可以唯一確定一棵二叉樹,所以此處可以驗證刪除結點15後的二叉搜索樹是否跟我們預想的一致。


       刪除根結點15後的新二叉搜索樹如下:


       

       至此,簡單的測試實例已經測試完畢。下面將貼出完整代碼。


2、完整代碼如下:

  1. #include <iostream>
  2. using namespace std;
  3. /* 二叉搜索樹節點 */
  4. class BSTNode
  5. {
  6. private:
  7. double key; // 關鍵字
  8. BSTNode *lchild; // 左孩子
  9. BSTNode *rchild; // 右孩子
  10. BSTNode *parent; // 父節點
  11. friend class BSTree;
  12. public:
  13. BSTNode(double k = 0.0, BSTNode *l = NULL, BSTNode *r = NULL, BSTNode *p = NULL) :key(k), lchild(l), rchild(r), parent(p){}
  14. };
  15. /* 二叉搜索樹 */
  16. class BSTree
  17. {
  18. private:
  19. BSTNode *root; // 根節點
  20. /* 以下是內部接口 */
  21. // 先序遍歷
  22. void preOrder_Tree_Walk(BSTNode *x)
  23. {
  24. if (x != NULL)
  25. {
  26. cout << x->key << " ";
  27. preOrder_Tree_Walk(x->lchild);
  28. preOrder_Tree_Walk(x->rchild);
  29. }
  30. }
  31. // 中序遍歷
  32. void inOrder_Tree_Walk(BSTNode *x)
  33. {
  34. if (x != NULL)
  35. {
  36. inOrder_Tree_Walk(x->lchild);
  37. cout << x->key << " ";
  38. inOrder_Tree_Walk(x->rchild);
  39. }
  40. }
  41. // 後序遍歷
  42. void postOrder_Tree_Walk(BSTNode *x)
  43. {
  44. if (x != NULL)
  45. {
  46. postOrder_Tree_Walk(x->lchild);
  47. postOrder_Tree_Walk(x->rchild);
  48. cout << x->key << " ";
  49. }
  50. }
  51. /**
  52. * 查找(遞歸實現)
  53. * 輸入:一個指向根節點的指針x,和待查找的關鍵字k
  54. * 輸出:指向關鍵字爲k的節點的指針(若存在,否則輸出NIL)
  55. */
  56. BSTNode* tree_Search(BSTNode *x, double k)
  57. {
  58. if (x == NULL || k == x->key) // 如果找不着就返回NIL,找到了則返回對應節點的指針
  59. return x;
  60. if (k < x->key) // 關鍵字小於當前節點的關鍵字,查找就在左子樹中繼續
  61. return tree_Search(x->lchild, k);
  62. else // 關鍵字大於當前節點的關鍵字,查找就在右子樹中繼續
  63. return tree_Search(x->rchild, k);
  64. }
  65. /**
  66. * 查找(迭代實現)
  67. * 輸入:一個指向根節點的指針x,和待查找的關鍵字k
  68. * 輸出:指向關鍵字爲k的節點的指針(若存在,否則輸出NIL)
  69. */
  70. BSTNode* iterative_Tree_Search(BSTNode *x, double k)
  71. {
  72. while (x != NULL && k != x->key)
  73. {
  74. if (k < x->key) // 關鍵字小於當前節點的關鍵字,查找就在左子樹中繼續
  75. x = x->lchild;
  76. else // 關鍵字大於當前節點的關鍵字,查找就在右子樹中繼續
  77. x = x->rchild;
  78. }
  79. return x; // 如果找不着就返回NIL,找到了則返回對應節點的指針
  80. }
  81. // 查找以節點x爲根的子樹的最小關鍵字並返回其節點指針
  82. BSTNode* tree_Minimum(BSTNode *x)
  83. {
  84. while (x->lchild != NULL)
  85. {
  86. x = x->lchild; // 從樹根x沿着lchild指針一直往下找,直到遇到一個NIL
  87. }
  88. return x;
  89. }
  90. // 查找以節點x爲根的子樹的最大關鍵字並返回其節點指針
  91. BSTNode* tree_Maximum(BSTNode *x)
  92. {
  93. while (x->rchild != NULL)
  94. {
  95. x = x->rchild; // 從樹根x沿着rchild指針一直往下找,直到遇到一個NIL
  96. }
  97. return x;
  98. }
  99. // 查找節點x的後繼節點並返回
  100. BSTNode* tree_Successor(BSTNode *x)
  101. {
  102. BSTNode *y = NULL;
  103. if (x->rchild != NULL) // 若節點x的右孩子不爲空,則x的後繼節點就是其右子樹中的最小關鍵字節點
  104. return tree_Minimum(x->rchild);
  105. // 若節點x的右孩子爲空,則有以下兩種情況
  106. // a.結點x是其父結點的左孩子,則結點x的後繼結點爲它的父結點;
  107. // b.結點x是其父結點的右孩子,則結點x的後繼結點爲x的最底層祖先,同時滿足“這個最底層祖先的左孩子也是結點x的祖先”的條件。
  108. y = x->parent; // 若節點x的右孩子爲空,先令y爲x的父節點
  109. while (y != NULL && x == y->rchild) // 從x開始沿樹而上直到遇到這樣一個節點x,這個節點x是它的雙親y的左孩子,此時雙親即爲後繼節點
  110. {
  111. x = y;
  112. y = y->parent;
  113. }
  114. return y;
  115. }
  116. // 查找節點x的前驅節點並返回
  117. BSTNode* tree_Predecessor(BSTNode *x)
  118. {
  119. BSTNode *y = NULL;
  120. if (x->lchild != NULL) // 若節點的左孩子不爲空,則x的前驅節點就是其左子樹中的最大關鍵字節點
  121. return tree_Maximum(x->lchild);
  122. // 若節點x的左孩子爲空,則有以下兩種情況
  123. // a.結點x是其父結點的右孩子,則結點x的前驅結點爲它的父結點;
  124. // b.結點x是其父結點的左孩子,則結點x的前驅結點爲x的最底層祖先,同時滿足“這個最底層祖先的右孩子也是結點x的祖先”的條件。
  125. y = x->parent; // 若節點的左孩子爲空,先令y爲x的父節點
  126. while (y != NULL && x == y->lchild) // 從x開始沿樹而上直到遇到這樣一個節點x,這個節點x是它的雙親y的右孩子,此時雙親即爲前驅節點
  127. {
  128. x = y;
  129. y = y->parent;
  130. }
  131. return y;
  132. }
  133. // 將節點z插入到以T爲根節點的二叉搜索樹中
  134. void tree_Insert(BSTNode *&T, BSTNode *z)
  135. {
  136. BSTNode *x = T;
  137. BSTNode *y = NULL;
  138. while (x != NULL) // 使得指針沿樹向下移動,向左或向右移動取決於z->key和x->key的比較
  139. {
  140. y = x;
  141. if (z->key < x->key) // 關鍵字小於當前節點的關鍵字,向左子樹移動
  142. x = x->lchild;
  143. else // 關鍵字大於或等於當前節點的關鍵字,向右子樹移動
  144. x = x->rchild;
  145. }
  146. z->parent = y; // 節點z的父節點指針指向遍歷到的節點y
  147. if (y == NULL) // 若y爲NIL,說明原樹爲空
  148. T = z; // 將節點z作爲根節點插入
  149. else if (z->key < y->key) // 若y不爲NIL,且z的關鍵字小於y的關鍵字
  150. y->lchild = z; // 將y的左孩子指針指向節點z
  151. else // 若y不爲NIL,且z的關鍵字大於等於y的關鍵字
  152. y->rchild = z; // 將y的右孩子指針指向節點z
  153. }
  154. // 用一棵以v爲根的子樹來替換一棵以u爲根的子樹,節點u的雙親變成節點v的雙親,並且v成爲u的雙親的相應孩子
  155. void transplant(BSTNode *&T, BSTNode *u, BSTNode *v)
  156. {
  157. if (u->parent == NULL) // 節點u爲根節點時
  158. T = v; // 節點v直接替換u作爲根節點
  159. else if (u == u->parent->lchild) // 節點u是其父節點的左孩子
  160. u->parent->lchild = v; // 父節點的左孩子指向節點v
  161. else // 節點u是其父節點的右孩子
  162. u->parent->rchild = v; // 父節點的右孩子指向節點v
  163. if (v != NULL)
  164. v->parent = u->parent; // 更新節點v的父節點指針
  165. }
  166. // 將節點z從以T爲根節點的二叉搜索樹中刪除
  167. BSTNode* tree_Delete(BSTNode *&T, BSTNode *z)
  168. {
  169. BSTNode *y = NULL;
  170. if (z->lchild == NULL) // 若z的左孩子爲NIL,直接用z的右孩子替換z
  171. transplant(T, z, z->rchild);
  172. else if (z->rchild == NULL) // 若z的右孩子爲NIL,直接用z的左孩子替換z
  173. transplant(T, z, z->lchild);
  174. else // 若z有兩個孩子
  175. {
  176. // 先讓y爲z的後繼,也即z的右子樹中的最小關鍵字
  177. // y肯定沒有左孩子,否則最小關鍵字就是那個左孩子而不是y了
  178. y = tree_Minimum(z->rchild);
  179. if (y->parent != z) // 若y不是z的右孩子
  180. {
  181. transplant(T, y, y->rchild); // 先用y的右孩子替換y
  182. y->rchild = z->rchild; // y的右子樹指針指向原來z的右子樹指針
  183. y->rchild->parent = y; // 原來z的右子樹(現爲y的右子樹)的父節點指針更新爲y
  184. }
  185. transplant(T, z, y); // 然後再用y替換z
  186. y->lchild = z->lchild; // y的左子樹指針指向原來z的左子樹指針
  187. y->lchild->parent = y; // 原來z的左子樹(現爲y的左子樹)的父節點指針更新爲y
  188. }
  189. return z;
  190. }
  191. // 銷燬二叉搜索樹
  192. void tree_Destory(BSTNode *&T)
  193. {
  194. if (T == NULL)
  195. return;
  196. if (T->lchild != NULL)
  197. return tree_Destory(T->lchild);
  198. if (T->rchild != NULL)
  199. return tree_Destory(T->rchild);
  200. delete T;
  201. T = NULL;
  202. }
  203. public:
  204. BSTree() :root(NULL){}
  205. /* 以下是外部接口 */
  206. // 先序遍歷
  207. void PreOrder_Tree_Walk()
  208. {
  209. preOrder_Tree_Walk(root); // 傳入根節點
  210. cout << endl;
  211. }
  212. // 中序遍歷
  213. void InOrder_Tree_Walk()
  214. {
  215. inOrder_Tree_Walk(root); // 傳入根節點
  216. cout << endl;
  217. }
  218. // 後序遍歷
  219. void PostOrder_Tree_Walk()
  220. {
  221. postOrder_Tree_Walk(root); // 傳入根節點
  222. cout << endl;
  223. }
  224. // 遞歸查找
  225. BSTNode* Tree_Search(double key)
  226. {
  227. return tree_Search(root, key); // 傳入根節點和待查找的關鍵字key
  228. }
  229. // 迭代查找
  230. BSTNode* Iterative_Tree_Search(double key)
  231. {
  232. return iterative_Tree_Search(root, key); // 傳入根節點和待查找的關鍵字key
  233. }
  234. // 最小關鍵字
  235. BSTNode* Tree_Minimum(BSTNode *x)
  236. {
  237. return tree_Minimum(x); // 傳入子樹樹根,查詢子樹的最小關鍵字
  238. }
  239. // 最大關鍵字
  240. BSTNode* Tree_Maximum(BSTNode *x)
  241. {
  242. return tree_Maximum(x); // 傳入子樹樹根,查詢子樹的最大關鍵字
  243. }
  244. // 後繼
  245. BSTNode* Tree_Successor(BSTNode *x)
  246. {
  247. return tree_Successor(x); // 查詢節點x的後繼節點
  248. }
  249. // 前驅
  250. BSTNode* Tree_Predecessor(BSTNode *x)
  251. {
  252. return tree_Predecessor(x); // 查詢節點x的前驅節點
  253. }
  254. // 插入
  255. void Tree_Insert(double key)
  256. {
  257. BSTNode *z = new BSTNode(key, NULL, NULL, NULL); // 根據關鍵字生成新節點
  258. if (z == NULL)
  259. return;
  260. tree_Insert(root, z); // 傳入樹根以及待插入的結點
  261. }
  262. // 刪除
  263. void Tree_Delete(double key)
  264. {
  265. BSTNode *z, *node;
  266. z = iterative_Tree_Search(root, key); // 根據給定的關鍵字,查找樹中是否存在該關鍵字的結點
  267. if (z != NULL) // 若存在
  268. {
  269. node = tree_Delete(root, z); // 傳入樹根以及待刪除的結點
  270. if (node != NULL)
  271. delete node;
  272. }
  273. }
  274. // 銷燬二叉搜索樹
  275. void Tree_Destory()
  276. {
  277. tree_Destory(root);
  278. }
  279. ~BSTree()
  280. {
  281. Tree_Destory();
  282. }
  283. };
  284. int main()
  285. {
  286. int i, j, n;
  287. double *arr;
  288. BSTree *tree = new BSTree();
  289. cout << "請輸入結點/關鍵字個數: " << endl;
  290. cin >> n;
  291. arr = new double[n];
  292. cout << "請依次輸入關鍵字(注意每棵子樹的根節點都要比它的孩子結點先輸入): " << endl;
  293. for (i = 0; i < n; i++)
  294. {
  295. cin >> arr[i]; // 依次輸入關鍵字
  296. tree->Tree_Insert(arr[i]); // 調用插入函數,根據關鍵字生成二叉搜索樹
  297. }
  298. cout << endl;
  299. cout << "二叉搜索樹先序遍歷的結果爲: ";
  300. tree->PreOrder_Tree_Walk();
  301. cout << "二叉搜索樹中序遍歷的結果爲: ";
  302. tree->InOrder_Tree_Walk();
  303. cout << "二叉搜索樹後序遍歷的結果爲: ";
  304. tree->PostOrder_Tree_Walk();
  305. cout << endl;
  306. double seaKey;
  307. cout << "請輸入要查找的結點關鍵字: " << endl;
  308. cin >> seaKey;
  309. BSTNode *seaNode = tree->Tree_Search(seaKey);
  310. if (seaNode)
  311. cout << "查找成功" << endl;
  312. else
  313. cout << "查找失敗, 關鍵字爲" << seaKey << "的結點不存在" << endl;
  314. cout << endl;
  315. double delKey;
  316. cout << "請輸入要刪除的結點關鍵字: " << endl;
  317. cin >> delKey;
  318. tree->Tree_Delete(delKey);
  319. // 通過先序與中序遍歷,或者中序與後序遍歷可以唯一確定一棵二叉樹
  320. // 因此此處可以驗證刪除後的二叉搜索樹是否與你預想的一樣
  321. cout << "刪除操作後先序遍歷二叉搜索樹的結果爲: ";
  322. tree->PreOrder_Tree_Walk();
  323. cout << "刪除操作後中序遍歷二叉搜索樹的結果爲: ";
  324. tree->InOrder_Tree_Walk();
  325. cout << endl;
  326. tree->Tree_Destory(); // 銷燬二叉樹
  327. delete[] arr;
  328. system("pause");
  329. return 0;
  330. }


附註:

1、衛星數據:

       衛星數據是指一條記錄中除了關鍵字key以外的其他數據。因爲一個記錄可能包含多個數據項,但是一般的排序算法只關心key,其他的項都是跟着key走,像“衛星”一樣。另外,也可參考《算法導論》(中文第3版)第81頁,也即書中“第二部分”的序言中所涉及到的衛星數據的相關說法。

2、θ(n):

       θ是一個漸進符號,在表示時間複雜度或空間複雜度時可能會用到。當僅表示上界時,使用符號Ω(n);當僅表示下界時,使用符號O(n);當表示上下界都漸進於某個函數時,使用符號θ(n)。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章