資料結構 - Heap
- 文章發表於
Heap 資料結構
Heap 是一種特殊的樹狀資料結構,該結構必須符合兩個條件:
- 完全二元樹 (Complete Binary Tree)
- 節點的值必須大於等於 (maxHeap) 或小於等於 (minHeap) 其子節點的值
舉 minHeap 來說
- 第一張圖只有一個節點,兩者皆符合
- 第二張圖則是一個完全二元樹,節點也都小於等於其子節點,也符合 minHeap 的條件
- 第三張圖儘管節點都小於等於其子節點,但並不是完全二元樹,因此不符合 minHeap 的條件
✅ | ✅ | ❌ |
---|---|---|
API
class Heap {constructor() {}/** get the top element */extractRoot() {}/** insert a new element */insert() {}/** get the size of heap */size() {}/** peek the top element */peek() {}}
constructor
Heap 的 constructor 一開始需要放入比較函式,在每次 Heapify (往上移動) 的過程中都會透過該比較函式來決定是否要交換節點的位置,比較函式的定義如下:
class Heap {constructor(compare) {if (typeof compare !== 'function') {throw new Error('Heap constructor expects a compare function')}// 比較函式this._compare = compare// 用 Array 作為 Heap 的儲存結構this._nodes = []// 儲存 min heap 最大的節點this._leaf = null}}
Insert
首先我們先來看如何插入一個值到 Heap 中,假設我們要插入 3
,首先會將 3
插入到 Heap 的最後一個位置,然後再進行 Heapify, 而 Heapify 的過程就是將 3
與其母節點進行比較,如果 3
比母節點小,則將其交換位置,直到 3
比母節點大或是已經到達根節點為止。
pesudo code
class Heap {constructor() {}insert(value) {// 插入到最後一個位置this.nodes.push(value)// 將該值進行 Heapifythis.heapifyUp(this.size() - 1)// 儲存 min heap 最大的節點if (this._compare(value, this.leaf) > 0) {this.leaf = value}}}
視覺化
實作
實踐 insert
方法,首先我們先來看看如何實作 heapifyUp,heapifyUp 的過程就是將最新插入的節點與其母節點進行比較,如果該節點比母節點小,則將其交換位置,直到最新插入的節點比母節點大或是已經到達根節點為止。
- 找出最新插入的節點 (
this._nodes[this.size() - 1]
) 以及其母節點 (Math.floor((childIndex - 1) / 2)
)
class Heap {heapifyUp(idx) {// Step 1let childIndex = idxlet parentIndex = Math.floor((childIndex - 1) / 2)// TODO: Step 2// ...}}
- 比較兩個節點的值
class Heap {_compareAt(i, j) {return this._compare(this._nodes[i], this._nodes[j])}_shouldSwap(parentIndex, childIndex) {return this._compareAt(parentIndex, childIndex) > 0}heapifyUp(idx) {// Step 1let childIndex = idxlet parentIndex = Math.floor((childIndex - 1) / 2)// Step 2while (this._shouldSwap(parentIndex, childIndex)) {// TODO: Step 3}}}
- 如果最新插入的節點比母節點小,則交換位置,並且繼續往上比較,直到最新插入的節點比母節點大或是已經到達根節點為止
class Heap {heapifyUp(idx) {// Step 1let childIndex = idxlet parentIndex = Math.floor((childIndex - 1) / 2)// Step 2while (this._shouldSwap(parentIndex, childIndex)) {// Step 3this._swap(parentIndex, childIndex)childIndex = parentIndexparentIndex = Math.floor((childIndex - 1) / 2)}}}
最後則是將 minHeap insert
方法實作出來,首先我們先將新的節點插入到最後一個位置,然後再進行 heapifyUp 的過程,並記錄該 minHeap 最大的節點
class Heap {insert(value) {this._nodes.push(value)this._heapifyUp(this.size() - 1)if (this._leaf === null || this._compare(value, this._leaf) > 0) {this._leaf = value}}}
const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end('Hello world'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
extractRoot
再來看看我們如何從 minHeap 中取出最小的節點,首先我們會先將根 (root) 節點取出,然後再將最後一個節點放到根節點的位置,接著再進行 heapifyDown 的過程,而 heapifyDown 的過程就是將根節點與其子節點進行比較,如果該節點比子節點大,則將其交換位置,直到該節點比子節點小或是已經到達葉節點為止。
視覺化
實作
實作 extractRoot
我們可以分成兩個步驟:
- 將根節點取出,並將最後一個節點放到根節點的位置
class Heap {// ...extractRoot() {if (this.size() === 0) {return null}// Step 1const root = this._nodes[0]this._nodes[0] = this._nodes[this.size() - 1]this._nodes.pop()// TODO: Step 2}// ...}
- 將根節點與其子節點進行比較,如果該節點比子節點大,則將其交換位置,直到該節點比子節點小或是已經到達葉節點為止
class Heap {// ..._hasLeftChild(parentIndex) {let leftChildIndex = parentIndex * 2 + 1return leftChildIndex < this.size()}_hasRightChild(parentIndex) {let rightChildIndex = parentIndex * 2 + 2return rightChildIndex < this.size()}_compareChildrenOf(parentIndex) {if (!this._hasLeftChild(parentIndex) && !this._hasRightChild(parentIndex)) {return null}const leftChildIndex = parentIndex * 2 + 1const rightChildIndex = parentIndex * 2 + 2if (!this._hasLeftChild(parentIndex)) {return rightChildIndex}if (!this._hasRightChild(parentIndex)) {return leftChildIndex}return this._compareAt(leftChildIndex, rightChildIndex) > 0 ? rightChildIndex : leftChildIndex}_heapifyDown(idx) {let parentIndex = idxlet childIndex = this._compareChildrenOf(parentIndex)while (this._shouldSwap(parentIndex, childIndex)) {this._swap(parentIndex, childIndex)parentIndex = childIndexchildIndex = this._compareChildrenOf(parentIndex)}}extractRoot() {if (this.size() === 0) {return null}// Step 1const root = this._nodes[0]this._nodes[0] = this._nodes[this.size() - 1]this._nodes.pop()// Step 2this._heapifyDown(0)if (root === this._leaf) {this._leaf = this._nodes[0]}return root}// ...}
const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end('Hello world'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
總結
最後 Heap 的時間複雜度與空間複雜度如下:
Insert 的操作是在 Array 的最後插入,在進行 heapifyUp
的操作。而 heapifyUp
最壞情況下需要遍歷從末尾到根的路徑,其長度為樹的高度,因此時間複雜度為 O(log n)。
同理 extractRoot 的操作是將根與最後一個元素交換,再進行 heapifyDown
的操作,最壞情況下也需要遍歷從根到葉子的路徑,其長度為樹的高度,因此時間複雜度也為 O(log n)。
而 peekRoot 的操作只是取得根的值,因此時間複雜度為 O(1)。
操作 | 時間複雜度 | 空間複雜度 |
---|---|---|
insert | O(log n) | O(n) |
extractRoot | O(log n) | O(n) |
peekRoot | O(1) | O(1) |
下一篇,我們將介紹如何使用 Heap 來實作 Priority Queue。