【问题标题】:Use tail recursion to find maxDepth of Binary Tree使用尾递归找到二叉树的最大深度
【发布时间】:2019-09-08 07:58:03
【问题描述】:

我正在努力解决一个问题Maximum Depth of Binary Tree - LeetCode

这个问题在 leetcode 教程中作为尾递归的练习给出。 tail recursion - LeetCode

给定一棵二叉树,求其最大深度。

最大深度是从根节点到最远叶节点的最长路径上的节点数。

注意:叶子是没有子节点的节点。

示例:

给定二叉树[3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回其深度 = 3。

从层次定义看问题的标准解

class Solution:
    def maxDepth(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """ 
        if root is None: 
            return 0 
        else: 
            left_height = self.maxDepth(root.left) 
            right_height = self.maxDepth(root.right) 
            return max(left_height, right_height) + 1 

但是,它不是尾递归

尾递归是一种递归,其中递归调用是递归函数中的最后一条指令。并且函数中应该只有一个递归调用。

我阅读了所有其他提交和讨论,但没有找到尾递归解决方案。

如何使用尾递归解决问题?

【问题讨论】:

    标签: python recursion


    【解决方案1】:

    任何递归程序都可以成为堆栈安全的

    我写了很多关于递归的文章,当人们错误地陈述事实时我很难过。不,这依赖于 sys.setrecursionlimit() 这样的愚蠢技术。

    在 python 中调用函数会添加一个堆栈帧。所以我们不会写f(x)来调用函数,而是写call(f,x)。现在我们可以完全控制评估策略 -

    # btree.py
    
    def depth(t):
      if not t:
        return 0
      else:
        return call \
          ( lambda left_height, right_height: 1 + max(left_height, right_height)
          , call(depth, t.left)
          , call(depth, t.right)
          )
    

    实际上是完全相同的程序。那么call是什么?

    # tailrec.py
    
    class call:
      def __init__(self, f, *v):
        self.f = f
        self.v = v
    

    所以call 是一个具有两个属性的简单对象:调用函数f 和调用它的值v。这意味着 depth 返回一个 call 对象,而不是我们需要的数字。只需要再调整一次 -

    # btree.py
    
    from tailrec import loop, call
    
    def depth(t):
      def aux(t):                        # <- auxiliary wrapper
        if not t:
          return 0
        else:
          return call \
            ( lambda l, r: 1 + max(l, r)
            , call(aux, t.left)
            , call(aux, t.right)
            )
      return loop(aux(t))                # <- call loop on result of aux
    

    循环

    现在我们需要做的就是编写一个足够熟练的loop 来评估我们的call 表达式。这里的答案是我在this Q&A (JavaScript) 中写的评估器的直接翻译。我不会在这里重复我自己,所以如果你想了解它是如何工作的,我会在我们在那篇文章中构建 loop 时逐步解释它 -

    # tailrec.py
    
    from functools import reduce
    
    def loop(t, k = identity):
      def one(t, k):
        if isinstance(t, call):
          return call(many, t.v, lambda r: call(one, t.f(*r), k))
        else:
          return call(k, t)
      def many(ts, k):
        return call \
          ( reduce \
              ( lambda mr, e:
                  lambda k: call(mr, lambda r: call(one, e, lambda v: call(k, [*r, v])))
              , ts
              , lambda k: call(k, [])
              )
          , k
          )
      return run(one(t, k))
    

    注意到一个模式? loopdepth 一样递归,但我们在这里也使用 call 表达式进行递归。注意loop 如何将其输出发送到run,在那里发生了明确无误的迭代 -

    # tailrec.py
    
    def run(t):
      while isinstance(t, call):
        t = t.f(*t.v)
      return t
    

    检查你的工作

    from btree import node, depth
    
    #   3
    #  / \
    # 9  20
    #   /  \
    #  15   7
    
    t = node(3, node(9), node(20, node(15), node(7)))
    
    print(depth(t))
    
    3
    

    堆栈与堆

    您不再受 python 堆栈限制 ~1000 的限制。我们有效地劫持了 python 的评估策略并编写了我们自己的替代品loop。我们没有将函数调用帧扔到堆栈上,而是将它们换成堆上的延续。现在唯一的限制是您计算机的内存。

    【讨论】:

    • 即使多年后,我仍然觉得我必须回到那个问答环节。很高兴看到你还在。
    【解决方案2】:

    你不能。您可以轻松地看到,不可能同时消除所有 LHS 尾调用和 RHS 尾调用。您可以消除一个,但不能消除另一个。让我们谈谈那个。


    让我们坦率地说递归在 Python 中通常是一个坏主意。它没有针对递归解决方案进行优化,甚至没有实现微不足道的优化(如尾调用消除)。不要在这里这样做。

    但是,它可以是一种很好的语言来说明在其他语言中可能难以掌握的概念(即使这些可能更适合您正在寻找的解决方案),所以让我们深入研究。

    正如你所理解的:递归是一个调用自身的函数。虽然每个函数的逻辑可能会发生变化,但它们都有两个主要部分:

    1. 基本情况

    这是一个普通的情况,通常类似于return 1 或其他简并情况

    1. 递归案例

    这是函数决定它必须更深入并递归到自身的地方。

    对于尾递归,重要的部分是在递归的情况下,函数在递归之后不必做任何事情。更优化的语言可以推断出这一点,并在它递归到新调用时立即丢弃包含旧调用上下文的堆栈帧。这通常通过函数参数传递所需的上下文来完成。

    想象一下这样实现的求和函数

    def sum_iterative(some_iterable: List[int]) -> int:
        total = 0
        for num in some_iterable:
            total += num
        return total
    
    def sum_recursive(some_iterable: List[int]) -> int:
        """This is a wrapper function that implements sum recursively."""
    
        def go(total: int, iterable: List[int]) -> int:
            """This actually does the recursion."""
            if not iterable:  # BASE CASE if the iterable is empty
                return 0
            else:             # RECURSIVE CASE
                head = iterable.pop(0)
                return go(total+head, iterable)
    
        return go(0, some_iterable)
    

    您是否看到我必须定义一个辅助函数,该函数接受一些不是由用户自然传递的参数?这可以帮助你。

    def max_depth(root: Optional[TreeNode]) -> int:
        def go(maxdepth: int, curdepth: int, node: Optional[TreeNode]) -> int:
            if node is None:
                return maxdepth
            else:
                curdepth += 1
                lhs_max = go(max(maxdepth, curdepth), curdepth, node.left)
                # the above is the call that cannot be eliminated
                return go(max(lhs_max, curdepth), curdepth, node.right)
        return go(0, 0, root)
    

    为了好玩,这是 Haskell 中的一个非常丑陋的例子(因为我想复习一下我的函数式)

    data TreeNode a = TreeNode { val   :: a
                               , left  :: Maybe (TreeNode a)
                               , right :: Maybe (TreeNode a)
                               }
    treeDepth :: TreeNode a -> Int
    treeDepth = go 0 0 . Just
      where go :: Int -> Int -> (Maybe (TreeNode a)) -> Int
            go maxDepth _        Nothing     = maxDepth
            go maxDepth curDepth (Just node) = let curDepth' = curDepth + 1 :: Int
                                                   maxDepth' = max maxDepth curDepth' :: Int
                                                   lhsMax    = go maxDepth' curDepth' (left node)
                                               in  go lhsMax curDepth' (right node)
    
    root = TreeNode 3 (Just (TreeNode 9 Nothing Nothing)) (Just (TreeNode 20 (Just (TreeNode 15 Nothing Nothing)) (Just (TreeNode 7 Nothing Nothing)))) :: TreeNode Int
    
    main :: IO ()
    main = print $ treeDepth root
    

    【讨论】:

    • 我的原始代码在go 中有一个错字,将root.leftroot.right 传递给递归而不是node.leftnode.right,因此它无限递归。哎呀!
    【解决方案3】:

    可能有点晚了,但您可以传递子树列表并始终删除根元素。对于每个递归,您可以计算删除的数量。

    这里是 Haskell 中的一个实现

    data Tree a 
        = Leaf a
        | Node a (Tree a) (Tree a)
        deriving Show
    
    depth :: Tree a -> Integer
    depth tree = recursion 0 [tree]
        where 
            recursion :: Integer -> [Tree a] -> Integer
            recursion n [] = n
            recursion n treeList = recursion (n+1) (concatMap f treeList)
                where
                    f (Leaf _) = []
                    f (Node _ left right) = [left, right]
    
    root = Node 1 (Node 2 (Leaf 3) (Leaf 3)) (Leaf 7)
    
    main :: IO ()
    main = print $ depth root
    

    【讨论】:

      【解决方案4】:

      每个递归算法都可以变成尾递归算法。有时这并不简单,您需要使用稍微不同的方法。

      如果使用尾递归算法来确定二叉树的深度,您可以通过将要访问的子树列表与深度信息一起累积来遍历树。因此,您的列表将是一个元组列表(depth: Int, node: tree),而您的第二个累加器将记录最大深度。

      这里是算法的概要

      • 从列表toVisit 开始,其中包含一个元组(1, rootNode)maxDepth 设置为0
      1. 如果toVisit列表为空,返回maxValue
      2. 从列表中弹出头像
      3. 如果头部是EmptyTree,继续尾部,maxValue 保持不变
      4. 如果头部是Node,则更新toVisit,将左右子树添加到其尾部,增加元组中的深度并检查弹出头部的深度是否大于存储在@987654331中的深度@累加器

      这是一个 Scala 实现

      abstract class Tree[+A] {
        def head: A
        def left: Tree[A]
        def right: Tree[A]
        def depth: Int
        ...
      }
      case object EmptyTree extends Tree[Nothing] {...}
      
      case class Node[+A](h: A, l: Tree[A], r: Tree[A]) extends Tree[A] {
      
        override def depth: Int = {
      
          @tailrec
          def depthAux(toVisit: List[(Int, Tree[A])], maxDepth: Int): Int = toVisit match {
            case Nil => maxDepth
            case head :: tail => {
              val depth = head._1
              val node = head._2
              if (node.isEmpty) depthAux(tail, maxDepth)
              else depthAux(toVisit = tail ++ List((depth + 1, node.left), (depth + 1, node.right)),
                            maxDepth = if (depth > maxDepth) depth else maxDepth)
            }
          }
      
          depthAux(List((1, this)), 0)
        }
       ...
      }
      
      
      

      对于那些对 Haskell 更感兴趣的人

      data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show)
      
      depthAux :: [(Int, Tree a)] -> Int -> Int
      depthAux [] maxDepth = maxDepth
      depthAux ((depth, Empty):xs) maxDepth = depthAux xs maxDepth
      depthAux ((depth, (Node h l r)):xs) maxDepth = 
          depthAux (xs ++ [(depth + 1, l), (depth + 1, r)]) (max depth maxDepth) 
      
      depth :: Tree a -> Int
      depth node = depthAux [(1, node)] 0
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-11-22
        • 2016-01-24
        • 1970-01-01
        • 2015-04-08
        • 2015-02-12
        • 1970-01-01
        • 2016-06-14
        • 1970-01-01
        相关资源
        最近更新 更多