五分鍾帶你讀懂 堆 —— heap(內含JavaScript代碼實現!!)


一、概念

 說起堆,我們就想起了土堆,把土堆起來,當我們要用土的時候,首先用到最上面的土。類似地,堆其實是一種優先隊列,按照某種優先級將數字“堆”起來,每次取得時候從堆頂取。

 堆是一顆完全二叉樹,其特點有如下幾點:
 1.可以使用一維數組來表示。其中,第i個節點的父節點、子節點index如下:
  - parent(i) = (i - 1) / 2; (取整)
  - leftChild(i) = i * 2 + 1;
  - rightChild(i) = i * 2 + 2;
 2.堆中某個節點的值總是不大於(最小堆)或不小於(最大堆)其父節點的值
注1:為什么可以用一維數組來表示堆,因為堆是一顆完全二叉樹,可以通過下標計算獲得其父子節點的下標
注2:除了根節點外,其它節點的排序是未知的。(也就是說我們只知道最大or最小值是根節點的值,不知道其它節點的順序)

二、上浮(shiftUp)和下沉(shiftDown)

1. 上浮(shiftUp)(以構建最小堆為例)

上浮操作就是,如果一個節點比它父節點小,則將它與父節點交換位置。這個節點在數組中的位置上升。

2. 下沉(shiftDown)

下沉操作就是,如果一個節點比它子節點大,那么它需要向下移動。稱之為“下沉”。

三、堆的構建

給定一個一維數組[4,1,3,2,16,9,10,14,8,7],怎么把它構建成一個堆呢?使用自底向上的方法將數組轉化為堆。
大體思路為:如果每一個節點都是一個堆(以這個節點為根),那么這個數組肯定是一個堆。自底向上的方法意思就是,自底向上依次調整每個節點,使得以該節點為根的樹是一個堆。
(以最大堆為例)

  1. 首先將數組還原成一個完全二叉樹
    image
  2. 從倒數第一個非葉子節點開始(i = (n/2)-1,其中,n是數組的長度),依次對每個節點執行步驟3,將其調整成最大堆,直至第0個節點。
    注:這里要說一下為啥從第一個非葉子節點開始,因為葉節點沒有孩子節點,可以把它看成只有一個元素的堆。
  3. 將以第i個節點為根節點的子樹調整為最大堆。假設根節點A[i]的左右子樹的根節點index分別為left[i]和right[i],其中,left[i]和right[i]都是最大堆。采用逐級下降的方法進行調整,具體步驟如下:
    (1) 從A[i]、A[left[i]]、A[right[i]]中找到最大的值,將其下標存到largest中。如果A[i]是最大的,那么以i為根節點的子樹是最大堆;否則,交換A[i]和A[largest]的值。
    (2) 對下標為largest為根的子樹重復(1)操作,直至largest為葉子節點。

時間復雜度:O(n)
 (此推到過程以后有時間了再說,可以先記一下)

如圖所示:

  • 從 i = 4 開始遍歷,此時,A[4] = 16,它是一個最大堆;
  • i = 3,A[3] = 2,與A[7]交換,因為i = 7是葉子節點,所以調整完畢。
  • i = 2,A[2] = 3,與A[6]交換,因為i = 6是葉子節點,所以調整完畢。此時,該樹變成:
    image
  • i = 1,A[1] = 1,與A[4]交換。因為i = 4不是葉子節點,所以要對以4為根的子樹進行上述操作;此時,A[4] = 1,與A[9]交換,i = 9是葉子節點,調整完畢。如下圖所示:
    image
  • i = 0,A[0] = 4,與A[1]交換。因為i = 1不是葉子節點,對以1為根的子樹進行上述操作;此時,A[1] = 4,與A[3]交換,i = 3不是葉子節點,對以3為根的子樹重復進行操作;此時A[3] = 4,與A[8]交換,i = 8是葉子節點,調整完畢。最終得到最大堆:
    image

四、堆的其它方法

1.插入

向數組最后插入一個數據,然后再向上調整。還以上述數組為例,插入一個11。

  • 在數組最后插入11
    image
  • 比較該節點與其父節點的大小,11 > 7,交換兩者,進行上浮。重復該步驟,11 < 14,此時滿足最大堆性質,插入完畢。
    image

時間復雜度:O(logn)

2.刪除(指刪除堆頂的數據)

交換堆頂和最后一個數據,刪除最后一個數據,再對堆頂數據進行向下調整算法。

  • 交換堆頂和最后一個數據,並刪除最后一個數據。
    image
  • 堆根節點進行下沉操作:對比該節點和其左右子節點,將該節點與最大的節點進行交換。重復此操作,直至該節點為葉子節點。(因為這個節點原本就是葉子節點交換上去的,比上面所有層的節點都小,所以調整完畢后,這個節點一定仍是葉子節點)
    • 1與14交換
    • 1與8交換
    • 1與4交換
      image

時間復雜度:O(logn)

五、堆排序

基本思路:

  • 將輸入的數組建成最大堆。此時,堆頂元素就是最大值。
  • 交換堆頂元素和末尾元素。此時,末尾元素是最大值。
  • 將剩下 n-1 個元素重新構造成一個堆。重復執行上述步驟。

舉個簡單的例子:[14, 8, 10, 4]

  1. 將該數組構建成最大堆
    image
  2. 交換堆頂元素和末尾元素。
    image
  3. 忽略末尾元素,將剩下的元素利用下沉法重新構造為一個最大堆。
    image
  4. 重復以上步驟,直至剩下的元素只剩下一個。最終得到如下結果,排序完畢:
    image

時間復雜度:O(nlogn)

六、堆的應用

1. 優先隊列(先填個坑,回頭補上)

2. 求Top K

不管是面試還是啥,每次問到求Top K的問題,我們第一反應就是利用堆,但是怎么用呢?
Top K問題可抽象成兩類,一類是針對靜態數據集合,即數據是事先確定的;一類是針對動態數據集合,即數據動態地加入到集合中。

針對靜態集合

維護一個大小為K的小頂堆,順序遍歷數組,從數組中取出數據與堆頂元素比較。如果比堆頂元素大,則把堆頂元素刪除,並把該元素插入堆中;反之則不做處理,繼續遍歷數組。數組遍歷完畢后,堆中的數據就是前K大數據了。
時間復雜度:O(nlogK)

針對動態集合

其實思路與靜態集合大體一致,只不過靜態集合是遍歷數組,將數組中的元素與堆頂比較然后然后進行一系列操作;而動態集合是每加入一個元素,將該元素與堆頂比較,然后進行操作。
時間復雜度:O(logK)

七、JavaScript實現堆類

js中並沒有“堆”這個數據結構,只能手動實現,以下是源代碼。

class MaxHeap {
    constructor(heap) {
        this.heap = heap;
        this.heapSize = heap.length;
        this.buildMaxHeap();
    }

    // 構建最大堆
    buildMaxHeap() {
        for (let i = Math.floor(this.heapSize / 2) - 1; i >= 0; i--) {
            this.maxHeapify(i);
        }
    }

    //將以i為根節點的子樹調整為最大堆
    maxHeapify(index) {
        let left = 2 * index + 1;
        let right = 2 * index + 2;
        let largest = index;
        if (left < this.heapSize && this.heap[left] > this.heap[largest]) largest = left;
        if (right < this.heapSize && this.heap[right] > this.heap[largest]) largest = right;
        if (largest !== index) {
            this.swapNum(index, largest);
            this.maxHeapify(largest);
        }
    }

    //交換i,j的值
    swapNum(i, j) {
        let temp = this.heap[i];
        this.heap[i] = this.heap[j];
        this.heap[j] = temp;
    }

    //插入一個數
    insert(num) {
        this.heap.push(num);
        this.heap.heapSize = this.heap.length;
        let index = this.heap.heapSize - 1;
        while (index != -1) {
            index = this.shiftUp(index);
        }
        console.log(this.heap);
    }

    //刪除堆頂元素
    pop() {
        this.swapNum(0, this.heapSize - 1);
        this.heap.pop();
        this.heapSize = this.heap.length;
        let index = 0;
        while (1) {
            let temp = this.shiftDown(index);
            if (temp === index) break;
            else index = temp;
        }
    }

    //堆排序
    heapSort() {
        while (this.heapSize > 1) {
            this.swapNum(0, this.heapSize - 1);
            this.heapSize -= 1;
            let index = 0;
            while (1) {
                let temp = this.shiftDown(index);
                if (temp === index) break;
                else index = temp;
            }
        }
        this.heapSize = this.heap.length;
    }

    //上浮操作 - 將當前節點與父節點進行比較,如果該節點值大於父節點值,則進行交換。
    shiftUp(index) {
        let parent = Math.ceil(index / 2) - 1;
        if (this.heap[index] > this.heap[parent] && parent >= 0) {
            this.swapNum(index, parent);
            return parent;
        }
        return -1;
    }

    // 下沉操作 - 將當前節點與左右子節點進行比較,如果該節點值不是最大,則進行交換
    shiftDown(index) {
        let left = Math.floor(index * 2) + 1;
        let right = left + 1;
        let largest = index;
        if (left < this.heapSize && this.heap[left] > this.heap[largest]) largest = left;
        if (right < this.heapSize && this.heap[right] > this.heap[largest]) largest = right;
        if (largest !== index) {
            this.swapNum(index, largest);
        }
        return largest;
    }

}

八、參考文章:

  1. 《算法導論第3版》
  2. https://blog.csdn.net/qq_41754573/article/details/103371579
  3. https://www.jianshu.com/p/6b526aa481b1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM