二叉树理论基础
满二叉树
深度为k,节点 2 k − 1 2^k-1 2k−1 个。
完全二叉树
严格按照从左到右完全填充的结构来。堆就是一种完全二叉树
二叉搜索树
- 有序树,左子树小于根节点,右子树大于根节点。
平衡二叉搜索树(AVL)
- 它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。(增加查询效率);
- C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树。
链式存储和顺序存储
链式存储对于二叉树的定义:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
要学会手撸构造函数。
顺序存储用数组来存放节点。
- 如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
二叉树的遍历方式
- 前序遍历
- 中序遍历
- 后序遍历
说的就是根的相对位置。
- 递归遍历
- 迭代遍历
递归遍历简单,迭代遍历要借助栈/队列。
- 深度优先遍历
- 广度优先遍历(层次遍历)
二叉树的递归遍历
通用的递归三要素:
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
以下以前序遍历为例:
- 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
void traversal(TreeNode* cur, vector<int>& vec)
这里使用&引用运算符传入vec,使vec可修改。
- 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (cur == NULL) return;
- 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
但是题目的要求是教我们返回最终的res数组,因此不应该直接调用含有递归的函数,应该在外面再加一个函数包装一下,最终代码:
class Solution {
public:
void traversal(TreeNode *node, vector<int> &res) {
if (node == NULL) {
return;
}
res.push_back(node->val);
traversal(node->left, res);
traversal(node->right, res);
}
vector<int> preorderTraversal(TreeNode *root) {
vector<int> res;
traversal(root, res);
return res;
}
};
中序遍历就是改一下7~9行代码的位置:
class Solution {
public:
void traversal(TreeNode *node, vector<int> &res) {
if (node == NULL) {
return;
}
traversal(node->left, res);
res.push_back(node->val);
traversal(node->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root, res);
return res;
}
};
后序同样:
class Solution {
public:
void traversal(TreeNode *node, vector<int> &res) {
if (node == NULL) {
return;
}
traversal(node->left, res);
traversal(node->right, res);
res.push_back(node->val);
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root, res);
return res;
}
};
实际上,函数的递归顺序是永远不会变的,变得是需要进行操作的节点:
- 绿色:前序
- 蓝色:中序
- 红色:后序
二叉树的递归改迭代
实际上二叉树的迭代不常用,因为递归的算法可解释性很强,也很简洁。因此最好掌握遍历的迭代算法。核心就是自己使用栈来模拟递归时系统的函数调用栈。
可以写出一个缺少遍历位置的基本框架:
class Solution {
public:
vector<int>res;
void pushLeftBranch(TreeNode *p){
while(p != nullptr){
//前序遍历的位置
st.push(p);
p = p->left;
}
}
stack<TreeNode*> st;
vector<int> inorderTraversal(TreeNode *root) {
//visited指向上次遍历完的子树根节点
TreeNode* visited = new TreeNode();
pushLeftBranch(root);
while(!st.empty()){
TreeNode* p = st.top();
//p的左子树被遍历完了,且右子树没有被遍历过
if((p->left == nullptr || p->left == visited) && p->right != visited){
//中序遍历的位置
pushLeftBranch(p->right);
}
//p的右子树被遍历完了
if(p->right == nullptr || p->right == visited){
visited = st.top();
//后序遍历的位置
st.pop();
}
}
return res;
}
};
- 前序遍历的时机:被压入栈时;
- 中序遍历的时机:左子树被遍历完切换到右子树时;
- 后序遍历的时机:子树出栈时。
这里用了一个变成visited用于存放已经遍历过的子树的根节点。这样就完成了统一的迭代遍历的框架。
总结
二叉树的递归遍历代码简洁且容易理解,后面解决的问题也一般都用递归来解决。迭代法的统一框架适当记忆。