c++重拾 STL之heap(堆)

0、誤區!

1、堆排序排完後的堆和大頂堆、小頂堆不是一個概念!
2、堆分爲大頂堆和小頂堆,即要麼大頂堆(大根堆/最大堆),要麼小頂堆。
3、對於堆,堆的根節點一定是堆中所有節點的最大值或者最小值。
4、大頂堆只是說這個堆總每一個節點滿足:每一個節點大於或者等於其左右娃。並非這個堆一定是從大到小的序列。
5、所以才必須要有堆排序呀!堆排序排完了之後的,才一定是一個有序的序列。
6、堆實際上是用數組或者vector表現出來的一顆具有特殊結構的完全二叉樹。

1、heap

頭文件#include <algorithm>,STL在<algorithm.h>中實現了對存儲在數組或vector中的元素進行堆操作的函數,包括make_heap, pop_heap, push_heap, sort_heap。
【兩層:上層heap,底層vector(或數組)】,即用數組或vector數據容器來實現堆。
默認情況下是max-heap,該大頂堆實際上是以一個vector表現的完全二叉樹。

2、heap操作的四個函數:

  • make_heap( ):建立堆(要麼大頂堆,要麼小頂堆)
  • push_heap( ): 在堆中添加元素
  • pop_heap( ): 在堆中刪除元素
  • sort_heap( ): 堆排序
    相關參數:
    _First, _Last:可以隨機訪問的迭代器/ 指針
    _Comp: 比較函數(仿函數),其規則——如果函數的第一個參數小於第二個參數應返回true,否則返回false。默認less

使用建議
大頂堆,就每一個函數都加上第三個參數less<int>(),假如元素是int類型的
小頂堆,就每一個函數都加上第三個參數greater<int>(),假如元素是int類型的,一直加上,一直一致。

建立堆

make_heap(_First, _Last, _Comp)
默認是建立大最大堆的

在堆中添加元素

push_heap(_First, _Last,_Comp)
要先在底層容器(數組或vector)里加入數據,再調用push_heap()。
實現細節:(1)添加元素到vector的尾部;(2)重新調整堆。
該算法必須是在滿足堆序的條件下,添加元素。
如,插入15到當前的大根堆裏,vector容器名字爲max_heap:

max_heap.push_back(15);
push_heap(max_heap.begin(), max_heap.end());

在堆中刪除元素

pop_heap(_First, _Last,_Comp)
實現細節:(1)刪除堆頂元素;(2)用尾部元素替代max_heap[0];(3)重新調整堆。
(pop_heap操作實際上是我們把堆頂元素取出來,放到了數組或vector容器的末尾,用原來的末尾元素去替代,然後end迭代器減1,執行siftdown()下溯函數來重新調整堆)
注意算法執行完畢後,最大的元素並沒有被取走,而是放於底層容器的末尾。如果要取走,則可以使用底部容器(vector)提供的pop_back()函數。
pop_heap()操作後,再調用max_heap.pop_back(),從底層容器中刪掉原堆頂元素。

pop_heap(max_heap.begin(), max_heap.end());//取出了堆頂元素(也叫刪除堆頂元素),放到了底層容器的末尾,原來末尾的元素替代堆頂,end迭代器減1,重新siftdown了堆
max_heap.pop_back();//從底層容器(數組或vector)中刪除了元素

堆排序

sort_heap(_First, _Last,_Comp)
既然每次pop_heap可以獲得堆頂的元素(假如是大頂堆,每次都獲得最大的元素,取出放到了底層容器的末尾),那麼我們持續對整個heap做pop_heap操作,每次講操作的範圍向前縮減一個元素(就是每次end迭代器減1)。最終我們可以獲得一個遞增的序列。
注意:這個排序是在一個堆上進行的。

關於堆排序,可以看看這個博客<<白話經典算法系列之七 堆排序>> https://blog.csdn.net/morewindows/article/details/6709644

3、例1:基本操作的使用

底層數據容器:vector
1、
建立小頂堆min;
在小頂堆中插入元素;
刪除小頂堆的元素(刪的是h[0]);
小頂堆下的堆排序——> 降序的序列。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void printHeap(vector<int> &v){
    for(vector<int>::iterator it= v.begin();it!=v.end();++it){
        cout<< *it <<" ";
    }
    cout<<"\n"<<endl;
}

int main()
{
    vector<int> min={10,30,22,6,15,9};

    //建立小頂堆
    make_heap(min.begin(), min.end(), greater<int>());
    printHeap(min);//6 10 9 30 15 22

    //插入元素
    min.push_back(20);
    push_heap(min.begin(),min.end(), greater<int>());//該算法前提:必須在堆的條件下
    printHeap(min); //6 10 9 30 15 22 20   仍爲小頂堆

    //刪除堆頂元素
    pop_heap(min.begin(),min.end(), greater<int>());
    printHeap(min);//9 10 20 30 15 22 6  不爲小頂堆 這個pop_heap操作後,實際上是把堆頂元素放到了末尾
    min.pop_back();//這才徹底在底層vector數據容器中刪除
    printHeap(min);//9 10 20 30 15 22  仍爲小頂堆

    //堆排序  保持greater,小頂堆,得到的是降序
    sort_heap(min.begin(),min.end(), greater<int>());//試了用less,結果雜亂無章
    printHeap(min);//30 22 20 15 10 9 注意結果是降序的哦!!!其實是調用了很多次pop_heap(...,greater..),每一次都把小頂堆堆頂的元素往末尾放,沒放一次end迭代器減1

    return 0;
}

2、大頂堆,以及堆排序爲升序的例子
把上面code裏所有的第三個參數改爲less<int>()

4、應用:數據流中的中位數

如何得到一個數據流中的中位數?如果從數據流中讀出奇數個數值,那麼中位數就是所有數值排序之後位於中間的數值。如果從數據流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。我們使用Insert()方法讀取數據流,使用GetMedian()方法獲取當前讀取數據的中位數。
解決:
採用大頂堆和小頂堆實現。把數據分爲兩部分,左邊部分整體上要小於右邊部分。
左邊部分爲大頂堆,右邊部分爲小頂堆,往兩邊堆中不斷插入數據。
定:當數據總數爲偶數時,插入到min堆,總數爲奇數時,插入到max堆。插入過程中要保證左邊部分總體小於右邊部分,要不斷調整。最後,總數爲偶數時候,中位數爲max堆頂元素和min堆頂元素之和的平均,爲奇數,則爲min堆的堆頂元素。

class Solution {
private:
    vector<int> max;//大頂堆(左部分數據,堆頂爲堆的最大值(爲max[0]))
    vector<int> min;//小頂堆(右部分數據,堆頂爲堆的最小值(爲min[0]))
public:
    void Insert(int num)
    {
        if(((max.size()+min.size())&1)==0)//總數偶數
        {   /*來了一個數,此時總數爲偶數,原計劃加入到小頂堆,(奇數則到大頂堆,保證兩邊均勻分配)
              
              但是要保證小頂堆的最小值一直大於大頂堆的最大值(即小頂堆的數大於大頂堆的):
                  if num<大頂堆的最大值:
                      則反而插入到大頂堆中(大頂堆元素多了1個);
                      則把大頂堆的最大值刪掉,重新插入到小頂堆去;
                  else:
                      插入元素到小頂堆去。
            */
            if(max.size()>0 && num< max[0])
            {
                //添加num到大頂堆
                max.push_back(num);
                push_heap(max.begin(),max.end(),less<int>());//仿函數less保證插入後仍爲大頂堆
                
                //把大頂堆裏的最大值刪掉,添加到小頂堆裏面去
                num= max[0];
                pop_heap(max.begin(),max.end(),less<int>());
                max.pop_back();
                
                min.push_back(num);//push_heap前必須push_back到底層容器
                push_heap(min.begin(), min.end(), greater<int>());//仿函數greater保證插入後仍爲小頂堆
            }else
            {
                min.push_back(num);          //這塊,和上面有公共代碼,可以化簡
                push_heap(min.begin(),min.end(),greater<int>());
            }
        }else//總數奇數,原計劃送到大頂堆
        {
            if(min.size()>0 && num>min[0])
            {
                min.push_back(num);
                push_heap(min.begin(),min.end(),greater<int>());
                
                num=min[0];
                pop_heap(min.begin(), min.end(),greater<int>());
                min.pop_back();
                
                max.push_back(num);
                push_heap(max.begin(),max.end(), less<int>());
            }else
            {
                max.push_back(num);
                push_heap(max.begin(),max.end(),less<int>());
            }
        }
}

    double GetMedian()
    { 
        double median=0;
        if((max.size()+min.size())==0)
            return 0;
        
        if(((max.size()+min.size()) &1)==0)
        {
            median= ((double)max[0]+(double)min[0])/2;
        }else
        {
            median= min[0];
        }
        return median;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章