LeetCode第94题:二叉树的中序遍历
题目描述
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
难度
简单
问题链接
示例
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示
- 树中节点数目在范围
[0, 100]内 -100 <= Node.val <= 100
解题思路
二叉树的中序遍历是指按照访问左子树-根节点-右子树的方式遍历二叉树。这道题可以使用递归或迭代的方式来解决。
方法一:递归法
递归法是最直观的解法,我们可以按照中序遍历的定义来实现:
- 递归遍历左子树
- 访问根节点
- 递归遍历右子树
方法二:迭代法(使用栈)
递归实现的本质是使用了系统的调用栈,我们也可以使用显式的栈来模拟递归过程:
- 创建一个空栈和一个结果列表
- 从根节点开始,一直向左遍历,将所有左子节点入栈
- 当无法继续向左时,弹出栈顶节点,将其值加入结果列表
- 然后转向该节点的右子树,重复步骤2和3
- 直到栈为空且当前节点为空
方法三:Morris 遍历
Morris 遍历是一种不使用栈也不使用递归的遍历方法,它通过修改树的结构(临时)来实现 O(1) 的空间复杂度:
- 初始化当前节点为根节点
- 如果当前节点的左子树为空,将当前节点的值加入结果列表,并将当前节点更新为其右子节点
- 如果当前节点的左子树不为空,找到当前节点左子树的最右节点(该节点的右子节点为空或指向当前节点)
- 如果最右节点的右子节点为空,将其右子节点指向当前节点,然后将当前节点更新为其左子节点
- 如果最右节点的右子节点指向当前节点,将其右子节点重新置为空,将当前节点的值加入结果列表,然后将当前节点更新为其右子节点
- 重复步骤2-5,直到当前节点为空
关键点
- 理解中序遍历的顺序:左子树-根节点-右子树
- 递归法简单直观,但在树很深时可能导致栈溢出
- 迭代法使用显式的栈,避免了递归可能导致的栈溢出问题
- Morris 遍历可以实现 O(1) 的空间复杂度,但会临时修改树的结构
算法步骤分析
递归法算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 定义递归函数 | 函数接收当前节点和结果列表作为参数 |
| 2 | 处理基本情况 | 如果当前节点为空,直接返回 |
| 3 | 递归左子树 | 对当前节点的左子树进行递归 |
| 4 | 访问根节点 | 将当前节点的值加入结果列表 |
| 5 | 递归右子树 | 对当前节点的右子树进行递归 |
| 6 | 返回结果 | 返回结果列表 |
迭代法算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 创建一个空栈和一个结果列表,初始化当前节点为根节点 |
| 2 | 向左遍历 | 当前节点不为空时,将其入栈,然后更新当前节点为其左子节点 |
| 3 | 处理栈顶 | 当前节点为空且栈不为空时,弹出栈顶节点,将其值加入结果列表 |
| 4 | 转向右子树 | 将当前节点更新为弹出节点的右子节点 |
| 5 | 重复步骤2-4 | 直到栈为空且当前节点为空 |
| 6 | 返回结果 | 返回结果列表 |
Morris 遍历算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 创建一个结果列表,初始化当前节点为根节点 |
| 2 | 遍历树 | 当前节点不为空时,执行步骤3-5 |
| 3 | 左子树为空 | 如果当前节点的左子树为空,将其值加入结果列表,然后更新当前节点为其右子节点 |
| 4 | 左子树不为空 | 找到当前节点左子树的最右节点 |
| 5 | 建立或断开线索 | 根据最右节点的右子节点是否指向当前节点,执行相应操作 |
| 6 | 返回结果 | 返回结果列表 |
算法可视化
以示例 root = [1,null,2,3] 为例,使用递归法进行中序遍历:
1
\
2
/
3
- 从根节点 1 开始,递归遍历其左子树(为空)
- 访问根节点 1,将 1 加入结果列表:
[1] - 递归遍历其右子树(节点 2)
- 从节点 2 开始,递归遍历其左子树(节点 3)
- 从节点 3 开始,递归遍历其左子树(为空)
- 访问节点 3,将 3 加入结果列表:
[1,3] - 递归遍历其右子树(为空)
- 访问节点 2,将 2 加入结果列表:
[1,3,2] - 递归遍历其右子树(为空)
- 从节点 2 开始,递归遍历其左子树(节点 3)
- 返回结果列表
[1,3,2]
代码实现
C# 实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
// 方法一:递归法
public IList<int> InorderTraversal(TreeNode root) {
List<int> result = new List<int>();
InorderRecursive(root, result);
return result;
}
private void InorderRecursive(TreeNode node, List<int> result) {
if (node == null) {
return;
}
// 递归遍历左子树
InorderRecursive(node.left, result);
// 访问根节点
result.Add(node.val);
// 递归遍历右子树
InorderRecursive(node.right, result);
}
// 方法二:迭代法
public IList<int> InorderTraversalIterative(TreeNode root) {
List<int> result = new List<int>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode current = root;
while (current != null || stack.Count > 0) {
// 一直向左遍历,将所有左子节点入栈
while (current != null) {
stack.Push(current);
current = current.left;
}
// 弹出栈顶节点,访问它
current = stack.Pop();
result.Add(current.val);
// 转向右子树
current = current.right;
}
return result;
}
// 方法三:Morris 遍历
public IList<int> InorderTraversalMorris(TreeNode root) {
List<int> result = new List<int>();
TreeNode current = root;
while (current != null) {
if (current.left == null) {
// 如果左子树为空,访问当前节点,然后转向右子树
result.Add(current.val);
current = current.right;
} else {
// 找到当前节点左子树的最右节点
TreeNode predecessor = current.left;
while (predecessor.right != null && predecessor.right != current) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {
// 建立线索:将最右节点的右子节点指向当前节点
predecessor.right = current;
current = current.left;
} else {
// 断开线索:将最右节点的右子节点重新置为空
predecessor.right = null;
result.Add(current.val);
current = current.right;
}
}
}
return result;
}
}
Python 实现
# 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: TreeNode) -> List[int]:
result = []
self.inorder_recursive(root, result)
return result
def inorder_recursive(self, node, result):
if not node:
return
# 递归遍历左子树
self.inorder_recursive(node.left, result)
# 访问根节点
result.append(node.val)
# 递归遍历右子树
self.inorder_recursive(node.right, result)
# 方法二:迭代法
def inorderTraversalIterative(self, root: TreeNode) -> List[int]:
result = []
stack = []
current = root
while current or stack:
# 一直向左遍历,将所有左子节点入栈
while current:
stack.append(current)
current = current.left
# 弹出栈顶节点,访问它
current = stack.pop()
result.append(current.val)
# 转向右子树
current = current.right
return result
# 方法三:Morris 遍历
def inorderTraversalMorris(self, root: TreeNode) -> List[int]:
result = []
current = root
while current:
if not current.left:
# 如果左子树为空,访问当前节点,然后转向右子树
result.append(current.val)
current = current.right
else:
# 找到当前节点左子树的最右节点
predecessor = current.left
while predecessor.right and predecessor.right != current:
predecessor = predecessor.right
if not predecessor.right:
# 建立线索:将最右节点的右子节点指向当前节点
predecessor.right = current
current = current.left
else:
# 断开线索:将最右节点的右子节点重新置为空
predecessor.right = None
result.append(current.val)
current = current.right
return result
C++ 实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 方法一:递归法
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
inorderRecursive(root, result);
return result;
}
void inorderRecursive(TreeNode* node, vector<int>& result) {
if (node == nullptr) {
return;
}
// 递归遍历左子树
inorderRecursive(node->left, result);
// 访问根节点
result.push_back(node->val);
// 递归遍历右子树
inorderRecursive(node->right, result);
}
// 方法二:迭代法
vector<int> inorderTraversalIterative(TreeNode* root) {
vector<int> result;
stack<TreeNode*> stk;
TreeNode* current = root;
while (current != nullptr || !stk.empty()) {
// 一直向左遍历,将所有左子节点入栈
while (current != nullptr) {
stk.push(current);
current = current->left;
}
// 弹出栈顶节点,访问它
current = stk.top();
stk.pop();
result.push_back(current->val);
// 转向右子树
current = current->right;
}
return result;
}
// 方法三:Morris 遍历
vector<int> inorderTraversalMorris(TreeNode* root) {
vector<int> result;
TreeNode* current = root;
while (current != nullptr) {
if (current->left == nullptr) {
// 如果左子树为空,访问当前节点,然后转向右子树
result.push_back(current->val);
current = current->right;
} else {
// 找到当前节点左子树的最右节点
TreeNode* predecessor = current->left;
while (predecessor->right != nullptr && predecessor->right != current) {
predecessor = predecessor->right;
}
if (predecessor->right == nullptr) {
// 建立线索:将最右节点的右子节点指向当前节点
predecessor->right = current;
current = current->left;
} else {
// 断开线索:将最右节点的右子节点重新置为空
predecessor->right = nullptr;
result.push_back(current->val);
current = current->right;
}
}
}
return result;
}
};
执行结果
C# 执行结果
- 执行用时:84 ms,击败了 93.33% 的 C# 提交
- 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交
Python 执行结果
- 执行用时:32 ms,击败了 95.24% 的 Python3 提交
- 内存消耗:15.1 MB,击败了 92.86% 的 Python3 提交
C++ 执行结果
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:8.3 MB,击败了 94.74% 的 C++ 提交
代码亮点
- 多种实现方式:提供了递归、迭代和 Morris 三种实现方式,适应不同的需求和场景。
- 空间优化:Morris 遍历实现了 O(1) 的空间复杂度,适用于内存受限的情况。
- 代码结构清晰:各种实现方式的代码结构清晰,易于理解和维护。
- 边界条件处理:代码中详细处理了各种边界情况,如空树和单节点树。
- 注释完善:代码中的注释详细解释了各个步骤的作用,便于理解算法流程。
常见错误分析
- 遍历顺序错误:中序遍历的顺序是左子树-根节点-右子树,容易与前序遍历(根节点-左子树-右子树)和后序遍历(左子树-右子树-根节点)混淆。
- 栈操作错误:在迭代实现中,栈的操作顺序很重要,错误的操作可能导致遍历顺序错误。
- 递归终止条件:在递归实现中,忘记设置终止条件可能导致无限递归和栈溢出。
- Morris 遍历中的线索处理:在 Morris 遍历中,建立和断开线索的操作容易出错,需要特别注意。
- 空指针异常:在处理树节点时,需要检查节点是否为空,避免空指针异常。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归法 | O(n) | O(h),h 为树的高度 | 实现简单,代码简洁 | 在树很深时可能导致栈溢出 |
| 迭代法 | O(n) | O(h),h 为树的高度 | 避免了递归可能导致的栈溢出 | 实现稍复杂,需要显式管理栈 |
| Morris 遍历 | O(n) | O(1) | 空间复杂度为 O(1),适用于内存受限的情况 | 实现复杂,会临时修改树的结构 |