一、堆的基本介紹
1、堆的概念
堆是一類特殊的數據結構的統稱。堆通常是一個可以被看做一棵樹的數組的對象。堆滿足以下的性質
(1)堆中某個節點的值總是不大於或不小於其父節點的值
(2)堆總是一棵完全二叉樹
2、堆的分類
(1)最小堆:任一節點的關鍵碼均小於等於它的左右孩子的關鍵碼,位於堆頂的節點的關鍵碼最小(向上調整)
(2)最大堆:任一節點的關鍵碼均大於等於它的左右孩子的關鍵碼,位於堆頂的節點的關鍵碼最大(向下調整)
3、堆中下標爲i的節點的特點
因爲堆是存儲在下標爲0 開始計數的數組中,因此在堆中給定下標爲i的節點時:
(1)如果i=0,節點i爲根節點, 沒有雙親節點; 否則節點i的雙親節點爲節點(i-1)/2
(2)如果2*i+1>n-1,則節點i無左孩子,否則節點i的左孩子爲節點2*i+1
(3)如果2*i+2>n-1,則節點i無右孩子,否則節點i的右孩子爲節點2*i+2
二、堆的基本操作
1、堆的創建
給定數組:int array = {23,14,8,90,45,70,66}
先將數組用二叉樹表示出來:
因爲現在還是二叉樹,不算意義上的堆,所以我們需要把它調整成爲最小堆or最大堆
(1)向下調整:(最大堆)
我們可以將調整一整棵樹細化成爲調節樹的左右子樹爲最小堆,再去調節以左右子樹爲雙親節點的左右子樹……,一直到整棵都被調整完。具體過程如下:
* a、找到樹中最後一個非葉子節點,然後開始調整
* b、比較其左右孩子節點的大小,找出較大的孩子節點
* c、用較大的孩子節點去和它的雙親作比較。倘若孩子節點大於雙親節點,則交換雙親和孩子節點,並更新雙親節點和孩子節點;若小於,則結束此次的調整。
調整上圖的二叉樹爲最大堆,如下圖所示:
//向下調整(最大堆)
void _AdjustDown(size_t parent)
{
//用child標記左右孩子中最小的孩子,默認左孩子是最小的
size_t child = parent * 2 + 1;
size_t size = _array.size();
Compare com; //比較器 //(小於的參數放在前面)
while (child < size)
{
//找左右孩子中最小的
if (child + 1 < size && com(_array[child + 1], _array[child])) //左右孩子之間比較
{
child = child + 1;
}
//用大孩子和雙親去比較
if (com(_array[child], _array[parent]))
{
swap(_array[parent], _array[child]);
//調整子樹
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
注:這裏也可不用比較器
(2)向上調整(最小堆)
向上調整的過程基本上和向下調整差不多,就只是比較大小的時候是相反的,這裏就不過多贅述了。直接上代碼吧!!!
//向上調整(最小堆)
void _AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (_array[child] > _array[parent])
{
swap(_array[child], _array[parent]);
parent = child;
child = (parent - 1) / 2;
}
else
break;
}
}
2、插入
往堆裏插入元素就是在已經調整好的最大堆or最小堆的最後面插入元素,但插入之後可能會破會堆的結構,因此需要將堆重新調整,使其滿足最大堆or最小堆。
//尾插
void Push(const T& data)
{
_array.push_back(data);
if (_array.size()>1)
{
_AdjustUp(_array.size()-1);
}
}
3、堆的刪除
堆的刪除就是從堆中刪除堆頂元素。刪除堆頂元素之後,用堆的最後一個葉子結點去填補剛被刪除的堆頂元素,並將堆的實際元素減一。但用最後一個元素取代堆頂的元素之後可能會破壞堆的平衡,因此需要將堆重新調整,使其滿足最大堆or最小堆。
//刪除堆中元素
void Pop()
{
if (Empty())
{
return;
}
size_t last = _array.size() - 1;
swap(_array[0], _array[last]);
_array.pop_back();
if (_array.size()>1)
{
_AdjustDown(0);
}
}
4、堆排序(一種不穩定的排序算法)
(1)先創建好堆(見上述具體過程)。升序排列時創建大堆,降序排列時創建小堆
(2)排序(就是讓剛纔建好的大堆or小堆變得有序起來)。具體過程如下
* a、把堆頂元素array[0]和堆的最後一個元素交換
* b、最堆元素個數減一(這樣就是爲了避開剛交換的元素去調整剩下的元素)
* c、當交換元素之後,堆肯定會不滿足最堆的定義,此次需要調整節點
**
注:重複上圖的過程一直到堆排列有序
**
//調整堆
void AdjustHeap(int array, size_t size, size_t parent)
{
//標記最大的孩子,默認左孩子最大
size_t child = parent * 2 + 1;
while (child < size)
{
//找左右孩子中最大的孩子
if (child + 1 < size && array[child + 1] > array[child])
{
child = child + 1;
}
//用最大的孩子去檢測雙親
if (array[parent] < array[child])
{
swap(array[parent], array[child]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
//堆排序
void HeapSort(int* array, size_t size)
{
//創建堆
//倒數第一個非葉子節點的位置
int root = (size - 2) / 2;
for (; root >= 0; --root)
{
AdjustHeap(array, size, root);
}
//堆排序
for (int i = 0; i < size; i++)
{
swap(array[0], array[size - i - 1]);
AdjustHeap(array[0], size - i - 1, 0)
}
}
5、堆的應用
(1)堆的應用之大數據處理:從1000億個數據中找出最大的前k個數
思路:
* a、先取出k個數據
* b、將去取出的數據建爲小堆(建立小堆的話,堆頂元素就是最大的,這樣 就可以用比堆頂大的元素去交換,但倘若建大堆的話,堆頂元素就是最大的,這樣後面的數據元素就會進不去,這樣只會找到一個最大的元素)
* c、將剩下的數據依次和堆頂比較,若比堆頂大,則去替換堆頂,然後調整堆。週而復始,依次進行