以下内容只是简单总结
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 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,因为它们下面没有节点。
二叉树的根节点的高度是整个树的高度。 特定节点的高度是从该节点到叶节点的最长路径上的边数。
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
与链表类似,但是比链表多了一个指针。
递归遍有前序遍历、中序遍历、后序遍历。
我觉得有个题的答主说得很对。答主意思是,递归遍历就是要想清楚要做什么,什么时候停止,而且不用太在意拘役的递归过程,只是要想清楚让计算机干什么(计算机都可能溢出,人脑遍历就不现实了)。
例如,前序遍历时,我想先遍历头节点,遍历之后;我想再遍历左节点,那么我只要告诉编译器我想遍历左节点;再然后是右节点。那么中序遍历、后序遍历也是同样的道理。
代码随想录内容:
那么在实现递归遍历时,要抓住三个点:确定递归函数的参数和返回值、确定终止条件、确定单层递归的逻辑。
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;
}
};
前面提到的递归三个点,以下说明:
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;
}
};
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;
}
};
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,
栈实现,栈存放节点,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;
}
};
中序遍历是左中右的顺序,不能是先处理再访问的顺序了。
中序遍历先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点,也就是在把节点的数值放进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指向空,栈为空,访问结束,返回结果
后序遍历是左右中,先序遍历是中左右。
那么可以调整一下先序遍历的代码顺序,又中左右变成中右左的遍历顺序,然后再反转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;
}
};
分析与思路:
针对深度优先遍历的先序、中序、后序遍历,可以使用统一的迭代法来实现。
从上面的迭代法来看,前后序遍历有关联,可以先处理再访问。但是中序遍历则是先访问再处理。以中序遍历为例,使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。以下优先对中序遍历处理。
可以使用标记法实现,将访问的节点放入栈中,把要处理的节点也放入栈中同时做标记。如何标记呢?把要处理的节点放入栈之后,紧接着放入一个空指针作为标记。
也就是说,中序遍历的统一迭代法把访问和处理的节点都存入栈中,并且标记要处理的节点,即存入要处理的节点后再存入一个空指针作为标记。
做法:
首先,访问时,把所有节点按照右中左的顺序入栈,其中,中节点入栈后要存一个空指针;
然后,处理时,遇到空指针弹出,把下一个元素存入数组中;非空指针则直接存入数组中。
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;
}
};
思路: 和中序遍历一样做法一样,前序遍历顺序是中左右,那么按照右左中顺序入栈,还是对中节点进行标记。
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;
}
};
思路: 和中序遍历一样做法一样,后序遍历顺序是左右中,那么按照中右左顺序入栈,对中节点进行标记。
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;
}
};