高级数据结构(Ⅱ)优先队列(Priority Queue)

许多应用程序都需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序。很多情况下我们会收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素,如此这般。例如,你可能有一台能够同时运行多个应用程序的电脑(或者手机)。这是通过为每个应用程序事件分配一个优先级,并总是处理下一个优先级最高的事件来实现的。例如,绝大多数手机分配给来电的优先级都会被游戏程序的高。

在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素插入元素。这种数据类型叫做优先队列。优先队列的使用和队列(删除最老的元素)以及栈(删除最新的元素)类似,但高效地实现它则更有挑战性。

在本篇中,我们会学习一种基于二叉堆数据结构的一种优先队列的经典实现方法,用数组保存元素并按照一定条件排序,以实现高效地(对数级别的)删除最大元素和插入元素操作。

API

优先队列是一种抽象数据类型,它表示了一组值和对这些值的操作,它的抽象层使我们能够方便地将应用程序和各种具体实现隔离开来。

泛型优先队列的API如下

public class MaxPQ <Key extends Comparable<Key>>
-----------------------------------------------------------------------MaxPQ()                   创建一个优先队列MaxPQ(int max)            创建一个初始容量为max的优先队列MaxPQ(Key[] a)            用a中的元素创建一个优先队列void insert(Key v)             向优先队列中插入一个元素Key max()                     返回最大元素Key delMax()                  删除并返回最大元素boolean isEmpty()                 返回队列是否为空int size()                    返回优先队列中元素个数

堆的定义

数据结构二叉堆能够很好地实现优先队列的基本操作。在二叉堆的数组中,每个元素都要保证大于等于另外两个特定位置的元素。相应地,这些位置的元素又至少要大于等于数组中的另外两个元素,以此类推。如果我们将所有的元素画成一颗二叉树,将每个较大元素和较小的元素用边连接就可以很容易地看出这种结构。

命题当一颗二叉树的每个结点都大于等于它的另外两个子节点时,它被称为堆有序

相应地,在堆有序的二叉树中,每个结点都小于等于它的父节点(如果有的话)。从任意结点向上,我们都能得到一列非递减的元素;从任意结点向下,我们都能得到一列非递增的元素。

命题根节点是堆有序的二叉树中最大的结点

二叉堆表示法

如果我们用指针来表示堆有序的二叉树,那么每个元素都需要三个指针来找到它的上下结点(父节点和两个子节点)。如下图所示,如果我们使用完全二叉树,表达就会变的特别方便

可以先定下根节点,然后一层一层地由上向下、从左至右,在每个结点的下方连接两个更小的结点,直至将N个结点全部连接完毕。完全二叉树只用数组而不需要指针就可以表示。具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子节点在位置2和位置3,而子节点的子节点分别在位置4、5、6和7,以此类推。

定义二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储(不使用数组的第一个位置)

在一个堆中,位置k的结点的父节点的位置为[k/2]向下取整,而它的两个子节点的位置则分别为2k2k+1。用数组(堆)实现的完全二叉树的结构是很严格的,但它的灵活性已经足以让我们高效地实现优先队列。用它们我们能将实现对数级别的插入元素和删除最大元素的操作。利用数组中无需指针即可沿树上下移动的便利和以下性质,算法保证了对数复杂度的性能。

命题一颗大小为N的完全二叉树的高度为lg[N]

上图完全二叉树的堆的表示如下

堆的算法

我们用长度为N+1的私有数组pq[]来表示一个大小为N的堆。我们不会使用pq[0],堆元素放在pq[1]pq[N]中。在排序算法中,我们只通过私有辅助函数less()exch()来访问元素,但因为所有的元素都在数组pq[]中,我们在下面基于堆的优先队列中会用更紧凑的实现方式,不再将数组作为参数传递。堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复。我们称这个过程为堆的有序化(reheapifying)

堆实现的比较和交换的方法如下所示

private boolean less(int i, int j) {return pq[i].compareTo(pq[j]) < 0;
}private void exch(int i, int j) {Key t = pq[i];pq[i] = pq[j];pq[j] = t;
}

在堆有序化的过程中我们会遇到两种情况。当某个结点的优先级上升(或是在堆底加入一个新的元素)时,我们需要由下至上恢复堆的顺序。当某个结点的优先级下降(例如,将根节点替换为一个较小的元素)时,我们需要由下至上恢复堆的顺序。首先来学习如何实现这两种辅助操作,然后再用它们实现插入元素和删除最大元素的操作。

由下至上的堆的有序化(上浮)

如果堆的有序状态因为某个结点变得比它的父节点更大而被打破,那么我们就需要通过交换它和它的父节点来修复堆。交换后,这个结点比它的两个子结点都大(一个是曾经的父节点,另一个比它更小,因为它是它曾经父节点的子节点),但这个结点仍然可能比它现在的父节点更大。我们可以一遍遍地用同样的方法恢复秩序,将这个结点不断向上移动直到我们遇到了一个更大的父节点。位置k的结点的父结点为[k/2]swim()方法中的循环可以保证只有位置k上的结点大于它的父节点时堆的有序状态才会被打破。因此只要该结点不再大于父结点,堆的有序状态就恢复了。当一个结点优先级太大的时候它需要浮(swim)到堆的更高层,详细代码如下

private void swim(int k) {while(k > 1 && less(k / 2, k)) {exch(k / 2, k);k = k / 2;}
}

相应图解如下

由上至下的堆的有序化(下沉)

如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小了而被打破了,那么我们可以通过将它和它的两个子结点中的较大者交换来恢复堆。交换可能会在子结点处继续打破堆的有序状态,因此我们都需要用相同的方式来将其修复,将结点向下移动直到它的子结点都比它更小或者是达到了堆的底部。由位置为k的结点的子结点为2k2k+1可以直接得到对应的代码。方法名为了形象表示这个过程称为sink()下沉,即当一个结点优先级太低时它需要沉(sink)到堆的更低层,代码实现如下

private void sink(int k) {while(2 * k <= N) {int j = 2 * k;if(j < N && less(j, j + 1)) j++;if(!less(k, j)) break;exch(k, j);k = j;}
}

相应图解如下

插入元素

我们将新元素插入到数组末尾,增加堆的大小并让这个元素上浮到合适的位置(如下图左半部分所示)

删除最大元素

我们从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置(如下图右半部分所示)

基于堆的优先队列

优先队列由一个基于堆的完全二叉树表示,存储于数组pq[1..N]中,pq[0]没有使用,pq[0]的值有时可以作为哨兵来用。在insert()中,我们将N加1并把新元素添加在数组最后,然后用swim()恢复堆的秩序。在delMax()中,我们从pq[1]中得到需要返回的元素,然后将pq[N]移动到pq[1],将N减1并用sink()来恢复堆的秩序。同时我们还将不再使用的pq[N+1]设为null,以便系统回收它所占用的空间。此处省略动态调整数组大小的代码,有兴趣的朋友可以自己写写。

命题对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大元素的操作需要不超过2lgN次比较

基于堆的优先队列详细代码如下

public class MaxPQ<Key extends Comparable<Key>> {private Key[] pq;    //基于堆的完全二叉树private int N = 0;   //存储于pa[1..N]中,pq[0]没有使用public MaxPQ(int maxN) {pq = (Key[]) new Comparable[maxN + 1];this.N = maxN;}public boolean isEmpty() {return N == 0;}public int size() {return N;}public void insert(Key v) {pq[++N] = v;swim(N);}public Key delMax(Key v) {Key max = pq[1];     //根节点得到最大元素exch(1, N--);        //将其和最后一个结点交换pq[N + 1] = null;    //防止对象游离sink(1);             //恢复堆的有序性return max;}private boolean less(int i, int j) {return pq[i].compareTo(pq[j]) < 0;}private void exch(int i, int j) {Key t = pq[i];pq[i] = pq[j];pq[j] = t;}private void swim(int k) {while(k > 1 && less(k / 2, k)) {exch(k / 2, k);k = k / 2;}}private void sink(int k) {while(2 * k <= N) {int j = 2 * k;if(j < N && less(j, j + 1)) j++;if(!less(k, j)) break;exch(k, j);k = j;}}}

下图详细描述了在堆上相应操作的详细过程

堆排序

既然已经写完了基于堆的优先队列,顺便也总结下堆排序吧~

堆排序可以分为两个阶段。在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中;然后在下沉排序阶段,我们从堆中按递减顺序取出所有元素并得到排序结果。为了和我们学过的代码保持一致,我们将使用一个面向最大元素的优先队列并重复删除最大元素。我们不再使将优先队列的具体表示隐藏,并将直接使用swim()sink()操作。这样我们在排序时就可以将需要排序的数组本身作为堆,因此无需任何额外空间。

注意:将对应下标减1可以得到与其他排序算法一样的排序结果。否则将是忽略下标0的排序算法

class HeapSort<Key extends Comparable<Key>> {public void sort(Comparable[] a) {int N = a.length - 1;//构造堆for(int k = N / 2; k >= 1; k--) {sink(a, k, N);}//销毁堆while(N > 1) {exch(a, 1, N--);sink(a, 1, N);}}private boolean less(Comparable[] a , int i, int j) {return a[i].compareTo(a[j]) < 0;}private void exch(Comparable[] a, int i, int j) {Key t = (Key) a[i];a[i] = a[j];a[j] = t;}private void sink(Comparable[] a, int k, int N) {while(2 * k <= N) {int j = 2 * k;if(j < N && less(a, j, j + 1)) j++;if(!less(a, k, j)) break;exch(a, k, j);k = j;}}}

堆排序详细代码轨迹如下图所示


参考资料:《算法》第四版

高级数据结构(Ⅱ)优先队列(Priority Queue)相关推荐

  1. C语言优先队列作用,C语言实现优先队列(priority queue)

    堆排序是一个比较优秀的算法,堆这种数据结构在现实生活中有很多的应用,比如堆可以作为一个优先队列来使用,作为一个高效的优先队列,它与堆的结构一样,都有最大优先队列,最小优先队列.优先队列priority ...

  2. 高级数据结构---优先队列

    高级数据结构-优先队列 原理:参考趣学数据结构 代码: #include <stdio.h> #include <stdlib.h> int r[] = { -1,1,4,59 ...

  3. 优先队列(priority queue)

    目录 一,优先队列 二,STL优先队列 三,二叉堆实现优先队列 四,二项堆实现优先队列 五,线段树实现优先队列 六,OJ实战 力扣 295. 数据流的中位数 一,优先队列 优先队列可以插入元素.查询最 ...

  4. c++ STL:队列queue、优先队列priority queue 的使用

    说明:本文全文转载而来,原文链接:http://www.cppblog.com/wanghaiguang/archive/2012/06/05/177644.html C++ Queues(队列) C ...

  5. Java优先队列 Priority Queue

    2019独角兽企业重金招聘Python工程师标准>>> 优先队列是一种用来维护一组元素构成的结合S的数据结构,其中每个元素都有一个关键字key,元素之间的比较都是通过key来比较的. ...

  6. 数据结构及算法基础--优先队列(Priority Queue)

    这真的是一个包含很多东西的数据结构.我们会逐步解析,来讲解优先队列: 首先知道什么是优先队列: 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除(first in, last ou ...

  7. C语言实现升序优先队列Ascending priority queue(附完整源码)

    实现升序优先队列Ascending priority queue node结构体 升序优先队列Ascending priority queue完整源码(定义,实现,main函数测试) node结构体 ...

  8. 算法与数据结构基础 - 堆(Heap)和优先级队列(Priority Queue)

    堆基础 堆(Heap)是具有这样性质的数据结构:1/完全二叉树 2/所有节点的值大于等于(或小于等于)子节点的值: 图片来源:这里 堆可以用数组存储,插入.删除会触发节点shift_down.shif ...

  9. 数据结构与算法笔记:贪心策略之BSTBBST, Hashtable+Dictionary+Map, Priority Queue~Heap, Minium Spanning Tree

    BST & BBST BST(Binary Search Tree) 二叉搜索树,也就是使用二叉树来做查找 BBST(Balanced Binary Search Tree) 平衡二叉搜索树 ...

最新文章

  1. ASP.NET控件事件丢失的探究
  2. java整数类型int字面值_JavaKotlinAndroidGuide
  3. TensorFlow官方入门实操课程-全连接神经网络分类
  4. 类加载的三个阶段——加载、链接以及初始化,类的主动使用和被动使用
  5. HDU4462-稻草人
  6. Java 多线程 —— ThreadLocal
  7. python写我爱你_12个精选Python教程我的初恋故事。
  8. NYOJ-背包问题(贪心)
  9. 浅谈Android五大布局
  10. 小D课堂 - 零基础入门SpringBoot2.X到实战_第三节SpringBoot热部署devtool和配置文件自动注入实战_16、注解配置文件自动映射到属性和实体类实战...
  11. 阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_4_解决线程安全问题_同步代码块...
  12. 《区块链技术指南》电子书推荐!
  13. python黑帽子(黑客与渗透测试编程之道)
  14. 四叉树算法原理与实现
  15. 电脑连手机热点DNS服务器无响应,电脑连接手机热点无法上网解决方法有哪些
  16. tensorflow获取动态shape
  17. 开方在java中怎么计算_JAVA BigDecimal使用牛顿迭代法计算平方根(开方)
  18. ROMS 编译upwelling.h
  19. execv bad address
  20. 树莓派SPI连接TFT屏幕

热门文章

  1. 回文数(指正序(从左向右)和倒序(从右向左)读都是一样的整数)
  2. ZBrush中Nudge推动笔刷介绍
  3. ESL第五章 基扩张和正则化 【自然】三次样条/似然比检验/自然正则化、自由度/光滑矩阵/收缩光滑/局部拟合/等价核、非参逻辑回归、多维张量积/加性样条、RKHS/径向基、小波光滑/自适应滤波、B样条
  4. 最大长方形 (Maximum Submatrix Largest Rectangle)(涵盖各种求最大矩形题目)
  5. 汽车UDS诊断详解及Vector相关工具链使用说明——2.1.1 诊断和通讯管理功能单元概述
  6. 小程序内长按识别二维码
  7. 计算机学院优研计划,西电计算机考研总结 优研计划夏令营
  8. Clinically Applicable AI System for Accurate Diagnosis, Quantitative Measurements...论文总结
  9. [转]C++中的头文件和源文件 .h .cpp
  10. linux修改文件所有者和文件所在组chgrp  和chown