1. 程式人生 > >20172308 《程式設計與資料結構》第七週學習總結

20172308 《程式設計與資料結構》第七週學習總結

教材學習內容總結

第 十一 章 二叉查詢樹

一、概述
二叉查詢樹是一種含有附加屬性的二叉樹,即其左孩子小於父結點,父結點小於或等於右孩子
(二叉查詢樹的定義是二叉樹定義的擴充套件)
二、 用連結串列實現二叉查詢樹

  • addElement操作:
    addElement方法根據給定元素的值,在樹中的恰當位置新增該元素,如果這個元素不是 comparable,則addElement方法會丟擲NoComparableElemementException;
    如果樹為空,則這個新元素就將成為根結點;
    如果樹非空,這個新元素會與樹根元素進行比較:
    如果它小於根結點中儲存的那個元素且根的左孩子為null,則這個新元素就將成為根的左孩子。
    如果這個新元素小於根結點中儲存的那個元素且根的左孩子不是null,則會遍歷根的左孩子,並再次進行比較操作;
    如果這個新元素大於或等於樹根儲存的那個元素且根的右孩子為null,則這個新元素會成為根的右孩子,
    如果這個新元素大於或等於樹根處儲存的那個元素且根的右孩子不是null,則會遍歷根的右孩子,並再次進行比較操作
    如圖:

  • removeElemen操作
    removeElement方法負責從二叉查詢樹中刪除給定的 Comparable元素;或者,當在樹中找不到給定目標元素時,則丟擲 Element Not FoundException異常;
    與前面的線性結構研究不同,這裡不能簡單地通過刪除指定結點的相關引用指標而刪除該結點;
    相反,這裡必須推選出另一個結點來代替要被刪除的那個結點,受保護方法 replacement返回指向一個結點的引用,該結點將代替要刪除的結點;
    選擇替換結點的三種情況如下:
  1. 如果被刪除結點沒有孩子,則 replacement返回null
  2. 如果被刪除結點只有一個孩子,則 replacement返回這個孩子
  3. 如果被刪除結點有兩個孩子,則 replacement會返回中序後繼者(因為相等元素會放到右邊)
  • replace方法
    程式碼如下:
private BinaryTreeNode<T> replacement(BinaryTreeNode<T> node) {
        BinaryTreeNode<T> result = null;
        if ((node.left == null) && (node.right == null)) {
            result = null;
        } else if ((node.left != null) && (node.right == null)) {
            result = node.left;

        } else if ((node.left == null) && (node.right != null)) {
            result = node.right;
        } else {
            BinaryTreeNode<T> current = node.right;// 初始化右側第一個結點
            BinaryTreeNode<T> parent = node;

            // 獲取右邊子樹的最左邊的結點
            while (current.left != null) {
                parent = current;
                current = current.left;
            }

            current.left = node.left;

            // 如果當前待查詢的結點
            if (node.right != current) {
                parent.left = current.right;// 整體的樹結構移動就可以了
                current.right = node.right;
            }

            result = current;
        }
        return result;
    }
  • removeAllOccurrences操作
    removeAllOccurrences方法負責從二叉查詢樹中刪除指定元素的所有存在;
    或者,當在樹中找不到指定元素時,則丟擲 ElementNotFoundException異常;
    如果指定的元素不是Comparable,則removeAllOccurrences方法也會丟擲 ClassCastException異常,該方法會呼叫一次 removeElement方法,以此確保當樹中根本不存在指定元素時會丟擲異常,
    只要樹中還含有目標元素,就會再次呼叫 removeElement方法,注意, removeAllOccurrences方法使用了 LinkedBinaryTree類的 contans方法,還要注意,在 LinkedBinaryTree類中的find方法已經被過載了,以便利用二又查詢樹的有序屬性

  • removeMin操作
    最小元素在二又查詢樹中的位置有3種可能情形:
  1. 如果樹根沒有左孩子,則樹根就是最小元素,而樹根的右孩子會變成新的根結點
  2. 如果樹的最左側結點是一片葉子,則這片葉子就是最小元素,這時只需設定其父結點的左孩子引用為mul即可
  3. 如果樹的最左側結點是一個內部結點,則需要設定其父結點的左孩子引用指向這個將刪除結點的右孩子

三、用有序列表實現二叉查詢樹

  1. 樹的主要使用之一就是為其他集合提供高效的實現
  2. 我們假定 BinarySearchTreeList實現中用到的 LinkedBinarySearchTree實現是一種帶有附加屬性的平衡二叉查詢樹,這種附加屬性是:任何結點的最大深度為log2 n其中n為樹中儲存的元素數目。
    在我們的平衡二又查詢樹假設之下,add操作和remove操作都要求重新平衡化樹,這一點根據所使用的演算法會對分析有所影響。
    另外還要注意一點:雖然樹實現中的有些操作更為有效,比如 removelast,last和contains,但在利用樹實現時,也有一些操作會變得低效,比如 removeFirst和first

四、平衡二叉查詢樹

  1. 如果二叉查詢樹不平衡,其效率可能比線性結構的還要低
  2. 蛻化樹:看起來更像一個連結串列,但實際上它的效率比連結串列的還低,因為每個結點還附帶額外的開銷
    如圖(b)所示:

  3. 平衡化技術
  • 右旋
    要平衡化該樹,我們需要:
    使樹根的左孩子元素成為新的根元素
    使原根元素成為這個新樹根的右孩子元素。
    使樹根的左孩子的右孩子,成為原樹根的新的左孩子

  • 左旋
    要平衡化該樹,我們需要:
    使樹根的右孩子元素成為新的根元素
    使原根元素成為這個新樹根的左孩子元素。
    使原樹根右孩子結點的左孩子,成為原樹根的新的右孩子。

  • 右左旋
    並非所有的不平衡問題都可以只進行某一種旋轉就能解決
    對於由樹根右孩子的左子樹中較長路徑而導致的不平衡,
    我們必須先讓樹根右孩子的左孩子,繞著樹根的右孩子進行一次右旋,然後再讓所得的樹根右孩子繞著樹根進行一次左旋

  • 左右旋
    對於由樹根左孩子的右子樹中較長路徑而導致的不平衡,
    我們必須先讓樹根左孩子的右孩子繞著樹根的左孩子進行一次左旋,然後再讓所得的樹根左孩子繞著樹根進行一次右旋

五、實現二叉查詢樹:AVL樹

  1. 對於樹中的任何結點,如果其平衡因子(其左右子樹的高度差大於1或小於-1),則以該結點為樹根的子樹需要重新平衡
  2. 樹只有兩種途徑變得不平衡:插入或刪除結點,因此每次進行這兩種操作後都必須更新平衡因子,然後從操作的結點處檢查樹的平衡性,一直檢查上溯至根結點
    因此:AVL樹通常實現為每個結點都包含一個指向其父結點的引用

六、實現二叉查詢樹:紅黑樹
(1). 紅黑樹:一種平衡二叉查詢樹,其中的每個結點儲存一種顏色(紅或黑,用布林值實現,false等價於紅色)
控制結點顏色的規則:

  • 根結點為黑色
  • 紅色結點的所有孩子都為黑色
  • 從樹根到樹葉的每條路徑都包含同樣數目的黑色結點

(2). 在某種程度上,紅黑樹的平衡限制沒有AVL樹那麼嚴格,但是,他們的序仍然是logn
(3). 紅黑樹中的元素插入
紅黑樹的插入操作類似於前面的addElement方法,但是這裡總是把插入的新元素顏色設定為紅色,
插入新元素之後,必要時將重新平衡化該樹,根據需要改變元素的顏色以便維持紅黑樹的屬性

  • 紅黑樹插入元素之後的重新平衡化是一個迭代過程,該迭代過程的終止條件有兩種形式:
形式1:current == root (current是當前正在處理的結點)
我們總是設定根結點顏色為黑色,而所有路徑都包括樹根,因此不能違背各條路徑都擁有同樣數目黑色元素這一規則

形式2:current.parent.color == black(即當前結點的父結點顏色為黑色)
current所指向的結點總是一個紅色結點,這意味著,如果當前結點的父結點是黑色,則可滿足所有規則,因為紅色結點並不影響路徑中的黑色結點數目;
另外由於是從插入點處上溯處理,因此早已平衡了當前結點下面的子樹
  • 重新平衡化的每次迭代,我們都關注於如何著色當前結點的兄弟結點:
    當前結點的父結點只有兩種可能:左孩子或右孩子

可能一:
如果是左孩子,利用 current.parent.parent.left.color 得到顏色資訊(null元素的顏色為黑色),且存在兩種情況:
父結點的兄弟為紅色或黑色:這兩種情況下,我們闡述的處理步驟都將發生在一個迴圈內部(該迴圈的終止條件如前所述)
如果父結點的兄弟為紅色,這時的處理步驟如下:

設定current的父親的顏色為 black
設定父結點的兄弟的顏色為black
設定 current的祖父的顏色為red
設定 current指向current的祖父

如果父結點的兄弟為黑色,首先要檢視current是左孩子還是右孩子:
如果current是右孩子,則必須設定current等於其父親,在繼續之前還要再向左旋轉current.right;
後面的步驟,與開始時current為左孩子一樣

如果current是左孩子:
設定current的父親的顏色為black
設定current的祖父的顏色為red
如果current的祖父不等於null,則讓current的父親繞著current的祖父向右旋轉

可能二:
如果是右孩子,存在兩種情況:
父結點的兄弟為紅色或黑色:這兩種情況下,我們闡述的處理步驟都將發生在一個迴圈內部(該迴圈的終止條件如前所述)

如果父結點的兄弟為紅色,這時的處理步驟如下(同當前結點的父結點的兄弟顏色為紅時):

設定current的父親的顏色為 black
設定父結點的兄弟的顏色為black
設定 current的祖父的顏色為red
設定 current指向current的祖父

如果父結點的兄弟為黑色,首先要檢視current是左孩子還是右孩子(與當前結點的父結點的兄弟顏色為黑時,操作對稱):
如果current是左孩子,則必須設定current等於其父親,在繼續之前還要再向右旋轉current.left;
後面的步驟,與開始時current為右孩子一樣

如果current是右孩子:
設定current的父親的顏色為black
設定current的祖父的顏色為red
如果current的祖父不等於null,則讓current的父親繞著current的祖父向左旋轉

(4). 紅黑樹中的元素刪除
與元素插入的那些情況一樣,刪除的兩種情況也是對稱的——取決於 current是左孩子還是右孩子。
當 current為右孩子時:
(在插入時,我們最關注的是當前結點的父親的兄弟的顏色)
而對刪除而言,焦點要放在當前結點的兄弟的顏色上(用 current.parent. left.color來指代這種顏色):
還要觀察該兄弟的孩子的顏色,要注意的重要一點是:顏色的預設值是black;
這樣,任何時刻如果試圖取得mull物件的顏色,結果都將是black

其他的情況很容易推匯出來,只要把上述情況中的“左”換成“右”、“右”換成“左”即可

如果兄弟的顏色是red,則在做其他事之前必須完成如下處理步驟:

* 設定兄弟的顏色為black
* 設定current的父親的顏色為red
* 讓兄弟繞著 current的父親向右旋轉
* 設定兄弟等於 current的父親的左孩子

下面再繼續處理過程:不管這個初始兄弟是red還是 black,這裡的處理會根據兄弟的孩子的顏色分成兩種情況:
如果兄弟的兩個孩子都是black(或null),則需要

* 設定兄弟的顏色為red
* 設定 current等於 current的父親

如果兄弟的兩個孩子不全為black,則將檢視兄弟的左孩子是否是black,如果是,則在繼續之前必須完成如下步驟:

* 設定兄弟的右孩子的顏色為 black
* 設定兄弟的顏色為red
* 讓兄弟的右孩子繞著兄弟本身向右旋轉
* 設定兄弟等於cumt的父親的左孩子

最後是兄弟的兩個孩子都不為 black這一情況,這時必須:

* 設定兄弟的顏色為 current的父親的顏色。
* 設定 current的父親的顏色為black
* 設定兄弟的左孩子的顏色為black
* 讓兄弟繞著 current的父親向右旋轉
* 設定 current等於樹根

該迴圈終止之後,我們要酬除該結點,並設定其父親的孩子引用為mull

教材學習中的問題和解決過程

問題1:紅黑樹這樣設計的意義在哪裡呢?

問題1解析:

【參考資料】

問題2:平衡化的迭代過程中,如果當前結點的父結點是左孩子,為什麼不用像父結點是右孩子時那樣討論父結點是紅的還是黑的?

問題2解析:

【參考資料】

程式碼執行中的問題及解決過程

問題1:
問題1解決過程:

本週錯題

錯題1:

錯題1解析:

程式碼託管

結對及互評

  • 部落格中值得學習的或問題:
    • 侯澤洋同學的部落格排版工整,介面很美觀,並且本週還對部落格排版、字型做了調整,很用心
    • 問題總結做得很全面:對課本上不懂的程式碼會做透徹的分析,即便可以直接拿過來用而不用管他的含義
    • 對教材中的細小問題都能夠關注,並且主動去百度學習,printTree()方法分析的很到位
    • 程式碼中值得學習的或問題:
    • 對於程式設計的編寫總能找到角度去解決
  • 本週結對學習情況
    • 20172302
    • 結對學習內容
      • 第十章內容:樹

學習進度條

程式碼行數(新增/累積) 部落格量(新增/累積) 學習時間(新增/累積) 重要成長
目標 5000行 30篇 400小時
第一週 0/0 1/1 4/4
第二週 560/560 1/2 6/10
第三週 415/975 1/3 6/16
第四周 1055/2030 1/4 14/30
第五週 1051/3083 1/5 8/38
第六週 785/3868 1/6 16/54