本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激
一、AVL树介绍
在上一节中,我们认识到了如果已知序列是一个有序序列,那么生成的BST将是一条链,查找的复杂度会达到O(n),这样就无法实现BST对数据查询优化的目的。所以AVL树就诞生了,其目的是为了使BST的高度在每次插入元素后仍然能保持O(logn)的级别,在建立BST的过程中,通过增加平衡调整功能,使任意结点的左子树和右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度之差称为该结点的平衡因子。由于需要对每个结点都要得到平衡因子,因此需要在树的结构中加入一个变量height,用来记录以当前结点为根节点的子树的高度。
二、AVL树的基本操作
AVL树的删除操作比较复杂,所以书内只给出了AVL树的查找、插入和创建过程。查找操作和BST一模一样,因为AVL树本身就是一颗BST。AVL树的核心是插入操作,因为AVL树要实现BST高度的平衡,所以在进行BST的插入过程中,需要加入平衡调整操作,具体实现工具有左旋和右旋。
左旋:某一根结点的右子结点做根结点,并且保证更改后的树的结构仍然是BST,具体操作如下:
(这里截一张图吧,有点难看······)
(1)让B的左子树成为A的右子树;
(2)让A成为B的左子树;
(3)将根结点设定为结点B;
对应代码如下:
void Lrotation(int &root){ //左旋
int temp = node[root].rchild; //注意体会这一步对应线性链表的哪一步?
node[root].rchild = node[temp].lchild;
node[temp].lchild = root;
updataHeight(root);
updataHeight(temp);
root = temp; //更改树的结构
}
右旋:某一根结点的左子结点做根结点,并且保证更改后的树的结构仍然是BST,具体操作如下:
(1)让A的右子树成为B的左子树;
(2)让B成为A的右子树;
(3)将根结点设定为结点A;
对应代码如下:
void Rrotation(int &root){ //右旋
int temp = node[root].lchild;
node[root].lchild = node[temp].rchild;
node[temp].rchild = root;
updataHeight(root);
updataHeight(temp);
root = temp;
}
接下来,就是根据树的不平衡结构进行相应的平衡调整操作。当树的平衡因子绝对值大于1时(2或-2),树就会变得不平衡,即左右子树的高度差的绝对值大于1。对平衡因子为2时,存在LL型和LR型,当平衡因子为-2时,存在RL型和RR型。对2的情况进行讨论:
当结点A的左孩子的平衡因子是1时为LL型,是-1时为LR型
对于LL型,可以把以C为根结点的子树看做一个整体,然后以结点A作为root进行右旋,便可以达到平衡;对于LR型,先忽略结点A,以结点C为root进行左旋,转化为LL型处理。注意不能直接对LR型的根结点进行右旋,这样右旋下来还是不平衡的。
所以可以得到普适性的步骤为:LR (对root->lchild左旋)-> LL (对root右旋)->平衡
当树的平衡因子等于-2时,对应平衡操作和等于2时的操作互为逆操作,
当结点A的右孩子结点的平衡因子是-1时为RR型,是1时为RL型
普适性的步骤为:RL (对root->rchild右旋)-> RR (对root左旋)-> 平衡
完整的插入代码如下,在BST的基础上增加了调整功能:
void insert(int &root , int data){
if(root == -1) { //查找失败,插入结点
root = newNode(data);
return;
}
if(node[root].data > data) {
insert(node[root].lchild , data);
updataHeight(root); //插入完成后更新高度(递归的归的体现)
if(getBalanceFactor(root) == 2) { //注意这里,如果是向左子树插入的,若不平衡则只有可能是左子树高与右子树,即平衡因子为2
if(getBalanceFactor(node[root].lchild) == 1) Rrotation(root);
else if(getBalanceFactor(node[root].lchild) == -1){
Lrotation(node[root].lchild);
Rrotation(root);
}
}
}
if(node[root].data < data){
insert(node[root].rchild , data);
updataHeight(root);
if(getBalanceFactor(root) == -2) {
if(getBalanceFactor(node[root].rchild) == -1) Lrotation(root);
else if(getBalanceFactor(node[root].rchild) == 1){
Rrotation(node[root].rchild);
Lrotation(root);
}
}
}
}
三、小题练习
1.二叉查找树的平衡因子
思路:建立BST,在结点中加入记录高度的变量,更新高度,计算平衡因子;
实现:静态链表+BST+平衡因子
这一章和BST紧密联系,是目前学习范围内仅有的几种二叉树形态,请熟练掌握二叉树的基本操作;
完整代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 50;
int BST[MAXN] = {};
struct Node{
int data;
int lchild;
int rchild;
int height;
}node[MAXN];
int index = 0;
int n , counter = 1;
int getHeight(int root){ //获取高度
if(root == -1) return 0;
return node[root].height;
}
int getBalanceFactor(int root){ //获取平衡因子
return getHeight(node[root].lchild) - getHeight(node[root].rchild);
}
void updataHeight(int root){ //更新高度
node[root].height = max(getHeight(node[root].lchild) , getHeight(node[root].rchild)) + 1;
}
//以上为新增操作,最好是分模块实现,因为这些模块会被独立反复调用
void lrd(int root){
if(root == -1) return;
lrd(node[root].lchild);
printf("%d", getBalanceFactor(root)); //获取平衡因子
if(counter < n) {
printf(" ");
counter++;
}
lrd(node[root].rchild);
}
int newNode(int v){
node[index] = {v , -1 , -1 , 1};
return index++;
}
void insert(int &root , int data){
if(root == -1) { //查找失败,插入结点
root = newNode(data);
return;
}
if(node[root].data > data) {
insert(node[root].lchild , data);
updataHeight(root); //插入完成后更新高度(递归的归的体现)
}
if(node[root].data < data){
insert(node[root].rchild , data);
updataHeight(root);
}
}
void create(){
int root = -1;
for(int i = 0; i <= n - 1; i++) insert(root , BST[i]);
}
int main(){
scanf("%d", &n);
for(int i = 0; i <= n - 1; i++) scanf("%d", BST + i);
create();
lrd(0);
}
2.平衡二叉树的判定
思路:在上一问的基础上加入判断操作,如果不是平衡二叉树,更新判断标志,回溯;
实现:静态链表+BST+平衡因子
完整代码如下:
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int MAXN = 50;
int BST[MAXN] = {};
struct Node{
int data;
int lchild;
int rchild;
int height;
}node[MAXN];
int index = 0;
int n , counter = 1;
int getHeight(int root){ //获取高度
if(root == -1) return 0;
return node[root].height;
}
int getBalanceFactor(int root){ //获取平衡因子
return getHeight(node[root].lchild) - getHeight(node[root].rchild);
}
void updataHeight(int root){ //更新高度
node[root].height = max(getHeight(node[root].lchild) , getHeight(node[root].rchild)) + 1;
}
//以上为新增操作,最好是分模块实现,因为这些模块会被独立反复调用
bool flag = true;
void lrd(int root){
if(!flag) return;
if(root == -1) return;
lrd(node[root].lchild);
if(abs(getBalanceFactor(root)) > 1) { //获取平衡因子
flag = false;
return;
}
lrd(node[root].rchild);
}
int newNode(int v){
node[index] = {v , -1 , -1 , 1};
return index++;
}
void insert(int &root , int data){
if(root == -1) { //查找失败,插入结点
root = newNode(data);
return;
}
if(node[root].data > data) {
insert(node[root].lchild , data);
updataHeight(root); //插入完成后更新高度(递归的归的体现)
}
if(node[root].data < data){
insert(node[root].rchild , data);
updataHeight(root);
}
}
void create(){
int root = -1;
for(int i = 0; i <= n - 1; i++) insert(root , BST[i]);
}
int main(){
scanf("%d", &n);
for(int i = 0; i <= n - 1; i++) scanf("%d", BST + i);
create();
lrd(0);
if(flag) printf("Yes");
else printf("No");
}
3.平衡二叉树的建立
思路:完整的AVL树实现,在插入结束后,更新树的高度,并实时判断平衡因子,如果不平衡,判断是哪种不平衡,进行相应的旋转操作,注意这些步骤都在插入结束后进行,也就是在合并子问题的时候进行;
实现:静态链表+BST+平衡操作
完整代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 50;
int BST[MAXN] = {};
struct Node{
int data;
int lchild;
int rchild;
int height;
}node[MAXN];
int index = 0;
int n , counter = 1;
int getHeight(int root){ //获取高度
if(root == -1) return 0;
return node[root].height;
}
int getBalanceFactor(int root){ //获取平衡因子
return getHeight(node[root].lchild) - getHeight(node[root].rchild);
}
void updataHeight(int root){ //更新高度
node[root].height = max(getHeight(node[root].lchild) , getHeight(node[root].rchild)) + 1;
}
void Lrotation(int &root){ //左旋
int temp = node[root].rchild; //注意体会这一步对应线性链表的哪一步?
node[root].rchild = node[temp].lchild;
node[temp].lchild = root;
updataHeight(root);
updataHeight(temp);
root = temp; //更改树的结构
}
void Rrotation(int &root){ //右旋
int temp = node[root].lchild;
node[root].lchild = node[temp].rchild;
node[temp].rchild = root;
updataHeight(root);
updataHeight(temp);
root = temp;
}
//以上为新增操作,最好是分模块实现,因为这些模块会被独立反复调用
void rld(int root){
if(root == -1) return;
printf("%d", node[root].data);
if(counter < n) {
printf(" ");
counter++;
}
rld(node[root].lchild);
rld(node[root].rchild);
}
int newNode(int v){
node[index] = {v , -1 , -1 , 1};
return index++;
}
void insert(int &root , int data){
if(root == -1) { //查找失败,插入结点
root = newNode(data);
return;
}
if(node[root].data > data) {
insert(node[root].lchild , data);
updataHeight(root); //插入完成后更新高度(递归的归的体现)
if(getBalanceFactor(root) == 2) {//注意这里,如果是向左子树插入的,若不平衡则只有可能是左子树高与右子树,即平衡因子为2
if(getBalanceFactor(node[root].lchild) == 1) Rrotation(root);
else if(getBalanceFactor(node[root].lchild) == -1){
Lrotation(node[root].lchild);
Rrotation(root);
}
}
}
if(node[root].data < data){
insert(node[root].rchild , data);
updataHeight(root);
if(getBalanceFactor(root) == -2) {
if(getBalanceFactor(node[root].rchild) == -1) Lrotation(root);
else if(getBalanceFactor(node[root].rchild) == 1){
Rrotation(node[root].rchild);
Lrotation(root);
}
}
}
}
int create(){
int root = -1;
for(int i = 0; i <= n - 1; i++) insert(root , BST[i]);
return root;
}
int main(){
scanf("%d", &n);
for(int i = 0; i <= n - 1; i++) scanf("%d", BST + i);
rld(create());
}
备注
1.AVL树可以实现在插入时实现BST的平衡,如果需要在删除时实现BST的平衡,可以采用交替删除的方式,或者删除最高的子树前驱或后继;
2.注意和最开始学习的线性链表进行关联,例如引用的作用是什么?引用是为了更新树的结构,那具体是在哪里更新的?对应线性链表中的哪一步?还有int temp = node[root].rchild;这句话是为了干什么?为什么要开一个临时变量保存右子树结点地址?这又对应线性链表的哪一步?
3.引用所更改的就是根结点的父节点中对根结点的地址的记录,只有这样才算对树的结构做出了改变,当然还可以采用返回结点,在父节点的递归中更改的做法,这种做法是最原始的做法;
4.开临时变量保存子树结点地址的操作是为了防止因父节点子树地址改变而找不到原有的子结点的情况,这和线性链表的插入操作中为了防止“断链”而不能更改代码顺序一样,一般的数据结构书都只写明了不能更改顺序,其实是可以更改的,不过就是要加入临时变量保存下一个元素结点的地址。