数据结构(五)优先队列和堆排序
有时候我们会收集一些元素,然后处理当前键值最大的元素。然后再收集更多,再处理。例如,在手机来电、短信、游戏三个程序中,来电的优先级是最高的,我们总希望先处理来电请求。
满足这种场景的数据结构,需要支持两种操作:删除最大(或最小)元素、插入元素。这种数据类型,就叫优先队列
。优先队列也是一种队列,但是跟前面提到的先进先出队列不同,优先队列的“出”,是根据优先级来的(最大或最小的先出)。
优先队列在很多地方有应用,例如:
- 数据压缩:哈夫曼编码算法
- 最短路径算法:Dijkstra算法
- 最小生成树算法:Prim算法
- 事件驱动仿真:顾客排队算法
- 选择问题:查找第k个最小元素
初级实现
数组(无序)
我们可以用 前面提到的 下压栈的方式来实现优先队列。 只是在其中加入一段内循环代码,将最大元素和栈顶元素交换,然后弹栈删除它。
数组(有序)
在插入操作中添加代码,将较大的元素往右边移动一格,以保证数组有序。这样,栈顶的元素永远是最大的,弹栈删除即可。
链表实现
可以用 前面提到的 基于链表的下压栈。 修改 pop()
找到并返回最大的元素, 或者修改 push()
保证所有元素逆序,并用 pop()
删除并返回链表的首元素(最大元素)。
不同实现的比较
实现 | 插入 | 删除 | 寻找最小值 |
---|---|---|---|
无序数组 | 1 | n | n |
无序链表 | 1 | n | n |
有序数组 | n | 1 | 1 |
有序链表 | n | 1 | 1 |
二叉搜索树 | logn(平均) | logn(平均) | logn(平均) |
平衡二叉搜索树 | logn | logn | logn |
二叉堆 | logn | logn | 1 |
完全二叉树
在讨论堆之前,得先了解一下什么是 完全二叉树(Complete Binary Tree)。在一颗二叉树中,每一个结点都与深度为K的满二叉树中编号从 1 至 n 的结点一一对应,这样的二叉树称为完全二叉树。
堆
什么是堆
所谓堆,就是利用树的结构来维护一组数据。我们平时说的堆,一般都是指 二叉堆 。如果在一颗完全二叉树中,每个结点都 大于等于 它的两个子节点,那么我们就说这个完全二叉树是 堆有序 的。一组能以堆有序的完全二叉树排序、并在数组中按层级存储的元素,我们就称为二叉堆。二叉堆能够比初级实现更好地实现优先队列。
要点:
- 完全二叉树
- 堆有序(父节点 >= 两个子节点)
堆的性质
在一个基于完全二叉树的堆中,如果下标从 1 开始,那么 k 的两个子节点分别为 2k 和 2k+1。如果下标从 0 开始,那么 k 的两个子节点分别为 2k+1 和 2(k+1)
通过这个性质,我们可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
堆有序化
上浮法实现堆有序化
1 | private void swim(int k){ |
下沉法实现堆有序化
1 | private void sink(int k){ |
基于堆的优先队列
算法 2.6
1 | public class MapPQ<key extends Comparable<key>>{ |
堆排序
基于优先队列的排序方法:将所有元素插入一个查找最小元素的优先队列,然后重复调用删除最小元素的操作,将他们按顺序删去。
堆排序就是一种这样的方法。分为两个阶段:
- 构造堆:将数组以堆的形式安排
- 下沉排序:按递减顺序取出所有元素,得到排序结果
算法2.7
1 | public static void sort(Comparable[] a){ |
排序算法总结
算法 | 稳定性 | 原地 | 时间复杂度 | 空间复杂度 | 备注 |
---|---|---|---|---|---|
选择排序 | 不稳定 | 是 | N² | 1 | |
插入排序 | 稳定 | 是 | (N,N²) | 1 | 取决于输入元素的排列情况 |
希尔排序 | 不稳定 | 是 | NlogN ? | 1 | |
快速排序 | 不稳定 | 是 | NlogN | lgN | 效率由概率提供保证 |
三向快速排序 | 不稳定 | 是 | (N,NlogN) | lgN | |
归并排序 | 稳定 | 否 | NlogN | N | |
堆排序 | 不稳定 | 是 | NlogN | 1 |
快速排序是最快的通用排序算法。Java系统库中 java.util.Arrays.sort() 根据不同的类型参数使用了不同的排序方法。对于原始数据类型,Java选择了三向快速排序,而对于引用类型,则是用归并排序。这些选择实际上是用速度和空间来换取稳定性。
本文部分内容参考自: