94.二叉树的中序遍历
1.题目介绍
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
2.题解
2.1 递归
首先我们需要了解什么是二叉树的中序遍历:按照访问左子树——根节点——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。递归终止的条件为碰到空节点。
代码
//
// Created by trmbh on 2023-10-26.
// 94.二叉树中序遍历
#include<iostream>
#include<vector>
#include<string>
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:
std::vector<int> inorderTraversal(TreeNode *root) {
using namespace std;
if (root == nullptr) return arr;
else {
inorderTraversal(root->left);
arr.push_back(root->val);
inorderTraversal(root->right);
}
return arr;
}
private:
std::vector<int> arr;
};
int main(){
Solution solution;
TreeNode n1(3);
TreeNode n2(2, &n1, nullptr);
TreeNode n(1, nullptr,&n2);
std::vector<int> arr = solution.inorderTraversal(&n);
for (int num:arr){
std::cout << num << ' ';
}
}
2.2迭代
思路
在递归中,我们其实隐式地使用了栈来保存前面节点的信息;所以这里如果我们要使用迭代的方法的话,就应该使用显式栈来保存节点信息,并在返回时提供节点信息。
这里大循环的终止条件是节点遍历完毕且栈空(若栈非空,代表还有前置节点需要回退)
代码
class Solution {
public:
std::vector<int> inorderTraversal(TreeNode *root) {
std::stack<TreeNode *> stk;
std::vector<int> arr;
while(!stk.empty() || root != nullptr) {
while (root != nullptr) {
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
arr.push_back(root->val);
root = root->right;
}
return arr;
}
};
复杂度分析
-
时间复杂度:\(O(n)\),其中\(n\)为二叉树节点的个数。
二叉树的遍历中每个节点会被访问一次且只会被访问一次。 -
空间复杂度:\(O(n)\)。
空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到\(O(n)\)的级别。
2.3 Morris 中序遍历
思路
Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 O(1)。
就像我们用栈存储了节点之间的前后关系, 这里巧妙地利用了\(predecessor\)右子树设置的方法为我们保存了返回的节点信息
Morris 遍历算法整体步骤如下:(假设当前遍历到的节点为\(x\) ) :
1.如果\(x\)无左孩子,先将\(x\)的值加入答案数组,再访问\(x\)的右孩子,即\(x=x.right\)。
2.如果\(x\)有左孩子,则找到\(x\)左子树上最右的节点(即左子树中序遍历的最后一个节点,\(x\)在中序遍历中的前驱节点),我们记为\(predecessor\)。
根据\(predecessor\)的右孩子是否为空,进行如下操作。
- 如果\(predecessor\)的右孩子为空,则将其右孩子指向\(x\) ,然后访问\(x\)的左孩子,即\(x=x.left\)。
- 如果\(predecessor\)的右孩子不为空,则此时其右孩子指向\(x\) ,说明我们已经遍历完\(x\)的左子树,
我们将\(predecessor\)的右孩子置空,将\(x\)的值加入答案数组,然后访问\(x\)的右孩子,即\(x=x.right\)。
3.重复上述操作,直至访问完整棵树。
其实整个过程我们就多做一步:假设当前遍历到的节点为 x,将 x 的左子树中最右边的节点的右孩子指向 x,
这样在左子树遍历完成后我们通过这个指向走回了 x,且能通过这个指向知晓我们已经遍历完成了左子树,而不用再通过栈来维护,省去了栈的空间复杂度。
代码
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode *predecessor = nullptr;
while (root != nullptr) {
// 如果左子树不为空,我们就要遍历左子树,寻找pre节点;
// 1.若pre节点右子树为空,说明左子树尚未遍历完,链接pre节点和当前root, root = root -> left 继续遍历左子树
// 2.若pre节点右子树不为空,说明这里的root是通过上一个pre节点的右子树返回回来的(左子树已经遍历完),故存入该root到答案数组, 断开pre节点链接,遍历右子树
if (root->left != nullptr) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor->right == nullptr) {
predecessor->right = root;
root = root->left;
}
// 说明左子树已经访问完了,我们需要断开链接, 继续访问右子树
else {
res.push_back(root->val);
predecessor->right = nullptr;
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子(左子树访问完毕,开始通过right返回前置节点,相当于出栈操作)
else {
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};
复杂度分析
-
时间复杂度:\(O(n)\),其中\(n\)为二叉树的节点个数。
Morris 遍历中每个节点会被访问两次,因此总时间复杂度为\(O(2n)=O(n)\)。 -
空间复杂度:\(O(1)\)。