算法设计与分析——堆(四):优先队列

分类目录:《算法设计与分析》总目录
相关文章:
·堆(一):基础知识
·堆(二):维护堆的性质
·堆(三):建堆
·堆(四):优先队列
·排序算法:堆排序
·利用heapq模块实现堆


本文我们要介绍堆的一个常见应用:作为高效的优先队列。和堆一样,优先队列也有两种形式:最大优先队列最小优先队列。这里我们关注于如何基于最大堆实现最大优先队列,但是在本博客主要采用的Python语言中,用heapq模块构建的堆是最小堆。

优先队列是一种用来维护由一组元素构成的集合 S S S的数据结构,其中的每一个元素都有一个相关的值,称为关键字。一个最大优先队列支持以下操作:

  • heap_insert(S, x):把元素 x x x插入集合 S S S中。这一操作等价于 S = S ∪ { x } S=S\cup \{x\} S=S{x}
  • heap_maximum(S):返回 S S S中具有最大键字的元素。
  • heap_extract_max(S):去掉并返回 S S S中的具有最大键字的元素。
  • heap_increase_key(S, i, k):将元素 x x x的关键字值增加到 k k k,这里假设 k k k的值不小于 x x x的原关键字值。

最大优先队列的应用有很多,其中一个就是在共享计算机系统的作业调度。最大优先队列记录将要执行的各个作业以及它们之间的相对优先级。当一个作业完成或者被中断后,调度器调用extract_max(s)从所有的等待作业中,选出具有最高优先级的作业来执行。在任何时候,调度器可以调用heap_insert(S, x)把一个新作业加入到队列中来。

相应地,最小优先队列支持的操作也包括插入、最小元素、去掉并返回最小元素等。最小优先队列可以被用于基于事件驱动的模拟器。队列中保存要模拟的事件,每个事件都有一个发生时间作为其关键字。事件必须按照发生的时间顺序进行模拟,因为某一事件的模拟结果可能会触发对其他事件的模拟。在每一步,模拟程序调用extract_min(S)来选择下一个要模拟的事件。当一个新事件产生时,模拟器通过调用heap_insert(S, x)将其插入最小优先级队列中。

显然,优先队列可以用堆来实现。对一个像作业调度或事件驱动模拟器这样的应用程序来说,优先队列的元素对应着应用程序中的对象。通常,我们需要确定哪个对象对应一个给定的优先队列元素,反之亦然。因此,在用堆来实现优先队列时,需要在堆中的每个元素里存储对应对象的句柄。句柄(如一个指针或一个整型数等)的准确含义依赖于具体的应用程序。同样,在应用程序的对象中,我们也需要存储一个堆中对应元素的句柄。通常,这一句柄是数组的下标。由于在堆的操作过程中,元素会改变其在数组中的位置,因此,在具体的实现中,在重新确定堆元素位置时,我们也需要更新相应应用程序对象中的数组下标。因为对应用程序对象的访问细节强烈依赖于应用程序及其实现方式,所以这里我们不做详细讨论。需要强调的是,这些句柄也需要被正确地维护。
现在,我们来讨论如何实现最大优先队列的操作。过程heap_maximum(S)可以在 Θ ( 1 ) \Theta(1) Θ(1)时间内实现找出最大元素操作。

def heap_maximum(S):
	return S[0]

过程heap_extract_max(S)实现去掉并返回 S S S中的具有最大键字的元素的操作。它与heapSort(arr)过程中的for循环体部分很相似。

def heap_extract_max(S):
	max = heap_maximum(S)
	A[0] = A[len(A) - 1]
	max_heap(A)
	return max

heap_extract_max(S)的时间复杂度为 O ( lg ⁡ n ) O(\lg n) O(lgn)。因为除了时间复杂度为 O ( lg ⁡ n ) O(\lg n) O(lgn)max_heap(A)以外,它的其他操作都是常数阶的。

heap_increase_key(S, i, k)能够实现将元素 x x x的关键字值增加到 k k k,这里假设 k k k的值不小于 x x x的原关键字值操作。在优先队列中,我们希望增加关键字的优先队列元素由对应的数组下标 i i i来标识。这一操作需要首先将元素 A [ i ] A[i] A[i]的关键字更新为新值。因为增大 A [ i ] A[i] A[i]的关键字可能会违反最大堆的性质,所以上述操作采用了类似于《排序算法(一):插入排序》中插入循环的方式,在从当前结点到根结点的路径上,为新增的关键字寻找恰当的插入位置。在heap_increase_key(S, i, k)的操作过程中,当前元素会不断地与其父结点进行比较,如果当前元素的关键字较大,则当前元素与其父结点进行交换。这过程会不断地重复,直到当前元素的关键字小于其父结点时终止,因为此时已经重新符合了最大堆的性质。

def heap_increase_key(S, i, k):
	if k < A[i]:
		print('New key is smaller than current key.')
		return k
	A[i] = k
	while i > 0 and A[parent(i)] < A[i]:
		A[i], A[parent(i)] = A[parent(i)], A[i]
		i = parent(i)

下图显示了heap_increase_key(S, i, k)的一个操作过程。在包含 n n n个元素的堆上,heap_increase_key(S, i, k)的时间复杂度是 O ( lg ⁡ n ) O(\lg n) O(lgn)
更新堆中的元素
同理,heap_insert(S, x)能够实现把元素 x x x插入集合 S S S中的操作。它的输入是要被插入到最大堆 S S S中的新元素的关键字。heap_insert(S, x)首先通过增加一个关键字为 A [ l e n ( A ) − 1 ] − 1 A[len(A) - 1] - 1 A[len(A)1]1的叶结点来扩展最大堆。然后调用heap_increase_key(S, len(A), x)为新结点设置对应的关键字,同时保持最大堆的性质。

def heap_insert(S, x):
	A[len(A)] = A[len(A) - 1] - 1
	heap_increase_key(S, len(A), x)

综上所述,在一个包含 n n n个元素的堆中,所有优先队列的操作都可以在 O ( lg ⁡ n ) O(\lg n) O(lgn)时间内完成。

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值