数据结构与算法-堆、基于堆实现的优先队列、堆排序

堆的概念

堆是一种树形数据结构,每个节点都有一个值,通常我们说的堆是指二叉堆,是一种完全二叉树结构,堆的特点是根结点的值最小(或最大),根节点大的称为大顶堆、根节点小的称为小顶堆,且根结点的两个子树也是一个堆,这里的堆区别于内存中的堆。堆可用于堆排序、优先队列等。在这里我们使用数组存储,在数组中按层级储存(不使用数组第一个位置)。
二叉堆

如何构造堆?

如何构造堆就是如何让堆有序化,因为刚开始输入数组中的数字都是乱序的,不符合堆的特点(堆的特点是根结点的值最小或最大)所以不叫堆。
所以当一颗二叉树的根节点都大于或者小于它的两个子节点的时候,它被称为堆有序。
下面介绍两种堆有序的方法以构造大顶堆为例
由下至上的堆有序化(上浮)
既然是由下至上,所以我们肯定是从最后节点出发,如果该节点大于父节点,交换两节点,如果交换后该节点还大于其现在的父节点,继续换。直到遇到一个更大的父节点,就结束。

void swim(int k)
{
while(k>1&&num[k/2]<num[k]) //如果该节点(num[k])大于父节点(num[k/2]),则继续交换
{
  //交换操作
  int temp=num[k];
  num[k]=num[k/2];
  num[k/2]=temp;
  k=k/2//继续往上
}
}

由上至下的堆有序化(下沉)
有了上浮的思想,其实下沉也差不多,从第一个节点开始,如果比其两个子节点还小,那么就与两个子节点中较大的来交换,如果他还比其现在的两个子节点小,继续下沉。直到遇到没有比他更小的子节点。

void sink(int k){
while(2*k<N){
     int j=2*k;   
     if(num[j]>num[j+1]) j++; //确定交换节点,左右谁大谁与父节点换。
      if(num[k]<num[j])//如果父节点小于其左右子节点中最大的,那么交换。
      {
         int temp=num[k];
         num[k]=num[j];
         num[j]=temp;
         k=j;
       }else break;//否则满足条件退出循环    
}
}

当把所有节点遍历完后,顶点应该就是这些数据里最大的数了。

使用二叉堆实现优先队列

  • 优先队列概念

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。

简单点说就是一个普通队列赋予了优先级,本来先进先出,现在是优先级最高先出。
我自己的理解来说就是一个排序好的队列。(不够严谨,仅供参考)

  • 场景描述

许多应用程序都需要处理有序的元素,但不一定要他们全部有序,比如我每次处理完当前任务,下一个执行的肯定是优先级最高的。所以这就需要一个优先级别的队列,如果这个队列容量为10,使用普通排序方法也可以找出前十个,但是效率太低,做了很多无用功,而且每当第一个元素出队,都需要重新排序一次。

  • 堆实现
    这个时候我们就可以考虑我们前面所说的堆这种结构了
    假设我们已经构造好了一个堆,那么第一个元素肯定是最大的,就可以放入优先队列中,那么我们的堆中就可以删除这个数字了,假设存到最后一个元素就是模拟了删除
    将第一个元素与最后一个元素交换,然后N-1,这个时候的堆是乱序的,我们需要让它再次有序,这个时候执行下沉,将第一个元素下沉到一个合适的位置,操作完成后。第一个元素又是当前最大元素了。 那么当优先队列中最大的执行完删除后就可以添加此元素进入队列中了。基于堆的优先队列,插入元素操作不超过lgN+1次比较,删除最大元素操作不超过2lgN比较。这个数据是专家的出来的结论,可证明。

void delMax(){  //处理获得的最大元素
int max=num[1]; //将第一个元素与最后一个交换
num[1]=num[N];
num[N]=max;
N--;
sink(1);//重新下沉
return max; //返回最大值
}
  • 插入操作
    其实上面的操作跟堆的删除操作是一样的,有出得有入啊,那么堆的插入呢?
    插入同样很简单,一般插入都是插入到最后一个元素,然后上浮到合适位置。
//插入操作一般使用上浮,上文中的函数swim()
void insert(int insValue){ //insValue为需要插入的值,N为当前堆中最后一个元素。
num[N+1]=insValue; //插入到最后一个元素后面
swim(N+1//调用上浮函数,浮到合适位置。
}

堆排序

有了上面的思路以后,其实它的排序大家也差不多清楚了,我们可以把任意优先队列变成一种排序方法,如果用无序数组实现优先队列,其实相当于做了一次选择排序。而如果用基于堆的优先队列这样做其实就是一种新的排序——堆排序。
首先我们可以这么分析,用一个数组储存了一些数字以后,先肯定要把它构造成堆,那么可以用上浮或者下沉的方式,循环遍历所有元素来构造一个堆。然后为了避免空间的浪费,每次排序好后将堆顶元素与最后一个元素交换,然后N-1,将交换后的元素下沉,恢复有序。循环这个操作这样最后的数组就一个递增数组。所以大顶堆排序出来的是递增数组,小顶堆排序出来的是递减数组。

void sort(){
  for(int i=1;i<=N;i++) //使用下沉法构造堆
    sink(i);
    int j=N;
while(j>1){
    int temp=num[1]; //第一个与最后一个交换
    num[1]=num[j];
    num[j]=temp;
    j--;
    sink(1); //重新构造有序
}
}

其实在构造堆的代码中我们是可以优化的,因为我们在这个阶段的目标只是构造一个堆有序,使最大元素在数组开头。所以我们可以避免很多重复操作,从树的倒数第二行开始使用sink函数将其都排成堆,然后再往上一行 倒数第三行,因为刚刚已经把倒数第二行排成了堆,所以在这行用sink可以直接把两个子节点变成一个堆,而不用担心还会遍历到倒数第二行去。同理,着层使用,直到在第一行调用一次sink 扫描就结束了。

for(int i=N/2;i>=1;i--) //N/2即倒数第二行,别问我为什么是。。
     sink(i);

总结

堆排序是我们所知唯一能够同时最优利用空间和时间的方法,空间上,她它只有在交换的时候需要一个额外的空间,时间上最多需要2NlgN+2N次比较,而相比较块排N²/2;数据量越大越明显。另一方面用堆实现的优先队列在现代应用程序中已经越来越重要了,因为它的插入和删除最大元素操作可以保证对数级运算时间。


注:文中代码均只展现核心思想。

猜你喜欢

转载自blog.csdn.net/hjsir/article/details/78507602