我有一个调度算法,我在其中比较优先级/任务元组列表的最小值和最大值,对它们执行一些更改其优先级的操作,然后将它们重新插入列表并使列表更新得当。 heapq是最好的数据结构吗?我将如何进行初始比较(这基本上将确定优先级值是否足够远,需要进一步操作;如果不是,函数将停止)而不会弹出?一旦进行了比较,我将如何将最大值与最小值一起使用,因为heapq仅用于弹出最小值?
答案 0 :(得分:3)
heapq
仅提供最小堆 - 也就是说,您可以在O(log N)时间内弹出min
值,但不能在max
值中弹出。{/ p>
如果您想要一个类似于heapq
的双面数据结构,则有一些基本选项。
首先,常规最小堆的问题是什么?它不仅仅是API;找到最大值需要O(n)
时间而不是O(1)
时间,因此弹出它需要O(n)
而不是O(log n)
,这是您想要改进的关键。
一个简单的黑客攻击涉及保留两个堆,一个具有正常值,一个具有正常值,因此它们向后排序。这是伪代码的实现:
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
版本交错为单个结构,并且可以很容易地在上面的“检查”情况下做正确的事情(而不是留下值)。
但是,如果你想要一个双端优先级队列的对称性能,你实际上不能比平衡树或跳转列表做得更好。 (好吧,不是出于一般目的。如果你有特定的行为特征,那可能不是真的。)而且还有比min-max二进制堆更多的AVL树,红黑树和跳过列表的实现。因此,搜索PyPI和ActiveState配方“平衡树”,“红黑树”,“AVL树”,“跳过列表”等,你会发现像bintrees
和{{3}这样的东西这应该都可以。
但是,我建议skiplist
。它使用平衡树和数组的特殊混合而不是经过充分研究的数据结构,乍一看可能会让您认为它不那么值得信赖。但是,我相信它比任何竞争模块都有更多的使用和实际测试,并且它也经过了相当大的优化。 (当您处理A * log Bn + C
性能时,更改A
或C
通常比更改B
有更大的影响。)它也有一个很好的界面 - 实际上,其中一些。如果您使用blist.sortedlist
,则只需执行sl[0]
,sl[-1]
,sl.pop(0)
,sl.pop(-1)
和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)
。
另请参阅blist
,了解可替代此处可能相关的二进制堆的其他类型的堆。
最后一点,igorrs的评论提醒我:
有各种不同的数据结构可以在这里为您提供相同的最坏情况算法复杂性。有时,任何避免O(n)
的东西都足够好,所以你应该选择最简单的实现并完成它。但有时候(特别是很多操作但很小n
或非典型数据),常数因子,最佳情况等都可以产生巨大的差异。在这种情况下,正确的做法是构建多个实现并使用实际数据进行测试,并查看最快的内容。
答案 1 :(得分:1)
鉴于您正在考虑堆,我可以假设您的期望(n
是元素的总数):
O(1)
时间内找到最小的密钥和最大的密钥。O(log(n))
时间内重新插入(使用已更改的键)具有最小键的元素和具有最大键的元素。这可以通过min-max heap来完成。不幸的是,我认为这不适用于Python的标准库。
如果你放宽了第一个要求,那么任何平衡的树(例如红黑)都可以做到这一点,所有操作都需要O(log(n))
时间。
Python的标准库也不提供任何平衡树,因此您必须自己动手或寻找实现。