AVL树

AVL树是一种自平衡二叉搜索树,确保任意节点的两个子树高度差不超过1,从而保证操作效率在O(logn)。文章介绍了AVL树的概念、平衡因子、旋转操作(左旋、右旋)以及四种不平衡情况的调整策略,包括LL、RR、LR、RL。此外,还概述了AVL树的插入、删除和查找操作,并讨论了如何处理相同值的情况。

AVL树

普通的二叉搜索树的退化

在使用二叉搜索树时,只规定了左子树的值小于根节点的值,右子树的值大于根节点的值,并没有对树的形状做出要求。

在树较为平衡(左右子树高度相差较小)时,如下图所示:
平衡时
我们在二叉搜索树上进行的增加、删除、查找的操作的时间复杂度平均为 O ( l o g   n ) O(log\,n) O(logn)

但是在树比较倾斜时,在二叉搜索树上的操作的时间复杂度也会随之增加,树越倾斜,退化也就越严重,时间复杂度也就越高。

一种极端情况下,当所有的节点(除叶节点外)都只有一个子树或子节点时,该树结构会退化为链表,相应的时间复杂度也会退化为 O ( n ) O(n) O(n)
不平衡时

为了解决这个问题,平衡树也就应运而生。其中最早的平衡树便是本文的重点——AVL树


什么是AVL树

AVL树是最早被发明的自平衡二叉搜索树。它的名字不是自平衡二叉搜索树的缩写,而是来源于它的发明者G. M. Adelson-VelskyEvgenii Landis

AVL树在二叉搜索树的性质上增加了一条——任一节点的两个子树高度差最大为1。因此,AVL树在执行增加、删除、查找操作的时间复杂度在平均、最坏情况下均为 O ( l o g   n ) O(log\,n) O(logn)。不会出现二叉搜索树中退化的情况。


平衡因子

AVL树中使用这个参数来判定当前节点是否是平衡的。平衡因子由当前节点的左子树高度减去右子树高度得到。

所以当一个节点的平衡因子大于1或小于-1时,则可以判定当前节点不平衡,需要进行调整。

另外,AVL树中也可以不保存节点的平衡因子,而保存树的高度,在需要时计算得到。


旋转

旋转是一种改变树的平衡性的操作,几乎所有的平衡树都是通过旋转操作来调整树的结构,使树保持平衡状态。

旋转分为两类:

  • 左旋转
  • 右旋转

左旋转

总共3步:

  • 将根节点的右子节点设置为右子节点的左子节点。
  • 将右子节点的左子节点设置为根节点。
  • 将右子节点设置为根节点。

相应的代码为:

AVLTreeNode* RotateL(AVLTreeNode* root) {
    AVLTreeNode* temp = root->right;
    root->right = temp->left;
    temp->left = root;

    return temp;
}

旋转前后对比:
左旋转

从图中可以看出,左旋转之后,相比于原来的树,左子树高度增加了1,右子树高度减少了1。

右旋转

类似于左旋转,也为三步:

  • 将根节点的左子节点设置为左子节点的右子节点。
  • 将左子节点的右子节点设置为根节点。
  • 将左子节点设置为根节点。

相应的代码为:

AVLTreeNode* RotateR(AVLTreeNode* root) {
    AVLTreeNode* temp = root->left;
    root->left = temp->right;
    temp->right = root;

    return temp;
}

旋转前后对比:
右旋转

从图中可以看出,右旋转之后,相比于原来的树,右子树高度增加了1,左子树高度减少了1。


AVL树中不平衡情况及对应旋转操作

在操作AVL树过程中,总共会遇到4种不平衡的情况:LLRRLRRL

LL情况——右旋转

LL情况是指根节点的子节点的子树高度比右子树高度大2的情况。即:
LL
图中A节点为根节点。

进行一次右旋转即可使树重新平衡。

RR情况——左旋转

RR情况是指根节点的子节点的子树高度比左子树高度大2的情况。即:
RR
图中A节点为根节点。

进行一次左旋转即可使树重新平衡。

LR情况——先左旋转,再右旋转

LR情况是指根节点的子节点的子树比左子树高度大2的情况。即:
LR
图中A节点为根节点。

此时需进行两次旋转:

  • 先以左子节点(B)为根进行一次左旋转。(将情况转化为LL情况)
  • 再以A节点为根进行一次右旋转

RL情况——先右旋转,再左旋转

RL情况是指根节点的子节点的子树比右子树高度大2的情况。即:
RL
图中A节点为根节点。

此时需进行两次旋转:

  • 先以右子节点(B)为根进行一次右旋转。(将情况转化为RR情况)
  • 再以A节点为根进行一次左旋转

AVL树的操作

在AVL树上操作主要有三种:增加删除查找

三种操作与二叉搜索树上对应的三种操作基本相同,只是增加、删除操作有可能会引起树的不平衡,需要进行额外调整。

为便于说明,此处先对AVL树结构进行说明:

class AVLTree {
  private:
    class AVLTreeNode {
      public:
        int val;              /// 当前节点所存的数值
        int height;           /// 当前节点的高度
        AVLTreeNode* left;    /// 左子树
        AVLTreeNode* right;   /// 右子树

        // 得到root节点的高度
        static int Height(AVLTreeNode* root);

        AVLTreeNode(int val = 0);
        ~AVLTreeNode();
    };

    // 内部树结构
    AVLTreeNode* root;

    // 计算root节点的高度
    void CalcHeight(AVLTreeNode* root);

    // 取得以root为根的树上的最大值节点
    AVLTreeNode* FindMax(AVLTreeNode* root);

    // 取得以root为根的树上的最小值节点
    AVLTreeNode* FindMin(AVLTreeNode* root);

    // LL情况,以root为根进行右旋转
    AVLTreeNode* RotateLL(AVLTreeNode* root);
    // LR情况,先以root节点左儿子为根进行左旋转,再以root为根进行右旋转
    AVLTreeNode* RotateLR(AVLTreeNode* root);
    // RR情况,以root为根进行左旋转
    AVLTreeNode* RotateRR(AVLTreeNode* root);
    // RL情况,先以root节点右儿子为根进行右旋转,再以root为根进行左旋转
    AVLTreeNode* RotateRL(AVLTreeNode* root);

    // 向AVL树内插入值为val的一个节点,返回插入该节点后,整个树的根节点
    // result返回插入结果,成功插入则返回1,如果该值已经存在则返回0
    AVLTreeNode* Insert(AVLTreeNode* root, int val, int& result);

    // 在AVL树中删除值为val的节点,如果不存在则跳过,返回删除该节点后,整个树的根节点
    // result返回删除结果,成功删除则返回1,如果该值不存在则返回0
    AVLTreeNode* Remove(AVLTreeNode* root, int val, int& result);

    // 在AVLTree销毁时,递归释放整颗树
    void DeleteRoot(AVLTreeNode* root);

    // 中序遍历整颗树
    void Print(AVLTreeNode* root);

  public:
    AVLTree();
    ~AVLTree();

    // 向AVL树内插入值为val的一个节点,若成功插入则返回1,若已存在些节点则返回0
    int Insert(int val);
    // 在AVL树中删除值为val的节点,若成功删除则返回1,若该节点不存在则返回0
    int Remove(int val);
    // 在AVL树中查找值为val的节点,该节点存在则返回1,否则返回0
    int Find(int val);

    // 中序遍历整颗树
    void Print();
};

其中两个辅助函数具体实现为:

int AVLTree::AVLTreeNode::Height(AVLTreeNode* root) {
    // 返回当前节点的高度,若为空则返回-1
    return root ? root->height : -1;
}
void AVLTree::CalcHeight(AVLTreeNode* root) {
    // 当前节点的高度为左、右子树最高者高度 + 1
    root->height = std::max(AVLTree::AVLTreeNode::Height(root->left),
                            AVLTree::AVLTreeNode::Height(root->right)) +
                   1;
}

插入

主要分4步:

  • 寻找到插入的位置。(如果值已经存在则插入失败)
  • 创建节点并插入。
  • 如果插入操作引起了不平衡,进行调整。
  • 回溯时计算出沿插入路径上所有节点的新高度。

插入操作函数为:

AVLTree::AVLTreeNode* AVLTree::Insert(AVLTreeNode* root, int val, int& result) {
    result = 0;
    // 如果当前节点为空,则返回一个新建的节点
    if (!root) {
        root = new AVLTreeNode(val);
        result = 1;
        return root;
    }

    // 当前节点值比要插入的值大,则应向左子树进行插入
    if (root->val > val) {
        // 向左子树插入值为val的节点
        root->left = Insert(root->left, val, result);

        // 如果在向左子树插入节点后,引起了树的不平衡(即左子树比右子树高2个单位),则需进行旋转调整
        // 因为是向左子树插入,所以左子树的高度必定大于等于右子树的高度
        if (AVLTreeNode::Height(root->left) -
                AVLTreeNode::Height(root->right) ==
            2) {
            // 如果是插入的左子节点的左子树,则对应LL情况,只需进行右单旋转即可
            if (val < root->left->val) root = RotateLL(root);
            // 否则为LR情况,需进行两次旋转
            else
                root = RotateLR(root);
        }
    }
    // 当前节点值比要插入的值小,则应向右子树进行插入
    else if (val > root->val) {
        // 向左子树插入值为val的节点
        root->right = Insert(root->right, val, result);

        // 如果在向右子树插入节点后,引起了树的不平衡(即右子树比左子树高2个单位),则需进行旋转调整
        // 因为是向右子树插入,所以右子树的高度必定大于等于左子树的高度
        if (AVLTreeNode::Height(root->right) -
                AVLTreeNode::Height(root->left) ==
            2) {
            // 如果是插入的右子树的右子树,则对应RR情况,只需进行左单旋转即可
            if (val > root->right->val) root = RotateRR(root);
            // 否则为RL情况,需进行两次旋转
            else
                root = RotateRL(root);
        }
    }

    // 回溯时计算出沿插入路径上所有节点的新高度
    CalcHeight(root);
    return root;
}

由于高度是通过回溯进行更新的,所以当插入完成后,以root为根的左右子树的高度均以更新完成,只需要用左右子树新的高度差即可判断出是否不平衡。

如果不平衡,通过插入值valroot节点值的相对大小即可判断出相应的不平衡的类型。进行对应的操作即可。

删除

与插入类似,也分为4步:

  • 寻找到要删除的节点的位置。(如果值不存在则删除失败)
  • 如果对应节点只有一个子节点,直接删除;若有两个子节点,则需找出对应的替代节点之后,再进行删除。
  • 如果删除操作引起了不平衡,进行调整。
  • 回溯时计算出沿删除路径上所有节点的新高度。

具体实现如下:

AVLTree::AVLTreeNode* AVLTree::Remove(AVLTreeNode* root, int val, int& result) {
    result = 0;
    if (!root) {
        return nullptr;
    }
    // 待删除节点位于右子树
    if (root->val < val) {
        // 在右子树中删除节点
        root->right = Remove(root->right, val, result);
        // 如果删除后引起了树的不平衡(即左子树的高度与右子树的高度差为2),则需要进行旋转调整
        if (AVLTreeNode::Height(root->left) -
                AVLTreeNode::Height(root->right) ==
            2) {
            // 如果右儿子的左子树的高度小于右儿子的右子树的高度,
            // 则删除的节点位于右儿子的左子树上,不平衡情况对应RR,
            // 进行左旋转即可
            if (AVLTreeNode::Height(root->right->left) <
                AVLTreeNode::Height(root->right->right))
                root = RotateRR(root);
            // 否则删除的节点位于右儿子的右子树上,不平衡情况对应RL,
            // 需进行两次旋转
            else
                root = RotateRL(root);
        }
    }
    // 待删除节点位于左子树
    else if (root->val > val) {
        // 在左子树中删除节点
        root->left = Remove(root->left, val, result);

        // 如果删除后引起了树的不平衡(即右子树的高度与左子树的高度差为2),则需要进行旋转调整
        if (AVLTreeNode::Height(root->right) -
                AVLTreeNode::Height(root->left) ==
            2) {
            // 如果左儿子的右子树的高度小于左儿子的左子树的高度,
            // 则删除的节点位于左儿子的右子树上,不平衡情况对应LL,
            // 进行右旋转即可
            if (AVLTreeNode::Height(root->left->right) <
                AVLTreeNode::Height(root->left->left))
                root = RotateLL(root);
            // 否则删除的节点位于左儿子的左子树上,不平衡情况对应LR,
            // 需进行两次旋转
            else
                root = RotateLR(root);
        }
    }
    // 否则应当删除当前节点
    else {
        result = 1;
        int t;
        // 当前节点有两个子节点,则从中选取高度大的一个子树进行替代,避免引起树的不平衡
        if (root->left && root->right) {
            // 左子树高度大于右子树时,选取左子树中最大值替代当前节点,并删除原最大值节点
            if (AVLTreeNode::Height(root->left) >
                AVLTreeNode::Height(root->right)) {
                AVLTreeNode* temp = FindMax(root->left);
                root->val = temp->val;
                root->left = Remove(root->left, temp->val, t);
            }
            // 否则,选取右子树中最小值替代当前节点,并删除原最小值节点
            else {
                AVLTreeNode* temp = FindMin(root->right);
                root->val = temp->val;
                root->right = Remove(root->right, temp->val, t);
            }
        }
        // 如果当前节点只有一个子节点或没有子节点,直接删除即可
        else {
            AVLTreeNode* temp = root;
            root = root->left ? root->left : root->right;
            delete temp;
        }
    }

    // 如果返回节点不为空时,回溯地计算出删除路径上所有节点的新高度
    if (root) CalcHeight(root);
    return root;
}

其中两个辅助函数实现如下:

AVLTree::AVLTreeNode* AVLTree::FindMax(AVLTreeNode* root) {
    if (!root) return nullptr;
    AVLTreeNode* temp = root;
    while (temp->right) temp = temp->right;
    return temp;
}
AVLTree::AVLTreeNode* AVLTree::FindMin(AVLTreeNode* root) {
    if (!root) return nullptr;
    AVLTreeNode* temp = root;
    while (temp->left) temp = temp->left;
    return temp;
}

与插入类似,通过高度差判断是否不平衡,通过相对值大小判断具体不平衡类型。

查找

与二叉搜索树查找相同,不再赘述。


相同值的处理

上述结构仅支持序列中没有相同值的情况,如果存在一组有相同值的序列,并且想将相同的值都存储下来,则需要对此结构进行一些修改。

一种办法是在AVLTreeNode结构中添加一个indexs属性用于存储对应值在原序列中的位置,如果插入时值相同,则向indexs中添加对应的位置编号。


附注

文中完整代码请参考:https://github.com/anscor/BlogCode/tree/master/AVLTree

另附两个数据结构的可视化网站,在进行较为抽象的数据结构与算法学习时,可以用于直观地检验自己编写代码的正确性:


参考

  1. https://zh.wikipedia.org/wiki/AVL树
  2. https://gaomf.cn/2017/02/11/Data_Structure_AVLTree/
07-07
AVL是一种自平衡的二叉查找,它确保了的高度始终保持在对数级别,从而保证了查找、插入和删除操作的时间复杂度为O(log n)。这种数据结构以它的发明者G.M. Adelson-Velsky和E.M. Landis的名字命名[^1]。 ### AVL的定义 - **平衡因子**:每个节点都有一个平衡因子,它是该节点左子树的高度减去右子的高度。对于AVL来说,所有节点的平衡因子只能是-1, 0或1。 - **空**:如果T是空,则它自然是一个AVL。 - **非空**:若T不是空,则其左右子TL和TR都必须是AVL,并且对于任意节点,|HL - HR| ≤ 1(其中HL和HR分别表示左子树和右子的高度)。 ### AVL的操作 当进行插入或删除操作时,可能会破坏AVL的平衡性,这时需要通过旋转来重新恢复平衡: - **单旋转**: - 左单旋(Single Rotate with Left)用于处理左孩子的左子树过高。 - 右单旋(Single Rotate with Right)用于处理右孩子的右子过高。 - **双旋转**: - 左右双旋(Double Rotate with Left)用于处理左孩子的右子过高的情况。 - 右左双旋(Double Rotate with Right)用于处理右孩子的左子树过高的情况。 这些旋转操作可以保持AVL的性质不变,并且能够快速地调整的结构以维持平衡。 ### AVL的实现 下面是一个简单的C语言代码示例,展示了如何定义AVL节点以及实现基本的插入操作。这个例子中包含了必要的函数声明和一些核心逻辑。 ```c #include <stdio.h> #include <stdlib.h> // 定义AVL节点结构体 typedef struct AvlNode { int element; struct AvlNode *left; struct AvlNode *right; int height; // 节点的高度 } *AvlTree, *Position; // 获取两个整数中的较大者 int max(int a, int b) { return (a > b) ? a : b; } // 计算给定节点的高度 static int Height(AvlTree T) { if (T == NULL) return -1; // 空节点高度为-1 else return T->height; } // 单旋转 - 左左情况 AvlTree singlerotatewithLeft(AvlTree K2) { Position K1 = K2->left; K2->left = K1->right; K1->right = K2; // 更新节点高度 K2->height = max(Height(K2->left), Height(K2->right)) + 1; K1->height = max(Height(K1->left), K2->height) + 1; return K1; // 新根 } // 单旋转 - 右右情况 AvlTree singlerotatewithRight(AvlTree K2) { Position K1 = K2->right; K2->right = K1->left; K1->left = K2; // 更新节点高度 K2->height = max(Height(K2->left), Height(K2->right)) + 1; K1->height = max(Height(K1->right), K2->height) + 1; return K1; // 新根 } // 双旋转 - 左右情况 AvlTree doublerotatewithLeft(AvlTree K3) { K3->left = singlerotatewithRight(K3->left); return singlerotatewithLeft(K3); } // 双旋转 - 右左情况 AvlTree doublerotatewithRight(AvlTree K3) { K3->right = singlerotatewithLeft(K3->right); return singlerotatewithRight(K3); } // 插入新元素到AVLAvlTree Insert(int x, AvlTree T) { if (T == NULL) { // 如果为空,创建新节点 T = (AvlTree)malloc(sizeof(struct AvlNode)); if (T == NULL) printf("Out of space!!!"); else { T->element = x; T->left = T->right = NULL; T->height = 0; // 新叶节点高度为0 } } else if (x < T->element) { // 向左子树插入 T->left = Insert(x, T->left); // 检查并修复平衡 if (Height(T->left) - Height(T->right) == 2) { if (x < T->left->element) T = singlerotatewithLeft(T); // 左左旋转 else T = doublerotatewithLeft(T); // 左右旋转 } } else if (x > T->element) { // 向右子插入 T->right = Insert(x, T->right); // 检查并修复平衡 if (Height(T->right) - Height(T->left) == 2) { if (x > T->right->element) T = singlerotatewithRight(T); // 右右旋转 else T = doublerotatewithRight(T); // 右左旋转 } } // 更新高度 T->height = max(Height(T->left), Height(T->right)) + 1; return T; } ``` 上述代码提供了AVL的基本框架,包括节点定义、插入操作及必要的旋转方法。实际应用中可能还需要添加更多的功能,如删除节点、查找特定值等。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值