Contents
0 捋清概念
首先看一下二叉树的前中后序遍历是以怎么样的访问顺序规定的。规则是这样的:
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
「这里的前中后,其实指的就是中间节点的遍历顺序」。验证一下找到感觉没有:
![](https://i-blog.csdnimg.cn/blog_migrate/833dcf68c1f4c886174b2c1dc90d0896.png#pic_center)
对于上图,前中后序遍历顺序如下:
- 前序遍历(中左右):5 4 1 2 6 7 8
- 中序遍历(左中右):1 4 2 5 7 6 8
- 后序遍历(左右中):1 2 4 7 8 6 5
其次,搞清楚深度优先搜素(DFS)、广度优先搜索(BFS)与前中后序遍历及层次遍历之间的关系:
深度优先搜索指的是从树的根节点开始,沿向下路径一直走,走到叶子结点后向上返回;而广度优先搜索指的是从树的根节点开始,逐层遍历树,叶子结点所在的层是最后才会被访问到的。深度优先搜索又可分为前序遍历、中序遍历、后序遍历;广度优先搜索主要是对树的层次遍历。
深度优先搜索:
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
广度优先搜索:
- 层次遍历(迭代法)
树的前中后序遍历利用递归方法可以通过简单的改变左中右节点的递归顺序实现,故递归前中后序遍历的实现是有模板的;而其递归逻辑都是可以借助栈使用迭代的方式来实现,但是使用迭代方式时由于栈先进后出的特性,中序遍历与前序、后序遍历实现略有不同。
而广度优先遍历的实现一般使用队列来实现,这也是由队列先进先出的特点所决定的,因为只有先进先出的结构,才能一层一层的来遍历二叉树。
总结一句话,迭代方法时,层次遍历用队列,前中后序遍历用栈。
1 层次遍历
1. 1 二叉树的层次遍历(102)
题目:
给你一个二叉树,请你返回其按层序遍历得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
![](https://i-blog.csdnimg.cn/blog_migrate/ab73ea825e08ee285bbbbfd9a25c9f25.png#pic_center)
思路:
每次循环开始时获取队列que的长度,该长度即为遍历某一层时该层的节点数。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> res;
if(root != NULL) que.push(root);
while(!que.empty()){
int size = que.size();
vector<int> temp;
for(int i=0; i<size; i++){
TreeNode* node = que.front();
que.pop();
temp.push_back(node->val);
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
res.push_back(temp);
}
return res;
}
};
1.2 二叉树的层次遍历 II(107)
示例:
![](https://i-blog.csdnimg.cn/blog_migrate/3aea51eb90b2dd7fa2615211825253cf.png#pic_center)
思路:
简简单单,把上题的结果做一个翻转就好啦!C++标准库中reverse(res.begin(), res.end())
实现。
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> res;
if(root != NULL) que.push(root);
while(!que.empty()){
vector<int> temp;
int size = que.size();
for(int i=0; i<size; i++){
TreeNode * node = que.front();
que.pop();
temp.push_back(node->val);
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
res.push_back(temp);
}
reverse(res.begin(), res.end());
return res;
}
};
1.3.二叉树的层平均值(637)
题目:
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
示例:
![](https://i-blog.csdnimg.cn/blog_migrate/03e40da867c3dd5e857273dec3d0dbf9.png#pic_center)
思路:
简简单单,每一层的数求和除以个数就好啦!发现没有,只要通过模板轻轻松松获得树的每一层的节点,之后想干什么就得心应手了。
class Solution {
public:
vector<double> averageOfLevels(TreeNode* root) {
queue<TreeNode*> que;
if(root != NULL) que.push(root);
vector<double> res;
while(!que.empty()){
int size = que.size();
double sum = 0;
for(int i=0; i<size; i++){
TreeNode* temp= que.front();
que.pop();
sum += temp->val;
if(temp->left) que.push(temp->left);
if(temp->right) que.push(temp->right);
}
res.push_back(sum / size);
}
return res;
}
};
1.4.找树左下角的值(513)
题目:
给定一个二叉树,在树的最后一行找到最左边的值。
示例:
![](https://i-blog.csdnimg.cn/blog_migrate/7d796b96747365611d3e3f06c7277d26.png#pic_center)
思路:
我的思路是,首先利用递归获取树的最大深度(一行代码搞定),因为树最下角的值一定在树的最大深度那一层上,所以当遍历到的树的最后一层时,输出该层的第一个数即可,而每一层的树均保存在队列中,队列的先进先出性质使得输出队列的头元素即可。
class Solution {
public:
int dep(TreeNode* node){
if(node == NULL) return 0;
return max(dep(node->left), dep(node->right)) + 1;
}
int findBottomLeftValue(TreeNode* root) {
int depth = dep(root);
queue<TreeNode*> que;
if(root !=NULL) que.push(root);
for(int i=0; i<depth; i++){
int size = que.size();
for(int j=0; j<size; j++){
if(i == depth -1 && !que.empty())
return (que.front())->val;
TreeNode* node = que.front();
que.pop();
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
}
return 0;
}
};
2 前中后序遍历
Leetcode中以下三道题是关于树的前中后序遍历,且面试中常常出现。如下对比着来看这三种遍历。
- 二叉树的前序遍历(144)
- 二叉树的后序遍历(145)
- 二叉树的中序遍历(94)
2.1 递归模板实现
递归三部曲(每次使用递归考虑的三个点):
-
确定递归函数的参数和返回值
因为要打印出前序遍历节点的数值,所以参数里需要传入节点和一个vector用于存放节点的值,函数不需要有返回值,故返回类型为void; -
确定递归基
当节点为空时,直接return即可; -
确定单层递归的逻辑
前序遍历是中左右的循序,所以单层递归的逻辑是要先取中节点的值存入vector中,然后再递归左、右子节点即可。
使用递归方法前中后序遍历树,仅仅改变单层递归的逻辑顺序即可。以下为使用递归方法遍历树的模板。
class Solution {
public:
void handdle(TreeNode* node, vector<int>& res){ # 注意这里的形参res类型为引用哦!
if(node == NULL) return;
# 以下为单层递归的逻辑,只需根据前中后序遍历树的节点顺序即可。
res.push_back(node->val); # 中
handdle(node->left, res); # 左
handdle(node->right, res); # 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> out;
handdle(root, out);
return out;
}
};
2.2 非递归实现二叉树的前序遍历(144)
思路:
前序遍历是中左右,每次先处理的是中间节点,那么先将跟节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入右孩子,再加入左孩子呢?
因为这样出栈的时候才是中左右的顺序。
动画理解:(来自代码随想录)
![](https://i-blog.csdnimg.cn/blog_migrate/778474746339b505e5f697b3a8a6ae5a.gif#pic_center)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> stk;
if(root != NULL) stk.push(root);
while(!stk.empty()){
TreeNode* cur = stk.top();
stk.pop();
res.push_back(cur->val); # 中
if(cur->right) stk.push(cur->right); # 右
if(cur->left) stk.push(cur->left); # 左
}
return res;
}
};
2.3 非递归实现二叉树的后序遍历(145)
思路:
对于二叉树的前序遍历,其节点访问顺序为中左右;而对于二叉树的后序遍历,其节点访问顺序为左右中,可以将迭代实现前序遍历中的左右顺序颠倒,变成中右左,然后将结果翻转即可变为左右中,及得到二叉树的后序遍历结果。
![](https://i-blog.csdnimg.cn/blog_migrate/127dd3cda21ced2e822670c4ad63d37c.png#pic_center)
2.4 非递归实现二叉树的中序遍历(94)
思路:
咦,为什么后序遍历仅仅是改一下前序遍历的节点访问顺序就可以,而中序遍历不行呢?对于我们总说的三种顺序,在实现时总是先获得中间(根)节点的。首先前序遍历比较简单,是因为首先拿到中节点的值,把它存入容器中后就可以将它从栈中pop掉;而后序遍历利用了顺序的对称性,使得仅仅改变节点访问顺序而得以简单实现。对于中序遍历,首先拿到中节点的值,需要将该值压入栈中之后再去获得该值的左子节点,即首先拿到的值不可以被直接处理,故中序遍历比较麻烦一些。
动画理解:(来自代码随想录)
![](https://i-blog.csdnimg.cn/blog_migrate/c9d1f3c0a24f4541eed1d3d578b28d17.gif#pic_center)
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> stk;
TreeNode* cur = root;
while(cur != nullptr || !stk.empty()){
if(cur!=nullptr){
stk.push(cur);
cur = cur->left; # 左
}
else{
cur = stk.top();
stk.pop();
res.push_back(cur->val); # 中
cur = cur->right; # 右
}
}
return res;
}
};
3 总结
对于层次遍历的迭代实现,记得使用队列存放每一层的节点值,记得利用queue.size()获取每一层的节点个数;层次遍历没有递归实现;
对于前中后序遍历的递归实现,把一个模板背会就搞定了;
对于前中后序遍历的迭代实现,首先记得使用栈,其次前序遍历和后序遍历有相似性,而中序遍历需要不断寻找每个节点的左子节点,遇到空时返回当前栈顶元素,此时的栈顶元素即为我们所说的"中",返回"中"之后再去访问"右"即可。
欢迎关注【OAOA】