0.碎碎念
什么序遍历就是“中”在什么地方。前序遍历就是“中左右”,中序遍历就是“左中右”。
为了便于理解所谓的“序”,你可以这样理解:
以前序(中左右为例子)
首先从 1 开始(中),遍历到 2 (左),这时相当于原图变成了:
处理完这“左大树”,才会去处理“右大树”
同时,左大树可以再分为“左中树”->"左小树"(如果树够复杂),直到分成了 这样的无孩子节点,就可以结束一个“左小树”的遍历了。
聪明的你或许会发现,这和递归很像?的确,这种一层套一层无疑是递归的主场,但利用栈的话用迭代也能实现,但这是提升项了,最好也能掌握,这能拓展你的思维。
0.1关于节点的构造
这里都用的是Leetcode的构造方式:
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){}
};
后面三行是构造函数,我后面会单独出一篇文章讲讲构造函数(望捧场!)
1.递归大类
递归类遍历是二叉树遍历中最简单易懂最容易实现的,那就不需要太多废话是吧()
1.1.前序遍历
中左右决定了每一次访问新树(左右孩子)都要先把当前节点输出,那代码非常好写:
class Solution {
public:
vector<int> ans;
void travel(TreeNode* x){
if (x == nullptr)
return; // 如果当前节点为空,直接返回
ans.push_back(x->val); // 将当前节点的值加入到结果列表中
travel(x->left); // 递归地遍历左子节点
travel(x->right); // 递归地遍历右子节点
}
vector<int> preorderTraversal(TreeNode* root) {
travel(root);
return ans;
}
};
其中,末尾的节点可以看成一个左右孩子都是空节点的树,所以在程序看来树是长这样:
访问到了空节点就代表到底了,该输出了。这能帮助你理解后面的迭代法。
1.2.中序遍历
同理可得:
class Solution {
public:
vector<int> ans;
void travel(TreeNode* x){
if(x == nullptr)
return;
travel(x->left);
ans.push_back(x->val);
travel(x->right);
}
vector<int> inorderTraversal(TreeNode* root) {
travel(root);
return ans;
}
};
1.3.后序遍历
class Solution {
public:
vector<int> ans;
void travel(TreeNode* x){
if(x == nullptr)
return;
travel(x->left);
travel(x->right);
ans.push_back(x->val);
}
vector<int> postorderTraversal(TreeNode* root) {
travel(root);
return ans;
}
};
1.4.总结
可以看出,递归法非常的简单粗暴且明了,前中后序只需要更改几行代码便可实现,这是因为我们可以让左子树,中节点,右子树来按照我们想要的顺序来处理,且互不干扰。
2.迭代大类
2.1.局限
如果是前序遍历的话,我们可以用如下代码简单实现:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> travel;
vector<int> ans;
travel.push(root);
while(!(travel.empty())){
TreeNode* temp = travel.top();
travel.pop();
if(temp == nullptr)
continue;
travel.push(temp->right);
travel.push(temp->left);
ans.push_back(temp->val);
}
return ans;
}
};
在每次处理一个新元素时,我们都会先输出这个元素,因为是前序遍历(中左右),不管有没有左右子树都会先输出处理元素。
那中序,后序呢?
以中序为例,是左中右,你的操作元素(中)是要在左子树全部操作完再输出的!
这就代表你无法像类似前序这样只用栈来进行中序遍历,因为你不知道该什么时候输出中节点!
那解决方法也很简单 ------------------------标记中节点!
2.2中节点标记法
我们知道了,之所以无法用栈来处理中后序的原因是不知道什么时候输出中结点,那么我们只要找到一种方法标记中节点即可。
这里我推荐在每次处理中节点后压入一个空节点
为什么这样会比较好呢?
遍历二叉树我们会输出答案的标志是什么?必然是遍历到尾了----访问到了空节点,所以我们在中节点后压入一个空节点,就能让我们在左子树全部处理完后正确的输出空节点(中序),而且不用单独写一个判断。
当然,你闲的没事也可以压入一个值为114514的节点,只要能起到标记作用就好。
那这样的话,代码就很简单了:
2.2.1标记法前序
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> a;
vector<int> ans;
a.push(root);
if(!root)
return ans;
while(a.empty() == false){
TreeNode* cur = a.top();
a.pop();
if(cur == nullptr){
cur = a.top();
a.pop();
ans.push_back(cur->val);
}else{
if(cur->right)a.push(cur->right);
if(cur->left)a.push(cur->left);
a.push(cur);
a.push(nullptr);
}
}
return ans;
}
};
2.2.2标记法中序
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> a;
vector<int> ans;
a.push(root);
if(!root)
return ans;
while(a.empty() == false){
TreeNode* cur = a.top();
a.pop();
if(cur == nullptr){
cur = a.top();
a.pop();
ans.push_back(cur->val);
}else{
if(cur->right)a.push(cur->right);
a.push(cur);
a.push(nullptr);
if(cur->left)a.push(cur->left);
}
}
return ans;
}
};
2.2.3标记法后序
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> a;
vector<int> ans;
a.push(root);
if(!root)
return ans;
while(a.empty() == false){
TreeNode* cur = a.top();
a.pop();
if(cur == nullptr){
cur = a.top();
a.pop();
ans.push_back(cur->val);
}else{
a.push(cur);
a.push(nullptr);
if(cur->right)a.push(cur->right);
if(cur->left)a.push(cur->left);
}
}
return ans;
}
};
至于为什么前序(中左右)的代码中节点是最后压入的,因为栈是先进后出,后压入才能做到第一个操作的是中节点!
3.总结
总体上二叉树遍历就这几种,但是还有一种O(1)的逆天遍历:莫里斯(Morris)遍历
大家可以自行去了解。总体来说,个人觉得递归法是最优解,但指不定哪个HR就问你迭代呢()
有不足之处还望多多指正!
题目链接: