注意:heap並不是STL的容器或者配接器,但卻是priority queue的底層實現。並且heap運用的是常用的最大堆以及堆排序的方法,所以值得一看
一、最大堆和隱式表示法
- heap的數據結構是一個最大堆,最大堆就是一個完全二叉樹,並且每個父節點都大於它的子節點(左右子節點的大小沒有限制,不需要大的子節點一定要放哪邊)
- 隱式表示法就是一個二叉樹,可以用一個數組來表示,如圖所示
任意一個節點的父子節點可以計算得到:
設某個節點索引值爲index,則節點的左子節點索引爲: 2*index+1
右子節點索引爲: 2*index+2
父節點索引爲: (index-1)/2
heap中總共有四個函數,push_heap,pop_heap,sort_heap,make_heap,下面依次介紹
二、push_heap
作用:將數組中最後一個數據插入到最大堆中
代碼示例
int a[] = { 68, 31, 65, 21, 24, 32, 26, 19, 16, 13 };
vector<int> b(a, a + 10);//創建一個最大堆,並放入數組中
b.push_back(50);//把新加入的元素放在數組末尾
push_heap(b.begin(), b.end());//調用push_heap將數組末尾的元素放到合適的位置
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
結果如下:
過程圖解
將50依次和自己的父節點比較,如果比父節點大,就和父節點交換位置,如果比父節點小,就結束這個過程。push_heap就是執行這個過程。
源碼解析:
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__push_heap(_RandomAccessIterator __first,
_Distance __holeIndex, _Distance __topIndex, _Tp __value)
//這是真正算法執行的地方了,__holeIndex表示需要插入的值的當前位置,value就是需要插入最大堆的值了
{
_Distance __parent = (__holeIndex - 1) / 2;//找到父節點
while (__holeIndex > __topIndex && *(__first + __parent) < __value) {
//如果沒有到堆頂或者需要插入的值大於父節點值的時候,循環都不會停止
*(__first + __holeIndex) = *(__first + __parent);//因爲執行到這一步,肯定是value>*(__first + __parent),所以就把父節點的值下移到子節點
__holeIndex = __parent;//需要插入的值此時移到原父節點處
__parent = (__holeIndex - 1) / 2;//找到新位置的父節點
}
*(__first + __holeIndex) = __value;//將新值填入到最後的找到的合適位置,相當於__value是新值的臨時存放位置,__holeIndex是新值索引的臨時存放位置
}
template <class _RandomAccessIterator, class _Distance, class _Tp>
inline void
__push_heap_aux(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Distance*, _Tp*)
{
__push_heap(__first, _Distance((__last - __first) - 1), _Distance(0),
_Tp(*(__last - 1)));
//一般_Distance都是ptrdiff_t,這裏就是把(__last - __first) - 1強轉成ptrdiff_t類型。
//(__last - __first) - 1表示的是最後一個元素到堆頂的距離,也就是數組中最後一個元素的下標了
//_Distance(0)就是堆頂的下標了
}
template <class _RandomAccessIterator>
inline void
push_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
//傳入首尾迭代器,會認爲__first~__last-2都是已經排好的最大堆,而__last-1就是等待插入的元素
{
__push_heap_aux(__first, __last,
__DISTANCE_TYPE(__first), __VALUE_TYPE(__first));
}
三、pop_heap
作用:將最大堆中的根節點移出最大堆,移到數組的最後,所以相當於數組的尺寸沒變,但是最大堆尺寸減一
代碼示例
int a[] = { 68, 31, 65, 21, 24, 32, 26, 19, 16, 13 };
vector<int> b(a, a + 10);
b.push_back(50);
push_heap(b.begin(), b.end());
pop_heap(b.begin(), b.end());
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
結果如圖所示:
過程圖解
先將根節點放到數組最後一個位置,然後將該位置原元素,也就是24先臨時保存起來
然後從根節點開始,比較它的子節點和24這三個數值,哪個更大,如果是其中一個子節點更大,那就把該子節點放到空着的根節點位置,這時就空出來一個子節點的位置(如步驟三中所示),就將24繼續和空出來的節點的子節點比較,如果還是子節點更大,繼續執行之前的流程,如果是24更大,就將24填入空着的節點。
源碼解析:
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
_Distance __len, _Tp __value)
//__holeIndex索引所在位置表示爲空缺值,先讓子節點不斷上提彌補空缺值,然後將value加入到重新排列過的最大堆中。
{
_Distance __topIndex = __holeIndex;//__topIndex是根節點的索引
_Distance __secondChild = 2 * __holeIndex + 2;//找到__holeIndex的右子節點
while (__secondChild < __len) {//直到從根節點循環到最後一層才結束
//找到左右子節點中較大的那個值,將其索引值存入__secondChild
if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
__secondChild--;
//將__holeIndex的左右子節點中較大值填入__holeIndex位置
*(__first + __holeIndex) = *(__first + __secondChild);
__holeIndex = __secondChild;//__holeIndex向下移動到空着的子節點位置
__secondChild = 2 * (__secondChild + 1);//重新尋找右子節點
}
if (__secondChild == __len) {//這種情況下是右子節點不在堆的範圍中,但是左子節點在,所以將左子節點上提
*(__first + __holeIndex) = *(__first + (__secondChild - 1));
__holeIndex = __secondChild - 1;
}
//將__value重新加入到最大堆中去
//並且此時初始位置是__holeIndex,這個位置的元素剛好被提到父節點去了
__push_heap(__first, __holeIndex, __topIndex, __value);
}
template <class _RandomAccessIterator, class _Tp, class _Distance>
inline void
__pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
_RandomAccessIterator __result, _Tp __value, _Distance*)
//__result是指向數組中的最後一個元素,用來存儲根節點
//__value是原數組最後一個元素的值
{
*__result = *__first;
__adjust_heap(__first, _Distance(0), _Distance(__last - __first), __value);
//由於第三個參數是len,最大堆的尺寸,所以不需要-1,如果是最後一個元素的索引,就需要-1了
}
template <class _RandomAccessIterator, class _Tp>
inline void
__pop_heap_aux(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Tp*)
{
__pop_heap(__first, __last - 1, __last - 1,
_Tp(*(__last - 1)), __DISTANCE_TYPE(__first));
//注意這邊傳入的參數把最大堆尺寸由原來的__last - __first - 1變爲了__last - __first - 2
//相當於縮小了最大堆的尺寸
}
template <class _RandomAccessIterator>
inline void pop_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last)//傳入首尾迭代器
//將__first指向的元素放到__last-1,並重新調整最大堆的位置。
{
__pop_heap_aux(__first, __last, __VALUE_TYPE(__first));
}
四、sort_heap
作用:因爲最大堆是存儲在數組中的,所以利用最大堆把數組中的元素排序
代碼示例:
int a[] = { 68, 31, 65, 21, 24, 32, 26, 19, 16, 13 };
vector<int> b(a, a + 10);
b.push_back(50);
push_heap(b.begin(), b.end());
sort_heap(b.begin(), b.end());
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
結果如圖所示:
過程圖解
就是不斷使用pop_heap,每次都把堆中最大的值放到堆得末尾,然後找到剩下元素中最大的值放到根節點
源碼解析
void sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
while (__last - __first > 1)//每執行一次,last都會減一,最大堆的規模都會減小一,所以知道最大堆上只有一個元素時,說明已經排序完畢。
pop_heap(__first, __last--);
}
五、make_heap
作用:就是將一個無序的數組排列成最大堆的形式
代碼示例:
int a[] = { 24, 32, 31, 65, 26, 68, 19, 16, 21, 13 };//這裏是無序的
vector<int> b(a, a + 10);
make_heap(b.begin(), b.end());
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
結果如圖所示:
可以看到經過make_heap,就排列成最大堆的模式了。
源碼解析
template <class _RandomAccessIterator, class _Tp, class _Distance>
void
__make_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Tp*, _Distance*)
{
if (__last - __first < 2) return;//如果元素個數爲1或者0就可以
_Distance __len = __last - __first;
_Distance __parent = (__len - 2)/2;//計算出最後一個元素__len - 2的父節點,然後從__parent 節點往前,就都是由子節點的了。
while (true) {
__adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));//從最後一個父節點開始,每次把父
//節點下沉,相當於保證了每個父節點都比自己最相鄰的那兩個子節點大,然後依次往頂層循環,這樣保證所有的父節
//點都比自己的子節點大了。
if (__parent == 0) return;
__parent--;
}
}
template <class _RandomAccessIterator>
inline void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
__make_heap(__first, __last,
__VALUE_TYPE(__first), __DISTANCE_TYPE(__first));
}