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

20172323 2018-2019-1 《程式設計與資料結構》第七週學習總結

20172323 2018-2019-1 《程式設計與資料結構》第七週學習總結

教材學習內容總結

本週學習了第11章二叉查詢樹

  • 11.1概述
    • 二叉查詢樹的左孩子小於父結點,而父結點又小於或等於其右孩子
    • 二叉查詢樹的定義是二叉樹定義的擴充套件
操作 說明
addElement 往樹中新增一個元素
removeElement 從樹中刪除一個元素
removeAllOccurrences 從樹中刪除所指定元素的任何存在
removeMin 刪除樹中的最小元素
removeMax 刪除樹中的最大元素
findMin 返回一個指向樹中最小元素的引用
findMax 返回一個指向樹中最大元素的引用
  • 11.2用連結串列實現二叉查詢樹
    • 每個BinarySearchTreeNode物件要維護一個指向結點所儲存元素的引用,另外還要維護指向結點的每個孩子的引用
    • addElement操作:根據給定元素值往樹中恰當位置新增元素,要求元素型別是Comparable,否則丟擲NoComparableElementException異常。該樹為空,元素成為根結點;小於根結點但左孩子不為null將會遍歷根的左孩子直到找到合適位置。

      如圖,值為20的一個元素新增到該樹中,比較根結點比45小,應該往左子樹的方向新增,左孩子12不為null,所以又往右子樹的方向往下走到37,再往左子樹走,直到成為24的左子樹。
    • removeElement操作:從二叉查詢樹中刪除一個元素時,必須推選出另一結點來代替要被刪除的結點。
      • 選擇替換結點的三種操作
        - 如果被刪除結點沒有孩子,則replacement返回null
        - 如果被刪除結點只有一個孩子,則replacement返回這個孩子
        - 如果被刪除結點有兩個孩子,則replacement會返回中序後繼者

        要想刪除有兩個孩子的z結點,首先找到它的中序後繼者y結點,將y移除,再將y結點替換z結點即可
    • removeAllOccurences操作:該方法會呼叫一次removeElement方法,以此確保當樹中根本不存在指定元素會丟擲異常
    • removeMin操作:
      最小元素在二叉查詢樹中的三種情況
      • 如果沒有左孩子,根結點是最小元素。此時根的右孩子成為新的根結點
      • 如果樹的最左側結點是葉結點,這個根結點就是最小元素,只需設定該結點為null
      • 若樹的最左端是一個內部結點,則需要設定其父結點的左孩子引用指向這個將刪除結點的右孩子
  • 11.3 用有序列表實現二叉查詢樹
    • 樹的主要使用之一就是為其它集合提供高效的實現
    • BinarySearchTreeList實現中的用到的是一種帶有附加屬性的平衡二叉查詢樹,其附加屬性在於:任何結點的最大深度為log2(n),n為樹中儲存的元素
    • 在平衡二叉查詢樹假設之下,add操作和remove操作都要求重新平衡化樹
  • 11.4 平衡二叉查詢樹
    • 如果二叉查詢樹不平衡,其效率可能比線性結構的還要低
    • 如下是一個蛻化樹的例子

      如果沒有平衡假設,在此樹中新增一個大於70的元素,則它的時間複雜度將為O(n),而我們的目標在於保持樹的最大路徑長度為log2(n)
    • 右旋:右旋可以解決樹根左孩子的左子樹中較長的路徑而導致的不平衡

      右旋的三步
      1.使樹根的左孩子元素成為新的根元素
      2.使原根元素成為這個新樹根的右孩子元素
      3.使原樹根的左孩子的右孩子,成為原樹根的新的左孩子

    • 左旋:左旋可以解決樹根右孩子的右子樹中較長的路徑而導致的不平衡
    • 右左旋:對於樹根右孩子的左子樹較長路徑不平衡,讓樹根右孩子的左孩子繞樹根右孩子進行一次右旋,再讓所得樹根右孩子繞著樹根進行一次左旋
    • 左右旋

  • 11.5 實現二叉查詢樹:AVL樹
    • 自樹根而下的路徑最大長度必須不超過log2n而且自樹根而下的路徑長度必須不小於log2n- 平衡因子:右子樹的高度減去左子樹的高度。如果平衡因子大於1或者小於-1則認為以該結點為樹根的子樹需要重新平衡
    • 樹只有兩種途徑變得不平衡:插入結點或刪除結點
    • 此圖給出了各種旋轉的示意
    • AVL樹的右旋、左旋、右左旋、左右旋
  • 11.6 實現二叉查詢樹:紅黑樹
    • 每個結點儲存一種顏色,通常用一個布林值來實現,值false等價於紅色
    • 規則如下

         根結點為黑色              
         紅色結點的所有孩子為黑色                 
         從樹根到樹葉的每條路徑都包含同樣數目的黑色結點              
    • 在某種程度上,紅黑樹的平衡限制沒有AVL樹那麼嚴格,但是,它們的序仍然是logn。
    • 路徑中至多有一半是紅色結點,至少有一半是黑色結點。由此可得出紅黑樹的最大高度約為2logn。
    • 紅黑樹中的元素插入
      • 元素插入之後要滿足紅黑樹的平衡規則,所以要對樹進行重新著色
      • 因為需要滿足從樹根到樹葉的每條路徑都包含同樣數目的黑色結點,所以在著色的過程中要考慮到當前結點的兄弟結點的顏色情況。
      • 根據被插入節點的父結點的情況,可以將"當節點z被著色為紅色結點,並插入二叉樹"劃分為三種情況來處理。

          ① 情況說明:被插入的結點是根結點。
            處理方法:直接把此結點塗為黑色。
          ② 情況說明:被插入的結點的父結點是黑色。
            處理方法:什麼也不需要做。結點被插入後,仍然是紅黑樹。
          ③ 情況說明:被插入的結點的父結點是紅色。
            處理方法:那麼,該情況與紅黑樹的特性“從一個結點到該結點的子孫結點的所有路徑上包含相同數目的黑結點。”相沖突。這種情況下,被插入結點是一定存在非空祖父結點的;進一步的講,被插入結點也一定存在叔叔結點(即使叔叔結點為空,我們也視之為存在,空結點本身就是黑色結點)。理解這點之後,我們依據"叔叔結點的情況",將這種情況進一步劃分為3種情況(Case)。
/ 現象說明 處理策略
case1 當前結點的父結點是紅色,且當前結點的祖父結點的另一個子結點(叔叔結點)也是紅色。 (01) 將“父結點”設為黑色。(02) 將“叔叔結點”設為黑色。(03) 將“祖父結點”設為“紅色”。(04) 將“祖父結點”設為“當前結點”(紅色結點);即,之後繼續對“當前結點”進行操作。
case2 當前結點的父結點是紅色,叔叔結點是黑色,且當前結點是其父結點的右孩子 (01) 將“父結點”作為“新的當前結點”。(02) 以“新的當前結點”為支點進行左旋。
case3 當前結點的父結點是紅色,叔叔結點是黑色,且當前結點是其父結點的左孩子 (01) 將“父結點”設為“黑色”。(02) 將“祖父結點”設為“紅色”。(03) 以“祖父結點”為支點進行右旋。
  • 以上插入情況的分類是在網上查詢到的資料中給出的,教材上給出的紅黑樹的元素插入的分類似乎是有問題的,因為我始終無法理解為何要判斷當前結點的父結點為左孩子和右孩子,且為右孩子時為何要根據兄弟結點的顏色情況分兩種討論,而為左孩子時又不再需要討論了。

    • 紅黑樹中的元素刪除
      • 紅黑樹元素刪除之後的重新平衡化這一過程的終止條件是(current == root)或者(current.color == red).
      • 插入時依然要關注當前結點的父親的兄弟的顏色
      • 如果叔叔的顏色是red

           設定叔叔的顏色為black    
           設定current的父親顏色為red        
           讓叔叔繞著current的父親向右旋轉          
           設定叔叔等於current父親的左孩子           
           如果叔叔的兩個孩子都是black或者null則需要設定叔叔的顏色為red,設定current等於current的父親
           如果叔叔的兩個孩子不全為black,如果叔叔的左孩子是black,則設定右孩子也為black,設定叔叔的顏色為red,再讓兄弟的右孩子繞著兄弟本身向右旋轉,最後設定叔叔等於current父親的左孩子
           如果叔叔的兩個孩子都不為black,則設定叔叔的顏色為current父親的顏色,設定current父親的顏色為black,設定叔叔的左孩子的顏色為black,讓叔叔繞著current的父親向右旋轉,設定current等於樹根。
           迴圈終止後刪除該結點,設定其父親的孩子引用為null

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

  • 問題1:測試教材程式碼LinkedBinarySearchTree類的find方法時,當找不到目標元素時,系統會丟擲一個錯誤來(如圖),如何讓它報錯之後跟著執行接下來的程式

    如果程式完全不處理異常,那麼該程式將會(非正常)終止並給出一條訊息來描述發生的是什麼異常以及發生在程式的什麼地方。但此處已經對異常給出了一個解決
if (current == null)
                throw new ElementNotFoundException("LinkedBinaryTree");

為什麼還是會出現這樣的問題?

  • 問題1解決方案:問題出錯的地方好像和我想的可能出現問題的地方差別有些大,甚至可以說問題的解決是我誤打誤撞從而解決的,只需要簡單地將throw修改為return就可以處理異常並且繼續執行下去。原理是什麼?
    最基本的丟擲異常的知識忘得一乾二淨,這裡的if語句其實嚴格上算不上捕捉異常,因為try-catch語句都沒能用上,只能說是列舉了一種情況當current==null時,return一個值,如果在這裡用上throw語句,那麼執行到這步時,他將會立即終止程式,並返回一個錯誤值,而return就和之後的return等同了,只不過是返回的東西不太一樣罷了。

  • 問題2:紅黑樹的重新平衡化過程是一種迭代過程,迭代過程的終止條件有兩個,一是current == root,二是current.parent.color == black是如何執行的,同時按照書上的圖示,紅黑樹平衡之後,樹的結構似乎沒有發生改變,那麼是在什麼地方實現了平衡?
  • 問題2解決方案:首先,紅黑樹的插入方法類似於之前的addElement方法,所以就按照之前給出的新增方法,小於根元素往左子樹的方向新增,大於或等於根元素就往右子樹的方向新增,但是新增元素之後可能造成之前出現過的一些情況--某一邊的子樹的深度會遠大於另一邊子樹的深度,即平衡因子會大於1或者小於-1,那麼這裡為什麼就不需要再用左旋右旋的方法了呢?翻回11.4平衡二叉查詢樹提出的問題,為什麼要平衡二叉查詢樹,是因為要防止其效率比線性結構還要低,其主要思想就是要使得各種對二叉查詢樹的操作的時間複雜度要保持在O(logn)而不是和線性結構一樣的O(n)。AVL樹提供的方法就是改變樹的原有結構使之重新平衡,而紅黑樹提供的是另一種平衡方法,不是靠改變樹的結構完成的。紅黑樹控制結點顏色有三個規則,上面已經給出。因為每一條路徑中至多有一半結點是紅色結點,至少有一半結點是黑色結點,所以從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。所以進行查詢、新增、刪除等操作時,遍歷樹時的最長路徑仍然是logn。
    紅黑樹的迭代過程是對紅黑樹的重新著色,而且著色是從插入點上溯到樹根的,所以迭代終止的第一種條件就是當current判斷為樹根時,迭代終止,將樹根顏色重新定義為黑色後整個紅黑樹的重新著色完成。第二種條件的意思是,新插入的結點設定成為紅色,如果它的父結點已經是黑色的了,那麼意味著整個紅黑樹已經是符合規則的,不需要再進行重新的著色。

  • 問題3:就查詢而言,紅黑樹的查詢依然是要遍歷每一個元素,但在蛻化樹的情況下紅黑樹進行查詢的時間複雜度似乎依然是O(n),它是如何解決蛻化樹問題的?
  • 問題3解決方案:這裡犯了先入為主的錯誤,因為在紅黑樹的插入規則下,整數列表“3 5 9 12 18 20”是不會形成樹中蛻化樹的結構的,因此遍歷時也就不需要每個元素從根到葉的看一遍,時間複雜度自然不會是O(n)。

程式碼除錯中的問題和解決過程

  • 問題1:PP11.8在二叉樹的基礎上完成AVL樹的方法,其中關於左旋右旋等方法如何用程式碼實現。

  • 問題1解決方案:首先是在二叉樹的基礎上完成,移植二叉樹的各種方法。但是需要重新設定一個AVL樹結點的方法,除了設定指向左孩子、右孩子的指標之外,還需要設定一個int變數儲存結點所在的高度,用於實現AVL樹中的平衡因子。之後寫各類旋轉的方法,譬如右旋
private AVLTreeNode<T> rightRightRotation(AVLTreeNode<T> k2) {
    AVLTreeNode<T> k1;

    k1 = k2.left;
    k2.left = k1.right;
    k1.right = k2;

    k2.height = max( height(k2.left), height(k2.right)) + 1;
    k1.height = max( height(k1.left), k2.height) + 1;

    return k1;
}


如圖,該樹不平衡時,將整個左子樹繞著k2點進行旋轉,k1是k2的左孩子,於是k1成為新的根結點,k1的右孩子成為k2的左孩子,k2設定為k1的右孩子。之後再重新定義k1、k2的高度,k2從左右子樹中選出較長的一支作為其高度,加一是因為樹的高度從0開始。k1也是從它的左右子樹中取出較長一支,但這裡的右子樹可以直接呼叫k2的高度。

再分析一下右左旋的情況

private AVLTreeNode<T> leftRightSpin(AVLTreeNode<T> node) {
        node.left = rightRightSpin(node.left);

        return leftLeftSpin(node);
    }


原理即是讓初始結點的右孩子的左孩子繞初始結點的右孩子進行一次右旋node.left = rightRightSpin(node.left);,再讓初始結點的右孩子繞著初始結點進行一次左旋return leftLeftSpin(node);
類似地可以寫出右旋和左右旋的方法,但是什麼時候呼叫右旋,什麼時候進行左右旋的方法還沒有進行定義。以新增元素為例,這裡編寫了一公一私兩個方法

public void addElement(T key) {
        root = addElement(root, key);
    }
    private AVLTreeNode<T> addElement(AVLTreeNode<T> tree, T element) {

        if (!(element instanceof Comparable)) {
            throw new NonComparableElementException("AVLTreeNode");
        }

        if (tree == null) {
            // 新建節點
            tree = new AVLTreeNode<T>(element, null, null);
            if (tree==null) {
                throw new EmptyCollectionException("EmptyCollectionException");
            }
        } else {

            if (element.compareTo(tree.getElement()) < 0) {    // 應該將key插入到"tree的左子樹"的情況
                tree.left = addElement(tree.left, element);
                // 插入節點後,若AVL樹失去平衡,則進行相應的調節。
                if (height(tree.right) - height(tree.left) == -2) {//因為查到左子樹,必然左側感度大於右側暗度
                    if (element.compareTo(tree.left.getElement()) < 0)
                        tree = leftLeftSpin(tree);
                    else
                        tree = leftRightSpin(tree);
                }
            } else if (element.compareTo(tree.getElement()) >= 0) {    // 應該將key插入到"tree的右子樹"的情況
                tree.right = addElement(tree.right, element);
                // 插入節點後,若AVL樹失去平衡,則進行相應的調節。
                if (height(tree.left) - height(tree.right) == -2) {
                    if (element.compareTo(tree.right.getElement()) > 0)
                        tree = rightRightSpin(tree);
                    else
                        tree = rightLeftSpin(tree);
                }
            }
        }

        tree.height = Math.max( height(tree.left), height(tree.right)) + 1;

        return tree;
    }

最後列印樹的方法呼叫了EXpressionTree的PrintTree方法,結果如下

程式碼託管

上週考試錯題總結

上週的測試似乎都是錯在沒有看清楚單詞-_-||

中文 英文
前序遍歷 preorder traversal
中序遍歷 inorder traversal
後序遍歷 postorder traversal
層序遍歷 level-order traversal

結對及互評

  • 部落格中值得學習的或問題:
  • 基於評分標準,我給譚鑫的部落格打分:8分。得分情況如下:
    正確使用Markdown語法(加1分):
    模板中的要素齊全(加1分)
    教材學習中的問題和解決過程, 三個問題加3分
    程式碼除錯中的問題和解決過程, 三個問題加3分

  • 基於評分標準,我給方藝雯的部落格打分:8分。得分情況如下:、
    正確使用Markdown語法(加1分):
    模板中的要素齊全(加1分)
    教材學習中的問題和解決過程, 兩個問題加2分
    程式碼除錯中的問題和解決過程, 四個問題加4分

  • 本週結對學習情況
  • 上週部落格互評情況

其他

教材學習在紅黑樹這個地方卡了殼,前思後想冥思苦想地看教材上的講解,一邊寫部落格一邊想一邊提出疑問,導致寫了一大堆囉嗦話,可是最終也沒有理得太順,也不知道我挑的教材上出現的錯誤是不是隻是我沒有理解到他真正的意圖。

學習進度條

程式碼行數(新增/累積) 部落格量(新增/累積) 學習時間(新增/累積) 重要成長
目標 5000行 30篇 400小時
第一週 0/0 1/1 8/8
第二週 470/470 1/2 12/20
第三週 685/1155 2/4 10/30
第四周 2499/3654 2/6 12/42
第六週 1218/4872 2/8 10/52
第七週 590/5462 1/9 12/64
第八週 993/6455 1/10 12/76

參考資料