数据结构——堆
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