文章目录
1.概念和性质
1.1.树
(1)树可以没有结点,这种情况树称为空树。
(2)满足连通、边数等于顶点数减1的结构一定是一棵树。
(3)当树只有一个结点(即根节点)时,根结点也算叶子结点。
(4)根节点不是任何结点的子结点,在题目中经常会利用这个性质确定根节点。
1.2.完全二叉树(complete binary tree)
概念:除最下面一层,其余层都达到最大结点数,最下面一层从左到右存在若干结点。
特殊情况:满二叉树(full binary tree)——各层都达到最大结点数。
特殊的存储方式:对结点从上到下、从左到右编号(从1开始),对任一编号为x的结点,其左孩子的编号一定为2x,右孩子为2x+1。且这种存放顺序为其层序遍历序列。
(1) 判断某个结点是否为叶结点的标志:记该结点下标为x,其左子结点标号2x大于结点总个数n。
(2) 判断某个结点为空结点的标志:下标大于总个数。
2.二叉树
2.1.建树
1.存储结构——二叉链表
struct node {
int val;
node *left, *right;
};
2.结点的插入
思路:假定根据给定条件插入位置应该是确定的,即插入位置只有一个,因此插入位置就是数据域在二叉树中查找失败的位置。因此在递归查找的过程中,最后到达空树(死胡同)的地方就是插入的位置。
注:这里root是引用的指针类型,是由于调用函数需要改变传入实参的值,这样才能将结点插入。
void insert(node *&root, int x) { //注意root为引用类型
if (root == NULL) {
root = new node;
root->val = x;
root->left = root->right = NULL; //务必令新结点的左右指针域为NULL
}
else if (由于给定条件,结点应该插入在左子树) {
insert(root->left, x);
}
else insert(root->right, x);
}
3.二叉树的创建
注意这里初始化root为NULL
int data[maxn]; //建树的数据域
node* root=NULL; //新建空根节点
for(int i=0;i<n;i++){
insert(root, data[i]);
}
4.DFS
例如要统计每层的结点数:
void dfs(node *root,int depth) {
if (root == NULL) { //递归边界
return;
}
num[depth]++;
dfs(root->left, depth + 1);
dfs(root->right, depth + 1);
}
5.例题
由于需要建树的题目不多,有一个经典题是根据给定序列建BST。
https://pintia.cn/problem-sets/994805342720868352/problems/994805355987451904
2.2.二叉树的静态表示
静态表示即不使用指针来存储子结点的位置,做题时会更简便。因此以下算法均使用静态表示。
struct node{
int left,right;
int val;
};
2.3. 二叉树遍历
先序、中序、后序均采用递归算法即可,比较简单。这里展示层序遍历代码,是BFS算法:
void layerorder(int root) {
queue<int> q;
q.push(root);
while (!q.empty()) {
int now = q.front(); //取出队首元素
q.pop(); //出队
cout << no[now].val<<" "; //访问
if (no[now].left != -1) q.push(no[now].left);
if (no[now].right != -1) q.push(no[now].right);
}
}
2.4.几种遍历之间的转换
https://blog.csdn.net/weixin_43590232/article/details/104002692
3.一般意义的树的遍历
与二叉树遍历算法相同,只是结构体中用vector< int > child来存储子结点
3.1.深度优先搜索(DFS)与先根遍历
(1)对所有DFS求解过程,都可以把它画成树的形式,此时搜索时的死胡同(递归边界)等价与树中叶子结点,而岔道口等价与树中的非叶子结点。这也是为什么DFS算法中有剪枝的概念,就是从树的角度理解才产生的概念,即发现子树不可能存在问题的解,则剪掉该子树。
(2)对树的DFS遍历过程就是树的先根遍历过程(先访问根节点,再从左至右依次访问所有子树)
3.2.广度优先搜索(BFS)与层序遍历
(1)所有的BFS求解过程,都可以转换为树的层序遍历求解过程;
(2)树的层序遍历过程也就是一个广度优先搜索的过程。
3.3.相关题目
https://blog.csdn.net/weixin_43590232/article/details/104068637
4.二叉搜索树
也叫二叉查找树,概念就不解释了,直奔主题。
4.1.性质
二叉树搜索树有一个重要的性质:
对二叉搜索树进行中序遍历,遍历的结果是有序的。
4.2.相关题目
https://blog.csdn.net/weixin_43590232/article/details/104083675
https://blog.csdn.net/weixin_43590232/article/details/104086605
https://blog.csdn.net/weixin_43590232/article/details/104213909
5.AVL树
5.1 概念
当二叉查找树结构不当,退化为链式时,查找的时间复杂度为O(n)
,那么为了使树的结构能使插入元素后仍能保持O(logn)
的级别,产生了二叉平衡树(AVL树)。因此,AVL树仍是二叉查找树。平衡是指:对AVL数的任一结点,其左右子树的高度差的绝对值不超过1。其中左右子树的高度差称该结点的平衡因子。
5.2调整方法
对于左旋、右旋,有两种不同的定义,某些书的解释是将根结点的左孩子作为根结点是左旋,有的则以逆时针的方向作为左旋的判断。为了方便理解,这里选择第二种。
(1)左旋
即逆时针旋转,B取代A作为根结点。A成为B的左孩子,B的左孩子成为A的右孩子。
调整步骤如下:
void L(node* &root) {
node* temp = root->right;
root->right = temp->left;
temp->left = root;
root = temp;
}
(2)右旋
右旋与左旋只是方向相反,思路相同。
void R(node* &root) {
node* temp = root->left;
root->left = temp->right;
temp->right = root;
root = temp;
}
5.3 树型
但是情况不止以上两种,为了方便理解,引入了树型的概念。
首先,结论1:只要把最靠近插入结点的失衡结点调整到正常,路径上的所有结点就都会平衡。
插入结点就是“破坏者”,最近的失衡结点是“被破坏者”。如下图,灰色结点表示“破坏者”,不论破坏者是B的左孩子还是左孩子,情况都相同。需要先对C子树进行左旋,再对A右旋。由于破坏者在被破坏者左子树的右子树上,称为LR树型。需要先L旋转,再R旋转。如果破坏者在被破坏者左子树的左子树上,称为LL型,只需要右旋。
类似的,对于RR,RL型亦是如此。
5.5 计算平衡因子
由于平衡因子不能由孩子结点的平衡因子直接求得,而高度可以由其孩子结点的高度求得,因此求平衡因子需要先求高度。
int getHeight(node *root) {
if (root == NULL) return 0;
int l = getHeight(root->left); //左孩子的高度
int r = getHeight(root->right); //右孩子的高度
return max(l, r) + 1;
}
int getBalanceFactor(node* root) {
return getHeight(root->left) - getHeight(root->right);
}
5.4 插入算法
void insert(node* &root, int val) {
if (root == NULL) {
root = new node;
root->val = val;
root->left = root->right = NULL;
return;
}
if (root->val > val) {
insert(root->left, val);
if (getBalanceFactor(root) == 2) {
if (getBalanceFactor(root->left) == 1) R(root); //LL型
else { //LR型
L(root->left);
R(root);
}
}
}
else {
insert(root->right, val);
if (getBalanceFactor(root) == -2) {
if (getBalanceFactor(root->right) == -1) L(root); //RR型
else { //RL型
R(root->right);
L(root);
}
}
}
}