我有一个调度算法,我比较优先级/任务元组列表的最小值和最大值,对它们进行一些更改优先级的操作,然后将它们重新插入列表并适当地更新列表。heapq 会是最好的数据结构吗?我将如何在不弹出的情况下进行初始比较(这基本上是确定优先级值是否相距足够远以需要进一步操作;如果不是,该函数将停止)?一旦进行了比较,我将如何将最大值与最小值一起使用,因为 heapq 是为仅弹出最小值而设计的?
2 回答
heapq
只提供一个最小堆——也就是说,您可以min
在 O(log N) 时间内弹出值,但不能弹出max
值。
如果你想要一个类似于 的双面数据结构heapq
,有几个基本选项。
首先,常规最小堆有什么问题?这不仅仅是 API;找到最大值需要O(n)
时间而不是O(1)
时间,因此弹出它需要O(n)
而不是O(log n)
,这是您要改进的关键。
一个简单的 hack 涉及保留两个堆,一个具有正常值,一个具有正常值的修饰,以便它们向后排序。这是伪代码中的实现:
def push(self, value):
insert into both normal and reversed heaps
def minpop(self):
check that the min value of normal hasn't reached the min value of reversed
pop and return the min value of normal
def maxpop(self):
check that the min value of reversed hasn't reached the min value of normal
pop and return the min value of reversed
乍一看,似乎每个操作的最坏情况行为应该是 minheap 的两倍,但事实并非如此。特别是,最坏情况下的空间是曾经插入的元素数量,它可能远高于插入数量的两倍 - 删除的数量。(例如,如果您插入了 1000 个项目并删除了 100,则 900 >> 200。)
有许多用例不起作用,如果它在您的用例中不起作用,那应该是显而易见的。但是当它合适的时候,它是非常简单的。
如果不合适,您可以使用真正的最小-最大堆。这基本上只是将最小堆的normal
和reversed
版本交错到单个结构中,并且可以在上面的“检查”案例中轻松地做正确的事情(而不是留下值)。
但是,如果您想要双端优先级队列的对称性能,您实际上无法比平衡树或跳过列表做得更好。(嗯,不是出于一般目的。如果你有特定的行为特征,那可能不是真的。) AVL 树、红黑树和跳过列表的实现比 min-max 二叉堆要多得多。因此,在 PyPI 和 ActiveState 配方中搜索“平衡树”、“红黑树”、“AVL 树”、“skiplist”等,你会发现像bintrees
and之类的东西skiplist
,它们应该都可以工作。
但是,我建议blist
. 它使用平衡树和数组的特殊混合,而不是经过充分研究的数据结构,乍一看可能会让您认为它不太可信。然而,我相信它比任何竞争模块都得到了更多的使用和实际测试,并且它也得到了相当大的优化。(当您处理A * log Bn + C
性能时,更改A
或C
通常比更改具有更大的影响B
。)它还有一个很好的界面——实际上,其中有一些。如果你使用blist.sortedlist
,你可以做sl[0]
, sl[-1]
, sl.pop(0)
, sl.pop(-1)
, and sl.add(x)
, 几乎完全符合你的预期。
所以,你的代码看起来像这样(如果我理解你的英文描述):
class MyQueue(object):
def __init__(self):
self.sl = blist.sortedlist(key=operator.itemgetter(0))
def add(self, priority, task):
self.sl.add((priority, task))
def step(self):
if self.sl[-1][0] - self.sl[0][0] < MyQueue.EPSILON:
return
minprio, mintask = self.sl.pop(0)
maxprio, maxtask = self.sl.pop(-1)
newminprio, newmaxprio = recalc_priorities(minprio, maxprio)
self.add(newminprio, mintask)
self.add(newmaxprio, maxtask)
这些方法中的任何一个的问题在于,偷看双方的最坏情况是O(log N)
而不是O(1)
。但是有一个简单的方法可以解决这个问题,如果这些是您需要的唯一操作:只需将这些值缓存起来:
class MyQueue(object):
def __init__(self):
self.sl = blist.sortedlist(key=operator.itemgetter(0))
self.minprio, self.maxprio = None, None
def add(self, priority, task):
self.sl.add((priority, task))
if prio < self.minprio: self.minprio = prio
elif prio > self.maxprio: self.maxprio = prio
def step(self):
if self.maxprio - self.minprio < MyQueue.EPSILON:
return
minprio, mintask = self.sl.pop(0)
maxprio, maxtask = self.sl.pop(-1)
newminprio, newmaxprio = recalc_priorities(minprio, maxprio)
self.add(newminprio, mintask)
self.add(newmaxprio, maxtask)
self.minprio, self.maxprio = sl[0][0], sl[-1][0]
这使得快速路径通过step
O(1)
而不是O(log n)
,它使所有现有O(log n)
操作保持不变O(log n)
。
另请参阅Wikipedia以讨论其他类型的堆,这些堆可以替代此处可能相关的二进制堆。
最后一点,igorrs 的评论让我想起:
有多种不同的数据结构可以在这里获得相同的最坏情况算法复杂性。有时,避免任何事情O(n)
就足够了,因此您应该使用最简单的实现并完成它。但有时(特别是对于许多操作但很小n
,或非典型数据),常数因子、最佳情况等可能会产生巨大的差异。在这种情况下,正确的做法是构建多个实现并使用真实数据进行测试,看看什么是最快的。
鉴于您正在考虑堆,我可以假设您的期望(n
元素总数)是:
- 及时找到最小的钥匙和最大的钥匙
O(1)
。 - 及时重新插入(使用更改的键)具有最小键的元素和具有最大键的元素
O(log(n))
。
这可以通过min-max heap来完成。不幸的是,我认为这在 Python 的标准库中不可用。
如果您放宽第一个要求,任何平衡树(例如红黑树)都可以解决问题,并O(log(n))
为所有所需的操作留出时间。
Python 的标准库也不提供任何平衡树,因此您必须自己动手或寻找实现。