平衡二叉树(AVL)

1. 背景

由于二叉排序树存在因插入顺序不合理导致“失衡”而退化成链表问题,于是有科学家提出了通过树高去调整二叉排序树出现“失衡”的情况(其实不止插入会导致“失衡”,删除也会导致“失衡”)。
ps:这里的失衡是指二叉排序树的左右子树高度差的绝对值大于1

2. 重点

1.平衡二叉树,本质上也是二叉排序树,所以拥有二叉排序树的所有性质。
2.平衡二叉树的学习重点,在于平衡条件以及平衡调整
总之,我们可以这样理解,在二叉排序树的基础上,再给二叉排序树定义一种性质(左右子树高度差的绝对值不大于1),然后我们在二叉排序树的插入或者删除的时候都去维护这个性质,这样二叉排序树就“平衡”,这时候,它就变成了平衡二叉树。
ps:数据结构,就是定义一种或多种性质,并去维护这些性质

3. 思考

高度同为H的二叉排序树和平衡二叉树,他们的节点数量分别在什么范围之内?
二叉排序树:
当退化成链表时,若二叉排序树的树高为H,则节点数量就为H;若二叉排序树是一棵满二叉树时,则节点数量为2H-1,所以我们得到高度为H的二叉排序树的节点数量范围为:H ⩽ SIZE(H) ⩽ 2H-1
平衡二叉树:
对于平衡二叉树,我们分析最少节点时,定义low(H)代表高度为H的平衡二叉树的最少节点数量,则根据左右子树高度差的绝对值小于等于1,则我们可以得到:low(H) = low(H-1) + low(H-2) + 1,这个等式的增长率大约为1.618H;最多节点时,一样是满二叉树的情况,最多节点数为:2H-1,所以高度为H的平衡二叉树的节点数量为:1.618H ⩽ SIZE(H) ⩽ 2H-1

4. 性质

1.平衡二叉树是二叉排序树
2.平衡二叉树的左右子树之间的高度差不超过1

5. 平衡调整策略

1.平衡调整发生在回溯阶段的第一个失衡节点处
2.理解平衡策略的关键在于:分析清楚四种情况下,ABCD四棵子树的树高关系
1).LL型,大右旋
2).LR型,先小左旋,再大右旋
3).RL型,先小右旋,再大左旋
4).RR型,大左旋
这里只是先提出调整策略,下面才开始解释。

5.1. 调整策略解释

在解释调整策略之前,我们必须明白,对于一个根节点,其高度若为H,其左子树高度若为HL,右子树高度为HR,则H = max(HL, HR) + 1。
为何平衡调整发生在回溯阶段的第一个失衡节点处?
由于我们是递归实现插入和删除,对于平衡二叉树,我们需要维护树高属性,对于插入和删除可能会影响到树高,所以每次插入和删除都需要去判断是否需要更新树高,当树高发生改变时,从插入和删除的节点开始,其祖先节点的树高都发生了改变,所以我们在回溯过程中需要更新树高,若某个祖先节点中,出现了左右子树高度差大于1的情况,就需要调整。
我们可以通过逻辑结构图来说明:
我们有以下平衡二叉树,照着这个二叉树,我们来分别分析插入和删除导致调整的情况:
图1
image
插入
在图1的二叉树基础上,我们插入一个节点18.
image
这时,由于新插入18,对于路径54->23->17->19->18,从插入18成功后,有一个回溯过程,回溯方向为18->19->17->23->54,在这个回溯过程中,我们都得更新这些节点的树高,并且更新到23时,发现对于23节点,其左子树的树高HL为3,右子树树高HR为1,|HL-HR|=2,这时候发现不平衡,所以得调整。
删除
还是从图1的二叉树基础上,我们删除91节点。
image
删除完节点91后,回溯过程为77->54,这时候77节点的树高被更新为1,而更新到54节点时,发现54左子树HL高度为3,而右子树高度HR为1,则时候左右子树高度差|HL-HR|=2,需要调整。
注意:这里,我们要特别清楚和记住,由于插入和删除都是递归操作,所以插入和删除有可能会影响其祖先节点的树高,我们都得先更新树高,再去判断更新树高后的节点的左右子树是否发生失衡(其左右子树高度差大于1)
这里,模拟一下平衡二叉树的插入时向下递归和向上回溯。
插入时向下递归
image
插入时向上回溯
image
对于回溯,中间涉及到的失衡调整过程中,节点位置和节点高度的变化过程,我们这里先不细说,这只是为了大概模拟向下递归和向上回溯的过程,让我们脑海里右一个大概的流程。

5.2. 4种调整情况分析

这里,我们先介绍一下最小失衡二叉树的概念,在平衡二叉树中插入或删除一个结点后,从该结点起向上寻找第一个不平衡的结点(左右子树高度差大于1),以确定该树是否失衡。若找到,则以该结点为根的子树称为最小失衡子树。
插入情况产生的最小失衡二叉树
image
删除情况产生的最小失衡二叉树
image

5.2.1 LL型

发生失衡时,最小失衡二叉树的左子树高度大于右子树高度,且左子树的左子树高度也同样大于左子树的右子树高度。如下图:
image
假设上图是一个AVL树的最小失衡二叉树,在根节点23发生失衡,其中左子树高度为3,大于右子树高度1,且23节点的左子树的左子树高度为2,大于23节点左子树的右子树高度1,这时候就是LL型,我们给出LL型的示例图,如下:
image
上图中,是LL型的最小失衡二叉树,其中A和B分别是最小失衡二叉树根节点左孩子节点的子树,C和D分别是最小失衡二叉树根节点右孩子节点的子树,我们这里来分析这四棵子树的高度之间的关系:
假设A、B、C、D的高度分别为HA、HB、HC、HD,K2、K3的高度为HK2、HK3,由于是LL型失衡,故HK3 + 2 = HK2,而HK2 = HA + 1,HA = HB + 1,HK3 = max(HC, HD) + 1,故我们推出HA = HB + 1 = max(HC, HD) + 2。
对于LL型失衡,我们从失衡的哪个节点开始,进行大右旋,就可以解决失衡,如图:
image
调整完之后,HA = HB + 1 = max(HC, HD) + 2这个等式依旧成立。但是这里需要注意的是,由于K1和K2节点的位置改变了,且K1节点变成了K2节点的子节点,所以我们需要在旋转完毕后先更新K1节点的高度,再更新K2节点的高度。

5.2.1 LR型

发生失衡时,最小失衡二叉树的左子树高度大于右子树高度,且左子树的右子树高度大于左子树的右子树高度。下面给出LR型的模型,如下图:
image
照例,我们来分析子树A、B、C、D之间高度的等式关系,由于是LR类型,所以对于K1节点来说,左子树高度和右子树高度的等式关系为:HK2 = HD + 2,且HK2 = max(HB, HC) + 2,HK3 = HA + 1,由上述等式,我们可以得出,HA = max(HB, HC) = HD
对于LR型失衡,我们先从失衡的节点的左节点进行一次小左旋,把它转化成LL型,再从失衡节点开始进行一次大右旋。如图
小左旋
image
进行小左旋之后,我们可以明显看到这时候是LL型,故再来一次大右旋。
大右旋
image
大右旋后,发现此时最小失衡二叉树已经平衡,且是一棵完美的平衡二叉树。

5.2.3 RR型

RR型的分析过程和LL型一样,只是旋转操作是镜像操作。

5.2.4 RL型

同理,RL型分析过程和LR一样,同样操作也是镜像操作。
至此,我们已经分析完失衡调整的4中情况,可以进行代码设计。

结构定义

由于平衡二叉树是通过调整树高去维护平衡,所以我们必须在原来二叉排序树的结构定义加上树高的属性,则平衡二叉树的结构定义如下:

struct Node {
	int key, h;
	Node *lchild, *rchild
};

节点初始化

Node *getNewNode(int key) {
    Node *p = new Node;
    p->key = key;
    p->h = 1;//由于插入时都是插入到叶子节点,所以高度为1
    p->lchild = p->rchild = NIL;
    return p;
}

p->lchild = p->rchild = NIL中的NIL是一个虚拟叶子节点,是为了方便计算树高而定义出来的。

NIL节点的定义

另外我们还定义了一个NIL节点,表示虚拟叶子节点,是接在真正的叶子节点下面,这里定义NIL节点是为了更新高度好处理。

Node __NIL;
#define NIL (&__NIL)
__attribute__((constructor))
void init_NIL(void) {
    NIL->key = -1;
    NIL->lchild = NIL->rchild = NIL;
    NIL->h = 0;
    return ;
}

这里,我们给出未带NIL节点和带NIL节点的图示
image

树高计算

void update_height(Node *root) {
    root->h = max(root->lchild->h, root->rchild->h) + 1;
}

在树高计算函数里,如果root参数是真实的叶子节点,且我们没有用定义NIL节点,则叶子节点的左右孩子节点都是NULL,这时候我们就不能通过访问孩子节点的高度来更新父节点的高度,除非做判空操作,非空才用子节点的树高更新父节点的树高。

节点插入

插入节点的操作更二叉排序树插入节点的操作相似,只是我们要多维护更新树高和失衡调整的操作,即在维护二叉排序树性质的同时,还要去维护平衡二叉树的性质。

Node *insert(Node *root, int key) {
    if (root == NIL) {
        return getNewNode(key);
    }
    if (key == root->key) {
        return root;
    }
    if (key < root->key) {
        root->lchild = insert(root->lchild, key);
    } else {
        root->rchild = insert(root->rchild, key);
    }
    update_height(root);//插入完毕后,更新树高
    return maintain(root);//更新树高完毕后,才能去进行失衡调整的判断和失衡调整
}

节点删除

节点删除也是同理,在维护二叉树的性质的同时要维护平衡二叉树的性质。

Node *erase(Node *root, int key) {
    if (root == NIL) {
        return NIL;
    }
    if (key < root->key) {
        root->lchild = erase(root->lchild, key);
    } else if (key > root->key) {
        root->rchild = erase(root->rchild, key);
    } else {
        if (root->lchild == NIL && root->rchild == NIL) {
            delete root;
            return NIL;
        } else if (root->lchild == NIL || root->rchild == NIL) {
            Node *temp = root->lchild == NIL ? root->rchild : root->lchild;
            delete root;
            return temp;
        } else {
            Node *temp = predecessor(root);
            root->key = temp->key;
            root->lchild = erase(root->lchild, temp->key);
        }
    }
    update_height(root);
    return maintain(root);
}

失衡调整

失衡调整函数主要分为两大部分:左右子树高度差判断,类型判断和处理。
1.左右子树高度差判断:左右子树高度差小于2,则新插入节点后,还是平衡二叉树还是平衡状态,无需调整;左右子树高度差大于1,进行类型判断,并根据相应的类型进行调整处理。
2.类型判断:
LL型 大右旋
LR型 小左旋 大右旋
RL型 小右旋 大左旋
RR型 大左旋

Node *maintain(Node *root) {
	//根据高度差判断是否需要失衡跳帧
    if (abs(root->lchild->h - root->rchild->h) < 2) {
        return root;
    }
	//L型入口
    if (root->lchild->h > root->rchild->h) {
		//LR型入口
        if (root->lchild->rchild->h > root->lchild->lchild->h) {
			//小左旋
            root->lchild = left_rotate(root->lchild);
        }
		//LL LR型都需要大右旋
        root = right_rotate(root);
    }
	//R型入口
    if (root->rchild->h > root->lchild->h) {
		//RL型入口
        if (root->rchild->lchild->h > root->rchild->rchild->h) {
			//小右旋
            root->rchild = right_rotate(root->rchild);
        }
		//RR RL型都需要打左旋
        root = left_rotate(root);
    }
    return root;
}

旋转操作

左右旋操作都很简单,自己可以画图模拟,唯一需要注意的是旋转后要更新发生移动的节点的树高:
1.对于左旋,旋转后原根节点变成了其右孩子的左节点,故我们需要更新原根节点(新根节点的左孩子)高度,再去更新新根节点的高度。
2.对于左旋,旋转后原根节点变成了其左孩子的左节点,故我们需要更新原根节点(新根节点的右孩子)高度,再去更新新根节点的高度。

左旋操作

Node *left_rotate(Node *root) {
    Node *temp = root->rchild;
    root->rchild = temp->lchild;
    temp->lchild = root;
    update_height(root);
    update_height(temp);
    return temp;
}

右旋操作

Node *right_rotate(Node *root) {
    Node *temp = root->lchild;
    root->lchild = temp->rchild;
    temp->rchild = root;
    update_height(root);
    update_height(temp);
    return temp;
}

输出函数

这里我们采用先序遍历,但是修改输出函数的输出格式,先输出根节点的key值,在输出更节点的高度,在输出左右子树的key值。

void output(Node *root) {
    if (root == NIL) {
        return ;
    }
    printf("(%d(%d) | %d, %d)
", root->key, root->h,
           root->lchild->key, root->rchild->key);
    output(root->lchild);
    output(root->rchild);
    return ;
}

完整代码

完整的代码我加上了调试信息,方便我们理解代码的失衡调整过程。

/*************************************************************************
        > File Name: my_avl_tree.cpp
        > Author: ydqun
        > Mail: qq28*****5@163.com
        > Created Time: Wed 21 Apr 2021 05:41:39 PM CST
 ************************************************************************/
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>

using namespace std;

struct Node {
    int key, h;
    Node *lchild, *rchild;
};

Node __NIL;
#define NIL (&__NIL)
__attribute__((constructor))
void init_NIL(void) {
    NIL->key = -1;
    NIL->lchild = NIL->rchild = NIL;
    NIL->h = 0;
    return ;
}

const char *maintain_str[] = {"", "LL", "LR", "RL", "RR"};

Node *getNewNode(int key) {
    Node *p = new Node;
    p->key = key;
    p->h = 1;//由于插入时都是插入到叶子节点,所以高度为1
    p->lchild = p->rchild = NIL;
    return p;
}

void update_height(Node *root) {
    root->h = max(root->lchild->h, root->rchild->h) + 1;
}

Node *left_rotate(Node *root) {
    printf("left_rotate: %d
", root->key);
    Node *temp = root->rchild;
    root->rchild = temp->lchild;
    temp->lchild = root;
    update_height(root);
    update_height(temp);
    return temp;
}

Node *right_rotate(Node *root) {
    printf("right rotate: %d
", root->key);
    Node *temp = root->lchild;
    root->lchild = temp->rchild;
    temp->rchild = root;
    update_height(root);
    update_height(temp);
    return temp;
}

Node *maintain(Node *root) {
    if (abs(root->lchild->h - root->rchild->h) < 2) {
        return root;
    }
    int maintain_type;
    if (root->lchild->h > root->rchild->h) {
        if (root->lchild->rchild->h > root->lchild->lchild->h) {
            root->lchild = left_rotate(root->lchild);
            maintain_type = 2;
        } else {
            maintain_type = 1;
        }
        root = right_rotate(root);
    }
    if (root->rchild->h > root->lchild->h) {
        if (root->rchild->lchild->h > root->rchild->rchild->h) {
            root->rchild = right_rotate(root->rchild);
            maintain_type = 3;
        } else {
            maintain_type = 4;
        }
        root = left_rotate(root);
    }
    printf("AVL Maintain: %s
", maintain_str[maintain_type]);
    return root;
}


Node *insert(Node *root, int key) {
    if (root == NIL) {
        return getNewNode(key);
    }
    if (key == root->key) {
        return root;
    }
    if (key < root->key) {
        root->lchild = insert(root->lchild, key);
    } else {
        root->rchild = insert(root->rchild, key);
    }
    update_height(root);
    return maintain(root);
}

Node *predecessor(Node *root) {
    Node *temp = root->lchild;
    while (temp->rchild != NIL) {
        temp = temp->rchild;
    }
    return temp;
}

Node *erase(Node *root, int key) {
    if (root == NIL) {
        return NIL;
    }
    if (key < root->key) {
        root->lchild = erase(root->lchild, key);
    } else if (key > root->key) {
        root->rchild = erase(root->rchild, key);
    } else {
        if (root->lchild == NIL && root->rchild == NIL) {
            delete root;
            return NIL;
        } else if (root->lchild == NIL || root->rchild == NIL) {
            Node *temp = root->lchild == NIL ? root->rchild : root->lchild;
            delete root;
            return temp;
        } else {
            Node *temp = predecessor(root);
            root->key = temp->key;
            root->lchild = erase(root->lchild, temp->key);
        }
    }
    update_height(root);
    return maintain(root);
}

void output(Node *root) {
    if (root == NIL) {
        return ;
    }
    printf("(%d(%d) | %d, %d)
", root->key, root->h,
           root->lchild->key, root->rchild->key);
    output(root->lchild);
    output(root->rchild);
    return ;
}


void clear(Node *root) {
    if (root == NIL) {
        return ;
    }
    clear(root->lchild);
    clear(root->rchild);
    delete root;
    return ;
}

void menu(void) {
    cout << "Please enter the operation: " << endl;
    printf("Please enter the operation: 
");
    printf("Press 1 to insert a node to avl tree
");
    printf("Press 2 to delete a node to avl tree
");
    printf("Press CTRL + D to quit!
");
}

int main() {
    int op, val;
    Node *root = NIL;
    while (scanf("%d", &op) != EOF) {
        if (op != 1 && op != 2) {
            printf("Please enter the right operation!
");
            continue;
        }
        printf("Enter the key you want to %s:
", op == 1 ? "insert" : "delete");
        scanf("%d", &val);
        switch (op) {
            case 1:
                root = insert(root, val);
            break;
            case 2:
                root = erase(root, val);
            break;
            default:
            break;
        }
        output(root);
        cout << endl;
    }
    return 0;
}

测试

ydqun@VM-0-9-ubuntu avl_tree % ./a.out                                                                                                                                                      [127]

1
Enter the key you want to insert:
56
(56(1) | -1, -1)

1
Enter the key you want to insert:
30
(56(2) | 30, -1)
(30(1) | -1, -1)

1
Enter the key you want to insert:
15
right rotate: 56
AVL Maintain: LL
(30(2) | 15, 56)
(15(1) | -1, -1)
(56(1) | -1, -1)

1
Enter the key you want to insert:
32
(30(3) | 15, 56)
(15(1) | -1, -1)
(56(2) | 32, -1)
(32(1) | -1, -1)

1
Enter the key you want to insert:
33
left_rotate: 32
right rotate: 56
AVL Maintain: LR
(30(3) | 15, 33)
(15(1) | -1, -1)
(33(2) | 32, 56)
(32(1) | -1, -1)
(56(1) | -1, -1)

2
Enter the key you want to delete:
30
left_rotate: 15
AVL Maintain: RR
(33(3) | 15, 56)
(15(2) | -1, 32)
(32(1) | -1, -1)
(56(1) | -1, -1)

1
Enter the key you want to insert:
78
(33(3) | 15, 56)
(15(2) | -1, 32)
(32(1) | -1, -1)
(56(2) | -1, 78)
(78(1) | -1, -1)

原创博文,转发请加上原链接

原文地址:https://www.cnblogs.com/ydqblogs/p/14680857.html