【问题标题】:Efficient recursive iterator: Possible?高效的递归迭代器:可能吗?
【发布时间】:2020-01-18 13:30:41
【问题描述】:

这件事困扰了我很久。我想要一个高效、简单和 Pythonic 的二叉树(或类似的嵌套结构)的迭代器。例如像这样的用法:

for value in values(root):
    do_something_with_value(value)

print(sum(values(root)))

这里root是树的根节点,节点有.value.left.rightvalues 应该为我提供所需的迭代器/可迭代树的值。

设 n 为树中的节点数,h 为树的高度。

尝试 1:太慢了

def values(root):
    if root:
        yield from values(root.left)
        yield root.value
        yield from values(root.right)

简单、pythonic、懒惰,并且只占用 O(h) 空间。这应该是它。但是......它是堆叠的生成器,每个值都通过整个生成器堆栈产生。所以整个迭代在最坏情况下需要 O(n2) 时间,即使在最好情况下也需要 O(n log n) 时间。

尝试 2:太复杂了

def values(root):
    stack = []
    while root or stack:
        while root:
            stack.append(root)
            root = root.left
        root = stack.pop()
        yield root.value
        root = root.right

使用一堆节点进行迭代。只需要 O(h) 空间和 O(n) 时间,但是比上面的完全自然递归要复杂得多。

尝试 3:太大

def values(root):
    result = []
    def collect_values(root):
        if root:
            collect_values(root.left)
            result.append(root.value)
            collect_values(root.right)
    collect_values(root)
    return result

这会在一个半全局列表中收集所有值。自然递归和 O(n) 时间,但遗憾的是 O(n) 空间并且不是惰性的。

尝试 4:无法正常工作

我想也许我可以滥用半全局生成器,而不是半全局列表。作为一种从递归内部直接到外部的管道。递归会将send 值放入其中,外部可以获取它们。像这样的:

def values(root):
    pipe = magic_generator_pipe()
    def iterate(root):
        if root:
            iterate(root.left)
            pipe.send(root.value)
            iterate(root.right)
    iterate(root)
    yield from pipe

但我无法让它工作,我不确定它是否可能。

尝试 5:也许

threadingasyncio 的东西?我的另一个想法是values 函数启动一个新线程。此线程递归地迭代树并将值传递给values 函数中的主线程,这会将它们产生给原始的外部调用者。他们适当地相互锁定。但我对此没有太多经验。

问题

有没有办法实现我想要的一切?

  1. Pythonic 惰性迭代器
  2. O(n) 总时间
  3. O(h) 空间
  4. 自然递归

所以基本上我想要尝试 1 之类的东西,但速度很快。因为我用递归yield from 解决了几个问题,总是对时间复杂度感到不好。

澄清一下:“太复杂”是指迭代算法(它并不复杂,但与自然递归相比)。仅以“技术”方式“复杂”的解决方案(例如使用额外的线程或@chepner 的蹦床想法)仍然很有趣。我只是坚持自然递归(或类似算法简单的东西)和其他三个目标。

【问题讨论】:

  • 这听起来可能更复杂,但复杂的部分至少被隔离在一个可重复使用的单个部分中。将values写为尾递归函数,然后使用蹦床(例如github.com/0x65/trampoline)实现尾调用优化。 (虽然,您希望累加器成为一个迭代器,而不是一个列表。我认为这是可能的,虽然我没有考虑过。将一堆单项链接在一起的成本(或至少很小的)迭代器可能会破坏这种方法的目的。)
  • @chepner 嗯,我们可以用我的 两个 递归调用进行尾递归吗?关于这些超出目的的额外内容:我的“太复杂”实际上是指迭代 algorithm (它还不错,但与自然递归相比)。仅以“技术”方式(例如使用额外的线程或蹦床)“复杂”的解决方案仍然很有趣。我只是坚持自然递归(或类似算法简单的东西)和其他三个既定目标。
  • 嗯。还有eli.thegreenplace.net/2017/…,它解释了如何使用 CPS 将斐波那契数的自然实现转换为尾递归函数,但遗憾的是并没有像我之前的链接那样提供算法的实际实现。也许如果你自己写复杂的部分...... :)

标签: python performance iterator


【解决方案1】:

我的方法可能无法满足您,因为它与您正在做的事情相反,即使用回调。您将一个回调方法传递给一个能够迭代结构的函数,该回调方法将为遇到的每个值调用。在此示例中,函数 walk 使用 callback 函数调用。在这种情况下,结构是一个遍历的树,并且对于每个节点值,都会调用回调函数。

def walk(root, callback):

    def tree_walk(node):
        if node['left']:
            tree_walk(node['left'])
        callback(node['value'])
        if node['right']:
            tree_walk(node['right'])

    tree_walk(root)


node4 = {'value': 4, 'left': None, 'right': None}
node3 = {'value': 3, 'left': node4, 'right': None}
node2 = {'value': 2, 'left': None, 'right': None}
node1 = {'value': 1, 'left': node2, 'right': node3}

def do_something_with_value(value):
    print('value:', value)

walk(node1, do_something_with_value)

打印:

value: 2
value: 1
value: 4
value: 3

【讨论】:

  • 对。我以前确实这样做过。虽然这确实不是我想要的。如前所述,我想要一个迭代器/可迭代的。这样我就可以像其他迭代器一样普遍使用它。在我的玩具示例中,您的方式完美地替换了 for 循环。但是如果循环体不仅仅是一个以值作为唯一参数的函数调用,或者如果我想以其他方式使用它,比如sum(values(root))?我认为它不起作用,至少在 sum 的情况下。
  • 注意:我编辑了问题以澄清我的意思,添加了sum 示例但保持for-loop 示例原样,以便您的答案仍然是该案例的完美替代品。
猜你喜欢
  • 2015-02-11
  • 1970-01-01
  • 2010-11-07
  • 1970-01-01
  • 2014-07-22
  • 2015-07-18
  • 1970-01-01
  • 2017-06-04
相关资源
最近更新 更多