满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k(从1开始算),有2^k-1个节点。
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树:有数值,是有序树 。
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树。是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉搜索树,本身符合二叉搜索树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树
unordered_map、unordered_set的底层实现是哈希表
主要有两种遍历方式:
和链表类似,但节点中多了两个指针,分别指向左子节点和右子节点
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) {}
};
void traversal (TreeNode* cur, vector<int>& vec)
if (cur == NULL)
return;
vec.push_back(cur->val); //中节点
traversal(cur->left, vec); //左节点
traversal(cur->right, vec); //右节点
完整前序遍历代码:
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) { //里面的两个参数为什么这么写?
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
同理,中序遍历:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
后序遍历:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
递归的实现:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,这就是递归可以返回上一层位置的原因。
迭代本质上是在模拟递归,因为在递归的过程中使用了系统栈,所以在迭代的解法中常用 Stack 来模拟系统栈
力扣题目链接144.二叉树的前序遍历
前序遍历为中左右,所以每次先处理中节点,先把根节点入栈,然后将右子节点入栈,再左子节点。先将右子节点入栈是因为这样出栈时顺序才是和入栈时反过来的中左右。
具体思路结合下面的代码看:
class Solution {
public:
struct TreeNode { //定义二叉树
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int val) : val(0), left(nullptr), right(nullptr) {}
};
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st; //用于迭代的栈
vector<int> result; //存储从栈中pop出来的正确顺序的结果
if (root == NULL) //空二叉树直接返回
return result;
st.push(root); //根节点直接入栈。也方便下面循环终止条件的开启
while(!st.empty()){
TreeNode* node = st.top(); //中节点
result.push_back(node->val); //存已经是正确顺序的值,选择通过树的节点来取值,而不是直接从栈中取
st.pop(); //已经处理完的数据
if (node->right)
st.push(node->right); //右子节点,空节点不入栈
if (node->left)
st.push(node->left); //左子节点,空节点不入栈
}
return result;
}
};
力扣题目链接94.二叉树的中序遍历
中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
因此要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st; //用来处理节点元素的栈
TreeNode* cur = root; //用来访问节点的指针
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
之前的方法不通用的原因是,在迭代法的中序遍历中,访问节点(遍历节点)和处理节点(将元素放进结果集)并不同步。
因此可以用标记法来进行统一,将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。
详见代码注释(下一个中序遍历有动图演示)
#include
using namespace std;
class Solution {
public:
vector<int> postorderTraversal(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(); //将该节点弹出,避免重复操作,下面再将中右左节点添加到栈中
st.push(node); //添加中节点到栈中
st.push(NULL); //中节点访问过,但是还没有处理,加入空节点做为标记
if (node -> right) //添加右节点到栈中
st.push(node -> right);
if (node -> left) //添加左节点到栈中
st.push(node -> left);
}
else { //只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); //除去NULL节点
node = st.top(); //记录NULL前的数值
result.push_back(node -> val);
st.pop(); //弹出已经处理完的元素,开启下一个循环
}
}
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(); //除去NULL节点
node = st.top(); //记录NULL前的数值
result.push_back(node -> val);
st.pop(); //弹出已经处理完的元素,开启下一个循环
}
}
return result;
}
};
属于广度优先遍历,需要借用一个辅助数据结构即队列来实现。
队列先进先出,符合一层一层遍历的逻辑;而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑
力扣题目链接102.二叉树的层序遍历
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que; //使用队列来保存节点
vector<vector<int>> result; //因为需要分别记录每一层的节点
if ( root!= NULL ) que.push(root);
while(!que.empty()){ //一层一层处理
int size = que.size(); //记录当前层的节点个数
vector<int> tmp; //用于记录当前层的每个节点数值
for (int i = 0; i < size; i++){
TreeNode* node =que.front(); //每处理完当前层的一个节点就pop掉
que.pop();
tmp.push_back(node -> val); //保存处理完的节点的数值
if (node -> left) //判断当前处理的节点是否有子节点,如果有则加入队列,也就是加入到下一层
que.push(node -> left);
if (node -> right)
que.push(node -> right);
}
result.push_back(tmp); //把当前层的所有节点数值加入到最终结果中
}
return result;
}
};