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

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

教材學習內容總結

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

  • 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)
    • 右旋:右旋能夠解決樹根左孩子的左子樹中較長的路徑而致使的不平衡
      html

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

    • 左旋:左旋能夠解決樹根右孩子的右子樹中較長的路徑而致使的不平衡
    • 右左旋:對於樹根右孩子的左子樹較長路徑不平衡,讓樹根右孩子的左孩子繞樹根右孩子進行一次右旋,再讓所得樹根右孩子繞着樹根進行一次左旋
    • 左右旋git

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

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

        ① 狀況說明:被插入的結點是根結點。
            處理方法:直接把此結點塗爲黑色。
          ② 狀況說明:被插入的結點的父結點是黑色。
            處理方法:什麼也不須要作。結點被插入後,仍然是紅黑樹。
          ③ 狀況說明:被插入的結點的父結點是紅色。
            處理方法:那麼,該狀況與紅黑樹的特性「從一個結點到該結點的子孫結點的全部路徑上包含相同數目的黑結點。」相沖突。這種狀況下,被插入結點是必定存在非空祖父結點的;進一步的講,被插入結點也必定存在叔叔結點(即便叔叔結點爲空,咱們也視之爲存在,空結點自己就是黑色結點)。理解這點以後,咱們依據"叔叔結點的狀況",將這種狀況進一步劃分爲3種狀況(Case)。
/ 現象說明 處理策略
case1 當前結點的父結點是紅色,且當前結點的祖父結點的另外一個子結點(叔叔結點)也是紅色。 (01) 將「父結點」設爲黑色。(02) 將「叔叔結點」設爲黑色。(03) 將「祖父結點」設爲「紅色」。(04) 將「祖父結點」設爲「當前結點」(紅色結點);即,以後繼續對「當前結點」進行操做。
case2 當前結點的父結點是紅色,叔叔結點是黑色,且當前結點是其父結點的右孩子 (01) 將「父結點」做爲「新的當前結點」。(02) 以「新的當前結點」爲支點進行左旋。
case3 當前結點的父結點是紅色,叔叔結點是黑色,且當前結點是其父結點的左孩子 (01) 將「父結點」設爲「黑色」。(02) 將「祖父結點」設爲「紅色」。(03) 以「祖父結點」爲支點進行右旋。
  • 以上插入狀況的分類是在網上查找到的資料中給出的,教材上給出的紅黑樹的元素插入的分相似乎是有問題的,由於我始終沒法理解爲什麼要判斷當前結點的父結點爲左孩子和右孩子,且爲右孩子時爲什麼要根據兄弟結點的顏色狀況分兩種討論,而爲左孩子時又再也不須要討論了
  • 從新理了一遍教材內容才發現好像左右孩子的狀況都有分清楚。
    手寫整理--當父親顏色爲紅色且父親是右孩子時

當父親是左孩子的時候

兩種運行是對稱的數據結構

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

      設置叔叔的顏色爲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八、20的添加操做和三、4步添加劇復,因此就直接寫結果了

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

  • 問題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的右孩子。以後再從新定義k一、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

結對及互評

  • 博客中值得學習的或問題:
    • 譚鑫這周的博客感受寫的沒前幾周好了,不知道是否是飄了。不過呢,他記錄的代碼問題「在無返回值的條件下語句有return的做用?」卻是對我頗有幫助
    • 方藝雯的博客記錄的很詳細同時很清晰,相比較而言個人博客東西一多就顯得雜亂無章了
  • 基於評分標準,我給譚鑫的博客打分:8分。得分狀況以下:
    正確使用Markdown語法(加1分):
    模板中的要素齊全(加1分)
    教材學習中的問題和解決過程, 一個問題加1分
    代碼調試中的問題和解決過程, 五個問題加5分

  • 基於評分標準,我給方藝雯的博客打分:6分。得分狀況以下:、
    正確使用Markdown語法(加1分):
    模板中的要素齊全(加1分)
    教材學習中的問題和解決過程, 三個問題加3分
    代碼調試中的問題和解決過程, 一個問題加1分

  • 本週結對學習狀況
  • 上週博客互評狀況

其餘

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

學習進度條

代碼行數(新增/累積) 博客量(新增/累積) 學習時間(新增/累積) 重要成長
目標 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

參考資料

相關文章
相關標籤/搜索