【问题标题】:All Paths for a Sum with return issues有返回问题的总和的所有路径
【发布时间】:2021-04-16 14:51:48
【问题描述】:

我有一个问题是要找到总和的所有路径。问题是:

给定一棵二叉树和一个数字“S”,找到从根到叶的所有路径,使得每条路径的所有节点值之和等于“S”。

我的递归方法是:

def all_sum_path(root, target):
    result = []
    find_sum_path(root, target, result, [])
    return result

def find_sum_path(root, target, result, new_path):
    if not root:
        return None
    new_path.append(root.value)
    diff = target - root.value
    if not root.left and not root.right and diff == 0:
        # copy the value of the list rather than a reference
        result.append(list(new_path))
    if root.left:
        return find_sum_path(root.left, diff, result, new_path)
    if root.right:
        return find_sum_path(root.right, diff, result, new_path)
    del new_path[-1]


class TreeNode():
    def __init__(self, _value):
        self.value = _value
        self.left, self.right, self.next = None, None, None

def main():
    root = TreeNode(1)
    root.left = TreeNode(7)
    root.right = TreeNode(9)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    root.right.left = TreeNode(2)
    root.right.right = TreeNode(7)

    print(all_sum_path(root, 12))

    root = TreeNode(12)
    root.left = TreeNode(7)
    root.right = TreeNode(1)
    root.left.left = TreeNode(4)
    root.right.left = TreeNode(10)
    root.right.right = TreeNode(5)

    print(all_sum_path(root, 23))

main()

输出是:

[[1, 7, 4]]
[[12, 7, 4]]

Process finished with exit code 0

但是,正确的做法应该是:

def all_sum_path(root, target):
    result = []
    find_sum_path(root, target, result, [])
    return result

def find_sum_path(root, target, result, new_path):
    if not root:
        return None
    new_path.append(root.value)
    diff = target - root.value
    if not root.left and not root.right and diff == 0:
        # copy the value of the list rather than a reference
        result.append(list(new_path))
    if root.left:
        find_sum_path(root.left, diff, result, new_path)
    if root.right:
        find_sum_path(root.right, diff, result, new_path)
    del new_path[-1]


class TreeNode():
    def __init__(self, _value):
        self.value = _value
        self.left, self.right, self.next = None, None, None

def main():
    root = TreeNode(1)
    root.left = TreeNode(7)
    root.right = TreeNode(9)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    root.right.left = TreeNode(2)
    root.right.right = TreeNode(7)

    print(all_sum_path(root, 12))

    root = TreeNode(12)
    root.left = TreeNode(7)
    root.right = TreeNode(1)
    root.left.left = TreeNode(4)
    root.right.left = TreeNode(10)
    root.right.right = TreeNode(5)

    print(all_sum_path(root, 23))

main()

有输出:

[[1, 7, 4], [1, 9, 2]]
[[12, 7, 4], [12, 1, 10]]

Process finished with exit code 0

我在这里有一些问题:

  1. 为什么我们在递归语句中不需要return?我也对return 语句如何将输出减少到只有一个感兴趣?

  2. 为什么我们不需要result = find_sum_path(root, target, result, [])?那么更新结果背后的逻辑是什么?

  3. 不知道为什么时间复杂度是O(N^2)?

上述算法的时间复杂度为O(N^2),其中‘N’是树中节点的总数。这是因为我们遍历每个节点一次(这将花费 O(N)),并且对于每个叶节点,我们可能必须存储它的路径,这将花费 O(N)。

提前感谢您的帮助。

【问题讨论】:

    标签: python recursion return binary-tree


    【解决方案1】:

    为什么我们不需要在递归语句中返回?

    为什么我们不需要 result = find_sum_path(root, target, result, [])?那么更新结果背后的逻辑是什么?

    result 列表(以及new_path 列表)通过引用(或者更确切地说是通过赋值,请参阅what does it mean by 'passed by assignment'?)通过递归堆栈,这意味着result 变量始终指向相同的位置在你的记忆中,因为它在 all_sum_path 中被初始化(只要它没有被重新分配),你可以根据需要对其进行变异。

    我还对 return 语句如何将输出减少到只有一个感兴趣?

    当您在解决方案中使用return 时,您将完全放弃在左子树完成后探索节点的右子树。

    if root.left: 
        return find_sum_path(root.left, diff, result, new_path)
    # -- unreachable code if `root.left` is not `None` --
    if root.right:
        return find_sum_path(root.right, diff, result, new_path)
    

    不知道为什么时间复杂度是O(N^2)?

    if not root.left and not root.right and diff == 0:
        # copy the value of the list rather than a reference
        result.append(list(new_path))
    

    这部分代码正在制作new_path 的完整副本,以将其附加到result。以二叉树为例,它介于高度不平衡和完全平衡之间,所有节点的值都为 0,S 也是 0。在这种情况下,您将制作 L(叶节点数)的副本 @ 987654333@ 每个最多包含H 个元素(树的高度)所以O(L * H) ~ O(N^2)

    所以最坏情况可能的时间复杂度肯定不是线性 O(N),但也不完全是 O(N^2)。

    【讨论】:

    • 感谢您的帮助。我还有一些问题,希望你能解释更多。 1. 由于结果是 find_sum_path 中的一个引用,因此它会在函数 find_sum_path 中得到更新。这就是为什么我不需要退货。 2. 对于return,如果使用return,为什么要放弃右子树? 3. 为什么new_path的副本会花费O(H)? 'S 也是 0' 中的 S 是什么?
    • 对不起,S 是目标总和。
    • 2.如果某个节点有左子节点,您正在执行return find_sum_path(root.left, diff, result, new_path),代码根本无法转到该节点的右子节点,我鼓励您空运行它或附加调试器并检查 3。当您到达叶节点并且满足您的条件,您希望对到目前为止收集在new_path 中的路径进行完整克隆,因为 new_path 只是在遍历期间添加和删除节点的临时缓冲区。在最坏的情况下,new_path 可以包含 len H 的树中最长的路径,即树的高度
    • 感谢您的解释。 return 将简单地终止我当前函数的执行,导致另一个子树的丢失。我仍然有点困惑,因为克隆new_path 的复杂性需要 O(H)。或者我们可以说克隆的复杂度是 O(N)?谢谢。
    【解决方案2】:

    首先,我想说您已经非常接近解决问题,而且您做得非常出色。递归是一种函数式遗产,因此将其与函数式风格一起使用会产生最佳结果。这意味着要避免诸如突变、变量重新分配和其他副作用之类的事情。这可以消除许多错误来源(和令人头疼的问题)!

    为了美化您的程序,我们可以先修改TreeNode,使其在构造时同时接受leftright 参数-

    class TreeNode():
      def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right
    

    现在我们可以定义两棵树,t1t2。请注意,我们不会重新分配 root -

    def main():
      t1 = TreeNode \
        ( 1
        , TreeNode(7, TreeNode(4), TreeNode(5))
        , TreeNode(9, TreeNode(2), TreeNode(7))
        )
        
      t2 = TreeNode \
        ( 12
        , TreeNode(7, TreeNode(4), None)
        , TreeNode(1, TreeNode(10), TreeNode(5))
        )
    
      print(all_sum_path(t1, 12))
      print(all_sum_path(t2, 23))
    
    main()
    

    预期的输出是 -

    [[1, 7, 4], [1, 9, 2]]
    [[12, 7, 4], [12, 1, 10]]
    

    最后我们实现find_sum。我们可以使用mathematical induction为我们的函数写一个简单的案例分析-

    1. 如果输入树t为空,返回空结果
    2. (归纳)t 为空。如果t.value 与目标q 匹配,则已找到解决方案;将t.value 添加到当前的path 和yield
    3. (归纳)t 不为空并且t.value 与目标q 不匹配。将t.value 添加到当前path 和新的子问题next_q;解决t.leftt.right 分支上的子问题-
    def find_sum (t, q, path = []):
      if not t:
        return                        # (1)
      elif t.value == q:
        yield [*path, t.value]        # (2)
      else:
        next_q = q - t.value          # (3)
        next_path = [*path, t.value]
        yield from find_sum(t.left, next_q, next_path)
        yield from find_sum(t.right, next_q, next_path)
    

    请注意我们如何不使用上面的.append 之类的突变。为了计算所有路径,我们可以写all_find_sum作为find_sum的扩展-

    def all_sum_path (t, q):
      return list(find_sum(t, q))
    

    就是这样,你的程序已经完成了:D


    如果您不想使用单独的生成器find_sum,我们可以将生成器扩展到位-

    def all_sum_path (t, q, path = []):
      if not t:
        return []
      elif t.value == q:
        return [[*path, t.value]]
      else:
        return \
          [ *all_sum_path(t.left, q - t.value, [*path, t.value])
          , *all_sum_path(t.right, q - t.value, [*path, t.value])
          ]
    

    请注意这两种变体之间的明显相似之处。任何编写良好的程序都可以轻松地在两种风格之间转换。

    【讨论】:

    • 感谢您的帮助!令人印象深刻的学习方式 1. 如何更有效地表达我的树。 2. yield 和 yield from 语句。 3. *。但我很新,还有关于 2 和 3 问题的其他问题。尤其是第3题。你介意解释一下吗?
    • 你介意看看我的另一个问题。我不知道为什么我可以在这里更新result,但我不能在那里更新_max。谢谢。
    • 为什么我们需要yield from 而不是yield from find_sum(t.left, next_q, next_path) 中的yield?我删除了from,结果完全不同。
    • 嗨强,yieldyield from 之间的区别很微妙。 yield 从生成器输出一个值,但 yield from(通常作为递归表达式)让给另一个迭代器!例如,yield [1,2,3] 将输出 [1,2,3]yield from [1,2,3]yield 1 yield 2 yield 3 相同。
    • 对于*,它扁平化了一层嵌套。例如,[1, *[2, 3], 4] 将返回 [1,2,3,4][*[1,2], *[3,4]] 将返回 [1,2,3,4]。您可以使用* 传播任何可迭代对象,例如[*"foo", *"bar"] 将返回["f","o","o","b","a","r"]。所有这些都可以写成没有 *[1] + [2,3] + [4] 返回[1,2,3,4][1,2]+[3,4] 返回[1,2,3,4]list(iter("foo")) + list(iter("bar")) 返回["f","o","o","b","a","r"]
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-08
    • 2011-05-24
    • 1970-01-01
    • 2023-04-03
    • 2011-05-31
    • 2018-12-28
    相关资源
    最近更新 更多