天天看點

資料結構--堆 Heap

1. 概念

  • 堆是一種特殊的樹

    a. 堆是完全二叉樹(除最後一層,其他層都是滿的,最後一層節點都靠左)

    b. 每一個節點都大于等于(或者都小于等于)其子樹中每個節點的值

資料結構--堆 Heap

2. 操作和存儲

  • 完全二叉樹适合用數組存儲,節省空間(不需要左右指針)
資料結構--堆 Heap

2.1 插入一個元素

資料結構--堆 Heap

2.2 删除堆頂元素

資料結構--堆 Heap
/**
 * @description: 堆
 * @author: michael ming
 * @date: 2019/5/26 22:22
 * @modified by: 
 */
#include <iostream>
#include <limits.h>
using namespace std;
class MinHeap
{
    int *arr;
    int capacity;
    int heap_size;
public:
    MinHeap(int cap)
    {
        heap_size = 0;
        capacity = cap;
        arr = new int [capacity];
    }
    ~MinHeap()
    {
        delete [] arr;
    }
    int heapsize()
    {
        return heap_size;
    }
    bool full()
    {
        return capacity == heap_size;
    }
    void MinHeapify(int i)
    {
        Heapify(i,heap_size);
    }
    void Heapify(int i, int size)
    {
        int l = left(i), r = right(i);
        int min = i;
        if(l < size && arr[l] < arr[i])
            min = l;
        if(r < size && arr[r] < arr[min])
            min = r;
        if(min != i)
        {
            swap(arr[i], arr[min]);
            Heapify(min,size);
        }
    }
    int parent(int i)
    {
        return (i-1)/2;
    }
    int left(int i)
    {
        return 2*i+1;
    }
    int right(int i)
    {
        return 2*i+2;
    }
    void delMin()
    {
        if(heap_size <= 0)
            return;
        arr[0] = arr[heap_size-1];
        heap_size--;
        MinHeapify(0);
    }
    int getMin()
    {
        return arr[0];
    }
    void insert(int val)
    {
        if(heap_size == capacity)
        {
            cout << "overflow!" << endl;
            return;
        }
        heap_size++;
        int i = heap_size-1;
        arr[i] = val;
        while(i > 0 && arr[parent(i)] > arr[i])
        {
            swap(arr[parent(i)], arr[i]);
            i = parent(i);
        }
    }
    void sort()
    {
        int size = heap_size;
        for(int i = heap_size-1; i >= 0; --i)
        {
            swap(arr[i], arr[0]);
            Heapify(0,--size);
        }
    }
    void print()
    {
        for(int i = 0; i < heap_size; ++i)
            cout << arr[i] << " ";
    }
};
int main()
{
    MinHeap minheap(8);
    minheap.insert(6);
    minheap.insert(8);
    minheap.insert(12);
    minheap.insert(4);
    minheap.insert(15);
    minheap.insert(0);
    minheap.insert(5);
    minheap.insert(9);
    minheap.insert(10);
    minheap.delMin();
    cout << minheap.getMin() << endl;
    return 0;
}           

複制

3. 堆排序(不穩定排序)

3.1 建堆

  • 方法1:一個一個的插入這種方式
  • 方法2:從倒數第一個有葉子節點的節點開始,檢查其子節點是否滿足堆,依次往前,直到堆頂,建堆的複雜度O(n)
資料結構--堆 Heap

3.2 排序

  • 建堆結束後,最大元素在堆頂,與最後一個元素交換(不穩定),然後對剩餘的 n-1 個元素重新建構堆,重複這個過程,最後隻剩1個元素,排序結束,建堆複雜度O(nlogn)
資料結構--堆 Heap

堆排序代碼見:https://blog.csdn.net/qq_21201267/article/details/80993672#t7

3.3 思考:為什麼快速排序要比堆排序性能好?兩者都是O(nlogn)

  1. 快排資料通路方式比較友好。快排通路資料是順序通路,堆排序是跳着通路,這樣對CPU緩存是不友好的
  2. 同樣的資料,堆排序中資料交換次數多餘快排。快排的交換次數不會比逆序度多;堆排序第一步建堆,打亂了資料原有順序,資料有序度降低,交換次數變多

4. 堆應用

4.1 優先級隊列

  • 優先級隊列用堆實作是最直接、最高效的。優先出隊,就是堆頂取出

    a. 合并有序小檔案

    把多個有序的小檔案的第一個元素取出,放入堆中,取出堆頂到大檔案,然後再從小檔案中取出一個加入到堆,這樣就把小檔案的元素合并到大檔案中了。

/**
 * @description: 合并有序小檔案
 * @author: michael ming
 * @date: 2019/5/29 22:10
 * @modified by: 
 */
#include "heap.cpp"
int main()
{
    int file0[10] = {11, 21, 31, 41, 51, 61, 71, 81, 91, 101};
    int file1[8] = {1, 2, 3, 4, 5, 6, 7, 80};
    int file2[9] = {13, 23, 33, 43, 53, 63, 73, 83, 93};
    int file3[10] = {12, 22, 32, 42, 52, 62, 72, 82, 92, 102};
    int file4[7] = {15, 25, 35, 45, 55, 65, 72};
    int len0 = 10, len1 = 8, len2 = 9, len3 = 10, len4 = 7;
    int i0, i1, i2, i3, i4, j;
    i0 = i1 = i2 = i3 = i4 = j = 0;
    const int new_len = len0+len1+len2+len3+len4;
    int bigFile[new_len];
    MinHeap intheap(5);
    intheap.insert(file0[i0]);
    intheap.insert(file1[i1]);
    intheap.insert(file2[i2]);
    intheap.insert(file3[i3]);
    intheap.insert(file4[i4]);
    int top;
    while(intheap.heapsize())
    {
        top = intheap.getMin();
        bigFile[j++] = top;
        intheap.delMin();
        if(i0 < len0-1 && top == file0[i0])   //可以用list做,就不用查找最小的是哪個檔案的
        {
            intheap.insert(file0[++i0]);
        }
        else if(i1 < len1-1 && top == file1[i1])
        {
            intheap.insert(file1[++i1]);
        }
        else if(i2 < len2-1 && top == file2[i2])
        {
            intheap.insert(file2[++i2]);
        }
        else if(i3 < len3-1 && top == file3[i3])
        {
            intheap.insert(file3[++i3]);
        }
        else if(i4 < len4-1 && top == file4[i4])
        {
            intheap.insert(file4[++i4]);
        }
    }
    for(i0 = 1, j = 0; j < new_len; i0++,++j)
    {
        cout << bigFile[j] << " ";
        if(i0%10 == 0)
            cout << endl;
    }
    return 0;
}           

複制

資料結構--堆 Heap

b. 高性能定時器

多個定時器,需要每隔很短的時間(比如1秒)掃描一遍,查詢哪個任務時間到了,需要開始執行,這樣有時候很多掃描是徒勞的,如果任務清單很長,掃描很耗時。采用小頂堆,最先執行的放在堆頂,就隻需要在堆頂時間到時執行堆頂任務即可,避免無謂的掃描。

4.2 用堆求 Top K(前K大資料)

a. 針對靜态資料(資料不變)

建立大小為K的小頂堆,周遊數組,數組元素與堆頂比較,比堆頂大,就把堆頂删除,并插入該元素到堆

b. 針對動态資料(資料不斷插入更新的)

在動态資料插入的時候就與堆頂比較,看是否入堆,始終維護這個堆,需要的時候直接傳回,最壞O(n*lgK)

/**
 * @description: 用堆求最大的k個元素
 * @author: michael ming
 * @date: 2019/5/30 0:06
 * @modified by: 
 */
#include "heap.cpp"
int main()
{
    int i = 0;
    const int len = 10;
    int data[len] = {2, 8, 1, 7, 12, 24, 6, 10, 90, 100};
    MinHeap intheap(5);//求前5大元素
    while(!intheap.full())
    {
        if(i < len)
            intheap.insert(data[i++]);
    }
    while(i < len)
    {
        if(data[i] > intheap.getMin())
        {
            intheap.delMin();
            intheap.insert(data[i]);
        }
        i++;
    }
    intheap.sort();
    intheap.print();
    return 0;
}           

複制

資料結構--堆 Heap

4.3 求中位數

靜态資料:先排序,中間位置的數就是中位數

動态資料:動态變化,中位數位置總在變動,每次都排序的話,效率很低,借助堆可以高效的解決。

資料結構--堆 Heap

插入資料到某個堆裡後,兩個堆資料量(應相等或者大堆比小堆多1)若不滿足括号要求,則将某個堆的堆頂元素移動到另一個堆,直到滿足括号要求,堆化複雜度O(lgn),大堆的堆頂就是中位數,求中位數複雜度O(1)。

同理,可以求99%響應時間(就是大于前面99%資料的那個資料)

跟上面過程類似,隻是大堆中儲存 n x 0.99 個資料,小堆存 n x 0.01 個資料

/**
 * @description: 利用大小兩個堆求中位數
 * @author: michael ming
 * @date: 2019/5/30 20:37
 * @modified by: 
 */
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
void printVec(vector<int> v)
{
    for (int i = 0; i < v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
int main()
{
    const int len = 7;
    int staticArr[len] = {7,-1,9,0,8,77,24};//-1,0,7,*8*,9,24,77
    vector<int> maxheap, minheap;
    for(int i = 0; i < len; ++i)
    {
        if(maxheap.empty())
        {
            maxheap.push_back(staticArr[i]);
            continue;
        }
        //----選擇插入哪個堆-----
        if(!maxheap.empty() && staticArr[i] <= maxheap[0])
        {
            maxheap.push_back(staticArr[i]);
            push_heap(maxheap.begin(),maxheap.end());//預設采用 < , 大堆
        }
        else if(!maxheap.empty() && staticArr[i] > maxheap[0])
        {
            minheap.push_back(staticArr[i]);
            push_heap(minheap.begin(),minheap.end(),greater<int>());//小堆,采用 >
        }
        //----平衡兩個堆的節點比列----
        if(maxheap.size() > minheap.size() && maxheap.size() - minheap.size() > 1)
        {
            minheap.push_back(maxheap[0]);//大堆頂進入小堆
            push_heap(minheap.begin(),minheap.end(),greater<int>());
            pop_heap(maxheap.begin(),maxheap.end());//堆頂到末尾了
            maxheap.pop_back();//删除到末尾的"堆頂"
        }
        else if(maxheap.size() < minheap.size())
        {
            maxheap.push_back(minheap[0]);
            push_heap(maxheap.begin(),maxheap.end());//預設采用 < , 大堆
            pop_heap(minheap.begin(),minheap.end(),greater<int>());
            minheap.pop_back();
        }
    }
    if(maxheap.size())
        cout << "中位數為:" << maxheap[0] << endl;
    return 0;
}           

複制

stl heap操作:http://www.cplusplus.com/reference/algorithm/pop_heap/

資料結構--堆 Heap

對上面程式稍加改造,對動态資料進行求解中位數

/**
 * @description: 中位數求解,利用大小堆,動态資料插入
 * @author: michael ming
 * @date: 2019/5/30 23:13
 * @modified by: 
 */
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
void printVec(vector<int> v)
{
    for (int i = 0; i < v.size(); ++i)
        cout << v[i] << " ";
    cout << endl;
}
int main()
{
    int num;
    vector<int> maxheap, minheap, allnum;
    while(cin >> num)
    {
        allnum.push_back(num);
        if(maxheap.empty())
        {
            maxheap.push_back(num);
            cout << "排序後的數組:" << endl;
            printVec(allnum);
            cout << "中位數為:" << maxheap[0] << endl;
            continue;
        }
        //----選擇插入哪個堆-----
        if(!maxheap.empty() && num <= maxheap[0])
        {
            maxheap.push_back(num);
            push_heap(maxheap.begin(),maxheap.end());//預設采用 < , 大堆
        }
        else if(!maxheap.empty() && num > maxheap[0])
        {
            minheap.push_back(num);
            push_heap(minheap.begin(),minheap.end(),greater<int>());//小堆,采用 >
        }
        //----平衡兩個堆的節點比列----
        if(maxheap.size() > minheap.size() && maxheap.size() - minheap.size() > 1)
        {
            minheap.push_back(maxheap[0]);//大堆頂進入小堆
            push_heap(minheap.begin(),minheap.end(),greater<int>());
            pop_heap(maxheap.begin(),maxheap.end());//堆頂到末尾了
            maxheap.pop_back();//删除到末尾的"堆頂"
        }
        else if(maxheap.size() < minheap.size())
        {
            maxheap.push_back(minheap[0]);
            push_heap(maxheap.begin(),maxheap.end());//預設采用 < , 大堆
            pop_heap(minheap.begin(),minheap.end(),greater<int>());
            minheap.pop_back();
        }
        sort(allnum.begin(),allnum.end());
        cout << "排序後的數組:" << endl;
        printVec(allnum);
        if(maxheap.size())
            cout << "中位數為:" << maxheap[0] << endl;
    }
    return 0;
}           

複制

資料結構--堆 Heap

4.4 思考:海量關鍵詞搜尋記錄,求topK

  • 用散清單去重,并累加搜尋次數
  • 再建立大小為K的小頂堆,周遊散清單,次數大于堆頂的,頂替堆頂入堆
  • (以上在散清單很大時,需要記憶體超過單機記憶體,怎麼辦?)
  • 建立n個空檔案,對搜尋關鍵詞求哈希值,哈希值對n取模,得到該關鍵詞被分到的檔案号(0到n-1)
  • 對每個檔案,利用散列和堆,分别求出topK,然後把n個topK(比如10個Top 20,200很小了吧)放在一起,出現次數最多的K(20)個關鍵詞就是這海量資料裡搜尋最頻繁的。