python - 如何避免在 heapq 中使用 _siftup 或 _siftdown
问题描述
我不知道如何在不使用_siftup
or的情况下有效地解决以下问题_siftdown
:
当一个元素乱序时,如何恢复堆不变量?
换句话说,更新old_value
到heap
,new_value
并继续heap
工作。您可以假设堆中只有一个old_value
。函数定义如下:
def update_value_in_heap(heap, old_value, new_value):
这是我的真实场景,有兴趣的可以阅读。
你可以想象它是一个小型的自动完成系统。我需要统计单词的频率,并保持前 k 个 max-count 单词,随时准备输出。所以我
heap
在这里使用。当一个字数++时,如果它在堆中,我需要更新它。所有单词和计数都存储在 trie-tree 的叶子中,堆
存储在 trie-tree 的中间节点中。如果你关心
out of heap 这个词,别担心,我可以从 trie-tree 的叶子节点得到它。当用户键入一个单词时,它将首先从堆中读取然后更新
它。为了获得更好的性能,我们可以考虑通过批量更新来降低更新频率。
那么,当一个特定的字数增加时,如何更新堆呢?
这是 _siftup 或 _siftdown 版本的简单示例(不是我的场景):
>>> from heapq import _siftup, _siftdown, heapify, heappop
>>> data = [10, 5, 18, 2, 37, 3, 8, 7, 19, 1]
>>> heapify(data)
>>> old, new = 8, 22 # increase the 8 to 22
>>> i = data.index(old)
>>> data[i] = new
>>> _siftup(data, i)
>>> [heappop(data) for i in range(len(data))]
[1, 2, 3, 5, 7, 10, 18, 19, 22, 37]
>>> data = [10, 5, 18, 2, 37, 3, 8, 7, 19, 1]
>>> heapify(data)
>>> old, new = 8, 4 # decrease the 8 to 4
>>> i = data.index(old)
>>> data[i] = new
>>> _siftdown(data, 0, i)
>>> [heappop(data) for i in range(len(data))]
[1, 2, 3, 4, 5, 7, 10, 18, 19, 37]
索引的成本为 O(n),更新的成本为 O(logn)。heapify
是另一种解决方案,但效率低于_siftup
or _siftdown
。
但是_siftup
and_siftdown
是 heapq 中的受保护成员,因此不建议从外部访问它们。
那么有没有更好更有效的方法来解决这个问题呢?这种情况的最佳做法?
感谢您的阅读,我非常感谢它帮助我。:)
已经参考heapq python - 如何修改堆排序的值,但没有回答我的问题
解决方案
@cglacet 的答案是完全错误的,但看起来非常合法。他提供的代码片段完全被破坏了!它也很难阅读。
_siftup()
被调用 n//2 次,heapify()
所以它不能比_siftup()
自己快。
要回答原始问题,没有更好的方法。如果您担心方法是私有的,请创建自己的方法来做同样的事情。
我唯一同意的是,如果您不需要长时间从堆中读取,那么在需要它们时将其懒惰可能是有益的。heapify()
问题是您是否应该为此使用堆。
让我们来看看他的片段的问题:
该heapify()
函数被多次调用以进行“更新”运行。导致这种情况的错误链如下:
- 他通过了
heap_fix
,但是期待heap
,对于sort
- 如果
self.sort
总是False
,self.heap
则总是True
- 他重新定义了
__getitem__()
and__setitem__()
每次分配或读取某些东西时都会调用_siftup()
它们_siftdown()
(注意:这两个在 C 中没有调用,所以它们使用__getitem__()
and__setitem__()
) - 如果
self.heap
isTrue
and are 被调用,则每次调用该__getitem__()
函数或交换元素。但是调用是在 C 中完成的,所以不会被调用,也不会进入无限循环__setitem__()
_repair()
_siftup()
siftdown()
heapify()
__getitem__()
- 他重新定义
self.sort
了这样称呼它,就像他试图做的那样,会失败 - 他读了一次,但更新了一个项目的
nb_updates
时间,而不是他声称的 1:1
我修复了这个例子,我尽可能地验证它,但我们都犯了错误。随意检查一下。
代码
import time
import random
from heapq import _siftup, _siftdown, heapify, heappop
class UpdateHeap(list):
def __init__(self, values):
super().__init__(values)
heapify(self)
def update(self, index, value):
old, self[index] = self[index], value
if value > old:
_siftup(self, index)
else:
_siftdown(self, 0, index)
def pop(self):
return heappop(self)
class SlowHeap(list):
def __init__(self, values):
super().__init__(values)
heapify(self)
self._broken = False
# Solution 2 and 3) repair using sort/heapify in a lazy way:
def update(self, index, value):
super().__setitem__(index, value)
self._broken = True
def __getitem__(self, index):
if self._broken:
self._repair()
self._broken = False
return super().__getitem__(index)
def _repair(self):
...
def pop(self):
if self._broken:
self._repair()
return heappop(self)
class HeapifyHeap(SlowHeap):
def _repair(self):
heapify(self)
class SortHeap(SlowHeap):
def _repair(self):
self.sort()
def rand_update(heap):
index = random.randint(0, len(heap)-1)
new_value = random.randint(max_int+1, max_int*2)
heap.update(index, new_value)
def rand_updates(update_count, heap):
for i in range(update_count):
rand_update(heap)
heap[0]
def verify(heap):
last = None
while heap:
item = heap.pop()
if last is not None and item < last:
raise RuntimeError(f"{item} was smaller than last {last}")
last = item
def run_perf_test(update_count, data, heap_class):
test_heap = heap_class(data)
t0 = time.time()
rand_updates(update_count, test_heap)
perf = (time.time() - t0)*1e3
verify(test_heap)
return perf
results = []
max_int = 500
update_count = 100
for i in range(2, 7):
test_size = 10**i
test_data = [random.randint(0, max_int) for _ in range(test_size)]
perf = run_perf_test(update_count, test_data, UpdateHeap)
results.append((test_size, "update", perf))
perf = run_perf_test(update_count, test_data, HeapifyHeap)
results.append((test_size, "heapify", perf))
perf = run_perf_test(update_count, test_data, SortHeap)
results.append((test_size, "sort", perf))
import pandas as pd
import seaborn as sns
dtf = pd.DataFrame(results, columns=["heap size", "method", "duration (ms)"])
print(dtf)
sns.lineplot(
data=dtf,
x="heap size",
y="duration (ms)",
hue="method",
)
结果
如您所见,“更新”方法使用_siftdown()
和_siftup()
渐近更快。
您应该知道您的代码做了什么,以及运行需要多长时间。如果有疑问,您应该检查一下。@cglaced 检查了执行需要多长时间,但他没有质疑需要多长时间。如果他这样做了,他会发现两者不匹配。而其他人则为之倾倒。
heap size method duration (ms)
0 100 update 0.219107
1 100 heapify 0.412703
2 100 sort 0.242710
3 1000 update 0.198841
4 1000 heapify 2.947330
5 1000 sort 0.605345
6 10000 update 0.203848
7 10000 heapify 32.759190
8 10000 sort 4.621506
9 100000 update 0.348568
10 100000 heapify 327.646971
11 100000 sort 49.481153
12 1000000 update 0.256062
13 1000000 heapify 3475.244761
14 1000000 sort 1106.570005
推荐阅读
- mongoose - 合并猫鼬中的字段
- debugging - Golang 调试器未运行
- kubernetes - Kubernetes 仅设置容器资源限制意味着资源请求的值相同
- prestashop - 如何在 PrestaShop 1.7.4.2 中使货币符号显示在数字之后并在它们之间添加空格
- omnet++ - 如何在 Veins 4.7.1 中获得碰撞
- python-3.x - 带有 .txt 文件的 CSV 阅读器
- javascript - 将函数名称移动到外部配置文件/模块时,函数未定义错误
- c++ - VSCode C/C++ Intellisense 是否完整的类成员?
- kotlin - 我将如何为模块提供动态令牌?
- apache-kafka - Spring kafka 中的事务