文章目录
一、二叉树理论基础
以下内容只是简单总结
- 二叉树有两种主要的形式:满二叉树和完全二叉树
- 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。满二叉树深度为k,有2^k-1个节点的二叉树。
-
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
-
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
-
二叉搜索树
满二叉树和完全二叉树都没有数值,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树
-
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树。平衡二叉搜索树要么是一棵空树,要么它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
-
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
-
二叉树的高度是垂直方向上树的长度的量度,它是从孩子到父母的向上方向测量的,叶节点的高度为0,因为它们下面没有节点。
-
二叉树的根节点的高度是整个树的高度。 特定节点的高度是从该节点到叶节点的最长路径上的边数。
二、二叉树存储方式
- 二叉树可以链式存储,也可以顺序存储,通常使用链式存储。
- 链式存储方式用指针,把分布在各个地址的节点串联一起,元素在内存分布不连续;顺序存储的方式用数组,元素在内存是连续分布的。
- 链式存储与顺序存储示意图
三、二叉树的遍历方式
- 二叉树主要有两种遍历方式:深度优先遍历和广度优先遍历
- 深度优先遍历:先往深走,遇到叶子节点再往回走。以中间节点顺序为主
- 前序遍历(递归法,迭代法):中左右
- 中序遍历(递归法,迭代法):左中右
- 后序遍历(递归法,迭代法):左右中
举例,下图,三种遍历
- 前序遍历(中左右):10 6 3 9 16 14 19
- 中序遍历(左中右):3 6 9 10 14 16 19
- 后序遍历(左右中):3 9 6 14 19 16 10
- 广度优先遍历:一层一层的去遍历。
- 层次遍历(迭代法)
四、二叉树的定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
与链表类似,但是比链表多了一个指针。
五、二叉树的递归遍历
递归遍有前序遍历、中序遍历、后序遍历。
我觉得有个题的答主说得很对。答主意思是,递归遍历就是要想清楚要做什么,什么时候停止,而且不用太在意递归的过程,只是要想清楚让计算机干什么(计算机都可能溢出,人脑遍历就不现实了)。
例如,前序遍历时,我想先遍历头节点,遍历之后;我想再遍历左节点,那么我只要告诉编译器我想遍历左节点;再然后是右节点。那么中序遍历、后序遍历也是同样的道理。
代码随想录内容:
那么在实现递归遍历时,要抓住三个点:确定递归函数的参数和返回值、确定终止条件、确定单层递归的逻辑。
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
1. 二叉树的前序遍历-题144
class Solution {
public:
//递归
void preorder(TreeNode* root, vector<int>& result)
{
if(root==nullptr) return;
result.push_back(root->val);
preorder(root->left, result);
preorder(root->right, result);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
preorder(root, result);
return result;
}
};
前面提到的递归三个点,以下说明:
- 确定递归函数的参数和返回值:参数里需要传入vector来放节点的数值,以及当前节点指针;不需要返回什么
- 确定终止条件:在递归的过程中,遍历到空指针就结束
- 确定单层递归的逻辑:由于这是前序遍历,中左右的顺序,因此首先存放中节点数值,再依次是左节点、右节点,直接调用函数即可
2. 二叉树的中序遍历-题94
class Solution {
public:
//递归
void preorder(TreeNode* root, vector<int>& result)
{
if(root==nullptr) return;
preorder(root->left, result);
result.push_back(root->val);
preorder(root->right, result);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
preorder(root, result);
return result;
}
};
3. 二叉树的后序遍历-题145
class Solution {
public:
//递归
void preorder(TreeNode* root, vector<int>& result)
{
if(root==nullptr) return;
preorder(root->left, result);
preorder(root->right, result);
result.push_back(root->val);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
preorder(root, result);
return result;
}
};
六、二叉树的迭代遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,
1. 二叉树的前序遍历-题144
栈实现,栈存放节点,vector存放数值:
先处理根节点,存入栈中,
开始遍历,访问栈顶元素,访问后要弹出,再存入vector容器中,此时相当于访问了中节点;
压入右节点,此时栈顶元素更新为右节点了。再做同样的操作,访问栈顶元素,访问后要弹出,再存入vector容器中;
压入左节点,此时栈顶元素更新为左节点了,再做同样的存储操作;
空节点不入栈。
这里重点是先处理再访问
class Solution {
public:
//迭代
vector<int> preorderTraversal(TreeNode* root)
{
stack<TreeNode*> st;
vector<int> result;
//1.空指针不入栈
if(root == nullptr) return result;
//2.先处理根节点
st.push(root);
//3.开始遍历 栈为空结束 所有节点遍历完结束
while(!st.empty())
{
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if(node->right) st.push(node->right);
if(node->left) st.push(node->left);
}
return result;
}
};
2. 二叉树的中序遍历-题94
中序遍历是左中右的顺序,不能是先处理再访问的顺序了。
中序遍历先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点,也就是在把节点的数值放进result数组中
这里重点是先访问再处理
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root)
{
stack<TreeNode*> st;
vector<int> result;
TreeNode* cur = root;
while(cur!=nullptr || !st.empty())
{
//1.指针遍历到左叶子节点
//首先入栈的是根节点,然后左节点从上往下依次入栈
if(cur!=nullptr)
{
st.push(cur);//访问的节点入栈
cur = cur->left;//更新为左节点
}
else
{
cur = st.top();//访问栈顶节点 先弹出左节点
st.pop();
result.push_back(cur->val);//中节点
cur = cur->right;//右节点
}
}
return result;
}
};
整个流程:
从根节点开始访问,依次访问4、1
此时cur指向空,访问栈顶元素1,弹出1,result存入1,cur更新为1的右节点;
此时cur指向空,再次访问栈顶元素4,弹出4,result存入4,cur更新为4的右节点;
此时cur指向节点2,栈中存入2,cur更新为2的左节点;
此时cur指向空,再次访问栈顶元素2,弹出2,result存入2,cur更新为2的右节点;
此时cur指向空,再次访问栈顶元素5,弹出5,result存入5,cur更新为5的右节点;
此时cur指向节点6,栈中存入6,cur更新为2的左节点;
此时cur指向空,再次访问栈顶元素6,弹出6,result存入6,cur更新为6的右节点;
此时cur指向空,栈为空,访问结束,返回结果
3. 二叉树的后序遍历-题145
后序遍历是左右中,先序遍历是中左右。
那么可以调整一下先序遍历的代码顺序,又中左右变成中右左的遍历顺序,然后再反转result数组,输出的结果顺序就是后序遍历的左右中顺序了。
class Solution {
public:
//迭代
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> result;
stack<TreeNode*> st;
//后序时 先序代码调整由中左右变成中右左 最后反转result
//1.空指针不入栈
if(root==nullptr) return result;
//2.先压入根节点
st.push(root);
while(!st.empty())
{
//3.先弹出根节点并存入
TreeNode* cur = st.top();
st.pop();
result.push_back(cur->val);
if(cur->left) st.push(cur->left);//4.左
if(cur->right) st.push(cur->right);//5.右
}
//6.反转数组
reverse(result.begin(), result.end());
return result;
}
};
七、二叉树的统一迭代法
分析与思路:
针对深度优先遍历的先序、中序、后序遍历,可以使用统一的迭代法来实现。
从上面的迭代法来看,前后序遍历有关联,可以先处理再访问。但是中序遍历则是先访问再处理。以中序遍历为例,使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。以下优先对中序遍历处理。
可以使用标记法实现,将访问的节点放入栈中,把要处理的节点也放入栈中同时做标记。如何标记呢?把要处理的节点放入栈之后,紧接着放入一个空指针作为标记。
也就是说,中序遍历的统一迭代法把访问和处理的节点都存入栈中,并且标记要处理的节点,即存入要处理的节点后再存入一个空指针作为标记。
1. 二叉树的中序遍历-题94
做法:
首先,访问时,把所有节点按照右中左的顺序入栈,其中,中节点入栈后要存一个空指针;
然后,处理时,遇到空指针弹出,把下一个元素存入数组中;非空指针则直接存入数组中。
class Solution {
public:
//统一迭代法
vector<int> postorderTraversal(TreeNode* root)
{
stack<TreeNode*> st;
vector<int> result;
//1.先处理根节点
if(root!=nullptr) st.push(root);
while(!st.empty())
{
TreeNode* cur = st.top();
//2.所有指针入栈 按照右中左顺序入栈
if(cur!=nullptr)
{
//注意!最开始存入了一个节点 要先弹出 避免重复入栈了
st.pop();//将该节点弹出,避免重复操作
//将右中左节点添加到栈中
if(cur->right) st.push(cur->right);//右
st.push(cur);//中
st.push(NULL);//标记
if(cur->left) st.push(cur->left);//左
}
else
{
//3.处理节点 空指针先弹出 再存下一个节点
st.pop();//空指针先弹出
cur = st.top();
st.pop();//下一个节点再弹出
result.push_back(cur->val);
}
}
return result;
}
};
2. 二叉树的前序遍历-题144
思路: 和中序遍历一样做法一样,前序遍历顺序是中左右,那么按照右左中顺序入栈,还是对中节点进行标记。
class Solution {
public:
//统一迭代法
vector<int> postorderTraversal(TreeNode* root)
{
stack<TreeNode*> st;
vector<int> result;
//1.先处理根节点
if(root != nullptr) st.push(root);
while(!st.empty())
{
TreeNode* cur = st.top();
//2.按照右左中顺序 中节点标记 所有节点入栈
if(cur!=nullptr)
{
st.pop();//避免重复操作
if(cur->right) st.push(cur->right);//右
if(cur->left) st.push(cur->left);//左
st.push(cur);//中
st.push(nullptr);//标记
}
//3.处理节点 空指针先弹出 下一个指针存入
else
{
st.pop();//空指针先弹出
cur = st.top();
st.pop();
result.push_back(cur->val);
}
}
return result;
}
};
3. 二叉树的后序遍历-题145
思路: 和中序遍历一样做法一样,后序遍历顺序是左右中,那么按照中右左顺序入栈,对中节点进行标记。
class Solution {
public:
//统一迭代法
vector<int> postorderTraversal(TreeNode* root)
{
stack<TreeNode*> st;
vector<int> result;
//1.先处理根节点
if(root != nullptr) st.push(root);
while(!st.empty())
{
TreeNode* cur = st.top();
//2.按照中右左顺序 中节点标记 所有节点入栈
if(cur!=nullptr)
{
st.pop();//避免重复操作
st.push(cur);//中
st.push(nullptr);//标记
if(cur->right) st.push(cur->right);//右
if(cur->left) st.push(cur->left);//左
}
//3.处理节点 空指针先弹出 下一个指针存入
else
{
st.pop();//空指针先弹出
cur = st.top();
st.pop();
result.push_back(cur->val);
}
}
return result;
}
};