对”堆”的理解

Posted Liberal-man

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对”堆”的理解相关的知识,希望对你有一定的参考价值。

对”堆”的理解

  • 向下调整
  • 向上调整
    堆是一种特殊的完全二叉树,至于什么是完全二叉树自己搜吧,这里就不讲了,看图:
    堆的样例

如上,所有父结点都比子结点要小,符合这样特点的完全二叉树我们称为小顶堆。反之,如果所有父结点都比子结点要大,这样的完全二叉树称为大顶堆。那这一特性的实际意义是做什么呢?
假如有12个数分别是80、7、35、24、19、50、11、2、16、25、67、97,要找出这12个数中最小的数,请问怎么办呢?最简单的方法就是将这12个数从头到尾依次扫一遍,用一个循环就可以解决。这种方法的时间复杂度是O(12)也就是O(N),如下,灰色部分就是相关算法:
如下,灰色部分就是相关算法:

#include <stdio.h>
#include <limits.h>
int main(){
    int A[] = { 80, 7, 35, 24, 19, 50, 11, 2, 16, 25, 67, 97};
    int i = 0, min = INT_MAX; // INT_MAX 表示无穷大
    for (i = 0; i < sizeof(A)/sizeof(int); i++){
        if (A[i] < min)
            min = A[i];
    }
    printf("%d\\n", min);
    return 0;
}

输出结果:2
现在我们需要删除其中最小的数2,然后增加一个新数17,再次找这12个数中最小的一个数。请问该怎么办呢?按上一步的方法,扫描所有的数,找到新的最小的数,这个时间复杂度也是O(N)。假如现在有12次这样的操作(删除最小的数后并添加一个新数)。那么整个时间复杂度就是 12O(12)=O(144) O(N2) 。那有没有更好的方法呢?堆这个特殊的结构恰好能够很好地解决这个问题。
首先我们先把这个12个数按照最小堆的要求放入一棵完全二叉树:
初始化堆

注意:我们这里,堆在程序中的保存方式,是用一个线性数组来存储的,比如数组A[N]。实际存储数据的时候,我们数据的排列起点是从A[1]开始的,A[0]没有使用,置空,主要是为了便于直观理解算法,降低复杂度。A[1]就是堆顶,A[i*2]就是其左孩子,A[i*2+1]就是其右孩子(i=1)。

很显然最小的数就是堆顶A[1]位置的2。接下来,我们将堆顶的数替换为17。新数已经不符合最小堆的特性,我们需要将新增加的数调整到合适的位置。那如何调整呢?

向下调整

替换顶部为17
我们需要将17与它的两个儿子7和24比较,并选择较小一个与它交换,交换之后如下。
向下调整1
我们发现此时还是不符合最小堆的特性,因此还需要继续向下调整。于是继续将17与它的两个儿子11和67比较,并选择较小一个交换,交换之后如下。
向下调整2
到此,还是不符合最小堆的特性,仍需要继续向下调整直到符合最小堆的特性为止。
向下调整3
我们发现现在已经符合最小堆的特性了。综上所述,当新增加一个数被放置到堆顶时,如果此时不符合最小堆的特性,则将需要将这个数向下调整,直到找到合适的位置为止,使其重新符合最小堆的特性。
向下调整路径描绘

向下调整的代码如下,godown函数是调整的实现:

#include <stdio.h>
#define N 12
void printHeap(int A[]){
    printf("            -----------%d-----------\\n", A[1]);
    printf("           /                       \\\\\\n");
    printf("      ----%d----               ----%d----\\n", A[2], A[3]);
    printf("     /          \\\\             /          \\\\\\n");
    printf("  --%d--     ---%d---     ---%d          %d\\n", A[4], A[5], A[6], A[7]);
    printf(" /      \\\\   /        \\\\   /\\n");
    printf("%d      %d %d        %d %d\\n", A[8], A[9], A[10], A[11] , A[12]);
}
void swap(int A[], int t, int i){
    A[t] = A[t] ^ A[i];
    A[i] = A[t] ^ A[i];
    A[t] = A[i] ^ A[t];
}
/* 向下调整。
 * @param A 堆的线性存储方式,数组
 * @param n 节点总数
 * @param i 需要向下调整的节点编号
 */
void godown(int A[], int n, int i) //传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整 
{
    int t,flag=0,count=0;//flag用来标记是否需要继续向下调整 
    //当i结点有儿子的时候(其实是至少有左儿子的情况下)并且有需要继续调整的时候循环窒执行
    while( i*2<=n && flag==0 )
    {        
        //首先判断他和他左儿子的关系,并用t记录值较小的结点编号 
        if( A[i] > A[i*2] )
            t=i*2;
        else
            t=i; 
        //如果他有右儿子的情况下,再对右儿子进行讨论 
        if(i*2+1 <= n)
        {
            //如果右儿子的值更小,更新较小的结点编号  
            if(A[t] > A[i*2+1])
                t=i*2+1;
        }
        //如果发现最小的结点编号不是自己,说明子结点中有比父结点更小的  
        if(t!=i)
        {
            swap(A, t, i);//交换
            i=t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整
            printf("\\n\\n第 %d 次调整堆:\\n", ++count);
            printHeap(A);
        }
        else
            flag=1;//则否说明当前的父结点已经比两个子结点都要小了,不需要在进行调整了,退出
    }
}

int main(){
    int A[N+1] = { 0, 2, 7, 24, 11, 67, 35, 25, 16, 19, 97, 80, 50}; // 这里为了直接展示调整堆算法,直接给出已经建好堆的数组,免去建堆这一步,A[0]是不使用的,堆从A[1]开始。
    printf("初始化堆:\\n");
    printHeap(A);
    A[1] = 17; // 测试,修改堆顶值
    printf("\\n\\n修改堆顶值为17:\\n");
    printHeap(A);
    godown(A, N, 1);
    return 0;
}

我们刚才在对17进行调整的时候,竟然只进行了3次比较,就重新恢复了最小堆的特性。现在最小的数依然在堆顶为7。之前那种从头到尾扫描的方法需要12次比较,现在只需要3次就够了。现在每次删除最小的数并新增一个数,并求当前最小数的时间复杂度是O(3),这恰好是 O(log212) O(log2N) 简写为 O(logN) 。假如现在有10000个数,进行1万次替换最小数的操作,使用原来扫描的方法计算机需要运行大约10000*10000=1亿次;而现在只需要 NlogN 次(N=10000),即14*10000次=14万次(214=16384,所以 log10000 的对数值大概是14)。从亿到几万的量级跨度,可是跌了不少啊,那么算下节省了 (1亿次/14万次) 约是7百多倍的计算量,这可是巨大的优化啊。
看下上面程序的运行结果:
向下调整运行结果

向上调整

说到这里,如果只是想新增一个值,而不是替换或者删除最小值又该如何操作呢?即如何在原有的堆上直接插入一个新元素呢?只需要直接将新元素插入到末尾,再根据情况判断新元素是否需要上移,直到满足堆的特性为止。如果堆的大小为N(即有N个元素),那么插入一个新元素所需要的时间也是 O(logN) 。例如我们现在要新增一个数15。
向上调整路径图
先将15与它的父结点35比较,发现比父结点小,为了维护最小堆的特性,需要与父结点的值进行交换。交换之后发现还是要比它此时的父结点24小,因此需要再次与父结点交换。至此又重新满足了最小堆的特性。向上调整完毕后如下。
向上调整结果图

向上调整的代码goup函数如下。:

#include <stdio.h>
#define N 13
void printHeap(int A[]){
    printf("            -----------%d-----------\\n", A[1]);
    printf("           /                       \\\\\\n");
    printf("      ----%d-----               ----%d----\\n", A[2], A[3]);
    printf("     /          \\\\             /          \\\\\\n");
    printf("  --%d--     ---%d---     ---%d          %d\\n", A[4], A[5], A[6], A[7]);
    printf(" /      \\\\   /        \\\\   /\\n");
    printf("%d      %d %d        %d %d\\n", A[8], A[9], A[10], A[11] , A[12]);
}
void printHeap1(int A[]){
    printf("            -----------%d-----------\\n", A[1]);
    printf("           /                       \\\\\\n");
    printf("      ----%d-----               ----%d----\\n", A[2], A[3]);
    printf("     /          \\\\             /          \\\\\\n");
    printf("  --%d--     ---%d---     ---%d---       %d\\n", A[4], A[5], A[6], A[7]);
    printf(" /      \\\\   /        \\\\   /        \\\\\\n");
    printf("%d      %d %d        %d %d        %d\\n", A[8], A[9], A[10], A[11] , A[12], A[13]);
}
void swap(int A[], int t, int i){
    A[t] = A[t] ^ A[i];
    A[i] = A[t] ^ A[i];
    A[t] = A[i] ^ A[t];
}
/* 向上调整。
 * @param A 堆的线性存储方式,数组
 * @param n 节点总数
 * @param i 需要向上调整的节点编号
 */
void goup(int A[], int n, int i) //传入一个需要向上调整的结点编号i
{
    int flag=0, count = 0; // flag用来标记是否需要继续向上调整
    if(1 == i)  return; //如果是堆顶,就返回,不需要调整了    
    //不在堆顶 并且 当前结点i的值比父结点小的时候继续向上调整 
    while(i!=1 && flag==0)
    {
        //判断是否比父结点的小 
        if (A[i]<A[i/2]) {
            swap(A, i, i/2);//交换和父节点的位置
            printf("\\n\\n第 %d 次调整堆:\\n", ++count);
            printHeap1(A);
        }
        else
            flag=1;//表示已经不需要调整了,当前结点的值比父结点的值要大 
        i=i/2; //更新编号i为它父结点的编号,从而便于下一次继续向上调整 
    }
}


int main(){
    int A[N+1] = { 0, 2, 7, 24, 11, 67, 35, 25, 16, 19, 97, 80, 50}; // 这里为了直接展示调整堆算法,直接给出已经建好堆的数组,免去建堆这一步,A[0]是不使用的,堆从A[1]开始。
    printf("初始化堆:\\n");
    printHeap(A);
    A[13] = 15; // 测试,堆尾增加值
    printf("\\n\\n堆尾增加一个15:\\n");
    printHeap1(A);
    goup(A, N, 13);
    return 0;
}

运行结果:
向上调整运行结果


始于2009-05-26,西理工科协;更新至2016-06-02,杭州。

以上是关于对”堆”的理解的主要内容,如果未能解决你的问题,请参考以下文章

植物大战 堆排序——纯C

python-堆排序

堆排序

堆排序

数据结构-堆的概念及实现

C语言实现的堆的向下调整算法和建堆算法