【C++】AVL树

简介: 平衡二叉搜索树AVL树的底层原理、模拟实现

1. 什么是AVL树?

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。 因此,两位俄罗斯的数学家 G.M.Adelson-VelskiiE.M.Landis 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整), ==即可降低树的高度,从而减少平均搜索长度。==

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 他的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  • 通过引入平衡因子来控制AVL树的左右子树高度差,平衡因子 = 右子树高度 - 左子树高度

image.png

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logn),搜索时间复杂度O(logn)


2. AVL树节点的定义

对于AVL树,我们需要增加一个变量bf(平衡因子)来控制树的状态。并新增一个父节点指针,用来指向该节点的父节点。这是为了方便后面插入节点时修改父节点的平衡因子。

template <class K, class V>
struct AVLTreeNode
{
   
   
    AVLTreeNode<K, V>* _left;   // 左孩子节点
    AVLTreeNode<K, V>* _right;  // 右孩子节点
    AVLTreeNode<K, V>* _parent; // 父节点
    pair<K, V> _kv;
    int _bf;
    //构造函数
    AVLTreeNode(const pair<K,V>& kv)
        :_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_kv(kv)
        ,_bf(0)
    {
   
   }
};

3. AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

pParent平衡因子的调整

pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子 -1 即可
  2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子 +1 即可

平衡因子的更新和旋转处理

image.png

image.png

下面我们举一个例子来说明更新完平衡因子后需要调整旋转处理的情况:

image.png

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 (kv.first > cur->_kv.first)
        {
   
   
            parent = cur;
            cur = cur->_right;
        }
        else if (kv.first < cur->_kv.first)
        {
   
   
            parent = cur;
            cur = cur->_left;
        }
        else
        {
   
   
            return false;
        }
    }
    //链接节点
    cur = new Node(kv);
    if (parent->_kv.first > kv.first)
        parent->_left = cur;
    else
        parent->_right = cur;

    cur->_parent = parent;//链接父节点

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

        if (parent->_bf == 1 || parent->_bf == -1)
        {
   
   
            //继续向上更新平衡因子
            parent = parent->_parent;
            cur = cur->_parent;
        }
        else if (parent->_bf == 0)
        {
   
   
            break;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
   
   
            //旋转处理
            if (parent->_bf == 2 && cur->_bf == 1)
                RotateL(parent);//左单旋
            else if (parent->_bf == -2 && cur->_bf == -1)
                RotateR(parent);//右单旋
            else if (parent->_bf == -2 && cur->_bf == 1)
                RotateLR(parent);//左右双旋
            else if (parent->_bf == 2 && cur->_bf == -1)
                RotateRL(parent);//右左双旋
            else
                assert(false);
            break;
        }
        else
            assert(false);
    }
    return true;
}

4. AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡(当节点的平衡因子为 2/-2 时),此时必须调整以这个节点为根的子树的结构,对这棵子树进行旋转处理。
旋转的目的是:

  • 让这棵子树的左右高度差不超过1;
  • 旋转时保持其是搜索树的结构;
  • 更新平衡因子;
  • 使子树的高度和插入前保持一致,从而不会继续影响上一层,旋转结束;

根据节点插入位置的不同,AVL树的旋转也有着不同的情况:

  1. 左单旋: 新节点插入较高右子树的右侧—右右;
  2. 右单旋: 新节点插入较高左子树的左侧—左左;
  3. 先左单旋再右单旋: 新节点插入较高左子树的右侧---左右:
  4. 先右单旋再左单旋: 新节点插入较高右子树的左侧---右左;

4.1 左单旋

当满足右子树比左子树高1且在右子树的右边插入节点时,右子树会比左子树高度高2,此时需要以parent为父节点进行左单旋。左单旋的抽象图如下:

抽象图:

image.png

对于左单旋来说,可以分为三种子情况。任何一棵子树进行左单旋,都会是下面这三种子情况之一:h==0h==1h==2

具象图:

image.png

这里我们需要注意的是当h==2时,对于c来说一定是x形状,但对于a、b来说一定是x、y、z中的一种。这里一共9种情况。

由于c的高度发生变化一定会引发旋转,在1、2、3、4四个位置任意一个位置插入都会引发30的平衡因子变为2,一共有4种情况。所以和上面的9种情况一起组合一下共有36种情况。

void RotateL(Node* parent)
{
   
   
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;

    //这里我们需要注意一下,subRL有可能为空
    if (subRL)
        subRL->_parent = parent;

    Node* ppnode = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    //判断父节点是否为_root节点
    if (parent == _root) {
   
   
        _root = subR;
        _root->_parent = nullptr;
    }
    else {
   
   
        if (parent == ppnode->_left)
            ppnode->_left = subR;
        else
            ppnode->_right = subR;
        subR->_parent = ppnode;
    }

    //调节平衡因子
    parent->_bf = subR->_bf = 0;
}

4.2 右单旋

当满足左子树的高度比右子树高1且在左子树的左边插入节点时,左子树的高度会比右子树高2,此时需要对parent为节点进行右单旋。

抽象图:

image.png

具象图:

image.png

//右单旋
void RotateR(Node* parent)
{
   
   
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

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

    //保存parent节点的_parent节点
    Node* ppnode = parent->_parent;

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

    //判断parent节点是否为_root节点
    if (parent == _root)
    {
   
   
        _root = subL;
        _root->_parent = nullptr;
    }
    else
    {
   
   
        if (parent == ppnode->_left)
            ppnode->_left = subL;
        else
            ppnode->_right = subL;
        subL->_parent = ppnode;
    }
    //调节平衡因子
    subL->_bf = parent->_bf = 0;
}

在这里我们稍微总结一下:左单旋和右单旋都是插入后一边高且为直线的情况,当插入后为折线时,单旋已经解决不了我们的需求了,这时就需要引入双旋来解决了。


4.3 左右双旋

左右双旋的情况如下,当a、d是高度为h的AVL子树时,b、c是高度为h-1的AVL子树,90是这棵树的根,当满足 "左子树比右子树高度高1且在左子树的右侧插入节点时" 我们需要先左单旋,再右单旋。

抽象图:

image.png

具象图:

image.png

void RotateLR(Node* parent)
{
   
   
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    int bf = subLR->_bf;
    //先以左子树为轴进行左单旋--30
    RotateL(parent->_left);
    //以根节点为轴进行右单旋--90
    RotateR(parent);
    //更新平衡因子
    if (bf == -1)
    {
   
   
        parent->_bf = 1;
        subLR->_bf = 0;
        subL->_bf = 0;
    }
    else if (bf == 1)
    {
   
   
        parent->_bf = 0;
        subLR->_bf = 0;
        subL->_bf = -1;
    }
    else if (bf == 0)
    {
   
   
        parent->_bf = 0;
        subLR->_bf = 0;
        subL->_bf = 0;
    }
    else
        assert(false);
}

4.4 右左双旋

右左双旋满足的是右子树比左子树高度高1且在右子树的左侧插入节点时,所以我们需要先进行右单旋,在进行左单旋。

具象图如下:

image.png

//右左双旋
void RotateRL(Node* parent)
{
   
   
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    int bf = subRL->_bf;

    RotateR(parent->_right);//以右子树为轴进行右单旋--90
    RotateL(parent);//以根节点为轴进行左单旋--30

    //调整平衡因子
    if (bf == 1)
    {
   
   
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if (bf == -1)
    {
   
   
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else if (bf == 0)
    {
   
   
        parent->_bf = 0;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else
        assert(false);
}

这里我们来总结一下:

假如以 parent 为根的子树不平衡,即 parent 的平衡因子为 2 或者 -2,则分以下情况考虑旋转:

  • parent 的平衡因子为 2,说明 parent 的右子树高,设 parent 的右子树的根为 ssubR
    1. 当 subR 的平衡因子为 1 时,执行左单旋;
    2. 当 subR 的平衡因子为 -1 时,执行右左双旋;

parent 的平衡因子为 -2,说明 parent 的左子树高,设 parent的左子树的根为 subL

  1. 当 subL 的平衡因子为 -1 时,执行右单旋;
  2. 当 subL 的平衡因子为 1 时,执行左右双旋。

旋转完成后,原 parent 为根的子树的高度降低为未插入时的高度,此时这棵子树已经平衡,不需要再向上更新。


5. AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子) 节点的平衡因子是否计算正确

验证其为二叉搜索树: 写一个中序遍历,将遍历结果依次打印,如果结果为有序,则说明该AVL树为一棵二叉搜索树。

验证其为平衡树: 求出每个节点的左右子树高度,看他们之差是否为 -1/0/1,同时在验证平衡的过程中将不符合要求的节点的key值打印出来,方便发生错误时进行调试。

//求树的高度
int Height()
{
   
   
    return _Height(_root);
}
int _Height(Node* root)
{
   
   
    if (root == nullptr)
        return 0;
    int leftHight = _Height(root->_left);
    int rightHight = _Height(root->_right);

    return leftHight > rightHight ? leftHight + 1 
    : rightHight + 1;
}
//判断是否为平衡二叉树
bool IsBalance()
{
   
   
    return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
   
   
    if (root == nullptr)
        return true;
    //根据平衡因子判断二叉树是否为平衡二叉树
    int leftH = _Height(root->_left);
    int rightH = _Height(root->_right);

    if (rightH - leftH != root->_bf) {
   
   
        cout << "节点的平衡因子异常" << endl;
        return false;
    }

    //这里需要判断每棵子树的高度差及平衡因子是否符合要求
    return abs(rightH - leftH) < 2
        && _IsBalance(root->_left)
        && _IsBalance(root->_right);
}

6. AVL树的性能

由于 AVL 树是一棵平衡二叉搜索树,其每个节点的左右子树的高度差都不超过1,所以 AVL 树是无限接近于满二叉树的,那么 AVL 进行查询的时间复杂度就无限接近于 O(logN),所以 AVL 进行查询非常高效;

但是如果要对 AVL 树做一些结构修改的操作,其性能就比较低;因为 AVL 树插入时需要调整其达到平衡,那么进行旋转的次数就比较多,更差的是在删除时,有可能要一直让旋转持续到根的位置;因此如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变) 或数据较少进行插入和删除,则可以考虑 AVL 树,但如果一个结构经常进行修改,AVL 则不太适合。


7. AVL树代码实现

template <class K, class V>
struct AVLTreeNode
{
   
   
    AVLTreeNode<K, V>* _left;   // 左孩子节点
    AVLTreeNode<K, V>* _right;  // 右孩子节点
    AVLTreeNode<K, V>* _parent; // 父节点
    pair<K, V> _kv;
    int _bf;
    //构造函数
    AVLTreeNode(const pair<K,V>& kv)
        :_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_kv(kv)
        ,_bf(0)
    {
   
   }
};

template <class K, class V>
class AVLTree
{
   
   
    typedef AVLTreeNode<K, V> Node;
public:
    //insert的实现
    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 (kv.first > cur->_kv.first)
            {
   
   
                parent = cur;
                cur = cur->_right;
            }
            else if (kv.first < cur->_kv.first)
            {
   
   
                parent = cur;
                cur = cur->_left;
            }
            else
            {
   
   
                return false;
            }
        }
        //链接节点
        cur = new Node(kv);
        if (parent->_kv.first > kv.first)
            parent->_left = cur;
        else
            parent->_right = cur;

        cur->_parent = parent;//链接父节点

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

            if (parent->_bf == 1 || parent->_bf == -1)
            {
   
   
                //继续向上更新平衡因子
                parent = parent->_parent;
                cur = cur->_parent;
            }
            else if (parent->_bf == 0)
            {
   
   
                break;
            }
            else if (parent->_bf == 2 || parent->_bf == -2)
            {
   
   
                //旋转处理
                if (parent->_bf == 2 && cur->_bf == 1)
                    RotateL(parent);//左单旋
                else if (parent->_bf == -2 && cur->_bf == -1)
                    RotateR(parent);//右单旋
                else if (parent->_bf == -2 && cur->_bf == 1)
                    RotateLR(parent);
                else if (parent->_bf == 2 && cur->_bf == -1)
                    RotateRL(parent);
                else
                    assert(false);
                break;
            }
            else
                assert(false);
        }
        return true;
    }

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

    //判断是否为平衡二叉树
    bool IsBalance()
    {
   
   
        return _IsBalance(_root);
    }

    //求树的高度
    int Height()
    {
   
   
        return _Height(_root);
    }

private:
    // 判断是否为平衡二叉树子函数
    bool _IsBalance(Node* root)
    {
   
   
        if (root == nullptr)
            return true;
        //根据平衡因子判断二叉树是否为平衡二叉树
        int leftH = _Height(root->_left);
        int rightH = _Height(root->_right);

        if (rightH - leftH != root->_bf) {
   
   
            cout << "节点的平衡因子异常" << endl;
            return false;
        }

        //这里需要判断每棵子树的高度差及平衡因子是否符合要求
        return abs(rightH - leftH) < 2
            && _IsBalance(root->_left)
            && _IsBalance(root->_right);
    }

    //求树的高度
    int _Height(Node* root)
    {
   
   
        if (root == nullptr)
            return 0;
        int leftHight = _Height(root->_left);
        int rightHight = _Height(root->_right);

        return leftHight > rightHight ? leftHight + 1 : rightHight + 1;
    }

    //左单旋
    void RotateL(Node* parent)
    {
   
   
        Node* subR = parent->_right;
        Node* subRL = subR->_left;

        parent->_right = subRL;

        //这里我们需要注意一下,subRL有可能为空
        if (subRL)
            subRL->_parent = parent;

        Node* ppnode = parent->_parent;
        subR->_left = parent;
        parent->_parent = subR;

        //判断父节点是否为_root节点
        if (parent == _root) {
   
   
            _root = subR;
            _root->_parent = nullptr;
        }
        else {
   
   
            if (parent == ppnode->_left)
                ppnode->_left = subR;
            else
                ppnode->_right = subR;
            subR->_parent = ppnode;
        }

        //调节平衡因子
        parent->_bf = subR->_bf = 0;
    }

    //右单旋
    void RotateR(Node* parent)
    {
   
   
        Node* subL = parent->_left;
        Node* subLR = subL->_right;

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

        //保存parent节点的_parent节点
        Node* ppnode = parent->_parent;

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

        //判断parent节点是否为_root节点
        if (parent == _root)
        {
   
   
            _root = subL;
            _root->_parent = nullptr;
        }
        else
        {
   
   
            if (parent == ppnode->_left)
                ppnode->_left = subL;
            else
                ppnode->_right = subL;
            subL->_parent = ppnode;
        }
        //调节平衡因子
        subL->_bf = parent->_bf = 0;
    }

    //左右双旋
    void RotateLR(Node* parent)
    {
   
   
        Node* subL = parent->_left;
        Node* subLR = subL->_right;

        int bf = subLR->_bf;

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

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

    //右左双旋
    void RotateRL(Node* parent)
    {
   
   
        Node* subR = parent->_right;
        Node* subRL = subR->_left;

        int bf = subRL->_bf;

        RotateR(parent->_right);//右单旋
        RotateL(parent);//左单旋

        //调整平衡因子
        if (bf == 1)
        {
   
   
            parent->_bf = -1;
            subR->_bf = 0;
            subRL->_bf = 0;
        }
        else if (bf == -1)
        {
   
   
            parent->_bf = 0;
            subR->_bf = 1;
            subRL->_bf = 0;
        }
        else if (bf == 0)
        {
   
   
            parent->_bf = 0;
            subR->_bf = 0;
            subRL->_bf = 0;
        }
        else
            assert(false);
    }

    //中序遍历子函数
    void _InOrder(Node* root)
    {
   
   
        if (root == nullptr)
            return;
        _InOrder(root->_left);
        cout << root->_kv.first << " ";
        _InOrder(root->_right);
    }

private:
    Node* _root = nullptr;
};
//验证是否为AVL树
void TestAVLTree1()
{
   
   
    int a[] = {
   
    4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    AVLTree<int, int> t1;
    for (auto e : a)
    {
   
   
        t1.Insert(make_pair(e, e));
        cout << e << "插入:" << t1.IsBalance() << endl;
    }

    t1.InOrder();
    cout << t1.IsBalance() << endl;
}
//用N个测试用例来验证AVL树
void TestAVLTree2()
{
   
   
    srand(time(0));
    const size_t N = 100000;
    AVLTree<int, int> t;
    for (size_t i = 0; i < N; ++i)
    {
   
   
        size_t x = rand() + i;
        t.Insert(make_pair(x, x));
        //cout << t.IsBalance() << endl;
    }
    t.InOrder();
    cout << t.IsBalance() << endl;
    cout << t.Height() << endl;
}

相关文章
|
25天前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
37 2
|
3月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树:它以苏联科学家Georgy Adelson-Velsky和Evgenii Landis的名字命名。
32 2
|
4月前
|
C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
42 5
|
5月前
|
C++
【C++】手撕AVL树(下)
【C++】手撕AVL树(下)
59 1
|
5月前
|
算法 测试技术 C++
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
40 2
|
5月前
|
Java C++ Python
【C++】手撕AVL树(上)
【C++】手撕AVL树(上)
57 0
|
6月前
|
C++
【c++】avl树
【c++】avl树
41 0
|
23天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
37 2
|
29天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
79 4