一、概念
說起堆,我們就想起了土堆,把土堆起來,當我們要用土的時候,首先用到最上面的土。類似地,堆其實是一種優先隊列,按照某種優先級將數字“堆”起來,每次取得時候從堆頂取。
堆是一顆完全二叉樹,其特點有如下幾點:
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],怎么把它構建成一個堆呢?使用自底向上的方法將數組轉化為堆。
大體思路為:如果每一個節點都是一個堆(以這個節點為根),那么這個數組肯定是一個堆。自底向上的方法意思就是,自底向上依次調整每個節點,使得以該節點為根的樹是一個堆。
(以最大堆為例)
- 首先將數組還原成一個完全二叉樹
、 - 從倒數第一個非葉子節點開始(i = (n/2)-1,其中,n是數組的長度),依次對每個節點執行步驟3,將其調整成最大堆,直至第0個節點。
注:這里要說一下為啥從第一個非葉子節點開始,因為葉節點沒有孩子節點,可以把它看成只有一個元素的堆。 - 將以第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是葉子節點,所以調整完畢。此時,該樹變成:

- i = 1,A[1] = 1,與A[4]交換。因為i = 4不是葉子節點,所以要對以4為根的子樹進行上述操作;此時,A[4] = 1,與A[9]交換,i = 9是葉子節點,調整完畢。如下圖所示:

- 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是葉子節點,調整完畢。最終得到最大堆:

四、堆的其它方法
1.插入
向數組最后插入一個數據,然后再向上調整。還以上述數組為例,插入一個11。
- 在數組最后插入11

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

時間復雜度:O(logn)
2.刪除(指刪除堆頂的數據)
交換堆頂和最后一個數據,刪除最后一個數據,再對堆頂數據進行向下調整算法。
- 交換堆頂和最后一個數據,並刪除最后一個數據。

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

時間復雜度:O(logn)
五、堆排序
基本思路:
- 將輸入的數組建成最大堆。此時,堆頂元素就是最大值。
- 交換堆頂元素和末尾元素。此時,末尾元素是最大值。
- 將剩下 n-1 個元素重新構造成一個堆。重復執行上述步驟。
舉個簡單的例子:[14, 8, 10, 4]
- 將該數組構建成最大堆

- 交換堆頂元素和末尾元素。

- 忽略末尾元素,將剩下的元素利用下沉法重新構造為一個最大堆。

- 重復以上步驟,直至剩下的元素只剩下一個。最終得到如下結果,排序完畢:

時間復雜度: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;
}
}
