使用尾递归找到二叉树的maxDepth

时间:2019-04-18 02:17:36

标签: python recursion

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

在leetcodes教程中,这些问题被安排为尾递归的一部分。 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 

但是,这不是尾巴递归

  

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

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

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

4 个答案:

答案 0 :(得分: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

答案 1 :(得分: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))

注意到一个模式? loop 的递归方式与 depth 相同,但我们在这里也使用 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 :(得分:0)

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

在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

答案 3 :(得分:0)

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

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

这里是算法的大纲

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

这是一个 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