数据结构--堆,可能我们只在《数据结构》这本书上接触过,实际应用中很少用到(至少我觉得是这样)。那么这到底是一种怎样的结构?引入它有什么好处?先来分析这两个问题
生活中,我们常可见到各种土堆,石堆等等,类似地,“堆”这个名词可以想到是顶端窄,下端越来越宽的金字塔结构。形象化的一种表示方式就是二叉树,当然它是一种特殊的二叉树,除最下层外都是满二叉树,并且最下层是从左到右依次排列没有空间隔(层序遍历不会遇到空节点)
堆可以用一维数组表示,从而大大简化空间成本,例如
注意到堆结构的特殊化,如果规定第一个元素从下标1开始,那么容易得到:
root = 1
value(i) = x[i]
leftchild(i) = 2*i
rightchild(i) = 2*i+1
parent(i) = i/2
null(i) = (i<1) or (i>n)
关键是一个节点编号i和它的左右子节点(2i,2i+1)的关系,有了这一点,就能很方便按照“堆”逻辑,对数组进行操作
接下来是“堆”的另一种重要属性,节点的值大于或等于父节点的值(称为小顶堆,类似的有大顶堆)。如果元素[1...n]满足,就称为满足性质heap(1,n)
第一个问题:假设有已经具备性质heap(1,n-1)的数组,要向其插入第n个元素并调整,使得最后满足关系heap(1,n),该怎么做?
利用堆的两个属性,我们可以将这个新的元素n与它的父节点比较,如果它比较小就互换位置以“上浮”,否则认为这就是它合适的位置,结束(交换操作始终保持着堆的性质,所以siftup操作完后整个数组即满足性质),这一实现类似后面的“优先级队列”insert
同样,设第一个元素1需要调整,已满足heap(2,n),类似地可以用子节点比较后“下沉”的方式,siftdown
第二个问题:需要取出堆中的最小元素(这个好办),并使剩下的(2...n)保持heap性质,怎么办呢?
这个问题直观上来看可能会让人犯难,移除第一个元素后,第二个元素作为根,按这样的逻辑后续数据全部得调整……
堆的性质很简单,它并不要求数据是有序的(往往会想成是这样),而只需要满足节点值比子节点值小,基于此,可以将最后一个节点n交换到1的位置,然后用“下沉”的方式维持heap(1,n-1)性质即可完成,这一操作不会破坏原本堆结构的其它部分,简单方便。详见后面的extract
对堆的操作只需要这两个接口,便可以实现排序,优先级队列等算法问题:
//使用一维数组表示堆,以1开始,n个元素
int heaparray[MAXSIZE],n=0;
void insert(int num)
{
int i,p;
heaparray(++n) = num;
for(i=n;i>1&&heaparray[p=i/2]>heaparray[i];i=p)
swap(p,i);
}
int extract()
{
int i,c;
int min = heaparray[1];
heaparray[1] = heaparray[n--];
for(i=1;(c=2*i)<=n;i=c)
{
if(c+1<=n && heaparray[c+1]<heaparray[c])
c++;
if(heaparray[i]<=heaparray[c])
break;
swap(c,i);
}
return min;
}
代码中的int可以换成抽象类型T,以支持更多的数据类型
用堆结构实现优先级队列的特点:相比有序序列和无序序列的插入取出元素的不同复杂度(O(1),O(n)),两种操作的时间复杂度都是O(logn),使得平均操作复杂度最好
有了堆数据结构,堆排序也就应运而生啦:
n=0;
void pqsort(int x[])
{
int i;
for(i=0;i<n;i++)
insert(x[i]);
for(i=0;i<n;i++)
x[i] = extract();
}
这样的堆排序简单易懂,但是需要额外的数据空间来存储堆数据结构
《编程珠玑》作者给出了一种精巧的算法,直接利用原数组直接操作的方式,不妨一看:
for i = [2,n]
siftup(i)
for (i= n;i>=2;i--)
swap(1,i)
siftdown(i-1)
其中siftup,siftdown即对数组两端元素进行调整堆操作的函数。注意到不同,这里做的是“大顶堆”调整,x[1]始终是最大的元素
第一个for循环将整个数组的每个数进行调整使其满足(大顶)堆的性质
第二个for循环依次将堆的第一个元素(最大),与位置i交换,使得排序过程中恰好是这样的一个情况:[1..i]为堆,[i...n]为已排序的序列,循环过程中随着i的减小,堆部分越来越小,排序序列越来越大直至到整个数组
----总结
堆排序的确用的不多,因为我们往往不关注性能,只关注正确性。使用良好的数据结构以及正确的边界处理,正是衡量程序员编码质量好坏的标准
就我们目前写代码而言,在提高程序健壮性的同时,需要多积累经验,学习一些比较巧妙的处理方式(这些方式往往需要很强的循环体边界处理能力)
掌握基本的数据结构,很重要~~