堆、堆排序与优先队列

堆、堆排序与优先队列

堆(heap)

  堆(Heap)是计算机科学中一类特殊的数据结构。堆通常用数组来实现。把堆近似看作一棵完全二叉树(最底层可能不是完全填满的,如下图),堆中结点的值总是不大于或不小于其父结点的值。
  根结点最大的堆叫做最大堆或大根堆。
  根结点最小的堆叫做最小堆或小根堆。

  最小堆示例:

在这里插入图片描述

  表示堆的数组 A A A有两个属性: A . l e n g t h A.length A.length表示数组元素的个数, A . h e a p _ s i z e A.heap\_size A.heap_size表示有多少个堆元素存储在数组中。有 0 ≤ A . h e a p _ s i z e ≤ A . l e n g t h 0\le A.heap\_size\le A.length 0A.heap_sizeA.length。树的根节点是 A [ 1 ] A[1] A[1]
  给定一个节点的下标 i i i那么它的父节点,左孩子和右孩子的下标:
   P A R E N T ( i ) = └ i / 2 ┘ , L E F T ( i ) = 2 i , R I G H T ( i ) = 2 i + 1 PARENT(i) = \llcorner i/2\lrcorner,LEFT(i)=2i,RIGHT(i)=2i+1 PARENT(i)=i/2,LEFT(i)=2i,RIGHT(i)=2i+1

 这是对于首元素为 A [ 1 ] A[1] A[1]而言的,如若是 A [ 0 ] A[0] A[0],相似地,为:
   P A R E N T ( i ) = └ ( i − 1 ) / 2 ┘ , L E F T ( i ) = 2 i + 1 , R I G H T ( i ) = 2 ( i + 1 ) PARENT(i) = \llcorner (i-1)/2\lrcorner,LEFT(i)=2i+1,RIGHT(i)=2(i+1) PARENT(i)=(i1)/2,LEFT(i)=2i+1,RIGHT(i)=2(i+1)

堆有关操作(以最小堆为例)

  1. 维护堆
  2. 建堆

维护堆

  这是用于维护堆的性质的过程。它的输入为一个数组 A A A和一个下标 i i i。在调用这个过程我们假定 A [ i ] A[i] A[i]的左右子树都满足最小堆的性质。但是 A [ i ] A[i] A[i]可能大于 A [ L E F T ( i ) ] A[LEFT(i)] A[LEFT(i)] A [ R I G H T ( i ) ] A[RIGHT(i)] A[RIGHT(i)],这就不满足最小堆的性质。维护堆的过程通过让 A [ i ] A[i] A[i]在最小堆中"逐级下降",从而使得以 A [ i ] A[i] A[i]为根节点的子树满足堆的性质。

#define PARENT(i) (i-1)/2
#define LEFT(i) 2*i+1
#define RIGHT(i) 2*(i+1)
//维护最小堆
void Min_Heapify(vector<int> &A,int i,int heapSize){
    int left = LEFT(i);
    int right = RIGHT(i);

    int min = i;

    if(left<heapSize&&A[min]>A[left]){
        min = left;
    }
    if(right<heapSize&&A[min]>A[right]){
        min = right;
    }

    if(min != i){
        //交换
        swap(A[i],A[min]);
        Min_Heapify(A,min,heapSize); 
    }
    
}

  时间复杂度分析:对于以一棵以 A [ i ] A[i] A[i]为根节点,大小为 n n n的子树, M i n _ H e a p i f y Min\_Heapify Min_Heapify的时间代价包括:调整 A [ i ] A[i] A[i]与其子节点的关系时间代价 Θ ( 1 ) \Theta(1) Θ(1),加上在 A [ i ] A[i] A[i]子树上运行 M i n _ H e a p i f y Min\_Heapify Min_Heapify的时间代价。这个子树的最大为2n/3(最坏情况发生在树的底层恰好半满的时候,如下图)。因此可得递归式:
T ( n ) ≤ T ( 2 n / 3 ) + Θ ( 1 ) T(n)\le T(2n/3)+\Theta(1) T(n)T(2n/3)+Θ(1)
由主定理可得 T ( n ) = O ( lg ⁡ n ) T(n)=O(\lg n) T(n)=O(lgn)

最坏情况(示例):

在这里插入图片描述

建堆

  可以通过自底向上的方法利用维护堆的过程把一个数组转换成最小堆。

void BulidMinHeap(vector<int> &A,int heapSize){
    //从非叶节点开始
    for(int i = heapSize/2;i>=0;i--){
        Min_Heapify(A,i,heapSize);
    }
}

时间复杂度分析:对于 M i n _ H e a p i f y Min\_Heapify Min_Heapify其时间复杂度为 O ( lg ⁡ n ) O(\lg n) O(lgn),会调用 n n n次,因此建堆的复杂度为 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)。的确这个上界是正确的,但不是渐近紧确的。利用以下两个性质可以得到更紧确的界:

  1. 含有 n n n个元素的堆的高度为 ⌊ lg ⁡ n ⌋ \lfloor\lg n\rfloor lgn
  2. 高度为 h h h的堆最多包含 ⌈ n / 2 h + 1 ⌉ \lceil n/2^{h+1}\rceil n/2h+1个节点

因此建堆的代价:
∑ h = 0 ⌊ lg ⁡ n ⌋ ⌈ n / 2 h + 1 ⌉ O ( h ) = O ( n ∑ h = 0 ⌊ lg ⁡ n ⌋ h 2 h ) \sum_{h=0}^{\lfloor\lg n\rfloor}\lceil n/2^{h+1}\rceil O(h)=O(n\sum_{h=0}^{\lfloor\lg n\rfloor}\frac{h}{2^h}) h=0lgnn/2h+1O(h)=O(nh=0lgn2hh)
对于
∑ h = 0 ⌊ lg ⁡ n ⌋ h 2 h \sum_{h=0}^{\lfloor\lg n\rfloor}\frac{h}{2^h} h=0lgn2hh
的求法。

解释如下:

对于无穷递减几何级数:
∑ i = 0 ∞ x k = 1 1 − x , ∣ x ∣ < 1 两 边 对 x 求 导 得 : ∑ i = 0 ∞ k x k − 1 = 1 ( 1 − x ) 2 , ∣ x ∣ < 1 两 边 同 乘 以 x 得 : ∑ i = 0 ∞ k x k = x ( 1 − x ) 2 , ∣ x ∣ < 1 于 是 有 : ∑ h = 0 ⌊ lg ⁡ n ⌋ h 2 h = 1 / 2 ( 1 − 1 / 2 ) 2 = 2 \sum_{i=0}^{\infin }x^k=\frac{1}{1-x},|x|<1\\两边对x求导得:\sum_{i=0}^{\infin }kx^{k-1}=\frac{1}{(1-x)^2},|x|<1\\两边同乘以x得:\sum_{i=0}^{\infin }kx^{k}=\frac{x}{(1-x)^2},|x|<1\\于是有:\sum_{h=0}^{\lfloor\lg n\rfloor}\frac{h}{2^h}=\frac{1/2}{(1-1/2)^2}=2 i=0xk=1x1,x<1xi=0kxk1=(1x)21,x<1xi=0kxk=(1x)2x,x<1h=0lgn2hh=(11/2)21/2=2
所以有:建堆的代价时间复杂度为 O ( n ) O(n) O(n)

堆排序

  堆排序算法,首先将输入数组 A [ 1 … … n ] A[1……n] A[1n]建成最大堆,其中 n = A . l e n g t h n=A.length n=A.length。然后交换 A [ 0 ] A[0] A[0] A [ n ] A[n] A[n]。就能将最小元素放在最尾,然后减少 h e a p s i z e heapsize heapsize。重新建堆,再进行交换。以此往复,就能得到排序的数组。

void HeapSort(vector<int> &A){
    BulidMinHeap(A,A.size());\\第2参数为heapSize
    for (size_t i = A.size(); i > 1 ; i--)
    {
        swap(A[0],A[i-1]);
        Min_Heapify(A,0,i-1);\\第3参数为heapSize
        
    }
}

时间复杂度分析:首先建堆为 O ( n ) O(n) O(n),进行了 n − 1 n-1 n1次的维护,一共是 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)。因此堆排序的时间复杂度是 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)

完整测试代码:

#include <iostream>
#include <vector>
using namespace std;

#define PARENT(i) (i-1)/2
#define LEFT(i) 2*i+1
#define RIGHT(i) 2*(i+1)

//维护最小堆
void Min_Heapify(vector<int> &A,int i,int heapSize){
    int left = LEFT(i);
    int right = RIGHT(i);

    int min = i;

    if(left<heapSize&&A[min]>A[left]){
        min = left;
    }
    if(right<heapSize&&A[min]>A[right]){
        min = right;
    }

    if(min != i){
        //交换
        swap(A[i],A[min]);
        Min_Heapify(A,min,heapSize); 
    }
    
}

void BulidMinHeap(vector<int> &A,int heapSize){
    for(int i = heapSize/2;i>=0;i--){
        Min_Heapify(A,i,heapSize);
    }
}

void HeapSort(vector<int> &A){
    BulidMinHeap(A,A.size());
    for (size_t i = A.size(); i > 1 ; i--)
    {
        swap(A[0],A[i-1]);
        Min_Heapify(A,0,i-1);
        
    }
}
int main(){
    vector<int> v1 = {7,9,8,4,5,9,10};
    HeapSort(v1);
    for(auto x: v1) cout<<x<<",";
    system("pause");
    return 0;
}

优先队列(priority queue)

优先队列(priority queue)是一种用来维护由一组元素构成的集合 S S S的数据结构。其中每个元素都有一个有关的值,称为关键字(key)。

以最小优先队列为例。一个最小优先队列应该支持的操作:

  1. I N S E R T ( S , x ) INSERT(S,x) INSERT(S,x):把元素 x x x插入到集合 S S S中。这一操作等价于 S = S ∪ x S=S\cup x S=Sx,时间复杂度 O ( lg ⁡ n ) O(\lg n) O(lgn)
  2. M I N I M U M ( S ) MINIMUM(S) MINIMUM(S):返回 S S S中具有最小关键字的元素,时间复杂度 Θ ( 1 ) \Theta(1) Θ(1)
  3. E X T R A C T − M I N ( S ) EXTRACT-MIN(S) EXTRACTMIN(S):去掉并返回 S S S中的具有最大关键字的元素,时间复杂度 O ( lg ⁡ n ) O(\lg n) O(lgn)
  4. D E C R E A S E − K E Y ( S , x , k ) DECREASE-KEY(S,x,k) DECREASEKEY(S,x,k):将元素 x x x的关键字值减少到 k k k,这里假设 k k k的值不大于 x x x的原关键字,,时间复杂度 O ( lg ⁡ n ) O(\lg n) O(lgn)

如何用堆实现最小优先队列?直接上代码(内附部分注释):

#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;

#define PARENT(i) (i-1)/2
#define LEFT(i) 2*i+1
#define RIGHT(i) 2*(i+1)

void Min_Heapify(vector<int> &A,int i,int heapSize){
    int left = LEFT(i);
    int right = RIGHT(i);

    int min = i;

    if(left<heapSize&&A[min]>A[left]){
        min = left;
    }
    if(right<heapSize&&A[min]>A[right]){
        min = right;
    }

    if(min != i){
        //交换
        swap(A[i],A[min]);
        Min_Heapify(A,min,heapSize); 
    }
    
}

void BulidMinHeap(vector<int> &A,int heapSize){
    for(int i = heapSize/2;i>=0;i--){
        Min_Heapify(A,i,heapSize);
    }
}

void HeapSort(vector<int> &A){
    BulidMinHeap(A,A.size());
    for (size_t i = A.size(); i > 1 ; i--)
    {
        swap(A[0],A[i-1]);
        Min_Heapify(A,0,i-1);
        
    }
}

class priority_queue
{
private:
    vector<int> key_heap;
    int heapsize;
public:

    priority_queue(const vector<int> &data):key_heap(data),heapsize(data.size()){
        BulidMinHeap(key_heap,heapsize);
    };
    ~priority_queue(){};

    //返回最小元素
    int Minimun(){
        return key_heap[0];
    }
    //剔除最小元素,并返回它
    int ExtractMin(){
        if(heapsize<1) {
            cout<<"error:heapsize小于1"<<endl;
            return INT32_MIN;
        }
        int min = key_heap[0];
        swap(key_heap[0],key_heap[heapsize-1]);
        --heapsize;
        Min_Heapify(key_heap,0,heapsize);
        return min;
    }
    //将key_heap[i]减少至key
    void DecreaseKey(int key,int i){
        if(i>heapsize) {
            cout<<"超出堆大小"<<endl;
            return;
        }
        if(key_heap[i]<key) {
            cout<<"key太大"<<endl;
            return;
        }
        key_heap[i] = key;
        //维护堆
        while(i>0&&key_heap[i]<key_heap[PARENT(i)]){
            swap(key_heap[i],key_heap[PARENT(i)]);
            i = PARENT(i);
        }
    }
    //insert key
    void InsertKey(int key){
        heapsize++;
        if(heapsize>key_heap.size()){
            key_heap.push_back(INT32_MAX);
        }else
        {
            key_heap[heapsize-1] = INT32_MAX;
        }
        DecreaseKey(key,heapsize-1);
    }

    //重载一个操作符,方便访问元素
    int& operator[](int i){return key_heap[i];}
    //返回对大小
    int HeapSize(){return heapsize;}
};

int main(){
    vector<int> testdata = {19,6,7,23,2,3};
    priority_queue p1(testdata);
    //查看是否建成最小堆
    for(size_t i = 0; i < p1.HeapSize(); i++){cout<<p1[i]<<",";}//输出2,6,3,23,19,7,确实是最小堆
    cout<<endl;
    cout<<"堆大小:"<<p1.HeapSize()<<endl;//输出6

    //测试 ExtractMin,连续剔除7次
    for (size_t i = 0; i < 7; i++)
    {
        cout<<"拿出:"<<p1.ExtractMin()<<","<<"堆大小变为:"<<p1.HeapSize()<<endl;
    }
    cout<<endl;

    //测试insert key
    cout<<"新插入元素:";
    for (size_t i = 0; i < 10; i++)
    {   
        int new_key = i;
        new_key = rand()%100;
        cout<<new_key<<",";
        p1.InsertKey(new_key);

    }
    cout<<endl;
    for(size_t i = 0; i < p1.HeapSize(); i++){cout<<p1[i]<<",";}
    system("pause");
    
}

Reference

  1. 《算法导论》第六章

  2. 堆维基百科

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值