堆的實現及堆排序
前兩天刷筆試題,判斷一個數組的序列可以構成堆。仔細想了想,腦海裡幾乎已經遺忘了堆的知識,今天又重新去看書,把堆的知識總結一下。
首先堆是一種陣列物件,它可以被看成一個完全二叉樹。在我們常見的堆中有大堆和小堆。對大堆來說,每個父節點都大於孩子結點;小堆恰好相反。而且,大堆/小堆的每個子樹也是一個大堆/小堆。
一般,我們都是用陣列來儲存堆的,當然根據情況,我們也可以選擇用vector來儲存。所以堆的結構如下。也可以很清楚看到父子節點的關係。
在學習一個新的資料結構,我們一定要很清楚怎樣去建立這個資料結構。下面,我們說說如何建立一個堆(這裡以大堆為例)。如上圖,可以很明顯看出來堆的整體形狀,然後我們一般是用一個數組去構建一個堆,當然你也可以採用插入的方式去構建堆。在這裡,我們用陣列構建,然後講堆的插入。
1.堆的構建
在給定的數組裡,我們可以先將所有的葉子結點認為是已經符合要求的子堆,那麼我們需要調整的第一個堆肯定是最後一個擁有葉子結點的父節點。如下圖,即是a[4]=13這個結點。在這裡也可以看出父節點和子節點的關係,child=parent*2-1。
我們建的是一個大堆,在這裡已經是一個堆的大概形狀了。所以我們需要調整父子關係。如果孩子大於父親,那麼交換父子。然後依次向下調整。原理比較簡單,就不畫圖說明了 。
void _AdjustDown(int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < _a.size())
{
if (child + 1 < _a.size() && _a[child + 1] > _a[child]) //在左右孩子中找最小的
++child;
if (_a[child]>_a[parent])
{
swap(_a[child], _a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
堆化陣列就是將所有的資料按照如上所示的方式一個個調整,直到所有的資料都符合大堆的規則。在這裡,我所建的堆的底層是一個vector。
Heap(T* a, size_t n) //堆化陣列
{
_a.reserve(n);
for (size_t i = 0; i < n; i++) //將所有的結點push進去
{
_a.push_back(a[i]);
}
//可以把所有的葉子結點看成是 一個合法的堆
//所以需要從最後一個結點的父節點開始向下調整
//child=parent*2+1;
for (int i = (_a.size() - 2 / 2); i >= 0; i--)
{
_AdjustDown(i);
}
}
2.堆的插入
上面給出的是對一個數組的堆化,那麼如果我們想插入完成一個堆的建立也是可以的。當我們在一個堆中插入一個數據,毫無疑問這個資料是插入在最後的,然後我們通過調整使得堆繼續滿足條件。
如圖,我們插入了11,這個堆已經不滿足條件了,然後我們通過對堆的調整,向上調整。直到把11調整到合適的位置。
void push(const T& x) //時間複雜度為log(N)
{
_a.push_back(x);
_AdjustUp(_a.size() - 1);
}
void _AdjustUp(int child)
{
int parent = (child - 1) >> 1;
while (child > 0)
{
if (_a[child] > _a[parent])
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else
break;
}
}
3.堆的刪除
既然有插入,那麼必不可少的也得有刪除。但是堆的刪除,我們需要考慮一下怎麼刪。按照堆的定義,我們只能刪除堆中第0個數據。如果我們直接刪除堆的這個資料,這個堆的結構都發生了改變,那麼我們該怎麼刪除呢?
在我們學習的過程中,有種方法叫做替代法,也就是說將a[0]和a[last]的資料進行交換,然後刪除掉最後一個數據。刪除最後一個葉子,對這個堆的結構並不會發生什麼影響,緊接著我們在通過對堆進行一個調整,保證這個堆仍是一個大堆/小堆。
void pop() //刪除的時間複雜度為log(N)
{
assert(_a.empty);
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
_AdjustDown(0); //刪除後需要堆化,將堆重新調整
}
4.堆排序
說了這麼多堆的基礎知識,那麼堆在我們資料結構中的應用呢?最常見的一個就是堆排。堆排在我們平常用的概率也是蠻大的。堆排採用的思想是:先對一個數組進行建堆,如果這是要升序,我們就建一個大堆,然後將堆中最大的資料和最後一個數據進行交換,將剩下的資料繼續調整為一個大堆,然後在將最大的數和剩下的數的最後一個進行交換……依次類推。
void AdjustDown(int* a, size_t n, size_t parent)
{
size_t child = parent * 2 + 1;
while (child < n)
{
if (child+1<n && a[child + 1]>a[child]) //child+1 防止越界
child++;
if (a[child] > a[parent])
{
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void HeapSort(int* a, size_t n)
{
assert(a);
for (int i = (n - 2) / 2; i>= 0; i--)
AdjustDown(a, n, i);
int end = n - 1;
while (end>0)
{
swap(a[0], a[end]);
AdjustDown(a, end, 0);
--end;
}
}
整理堆排的整個思路,我們可以計算出堆排的時間複雜度為N*log(N)。恢復堆的時間複雜度為log(N),對陣列的N個數來說,每次都需要這樣操作一遍,所以時間複雜度為N * log(N)。