【问题标题】:What is the time complexity of tree traversal using yield from?使用 yield from 遍历树的时间复杂度是多少?
【发布时间】:2016-12-10 19:56:20
【问题描述】:

深度优先树遍历示例:

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def add_child(self, child):
        self._children.append(child)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        yield self
        for c in self:
            yield from c.depth_first()

我知道yield from 不会立即使用生成器,而是将yield 向上传递给它的调用者。

但是这个传递的过程还是存在的,所以yield会从每个节点一直传递到它的根节点,我们可以用递归来描述运行时间(假设为简单起见是二叉树,但思路是一样的):

T(n) = 2*T(n/2) + Θ(n)

Θ(n) 存在是因为该节点必须将所有从其后代传递的yield 传递给其父节点。而由上式推导出的时间复杂度为:

O(nlogn)

但是,如果我根本不使用yieldyield from,树遍历的时间复杂度只有O(n)

我想知道我是否误解了yield 的工作原理,或者编写这样的递归生成器根本不可行。

【问题讨论】:

  • 公式中的 Θ(n) 不应该是 Θ(1) 吗?平衡树中节点的后代数不是常数吗?
  • @DYZ 我认为应该是 Θ(n)。它不传递后代的数量,而是传递其所有后代的yield 语句。
  • “所有的后代”是否包括后代的后代等? (我假设它没有。)如果不是,它仍然是一个常数。
  • @DYZ 它确实包括后代的后代,因为每个节点都会做两件事:(1)将自己屈服于其调用者(2)产生其后代以及其后代产生的后代的后代对它等等......
  • 哦,你是对的。那么它确实是 O(n log n),因为你本质上将遍历结果委托给了根节点,而不是在本地使用它们。

标签: python time-complexity yield


【解决方案1】:

来自yield from 的官方 Python 3.3 版本:https://www.python.org/dev/peps/pep-0380/

使用专门的语法开辟了优化的可能性 当有很长的发电机链时。这样的链条可能会出现,因为 例如,当递归遍历树结构时。开销 传递 next() 调用并在链中向下和向上生成值 在最坏的情况下,可能会导致应该是 O(n) 的操作 情况下,O(n**2)。

一种可能的策略是向生成器添加一个槽 持有被委派给的生成器的对象。当 next() 或 在生成器上进行 send() 调用,首先检查此插槽,然后 如果它是非空的,它引用的生成器被恢复 反而。如果它引发 StopIteration,则槽被清除并且主 发电机恢复。

这会将委托开销减少到 不涉及 Python 代码执行的 C 函数调用链。一种 可能的增强将是遍历整个链 循环中的生成器并在最后直接恢复生成器,尽管 那么 StopIteration 的处理就比较复杂了。

看起来yield from 仍然需要遍历树。但是这种遍历是由 C 中的解释器完成的,而不是 Python 中的。所以从技术上讲,它仍然是 O(n) 开销,但并不像听起来那么糟糕。

【讨论】:

  • 这是一个已解决的问题,还是他们仍在努力解决?
猜你喜欢
  • 1970-01-01
  • 2015-03-05
  • 1970-01-01
  • 2023-03-21
  • 2017-10-29
  • 1970-01-01
  • 2020-08-18
  • 1970-01-01
  • 2021-12-20
相关资源
最近更新 更多