Python 固定容量堆:heapreplace 总会换入新值,为什么可能把前三名越换越小

前天 3阅读

数据不断进入,只想保留最大的三个数,可以用小顶堆把当前门槛放在堆顶。但如果每来一条就直接heapreplace,低于门槛的新值也会被放进去,原本应该留下的高值反而出局。容量保持三,并不代表内容仍是前三名。

下面先让两个操作处理同一个低分候选,再把正确动作放进完整的Top-K函数。保存为demo.py,运行python demo.py。输入只有整数,全部在内存中执行,输出排序仅用于方便比较结果。

Python 固定容量堆:heapreplace 总会换入新值,为什么可能把前三名越换越小

AI模型生成概念示意:有限席位只接纳足够大的候选,较小候选被返回;柱形表示比较关系,不是性能测试结果。

import heapq

kept = [5, 8, 9]
replaced = kept.copy()
returned = heapq.heappushpop(kept, 2)
removed = heapq.heapreplace(replaced, 2)
assert returned == 2 and sorted(kept) == [5, 8, 9]
assert removed == 5 and sorted(replaced) == [2, 8, 9]
print("pushpop:", returned, sorted(kept))
print("replace:", removed, sorted(replaced))

def largest_k(values, k):
    if type(k) is not int or k < 0:
        raise ValueError("k must be a nonnegative integer")
    if k == 0:
        return []
    heap = []
    for value in values:
        if len(heap) < k:
            heapq.heappush(heap, value)
        else:
            heapq.heappushpop(heap, value)
    return sorted(heap, reverse=True)

data = [5, 8, 9, 2, 10, 8]
assert largest_k(data, 3) == sorted(data, reverse=True)[:3]
assert largest_k(data, 3) == [10, 9, 8]
assert largest_k([], 3) == []
assert largest_k([4], 3) == [4]
assert largest_k([7, 7], 2) == [7, 7]
assert largest_k(data, 0) == []
print("top three:", largest_k(data, 3))

empty = []
assert heapq.heappushpop(empty, 4) == 4 and empty == []
try:
    heapq.heapreplace([], 4)
except IndexError:
    print("empty heap: pushpop returns input; replace raises IndexError")
else:
    raise AssertionError("empty replace accepted")

返回的值可能就是刚来的候选

pushpop行输出2与[5, 8, 9],低分候选没有进入保留集合。这个组合动作的语义相当于先把新项放进堆,再取走所有候选中的最小项,因此可以把新项自己返回。堆原来已有三项,操作后仍有三项。

replace行输出5与[2, 8, 9]。heapreplace先移除原堆顶,再把新值放入,所以它一定返回旧堆中的一项。新值即使更小也会被接纳;函数并没有检查“这条数据值不值得换进去”。这适合明确需要替换的情形,不能直接当作Top-K筛选器。

填充阶段与筛选阶段要分开

largest_k在数量不足k时调用heappush,先建立候选集合;达到容量后再用heappushpop。实跑得到top three: [10, 9, 8],并与全量排序结果对照。小顶堆的堆顶代表已保留项中最小的一项,正好是新候选需要超过的门槛。

如果想用heapreplace实现同样算法,就应在堆已满且新值大于堆顶时才替换。不要先弹出再检查,否则门槛对应的旧值已经被移除。测试应同时包含低于门槛、高于门槛和相等值,不能只用持续递增的输入。

空堆与并列项也属于接口约定

最后一行显示空堆上的heappushpop直接返回输入,堆依然为空;heapreplace则抛IndexError。这解释了为何不能从空列表开始无条件使用pushpop,它不会替你完成最初的容量填充。示例对k为零直接返回,也覆盖输入不足k的情况。

重复整数会作为不同项目保留,本例[7, 7]仍输出两项。若业务要求不同分数或同分人员全部入选,已经是另一种规则,需要另外去重或扩展容量;不能只改变排序方向。真实记录还应明确比较键及同分次序,本例刻意只使用整数。

堆只保证顶端是最小值,内部列表并不是完整排序结果;最后调用sorted才产生降序展示。需要持续更新时可以长期保留这个堆,按需生成展示副本。若只是一次性取前几项,标准库的nlargest也值得优先考虑,能减少自行维护边界的代码。

资料核对日期:2026年10月2日。示例在本地 Python 3.12.14 实际执行,全部断言通过;输出对应文中固定输入。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。