文章圖片來源於 GitHub,網速不佳的朋友,請看《堆、堆排序和優先佇列的那些事》 或者 來我的技術小站 godbmw.com
1. 什麼是堆?
堆是一種資料結構,它是一顆完全二叉樹。
堆分為最大堆和最小堆:
- 最大堆:任意節點的值不大於其父親節點的值。
- 最小堆:任意節點的值不小於其父親節點的值。
如下圖所示,就是個最大堆:
注:本文中的程式碼實現是最大堆,最小堆的實現相似,不再冗贅。
2. 堆有什麼用途?
堆最常用於優先佇列以及相關動態問題。
優先佇列指的是元素入隊和出隊的順序與時間無關,既不是先進先出,也不是先進後出,而是根據元素的重要性來決定的。
例如,作業系統的任務執行是優先佇列。一些情況下,會有新的任務進入,並且之前任務的重要性也會改變或者之前的任務被完成出隊。而這個出隊、入隊的過程利用堆結構,時間複雜度是O(log2_n)
。
3. 實現堆結構
3.1 元素儲存
堆中的元素儲存,一般是借用一個陣列:這個陣列是從 1 開始計算的。更方便子節點和父節點的表示。
3.2 入堆
入堆即向堆中新增新的元素,然後將元素移動到合適的位置,以保證堆的性質。
在入堆的時候,需要shift_up
操作,如下圖所示:
插入 52 元素後,不停將元素和上方父親元素比較,如果大於,則交換元素;直到達到堆頂或者小於等於父親元素。
3.3 出堆
出堆只能彈出堆頂元素(最大堆就是最大的元素),調整元素為止保證堆的性質。
在入堆的時候,需要shift_down
操作,如下圖所示:
已經取出了堆頂元素,然後將位於最後一個位置的元素放入堆頂(圖中是16
被放入堆頂)。
重新調整元素位置。此時元素應該和子節點比較,如果大於等於子節點或者沒有子節點,停止比較;否則,選擇子節點中最大的元素,進行交換,執行此步,直到結束。
3.4 實現優化
在優化的時候,有兩個部分需要做:
swap
操作應該被替換為:單次賦值,減少賦值次數- 入堆操作:空間不夠的時候,應該開闢 2 倍空間,防止陣列溢位
3.5 程式碼實現
// MaxHeap.h
// Created by godbmw.com on 2018/9/19.
//
#ifndef MAXHEAP_MAXHEAP_H
#define MAXHEAP_MAXHEAP_H
#include <iostream>
#include <algorithm>
#include <cassert>
#include <typeinfo>
using namespace std;
template <typename Item>
class MaxHeap {
private:
Item* data; // 堆資料存放
int count; // 堆目前所含資料量大小
int capacity; // 堆容量大小
void shift_up(int k) {
Item new_item = this->data[k]; // 儲存新插入的值
// 如果新插入的值比父節點的值小, 則父節點的值下移, 依次類推, 直到到達根節點或者滿足最大堆定義
while( k > 1 && this->data[k/2] < new_item ) {
this->data[k] = this->data[k/2];
k /= 2;
}
this->data[k] = new_item; // k就是 新插入元素 應該在堆中的位置
}
void shift_down(int k) {
Item root = this->data[1];
// 在完全二叉樹中判斷是否有左孩子即可
while(2*k <= this->count) {
int j = k + k;
// 如果有右子節點,並且右節點 > 左邊點
if( j + 1 <= this->count && this->data[j + 1] > this->data[j]) {
j += 1;
}
// root找到了堆中正確位置 k 滿足堆性質, 跳出迴圈
if(root >= this->data[j]) {
break;
}
this->data[k] = this->data[j];
k = j;
}
this->data[k] = root;
}
public:
MaxHeap(int capacity) {
this->data = new Item[capacity + 1]; // 堆中資料從索引為1的位置開始儲存
this->count = 0;
this->capacity = capacity;
}
// 將陣列構造成堆:heapify
MaxHeap(Item arr[], int n) {
this->data = new Item[n+1];
this->capacity = n;
this->count = n;
for(int i = 0; i < n; i++) {
this->data[i + 1] = arr[i];
}
for(int i = n/2; i >= 1; i--) {
this->shift_down(i);
}
}
~MaxHeap(){
delete[] this->data;
}
// 返回堆中元素個數
int size() {
return this->count;
}
// 返回布林值:堆中是否為空
bool is_empty() {
return this->count == 0;
}
// 向堆中插入元素
void insert(Item item) {
// 堆空間已滿, 開闢新的堆空間.
// 按照慣例,容量擴大到原來的2倍
if(this->count >= this->capacity) {
this->capacity = this->capacity + this->capacity; // 容量變成2倍
Item* more_data = new Item[this->capacity + 1]; // data[0] 不存放任何元素
copy(this->data, this->data + this->count + 1, more_data); // 將原先 data 中的有效資料拷貝到 more_data 中
delete[] this->data;
this->data = more_data;
}
this->data[this->count + 1] = item; // 插入堆尾部
this->shift_up(this->count + 1); // 執行 shift_up,將新插入的元素移動到應該在的位置
this->count ++;
}
// 取出最大值
Item extract_max() {
assert(this->count > 0);
Item ret = this->data[1]; // 取出根節點
swap(this->data[1], this->data[this->count]); // 將根節點元素和最後元素交換
this->count --; // 刪除最後一個元素
this->shift_down(1); // shift_down 將元素放到應該在的位置
return ret;
}
};
#endif //MAXHEAP_MAXHEAP_H
複製程式碼
4. 堆排序
根據實現的MaxHeap
類,實現堆排序很簡單:將元素逐步insert
進入堆,然後再extract_max
逐個取出即可。當然,這個建堆的平均時間複雜度是O(n*log2_n)
程式碼如下:
template <typename T>
void heap_sort1(T arr[], int n) {
MaxHeap<T> max_heap = MaxHeap<T>(n);
for(int i = 0; i < n; i++) {
max_heap.insert(arr[i]);
}
for(int i = n -1; i >= 0; i--) {
arr[i] = max_heap.extract_max();
}
}
複製程式碼
仔細觀察前面實現的建構函式,建構函式可以傳入陣列引數。
// 將陣列構造成堆:heapify
MaxHeap(Item arr[], int n) {
this->data = new Item[n+1];
this->capacity = n;
this->count = n;
for(int i = 0; i < n; i++) {
this->data[i + 1] = arr[i];
}
for(int i = n/2; i >= 1; i--) {
this->shift_down(i);
}
}
複製程式碼
過程叫做heapify
,實現思路如下:
- 將陣列的值逐步複製到
this->data
- 從第一個非葉子節點開始,執行
shift_down
- 重複第 2 步,直到堆頂元素
這種建堆方法的時間複雜度是: O(n)
。因此, 編寫heap_sort2
函式:
// 建堆複雜度:O(N)
template <typename T>
void heap_sort2(T arr[], int n) {
MaxHeap<T> max_heap = MaxHeap<T>(arr, n);
for(int i = n -1; i >= 0; i--) {
arr[i] = max_heap.extract_max();
}
}
複製程式碼
上面闡述的兩種排序方法,藉助實現的最大堆這個類,都需要在類中開闢this->data
,空間複雜度為O(n)
。其實,藉助shift_down
可以實現原地堆排序,程式碼如下:
// 這裡的 swap 操作並沒有優化
// 請對比 MaxHeap 中的 shift_down 函式
template <typename T>
void __shift_down(T arr[], int n, int k) {
while( 2*k + 1 < n) {
int j = 2 * k + 1;
if( j + 1 < n && arr[j + 1] > arr[j]) {
j += 1;
}
if(arr[k] >= arr[j]) {
break;
}
swap(arr[k], arr[j]);
k = j;
}
}
// 原地堆排序
template <typename T>
void heap_sort3(T arr[], int n) {
for(int i = (n -1)/2; i>=0; i--) {
__shift_down(arr, n, i);
}
for(int i = n-1; i > 0; i--) {
swap(arr[0], arr[i]);
__shift_down(arr, i, 0);
}
}
複製程式碼
5. 測試
5.1 測試MaxHeap
類
測試程式碼如下:
#include <iostream>
#include <ctime>
#include <algorithm>
#include "MaxHeap.h"
#include "SortHelper.h"
#define HEAP_CAPACITY 10
#define MAX_NUM 100
using namespace std;
int main() {
MaxHeap<int> max_heap = MaxHeap<int>(HEAP_CAPACITY);
srand(time(NULL));
for(int i = 0; i < HEAP_CAPACITY + 5; i++) { // 容量超出初始化時的容量。測試:自動
max_heap.insert(rand() % MAX_NUM);
}
while( !max_heap.is_empty() ) {
cout<< max_heap.extract_max() << " "; // 控制檯輸出資料是從大到小
}
cout<<endl;
return 0;
}
複製程式碼
5.2 測試堆排序
藉助前幾篇文章的SortHelper.h
封裝的測試函式:
#include <iostream>
#include <ctime>
#include <algorithm>
#include "MaxHeap.h"
#include "SortHelper.h"
#define HEAP_CAPACITY 10
#define MAX_NUM 100
using namespace std;
int main() {
int n = 100000;
int* arr = SortTestHelper::generateRandomArray<int>(n, 0, n);
int* brr = SortTestHelper::copyArray<int>(arr, n);
int* crr = SortTestHelper::copyArray<int>(arr, n);
SortTestHelper::testSort<int>(arr, n, heap_sort1<int>, "first heap_sort");
SortTestHelper::testSort<int>(brr, n, heap_sort2<int>, "second heap_sort");
SortTestHelper::testSort<int>(crr, n, heap_sort3<int>, "third heap_sort");
delete[] arr;
delete[] brr;
delete[] crr;
return 0;
}
複製程式碼