第十章笔记·优先级队列

需求与动机

什么是优先级队列

优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现。

——wikipedia

应用需求

在医院门诊,如果只有一个医生,多位病人。按照通常流程来说,采用先到先服务的顺序(FIFO)。但是如果病人中有人患有心脏病,那么显然这个病人需要得到优先治疗。

在计算机系统中,绝大多数支持多任务,这种多任务调度也类似于医院门诊。CPU相当于医生,计算任务相当于病人。每个任务都有一个优先级指标(priority),优先级高的任务可以被CPU优先处理。

在算法中,也有大量应用。比如堆排序、霍夫曼编码等。

计算模式

以上问题可以被归纳为这样一种模式:服务端(医生、CPU、霍夫曼算法…)通过一种叫call-by-priority(循优先级访问)的方式访问客户数据(病人,任务,待编码字符…),客户数据都有一个优先级指标。这种访问方式需要对应一种数据结构,可以记录、维护所有数据的优先级指标,并通过接口对这些数据进行操作。

功能接口

以上为优先级队列(简称PQ)的接口定义规范。可以说PQ是一种抽象数据类型(ADT),不同的实现方式可以产生不同的数据结构,比如栈和队列(根据插入次序设置优先级)。

那么,这样一种数据结构具体怎么实现呢?

基本实现

采用不同数据结构实现PQ的接口效率对比

数据结构 insert(T)效率 getMax()效率 delMax()效率
向量 θ(n) θ(n) O(1)
有序向量 O(n) O(1) o(1)
列表 O(1) θ(n) θ(n)
有序列表 O(n) O(1) O(1)
BBST O(logn) O(logn) O(logn)

虽然BBST的效率是最好的,但是BBST的功能比PQ需要实现的功能多得多:比如PQ对于查找和删除只针对最大元素,而BBST是针对所有元素。

因此可以使用一种成本更低,效率更高的实现方式。这种结构应该介于基本数据结构向量和复杂数据结构平衡二叉树之间。

完全二叉堆

要介绍完全二叉堆,先要介绍完全二叉树。这里通过使用向量来表示一个完全二叉树,从而实现PQ结构。

1. 可以看到完全二叉树的节点与向量的节点是一一对应的(层次遍历),只要定义向量的每个节点的父节点、左孩子和右孩子,即可构建出完全二叉树。

2. 这种做法的好处是:物理结构是线性的向量,但是逻辑结构是完全二叉树。这种结构称为“完全二叉堆”。

完全二叉堆模板类:

可以看到,其作为PQ实现了insert,getMax,delMax三个标准接口,还实现了批量建堆的接口。另外,这些接口的实现利用了三个内部方法:下滤、上滤和Floyd建堆算法。

  • 堆序性

    1. 堆序性是完全二叉堆关于节点间次序的约束:任何一个节点在数值上都不大于其父节点。
    2. 因此,完全二叉堆的最大元素为根节点,对应的就是向量首元素。
  • 插入与上滤
    1. 在完全二叉堆中插入元素,只需以末元素插入向量即可。这样做的好处是不会破坏完全二叉树的结构性,新加入节点后仍然保持完全二叉树特征。但是这样会破坏堆序性——新插入的元素的值可能大于其父节点。
    2. 如果新元素大于其父节点,那么将该元素与其父元素对换位置即可重新恢复堆序性。若交换后的元素仍然大于其父节点,则继续交换,直到抵达根节点。这样一种逐层上升的过程称之为“上滤”。上滤完成后,完全二叉堆得到了完全恢复。

      插入与上滤实现代码:

    3. 可以看到,上滤的一次过程的时间为常数。而完全二叉树的树高在logn范围内。因此上滤过程的时间复杂度也在O(logn)范围。
    4. 这个效率可以更优化:在每次swap()交换时,都使用了三次赋值操作,相当于3logn时间。如果先将插入的元素进行一次备份,在每次交换时,只将其父节点下移,直到不能再上滤时,再把备份元素加入到最终位置。这样可以把赋值操作降低到logn+2。
    5. 另外,完全二叉堆的平均效率要比最坏效率好很多,平均上滤的高度是常数。因此,完全二叉堆是“低成本,高效率”的典型数据结构。
  • 删除与下滤

    删除最大元素之后,完全二叉堆的结构已经被破坏。为了重建,可以把最后一个元素移至根元素。移动之后,堆序性被破坏。这里与上滤的过程相似,依次把根元素与其孩子节点进行比较,如果比它们至少其中之一小,那么与它们中较大的节点交换。交换之后该元素下降了一层。然后继续重复下滤过程,直至满足堆序性为止。此过程完成后,整个堆的堆序性都得到了恢复。

    代码实现:

    与上滤算法一样,下滤算法的时间复杂度也是O(logn),交换过程也可以优化为logn次。但是与上滤算法每次只需比较一次不同的是,下滤算法每次需要和两个子节点进行比较。因此在常系数意义上,下滤算法花费时间更多。这一差异在二叉堆难以体现,在多叉堆中则比较明显。

  • 批量建堆

    根据任意的n个元素,将其构建成一个完全二叉堆。这个过程也称作“heapification”。算法思路是首先调用向量的copyFrom接口将数组复制到内部,再使用heapify()算法建堆。

    上一个算法称为“蛮力”算法。大致思路是按照层次遍历的顺序(从上至下,从左往右)依次插入元素,并完成上滤操作。

    这个算法的效率是底下的:就拿叶子节点而言,其深度为log(n)。对于每一次上滤操作的时间复杂度都为O(logn),一共有n/2个叶子节点,因此总体时间复杂度为O(nlogn)。

    而这种采用自下而上的下滤算法的思路是,先按照层次遍历构建二叉树。然后以自下而上,从右往左的顺序对非叶子节点使用delMax算法的后半部分建堆。如图所示:

    1. 对于数量为n的向量而言,其最后一个非叶子节点对应的秩为?n2??1。
    2. 这种算法的时间复杂度为O(n)。其比蛮力算法高效的原因在于:蛮力算法对多个元素进行了重复比较与交换操作。而下滤算法对一层的元素进行排序之后,不会再被比较和交换。蛮力算法其实是实现了所有元素的全排序,而下滤算法仅实现了“偏序”。

堆排序

堆排序和选择排序的方法类似。

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

——Wikipedia

堆排序也是不断调用delMax方法取出堆顶最大元素进行排序。

但是堆排序相对于选择排序O(n^2)的时间复杂度更高效:每次delMax为logn时间,一共只需nlogn。不仅在时间上,堆排序在空间上也很高效。

由于堆是建立在向量基础上,我们可以充分利用向量空间,使空间复杂度为常数。如上图,将向量空间分为堆空间和已排序元素两个部分,不断从堆空间取出最大元素并移至已排序空间,同时将堆空间最后一个元素移至堆顶。反复这个过程,直至堆空间为空。

算法模板:

排序过程:

左式堆

左式堆的设计是为了堆的高效合并。

一种直接的合并方法是:先将两个堆连接在一起,再通过建堆算法建堆。这种算法的效率为O(n+m)。

在建堆算法中,默认所有元素都是无序的。而现在要合并的两个堆都是已经满足堆序性的结构,因此理论上存在更优的合并策略。这种策略就是引用左式堆。

左式堆是一种特别的堆结构,它满足堆序性,却不满足堆结构:它不是一棵完全二叉树。对于堆而言,结构并不是本质要求。

  • NPL

    为了使左式堆在逻辑意义上满足二叉树结构,可以引入空节点来填补节点空缺。NPL(空节点路径长度),是指一个节点到一个外部节点的最小距离,用来衡量左式堆倾斜度的指标。

  • 左式堆的定义:

    任意一个节点的左孩子的npl值都大于等于其右孩子的npl值。

    那么对于含有n个节点的左式堆而言,其右侧链长度应为logn

  • 合并算法

    左式堆由于不再满足完全二叉树结构,物理上的紧凑性也难以满足。此时再使用向量已不适宜,转而采用二叉树作为派生基类。模板类如下:

    左式堆合并的思路为:存在两个左式堆A,B,假设A的根节点大于B。那么以递归的方法,将A的右子堆与堆B合并。合并完成后的堆继续作为A的右子堆。在合并完成后,还需要检查A的左孩子和右孩子的NPL值,如果NPL(L)小于NPL(R),将左右孩子替换。

    上图为两个左式堆合并的完整过程。代码如下:

    由于左式堆合并过程一直在右侧链进行,因此其时间复杂度不会超过右侧链的长度logn。

  • 插入与删除

    左式堆的插入其实也是合并操作,将带插入的节点视为只有一个节点的二叉树,与原堆进行合并,即实现了插入操作。

    删除操作也是合并。在左式堆的根节点被删除之后,将剩下的左右子堆合并成新的左式堆即可。

    由于合并操作的时间复杂度为O(logn),插入删除操作的时间复杂度也为O(logn)。

总结

  1. 从需求出发,存在一种“循优先级访问”的方式,必须设计这样一种数据结构满足这个要求,这个结构称为“优先级队列”(PQ)。
  2. 对PQ的功能做了定义之后,分别计算了使用现有数据结构实现的效率,发现只有BBST可以达到三个操作都是O(logn)的时间。但是BBST作为一种高级数据结构,其功能远大于PQ。引出堆这样一种较BBST简单但效率不低于BBST的结构。
  3. 介绍完全二叉堆的结构和堆序性特点,以及其三个内部方法:上滤、下滤和Floyd建堆。发现其三个标准接口的效率为O(1),O(logn),O(logn)。
  4. 根据堆的堆序性特点可以对元素进行排序,即每次把最大元素删除即可。堆排序的时间复杂度为nlogn。
  5. 堆在很多方面效率都很高,但在堆的合并上的效率不太让人满意。由此派生出左式堆这一结构完成堆的高效合并。左式堆的逻辑结构不再是完全二叉树,物理结构换成了二叉树。其重要的特性是NPL,保证了合并的效率为O(logn),且插入删除操作也保持在O(logn)。
时间: 2024-10-29 05:04:21

第十章笔记·优先级队列的相关文章

STL源码笔记(15)—堆和优先级队列(二)

STL源码笔记(15)-堆和优先级队列 优先级队列的源码实现基于heap的操作,底层容器默认是vector. 优先级队列简介 优先级队列跟队列类似,一端插入一端删除,不同的是,优先级队列的元素入队后会根据其优先级进行调整,默认情况下优先级高的将优先出队,在SGI STL中,优先级队列的功能保证由heap实现:stl_heap.h中,heap的分析见:STL堆源码分析 优先级队列构造函数 默认情况下,优先级队列使用vector作为底层容器,使用less作为比较函数,其在源码中的定义声明如下: te

STL源码笔记(14)—堆和优先级队列(一)

STL源码笔记(14)-堆和优先级队列 priority_queue是拥有权值观念的queue,跟queue类似,其只能在一端push,一端pop,不同的是,每次push元素之后再容器内部元素将按照一定次序排列,使得pop得到的元素始终是当前权值的极大值. 很显然,满足这个条件就需要某些机制了,缺省情况下使用max-heap大顶堆来实现,联想堆排序的实现,使用大顶完成序列从小到大的排序,过程大概是: 把堆的根元素(堆中极大值)交换到最后 堆的长度减1 这样每次取出堆中的极大值完成排序,刚好与优先

Rabbitmq中的优先级队列操作

1.%% 普通队列操作 in(X, 0, {queue, [_] = In, [], 1}) ->{queue, [X], In, 2}; in(X, 0, {queue, In, Out, Len}) when is_list(In), is_list(Out) -> {queue, [X|In], Out, Len + 1}; 优先级队列操作: in(X, Priority, Q = {queue, [], [], 0}) -> in(X, Priority, {pqueue, []

初学算法-基于最小堆的优先级队列C++实现

笔者近日实现了最小堆类及其派生的优先级队列,特将代码奉上,不足之处还请指出! 在实现优先级队列时,笔者表示萌萌哒没有用过template写派生类,结果写完了出现error: *** was not decleared in this scope..后来各种补上this->才完事,在CSDN(笔者的帖子地址? http://bbs.csdn.net/topics/391806995)上提问后才知道是模板参数依赖,笔者表示涨姿势了.. /**  * The Minimum Heap Class an

【转】java中PriorityQueue优先级队列使用方法

优先级队列是不同于先进先出队列的另一种队列.每次从队列中取出的是具有最高优先权的元素. PriorityQueue是从JDK1.5开始提供的新的数据结构接口. 如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头,字符串则按字典序排列. 由于网上的资料大多将优先级队列各个方法属性,很少有实例讲解的,为方便大家以后使用,我就写了个demo~ 如果想实现按照自己的意愿进行优先级排列的队列的话,需要实现Comparator接口.下面的方法,实现了根据某个变

STL学习系列七:优先级队列priority_queue容器

1.简介 最大值优先级队列.最小值优先级队列 优先级队列适配器 STL priority_queue 用来开发一些特殊的应用,请对stl的类库,多做扩展性学习 如果我们给每个元素都分配一个数字来标记其优先级,不妨设较小的数字具有较高的优先级,这样我们就可以在一个集合中访问优先级最高的元素并对其进行查找和删除操作了.这样,我们就引入了优先级队列 这种数据结构. 优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有(1)查找(2)插入一

优先级队列及小顶堆排序

优先级队列及小顶堆排序实现 /** @file          HeapSort.h *  @copyright     personal *  @brief         优先级队列及堆排序 *  @version       V1.0.0 *  @author        fangyuan *  @date          2015/12/31 *  @note          测试版本 */ #include "iostream" using namespace std

初学图论-Dijkstra单源最短路径算法基于优先级队列(Priority Queue)的实现

这一次,笔者使用了STL库中的优先级队列(Priority Queue)来完成Dijkstra算法中extract-min()语句(即从未选中的节点中选取一个距离原点s最小的点)的功能.由于优先级队列的插入.删除操作只需要logn的时间花费,因此降低了不少运行时间. 本文使用C++实现了这一基本算法.参考<算法导论>第24.3节. /**  * Dijkstra's Single Source Shortest Path Algorithm in C++  * Time Cost : O(Ml

STL之优先级队列priority_queue

摘要: priority_queue,自适应容器(即容器适配器):不能由list来组建: 最大值优先级队列(最大值始终在对首,push进去时候) 最小值优先级队列: 优先级队列适配器 STL  priority_queue priority_queue<int, deque<int> > pg; priority_queue<int, vector<int> > pg; STL中实现的方法: pg.empty(); pg.size(); pg.top();