今天我們來實現一個大頂堆,所謂大頂堆,即根節點的值大于等于其孩子節點的值。廢話少絮,直接開始。
堆是一個完全二叉樹,很適合用順序結構來實現,這里我們選擇數組。用數組實現堆時,我們不使用數組的0號位置,用1號位置來存放堆頂(當然也可以使用0號位置來存放堆頂,只是下面的性質要改變),此時,有兩個很重要的性質我們需要知道,如下:
- N號元素的父節點的位置為N/2。
- N號元素的左孩子的位置是2N,右孩子的位置是2N+1。
知道這些之后,我們就可以開始實現堆啦(撒花,終于開始了)。
我們從最基礎的開始,對于一個堆來說,它需要一個數組來存放數據,需要一個變量來記錄元素的個數,如下是一個基礎的大頂堆類:
template<typename Item>
class MaxHeap{
private:
Item* data; //數據
int count; //數據個數
int capacity; //容量
public:
MaxHeap(int capacity) { //構造函數分配空間
data = new Item(capacity+1);
count=0;
this->capacity = capacity;
}
~MaxHeap() { //析構函數釋放內存
delete[] data;
}
int size() { //返回堆元素的個數
return count;
}
bool isEmpty() { //判斷對是否為空
return count == 0;
}
};
data用來存放數據,由于不知道具體有多少數據,所以我們使用指針類型;count用來表示實際的元素個數;capacity表示最多能存放多少數據。用戶不需要直接訪問這些數據,所以我們將其作為私有類型。
然后我們提供一個構造函數,參數是堆的容量,我們在構造函數里為堆分配內存,并設置實際元素的個數和容量的大小。既然分配了內存,相應的,析構函數里就需要釋放這些內存。
除此之外,我們還提供了兩個工具函數,分別返回堆內元素的個數和判斷堆是否為空。
現在我們有了一個基礎的堆,并且可以給這個堆分配空間了:
MaxHeap<int> maxheap = MaxHeap<int>(100);
有了空間之后,我們怎么往這個空間里添加元素呢?我們需要提供一個插入函數:在不超過容量限制的情況下,每次將新元素放置在data的最后,此時,新元素可能不滿足大頂堆的性質,我們比較新元素與其父元素之間的大小關系,調整新元素與父元素的位置,直到整個堆滿足大頂堆的要求。
假設已經有了調整新元素與父元素之間位置的函數shiftUp,那么,插入函數就很簡單了:
void insert(Item item) { //向堆中插入一個元素
assert(count+1 <= capacity); //首先判斷空間是否夠用
data[count+1] = item; //將新元素放到數組的最后
count++; //元素個數加1
shiftUp(count); //對堆進行調整
}
重點是shiftUp函數,由于用戶不需要直接調用這個函數,所以我們將其作為私有的函數:
void shiftUp(int k) { //調整堆的第k個元素
while(k > 1 && data[k/2] < data[k]) { //判斷第k個元素是否比其父元素大
swap(data[k/2], data[k]); //交換
k /= 2;
}
}
學會了插入元素,下面我們要學習怎么從堆里刪除一個元素了:我們每次只能刪除堆頂的元素,然后將最后一個元素交換到堆頂,此時新的堆頂元素可能不滿足堆的性質,所以我們比較堆頂與其孩子之間的大小關系,并調整其位置,直到整個堆合法。
仍然假設已經有了調整新堆頂與孩子之間位置的函數shiftDown,那么,刪除函數也很簡單:
Item getMax() {
assert(count > 0);
Item res = data[1];
swap(data[1], data[count]);
count--;
shiftDown(1);
return res;
}
shiftDown函數的實現如下:首先判斷該元素是否有孩子節點,如果該元素還有右孩子,則比較兩個孩子的大小,找到較大的孩子,然后比較該元素與較大的孩子節點的大小,如果當前元素小于較大的孩子節點,則交換兩者的位置。然后,交換后的元素再次與其孩子進行比較,直到滿足堆的條件。
void shiftDown(int k) {
while(2*k <= count) {
int j = 2*k;
if(j+1 <= count && data[j] < data[j+1]) {
j += 1;
}
if(data[k] >= data[j]) {
break;
}
swap(data[k], data[j]);
k = j;
}
}
現在我們的對已經基本可以使用了。為了測試方便,我們還可以增加一個打印堆內元素的工具函數:
void printData() { //打印堆中的元素
for(int i = 1; i <= count; i++) {
cout<<data[i]<<" ";
}
}
下面我們可以測試一下了:
int main() {
MaxHeap<int> maxheap = MaxHeap<int>(100);//構造一個堆
srand(time(NULL));
for(int i = 0; i < 15; i++) {//給堆內的元素賦隨機值
maxheap.insert(rand() % 100);
}
maxheap.printData(); //打印堆內的數據
cout<<maxheap.size()<<endl;//打印堆元素的個數
while(!maxheap.isEmpty()) { //從大到小依次輸出堆內的每一個元素
cout<<maxheap.getMax()<<endl;
}
return 1;
}
我們的堆已經可以工作了,是不是很開心呀!也許不會說:
我不想每次都構造一個空的堆,然后再一個一個的插入元素啊,我想要直接用數組構造一個堆啊
聰明的你,這種想法簡直太棒了。一個一個插入元素的做法,時間復雜度是O(nlgn),如果直接使用數組來構造堆,時間復雜度可以為O(lgN)喲,你說你是不是很棒!
好了,現在我們開始寫這個構造函數吧。思路也很簡單:我們直接將數組元素作為堆的元素,然后從第一個非葉節點的元素(第一個有孩子的元素)開始(我們可以認為每個葉節點都是大頂堆),一直到堆頂元素,如果某個元素不滿足堆的性質,我們就調整其與孩子節點的位置。
MaxHeap(Item a[], int n) { //使用數組構造堆
data = new Item(n+1); //初始化堆
capacity = n;
for(int i = 0; i < n; i++) {
data[i+1] = a[i];
}
count = n;
for(int i = count/2; i >= 1; i--) {//調整堆
shiftDown(i);
}
}
我們現在已經寫完自己的堆啦!大頂堆類的完整代碼如下:
template<typename Item>
class MaxHeap{
private:
Item* data; //數據
int count; //數據個數
int capacity; //容量
void shiftUp(int k) { //調整堆的第k個元素
while(k > 1 && data[k/2] < data[k]) { //判斷第k個元素是否比其父元素大
swap(data[k/2], data[k]); //交換
k /= 2;
}
}
void shiftDown(int k) {
while(2*k <= count) {
int j = 2*k;
if(j+1 <= count && data[j] < data[j+1]) {
j += 1;
}
if(data[k] >= data[j]) {
break;
}
swap(data[k], data[j]);
k = j;
}
}
public:
MaxHeap(int capacity) { //構造函數分配空間
data = new Item(capacity+1);
count=0;
this->capacity = capacity;
}
MaxHeap(Item a[], int n) { //使用數組構造堆
data = new Item(n+1);
capacity = n;
for(int i = 0; i < n; i++) {
data[i+1] = a[i];
}
count = n;
for(int i = count/2; i >= 1; i--) {
shiftDown(i);
}
}
~MaxHeap() { //析構函數釋放內存
delete[] data;
}
int size() { //返回堆元素的個數
return count;
}
bool isEmpty() { //判斷對是否為空
return count == 0;
}
void insert(Item item) { //向堆中插入一個元素
assert(count+1 <= capacity); //首先判斷空間是否夠用
data[count+1] = item; //將新元素放到數組的最后
count++; //元素個數加1
shiftUp(count); //對堆進行調整
}
Item getMax() {
assert(count > 0);
Item res = data[1];
swap(data[1], data[count]);
count--;
shiftDown(1);
return res;
}
void printData() { //打印堆中的元素
for(int i = 1; i <= count; i++) {
cout<<data[i]<<" ";
}
}
};
參考簡書算法課,侵刪。