天天看點

【化解資料結構】詳解堆結構,并實作最小堆結構

【化解資料結構】詳解堆結構,并實作最小堆結構

大家好,我是小丞同學,一名大二的前端愛好者

這篇文章将講解資料結構中的堆

非常感謝你的閱讀,不對的地方歡迎指正

願你忠于自己,熱愛生活

歡迎大家關注本專欄,持續關注最新文章~

本專欄的其他内容

從這裡開始 【化解資料結構】從這裡開啟資料結構和算法

棧 【化解資料結構】什麼是棧?手寫實作一個棧結構!

隊列 【化解資料結構】詳解隊列,優先隊列,循環隊列,并實作一個隊列

集合 【化解資料結構】詳解集合結構,并實作一個集合

字典 【化解資料結構】詳解字典結構,并實作一個字典

樹 【化解資料結構】詳解樹結構,并實作二叉搜尋樹

知識點搶先看

什麼是堆?

如何實作一個堆結構?

手寫實作一個堆結構

LeetCode 實戰

碎碎念

在上一篇文章中,我們學習了樹結構,它是一個非順序結構,接下來我們再來學習一個非順序結構堆

一、什麼是堆結構?

你可能會知道在記憶體中有棧和堆之分,但是這裡堆和記憶體中的堆不一樣,這裡的堆是一種資料存儲的方式

堆實際上是一種特殊的隊列:優先隊列,關于優先隊列在隊列文章中已經有講過。也就是隊列中有很多待執行的任務,執行時會根據優先級來執行,優先級高的會先被執行

這也可以很容易了解,比如醫院急診室裡就有對病患的優先級之分,醫生會優先處理病情嚴重的患者,再處理相較弱的患者

對于堆而言它是一種抽象的資料結構,或者說邏輯上的資料結構,并不是實體上真實存在的資料結構

在這裡我們主要讨論的是二叉堆這種最常見的結構,它是用一棵完全二叉樹來實作的

對于二叉樹,我們在上一篇也有涉及,它是采用數組來實作的

是以二叉堆實際也是使用數組來實作的

那麼什麼是完全二叉樹呢?

完全二叉樹和滿二叉樹又類似,我們先來看看什麼是滿二叉樹

1. 滿二叉樹

樹中除了葉子節點,每個節點都有兩個子節點

一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。也就是說,如果一個二叉樹的層數為K,且結點總數是(2^k) -1 ,則它就是滿二叉樹。

是以對于滿二叉樹的節點而言,它的度要麼是 0,要麼是 2,也就是要麼有 2 個子節點,要麼是葉子節點

如圖就是一個滿二叉樹

【化解資料結構】詳解堆結構,并實作最小堆結構

2. 完全二叉樹

在滿二叉樹的性質上,最後一層的葉子節點,均在左樹上

若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。

如圖一棵完全二叉樹

【化解資料結構】詳解堆結構,并實作最小堆結構

它們的差別:

完全二叉樹最後一層沒有滿

滿二叉樹一定是完全二叉樹

完全二叉樹不一定是滿二叉樹

3. 堆的特點

好了了解了什麼是完全二叉樹,那堆有什麼特點呢?

堆是一棵完全二叉樹

任意節點都優于它的所有子節點

如果任意節點都大于它的所有子節點,那麼它叫做最大堆,也叫大頂堆

如果任意節點都小于它的所有子節點,那麼它叫做最小堆,也叫小頂堆

【化解資料結構】詳解堆結構,并實作最小堆結構

左邊是一個最大堆,所有的子節點都小于父節點

二、如何能夠實作一個堆結構呢?

在 JS 中通過數組來實作一個堆結構,其實本質就是一個數組。在上一篇文章結尾也說了,無論什麼資料結構,在記憶體中都隻是數組,或者對象罷了,所有的資料結構都是我們心中存在的,我們知道這麼做的好處是怎麼怎麼樣

在這裡選用數組來實作一個堆

利用廣度優先周遊,将樹填入數組裡,這樣我們就能用一個數組來表示一個堆了

【化解資料結構】詳解堆結構,并實作最小堆結構

小秘訣

左側子節點在數組中的位置是 2 * index + 1

右側子節點在數組中的位置是 2 * index + 2

父節點的位置是 (index - 1) / 2

是以我們不僅能夠使用數組來表示一個堆,我們還能擷取任意一個節點在數組中的位置,接下來我們就實作一個最小堆

三、堆中有哪些方法?

我們給堆添加一些方法,一遍它在插入時,能插到準确的位置,删除時,其他的元素也能進行合理的移動

方法 含義

swap() 交換兩個數

getParentIndex(i) 擷取 i 的父節點

getLeftIndex(i) 擷取 i 的左子節點

getRightIndex(i) 擷取 i 的右子節點

shirtUp(i) 上移操作

shirtDown(i) 下移操作

insert(value) 插入值

pop() 删除堆頂

peek() 擷取堆頂

size() 擷取堆的大小

四、手寫實作一個最小堆

在前面我們已經知道了最小堆的定義,它的所有節點都小于等于它的子節點,是以我們根據這個特性,以及3個小秘訣來實作一個最小堆

1. 建立一個 MinHeap 類

利用數組來實作一個堆類

class MinHeap {
    constructor() {
        this.heap = []
    }
}      

2. 實作 swap 方法

我們需要維護一個堆結構,在元素插入删除的時候,常常需要進行位置的變化,是以我們需要通過交換位置來實作

封裝一個 swap 方法,接收交換位置的兩個節點

swap(i1, i2) {
    const temp = this.heap[i1]
    [this.heap[i1], this.heap[i2]] = [this.heap[i2], temp]
}      

在這裡采用數組解構的方式來指派,看着舒服一點

3. 實作 getParentIndex 方法

getParentIndex 方法擷取某個節點父元素在數組中的位置

根據上面的小秘訣:父節點的位置是 (index - 1) / 2

在這裡我們采用二進制的方式來取值

小課堂:你知道 JavaScript 中的 ~~ 運算符是什麼意思嗎

getParentIndex(i) {
    // 取商 (i- 1)/2 等同于 Math.floor((i-1)/2)
    // 二進制數向右邊移一位,這樣剛好就是求商
    return (i - 1) >> 1
}      

4. 實作 getLeftIndex 方法

同樣的根據秘訣:左側子節點在數組中的位置是 2 * index + 1

getLeftIndex(i) {
    return i * 2 + 1
}      

5. 實作 getRightIndex 方法

getRightIndex(i) {
    return i * 2 + 2
}      

6. 實作 shirtUp 方法

這個方法是實作最小堆的關鍵之一,在我們插入元素時,需要對元素進行判斷,我們需要将插入的元素移到符合它的位置

如何實作呢?采用遞歸

首先我們需要先判斷節點的位置是否在堆的頂部,這也是遞歸結束的标記之一

接下來進行遞歸體的内容,我們遞歸實作的目的是通過交換使元素到達合适位置

是以判斷插入元素和父節點的值關系,如果父節點的值大于目前節點值,則進行上移(因為最小堆,小的在堆頂)

直至遞歸結束

shirtUp(index) {
    // 如果在堆頂,停止上移
    if(index == 0) return
    // 擷取父元素
    const parentIndex = this.getParentIndex(index)
    // 比較
    if (this.heap[parentIndex] > this.heap[index]) {
        // 交換
        this.swap(parentIndex, index)
        // 遞歸
        this.shirtUp(parentIndex)
    }
}      

7. 實作 insert方法

在寫好了上移 shirtUp 方法,我們就可以實作 insert 方法來看看我們實作的效果了

insert 方法的作用是插入一個元素,在堆中插入一個元素之後,我們需要通過 shirtUp 方法來将這個元素移到合适的位置,這個操作留給 shirtUp 方法來解決

注意哦,shirtUp 方法接收的是 index ,也就是索引值

insert(value) {
    this.heap.push(value)
    this.shirtUp(this.heap.length - 1)
}      

來看看在一個堆中插入元素是如何運作的吧,這是一個最大堆中的動圖,最小堆也一樣

【化解資料結構】詳解堆結構,并實作最小堆結構

時間複雜度是多少你知道嗎? O(logK)

8. 實作 pop 方法

為什麼需要有下移的方法,當我們直接删除堆頂時,會導緻整個堆的結構的變化,使得大小關系轉變,難以操作

是以我們在删除堆頂時,隻需要用數組尾部的元素,替換堆頂元素,這樣改變的就隻有首尾兩個元素,我們再對堆頂進行下移判斷,這樣通過不斷地交換,就能實作最小堆

pop() {
    // 用最後一個替換堆頂
    this.heap[0] = this.heap.pop()
    // 再下移
    this.shirtDown(0)
}      

9. 實作 shirtDown 方法

接下來我們實作最為關鍵的下移代碼,如何實作呢?

和左右子節點進行比較

左子節點小于目前節點,交換,繼續遞歸

右子節點小于目前節點,交換,遞歸

shirtDown(index) {
    const leftIndex = this.getLeftIndex(index)
    const rightIndex = this.getRightIndex(index)
    // 左側子節點小于目前節點
    if (this.heap[leftIndex] < this.heap[index]) {
        this.swap(leftIndex, index)
        this.shirtDown(leftIndex)
    }
    // 右側子節點小于目前節點
    if (this.heap[rightIndex] < this.heap[index]) {
        this.swap(rightIndex, index)
        this.shirtDown(rightIndex)
    }
}      

我們來看看删除堆頂時會發生什麼?

【化解資料結構】詳解堆結構,并實作最小堆結構

10. 實作 peek 方法

傳回堆頂元素,也就是堆的最小值,數組的第一位

peek() {
    return this.heap[0]
}      

11. 實作 size 方法

最後,實作最簡單的方法,通過數組的 length 來擷取即可

size() {
    return this.heap.length
}      

12. 完整的 MinHeap 類

// 寫一個最小堆
class MinHeap {
    constructor() {
        this.heap = []
    }
    // 擷取父節點
    getParentIndex(i) {
        // 取商 (i- 1)/2 等同于 Math.floor((i-1)/2)
        // 二進制數向右邊移一位,這樣剛好就是求商
        return (i - 1) >> 1
    }
    // 擷取左節點
    getLeftIndex(i) {
        return i * 2 + 1
    }
    getRightIndex(i) {
        return i * 2 + 2
    }
    // 交換兩個數的方法
    swap(i1, i2) {
        const temp = this.heap[i1]
        [this.heap[i1], this.heap[i2]] = [this.heap[i2], temp]
    }
    // 上移操作,最小堆,小的要在最上面
    shirtUp(index) {
        // 如果在堆頂,停止上移
        if (index == 0) return
        const parentIndex = this.getParentIndex(index)
        if (this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index)
            this.shirtUp(parentIndex)
        }
    }
    // 下移操作
    shirtDown(index) {
        const leftIndex = this.getLeftIndex(index)
        const rightIndex = this.getRightIndex(index)
        // 左側子節點小于目前節點
        if (this.heap[leftIndex] < this.heap[index]) {
            this.swap(leftIndex, index)
            this.shirtDown(leftIndex)
        }
        // 右側子節點小于目前節點
        if (this.heap[rightIndex] < this.heap[index]) {
            this.swap(rightIndex, index)
            this.shirtDown(rightIndex)
        }
    }
    // 插入 O(logK)
    insert(value) {
        this.heap.push(value)
        this.shirtUp(this.heap.length - 1)
    }
    // 删除堆頂
    pop() {
        // 用最後一個替換堆頂
        this.heap[0] = this.heap.pop()
        // 再下移
        this.shirtDown(0)
    }
    // 擷取堆頂
    peek() {
        return this.heap[0]
    }
    // 擷取大小
    size() {
        return this.heap.length
    }
}      

五、LeetCode 實戰

在前端世界中,堆也有它的應用場景,它能夠高效的找到最大值,最小值,時間複雜度為 O(1),

利用堆結構,我們可以輕松解決找出最大、最小元素、第 K 大元素登問題,但遠不止于這些

幾道 LeetCode 中關于堆的題目

繼續閱讀