二叉树理论基础
(下面描述并不是严谨的)
1、二叉树种类
满二叉树:除了最后一层所有节点都是满节点,每一层都被占满,k层就有2^k-1个节点
完全二叉树:二叉树层序遍历一直到最后一个节点没有缺节点
平衡二叉树:左右两颗子树最大深度不超过1,左右两颗字数都为平衡二叉树
二叉搜索树:左子树的值小于根节点的值小于右节点的值。
注:我们之前学习过的map,set,multi_map,multi_set底层的红黑树就是一种平衡二叉搜索树,其搜索效率为logn。
2、二叉树遍历
深度优先遍历:先在一条路遍历到底,最后迭代返回遍历到底另一条路
前序遍历:中左右(递归法,迭代法)
中序遍历:左中右(递归法,迭代法)
后序遍历:左右中(递归法,迭代法)
广度优先遍历:先遍历所有路,最后依次从第一条路开始遍历这条路后面的所有路
层序遍历:迭代法
3、二叉树存储
链式存储:指针,离散分布
顺序存储:数组,连续分布。当使用数组存储时,就是层序遍历放进数组中,则一个节点左孩子的索引为i*2+1,右孩子索引为i*2+2。
4、二叉树定义
struct TreeNode{
TreeNode* left;
TreeNode* right;
int value;
TreNode(int x):value(x),left(NULL),right(NULL){}
}
递归遍历
1、前序遍历(中左右)
void preordertraversal(TreeNode* node , vector<int> &res){
if(node==nullptr) return;
res.push_back(node->val);
preordertraversal(node->left,res);
preordertraversal(node->right,res);
}
2、中序遍历(左中右)
void inordertraversal(TreeNode* node , vector<int> &res){
if(node==nullptr) return;
inordertraversal(node->left,res);
res.push_back(node->val);
inordertraversal(node->right,res);
}
3、后序遍历(左右中)
void postordertraversal(TreeNode* node , vector<int> &res){
if(node==nullptr) return;
postordertraversal(node->left,res);
postordertraversal(node->right,res);
res.push_back(node->val);
}
迭代遍历
1、前序遍历(中左右)
由于是用栈来实现迭代,要想先遍历左节点,需要先把右节点放入栈中,代码如下
vector<int> preordertraversal(TreeNode* node){
stack<TreeNode*> st;
vector<int> res;
if(node==nullptr) return res;
st.push(node);
while(!st.empty()){
TreeNode* tmp = st.top();
st.pop();
if(tmp!=nullptr) res.push_back(tmp->val);
else continue;
if(tmp->right) st.push(tmp->right);
if(tmp->left) st.push(tmp->left);
}
return res;
}
2、中序遍历(左中右)
由于处理节点和压入栈节点(访问)的顺序不一样,也就是一开始肯定会访问根节点,但并不能处理根节点(节点值放入数组中),因此不能像递归那样简单的调换顺序,需要用一个指针遍历所有节点。代码如下:
在这里确立两套统一操作,当当前指针不为空时,继续向左遍历压栈,当指针为空时,弹出栈顶元素传入数组中,向右遍历压栈。
vector<int> inordertraversal(TreeNode* node){
stack<TreeNode*> st;
vector<int> res;
TreeNode* cur = Node;
while(cur!=nullptr || !st.empty()){
if(cur!=nullptr){
st.push(cur);
cur = cur->left;
}else{
cur = st.top();
st.pop();
res.push_back(cur->val);
cur = cur->right;
}
}
return res;
}
3、后序遍历(左右中)
在前序遍历基础上,只需要把处理左右节点的顺序倒换,则最终输出的数组为中右左的遍历顺序,再把这个数组翻转,则就为左右中的顺序。代码如下:
vector<int> postordertraversal(TreeNode* node){
stack<TreeNode*> st;
vector<int> res;
if(node==nullptr) return res;
st.push(node);
while(!st.empty()){
TreeNode* tmp = st.top();
st.pop();
if(tmp!=nullptr) res.push_back(tmp->val);
else continue;
if(tmp->left) st.push(tmp->left);
if(tmp->right) st.push(tmp->right);
}
reverse(res.begin(),res.end());
return res;
}
统一迭代
上述三种遍历方法中序遍历与其余两种遍历方法不统一,不像递归遍历只需要修改几行的顺序就可以,这里也有一种统一的写法可以提供参考,以卡哥代码的中序遍历为例:
可以看到在代码中添加了一个空节点来为访问但还没处理的节点做标记,有一行注释我觉得是思路所在:只有遇到空节点的时候,才将下一个节点放进结果集。
如果前序遍历,则先判断右节点再判断左节点再压栈;
如果后序遍历,则先压栈,再判断右节点再判断左节点。
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;
}