分类目录:《算法设计与分析》总目录 相关文章: ·堆(一):基础知识 ·堆(二):维护堆的性质 ·堆(三):建堆 ·堆(四):优先队列 ·排序算法(三):堆排序
本文我们要介绍堆的一个常见应用:作为高效的优先队列。和堆一样,优先队列也有两种形式:最大优先队列和最小优先队列。这里我们关注于如何基于最大堆实现最大优先队列,但是在本博客主要采用的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)时间内完成。
|