1.什么是二叉堆
二叉堆是一種特殊的堆,二叉堆是完全二元樹(二叉樹)或者是近似完全二元樹(二叉樹)。二叉堆有兩種:最大堆和最小堆。最大堆:父結點的鍵值總是大于或等于任何一個子節點的鍵值;最小堆:父結點的鍵值總是小于或等于任何一個子節點的鍵值。
上圖說:
可以看出,這是一顆完全二叉樹,同時這也是一個最大堆。
值得一提的是,完全二叉樹具有以下性質,在之后的分析中會使用到:
假設節點編號從1開始,編程實現時,數組從0號開始,所以可采用占位符把0號位置占用,或者將后續所有節點全部減一。
- 結點i的父結點為結點i/2 (注意這里是地板除法,舉個栗子,在C/JAVA...語言中 直接另1/2等于0,而不是0.5。具體可以這樣寫
floor(1/2)
也就是將1/2的結果在向下取整。) - 結點i的子結點分別為(2*i)和(2*i+1)
同時,基于完全二叉樹的特性,二叉堆通常使用數組來編寫。
2.二叉堆的基本操作
二叉堆的基本操作為插入,提取最大(最小)值。下面以最大堆作為例子來介紹,最小堆只是最大堆的鏡像反轉,所以我就不多說啦。
2.1插入
在上面的二叉堆中,我們插入了55這個節點。
我們來分析下,它是如何插入到這個二叉堆中的:
- 首先按照完全二叉樹的順序,我們在編號12這個地方生成55這個節點
- 接著,由于這是一個最大二叉堆,所以要比較它的父節點(如果有的話)和它的大小,這里55是大于7的,所以兩個交換了位置
- 循環第2步,直到它的父親節點不比它大,或者已經達到根節點。
從上面三步分析中,我們可以直到發生,一個插入的操作無非就是:插入到最后這個位置,向上更新兩步。所以我們可以寫出下面的代碼:
void insert(int e) {
ensureSize(); //保證空間還足夠插入
elem[length] = e; //插入
heapUp(); //向上更新
length++;
}
So,how to headUp()?
void heapUp() {
int i = length;
while (hasParentIndex(i) && elem[parentIndex(i)] < elem[i]) {
swap(elem[parentIndex(i)], elem[i]);
i = parentIndex(i);
}
}
我相信已經足夠簡單了。一直向上檢查是否有比自己小的元素,只要有,就交換。
知道怎么插入節點了,那么我們能夠運用它來做什么呢?(廢話,當然是插入操作了),其實最簡單的我們可以用它來生成二叉堆。
為了加深印象,我再貼一張,生成二叉堆動態的圖:
生成二叉堆
就隨機插入一些元素吧,randNumber
是我寫的隨機函數,各位同學看自己熟悉的語言怎么實現方便就怎么實現吧。
for (int i = 0; i < 10; i++) {
heap.insert(randNumber(0, 1000));
}
2.2 提取最大(最小)值
最大(最小)堆的根節點,代表了這個堆中的最大(最小)值,將它進行彈出,在把整棵樹最后的節點放置到根節點,再把它交換到恰當的位置,這就是提取操作。
表達的不是很好,所以還是貼圖吧
相信你已經能夠理解什么是提取操作了。其實具體就分為三步:
- Pop堆頂元素
- 把當前樹(數組)中最后一個非空元素提取上來
- heapDown,更新,只要遇到比自己大的元素就交換。
那么下面看代碼:
bool pop() {
if (!isEmpty()) {
elem[0] = elem[length - 1];
length--;
//swap down
heapDown();
return true;
} else {
return false;
}
}
void heapDown() {
int i = 0;
while (hasLeftChild(i)) {
//find the minimum index of this node's child
int largerIndex = leftSonIndex(i);
if (hasRightChild(i) && elem[rightSonIndex(i)] > elem[leftSonIndex(i)]) {
largerIndex = rightSonIndex(i);
}
if (elem[i] > elem[largerIndex]) {
break;
}
swap(elem[i], elem[largerIndex]);
i = largerIndex;
}
}
heapDown
操作比heapUp
略微復雜一點,因為從上往下時有左子樹和右子樹,我們要檢查哪個子樹更大,總是把更大的哪個往上堆,至于為什么,因為越往下越小嘛。
那么提取操作能做什么呢?沒錯,就是今天的應用-堆排序
3.二叉堆應用-堆排序
所謂的堆排序,也就是利用了二叉堆本身性質,在循環調用提取操作的一種排序方式,其平均時間復雜度為O(nlogn),是一種排序效率很高的排序方法。
還是老規矩,先上圖:
寫個driver來測試一下:
int main(void) {
Heap heap(1000);
//初始化隨機種子
srand((unsigned int) time(NULL));
for (int i = 0; i < 100000; i++) {
heap.insert(randNumber(0, 1000000));
}
while (!heap.isEmpty()) {
cout << heap.top() << " ";
heap.pop();
}
return 0;
}
本文中所有的源碼都可以在我的Github上找到:https://github.com/zzbb1199/DataStructure
除了本文,也推薦去看下這個視頻(需要去外面看看才行喲),10分鐘搞定堆,雖然是全英文的,但是講得簡短而且很清晰,學習數據結構的同時也鍛煉了自己的英語能力嘛:https://www.youtube.com/watch?v=t0Cq6tVNRBA&t=490s