具有自定义比较谓词的 heapq

Posted

技术标签:

【中文标题】具有自定义比较谓词的 heapq【英文标题】:heapq with custom compare predicate 【发布时间】:2012-02-11 03:21:48 【问题描述】:

我正在尝试使用自定义排序谓词构建堆。由于进入它的值是“用户定义”类型,我无法修改它们的内置比较谓词。

有没有办法做类似的事情:

h = heapq.heapify([...], key=my_lt_pred)
h = heapq.heappush(h, key=my_lt_pred)

或者更好的是,我可以将 heapq 函数包装在自己的容器中,这样我就不需要继续传递谓词了。

【问题讨论】:

***.com/questions/679731/min-heap-in-python 的可能重复项 How to make heapq evaluate the heap off of a specific attribute?的可能重复 【参考方案1】:
setattr(ListNode, "__lt__", lambda self, other: self.val <= other.val)

使用它来比较 heapq 中对象的值

【讨论】:

避免重新定义/重新封装对象的有趣方式! 谢谢!这正是我正在寻找的 虽然这可能适用于Leetcode,但这不适用于heapq【参考方案2】:

根据heapq documentation,自定义堆顺序的方法是让堆上的每个元素成为一个元组,第一个元组元素是一个接受普通Python比较的元素。

heapq 模块中的函数有点麻烦(因为它们不是面向对象的),并且总是需要我们的堆对象(一个堆化列表)作为第一个参数显式传递。我们可以通过创建一个非常简单的包装类来用一块石头杀死两只鸟,它允许我们指定一个key 函数,并将堆呈现为一个对象。

下面的类保留一个内部列表,其中每个元素都是一个元组,其中的第一个成员是一个键,在元素插入时使用key 参数计算,在堆实例化时传递:

# -*- coding: utf-8 -*-
import heapq

class MyHeap(object):
   def __init__(self, initial=None, key=lambda x:x):
       self.key = key
       self.index = 0
       if initial:
           self._data = [(key(item), i, item) for i, item in enumerate(initial)]
           self.index = len(self._data)
           heapq.heapify(self._data)
       else:
           self._data = []

   def push(self, item):
       heapq.heappush(self._data, (self.key(item), self.index, item))
       self.index += 1

   def pop(self):
       return heapq.heappop(self._data)[2]

(额外的self.index 部分是为了避免在评估的键值是平局并且存储的值不能直接比较时发生冲突 - 否则 heapq 可能会因 TypeError 而失败)

【讨论】:

非常好!你甚至可以更进一步,使用三元组 (self.key(item), id, item),其中 id 可以是作为类属性处理的整数,并在每次推送后递增。这样,您可以避免 key(item1) = key(item2) 时引发的异常。因为键是唯一的。 我实际上试图将这个(或基于这个的东西)推送到 Python 的 stdlib 中,但这个建议被拒绝了。 可惜,适合大多数 Python 特性的面向对象风格,并且 key 参数提供了额外的灵活性。 我使用了列表而不是元组,例如[self.key(item), id, item] 只要第一个索引是关键,它就可以正常工作。 如果元素不可比较并且键值存在关联,这将失败。我会将id(item) 作为元组的中间元素来打破平局。【参考方案3】:

定义一个类,在其中重写__lt__() 函数。请参见下面的示例(适用于 Python 3.7):

import heapq

class Node(object):
    def __init__(self, val: int):
        self.val = val

    def __repr__(self):
        return f'Node value: self.val'

    def __lt__(self, other):
        return self.val < other.val

heap = [Node(2), Node(0), Node(1), Node(4), Node(2)]
heapq.heapify(heap)
print(heap)  # output: [Node value: 0, Node value: 2, Node value: 1, Node value: 4, Node value: 2]

heapq.heappop(heap)
print(heap)  # output: [Node value: 1, Node value: 2, Node value: 2, Node value: 4]

【讨论】:

这似乎是迄今为止最干净的解决方案! 完全同意前面两个cmets。对于 Python 3,这似乎是一个更好、更清洁的解决方案。 我使用__gt__ 对此进行了测试,并且效果也很好。为什么我们使用哪种魔法方法并不重要?我在heapq 的文档中找不到任何内容。也许这与 Python 一般如何进行比较有关? heapq 中进行比较时,Python 首先查找__lt__()。如果未定义,它将查找__gt__()。如果两者都没有定义,则抛出TypeError: '&lt;' not supported between instances of 'Node' and 'Node'。这可以通过定义__lt__()__gt__() 来确认,在每个中放置一个打印语句,并让__lt__() 返回NotImplemented 要使这个解决方案成为一个完整的解决方案,需要有一个决胜局。为了在“lt”函数中的“self.val == other.val”打破平局,一种选择是引入其他字段(优先级或与您的业务领域相关的内容) 到 Node 类中,以便我们可以比较该字段并确保该字段的值不相等。【参考方案4】:

这两个答案的限制是它们不允许将关系视为关系。在第一个中,通过比较项目来打破关系,在第二个中通过比较输入顺序。让领带成为领带会更快,如果它们很多,它可能会产生很大的不同。基于上述和文档,尚不清楚这是否可以在 heapq 中实现。 heapq 不接受密钥,而在同一模块中从它派生的函数却接受,这似乎很奇怪。 PS: 如果您点击第一条评论中的链接(“可能重复...”),还有另一个定义 le 的建议,这似乎是一个解决方案。

【讨论】:

写“两个答案”的局限性在于,不再清楚它们是什么。【参考方案5】:

heapq documentation 表明堆元素可以是元组,其中第一个元素是优先级并定义排序顺序。

然而,与您的问题更相关的是,该文档包含一个discussion with sample code,说明如何实现自己的 heapq 包装函数来处理排序稳定性问题和具有相同优先级的元素(以及其他问题)。

简而言之,他们的解决方案是让 heapq 中的每个元素都是三元组,包括优先级、条目计数和要插入的元素。条目计数确保具有相同优先级的元素按照它们添加到堆中的顺序进行排序。

【讨论】:

这是正确的解决方案,heappush 和 heappushpop 都直接使用元组 这个解决方案是干净的,但不能涵盖所有自定义算法,例如最大堆字符串。

以上是关于具有自定义比较谓词的 heapq的主要内容,如果未能解决你的问题,请参考以下文章

自定义获取比较器

可以由调用者自定义的通用谓词?

如何对实体的自定义属性进行谓词

使用自定义谓词对 numpy 数组进行排序

在 Json 自定义 ContractResolver 的 ShouldSerialize 谓词中获取请求

如何在 Spark 中创建 UDF 以支持自定义谓词