【c++】資料結構———堆
堆是一種特殊的資料結構,它通常是一個可以被看做一棵樹的陣列物件。
What?那它到底是一棵樹,還是一個數組呢?答案是陣列。這個陣列以二叉樹的形式來維護。注意:這個二叉樹必須是完全二叉樹
堆結構的二叉樹儲存有兩種:
最大堆:每個父親結點的值都大於孩子結點。
最小堆:每個父親結點的值都小於孩子結點。
顧名思義,這種結構就是可以根節點為最大的/最小的結點。不過有一點要注意,堆內的元素並不一定是按陣列下標來排序的,很多
初學者會錯誤的認為最大/最小堆中下標為1的就是第一大/小,下標為2的就是第二大/小。。。。。。
你會問,堆既然是一個數組,那麼它如何以二叉樹的形式來儲存呢?——————答案:下標。
我們先來看一棵二叉樹的模型:(這是一棵亂序的完全二叉樹)紅色數字代表結點的值,黑色數字代表結點的下標。
以上,我們可以看出,每個根結點的下標 = ((左/右)孩子結點下標-1)/2,左孩子結點下標 = (根結點*2)+1,右孩子結點下
標 = (根結點*2)+ 2。這樣我們用陣列以下標
方式就可以很好的儲存一顆樹了(前提是這棵樹必須是完全二叉樹),我們就會得到一個數組 int a [] = {10,16, 18, 12, 11, 13, 15, 17, 14, 19};
下面我開始講如何建立並維護一個堆,以最大堆為例。
假設,我們現在已經有了一個亂序的堆(例上圖),如何讓它變成最大堆/最小堆呢?————堆的向下調整,向上調整。
向下調整:(調整根結點parent)
我們先要找到陣列中存放的最後一個非葉子結點parent(上圖中的下標為4值為11的結點)。找出它的左孩子leftchild和右孩rightchild
的最大值MaxChild,與父結點parent進行比較,若MaxChild>parent,則將它們倆的值進行交換。然後令parent的下標 = MaxChild下
標。再重複之前的操作。直到leftChild >= 陣列的size。
到這裡,我們才完成了一次向下調整,並不足以構成一個最大堆,在向下調整函式外面我們需要加上一個迴圈,每次向向下調整函式
傳入想調整的根結點。(最後一個非葉子結點只是一個迴圈的‘起點’)
下面看程式碼:
template<typename T>
class Heap
{
public:
//建堆
Heap(const T* a, int sz)
{
_a.resize(sz);
for (int i = 0; i < sz; i++)
{
_a[i] = a[i];
}
for (int i = (_a.size() - 2) / 2; i >= 0; i--)//外層迴圈,每次傳入根結點下標。(_a.size()-2)/2即為最後一個非葉子結點下標。
{
AdjustDown(i);
}
}
protected:
//向下調整
void AdjustDown(int root)
{
int parent = root;
size_t child = parent * 2 + 1;
while (child < _a.size())//當child>=_a.size()的時候,說明已經越界。
{
if (child + 1 < _a.size() && _a[child + 1] > _a[child])
{
child++;
}
if (_a[child] > _a[parent])
{
swap(_a[child], _a[parent]);//交換child與parent
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
protected:
vector<T> _a;//用Vector來代替一個數組
};
向上調整:(調整孩子結點child)
再來看向上調整,就簡單的多了。找到你想調整的結點child,找出它的父親結點parent,比較兩個的大小,若child < parent,不操
作。若child > parent, 則交換兩個的值,然後令child的下標 = parent的下標。重複以上操作,直到child的下標 <= 0(說明到達根結點)。
程式碼:
//向上調整
void AdjustUp(int root)
{
int child = root;
int parent = (child - 1) / 2;
Compare com;
while (child > 0)
{
if (!com(_a[child] ,_a[parent]))
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
依靠上面兩個演算法,我們就可以建立一個最大堆了,下面我再來講兩個維護堆的演算法。
Push插入
每次都插到陣列最後,然後針對這個結點進行一次向上調整。
//插入
void Push(const T& x)
{
_a.push_back(x);
AdjustUp(_a.size() - 1);
}
Pop刪除(刪除根結點,即陣列第一個元素)
有人可能會想,直接讓陣列向前挪動一位就行了。錯!,如果你這麼做,確實達到了刪除的效果,但是你破壞了整個堆的結構。
正確的做法是將根結點(第一個元素)與最後一個結點(最後一個元素)進行交換,刪除最後一個元素,然後再根結點進行一次向下調整。
//刪除
void Pop()
{
assert(!_a.empty());
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
AdjustDown(0);
}
堆有什麼應用?
1、堆排序,時間複雜度為O(N*lgN),最大堆,從小到大排序。最小堆,從大到小排序。
2、優先順序佇列。
3、大資料篩選,一個檔案中包含了1億個隨機數,如何快速找到最大(小)的100W個數字(時間複雜度為O(N*lgk))。
【解析】取前100 萬個整數,構造成了一棵陣列方式儲存的具有小頂堆,然後接著依次取下一個整數,如果它大於最小元素亦即堆頂元素,則將其賦予堆頂元素,然後用Heapify調整整個堆,如此下去,則最後留在堆中的100萬個整數即為所求 100萬個數字。該方法可大大節約記憶體。
堆