二叉树理论基础
二叉树种类
在解题过程中,主要有两种形式的二叉树:完全二叉树和满二叉树
满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树
完全二叉树:除了最底层是从左往右的节点,其余都是满二叉树的树
二叉搜索树
前面的两种树是没有数值的,而二叉搜索树有数值,二叉搜索树是一个有序树。
- 若它的左子树不为空,其左节点上的值都小于根节点
- 若它的右子树不为空,其右节点上的值都大于根节点
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树
平衡二叉搜索树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
二叉树的遍历方式
1、二叉树主要有两种遍历方式:
深度优先遍历:先往深走,遇到叶子节点再往回走。
广度优先遍历:一层一层的去遍历
2、那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
深度优先遍历(这里前中后,其实指的就是中间节点的遍历顺序)
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
广度优先遍历
- 层次遍历(迭代法)
二叉树的定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
递归遍历
每次写递归,按照如下三要素来写:
-
确定函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么在递归函数里加上这个参数,并且还要明确每次参数的返回值是什么进而确定递归函数的返回类型
-
确定终止条件:写完了递归算法,运行的时候,经常遇到栈溢出的错误,就是没有写终止条件或者终止条件结果不对,操作系统也是一个用栈结构来保存每一层递归的信息,如果递归没有终止,操作系统的栈必然会溢出。
-
确定单层递归的逻辑:确定每一层递归需要处理的信息,在这里会重复调用自己来实现递归的过程
以下以前序遍历为例:
- 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
void traversal(TreeNode* cur, vector<int>& vec)
- 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (cur == NULL) return;
- 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
力扣
144二叉树的前序遍历
145二叉树的后序遍历
94 二叉树的中序遍历
迭代遍历
前序遍历(迭代法)
在遍历的时候,需要考虑入栈出栈的顺序,在前序遍历:顺序是中左右,但是栈是先进后出的格式,于是要考虑进栈的时候先右后左,那么就可以使得出口i变成了中左右。
/**
* Definition for a binary tree node.
* 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:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root == nullptr) return result;
st.push(root); // 将根节点输入到栈
while (!st.empty()) {
TreeNode* node = st.top(); //取到中间节点, 使用node来存
st.pop(); // 弹出中节点
result.push_back(node->val); //取到此时的中节点的值
if (node->right) st.push(node->right); //将右节点入栈,为空不入
if (node->left) st.push(node->left); //将左节点入栈,为空不入
}
return result;
}
};
后续遍历:
后序遍历反而比较容易直接修改: 前序中左右
—>中右左
—>左右中
后序。只需要将入栈的顺序变成先左后头=右,然后再对返回结果进行反转就可以得到后续遍历。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
if (root == nullptr) return result;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left);
if (node->right) st.push(node->right);
}
reverse(result.begin(), result.end());
return result;
}
};
中序遍历:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result; // 创建返回结果
stack<TreeNode*> st; // 构建Node栈
TreeNode* cur = root; // 设置一个指针指向root节点
while (cur != nullptr || !st.empty()) { // 当我的指针或栈不为空的时候处理
if (cur != nullptr) { // 指针不为空,向后遍历,同时入栈
st.push(cur);
cur = cur->left; // 左中右
}
else { // 当指针指向空的时候,左边孩子节点为空,需要将节点记录,同时弹出栈头元素
cur = st.top();
st.pop();
result.push_back(cur->val); // 记录了cur指针指向的左节点的值
cur = cur->right;// 向右遍历
}
}
return result; // 返回结果
}
};
总结:对于迭代法,使用栈模拟二叉树的遍历过程,还是需要多模拟,手动写一写思路,在写的过程中判断处理情况。这点很重要。
统一迭代
因为在迭代遍历的时候并不是一种统一的方法,在实现过程中没有那么容易想到,于是有统一迭代方法。
统一迭代法的时候,只需要将前中后的原始顺序反向入栈即可:
- 前序:
中左右—>右左中
- 中序:
左中右—>右中左
- 后续:
左右中—>中右左
处理加入空指针记录中节点的值,利用这一位置来处理情况
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
};
总结
今天的内容比较难想到,只有递归遍历比较好想,但是递归的一个整体实现逻辑没有完全明白,对于迭代法,还需要加强栈的相关学习。