20172314 2018-2019-1《程序設計與數據結構》第七週學習總結

教材學習內容總結

概述

  • 二叉查找樹:是含附加屬性的二叉樹,即其左孩子小於父節點,而父節點又小於或等於右孩子。
  • 二叉查找樹的定義是二叉樹定義的擴展。
  • 二叉查找樹的各類操做

用鏈表實現二叉查找樹

  • 每一個BinaryTreeNode對象要維護一個指向結點所存儲元素的引用,另外還要維護指向結點的每一個孩子的引用。
  • LinkedBinarySearchTree類提供兩個構造函數:一個負責建立一個空的LinkedBinarySearchTree;另外一個負責建立一棵根結點爲特定元素的LinkedBinarySearchTree。這兩個構造函數都只是引用了LinkedBinaryTree中相應的那兩個構造函數。
  • addElement操做:就是根據給定元素的值,在樹中的恰當位置添加該元素。
    • 若是該元素不是Comparable,該方法會拋出NoComparableElementException異常。
    • 若是樹爲空,該元素稱爲新結點。
    • 若是樹非空,則依據二叉查找樹的性質,分別與某結點及其左右孩子比較,按照左孩子<父節點,父節點<=右孩子的規則將其添加到適當位置,或者稱爲左右孩子的孩子。
    • 向二叉樹中添加元素
  • removeElement操做:從二叉查找樹中刪除一個元素時,必須推選出另外一個結點(replacement方法找到這個結點)來代替要被刪除的那個結點。
    • 在樹中找不到給定目標元素時,拋出ElementNotFoundException異常。
    • 選擇替換結點的三種狀況
      • 被刪除結點沒有孩子,則replacement返回null
      • 被刪除結點只有一個孩子,replacement返回這個孩子
      • 被刪除結點有兩個孩子,replacement返回中序後繼者(由於相等元素會放到右邊)
    • 從二叉樹中刪除元素
  • removeAllOccurrences操做:從二叉查找樹中刪除指定元素的全部存在。
    • 在樹中找不到給定目標元素時,拋出ElementNotFoundException異常。
    • 若是該元素不是Comparable,該方法會拋出ClassCastException異常。
    • 該方法會調用一次removeElement方法,以確保當樹中根本不存在指定元素時會拋出異常。
    • 若是樹中還含有目標元素,就會再次調用removeElement方法。
  • removeMin操做:根據二叉查找樹的定義,最右側存最大的結點,最左側存最小元素。
    • 若是樹根沒有左孩子,根結點爲最小,右孩子變爲新的根結點。
    • 若是左孩子是葉子結點,將父結點的引用設爲null便可。
    • 若是左孩子是內部結點,則這個左孩子的右孩子將代替本身成爲它父節點的左孩子。

有序列表實現二叉查找樹

  • 樹的主要使用之一就是爲其它集合提供高效的實現。
  • LinkedBinarySearchTree類的方法與有序列表的方法之間存在着一一對應的關係。
  • 列表的一些常見操做
    html

  • 有序列表的特有操做
    java

  • BinarySearchTreeList實現的分析
    • BinarySearchTreeList的實現是一種帶有附加屬性(任何結點的最大深度爲log2n,其中n爲樹中儲存元素的個數)的平衡二叉查找樹
    • 樹實現會使有些操做變得高效,有些操做變得低效
    • add操做和remove操做須要從新平衡化樹
    • 有序列表的鏈表實現分析和二叉查找樹實現分析
  • 平衡二叉查找樹
    • 若是二叉查找樹不平衡,其效率可能比線性結構的還要低。例如蛻化樹看起來更像一個鏈表,事實上它的效率比鏈表的還低,由於每一個結點附帶有額外的開銷。
      node

    • 若是沒有平衡假設,當樹根是樹中最小元素而被插入元素是樹中最大的元素時,這種狀況下addElement操做的時間複雜性是O(n)而不是O(logn)。
    • 咱們的目標是保持樹的最大路徑長度爲(或接近)log2n。git

平衡化樹的四種方法

  • 自樹根向下的路徑最大長度不超過log2n,最小長度必須不小於log2(n-1)
  • 平衡因子指左子樹減右子樹深度的值。
  • 右旋
    • 一般是指左孩子繞着其父結點向右旋轉。是因爲樹根的左孩子的左子樹中較長的路徑致使的不平衡。
    • 如圖所示的初始樹,首先能夠計算他成爲平衡樹以後的樣子,他如今的最大路徑長度是3,最小路徑長度是1,樹中有6個元素,所以最大路徑長度應該是log26,即2。要平衡化該樹,須要三步
      • 使根的左孩子稱爲新根
      • 使原來的根元素稱爲新根的右孩子
      • 使原根的左孩子的右孩子成爲原樹根的新的左孩子
    • 如圖是依據上面得三步的右旋過程
  • 左旋
    • 一般指右孩子繞着其父結點向左旋轉。是因爲較長的路徑出如今樹根右孩子的右子樹中而致使的不平衡。
    • 一樣於右旋,爲了平衡化,須要三步
      • 使樹根的右孩子元素成爲新的根元素
      • 原根元素稱爲新根元素的左孩子
      • 原樹根右孩子的左孩子成爲原樹根新的右孩子
    • 如圖是依據上面的三步的左旋過程
  • 右左旋
    • 對於由樹根右孩子的左子樹中較長路徑而致使的不平衡,須要先讓樹根右孩子的左孩子繞其父結點進行一次右旋,再讓樹根的右孩子繞樹根進行一次左旋。
    • 如圖
  • 左右旋
    • 對於由樹根左孩子的右子樹中較長路徑而致使的不平衡,須要先讓樹根左孩子的右孩子繞其父結點進行一次左旋,再讓樹根的左孩子繞樹根進行一次右旋。

實現二叉查找樹:AVL樹

  • 對於樹中任何結點,若是其|平衡因子|(右子樹的高度減去左子樹的高度)>1,那麼以該結點爲樹根的子樹須要從新平衡。
  • 樹(或樹的任何子樹)只有兩種途徑變得不平衡:插入結點或刪除結點。所以在每次進行這兩種操做時,都必須更新平衡因子,而後從插入或刪除結點的那個地方開始檢查樹的平衡性。上溯到根結點,因此AVL樹一般最好實現爲每一個結點都包含一個指向父結點的引用。函數

  • AVL樹的右旋
    • 某結點的平衡因子爲-2,則左子樹過長,若是左孩子的平衡因子是-1,則這個結點的左子樹爲較長的路徑,將這個左孩子繞初始結點右旋一次便可平衡該樹。
  • AVL樹的左旋
    • 某結點的平衡因子是+2,則右子樹過長,若是右孩子的平衡因子是+1,則意味着較長的路徑處在這個右孩子的右子樹中,將該右孩子繞初始結點進行一次左旋便可平衡。
  • AVL樹的右左旋
    • 一樣根據平衡因子來判斷,某結點的平衡因子是+2,右孩子的平衡因子是-1,則過長的是右孩子的左子樹,須要進行一次右左雙旋(初始結點的右孩子的左孩子繞初始結點的右孩子進行一次右旋,再讓初始結點的右孩子繞初始結點進行一次左旋)如圖
  • AVL樹的左右旋
    • 一樣根據平衡因子來判斷,某結點的平衡因子是-2,右孩子的平衡因子是+1,則過長的是左孩子的右子樹,須要進行一次左右雙旋(初始結點的左孩子的右孩子繞初始結點的左孩子進行一次左旋,再讓初始結點的左孩子繞初始結點進行一次右旋)

實現二叉查找樹:紅黑樹

  • 紅黑樹是一種平衡二叉查找樹,其中的每一個結點存儲一種顏色(紅色或黑色,用布爾值表示,false表示紅色)。結點顏色的規則:
    • 根結點爲黑色
    • 紅色結點的全部孩子都爲黑色
    • 從樹根到樹葉的每條路徑都包含一樣數目的黑色結點
  • 某種程度上,紅黑樹中的平衡限制沒有AVL樹那麼嚴格,但他們的序仍然是logn。
  • 紅黑樹路徑中至多一半紅結點,至少一半黑結點。
  • 紅黑樹最大高度約爲2*logn,因而遍歷最長路徑的序仍然是logn。
  • 插入的結點認爲是紅色,空結點認爲是黑色。
  • 紅黑樹示意:
    學習

  • 紅黑樹中的元素插入
    • 開始把新元素的顏色設置成紅色,而後從新平衡化該樹,根據紅黑樹的屬性改變元素顏色,最後總會把根結點設爲黑色。
    • 插入以後的從新平衡化是一種迭代過程,從插入點上溯到樹根,迭代過程的終止條件有兩種
      • (current == root):緣由是每條路徑黑色元素相同,而根節點總爲黑色。
      • (current.parent.color == black):由於current所指向的每個結點都是紅色(開始時,老是把新元素設置成紅色,那麼其父結點不可能爲紅色),那麼若是當前結點的父結點是黑色,因爲黑色數目是固定不變的,而且平衡時上溯處理早已平衡了當前結點的下面子樹,因此只要知足這個條件,就能夠實現平衡。
        在每次迭代的過程當中,有如下狀況:
      • 父結點是左孩子
        • 右叔叔是紅色
          • 父結點爲黑
          • 右叔叔爲黑
          • 祖父爲紅
          • current由我變成父結點
        • 右叔叔是黑色
          • 我是右孩子的狀況下
            • current由我變成父結點
            • 繞current左旋,current變成左孩子
            • (current爲左孩子的步驟)
            • 父結點爲黑
            • 祖父爲紅
            • 若是祖父不爲空,讓父結點繞祖父右旋
      • 父結點是右孩子
        • 左叔叔是紅色
          • 父結點爲黑
          • 左叔叔爲黑
          • 祖父爲紅
          • current由我變成祖父
        • 左叔叔是黑色
          • 我是左孩子的狀況下
            • current由我變成父結點
            • 繞current右旋,current變成右孩子
            • (current爲右孩子的步驟)
            • 父結點爲黑
            • 祖父爲紅
            • 若是祖父不爲空,讓父結點繞祖父左旋
      • 以上兩種狀況是對稱的,而且最後都會把根結點變爲黑色。插入中最關注的是叔叔的顏色。
  • 紅黑樹中的元素刪除
    • 刪除元素以後須要從新平衡化(即從新着色),是一個迭代過程,終止條件有兩種:
      • (current == root)
      • (current.color == red)
      • 若是兄弟顏色是紅
        • 設置兄弟爲黑
        • 父結點爲紅
        • 兄弟繞父結點右旋
        • 舊兄弟絕交,新兄弟等於父結點的左孩子

          接下來無論兄弟是黑仍是紅都要進行的步驟:
      • 兄弟的兩個孩子都是black/null
        • 設置兄弟顏色是紅
        • current由我變爲父結點
      • 兄弟的兩個孩子不全爲黑
        • 左孩子爲黑
          • 兄弟的右孩子爲黑
          • 兄弟爲紅
          • 讓兄弟的右孩子繞兄弟右旋
          • 兄弟等於父結點的左孩子
      • 兄弟的兩個孩子都不爲黑
        • 兄弟爲父結點的顏色
        • 父結點爲黑
        • 兄弟左孩子顏色爲黑
        • 兄弟繞父結點右旋
        • current由我變爲樹根
      • 循環結束後刪除該結點,並設置父親的孩子引用爲null。刪除中最關注的是兄弟的顏色。

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

  • 問題一:在紅黑樹的刪除中,迭代的一個終止條件是(current.color == red),不能理解緣由。
  • 問題一解決:首先要明白的是最終目的是爲了讓樹知足紅黑樹的三個性質:根結點爲黑色,紅結點的孩子爲黑色,每條路徑黑色結點數目相同。刪除時有兩種狀況:
    • 刪除的是紅色:這種狀況下直接刪除就好,由於對於紅黑樹的第一個性質而言,不影響根節點,第二個性質,紅結點的父節點是黑色,子結點也是黑色,二者均不須要變色,對第三個性質,刪除紅色並不影響黑色結點的數目。因此說,刪除紅色結點能夠直接刪。
    • 刪除的是黑色:這種狀況下依然考慮紅黑樹的三個性質,能夠看出刪除黑色對三個性質都有影響,這種影響會一直上溯到根結點,當根結點爲紅色時,下面的所有都平衡了,整棵樹平衡,最後在根據性質把根結點變爲黑色便可。這就是迭代的第二個終止條件。
  • 問題二:removeElement操做的代碼理解有問題
  • 問題二解決:這個方法負責從二叉查找樹中刪除給定的comparable元素;或者當在樹中找不到給定的目標元素時,則拋出ElementNotFoundException異常。與前面的線性結構研究不一樣,這裏不能經過簡單的經過刪除指定結點的相關引用指針而刪除該結點。相反這裏必須推選出一個結點來取代要被刪除的結點。受保護方法,replacement返回指向一個結點的應用,該結點將代替要刪除的結點,選擇替換結點有三種狀況:
    • 若是被刪除結點沒有孩子那麼repalcement返回null
    • 若是有一個,那麼replacement返回這個孩子
    • 若是有兩個孩子,則replacement會返回終須後繼者。由於相等元素會放在後邊
    public T removeElement(T targetElement) {
        T result = null;
        if (isEmpty()) {//樹爲空時拋出異常
            throw new ElementNotFoundException("LinkedbinarySearchTree");
        } else {//樹不爲空
            BinaryTreeNode<T> parent = null;
            if (((Comparable<T>) targetElement).equals(root.getElement())) {//要刪除的元素是根結點
                result = root.element;
                BinaryTreeNode<T> temp = replacement(root);
                if (temp == null) {//找不到結點替換
                    root = null;
                } else {
                //用找到的結點替換根結點
                    root.element = temp.element;
                    root.setLeft(temp.getLeft());
                    root.setRight(temp.getRight());
                }
                modCount--;
            } else {//要刪除根節點的孩子
                parent = root;
                if (((Comparable<T>) targetElement)
                        .compareTo(root.getElement()) < 0) {//目標在根的左邊
                    result = removeElement(targetElement, root.getLeft(),
                            parent);
                } else {//目標在根的右邊
                    result = removeElement(targetElement, root.getRight(),
                            parent);
                }
            }
        }
        return result;
    }
    private T removeElement(T targetElement, BinaryTreeNode<T> node,
                            BinaryTreeNode<T> parent) {//用來刪除除根之外的目標元素
        T result = null;
        if (node == null) {
            throw new ElementNotFoundException("LinkedbinarySearchTree");
        } else {
            if (((Comparable<T>) targetElement).equals(node.getElement())) {//找到目標元素
                result = node.element;
                BinaryTreeNode<T> temp = replacement(node);//將node元素刪除
                //往下繼續查找目標元素,看看左右孩子是不是
                if (parent.right == node) {
                    parent.right = temp;
                } else {
                    parent.left = temp;
                }
                modCount--;
            } else {//若是目標元素比根結點小,則在根結點左側,再次使用該方法從左子樹中查找目標元素
                parent = node;
                if (((Comparable<T>) targetElement)
                        .compareTo(root.getElement()) < 0) {
                    result = removeElement(targetElement, root.getLeft(),
                            parent);
                } else {//目標元素比根結點大,再次使用該方法從右子樹中查找目標元素
                    result = removeElement(targetElement, root.getRight(),
                            parent);
                }
            }
        }
        return result;
    }
    // 刪除元素
    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;
    }
  • 問題三:對於removeElement方法中「若被刪除結點有兩個孩子,replacement返回中序後繼者」一句不能理解返回的是哪一個結點
  • 問題三解決:在課堂上,老師提到了前驅結點,就是對一棵樹進行中序排序,造成一個序列,書上所提到的返回中序後繼者的意思就是排序後的序列的被刪除結點的前驅結點或後驅結點均可以,由本身來定義。例如
    測試

    對這棵樹進行中序排序爲:2 3 4 5 6。刪除結點3後,能夠返回前驅結點2,也能夠返回後驅結點4。.net

代碼調試中的問題和解決過程

  • 問題一:對於AVL樹的實現,一開始想的是在printTree裏面添加,在輸出樹以前對樹進行平衡,而後測試類裏直接定義一個完整的樹,可是這樣作的話考慮到樹的不少狀況,
    tree = new AVLTreeNode<T>(element, (AVLTree<T>) null, null);若是傳入結點爲空須要建立一個結點,若是要添加的元素比根結點小tree.left = addElement(tree.left, element);表示將其添加到結點左邊,並當(height(tree.right) - height(tree.left) == -2)時,說明左子樹過長,這時若是(element.compareTo(tree.left.getElement()) < 0則說明元素比左孩子小,這樣應該使用tree.rightRightRotation(tree)方法,進行右旋,不然的話進行右左旋。
    但這時問題出現了,運行顯示Exception in thread "main" java.lang.NullPointerException錯誤,也就是空指針,在左旋方法中
    指針

    將代碼更改成若是(element.compareTo(tree.left.getElement()) < 0,進行左旋tree = leftLeftRotation(tree);不然進行左右旋tree = leftRightRotation(tree);,這是由於若是添加到左孩子的右孩子位置,符合「對於由樹根左孩子的右子樹中較長路徑而致使的不平衡,須要先讓樹根左孩子的右孩子繞其父結點進行一次左旋,再讓樹根的左孩子繞樹根進行一次右旋。」的定義,參考左右旋的圖示,符合要求,若是用右左旋的話,左子樹的左孩子是空的,並不能將她右旋,因此出現空指針錯誤。調試

代碼託管

上週考試錯題總結

結對及互評

  • 20172305譚鑫:關於無返回值的條件下語句return的做用,也是我不知道但沒提過的,學習了。代碼問題是對做業的解答過程,很詳細。
  • 20172323王禹涵本身總結的知識點很到位,對問題有深刻的去思考,圖片全面,很認真。

其餘

這章內容關於AVL樹和紅黑樹的描述很難理解,狀況太多了,須要進行分類整理並結合圖示,AVL須要主要弄清楚旋轉狀況,紅黑樹要時刻牢記向三個性質轉變。

學習進度條

代碼行數(新增/累積) 博客量(新增/累積) 學習時間(新增/累積)
目標 5000行 30篇 400小時
第一週 0/0 1/1 8/8
第二週 1163/1163 1/2 15/23
第三週 774/1937 1/3 12/50
第四周 3596/5569 2/5 12/62
第五週 3329/8898 2/7 12/74
第六週 4541/13439 3/10 12/86

參考:

相關文章
相關標籤/搜索