文章目录
一、优先队列详解
什么是优先队列?普通队列:先进先出;后进后出。而优先队列,顾名思义,优先队列:出队顺序和入队顺序无关;和优先级相关。优先队列与普通队列的区别在于出队顺序上。
那为什么要使用优先队列呢?因为并不是所有任务都是先到先得的,而是会动态选择优先级最高的任务去执行。比如操作系统会动态选择优先级最高的任务去执行。
1.优先队列的实现
优先队列与普通队列相比,只会在返回队首元素与出队方面有差别。优先队列可以使用普通线性结构来实现,比如数组与链表,此时出队需要扫描一遍线性结构,找到最大值,时间复杂度为 O ( n ) O(n) O(n),性能上不尽如人意。优先队列也可以使用顺序线性结构来实现,这里的顺序线性结构是指数据结构(数组或链表)本身维持着顺序,从大到小或者从小到大排列,此时出队将变得非常容易,时间复杂度为 O ( 1 ) O(1) O(1),而这种数据结构入队时,最差的情况时将整个数据结构扫描一遍才能找到插入位置,时间复杂度为 O ( n ) O(n) O(n)。这一章介绍的堆数据结构,入队与出队的时间复杂度均为 O ( l o g n ) O(logn) O(logn),这与二叉搜索树的时间复杂度不同,二叉搜索树的平均时间复杂度为 O ( l o g n ) O(logn) O(logn),所以堆数据结构是非常高效的
数据结构 | 入队 | 出队(拿出最大元素) |
---|---|---|
普通线性结构 | O(1) | O(n) |
顺序线性结构 | O(n) | O(1) |
堆 | O(logn) | O(logn) |
二、堆
堆的核心内容:两种实现,一个中心,三个技巧。
1.堆的两种实现
这里介绍两种常见的堆实现,一种是基于链表的实现-跳表,另一种是基于数组的实现-二叉堆。
1.1 基于链表的实现-跳表
跳表的算法实现如果没有经过精雕细琢,性能很不稳定;而且当数据量增加时,跳表的内存占用会明显增加。所以跳表只讲述原理,不详细讲述代码实现。
基于链表实现的堆中,链表是有序的。
比如:想在跳表中查找10
一级跳表中7指向节点7,下一个18指向节点18,因此,10在节点7与节点18之间,通过down
指针回到原始链表中找到了节点7,节点7的next指针即为节点10.
在上例的基础上,如果数据量继续增大,那么索引层数也会继续增大1-level,2-level,...n-level
,最终可以使链表实现二分查找,也就可以获得更好的效率,当然不可避免的增加了空间复杂度。
跳表的时间复杂度为索引的层数 * 平均每层索引遍历的个数
,其中索引的层数为二分查找的时间复杂度 O ( l o g n ) O(logn) O(logn),而平均每层索引遍历的个数是个常数,因此跳表的时间复杂度为 O ( l o g n ) O(logn) O(logn)。空间复杂度等同于索引节点的总个数 O ( n ) O(n) O(n)。
跳表的入堆与出堆操作:
- 入堆操作,只需要根据索引插到链表中,并更新索引
- 出堆操作,只需要删除头部(或者尾部),并更新索引
具体实现可以参考leetcode—1206. 设计跳表
1.2 基于数组的实现-二叉堆
-
二叉堆是一颗完全二叉树
-
二叉堆的性质:
最大堆:堆中某个节点的值总是不大于其父节点的值(并不要求上一层的值都大于下一层的值)
最小堆:堆中某个节点的值总是不小于其父节点的值
-
二叉堆的实现,我们可以使用二叉搜索树的实现方式来实现,同样我们可以用数组的形式来实现完全二叉树。
现在的问题就变成:用数组形式实现完全二叉树时,应该怎么找到每一个父节点的左右孩子?
很容易发现,在数组中,若父节点的索引为 n n n,则左孩子的索引为 2 n 2n 2n,右孩子的索引为 2 n + 1 2n+1 2n+1。若左或右孩子的索引为 n n n,则父节点的索引为 n 2 \frac{n}2 2n。p a r e n t ( i ) = i / 2 parent(i) = i / 2 parent(i)=i/2
l e f t c h i l d ( i ) = 2 ∗ i left child (i) = 2*i leftchild(i)=2∗i
r i g h t c h i l d ( i ) = 2 ∗ i + 1 rightchild(i ) =2*i +1 rightchild(i)=2∗i+1若二叉搜索树的索引从0开始,则
在数组中,若父节点的索引为 n n n,则左孩子的索引为 2 n + 1 2n+1 2n+1,右孩子的索引为 2 n + 2 2n+2 2n+2。若左或右孩子的索引为 n n n,则父节点的索引为 n − 1 2 \frac{n-1}2 2n−1。
1.2.1 二叉堆的基本框架
# 创建最大堆
class MaxHeap:
def __init__(self, arr=None, capacity=None):
# 如果数组容量为空
if not capacity:
self._data = Array()
# 如果容量不为空
else:
self._data = Array(capacity=capacity)
# 判断堆尺寸
def size(self):
return self._data.get_size()
# 判断堆是否为空
def is_empty(self):
return self._data.is_empty()
# 返回完全二叉树数组表示中,一个索引所表示的元素的父亲节点的索引 (i - 1)// 2
def _parent(self, index):
if index == 0:
raise ValueError('index-0 doesn\'t have parent.')
return (index - 1) // 2
# 返回完全二叉树数组表示中,一个索引所表示的元素的左孩子节点的索引 2 * i + 1
def _left_child(self, index):
return index * 2 + 1
# 返回完全二叉树数组表示中,一个索引所表示的元素的右孩子节点的索引 2 * i + 2
def _right_child(self, index):
return index * 2 + 2
1.2.2 向堆中添加元素和ShiftUp(上浮)
def add(self, e):
# 将元素添加到末尾
self._data.add_last(e)
# 上浮以满足最大堆的性质
# self._data.get_size() - 1为添加到末尾的元素的索引
self._sift_up(self._data.get_size