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

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

教材學習內容總結

  • 堆:是一顆完全二叉樹,每一個結點都小於等於左右孩子(最小堆),或大於等於左右孩子(最大堆)他的每一個子樹也是最小堆或最大堆。
  • 堆的操作
  • addElement操作(堆的插入)
    • 插入新結點時,是作為葉子結點插入的,且一定要保持樹的完整性。如果最下層不滿,插入到左邊的下一個空位,如果最底層是滿的,插入到新一層的左邊第一個位置。
    • 如果插入的數相對樹而言較小,則需要在插入後進行重排序
  • removeMin方法(堆的刪除)
    • 由於最小元素儲存在根處,為了維持樹的完整性,那麼只有一個能替換根的合法元素,且它是儲存在樹中最末一片葉子上的元素(將是樹中h層上最右邊的葉子)。
    • 將替換元素移到根部後,必須對該堆進行重排序。
  • findMin操作
    • 這個方法需要返回在最小堆中最小元素的引用,由於最小堆中最小元素在根處,所以只需返回根部元素即可。

使用堆:優先順序佇列

  • 具有更高優先順序的專案在先。
  • 具有相同優先順序的專案使用先進先出的方法來確定其排序。
  • 雖然最小堆不是一個佇列,但它提供了一個高效的優先順序佇列實現。

用連結串列實現堆

  • 由於要求插入元素後能夠向上遍歷樹,所以堆結點中必須儲存指向雙親的指標。
  • 由於BinaryTreeNode類沒有雙親指標,所以要建立一個HeapNode類開始連結串列實現。
    • addElement操作
      • 在適當位置新增一個新元素
      • 對堆進行重排序以維持其排序屬性
      • 將lastNode指標重新設定為指向新的最末結點
      • 在這個方法中用到了getNextParentAdd方法(返回指向某結點的引用,該結點為插入結點的雙親),還有一個heapifyAdd方法,(完成對堆的任何重排序,從新葉子開始向上處理至根處)
      • addElement操作有三步
        • 首先要確定插入結點的雙親(從堆的右下結點往上遍歷到根,再往下遍歷到左下結點,時間複雜度為2*logn)
        • 其次是插入新結點(只涉及簡單的賦值語句,時間複雜度為O(1))
        • 最後是從被插入葉子處到根處的路徑重排序,來保證堆的性質不被破壞(路徑長度為logn,最多需要logn次比較)
        • 因此addElement操作的複雜度為2*logn+1+logn,即為O(logn)
    • removeMin操作
      • 用最末結點處的元素替換根處元素
      • 對堆進行重排序
      • 返回初始的根元素

用陣列實現堆

  • 陣列相比於連結串列,優點是不需要遍歷來查詢最末一片葉子或下一個插入結點的雙親,直接檢視陣列的最末一個元素即可。
  • 在陣列中,樹根存放在0處,對於每一個結點n,n的左孩子位於陣列2n+1處,n的右孩子位於陣列的2(n+1)處。同樣,反過來查詢時,對於任何除了根之外的結點,n的雙親位於(n-1)/2位置處。
  • addElement操作
    • 在恰當位置處新增新結點
    • 對堆進行重排序以維持其屬性
    • 將count遞增1
    • 只需要一個heapifyAdd方法進行重排序,不需要重新確定新結點雙親位置。此方法的時間複雜度是O(logn)
  • removeMin操作
    • 用最末元素替換根元素
    • 對堆進行重排序
    • 返回初始根元素
    • 由於末結點儲存在陣列的count-1位置處,所以不需要確定最新末結點的方法,此方法複雜度為O(logn)
  • findMin操作
    僅僅返回根處元素即可,就是陣列的位置0處,複雜度為O(1)

使用堆:堆排序

  • 堆排序的過程(由兩部分構成,新增和刪除)
    • 將列表的每一元素新增到堆中
    • 一次一個將他們從根刪除,再次存放到陣列中
    • 最小堆時得到的排序結果是升序,最大堆時得到的排序結果是降序
    • 對於每一個結點來說,新增和刪除操作的複雜度都是O(logn),所以對於有n個結點的堆來說,複雜度是O(nlogn)
  • 堆排序的複雜度具體分析
    • 堆化過程一般是用父節點和他的孩子節點進行比較,取最大的孩子節點和其進行交換;但是要注意這應該是個逆序的,先排序好子樹的順序,然後再一步步往上,到排序根節點上。然後又相反(因為根節點也可能是很小的)的,從根節點往子樹上排序。最後才能把所有元素排序好。對於每個非葉子的結點,最多進行兩次比較操作(與兩個孩子)因此複雜度為2*n
    • 排序過程就是把根元素從堆中刪除,刪除每個元素複雜度為logn,對所有元素來說,複雜度為nlogn。
    • 所以是2*n + nlogn,複雜度是O(nlogn)

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

  • 問題一:getNextParentAdd方法的程式碼理解
  • 問題一解決:
    * 第一種情況

    首先考慮while迴圈,result為8,他是左孩子,所以不進入while迴圈,接著往下,他不是根結點,他不是父結點的右孩子,所以result為父結點5,即返回的插入結點的雙親。
    * 第二種情況

    result為0,雙親結點的左孩子為-2,所以進入while迴圈,result為3,接下來進入if語句,他不是根結點,而且父結點的右孩子(1)不為空,所以返回的插入結點的雙親結點是1
    * 第三種情況

    while迴圈中,result(2)不是左孩子,所以result為雙親結點1,往下,他是根結點,而且左子樹不為空,result為左孩子0.這種情況就相當於滿樹時插入到新一層的左邊位置。

    private HeapNode<T> getNextParentAdd() {
        HeapNode<T> result = lastNode;// 當前的空白結點
        while ((result != root) && (result.getParent().getLeft() != result))//如果當前不是一個滿堆,他不是左孩子
            result = result.getParent();
        if (result != root) {
            // /不是一個滿堆
            if (result.getParent().getRight() == null)//右邊是空位
                result = result.getParent();
            else {//右邊不空
                result = (HeapNode<T>) result.getParent().getRight();
                // 不斷的向下尋找最後的父結點
                while (result.getLeft() != null)
                    result = (HeapNode<T>) result.getLeft();
            }
        } else {// 當前堆是一個滿堆
            while (result.getLeft() != null)
                result = (HeapNode<T>) result.getLeft();
        }
        return result;
    }
  • 問題二:heapifyAdd方法的程式碼理解
  • 問題二解決:舉個例子,

    private void heapifyAdd() {
        T temp;
        HeapNode<T> next = lastNode;
        temp = next.getElement();
        while ((next != root)
                && (((Comparable) temp)             .compareTo(next.getParent().getElement()) < 0)) {
            next.setElement(next.getParent().getElement());
            next = next.parent;
        }
        next.setElement(temp);
    }


    如圖在樹中插入結點2,那麼next指向2,temp=2,這時next不是根結點,並且2(temp)比4(next.getParent().getElement())小,所以現在next的值設定為4(即原來2的那個位置變成4),next指向原來next的父親結點(即原來的4位置),然後將next的值設為temp即2(原來4的位置變為2),這樣就將2和4換位,實現了重排序。

  • 問題三:堆排序的過程
  • 問題三解決:首先把待排序的元素在二叉樹位置上排列,然後調整為大頂堆或小頂堆,接下來進行排序,以小頂堆為例,根結點為最小,所以拿掉根,然後對堆進行調整,使其符合小頂堆的要求,然後新的堆頂又是最小的,將其拿掉,如此迴圈...藍墨雲關於這個的測試題,重新梳理一下如圖,大頂堆的過程與其類似,但注意在程式碼實現中,小頂堆排序結果為升序,大頂堆排序結果為降序

  • 問題四:對於優先順序佇列的理解
  • 問題四解決:佇列是一種特徵為FIFO的資料結構,每次從佇列中取出的是最早加入佇列中的元素。但是,許多應用需要另一種佇列,每次從佇列中取出的應是具有最高優先權的元素,這種佇列就是優先順序佇列。優先順序佇列的特點:
    • 優先順序佇列是0個或多個元素的集合,每個元素都有一個優先權或值。
    • 當給每個元素分配一個數字來標記其優先順序時,可設較小的數字具有較高的優先順序,這樣更方便地在一個集合中訪問優先順序最高的元素,並對其進行查詢和刪除操作。
    • 在最小優先順序佇列(min Priority Queue)中,查詢操作用來搜尋優先順序最小的元素,刪除操作用來刪除該元素。在最大優先順序佇列(max Priority Queue)中,查詢操作用來搜尋優先順序最大的元素,刪除操作用來刪除該元素。
    • 每個元素的優先順序根據問題的要求而定。當從優先順序佇列中刪除一個元素時,可能出現多個元素具有相同的優先權。在這種情況下,把這些具有相同優先權的元素按先進先出處理。

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

  • 問題一:在做藍墨雲測試構造大頂堆並排序時,對於過程的輸出結果是每次將最大數從堆頂拿下時的結果,並不是顯示每次排序時的陣列情況。

  • 問題一解決:主要錯誤之處在於測試類,如圖中顯示,我的過程記錄是

    for(int i=0;i<8;i++){
        array2.addElement(array.removeMax);
        array2.removeMax();
        System.out.println("第"+i+"次為:"+ array2);
    }

    首先,在迴圈中array中每次拿掉的最大數新增到array2中,然後輸出的是array2中的最大數,那麼就相當於每次把array中的最大數輸出,array2沒有什麼作用。應該為

    for (int i = 0; i <a.length; i++) {
            array2[i]=array.removeMax();
            System.out.println("第"+ i +"次為:"+ array);
        }
    這樣就把array2的作用發揮出來了,array2中存放array中依次彈出的最大數,在array2中降序排列,然後每次輸出array陣列,顯示現在陣列情況,排序的結果就可以用類似上面的for迴圈依次將array2中的數輸出。
  • 問題二:在做PP12.1時,對於優先順序佇列的輸出情況理解有問題。
  • 問題二解決:測試類通過在迴圈中使用queue.removeNext()方法,是由於PriorityQueue類中removeNext方法繼承了ArrayHeap類中的removeMin方法,而在ArrayHeap類中,removeMin又使用了方法heapModifyRemove,其中

    else if (((Comparable)tree[left]).compareTo(tree[right]) < 0)
            next = left

    當left比right小時,返回左,我最開始不理解的是在優先順序佇列中,當優先順序相同時

    if (arrivalOrder >obj.getArrivalOrder())//優先順序相同時,先進先出
          result = 1;

    應返回左邊的arrivalOrder,但在heapModifyRemove中來說,卻返回compareTo右邊的,所以我覺得矛盾,進行多次測試後我發現輸出的結果沒有錯。又仔細看程式碼之後發現,優先順序相同時,返回1的情況是arrivalOrder較大,那麼就代表它是後進的,並不是跟優先順序一樣大的先,arrivalOrder越大代表晚進,應該後出,所以正好反一下。

  • 問題三:實現ArrayHeap類時,發現輸出結果總是多一個數

  • 問題三解決:參考了譚鑫同學的部落格,裡面提到了解決辦法,如圖

    新增語句之後解決問題,原因是在將根結點拿掉並用末葉子結點替換後並沒有將葉結點刪除,所以導致出現兩次,新增語句tree[count-1] = null;之後,將其值設定為空,相當於刪除。

程式碼託管

上週考試錯題總結

  • 錯題一:

  • 錯題一解決:CompareTo方法返回值是int型的,如果前者比後者大的話返回1,小的話返回-1,相等返回0.

結對及互評

  • 20172305譚鑫:譚鑫的部落格中提到的”問題1:實現優先順序佇列的二叉堆、d堆、左式堆、斜堆、二項堆都是什麼意思?和最小堆、最大堆有什麼區別?”是我沒有學習過的,學習到了左式堆,二項堆,斜堆等堆。同時參考他的部落格解決了一個程式碼問題,非常有價值。
  • 20172323王禹涵:王禹涵的部落格對程式碼的解釋很詳盡,排版精美,很舒適。

其他

這一章感覺原理很容易理解,邏輯簡單,但用程式碼實現不好理解,但是書上程式碼的可用性很高,然後實驗方面也花費較長時間,總結出來就是要儘早去掌握核心程式碼再開展學習。

學習進度條

程式碼行數(新增/累積) 部落格量(新增/累積) 學習時間(新增/累積)
目標 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
第七週 1740/15179 1/11 12/97
第八週 5947/21126 1/12 12/109

參考: