从零开始的C++学习生活 12:AVL树全面解析

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

[1. AVL树的概念](#1. AVL树的概念)

[2. AVL树的实现](#2. AVL树的实现)

[2.1 AVL树的结构](#2.1 AVL树的结构)

[2.2 AVL树的插入](#2.2 AVL树的插入)

[2.2.1 AVL树插入值的大概过程](#2.2.1 AVL树插入值的大概过程)

[2.2.2 平衡因子更新](#2.2.2 平衡因子更新)

[2.2.3 插入结点及更新平衡因子的代码实现](#2.2.3 插入结点及更新平衡因子的代码实现)

[2.3 旋转](#2.3 旋转)

[2.3.1 旋转的原则](#2.3.1 旋转的原则)

[2.3.2 左单旋](#2.3.2 左单旋)

[2.3.3 右单旋](#2.3.3 右单旋)

[2.3.4 左右双旋](#2.3.4 左右双旋)

[2.3.5 右左双旋](#2.3.5 右左双旋)

[2.4 AVL树的查找](#2.4 AVL树的查找)

[2.5 AVL树平衡检测](#2.5 AVL树平衡检测)

[2.6 AVL树的删除](#2.6 AVL树的删除)

总结


上一篇:从零开始的C++学习生活 11:二叉搜索树全面解析-CSDN博客

前言

在前面,我们学习了二叉搜索树。二叉搜索树(BST)作为一种常见的数据结构,虽然提供了快速的查找、插入和删除操作,但在最坏情况下(如插入有序数据)会退化成链表,导致时间复杂度从理想的O(log N)恶化到O(N)。为了解决这个问题,两位前苏联科学家G. M. Adelson-Velsky和E. M. Landis在1962年的论文《An algorithm for the organization of information》中提出了AVL树------一种自平衡二叉搜索树。

AVL树通过严格控制树的平衡性,确保在任何情况下树的高度都保持在O(log N)级别,从而保证了所有操作的时间复杂度都为O(log N)。我将详细解析AVL树的原理、实现方法以及平衡维护机制。

1. AVL树的概念

AVL树是最先发明的自平衡二叉查找树

其具有以下特性:

它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。

AVL树实现这里我们引入一个平衡因子(balance factor)的概念,每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1

AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。

思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?因为很多情况无法保证高度差一定为0。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0。

AVL树整体结点数量和分布和完全二叉树类似,高度可以控制在 log⁡NlogN ,那么增删查改的效率也可以控制在 O(log⁡N)O(logN) ,相比二叉搜索树有了本质的提升。

2. AVL树的实现

2.1 AVL树的结构

一般AVL树我们使用前面将的key/value组合,而在这里我们使用一个叫pair的类封装key和value(pair为库文件中的类)。这是一个专门处理两个值映射关系的类

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf; // balance factor

    AVLTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _bf(0)
    {}
};

2.2 AVL树的插入

2.2.1 AVL树插入值的大概过程

  1. 插入一个值按二叉搜索树规则进行插入。

  2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了。

  3. 更新平衡因子过程中没有出现问题,则插入结束。

  4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。

2.2.2 平衡因子更新

更新原则:

  • 平衡因子=右子树高度-左子树高度

  • 只有子树高度变化才会影响当前结点平衡因子。

  • 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在parent的左子树,parent平衡因子--

  • parent所在子树的高度是否变化决定了是否会继续往上更新

我们插入3后,其父节点4,6,3,8都发生了平衡因子变化,而其余的节点不会变化,因为只有平衡因子等于右子树高度-左子树高度,只有子树高度变化才会引起平衡因子的变化

如果平衡因子变为-1或者1,说明是从0变过去的,也就是增加了高度,所以还得对上面进行调整

如果是0,说明是从-1或者1变过去的,就相当于原来一个节点只有一个孩子,现在补了一个,从-1或者1变成了0,那高度变化了吗?没有,所以不用向上调整

当为2或者-2时,树不平衡,需要进行旋转调整

2.2.3 插入结点及更新平衡因子的代码实现

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            return false;
        }
    }

    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;

    // 更新平衡因子
    while (parent)
    {
        if (cur == parent->_left)
            parent->_bf--;
        else
            parent->_bf++;

        if (parent->_bf == 0)
        {
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 不平衡了,旋转处理
            break;
        }
        else
        {
            assert(false);
        }
    }
    return true;
}

2.3 旋转

在上面平衡因子的更新中,我们发现有的节点已经不满足平衡因子的绝对值小于2的条件了,变成了2或者-2,因此我们需要对树进行一系列的操作,使得平衡因子满足规则

2.3.1 旋转的原则

  1. 保持搜索树的规则

  2. 让旋转的树从不满足变平衡,其次降低旋转树的高度

旋转总共分为四种:左单旋、右单旋、左右双旋、右左双旋。

2.3.2 左单旋

当某个节点的右子树比左子树高2,且右子树的右子树比左子树高时,需要进行左单旋。

平衡因子更新到2,那么一定是从1更新到2,即右子树的高度+1

在这里这种情况左单旋还无法解决,我们把3和1删掉,假设4是新插入的节点

我们拆分这个树单独查看

我们可以把左单旋看作往左边"压",因为3节点翘得太高了,以至于导致平衡因子失衡。所以把3给压下去,换6上来,同时让3指向6的左孩子4。不要忘记让8之前指向的是3,现在要改为6

左单旋代码实现:

cpp 复制代码
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    if(subRL)
        subRL->_parent = parent;

    Node* parentParent = parent->_parent;

    subR->_left = parent;
    parent->_parent = subR;

    if (parentParent == nullptr)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (parent == parentParent->_left)
        {
            parentParent->_left = subR;
        }
        else
        {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }
    parent->_bf = subR->_bf = 0;
}

2.3.3 右单旋

右单旋其实和左单旋十分相似,只是旋的方向相反而已

(左图4的平衡因子是0)

我们这次把6压下去,换4上来,然后6指向4的右孩子7,8指向4

右单旋代码实现:

cpp 复制代码
void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;

    Node* parentParent = parent->_parent;

    subL->_right = parent;
    parent->_parent = subL;

    if (parentParent == nullptr)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if (parent == parentParent->_left)
        {
            parentParent->_left = subL;
        }
        else
        {
            parentParent->_right = subL;
        }
        subL->_parent = parentParent;
    }
    parent->_bf = subL->_bf = 0;
}

2.3.4 左右双旋

我们先看下面两个树

看起来区别就是4的左右孩子不一样

然后6肯定是失衡了的,我们都进行右旋看看

我们会发现上面的情形进行右单旋后,4上去后平衡因子却成了2

因为右旋后6要指向5,6本来就下去了,6下面再接个5,不就造成了平衡因子再次失衡吗

所以发生这种情况的条件是,4的右子树高度高于左子树,即4的平衡因子是1

因为当平衡因子是1的时候,就会发生错误,所以我们只需要改变4的平衡因子即可,就是对4进行左单旋

让4和5是往左下斜,而不是往右下斜,这样再次进行右旋即可

cpp 复制代码
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(parent->_left);
    RotateR(parent);

    if (bf == 0)
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    }
    else if(bf == 1)
    {
        subL->_bf = -1;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

2.3.5 右左双旋

当某个节点的右子树比左子树高2,且右子树的左子树比右子树高时,需要进行右左双旋:先对右子树进行右旋,再对当前节点进行左旋。

与左右双旋的案例十分相似,我们不再做过多的讨论

cpp 复制代码
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;

    RotateR(parent->_right);
    RotateL(parent);

    if (bf == 0)
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == 1)
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = -1;
    }
    else if (bf == -1)
    {
        subR->_bf = 1;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

2.4 AVL树的查找

AVL树的查找与普通二叉搜索树相同,但由于AVL树是平衡的,查找效率始终为O(log N)。

cpp 复制代码
Node* Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first <= key)
        {
            cur = cur->_right;
        }
        else
        {
            cur = cur->_left;
        }
    }
    return nullptr;
}

2.5 AVL树平衡检测

为了验证实现的AVL树是否正确,我们可以通过检查左右子树高度差的程序进行反向验证,同时检查结点的平衡因子更新是否出现了问题。

cpp 复制代码
int Height(Node* root)
{
    if (root == nullptr)
        return 0;
    
    int leftHeight = Height(root->_left);
    int rightHeight = Height(root->_right);
    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

bool IsBalanceTree(Node* root)
{
    if (nullptr == root)
        return true;
    
    int leftHeight = Height(root->_left);
    int rightHeight = Height(root->_right);
    int diff = rightHeight - leftHeight;
    
    if (abs(diff) >= 2)
    {
        cout << root->_kv.first << "高度差异常" << endl;
        return false;
    }
    
    if (root->_bf != diff)
    {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }
    
    return IsBalanceTree(root->_left) && IsBalanceTree(root->_right);
}

2.6 AVL树的删除

删除操作我们可以进行与二叉搜索树类似的操作:

首先查找元素是否在二叉搜索树中,如果不存在,则返回false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

  1. 要删除结点N左右孩子均为空
  2. 要删除的结点N左孩子位空,右孩子结点不为空,那么删除节点的父节点则应该指向删除节点的右孩子
  3. 要删除的结点N右孩子位空,左孩子结点不为空,那么删除节点的父节点则应该指向删除节点的左孩子
  4. 要删除的结点N左右孩子结点均不为空

也就是替换法,替换完成后就完了吗?当然不是,对节点的删除必然会导致平衡因子的改变,因此我们需要调整

由于我们使用替代法,所以实际真删除的是被替代的那个节点,因此得从该节点的父节点开始调整

如果是0,说明从-1或者1调整过去,高度减少了1,需要向上调整

如果是-1或者1,最大高度不变,就相当于有两根绳子,一根不变,另一根少了一节,高度差从原来的0变成了-1或者1,最大长度变吗?不变,平衡因子看的是最大高度,所以不需要调整

如果是-2或者2,不平衡,直接旋转

cpp 复制代码
bool Erase(const K& key)
{
    // 空树情况
    if (_root == nullptr)
        return false;
    
    Node* parent = nullptr;
    Node* cur = _root;
    
    // 查找要删除的节点
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            // 找到要删除的节点
            break;
        }
    }
    
    // 没有找到要删除的节点
    if (cur == nullptr)
        return false;
    
    // 情况1:要删除的节点有两个子节点
    if (cur->_left && cur->_right)
    {
        // 找到右子树的最小节点(后继)
        Node* minParent = cur;
        Node* minRight = cur->_right;
        while (minRight->_left)
        {
            minParent = minRight;
            minRight = minRight->_left;
        }
        
        // 用后继节点的值替换当前节点的值
        cur->_kv = minRight->_kv;
        
        // 转换为删除后继节点(后继节点最多只有一个子节点)
        cur = minRight;
        parent = minParent;
    }
    
    // 情况2和3:要删除的节点是叶子节点或只有一个子节点
    Node* child = nullptr;
    if (cur->_left)
        child = cur->_left;
    else
        child = cur->_right;
    
    // 更新父节点指针
    if (child)
        child->_parent = parent;
    
    // 更新根节点或父节点的子节点指针
    if (parent == nullptr)
    {
        _root = child;
    }
    else
    {
        if (cur == parent->_left)
            parent->_left = child;
        else
            parent->_right = child;
        
        // 更新平衡因子
        UpdateBalanceFactorAfterErase(parent, child);
    }
    
    delete cur;
    return true;
}

void UpdateBalanceFactorAfterErase(Node* parent, Node* child)
{
    Node* cur = parent;
    
    while (cur)
    {
        // 更新平衡因子
        if (child == cur->_left)
            cur->_bf++;
        else
            cur->_bf--;
        
        // 根据平衡因子决定下一步操作
        if (cur->_bf == 1 || cur->_bf == -1)
        {
            // 高度不变,停止更新
            break;
        }
        else if (cur->_bf == 0)
        {
            // 高度变化,继续向上更新
            child = cur;
            cur = cur->_parent;
        }
        else if (cur->_bf == 2 || cur->_bf == -2)
        {
            // 需要旋转
            Node* higherChild = nullptr;
            if (cur->_bf == 2)
            {
                higherChild = cur->_right;
                if (higherChild->_bf == 0)
                {
                    RotateL(cur);
                    cur->_bf = 1;
                    higherChild->_bf = -1;
                    break;
                }
                else if (higherChild->_bf == 1)
                {
                    RotateL(cur);
                }
                else // higherChild->_bf == -1
                {
                    RotateRL(cur);
                }
            }
            else // cur->_bf == -2
            {
                higherChild = cur->_left;
                if (higherChild->_bf == 0)
                {
                    RotateR(cur);
                    cur->_bf = -1;
                    higherChild->_bf = 1;
                    break;
                }
                else if (higherChild->_bf == -1)
                {
                    RotateR(cur);
                }
                else // higherChild->_bf == 1
                {
                    RotateLR(cur);
                }
            }
            
            // 旋转后继续向上更新
            child = cur;
            cur = cur->_parent;
        }
    }
}

总结

AVL树通过引入平衡因子和旋转操作,确保了二叉搜索树在任何情况下都能保持近似平衡的状态,从而保证了所有操作的时间复杂度都为O(log N)。虽然维护平衡需要额外的开销,但在需要频繁查找的场景下,AVL树提供了稳定的高性能保证。

插入操作的特点

  • 插入后最多只需要一次旋转即可恢复平衡

  • 旋转后子树高度恢复原状,不会影响更高层节点

删除操作的特点

  • 删除后可能需要多次旋转

  • 旋转后子树高度可能变化,需要继续向上更新平衡因子

  • 实现比插入操作更复杂

AVL树的实现虽然相对复杂,但它为我们理解更高级的数据结构(如红黑树)奠定了基础。在实际应用中,我们可以根据具体需求选择是否使用AVL树------当查找操作远多于插入和删除操作时,AVL树是一个优秀的选择。

相关推荐
czy87874754 小时前
用C语言实现组合模式
c语言·组合模式
我是华为OD~HR~栗栗呀4 小时前
华为OD-23届-测试面经
java·前端·c++·python·华为od·华为·面试
我是华为OD~HR~栗栗呀4 小时前
华为od面经-23届-Java面经
java·c语言·c++·python·华为od·华为·面试
逐步前行7 小时前
C标准库--C99--布尔型<stdbool.h>
c语言·开发语言
SunnyKriSmile7 小时前
C语言译码操作
c语言·算法·if语句·译码操作·switch语句
傻童:CPU7 小时前
C语言需要掌握的基础知识点之线性表
c语言·1024程序员节
Heavy sea7 小时前
Linux串口应用编程
linux·c语言·1024程序员节
再睡一夏就好7 小时前
【C++闯关笔记】详解多态
c语言·c++·笔记·学习·语法·1024程序员节
与己斗其乐无穷7 小时前
C++学习记录(22)异常
学习·1024程序员节