最大优先队列与堆

在《算法导论》中对队的介绍在第6章。堆排序的性能不是特别好,远远不如快速排序,但是在操作系统中的优先调度里面需要的优先队列结构中采用堆有着不错的性能。

什么是堆?堆可以理解成一种特殊的树,这种树的结点永远大于它的子节点。如:

堆

第一眼看上去就是一颗普通的二叉树,其实仔细的看就会发现这棵树上的父节点永远大于他的子节点,这就是堆。

用数组来表示二叉树和堆

还是看上面那个图,这次主要看序号。
假设这棵树用数组来表示,那么数组的序号如图所示。
我们可以发现以下3条规则:

  1. 父节点是 (子节点-1)/2 如0 = (2 - 1) / 2
  2. 左节点是 父节点2+1 如1 = 0 2 + 1
  3. 右节点是 父节点2+2 如2 = 0 2 + 2
    上面的除法是指整除,现在我们就可以用数组来表示这个堆了。

堆排序

如果,使这颗树永远保持这个特性,那么可以轻松证明他的根节点是树中各节点中的最大值节点。取出这个节点,放在数组的最后一位(或者是第一位),然后让这颗树继续保持这种特性,然后重复取出节点,那么最后这个数组就是一个排好顺序的数组。
道理很简单,关键在于两个问题:
1. 如何在开始的时候,如何初始化这个堆?
2. 如何在取出一个节点后,如何依旧保证堆的特性呢?
把问题分解后,是不是感觉不是特别难了呢?
对于上面这两个问题,有一个很基本的问题就是保持堆的特性,一直在强调堆的特性,其实就是父节点一定要大于他的左右子节点,如何用代码来表示:

void swap(int *a,int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

// arr是堆数组,k是要保证第几个节点维持该特性,n代表该堆数组的大小
void keep_sort(int *arr,int k,int n)
{
    // k是否有左节点,如果没有左节点,那就肯定也没有右节点,不用比较,退出即可
    if(k * 2 + 1  >= n)
    {
        return;
    }
    // k是否大于左节点,为真则交换父节点与左节点,因为左节点发生改变(并且一定是变小)
    // 所以,左节点要与自己的子节点进行比较是否变得比自己的子节点还小
    if(arr[k * 2 + 1] > arr[k])
    {
        swap(&arr[k * 2 + 1],&arr[k]);
        heap_property(arr,k * 2 + 1,n);
    }
    // 同理,看看有没有右节点
    if(k * 2 + 2  >= n)
    {
        return;
    }
    // 同理,k是否大于右节点
    if(arr[k * 2 + 2] > arr[k])
    {
        swap(&arr[k * 2 + 2],&arr[k]);
        heap_property(arr,k * 2 + 2,n);
    }
}

基本的问题解决了,接下来要解决上面的两个问题了。

void heap_sort(int *arr,int n)
{
    // 初始化节点,因为从n / 2 - 1开始才有子节点,所以从n / 2 - 1开始初始化
    // 如果想证明,可以根据左节点的计算方式来证明(其实就是逆运算)
    for(int i = n / 2 - 1;i >= 0;i--)
    {
        keep_sort(arr,i,n);
    }
    for(int i = 1;i < n;i++)
    {
        // 把最大值(根节点,数组下标为0的值)与数组末尾的值交换
        swap(arr[n - i],arr[0]);
        // 现在根节点不再是最大值,所以根节点需要重新排序,数组末尾的值已排好序
        // 所以,不再进行排序,数组总数-1
        keep_sort(arr,0,arr[n - i]);
    }
}

PS:尽量保证可以看懂,但是有一些部分可能还是用了一些办法,不过都可以证明的
PPS:上次写代码时没有这么顺利的,这次莫名其妙感觉完全会了

优先队列

其实,堆排序的速度是远远比不上快速排序的,但是他也有他的价值,就是优先队列。优先队列在操作系统的任务调度中很重要,操作系统可以根据任务中的重要性来选择哪个任务先执行,也可以根据哪个任务执行的速度最快来选择执行(实际中现代操作系统要比较复杂,集合了很多种算法)。
队列的特点是先进先出,优先队列就是优先级最高的先出队(如最大优先队列则是数值最大的先出队)。使用堆来实现优先队列的话,优先队列开始为空,优先队列的入队操作只需要进行一次维护堆的特性就可以完成,而优先队列出队也只需要进行一次维护堆的特性就可以完成。

// 设置队列中的最大容量,其实也可以设计为容量可变的优先队列,但是不再这里写了
#define MAX 100
// keep_sort函数依旧使用上面那个
void keep_sort(int *arr,int k,int n);
// 交换两个值 
void swap(int *a,int *b)
// 设置队列存储的空间和当前存值的多少
int arr[MAX],n;
/*
 * 上面不是已经有一个keep_heap的函数了么?为什么还要再写一个keep_heap_up函数呢?
 * keep_heap函数是与子节点比较大小,如果比子节点小就要继续往下比较
 * 对于出队来讲很合适,因为改变的是根节点,所以直接与他的子节点比较就可以了
 * 但是对于入队操作来讲,我们只能把最新的值放到最后,那么对于keep_heap就不太适合了
 * 当然,如果你硬要用keep_heap来完成入队操作也是可以的,不过就要仿照初始化堆操作
 * 耗时太多了,我们为什么不再写一个专门为入队操作向上的keep_heap_up函数呢
*/
void keep_heap_up(int *arr,int n)
{
    // 如果n节点比他的父节点大,n节点与父节点交换,由于父节点的值改变并且是变大了
    // 所以,要把该节点与父节点的父节点进行比较 
    if(arr[n] > arr[(n - 1) / 2])
    {
        swap(&arr[n],&arr[(n - 1) / 2]);
        keep_heap_up(arr,n);
    }
}
// 返回队列最高优先级元素(最大值)
int top()
{
    //判断队列是否为空,不空返回根节点
    if(n == 0)
    {
        printf("Error,the queue is empty,output is invalid!");
        return 0;
    }
    return arr[0];
}
// 入队
void push(int v)
{
    if(n == 100)
    {
        printf("Error,the queue is full!");
        return;
    }
    arr[n++] = v;
    // 不可以用keep_heap,而是要用keep_heap_up
    keep_heap_up(arr,n);
}
// 出队
int pop()
{
    int result = 0;
    if(n == 0)
    {
        printf("Error,the queue is empty,output is invalid!");
        return 0;
    }
    swap(&arr[0],&arr[--n]);
    keep_heap(arr, 0, n);
    return result;
}

在《算法导论》书中其实是让写四种方法的,入队、出队、队列头以及改变队列中第X个的值。但是我没有写改变队列中某个值的方法,这是我认为如果说是为了学堆的特性他是有价值的,但是在实际应用中是毫无意义的,因为第X个值是不可控的。在堆中,我们只可以保证根节点是优先级最高的,但是第X个值在堆中排列第几或者有什么意义,我们都不得而知,所以懒得写了。其实如果写的话也很简单,无非就是改变这个值的大小,然后重新针对该值维护堆的特性。
PS:打个广告,我在码云上面最近开始了一个将《算法导论》中的伪代码算法写成C/C++语言的小开源项目。我知道关于《算法导论》中的伪代码写成C/C++的现成代码有很多,主要还是为加深下影像。我也知道《算法导论》里面的精髓不止这些,但这也无妨,我也没打算只看这一遍。我还知道我写的代码可能不是最优解、不是根据《算法导论》中的思想来解、甚至存在错误也是相当有可能的,有错误您提、不是最优解您也说,写出来就是交流的,您提出来特别感激,免得其他人看了我的博客误人,那就不好了(虽然现在博客访问量很少),再次拜谢。如果您也在看《算法导论》,我强烈建议您也开个项目做做,说实话这本书啃了好几次,就这次看的最多,啃的最深。
PPS:关于这一篇的代码地址:如果可以加星就可以更好了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值