【小白向】leetcode中序遍历二叉树的三种实现方式

声明:以下代码均为leetcode讨论区大佬的代码,非本人所写(时间久远,忘了具体来源,知道的朋友可以评论区提醒,我会补充来源信息),本文仅对这三种解法进行分析,与各位交流学习。
三段代码都实现了中序遍历二叉树。

代码 1:递归遍历 + 闭包函数

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(root):
            if root is None:
                return
            dfs(root.left)
            nonlocal ans
            ans.append(root.val)
            dfs(root.right)

        ans = []
        dfs(root)
        return ans

详细讲解

  1. TreeNode 类定义

    • TreeNode 是树节点类,使用 __init__ 构造函数定义了节点的 valleftright 三个属性。
  2. Solution 类与方法定义

    • class Solution: 定义了一个 Solution 类,其中包含 inorderTraversal 方法,这是实现中序遍历的主要入口。
    • def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]: 通过类型注解,指定参数 root 是一个 Optional[TreeNode](即可以是 TreeNode 类型,也可以是 None),返回值是一个 List[int]
  3. 嵌套函数 dfs 定义

    • def dfs(root) 是一个嵌套函数,用于实现深度优先搜索(Depth-First Search, DFS)。嵌套函数的好处是它可以方便地访问外层函数中的变量。
    • if root is None: return:递归的终止条件,节点为 None 时直接返回。
    • dfs(root.left):递归遍历左子树。
    • nonlocal ans:关键字 nonlocal 用于声明我们需要访问外部函数中的 ans 列表,而不是在 dfs 中创建一个新的 ansans 在外层函数中定义,需要在内层函数中修改它。
    • ans.append(root.val):将当前节点的值添加到结果列表中。
    • dfs(root.right):递归遍历右子树。
  4. 主逻辑

    • ans = []:定义空列表来保存结果。
    • dfs(root):调用递归函数进行中序遍历。
    • return ans:返回结果列表。

总结

  • 代码使用深度优先的递归方式进行中序遍历,递归调用过程中需要借助 nonlocal 来修改外部变量。
  • 本质是通过递归调用函数实现从左到右遍历每个节点。

代码 2:显式栈模拟递归(颜色标记法)

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        WHITE, GRAY = 0, 1
        res = []
        stack = [(WHITE, root)]
        while stack:
            color, node = stack.pop()
            if node is None:
                continue
            if color == WHITE:
                stack.append((WHITE, node.right))  # 右子节点,先压栈后出栈
                stack.append((GRAY, node))          # 当前节点,稍后处理
                stack.append((WHITE, node.left))    # 左子节点,先压栈后出栈
            else:
                res.append(node.val)
        return res

详细讲解

  1. 颜色标记法的思想

    • 该方法模拟递归,用显式栈来实现中序遍历。
    • 使用颜色标记法,将节点分为两类:
      • WHITE(值为 0):表示节点未访问过。
      • GRAY(值为 1):表示节点已经访问过,需要将节点的值记录到结果中。
  2. 初始化颜色与栈

    • WHITE, GRAY = 0, 1:定义两个常量表示节点状态。
    • res = []:用于存储结果的列表。
    • stack = [(WHITE, root)]:初始化栈,将根节点与 WHITE 颜色一起放入栈中。
  3. while 循环遍历栈

    • while stack: 当栈不为空时循环。
    • color, node = stack.pop():弹出栈顶元素,获取颜色和节点。
    • if node is None: continue:如果节点为空,跳过此次循环。
    • if color == WHITE: 如果节点是 WHITE,表示尚未访问过:
      • 先压入右子节点 (WHITE, node.right)
      • 再压入当前节点(标记为 GRAY),表示稍后要访问它。
      • 最后压入左子节点 (WHITE, node.left)
    • else: 如果节点是 GRAY,表示已经访问过,直接将 node.val 添加到结果中。
  4. 返回结果

    • return res:返回最终的中序遍历结果。

总结

  • 这种方法显式地使用栈来模拟递归。
  • 使用颜色标记法有效区分了节点的处理阶段,使得中序遍历的逻辑更易于在迭代中实现。

代码 3:递归调用 + 辅助函数

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        result = []
        self.helper(root, result)
        return result

    def helper(self, node: Optional[TreeNode], result: List[int]) -> None:
        if node is None:
            return
        self.helper(node.left, result)
        result.append(node.val)
        self.helper(node.right, result)

详细讲解

  1. 递归实现

    • Solution 类中定义了两个方法:inorderTraversalhelper
  2. inorderTraversal 方法

    • result = []:初始化空列表 result 来存储中序遍历结果。
    • self.helper(root, result):调用辅助函数 helper,传入根节点和结果列表。
  3. 辅助函数 helper 的定义

    • def helper(self, node: Optional[TreeNode], result: List[int]) -> None: 定义辅助函数,递归遍历树节点,并将值存入结果列表。
    • if node is None: return:递归终止条件,当节点为空时返回。
    • self.helper(node.left, result):递归遍历左子树。
    • result.append(node.val):将当前节点的值添加到结果列表。
    • self.helper(node.right, result):递归遍历右子树。
  4. 返回结果

    • return result:返回最终的中序遍历结果。

总结

  • 这种方法将递归逻辑封装到一个辅助函数中,使得代码结构清晰。
  • 通过传递结果列表 result,确保每次递归调用都能正确地记录遍历的结果。

三种代码的对比与总结

  1. 代码 1

    • 使用嵌套函数 dfs,通过 nonlocal 关键字来修改外部变量 ans
    • 代码简洁,递归实现中序遍历,但是需要注意使用 nonlocal 来访问外部变量。
  2. 代码 2

    • 使用显式栈和颜色标记法模拟递归过程,是一种迭代方法。
    • 不使用系统的递归调用栈,避免了深度过大时的栈溢出。
    • 使用 WHITEGRAY 颜色来区分节点的访问状态,逻辑清晰且有利于理解节点在遍历过程中的不同状态。
  3. 代码 3

    • 使用辅助函数递归实现中序遍历,递归逻辑被封装在 helper 函数中,结构分离且易于理解。
    • 没有使用闭包函数,递归调用的逻辑集中在 helper 函数中。

选择合适的方法

  • 对于树的遍历,如果树的深度较小,使用代码 1 或代码 3 的递归实现通常更为简洁和直观。
  • 若担心递归深度过大导致栈溢出,代码 2 的显式栈模拟是更安全的选择。
  • 代码 2 更适合在不允许使用递归时实现二叉树遍历,迭代逻辑也使得它可以更灵活地控制遍历的过程。

Python 的栈容量多大?

Python 的栈容量受限于 Python 解释器和操作系统的栈深度限制,一般来说与递归调用深度密切相关。通常,Python 的递归调用深度最大限制默认是 1000,可以通过 sys.getrecursionlimit() 查看这一限制值,也可以通过 sys.setrecursionlimit() 来设置新的限制值。但是需要注意,过高的栈深度可能会导致程序栈溢出,造成内存崩溃或其他问题。

在实际应用中,如果递归的深度可能较大,建议考虑将递归实现改为迭代方式,以显式地使用栈来避免超出 Python 的最大递归限制。

为什么显式地使用栈能避免 Python 的最大递归限制?

显式地使用栈可以避免 Python 的最大递归限制,主要是因为递归调用会占用 Python 解释器的调用栈,而调用栈的大小在 Python 中是有限制的(通常为 1000 层)。每次递归调用都会在调用栈中增加一个新的帧,这样当递归深度过大时就会超出栈的容量,导致 RecursionError

显式使用栈(例如代码 2 中的显式栈模拟递归)是通过将递归逻辑改写为迭代逻辑,把原本由系统调用栈管理的递归状态手动管理起来。通过这种方式,节点的访问状态和遍历过程都是用用户自己管理的栈来实现,而不是依赖 Python 的调用栈。这样可以有效地避免系统递归深度的限制,同时在深度较大的情况下也可以控制栈的增长,并且避免了递归带来的栈溢出风险。

简单地说,显式栈模拟递归不依赖系统栈,而是通过自己的数据结构来保存状态,因此能够避免 Python 递归深度的限制。

为什么代码②中 node is None 使用的是 continue,而代码①和代码③中是 return

在代码②中,使用 if node is None: continue,而在代码①和代码③中使用 if node is None: return,这是因为这三种实现方式的逻辑结构有所不同:

  • 代码② (continue)

    • 代码②的遍历过程通过显式栈来模拟递归,所有节点的处理都在 while stack: 循环内完成。当 node is None 时,意味着栈中保存的节点是空的,我们不需要对它做进一步处理。因此使用 continue 跳过当前循环,继续处理下一个栈中的节点。
    • 使用 continue 是为了跳过处理当前节点,并接着处理下一个节点,因为我们需要不断从栈中取出元素直到栈为空。
  • 代码①和代码③ (return)

    • 在代码①和代码③中,递归的实现需要明确的递归终止条件。当 node is None,递归终止条件成立,表示当前路径到达了叶节点的末端,不再有其他节点可以访问,因此使用 return 结束当前递归分支的调用。
    • 使用 return 表示我们已经完成了当前路径的处理,递归直接返回控制权给上层调用。

总结来说,continuereturn 的作用在不同场景中:continue 适用于迭代结构,用来跳过某些条件;而 return 用于结束当前的递归分支。在代码②中,使用迭代方式显式处理栈,而在代码①和代码③中,递归调用逻辑需要用 return 来控制递归深度的退出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值