声明:以下代码均为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
详细讲解
-
TreeNode 类定义:
TreeNode
是树节点类,使用__init__
构造函数定义了节点的val
、left
、right
三个属性。
-
Solution 类与方法定义:
class Solution:
定义了一个Solution
类,其中包含inorderTraversal
方法,这是实现中序遍历的主要入口。def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
通过类型注解,指定参数root
是一个Optional[TreeNode]
(即可以是TreeNode
类型,也可以是None
),返回值是一个List[int]
。
-
嵌套函数
dfs
定义:def dfs(root)
是一个嵌套函数,用于实现深度优先搜索(Depth-First Search, DFS)。嵌套函数的好处是它可以方便地访问外层函数中的变量。if root is None: return
:递归的终止条件,节点为None
时直接返回。dfs(root.left)
:递归遍历左子树。nonlocal ans
:关键字nonlocal
用于声明我们需要访问外部函数中的ans
列表,而不是在dfs
中创建一个新的ans
。ans
在外层函数中定义,需要在内层函数中修改它。ans.append(root.val)
:将当前节点的值添加到结果列表中。dfs(root.right)
:递归遍历右子树。
-
主逻辑:
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
详细讲解
-
颜色标记法的思想:
- 该方法模拟递归,用显式栈来实现中序遍历。
- 使用颜色标记法,将节点分为两类:
WHITE
(值为0
):表示节点未访问过。GRAY
(值为1
):表示节点已经访问过,需要将节点的值记录到结果中。
-
初始化颜色与栈:
WHITE, GRAY = 0, 1
:定义两个常量表示节点状态。res = []
:用于存储结果的列表。stack = [(WHITE, root)]
:初始化栈,将根节点与WHITE
颜色一起放入栈中。
-
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
添加到结果中。
-
返回结果:
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)
详细讲解
-
递归实现:
- 在
Solution
类中定义了两个方法:inorderTraversal
和helper
。
- 在
-
inorderTraversal
方法:result = []
:初始化空列表result
来存储中序遍历结果。self.helper(root, result)
:调用辅助函数helper
,传入根节点和结果列表。
-
辅助函数
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)
:递归遍历右子树。
-
返回结果:
return result
:返回最终的中序遍历结果。
总结
- 这种方法将递归逻辑封装到一个辅助函数中,使得代码结构清晰。
- 通过传递结果列表
result
,确保每次递归调用都能正确地记录遍历的结果。
三种代码的对比与总结
-
代码 1:
- 使用嵌套函数
dfs
,通过nonlocal
关键字来修改外部变量ans
。 - 代码简洁,递归实现中序遍历,但是需要注意使用
nonlocal
来访问外部变量。
- 使用嵌套函数
-
代码 2:
- 使用显式栈和颜色标记法模拟递归过程,是一种迭代方法。
- 不使用系统的递归调用栈,避免了深度过大时的栈溢出。
- 使用
WHITE
和GRAY
颜色来区分节点的访问状态,逻辑清晰且有利于理解节点在遍历过程中的不同状态。
-
代码 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
表示我们已经完成了当前路径的处理,递归直接返回控制权给上层调用。
- 在代码①和代码③中,递归的实现需要明确的递归终止条件。当
总结来说,continue
和 return
的作用在不同场景中:continue
适用于迭代结构,用来跳过某些条件;而 return
用于结束当前的递归分支。在代码②中,使用迭代方式显式处理栈,而在代码①和代码③中,递归调用逻辑需要用 return
来控制递归深度的退出。