heap

堆 heap


利用完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在 O(1)~O(logn) 之间。

完全二叉树

  • 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。我们知道二叉树可以用数组模拟,堆自然也可以。
  • 从图中可以看出,元素的父亲节点数组下标是本身的1/2(只取整数部分),所以我们很容易去模拟,也很容易证明其所有操作都为log级别~~
堆还分为两种类型:大根堆、小根堆

顾名思义,就是保证根节点是所有数据中最大/小,并且尽力让小的节点在上方
不过有一点需要注意:堆内的元素并不一定数组下标顺序来排序的!!很多的初学者会错误的认为大/小根堆中
下标为1就是第一大/小,2是第二大/小……

我们刚刚画的完全二叉树中并没有任何元素,现在让我们加入一组数据吧!
下标从1到9分别加入:{8,5,2,10,3,7,1,4,6}。

如下图所示

现在我就来介绍一下堆的几个基本操作:

  • 上浮 shift_up;
  • 下沉 shift_down
  • 插入 push
  • 弹出 pop
  • 取顶 top
  • 堆排序 heap_sort

小根堆 为例

从上述未处理过的数据中可以很容易得出,根节点1元素8绝对不是最小的
但是它的一个子节点3(元素2)比它小,我们可以将它放到最高点,直接进行交换。
此外,子节点3的子节点7(元素1)似乎更适合在根节点
此时,我们无法直接和根节点交换的,那么就是用上浮 shift_up操作来完成。

操作过程如下

从当前结点开始,和它的父亲节点比较,若是比父亲节点来的小,就交换,然后将当前询问的节点下标更新为原父亲节点下标;否则退出。

伪代码如下:

Shift_up( i )
{
    while( i / 2 >= 1)
    {
        if( 堆数组名[ i ] < 堆数组名[ i/2 ] )
        {
            swap( 堆数组名[ i ] , 堆数组名[ i/2 ]) ;
            i = i / 2;
        }
        else break;
}

上浮操作结束后,节点3(元素8)与其子节点7(元素2)的位置并不正确。
因此,需要节点3下沉

节点的下沉策略如下所述

小根堆是尽力要让小的元素在较上方的节点,而下沉与上浮一样要以交换来不断操作。
让当前结点的子节点(如果存在)作比较,哪个比较小就和它交换,并更新询问节点的下标为被交换的子节点下标,否则退出。

伪代码如下所示

Shift_down( i , n )    //n表示当前有n个节点
{
    while( i * 2 <= n)
    {
        T = i * 2 ;
        if( T + 1 <= n && 堆数组名[ T + 1 ] < 堆数组名[ T ])
            T++;
        if( 堆数组名[ i ] < 堆数组名[ T ] )
        {
           swap( 堆数组名[ i ] , 堆数组名[ T ] );
            i = T;
        }
        else break;
}

插入操作

  • 如何在插入的时候维护堆?

每次进行数据插入的时候,往堆的最后插入,然后使用上浮操作。

伪代码如下所示

Push ( x )
{
    n++;
    堆数组名[ n ] = x;
    Shift_up( n );
}

弹出操作

使用根节点元素和尾节点进行交换,然后使现在的根元素下沉。

伪代码如下所示

Pop ( x )
{
    swap( 堆数组名[1] , 堆数组名[ n ] );
    n--;
    Shift_down( 1 );
}

取顶操作

返回节点数组[0]

堆排序

new 新数组,每次取堆顶元素放进去,然后弹掉堆顶

伪代码如下所示

Heap_sort( a[] )
{
    k=0;
    while( size > 0 )
    {
        k++;
        a[ k ] = top();
        pop();    
    }        
}

堆排序的时间复杂度是O(nlogn)

堆操作代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>

#define maxn 100010   //这部分可以自己定义堆内存多少个元素 

using namespace std;

struct Heap {
    int size, queue[maxn];
    Heap() {       //初始化
        size=0;
        for(int i=0; i<maxn; i++)
            queue[i]=0;
    }
    void shift_up(int i) { //上浮
        while(i > 1) {
            if(queue[i] < queue[i>>1]) {
                int temp = queue[i];
                queue[i] = queue[i>>1];
                queue[i>>1] = temp;
            }
            i >>= 1;
        }
    }
    void shift_down(int i) { //下沉
        while((i<<1) <= size) {
            int next = i<<1;
            if(next < size && queue[next+1] < queue[next])
                next++;
            if(queue[i] > queue[next]) {
                int temp = queue[i];
                queue[i] = queue[next];
                queue[next] = temp;
                i = next;
            } else return ;
        }
    }
    void push(int x) { //加入元素
        queue[++size] = x;
        shift_up(size);
    }
    void pop() {       //弹出操作
        int temp = queue[1];
        queue[1] = queue[size];
        queue[size] = temp;
        size--;
        shift_down(1);
    }
    int top() {
        return queue[1];
    }
    bool empty() {
        return size;
    }
    void heap_sort() {  //另一种堆排方式,由于难以证明其正确性
        //我就没有在博客里介绍了,可以自己测试
        int m=size;
        for(int i = 1; i <= size; i++) {
            int temp = queue[m];
            queue[m] = queue[i];
            queue[i] = temp;
            m--;
            shift_down(i);
        }
    }
};

int main()
{
    Heap Q;
    int n,a,i,j,k;
    cin>>n;
    for(i = 1; i <= n; i++) {
        cin >> a;
        Q.push(a); //放入堆内
    }

    for(i = 1; i <= n; i++) {
        cout << Q.top() << " ";  //输出堆顶元素
        Q.pop();        //弹出堆顶元素
    }
    return 0;
}

原文地址:https://www.cnblogs.com/openxyz/p/6923662.html