【C++】22.AVL树实现

news2025/1/11 14:00:31

文章目录

  • 1. AVL的概念
    • AVL树的逻辑理解
    • 四种形式
      • 1. LL型(左孩子的左子树上出现节点使结点失衡)
      • 2. RR型(右孩子的右子树上出现节点使结点失衡)
      • 3. LR型(左孩子的右子树上出现节点使结点失衡)
      • 4. RL型(右孩子的左子树上出现节点使结点失衡)
    • 总结
    • 注意事项:
  • 2. AVL树的实现
    • 2.1 AVL树的结构
    • 2.2 AVL树的插入
      • 2.2.1 AVL树插入一个值的大概过程
      • 2.2.2 平衡因子更新
      • 2.2.3 插入结点及更新平衡因子的代码实现
    • 2.3 旋转
      • 2.3.1 旋转的原则
      • 2.3.2 右单旋
      • 2.3.3 右单旋代码实现
      • 2.3.4 左单旋
      • 2.3.5 左单旋代码实现
      • 2.3.6 左右双旋
      • 2.3.7 左右双旋代码实现
      • 2.3.8 右左双旋
      • 2.3.9 右左双旋代码实现
    • 2.4 AVL树的查找
    • 2.5 AVL树平衡检测
    • 2.6 AVL树的删除
    • 2.7 AVL树的代码实现


1. AVL的概念

  • AVL树是最先发明的自平衡二叉查找树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。

  • AVL树得名于它的发明者G. M. Adelson-VelskyE. M. Landis是两个前苏联的科学家,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

  • AVL树实现这里我们引入一个平衡因子的概念,每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。

  • 思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0

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

0505ea8f97d640c51d9606f0cf5a7279

8ac7e13830f65fa5bdcc2e20126e822f


AVL树的逻辑理解

7fb97519f7bc0c14e2f7b75016b7a31a

此处的平衡因子=左子树高度-右子树高度

左旋:

da6a78f09ab9f66b78766e6ec6110545

右旋:

3fd53823585eb81df10b36b0bf9ec222


四种形式

1. LL型(左孩子的左子树上出现节点使结点失衡)

右旋:冲突的右孩变左孩

5e58417a5c9aecdc1663d1cb4e3b3886


2. RR型(右孩子的右子树上出现节点使结点失衡)

左旋:冲突的左孩变右孩

aa4d4e0705a5358dc2ddc8827e03a871


3. LR型(左孩子的右子树上出现节点使结点失衡)

先左旋左孩子,然后右旋

1ddf3cb76ba50ae829ea25613f525bcf


4. RL型(右孩子的左子树上出现节点使结点失衡)

先右旋右孩子,然后左旋

b620235645017d293b7f8bb5faabd216


总结

LL型RR型LR型RL型
右旋左旋左旋左孩子,然后右旋右旋右孩子,然后左旋
失衡节点:平衡因子=-2失衡节点:平衡因子=2失衡节点:平衡因子=-2失衡节点:平衡因子=2
失衡节点左孩子:平衡因子=-1失衡节点右孩子:平衡因子=1失衡节点左孩子:平衡因子=1失衡节点右孩子:平衡因子=-1

6f8415490a0e3560dc765692a50fb65a


注意事项:

同时两个结点失衡只需要调节距离插入节点最近的一个结点就可以了

136caf31b9b42ea722925bcac1efc6b1

插入结点后如果导致多个祖先结点失衡,只需调整距离插入结点最近的失衡结点,其他失衡结点会自然平衡。

AVL树的插入和二叉排序树一样,只是每次插入失衡后会旋转。


2. AVL树的实现

2.1 AVL树的结构

注意:平衡因子=右子树高度-左子树高度

// AVL树节点模板类,K为键类型,V为值类型
template<class K, class V>
struct AVLTreeNode
{
    // 存储键值对
    pair<K, V> _kv;
    
    // 左子节点指针
    AVLTreeNode<K, V>* _left;
    
    // 右子节点指针
    AVLTreeNode<K, V>* _right;
    
    // 父节点指针,用于在调整平衡时向上回溯
    AVLTreeNode<K, V>* _parent;
    
    // 平衡因子:右子树高度 - 左子树高度
    // bf = -1:左子树比右子树高1层
    // bf = 0:左右子树等高
    // bf = 1:右子树比左子树高1层
    int _bf;

    // 节点构造函数
    // 参数kv:要存储的键值对
    // 初始化列表:
    // - 将传入的键值对存储到_kv
    // - 将左、右、父指针都初始化为空
    // - 将平衡因子初始化为0
    AVLTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _bf(0)
    {}
};

// AVL树类模板
template<class K, class V>
class AVLTree
{
    // 类型别名,方便使用节点类型
    typedef AVLTreeNode<K, V> Node;
public:
    // 公有成员函数...

private:
    // 根节点指针,初始化为空
    Node* _root = nullptr;
};

2.2 AVL树的插入

2.2.1 AVL树插入一个值的大概过程

  1. 插入一个值按二叉搜索树规则进行插入。
  2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
  3. 更新平衡因子过程中没有出现问题,则插入结束
  4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。

2.2.2 平衡因子更新

更新原则:

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

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

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

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

更新停止条件:

  • 更新后parent的平衡因子等于0,更新中parent的平衡因子变化为-1->0 或者 1->0,说明更新前parent子树一边高一边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父亲结点的平衡因子,更新结束。

  • 更新后parent的平衡因子等于1-1,更新前更新中parent的平衡因子变化为0->1 或者 0->-1,说明更新前parent子树两边一样高,新增的插入结点后,parent所在的子树一边高一边低,parent所在的子树符合平衡要求,但是高度增加了1,会影响parent的父亲结点的平衡因子,所以要继续向上更新。

  • 更新后parent的平衡因子等于2-2,更新前更新中parent的平衡因子变化为1->2 或者 -1->-2,说明更新前parent子树一边高一边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:

    1、把parent子树旋转平衡。

    2、降低parent子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。

  • 不断更新,更新到根,跟的平衡因子是1-1也停止了。

更新到10结点,平衡因子为210所在的子树已经不平衡,需要旋转处理

5e9be42d13d58aafb01737196d34e0e1

更新到中间结点,3为根的子树高度不变,不会影响上一层,更新结束

8cc3cf1c3c9b82b37b39f758e3aa13df

最坏更新到根停止

441b3f233b40411eda2df9c920b4f366


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

// 插入键值对到AVL树中
// 参数kv:要插入的键值对
// 返回值:插入成功返回true,如果键已存在返回false
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--;  // 左子树高度增加,平衡因子减1
        else
            parent->_bf++;  // 右子树高度增加,平衡因子加1

        if (parent->_bf == 0)  // 平衡因子为0,表示左右子树高度相等,不需要继续向上调整
        {
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)  // 平衡因子为1或-1,需要继续向上调整
        {
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)  // 平衡因子为2或-2,树不平衡,需要进行旋转处理
        {
            break;
        }
        else
        {
            // 平衡因子不可能出现其他值
            assert(false);
        }
    }
    return true;
}

2.3 旋转

2.3.1 旋转的原则

  1. 保持搜索树的规则

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

旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。

说明:下面的图中,有些结点我们给的是具体值,如105等结点,这里是为了方便讲解,实际中是什么值都可以,只要大小关系符合搜索树的性质即可。

2.3.2 右单旋

  • 本图1展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0)a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树,是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体图2/图3/图4/图5进行了详细描述。

  • a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从-1变成-210为根的树左右高度差超过1,违反平衡规则。10为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。

  • 旋转核心步骤,因为5 < b子树的值 < 10,将b变成10的左子树,10变成5的右子树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

图1

ce6d9a45e32a90410b50f3aab1c7ff66

图2

bc0a37c0840b9904387dd04b3652e21a

图3

44ed2007b5612aa8f9e112cc2c1e5534

图4

c39249d8a5c05dc649b8ee33225c3754

图5

e8e91027eaf60cef0be19a4df58d6f3d


2.3.3 右单旋代码实现

// 右单旋函数 - 用于调整AVL树平衡
// 参数parent:要旋转的子树的根节点
void RotateR(Node* parent)
{
    // 保存相关节点指针
    Node* subL = parent->_left;      // subL是parent的左子树
    Node* subLR = subL->_right;      // subLR是subL的右子树
    Node* parentParent = parent->_parent;  // 保存parent的父节点

    // 第一步:处理subLR
    // 将parent的左指针指向subLR
    parent->_left = subLR;
    // 如果subLR存在,更新其父指针
    if (subLR)
        subLR->_parent = parent;

    // 第二步:处理parent节点
    // 将subL的右指针指向parent
    subL->_right = parent;
    // 更新parent的父指针为subL
    parent->_parent = subL;

    // 第三步:处理与上层的连接
    if (parentParent == nullptr)
    {
        // 如果parent是根节点
        _root = subL;           // 更新根节点为subL
        subL->_parent = nullptr;  // subL成为新的根,父指针置空
    }
    else
    {
        // 如果parent不是根节点,需要把subL与parent的父节点连接起来
        if (parent == parentParent->_left)
        {
            // parent是其父节点的左子树
            parentParent->_left = subL;
        }
        else
        {
            // parent是其父节点的右子树
            parentParent->_right = subL;
        }
        // 更新subL的父指针
        subL->_parent = parentParent;
    }

    // 第四步:更新平衡因子
    // 右单旋后,这两个节点的平衡因子都变为0
    parent->_bf = 0;
    subL->_bf = 0;
}

2.3.4 左单旋

  • 本图6展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0)a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树,是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体跟上面左旋类似。

  • a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从1变成210为根的树左右高度差超过1,违反平衡规则。10为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。

  • 旋转核心步骤,因为10 < b子树的值 < 15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

图6

074145fbc3e9eb7287a4005eb7da10eb


2.3.5 左单旋代码实现

// 左单旋函数 - 用于调整AVL树平衡
// 参数parent:要旋转的子树的根节点
void RotateL(Node* parent)
{
    // 保存相关节点指针
    Node* subR = parent->_right;     // subR是parent的右子树
    Node* subRL = subR->_left;       // subRL是subR的左子树
    Node* parentParent = parent->_parent;  // 保存parent的父节点

    // 第一步:处理subRL
    // 将parent的右指针指向subRL
    parent->_right = subRL;
    // 如果subRL存在,更新其父指针
    if(subRL)
        subRL->_parent = parent;

    // 第二步:处理parent节点
    // 将subR的左指针指向parent
    subR->_left = parent;
    // 更新parent的父指针为subR
    parent->_parent = subR;

    // 第三步:处理与上层的连接
    if (parentParent == nullptr)
    {
        // 如果parent是根节点
        _root = subR;           // 更新根节点为subR
        subR->_parent = nullptr;  // subR成为新的根,父指针置空
    }
    else
    {
        // 如果parent不是根节点,需要把subR与parent的父节点连接起来
        if (parent == parentParent->_left)
        {
            // parent是其父节点的左子树
            parentParent->_left = subR;
        }
        else
        {
            // parent是其父节点的右子树
            parentParent->_right = subR;
        }
        // 更新subR的父指针
        subR->_parent = parentParent;
    }

    // 第四步:更新平衡因子
    // 左单旋后,这两个节点的平衡因子都变为0
    parent->_bf = 0;
    subR->_bf = 0;
}

2.3.6 左右双旋

通过图7和图8可以看到,左边高时,如果插入位置不是在a子树,而是插入在b子树,b子树高度从h变成h+1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在b子树中,10为跟的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,需要用两次旋转才能解决,以5为旋转点进行一个左单旋,以10为旋转点进行一个右单旋,这棵树这棵树就平衡了。

图7

261e259df1bd911e70282647a8d4d1f6

图8

b7277317f6f20649ae7cf7cc360c4530

  • 图7和图8分别为左右双旋中h==0h==1具体场景分析,下面我们将a/b/c子树抽象为高度hAVL子树进行分析,另外我们需要把b子树的细节进一步展开为8和左子树高度为h-1ef子树,因为我们要对b的父亲5为旋转点进行左单旋,左单旋需要动b树中的左子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察8的平衡因子不同,这里我们要分三个场景讨论。

  • 场景1:h >= 1时,新增结点插入在e子树,e子树高度从h-1并为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为-1,旋转后85平衡因子为010平衡因子为1

  • 场景2:h >= 1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为1,旋转后810平衡因子为05平衡因子为-1

  • 场景3:h == 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新5->10平衡因子,引发旋转,其中8的平衡因子为0,旋转后8105平衡因子均为0

图9

69fa0871037bd378debf7fa5e57ed974


2.3.7 左右双旋代码实现

// 左右双旋函数(先左旋后右旋) - 用于调整AVL树平衡
// 参数parent:要旋转的子树的根节点
void RotateLR(Node* parent)
{
    // 保存相关节点指针
    Node* subL = parent->_left;    // subL是parent的左子树
    Node* subLR = subL->_right;    // subLR是subL的右子树
    
    // 保存旋转前subLR的平衡因子,用于后续更新各节点的平衡因子
    int bf = subLR->_bf;

    // 第一步:对parent的左子树进行左旋
    RotateL(parent->_left);
    // 第二步:对parent进行右旋
    RotateR(parent);

    // 根据旋转前subLR的平衡因子,更新旋转后三个节点的平衡因子
    if (bf == 0)  // subLR原本左右子树等高
    {
        // 三个节点平衡因子都调整为0
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)  // subLR原本左子树较高
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;  // parent节点需要调整为1
    }
    else if(bf == 1)  // subLR原本右子树较高
    {
        subL->_bf = -1;  // subL节点需要调整为-1
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        // 平衡因子不可能是其他值
        assert(false);
    }
}

2.3.8 右左双旋

  • 跟左右双旋类似,下面我们将a/b/c子树抽象为高度hAVL子树进行分析,另外我们需要把b子树的细节进一步展开为12和左子树高度为h-1ef子树,因为我们要对b的父亲15为旋转点进行右单旋,右单旋需要动b树中的右子树。b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察12的平衡因子不同,这里我们要分三个场景讨论。

  • 场景1:h >= 1时,新增结点插入在e子树,e子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后1012平衡因子为015平衡因子为1

  • 场景2:h >= 1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为1,旋转后1512平衡因子为010平衡因子为-1

  • 场景3:h == 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0,旋转后101215平衡因子均为0

26b180dccb3bbcccdc1d1ac1442bc5df


2.3.9 右左双旋代码实现

// 右左双旋函数(先右旋后左旋) - 用于调整AVL树平衡
// 参数parent:要旋转的子树的根节点
void RotateRL(Node* parent)
{
    // 保存相关节点指针
    Node* subR = parent->_right;   // subR是parent的右子树
    Node* subRL = subR->_left;     // subRL是subR的左子树
    
    // 保存旋转前subRL的平衡因子,用于后续更新各节点的平衡因子
    int bf = subRL->_bf;

    // 第一步:对parent的右子树进行右旋
    RotateR(parent->_right);
    // 第二步:对parent进行左旋
    RotateL(parent);

    // 根据旋转前subRL的平衡因子,更新旋转后三个节点的平衡因子
    if (bf == 0)  // subRL原本左右子树等高
    {
        // 三个节点平衡因子都调整为0
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == 1)  // subRL原本右子树较高
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = -1;  // parent节点需要调整为-1
    }
    else if (bf == -1)  // subRL原本左子树较高
    {
        subR->_bf = 1;    // subR节点需要调整为1
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        // 平衡因子不可能是其他值
        assert(false);
    }
}

2.4 AVL树的查找

那二叉搜索树逻辑实现即可,搜索效率为O(logN)

// 查找指定键值的节点
// 参数key:要查找的键值
// 返回值:如果找到返回对应节点指针,没找到返回nullptr
Node* Find(const K& key)
{
    // 从根节点开始查找
    Node* cur = _root;
    
    // 循环直到找到节点或到达叶子节点
    while (cur)
    {
        if (cur->_kv.first < key)  // 当前节点键值小于目标键值
        {
            cur = cur->_right;      // 在右子树中继续查找
        }
        else if (cur->_kv.first > key)  // 当前节点键值大于目标键值
        {
            cur = cur->_left;       // 在左子树中继续查找
        }
        else  // 找到目标节点
        {
            return cur;
        }
    }
    
    // 没找到目标节点,返回nullptr
    return nullptr;
}

2.5 AVL树平衡检测

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

// 计算树的高度
// 参数root:要计算的树的根节点
// 返回值:树的高度
int _Height(Node* root)
{
    if (root == nullptr)
        return 0;
    // 递归计算左右子树高度
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    // 返回较高的子树高度+1
    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

// 检查是否是平衡二叉树
// 参数root:要检查的树的根节点
// 返回值:是否是平衡二叉树
bool _IsBalanceTree(Node* root)
{
    // 空树也是AVL树
    if (nullptr == root)
        return true;

    // 计算当前节点的平衡因子
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;  // 实际的平衡因子

    // 判断是否平衡:
    // 1. 平衡因子绝对值不能>=2
    if (abs(diff) >= 2)
    {
        cout << root->_kv.first << "高度差异常" << endl;
        return false;
    }
    // 2. 实际计算的平衡因子要与存储的平衡因子相等
    if (root->_bf != diff)
    {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }
    
    // 递归检查左右子树是否平衡
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

// 测试函数1:基本功能测试
void TestAVLTree1()
{
    AVLTree<int, int> t;
    // 测试数据:包含需要双旋的情况
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    // 插入数据
    for (auto e : a)
    {
        t.Insert({ e, e });
    }
    // 中序遍历并检查是否平衡
    t.InOrder();
    cout << t.IsBalanceTree() << endl;
}

// 测试函数2:性能测试
void TestAVLTree2()
{
    const int N = 100000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));
    // 生成随机测试数据
    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand()+i);
    }

    // 测试插入性能
    size_t begin2 = clock();
    AVLTree<int, int> t;
    for (auto e : v)
    {
        t.Insert(make_pair(e, e));
    }
    size_t end2 = clock();
    cout << "Insert:" << end2 - begin2 << endl;

    // 验证树的属性
    cout << t.IsBalanceTree() << endl;
    cout << "Height:" << t.Height() << endl;
    cout << "Size:" << t.Size() << endl;

    // 测试查找性能
    size_t begin1 = clock();
    // 查找随机值
    for (size_t i = 0; i < N; i++)
    {
        t.Find((rand() + i));
    }
    size_t end1 = clock();
    cout << "Find:" << end1 - begin1 << endl;
}

2.6 AVL树的删除

AVL树的删除本章节不做讲解,有兴趣的同学可参考:《殷人昆 数据结构:用面向对象方法与C++语言描述》中讲解。


2.7 AVL树的代码实现

AVLTree.h

#pragma once

// AVL树节点的定义
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;

    // AVL树节点的构造函数
	// 参数kv: 传入的键值对,使用const引用避免不必要的拷贝
	AVLTreeNode(const pair<K, V>& kv)
   	 	// 初始化列表,按成员声明顺序初始化
   		:_kv(kv)        // 将传入的键值对拷贝到节点的_kv成员
    	, _left(nullptr)    // 初始化左子节点指针为空
    	, _right(nullptr)   // 初始化右子节点指针为空
    	, _parent(nullptr)  // 初始化父节点指针为空
    	, _bf(0)           // 初始化平衡因子为0,表示初始状态左右子树高度相同
	{}  // 构造函数体为空,因为所有初始化都在初始化列表中完成
};

// AVL树类定义
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    // 插入新节点
    bool Insert(const pair<K, V>& kv)
    {
        // 空树情况:创建根节点
        if (_root == nullptr)
        {
            _root = new Node(kv);
            return true;
        }

        // 寻找插入位置
        // parent指针用于记录当前遍历节点的父节点,初始化为nullptr,因为还未开始遍历
        Node* parent = nullptr;
        // cur指针用于遍历树,初始指向根节点,从根节点开始向下搜索合适的插入位置
        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)
        {
            // 如果新节点的key大于父节点的key,将新节点链接为父节点的右孩子
            parent->_right = cur;
        }
        else
        {
            // 如果新节点的key小于父节点的key,将新节点链接为父节点的左孩子
            parent->_left = cur;
        }
        // 设置新节点的父指针,使其指向父节点
        // 至此完成新节点的双向链接
        cur->_parent = parent;

        // 更新平衡因子并进行平衡调整
        while (parent)
        {
            // 更新当前父节点的平衡因子
            if (cur == parent->_right)
            {
                parent->_bf++;
            }
            else
            {
                parent->_bf--;
            }

            // 判断是否需要继续向上调整
            if (parent->_bf == 0)
            {
                // 当前子树高度未变,停止调整
                break;
            }
            else if (parent->_bf == 1 || parent->_bf == -1)
            {
                // 当前子树高度+1,继续向上调整
                cur = parent;
                parent = parent->_parent;
            }
            else if (parent->_bf == 2 || parent->_bf == -2)
            {
                // 当前节点失衡,需要旋转处理
                if (parent->_bf == -2 && cur->_bf == -1)
                {
                    // LL型:右单旋
                    RotateR(parent);
                }
                else if (parent->_bf == 2 && cur->_bf == 1)
                {
                    // RR型:左单旋
                    RotateL(parent);
                }
                else if (parent->_bf == -2 && cur->_bf == 1)
                {
                    // LR型:先左旋再右旋
                    RotateLR(parent);
                }
                else if (parent->_bf == 2 && cur->_bf == -1)
                {
                    // RL型:先右旋再左旋
                    RotateRL(parent);
                }
                else
                {
                    // 不应该出现的情况
                    assert(false);
                }
                break;
            }
            else
            {
                // 不应该出现的平衡因子值
                assert(false);
            }
        }

        return true;
    }

    // 中序遍历
    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }

    // 检查是否是平衡树
    bool IsBalanceTree()
    {
        return _IsBalanceTree(_root); //_IsBalanceTree在第210行
    }

    // 查找指定键值的节点
    Node* Find(const K& key)
    {
        Node* cur = _root;
        while (cur)
        {
            if (cur->_kv.first < key)
            {
                cur = cur->_right;
            }
            else if (cur->_kv.first > key)
            {
                cur = cur->_left;
            }
            else
            {
                return cur;
            }
        }
        return nullptr;
    }

    // 获取树的高度
    int Height()
    {
        return _Height(_root); //_Height在第239行
    }

    // 获取树的节点个数
    int Size()
    {
        return _Size(_root);
    }

private:
    // 递归计算节点个数
    int _Size(Node* root)
    {
        if (root == nullptr)
            return 0;
        return _Size(root->_left) + _Size(root->_right) + 1;
    }

    // 递归检查是否是平衡树
    bool _IsBalanceTree(Node* root)
    {
        if (nullptr == root)
            return true;

        // 计算当前节点的实际平衡因子
        int leftHeight = _Height(root->_left); //_Height在第239行
        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);
    }

    // 递归计算树的高度
    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;
    }

    // 中序遍历的递归实现
    void _InOrder(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        // 递归遍历左子树
        _InOrder(root->_left);
        // 访问当前节点,打印键值对
        cout << root->_kv.first << ":" << root->_kv.second << endl;
        // 递归遍历右子树
        _InOrder(root->_right);
    }
    
    /*
    // 假设有如下树结构:
    //       4
    //      / \
    //     2   6
    //    / \
    //   1   3

    // 执行步骤:
    1. _InOrder(4)
       1.1 _InOrder(2)
           1.1.1 _InOrder(1)
                 - 到达nullptr,返回
                 - 打印1
                 - 右子树为nullptr,返回
           1.1.2 打印2
           1.1.3 _InOrder(3)
                 - 到达nullptr,返回
                 - 打印3
                 - 右子树为nullptr,返回
       1.2 打印4
       1.3 _InOrder(6)
           - 左子树为nullptr,返回
           - 打印6
           - 右子树为nullptr,返回

    // 最终输出顺序:
    1:value1
    2:value2
    3:value3
    4:value4
    6:value6
    */

    // 右单旋
    void RotateR(Node* parent)
    {
        // 1. 保存需要用到的节点
        Node* subL = parent->_left;      // 左子树节点
        Node* subLR = subL->_right;      // 左子树的右子节点

        // 2. 处理subLR节点
        // subLR要变成parent的左子树
        parent->_left = subLR;
        // 如果subLR存在,更新它的父节点为parent
        if (subLR)
            subLR->_parent = parent;

        // 3. 保存parent的父节点,用于后续处理
        Node* ppNode = parent->_parent;

        // 4. 重建subL和parent的关系
        // parent要成为subL的右子树
        subL->_right = parent;
        // 更新parent的父节点为subL
        parent->_parent = subL;

        // 5. 处理特殊情况:如果parent是根节点
        if (parent == _root)
        {
            // subL成为新的根节点
            _root = subL;
            // 根节点的父指针为nullptr
            subL->_parent = nullptr;
        }
        else
        {
            // 6. 一般情况:将subL与原来parent的父节点连接
            // 判断parent是其父节点的左子树还是右子树
            if (ppNode->_left == parent)
            {
                ppNode->_left = subL;    // 如果parent是父节点的左子树
            }
            else
            {
                ppNode->_right = subL;   // 如果parent是父节点的右子树
            }
            // 更新subL的父节点
            subL->_parent = ppNode;
        }

        // 7. 更新平衡因子
        // 右旋转后,这两个节点的平衡因子都变为0
        parent->_bf = 0;
        subL->_bf = 0;
    }

    // 左单旋
    void RotateL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        
        // 改变subRL的父指针
        parent->_right = subRL;
        if (subRL)
            subRL->_parent = parent;
            
        // 保存parent的父节点
        Node* parentParent = parent->_parent;
        
        // 建立subR和parent的新关系
        subR->_left = parent;
        parent->_parent = subR;
        
        // 处理根节点的特殊情况
        if (parentParent == nullptr)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            // 将subR与parentParent连接
            if (parent == parentParent->_left)
            {
                parentParent->_left = subR;
            }
            else
            {
                parentParent->_right = subR;
            }
            subR->_parent = parentParent;
        }
        
        // 更新平衡因子
        parent->_bf = subR->_bf = 0;
    }

    // 先左旋再右旋
    void RotateLR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        // 记录subLR的平衡因子,用于后续更新其他节点的平衡因子
        int bf = subLR->_bf;

        // 先对左子节点左旋,再对父节点右旋
        RotateL(parent->_left);
        RotateR(parent);

        // 根据旋转前subLR的平衡因子更新各节点的平衡因子
        if (bf == -1)
        {
            subL->_bf = 0;
            parent->_bf = 1;
            subLR->_bf = 0;
        }
        else if (bf == 1)
        {
            subL->_bf = -1;
            parent->_bf = 0;
            subLR->_bf = 0;
        }
        else if (bf == 0)
        {
            subL->_bf = 0;
            parent->_bf = 0;
            subLR->_bf = 0;
        }
        else
        {
            assert(false);
        }
    }

    // 先右旋再左旋
    void RotateRL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        // 记录subRL的平衡因子,用于后续更新其他节点的平衡因子
        int bf = subRL->_bf;
        
        // 先对右子节点右旋,再对父节点左旋
        RotateR(parent->_right);
        RotateL(parent);
        
        // 根据旋转前subRL的平衡因子更新各节点的平衡因子
        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);
        }
    }

private:
    Node* _root = nullptr;  // 根节点指针
};

// 基本功能测试
void TestAVLTree1()
{
    AVLTree<int, int> t;
    // 测试数据数组
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    // 依次插入数据并检查树的平衡性
    for (auto e : a)
    {
        t.Insert({ e, e });
        cout << "Insert:" << e << "->";
        cout << t.IsBalanceTree() << endl;
    }

    // 打印树的中序遍历结果
    t.InOrder();
    // 最后检查一次树的平衡性
    cout << t.IsBalanceTree() << endl;
}

// 性能测试
void TestAVLTree2()
{
    const int N = 1000000;  // 测试数据量
    vector<int> v;
    v.reserve(N);
    srand(time(0));
    // 生成测试数据
    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand() + i);
    }

    // 测试插入性能
    size_t begin2 = clock();
    AVLTree<int, int> t;
    for (auto e : v)
    {
        t.Insert(make_pair(e, e));
    }
    size_t end2 = clock();

    // 验证树的正确性
    cout << t.IsBalanceTree() << endl;

    // 输出插入耗时和树的信息
    cout << "Insert:" << end2 - begin2 << endl;
    cout << "Height:" << t.Height() << endl;
    cout << "Size:" << t.Size() << endl;

    // 测试查找性能
    size_t begin1 = clock();
    // 对随机值进行查找测试
    for (size_t i = 0; i < N; i++)
    {
        t.Find((rand() + i));
    }
    size_t end1 = clock();
    cout << "Find:" << end1 - begin1 << endl;
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;
#include"AVLTree.h"

int main()
{
	TestAVLTree1();

	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2274939.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

php文件包含漏洞

基本 相关函数 php中引发文件包含漏洞的通常是以下四个函数&#xff1a; include()include_once()require()require_once() reuqire() 如果在包含的过程中有错&#xff0c;比如文件不存在等&#xff0c;则会直接退出&#xff0c;不执行后续语句。 include() 如果出错的话&a…

ELK实战(最详细)

一、什么是ELK ELK是三个产品的简称&#xff1a;ElasticSearch(简称ES) 、Logstash 、Kibana 。其中&#xff1a; ElasticSearch&#xff1a;是一个开源分布式搜索引擎Logstash &#xff1a;是一个数据收集引擎&#xff0c;支持日志搜集、分析、过滤&#xff0c;支持大量数据…

预训练语言模型——BERT

1.预训练思想 有了预训练就相当于模型在培养大学生做任务&#xff0c;不然模型初始化再做任务就像培养小学生 当前数据层面的瓶颈是能用于预训练的语料快被用完了 现在有一个重要方向是让机器自己来生成数据并做微调 1.1 预训练&#xff08;Pre - training&#xff09;vs. 传…

ElasticSearch 认识和安装ES

文章目录 一、为什么学ElasticSearch?1.ElasticSearch 简介2.ElasticSearch 与传统数据库的对比3.ElasticSearch 应用场景4.ElasticSearch 技术特点5.ElasticSearch 市场表现6.ElasticSearch 的发展 二、认识和安装ES1.认识 Elasticsearch&#xff08;简称 ES&#xff09;2.El…

mysql和redis的最大连接数

平时我们要评估mysql和redis的最大连接数&#xff0c;可以选择好环境&#xff08;比如4核8G&#xff09;,定好压测方法&#xff08;没有索引的mysql单表&#xff0c;redis单key&#xff09;进行压测&#xff0c;评估其最大并发量。 也可以查看各大云厂商的规格进行评估。 mys…

2025年中科院分区大类划分公布!新增8155本

2025年中科院分区表变更情况 扩大收录范围 2025年的期刊分区表在原有的自然科学&#xff08;SCIE&#xff09;、社会科学&#xff08;SSCI&#xff09;和人文科学&#xff08;AHCI&#xff09;的基础上&#xff0c;增加了ESCI期刊的收录&#xff0c;并根据这些期刊的数据进行…

机器人避障不再“智障”:HEIGHT——拥挤复杂环境下机器人导航的新架构

导读&#xff1a; 由于环境中静态障碍物和动态障碍物的约束&#xff0c;机器人在密集且交互复杂的人群中导航&#xff0c;往往面临碰撞与延迟等安全与效率问题。举个简单的例子&#xff0c;商城和车站中的送餐机器人往往在人流量较大时就会停在原地无法运作&#xff0c;因为它不…

Spring Boot教程之五十二:CrudRepository 和 JpaRepository 之间的区别

Spring Boot – CrudRepository 和 JpaRepository 之间的区别 Spring Boot建立在 Spring 之上&#xff0c;包含 Spring 的所有功能。由于其快速的生产就绪环境&#xff0c;使开发人员能够直接专注于逻辑&#xff0c;而不必费力配置和设置&#xff0c;因此如今它正成为开发人员…

加速物联网HMI革命,基于TouchGFX的高效GUI显示方案

TouchGFX 是一款针对 STM32 微控制器优化的先进免费图形软件框架。 TouchGFX 利用 STM32 图形功能和架构&#xff0c;通过创建令人惊叹的类似智能手机的图形用户界面&#xff0c;加速了物联网 HMI 革命。 TouchGFX 框架包括 TouchGFX Designer (TouchGFXDesigner)&#xff08;…

Java-数据结构-栈与队列(StackQueue)

一、栈(Stack) ① 栈的概念 栈是一种特殊的线性表&#xff0c;它只允许固定一端进行"插入元素"和"删除元素"的操作&#xff0c;这固定的一端被称作"栈顶"&#xff0c;对应的另一端就被称做"栈底"。 &#x1f4da; 栈中的元素遵循后…

案例研究:UML用例图中的结账系统

在软件工程和系统分析中&#xff0c;统一建模语言&#xff08;UML&#xff09;用例图是一种强有力的工具&#xff0c;用于描述系统与其用户之间的交互。本文将通过一个具体的案例研究&#xff0c;详细解释UML用例图的关键概念&#xff0c;并说明其在设计结账系统中的应用。 用…

【动态规划篇】欣赏概率论与镜像法融合下,别出心裁探索解答括号序列问题

本篇鸡汤&#xff1a;没有人能替你承受痛苦&#xff0c;也没有人能拿走你的坚强. 欢迎拜访&#xff1a;羑悻的小杀马特.-CSDN博客 本篇主题&#xff1a;带你解答洛谷的括号序列问题&#xff08;绝对巧解&#xff09; 制作日期&#xff1a;2025.01.10 隶属专栏&#xff1a;C/C题…

点击底部的 tabBar 属于 wx.switchTab 跳转方式,目标页面的 onLoad 不会触发(除非是第一次加载)

文章目录 1. tabBar 的跳转方式2. tabBar 跳转的特点3. 你的配置分析4. 生命周期触发情况5. 总结 很多人不明白什么是第一次加载&#xff0c;两种情况讨论&#xff0c;第一种情况假设我是开发者&#xff0c;第一次加载就是指点击微信开发者工具上边的编译按钮&#xff0c;每点击…

CUDA、CUDNN以及tensorRT的版本对应关系

各版本的对应除了可以看文件的命名上还可以查看TensorRT的Release日志&#xff1a; Release Notes :: NVIDIA Deep Learning TensorRT Documentation 这个是官方测试TensorRT的Release日志&#xff0c;里面指明了当前测试的TensorRT版本是在哪个CUDNN等库下的测试结果&#x…

设计模式(观察者模式)

设计模式&#xff08;观察者模式&#xff09; 第三章 设计模式之观察者模式 观察者模式介绍 观察者模式&#xff08;Observer Design Pattern&#xff09; 也被称为发布订阅模式 。模式定义&#xff1a;在对象之间定义一个一对多的依赖&#xff0c;当一个对象状态改变的时候…

Helm部署activemq

1.helm create activemq 创建helm文件目录 2.修改values.yaml 修改image和port 3. helm template activemq 渲染并输出 4. helm install activemq activemq/ -n chemical-park // 安装 5.启动成功

Untiy中如何嵌入前端页面,从而播放推流视频?

最近工作中频繁用到unity,虽然楼主之前也搞过一些UNTY游戏开发项目&#xff0c;但对于视频这块还是不太了解&#xff0c;所以我们采用的方案是在Unity里寻找一个插件来播放推流视频。经过我的一番寻找&#xff0c;发现了这款Vuplex 3D WebView&#xff0c;它可以完美的打通Unit…

rabbitmq的三个交换机及简单使用

提前说一下&#xff0c;创建队列&#xff0c;交换机&#xff0c;绑定交换机和队列都是在生产者。消费者只负责监听就行了&#xff0c;不用配其他的。 完成这个场景需要两个服务哦。 1直连交换机-生产者的代码。 在配置类中创建队列&#xff0c;交换机&#xff0c;绑定交换机…

Excel 技巧07 - 如何计算到两个日期之间的工作日数?(★)如何排除节假日计算两个日期之间的工作日数?

本文讲了如何在Excel中计算两个日期之间的工作日数&#xff0c;以及如何排除节假日计算两个日期之间的工作日数。 1&#xff0c;如何计算到两个日期之间的工作日数&#xff1f; 其实就是利用 NETWORKDAYS.INTL 函数 - weekend: 1 - 星期六&#xff0c;星期日 2&#xff0c;如…

初学stm32 --- DAC模数转换器工作原理

目录 什么是DAC&#xff1f; DAC的特性参数 STM32各系列DAC的主要特性 DAC框图简介&#xff08;F1/F4/F7&#xff09; 参考电压/模拟部分电压 触发源 关闭触发时(TEN0)的转换时序图 DMA请求 DAC输出电压 什么是DAC&#xff1f; DAC&#xff0c;全称&#xff1a;Digital…