浅谈堆

数据结构——堆

1. 概述

堆(也叫优先队列),是一棵完全二叉树,它的特点是父节点的值大于(小于)两个子节点的值(分别称为大顶堆和小顶堆)。它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等。

 

2. 堆的基本操作

堆是一棵完全二叉树,高度为O(lg n),其基本操作至多与树的高度成正比。在介绍堆的基本操作之前,先介绍几个基本术语:

A:用于表示堆的数组,下标从1开始,一直到n

PARENT(t):节点t的父节点,即floor(t/2)

RIGHT(t):节点t的左孩子节点,即:2*t

LEFT(t):节点t的右孩子节点,即:2*t+1

HEAP_SIZE(A):堆A当前的元素数目

下面给出其主要的四个操作(以大顶堆为例)

 

2.1 Heapify(A,n,t)

该操作主要用于维持堆的基本性质。假定以RIGHT(t)和LEFT(t)为根的子树都已经是堆,然后调整以t为根的子树,使之成为堆。

void heapify(int t)//t根节点,注意t是根节点在a数组中的位置,也就是一个编号,不是对应的数值 
{
    int left=t*2,right=t*2+1;//左右孩子编号,注意也是编号 
    int maxn=t;//maxn为t,t的左孩子,t的右孩子中最大的,注意也是编号 
    if(left<=n) {maxn=a[maxn]>a[left] ? maxn:left;}// ? :为三目运算符,如果=和?之间的条件为真,等号左边的maxn为 :左边的maxn;否则为:右边的left 
    if(right<=n){maxn=a[maxn]>a[right] ? maxn:right;}
    if(maxn!=t)//不是根节点,即要调整 
    {
        swap(a[maxn],a[t]);//根节点和最大的孩子节点交换 
        heapify(maxn);//因为把根节点调下来之后,根节点有可能使所在子树不再是一个堆,所以要递归调整 
    }
}

注意heapify的调整是从上往下的,因为递归的参数是maxn,若能更新,maxn一定是t的孩子节点

问题1:heapify什么时候结束

当t为叶子节点时,left与right都>n,所以maxn不会更新,不满足maxn!=t的条件,结束

问题2:为什么heapify操作要满足左右子树都是堆得时候才进行?

因为heapify操作的结束条件,一旦有一个结点不能更新,此操作结束,而结点更新的条件是比较左右孩子。如果子树不是堆,可能存在当前结点和左右孩子比较,不能更新,但孩子的孩子可能可以更新。子树是堆得话,孩子的孩子一定不能更新

heapify是另外3个操作的基础,所以一定要搞懂

 

2.2     BuildHeap(A,n)

该操作主要是将数组A转化成一个大顶堆。思想是,先找到堆的最后一个非叶子节点(即为第n/2个节点),然后从该节点开始,从后往前逐个调整每个子树,使之称为堆,最终整个数组便是一个堆。

问题1:为什么是非叶子节点?

因为本操作要以heapify操作为基础,具体看代码。而heapify操作是通过比较与孩子节点的关系实现的,所以要满足节点要有孩子节点。

问题2:为什么是最后一个/为什么要从后往前?

最后一个和从后往前的是相互对应的,从最后一个开始,就是从后往前开始。

有人说了,我从第一个节点开始,从前往后不行吗?不行。

因为本操作要联系heapify操作,heapify操作要求节点t的左右子树已经是一个堆。若从前往后调整,以t为根节点的左右子树,在这之前没有调整,不能保证是堆

void build()
{
    for(int i=n/2;i;i--)
    heapify(i);
}

 

2.3 GetMaximum(A,n)

该操作主要是获取堆中最大的元素,同时保持堆的基本性质。堆的最大元素即为第一个元素,将其保存下来,同时将最后一个元素放到A[1]位置,之后从上往下调整A,使之成为一个堆。

int get()
{
    int top=a[1];//取出栈顶
    a[1]=a[n];//把最后一个元素放到栈顶
    n--;//取出之后元素少一个
    heapify(1);
    return top;
}

 

2.4  Insert(A, n, t)

向堆中添加一个元素t,同时保持堆的性质。算法思想是,将t放到A的最后,然后从该元素开始,自下向上调整,直至A成为一个大顶堆。

void insert(int k)//在数组a中加入元素k,注意k不是编号,是一个确切的值 
{
    n++;
    a[n]=k;//把k放在最后一个地方 
    int p=n;//p为当前调整位置,每次都从最后一个位置开始 
    while(p>1&&a[p/2]<k)//p>1,即p不是根节点,同时p的父节点小于k,注意<后面不能是a[p]
        a[p]=a[p/2];//把p的父节点挪下来 
        p/=2;//p指向父节点的位置 
    }
    a[p]=k;//最终k的位置为p,赋值 
}

问题1:为什么<后面不能是a[p]?

 

 


由此可见,每次更新只是把父节点移到子节点处,父节点暂时保留原值,等到下一次以这个父节点为子节点时,再赋值。所以如果比较的是a[p],就比较的是原来父节点的值,而实际上此处父节点应为要添加的数k

 

问题2:insert操作为什么要自下而上?

问题3:insert操作结束条件为什么有p>1?

当p=1时为根节点,本操作为自下而上,根节点无法向上操作

 

3.  堆的应用

3.1  堆排序

堆的最常见应用是堆排序,时间复杂度为O(N lg N)。如果是从小到大排序,用大顶堆;从大到小排序,用小顶堆。

3.2  在O(n lg k)时间内,将k个排序表合并成一个排序表,n为所有有序表中元素个数。

【解析】取前100 万个整数,构造成了一棵数组方式存储的具有小顶堆,然后接着依次取下一个整数,如果它大于最小元素亦即堆顶元素,则将其赋予堆顶元素,然后用Heapify调整整个堆,如此下去,则最后留在堆中的100万个整数即为所求 100万个数字。该方法可大大节约内存。

3.3 一个文件中包含了1亿个随机整数,如何快速的找到最大(小)的100万个数字?(时间复杂度:O(n lg k))

 

4. 总结

堆是一种非常基础但很实用的数据结构,很多复杂算法或者数据结构的基础就是堆,因而,了解和掌握堆这种数据结构显得尤为重要。

 

5. 参考资料

经典算法教程《算法导论》

 

Codevs堆练习

黄金:2830、2879、2995、3110

钻石:1052、1063、1245、1246、2057、2573、3377

大师:1021、1765、2069、2913、3032

posted @ 2016-12-31 09:59  TRTTG  阅读(434)  评论(0编辑  收藏  举报