【C++】AVL树的讲解和编写

目录

一,AVL树认识

二,AVL树的实现

2-1,AVL树节点的定义

2-2,AVL树的插入框架

2-3,AVL树的旋转

2-4,AVL树的总设计

2-5,AVL树的验证

2-6,AVL树的测试


前言:

        二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序时,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度,即AVL树(一种平衡二叉搜索树)。


一,AVL树认识

        AVL树是具有此性质的二叉搜索树:任何左右子树高度之差(简称平衡因子)的绝对值不超过1,即平衡因子大小为-1/0/1。这里全部将右子树高度减去左子树高度为平衡因子,如下图:

        由此可看出,如果一棵AVL树有n个结点,其高度可保持在 O(log n),搜索时间复杂度在O(log n)

        注意:AVL树虽也是平衡二叉搜索树,但它不是红黑树。红黑树和AVL树虽都是自平衡的二叉搜索树,但它们之间有一些关键的区别。


二,AVL树的实现

2-1,AVL树节点的定义

        这里我们定义的AVL树来实现存储pair键值对型的数据,具体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)
    {    }
};

2-2,AVL树的插入框架

        AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:1. 按照二叉搜索树的方式插入新节点  2. 调整节点的平衡因子

        这里要注意,插入新节点后,可能会出现不平衡现象,若插入后不平衡,这时就需要进行旋转处理,将不平衡转变为平衡且要满足搜素规则。

        插入大致框架思路设计:若插入节点为根节点root,这里可直接结束;若插入节点不为根节点root,双亲结点的平衡因子需做出调正,这里我们将平衡因子设置为右子树减去左子树,即若插入结点在双亲结点 parent 的左边,parent->_bf--;若插入在双亲结点 parent 的右边,p->_bf++。这里要注意,插入后的结点也可能会影响爷爷结点或更高结点的平衡因子,至于是否影响还需看parent的平衡因子。不难发现,插入新节点后,若 parent->_bf == 0,说明parent所在的子树高度不变,在插入前,parent的_bf是1或-1,插入的结点在parent矮的那边,使得左右子树均匀,不影响爷爷结点或更高结点的平衡因子,这种情况直接可结束。若 parent->_bf == -1或1,说明parent所在的子树高度发生变化,会影响爷爷结点或更高结点的平衡因子,这里就要往上依次检查各个爷爷结点的平衡因子,若存在结点的平衡因子变为2或-2,这里就要进行旋转处理,(旋转的逻辑下面会具体分析,这里先明白逻辑框架)。这里旋转后会让平衡因子变为2或-2的回到插入之前,不会对上层有影响,因此旋转后可直接结束。代码设计如下:

bool Insert(const pair<K, V>& kv)
{
    //插入节点
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }
    Node* cur = _root;
    Node* parent = _root->_parent;
    while (cur)
    {
        if (cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else
        {
            return false;
        }
    }
    if (parent->_kv.first > kv.first)
    {
        parent->_left = new Node(kv);
        cur = parent->_left;
    }
    else
    {
        parent->_right = new Node(kv);
        cur = parent->_right;
    }
    cur->_parent = parent;

    //平衡因子的更新
    while (parent)
    {
        //更新平衡因子
        if (parent->_left == cur)
        {
            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) //平衡被破坏,旋转处理
        {
            //这里的代码进行旋转
        }
        else //表示原本的AVL树就出错
        {
            assert(false);
        }
    }
    return true;
}

2-3,AVL树的旋转

        旋转发生在插入结点后平衡失调的情况下。在以上代码中,一旦往上发现parent平衡失调就要开始旋转,旋转的目的就是要将不平衡变为平衡,即回到插入之前。旋转的方式跟插入结点的位置有关。插入结点共有四种方式:插入在parent左子树的左侧、插入在parent右子树的右侧、插入在parent左子树的右侧、插入在parent右子树的左侧。因此,对应的旋转方式也有四种。

1,左单旋

        当插入的结点在parent右子树的右侧而发生的旋转叫做左单旋。这里右子树的右侧表示右侧子树中的左右都可以。这里对应的表现形式如下图:

        有些人可能比较疑惑,为什么插入前的原图中a、b、c子树结构的高度都为h,只要满足平衡条件高度差不是可以随意吗?确实是这样,至于为什么会这样我们来往下分析情况。

        首先,我们要明白,上图中结构在插入前是一颗AVL树,插入后要想满足左单选的条件,上面插入前的原图中数据为30结点的平衡因子必须为1,插入后变为2,即c树中插入结点后高度必加1且插入后数据为60结点的高度满足平衡条件,因为我们在往上排查中发现的是最近一个结点的平衡失调,而原图中30所对应的结点是parent结点。至于其它的信息暂时不明确,我们往下分析。将a子树高度设为h(h >= 0),那么b和c两颗子树对应的高度只能有三种情况。

        1,b子树高度为h,c子树高度为h - 1。这种情况显然是不满足要求的,因为我们往c树中插入结点后,c树高度加1,变为h - 1 + 1 = h,parent对应的是数据为30的结点,parent->_bf = 1,满足平衡条件,根本不用旋转,所以这种情况根本不可能出现。

        2,b子树高度为h - 1,c子树高度为h。这时往c结构插入后,c树高度变为h + 1,数据为60的结点对应的平衡因子为h + 1 - (h - 1) = 2,不满足平衡条件,所以这种情况也不可能出现。

        3,b子树高度为h,c子树高度为h。这时插入后,60对应结点的平衡因子为1,parent->_bf == 2,满足所有条件。

        综上所述,发生左单旋时,a、b、c三课子树的高度只能都是h(h >= 0)。

        出现以上情况的旋转很简单,只要将parent右子树高度减1,左子树高度增1,这样一来就满足了插入之前的平衡模样。具体的左单旋旋转规则是这样的,令cur为上图中60对应的结点,将parent的右孩子指向b,cur的左孩子指向parent,这里要注意里面双亲结点的重新连接,最后更新平衡因子即可。

        以上逻辑的实现有两个坑:1,h为0的情况,这里要注意b子树结构,a和c不影响。2,parent是根节点和不是根节点的情况。

//左单旋: 插入在右子树的右侧(右侧:右侧子树中的左右都可以)

void RotateL(Node* parent)
{
    Node* cur = parent->_right;
    Node* curL = cur->_left;
    Node* pparent = parent->_parent;

    //旋转
    parent->_right = curL;
    if (curL)   //子树高度h不等于0,即为空树时的连接情况
        curL->_parent = parent;

    //更新节点之间的关联
    if (parent == _root)
    {
        _root = cur;
        _root->_parent = nullptr;
    }
    else
    {
        if (pparent->_left == parent)
        {
            pparent->_left = cur;
            cur->_parent = pparent;
        }
        else
        {
            pparent->_right = cur;
            cur->_parent = pparent;
        }
    }

    //旋转
    cur->_left = parent;
    parent->_parent = cur;

    //更新平衡因子,如上图,parent和cur的平衡因子都为0
    parent->_bf = 0;
    cur->_bf = 0;
}

2,右单旋

        当插入的结点在parent左子树的左侧而发生的旋转叫做右单旋。这里左子树的左侧表示左侧子树中的左右都可以。

        右单旋的逻辑思想以及结构跟左单旋基本相似,只不过这里是将parent左子树高度减1,右子树高度增1,这样一来就满足了插入之前的平衡模样。右单旋旋转规则是这样的,令cur为上图中30对应的结点,将parent的左孩子指向b,cur的右孩子指向parent,这里要注意里面双亲结点的重新连接,最后更新平衡因子即可。右单旋的注意事项要素与左单旋一样,这里不再说明,对应的表现形式如下图:

//右单旋: 插入在左子树的左侧(左侧:左侧子树中的左右都可以)

void RotateR(Node* parent)
{
    Node* cur = parent->_left;
    Node* curR = cur->_right;
    Node* pparent = parent->_parent;

    //旋转
    parent->_left = curR;
    if (curR)
        curR->_parent = parent;

    //更新节点之间的关联
    if (parent == _root)
    {
        _root = cur;
        _root->_parent = nullptr;
    }
    else
    {
        if (pparent->_left == parent)
        {
            pparent->_left = cur;
            cur->_parent = pparent;
        }
        else
        {
            pparent->_right = cur;
            cur->_parent = pparent;
        }
    }

    //旋转
    cur->_right = parent;
    parent->_parent = cur;

    //更新平衡因子
    parent->_bf = 0;
    cur->_bf = 0;
}

3,左右旋

        当插入在parent左子树的右侧时,要进行的旋转是左右旋,即先左单旋再右单旋。这里树结构的原理跟左单旋的原理一样——a高度h,b高度h - 1,c高度h - 1,d高度h(h >= 1)。但是这里存在特殊情况,即当a、b、c、d为空树时照样满足旋转条件,这时60为新插入的节点。

        这种情况的旋转将不能简单的进行上面那种单旋转,若只进行上面那种单旋,你会发现平衡依旧被破坏。这里左右旋的旋转规则是进行两次旋转,将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。

        这里的旋转问题不大,只需调用左右两次单旋即可,但问题是平衡因子的更新,由于插入情况是在左子树的右侧,所以插入的位置可能会是上图中的b子树或c子树(图中插入在b子树展示)。虽然无论插入在哪里都不会影响旋转后的目的,但是通过上图中可发现,平衡后30和90的平衡因子会因此出现不同的变化,60的平衡因子为0。平衡前90的平衡因子固定为-1,30的平衡因子固定为0,但是60的平衡因子可能为-1(新节点插入在b树结构中)也可能为1(新节点插入在c树结构中),且还有a、b、c、d都为空树时的情况。

        当插入后60的平衡因子为-1时(插入在b树结构),这里的情况如上图般,旋转后可看出30的平衡因子为0,90的平衡因子为1。

        当插入后60的平衡因子为1时(插入在c树结构),此时上图中b树高度应改为h - 1,c树高度应改为h,此时旋转后30的平衡因子为-1,90的平衡因子为0。

        当a、b、c、d都为空树时,此时就没有a、b、c、d结构,新插入的节点对应上图中的60,此时通过上图可发现,30,60,90的平衡因子都为0。

//左右旋: 插入在左子树的右侧,先左单旋再右单旋
void RotateLR(Node* parent)
{
    Node* cur = parent->_left;
    Node* curR = cur->_right;
    int curRbf = curR->_bf;

    //先左再右两次旋转
    RotateL(cur);
    RotateR(parent);

    //更新平衡因子
    if (curRbf == -1) //插入在b树,即curR的左边
    {
        curR->_bf = 0;
        cur->_bf = 0;
        parent->_bf = 1;
    }
    else if (curRbf == 1) //插入在c树,即curR的右边
    {
        curR->_bf = 0;
        cur->_bf = -1;
        parent->_bf = 0;
    }
    else if (curRbf == 0)
    {
        curR->_bf = 0;
        cur->_bf = 0;
        parent->_bf = 0;
    }
    else  //这里表示原本结构就出错的情况
    {
        assert(false);
    }
}

4,右左旋

        当插入在parent右子树的左侧时,要进行的旋转是右左旋,即先右单旋再左单旋。这种情况跟左右旋类似,结构如下图(图中演示插入在c树结构中的情况)。

        类比左右单旋,这里也分为三种情况:1,插入在b树中。2,插入在c树中。3,60为插入的新节点。通过上图分析不难发现,无论那种情况60所对应的高度旋转后都为0。

        当插入在b树中,此时上图中b树高度为h,c树高度为h - 1。30所对应的高度为0,90所对应的高度为1。

        当插入在c树中,此时如上图般,30所对应的高度为-1,90所对应的高度为0。

        当60为新插入的节点时,此时情况基本与左右旋一样,30和90所对应的高度都为0。

//右左旋:插入在右子树的左侧,先右单旋再左单旋
void RotateRL(Node* parent)
{
    Node* cur = parent->_right;
    Node* curL = cur->_left;
    int curLbf = curL->_bf;

    //先右再左两次旋转
    RotateR(cur);
    RotateL(parent);

    //更新平衡因子
    if (curLbf == -1) //插入b树中,即curL的左边
    {
        curL->_bf = 0;
        cur->_bf = 1;
        parent->_bf = 0;
    }
    else if (curLbf == 1) //插入c树中,即curL的右边
    {
        curL->_bf = 0;
        cur->_bf = 0;
        parent->_bf = -1;
    }
    else if (curLbf == 0)
    {
        curL->_bf = 0;
        cur->_bf = 0;
        parent->_bf = 0;
    }
    else   //这里表示原本结构就出错的情况
    {
        assert(false);
    }
}

2-4,AVL树的总设计

        旋转的方式一共对应插入在parent左子树的左侧、右子树的右侧、左子树的右侧、右子树的左侧四种,这时在以上代码框架中所对应的parent和cur的平衡因子会发生不同的变化,我们可以根据此变化来实现对应的旋转。

........ //这里对应的是上面框架中的代码

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) //右单旋
    {
        //注意: 以下代码中p只是变量parent的别名,而变量Node* parent中存储的是节点地址
        //这里的p不是节点空间地址的别名,p的改变只会影响parent变量的数值,不会影响节点本身
        /*Node* pparent = parent->_parent;
        Node*& p = parent;
        p = 0;*/  //这里parent的指向也为0,但节点本身的数据和指向不会改变

        RotateR(parent); //所以这里不能使用指针引用,以下同理
    }
    else if (parent->_bf == -2 && cur->_bf == 1) //左右旋
    {
        RotateLR(parent);
    }
    else  //右左旋 
    {
        RotateRL(parent);
    }
    break; //因为旋转完毕之后平衡恢复如初,可直接退出
}

.......... //这里对应的是上面框架中的代码

2-5,AVL树的验证

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

        1. 验证其为二叉搜索树——如果中序遍历可得到一个从小到大的有序序列,就说明为二叉搜索树。此种方式只需调用中序遍历算法查看即可,不用专门验证。

        2. 验证其为平衡树——平衡树的验证分为两步:第一步,每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)。第二步,节点的平衡因子是否计算正确。

        注意:这里AVL树的验证不能直接使用平衡因子_bf,因为若是平衡因子如若出错将导致失败。这里应在原树结构上入手,可使用左右子树的高度差来验证平衡。

bool IsBalance()
{
    if (!_root)
        assert(false);
    return _IsBalance(_root);
}

bool _IsBalance(const Node* root) 
{
    if (!root)
        return true;
    int leftheight = _Height(root->_left);
    int rightheight = _Height(root->_right);
    if (abs(rightheight - leftheight) >= 2)  //平衡失调
        return false;
    if (rightheight - leftheight != root->_bf) //平衡因子出错
        return false;
    return _IsBalance(root->_left) && _IsBalance(root->_right);
}

        不难发现,此种判断是一种前序的判断,会多次重复计算下层的高度,效率太低。我们可以使用后序法进行检查。后序判断法不会多次重复计算下层高度,效率得到很大的提高。

bool _IsBalance(const Node* root, int& height) 
{
    if (!root)
        return true;
    int leftheight = 0, rightheight = 0;
    if (!_IsBalance(root->_left, leftheight) || !_IsBalance(root->_right, rightheight))
    {
        return false;
    }
    if (abs(rightheight - leftheight) >= 2)  //平衡失调
        return false;
    if (rightheight - leftheight != root->_bf) //平衡因子出错
        return false;
    height = leftheight > rightheight : leftheight + 1 ? rightheight + 1;
    return true;
}

2-6,AVL树的测试

        在AVL树的测试这一点上,如若发现出错进行一步步调试可发现比较艰难。这里可先在几个比较敏感的地方进行输出,然后自己设置条件断点(注意:有些编译器上可能没有条件断点的设置,这里可自己编写),总代码如下:

#pragma once
#include <iostream>
#include <vector>
#include <assert.h>
#include <utility>
using namespace std;
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:
    AVLTree(): _root(nullptr){    }

.......//AVL树的插入算法,这里不在写入,如上

 

.......//AVL树的四个旋转算法,这里不在写入,如上   

    //AVL树高度
    int Height()
    {
        if (!_root) return 0;
        return _Height(_root);
    }
    int _Height(const Node* root)
    {
        if (!root) return 0;
        int leftheight = _Height(root->_left);
        int rightheight = _Height(root->_right);
        return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
    }

    //平衡检查
    bool IsBalance()
    {
        if (!_root) assert(false);

        int height = 0;
        return _IsBalance(_root,height);
    }
    //此方法依赖于平衡因子,一旦平衡因子出问题将导致失败
    //bool _IsBalance(const Node* root) 
    //{
    //    if (!root)
    //        return true;
    //    if (root->_bf >= 2 || root->_bf <= -2)
    //        return false;
    //    bool leftcheck = _IsBalance(root->_left);
    //    if (leftcheck == false)
    //        return false;
    //    bool rightcheck = _IsBalance(root->_right);
    //    if (rightcheck == false)
    //        return false;
    //    return true;
    //}
    //此种判断是一种前序的判断,会多次重复计算下层的高度,效率太低
    //bool _IsBalance(const Node* root) 
    //{
    //    if (!root)
    //        return true;
    //    int leftheight = _Height(root->_left);
    //    int rightheight = _Height(root->_right);
    //    if (abs(rightheight - leftheight) >= 2)  //平衡失调
    //        return false;
    //    if (rightheight - leftheight != root->_bf) //平衡因子出错
    //        return false;
    //    return _IsBalance(root->_left) && _IsBalance(root->_right);
    //}

    //后序判断方法,不会多次重复计算下层高度,效率较高
    bool _IsBalance(const Node* root, int& height) 
    {
        if (!root) return true;
        int leftheight = 0, rightheight = 0;

        //后序遍历计算
        if (!_IsBalance(root->_left, leftheight) || !_IsBalance(root->_right, rightheight)) 
            return false;
        if (abs(rightheight - leftheight) >= 2) return false; //平衡失调
        if (rightheight - leftheight != root->_bf) return false; //平衡因子出错
        height = leftheight > rightheight ? leftheight + 1 : rightheight + 1;
        return true;
    }

    //AVL树的节点数量
    size_t Size()
    {
        return _Size(_root);
    }
    size_t _Size(const Node* root)
    {
        if (!root) return 0;
        return _Size(root->_left) + _Size(root->_right) + 1;
    }

     

    //AVL的查找

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

    //中序输出数据(从小到大输出)
    void Inorder()
    {
        cout << "中序节点数据: ";
        _Inorder(_root);
        cout << endl;
    }
    void _Inorder(const Node* root)
    {
        if (!root) return;
        _Inorder(root->_left);
        cout << "(" << root->_kv.first << ", " << root->_kv.second << ")" << " ";
        _Inorder(root->_right);
    }

    //中序输出各节点平衡因子
    void InorderBf()
    {
        cout << "中序平衡因子: ";
        _InorderBf(_root);
        cout << endl;
    }
    void _InorderBf(const Node* root)
    {
        if (!root) return;
        _InorderBf(root->_left);
        cout << root->_bf << "  ";
        _InorderBf(root->_right);
    }

    //测试样例

    void test1()  
    {
        vector<int> v1 = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
        vector<int> v2 = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

        AVLTree<int, int> a;
        for (auto e : v2)
        {
            //调试方法技巧,先在某个地方输出必要数据,然后打出条件断点分析,即自己编写设置条件断点
            //if (e == 14) //因为断点无法设置到空白地方,这里以e等于14为例,
            //{            //这里设置的目的是为了打条件断点,画出在插入14前的树,单步跟踪,分析细节原因
            //    int x = 0;
            //}

            a.Insert(make_pair(e, 0));
        }
        a.Inorder();    //中序遍历数据first
        a.InorderBf();  //中序遍历平衡因子

        cout << a.Height() << endl;  //AVL高度

        if (a.IsBalance()) cout << "平衡" << endl;
        else cout << "不平衡" << endl;

        cout << a.Size() << endl;

        AVLTreeNode<int, int>* p = a.Find(16);
        if (p == nullptr) cout << "不存在" << endl;
        else cout << "存在此节点" << endl;
    }
private:
    Node* _root;
};

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值