标签:
优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现。
——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建堆算法。
堆排序和选择排序的方法类似。
选择排序(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)。
标签:
原文地址:http://blog.csdn.net/xiang_freedom/article/details/51148312