数据结构学习笔记(4)——优先级队列、堆
机械工业出版社《数据结构与算法——Python语言实现》学习笔记。
画图工具:draw.io
优先级队列、堆
1 优先级队列的ADT
形式化地将一个元素和它的优先级用一个 key-value 对进行建模。优先级队列 P 支持以下方法:
- P.add(k, v):向优先级队列中插入一个拥有键 k 和值 v 的元组。
- P.min():返回一个优先级队列 P 中拥有最小键值的元组 (k, v) (但没有移除该元组);如果队列为空,抛出错误。
- P.remove_min():从优先级队列 P 中移除一个拥有最小键值的元组 (k, v),并返回这个元组;如果队列为空,抛出错误。
- P.is_empty():如果优先级队列不包含任何元组,将返回True。
- len(P):返回优先级队列中元组的数量。
一个优先级队列中可能包含多个键值相等的条目,在这种情况下 min 和 remove_min 方法可能从具有最小键值的元组中任选一个返回。值可以是任何对象类型。
2 优先级队列的实现
2.1 使用未排序列表实现优先级队列
使用位置列表+组合设计模式的方法实现
操作 | 运行时间 |
---|---|
len | O(1) |
is_empty | O(1) |
add | O(1) |
min | O(n) |
remove_min | O(n) |
2.2 使用排序列表实现优先级队列
实现方法与未排序列表基本相同,在add()时扫描列表找到合适的位置
操作 | 运行时间 |
---|---|
len | O(1) |
is_empty | O(1) |
add | O(n) |
min | O(1) |
remove_min | O(1) |
3 堆
3.1 堆的数据结构
堆是一颗二叉树 T ,该树在它的节点上存储了集合中的元组并且满足两个附加属性:关系属性 以存储键的形式在 T 中定义;结构属性 以树 T 自身形状的方式定义。
关系属性:
- Heap-Order属性: 在堆 T 中,对于除了根的每个位置 p ,存储在 p 中的键值大于或等于存储在 p 的父节点的键值。
根据该属性容易知道 T 中从根到叶子的路径上的键值是以非递减顺序排列的,也就是说 一个最小的键总是存储在 T 的根节点中 。
由于效率的缘故,我们想让堆 T 的高度尽可能小,为此我们可以让堆 T 满足结构属性中的附加属性——它必须是完全二叉树——来满足这一需求。
- 堆的高度: 堆 T 有 n 个元组,则它的高度为
3.2 使用堆实现优先级队列
min操作
堆的属性保证了树的根部元组有最小的键值。
在堆中增加一个元组
先将新节点放在树底层最右节点相邻的位置,(如果树的底层节点已满或堆为空,则应存放在新一层的最左位置上)
插入元组后向上冒泡
在这个操作后,树 T 为完全二叉树,但是它 可能破坏了heap-order属性 ,因此我们应该将 p 位置上的键值与 p 的父节点 q 上的键值进行比较,通过元组交换操作向上冒泡使堆再次满足heap-order属性。
在堆中删除键值最小的元组
为了不破坏堆的结构,元组被删除后,由来自 最后位置(即树最底层的最靠右的位置) 的元组所填充。
删除操作后向下冒泡
在这个操作后,树 T 为完全二叉树,但是它 可能破坏了heap-order属性 ,因此我们应该将 p 位置上的键值与 p 的 两个孩子节点中键值较小的那个节点 q 上的键值进行比较,通过元组交换操作向下冒泡使堆再次满足heap-order属性。
3.3 基于堆的优先级队列的分析
由于堆是一颗二叉树,因此可以采用 基于数组的二叉树表示 来实现堆。
利用堆实现的优先级队列 P 操作 | 运行时间 |
---|---|
len§ | O(1) |
P.is_empty() | O(1) |
P.add() | O(log n) |
P.min() | O(1) |
P.remove_min() | O(log n) |
- 堆 T 是完全二叉树,高度为O(log n)
- 堆向上冒泡和向下冒泡执行交换的次数在最坏情况下等于 T 的高度(log n)。
3.4 自底向上构建堆*
略,详情可看书。
3.5 Python 的 heapq 模块
该模块提供对基于堆的优先级队列的支持。该模块不提供任何优先级队列类,而是提供一些函数,这些函数 把标准 Python 列表作为堆进行管理 。
Heapq 模块支持如下函数,假设这些函数调用之前,现有列表 L 已经满足 heap-order 属性:
函数 | 描述 | 复杂度 |
---|---|---|
heappush(L,e) | 将元素 e 存入列表 L ,并重新调整列表以满足 heap-order 属性 | O(log n) |
heappop(L) | 取出并返回列表 L 中拥有最小值的元素,并重新调整列表以满足 heap-order 属性 | O(log n) |
heappushpop(L,e) | 将元素 e 存入列表 L,同时取出并返回列表 L 中拥有最小值的元素 | O(log n) |
heapreplace(L,e) | 与 heappushpop 方法类似,但相当于在插入操作前执行 pop 操作 | O(log n) |
heapify(L) | 改变未排序的列表,使其满足 heap-order 属性,这个函数使用自底向上的堆构造方法 | O(n) |
nlargest(k,iterable) | 从一个给定的迭代中生成含有 k 个最大值的列表 | O(n + k log n) n为迭代的长度 |
nsmallest(k,iterable) | 从一个给定的迭代中生成含有 k 个最小值的列表 | O(n + k log n) n为迭代的长度 |
4 堆排序
堆排序算法分为两个阶段:
- 构造堆: 由于第 i 次 add 操作完成后堆有 i 个元组,所以第 i 次 add 操作的时间复杂度为 O(log i)。这一阶段整体的时间复杂度为O(n log n)(采用 自底向上堆构造的方法时间复杂度为O(n) )。
- 重复remove_min操作: 由于在第 j 次 remove_min 操作执行时堆中有 (n-j+1) 个元素,因此第 j 次 remove_min 操作的时间复杂度为 O(log(n-j+1))。这一阶段的时间复杂度为 O(n log n)。
- 命题:假设集合 C 的任意两个元素能在 O(1) 时间内完成比较,则堆排序算法能在 O(n log n)时间内完成含有 n 个元素的集合 C 的排序。
实现原地堆排序
一般来说,除了存储要排序的对象的序列之外,如果只使用少量的内存,我们就说该算法为原地(in-place)算法。
如果要排列的集合 C 是通过基于数组的序列实现的,我们可以使用 C 的左半部分来存储 堆中的元组 ,并且使用 C 的右半部分来存储 序列的元素 。
- 在算法的第一阶段,我们从一个空堆开始,并从左向右移动堆与序列之间的边界,一次一步,直至全部元素入堆,此时 C 的右半部分为空。这就是 堆构建 的过程。
- 在算法的第二阶段,我们从一个空的序列开始,并从右向左移动堆与序列之间的边界,一次一步,在第 i 步(i = 1, …, n)将堆中当前最大值元素从堆中移除并将其存储到索引为 n-i 的位置上。这就是 重复remove_min 的过程,可将其看成一种“选择排序”。
5 适应性优先级队列
增加两个方法:
- remove(): 用来删除优先级队列中的任一元组
- update(): 用新键替换元组现有的键
定位器
我们需要一种在优先级队列中找到用户元组的机制,因此提出 定位器(locator) 对象。 (定位器和位置不同,优先级队列的定位器并不代表结构中一个元素的具体位置) 对于一个优先级队列 P ,当执行 update 或者 remove 方法时,需要用户提供一个合适的定位器作为参数:
- P.update(loc, k, v): 用定位器 loc 代替键和值作为元组的标识。
- P.remove(loc): 从优先级队列中删除以 loc 标识的特定元组,并返回它的 (key, value) 对。
在具体实现时,可将定位器实例设计为 存储一个 key, value 和列表内元素的当前索引的元组 (key, value, index) 。然后用一个定位器序列表示堆,用户则会获得每个插入的元素的定位器实例的引用。注意,为了维护每个定位器实例在列表中的当前索引,我们需要 重写 swap() 方法 ,在每次 swap 操作,即元组交换时交换他们的index。