文章目录
前置知识
二叉树的类型
这里介绍几个比较典型的二叉树
满二叉树
如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
满二叉树,也可以说深度为k
,有2^k-1
个节点的二叉树。
完全二叉树
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
二叉搜索树
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树
平衡二叉搜索树
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树
二叉树的遍历
深度优先遍历
前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法)
广度优先遍历
层次遍历(迭代法)
二叉树的定义
struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
// 如果需要还可以写其他的初始化方法
};
二叉树遍历的LeetCode题目
144. 二叉树的前序遍历
94. 二叉树的中序遍历
145. 二叉树的后序遍历
二叉树的递归遍历
思路
使用递归时需要注意三点:
- 确定递归函数的参数和返回值(比如下面题目中就要记得在
vector<int>
后加&
); - 确定终止条件(如遇
NULL
就return
); - 确定单层递归内部的顺序和逻辑(不同顺序的遍历需要先后遍历
cur->left
,cur->right
,cur
).
前序遍历
class Solution {
public:
void helper(TreeNode* cur, vector<int>& ans){// 一定要注意这里的&
if(cur==nullptr)
return;
ans.push_back(cur->val);//中
helper(cur->left, ans);//左
helper(cur->right, ans);//右
return;
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
helper(root, ans);
return ans;
}
};
中序遍历
class Solution {
public:
void helper(TreeNode* cur, vector<int>& ans){// 一定要注意这里的&
if(cur==NULL)
return;
helper(cur->left, ans);//左
ans.push_back(cur->val);//中
helper(cur->right, ans);//右
return;
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
helper(root, ans);
return ans;
}
};
后序遍历
class Solution {
public:
void helper(TreeNode* cur, vector<int>& ans){// 一定要注意这里的&
if(cur==NULL)
return;
helper(cur->left, ans);//左
helper(cur->right, ans);//右
ans.push_back(cur->val);//中
return;
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
helper(root, ans);
return ans;
}
};
二叉树的迭代遍历
迭代遍历不依靠外部的递归函数, 而是通过自身的循环来实现.
需要使用stack
进行节点的存储.
其实递归遍历也隐式地使用了栈.
需要注意的是:
① 我们遍历的顺序无论是什么序, 都是先左后右, 但是入栈的时候应该先右后左;
② 不像递归, 我们可以在cur==NULL
的时候直接return
, 所以需要先判断再入栈.
前序遍历
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==NULL)
return ans;
stack<TreeNode*> st;
st.push(root);
while(!st.empty()){
TreeNode* cur = st.top();
st.pop();
ans.push_back(cur->val);//中
if(cur->right)//一定要注意这里, 是先push right, 再push left, 并且先判断再入栈
st.push(cur->right);//右
if(cur->left)
st.push(cur->left);//左
}
return ans;
}
};
后序遍历
由上图可以看到, 想要得到**“左右中"的后序遍历结果, 只要将先序遍历中的"中左右"改为"中右左”**, 最后再reverse
, 就可以达成.
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==NULL)
return ans;
stack<TreeNode*> st;
st.push(root);
while(!st.empty()){
TreeNode* cur = st.top();
st.pop();
ans.push_back(cur->val);//中
if(cur->left)
st.push(cur->left);//左
if(cur->right)
st.push(cur->right);//右
}
reverse(ans.begin(), ans.end());//翻转
return ans;
}
};
中序遍历
前面的前序遍历和后序遍历相比只是微调, 但是中序遍历的逻辑改动很大.
具体是: 先顺着左子树下降到底, 然后再逐步回滚, 过程中随时从左子树下降到底.
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==NULL)
return ans;
stack<TreeNode*> st;
TreeNode* cur = root;
while(cur!=NULL || !st.empty()){//注意这里的循环条件, 此时的st是可以在过程中为空的
if(cur!=NULL){// 只要条件允许, 就顺着左子树下降到底
st.push(cur);
cur = cur->left;
}else{// 下降到底了, 就回滚
cur = st.top();
st.pop();
ans.push_back(cur->val);
cur = cur->right;
}
}
return ans;
}
};
迭代遍历的变种(标记遍历)
可以看到: 刚才说的这种迭代遍历中, 不同的遍历顺序对代码逻辑的改动很大, 基本相当于重新写了一种遍历方式了.
有没有一种遍历方法, 可以让不同顺序的遍历基本相同, 只需要微调呢?
有的, 那就是标记遍历.
其思想大概是: 遇到本来需要出栈&处理(入ans队列)的节点时, 不慌着出栈&处理, 而是先塞回栈, 并且再塞一个NULL作为标记.
之后回过头来, 在栈中遇到NULL的时候, 再将下一个出栈&处理.
前序遍历
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==NULL)
return ans;
stack<TreeNode*> st;
st.push(root);
while(!st.empty()){
TreeNode* cur = st.top();
if(cur!=NULL){
st.pop();
if(cur->right)
st.push(cur->right);
if(cur->left)
st.push(cur->left);
st.push(cur);
st.push(NULL);
}else{
st.pop();
cur = st.top();
st.pop();
ans.push_back(cur->val);
}
}
return ans;
}
};
中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==NULL)
return ans;
stack<TreeNode*> st;
st.push(root);
while(!st.empty()){
TreeNode* cur = st.top();
if(cur!=NULL){
st.pop();
if(cur->right)
st.push(cur->right);
st.push(cur);
st.push(NULL);
if(cur->left)
st.push(cur->left);
}else{
st.pop();
cur = st.top();
st.pop();
ans.push_back(cur->val);
}
}
return ans;
}
};
后序遍历
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
if(root==NULL)
return ans;
stack<TreeNode*> st;
st.push(root);
while(!st.empty()){
TreeNode* cur = st.top();
if(cur!=NULL){
// st.pop();// 这两行要不要都行, 写上的话可以保持三种遍历方式的代码一致性, 但其实没有影响
// st.push(cur);
st.push(NULL);
if(cur->right)
st.push(cur->right);
if(cur->left)
st.push(cur->left);
}else{
st.pop();
cur = st.top();
st.pop();
ans.push_back(cur->val);
cout << cur->val << " " ;
}
}
return ans;
}
};
总结
二叉树的遍历操作可以说是解决二叉树相关问题基础中的基础, 一定要熟练掌握, 闭着眼睛也能写.
实际解题过程中可能递归用的更多一些, 但是迭代法不可不会.
一方面可能有面试官要求, 另一方面据说公司实战场合不喜欢用不好控制的递归.