C++之AVL树

一.介绍

作为对二叉搜索树的优化版本。AVL树是由俄罗斯的两位数学家G.M.Adelson-Velskii和E.M.Landis发明的,并以名字的首字母命名。

AVL树(可以为空树),在二叉搜索树的基础上具有以下性质:

  • 它的左右子树都是AVL树

  • 左右子树高度差(简称平衡因子)的绝对值不超过1(-1/0/1)

    平衡因子 = 右子树高度 - 左子树高度
    在这里插入图片描述

示例三图,都符合AVL树的规则:在二叉搜索树的继承上,每个节点的平衡因子都是-1、0、1

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

有N个结点的AVL树,,其高度 H ≈ l o g 2 N H \approx log_2N Hlog2N,则其查找的时间复杂度可以控制在 O ( l o g 2 N ) O(log_2N) O(log2N)

二.简单实现AVL树

1. 基本框架

template<class K>
struct AVLTreeNode
{
	AVLTreeNode<K>* _left;
	AVLTreeNode<K>* _right;
	AVLTreeNode<K>* _parent;

	K _key;
	int _bf;  // balance factor

	AVLTreeNode(const K& key)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _key(key)
		, _bf(0) //新节点平衡因子为0
	{}
};

template<class K>
struct AVLTree
{
	typedef AVLTreeNode<K> Node;
private:
	Node* _root = nullptr;
};

在这里插入图片描述

_root:指向AVL树的根节点

使用三叉链的结构(_parent),是为了后续的操作。

2. 插入结点(Insert)

由于AVL树的节点插入,可能会引起子树高度的变化,从而改变平衡因子,导致其不再符合AVL树的规则。

步骤:

  1. 按照搜索二叉树进行插入新节点
  2. 更新AVL树节点的平衡因子,当不再平衡时,进行旋转调整平衡

a. 更新平衡因子

平衡因子更新规则:

子树的高度改变,则需要更新其父节点的平衡因子;

直到更新到根节点或左右子树平衡(_bf=0)或者违反规则( _bf=2/-2)停止。

  • 新节点在父节点的左边,父节点的平衡因子–

  • 新节点在父节点的右边,父节点的平衡因子++
    在这里插入图片描述

节点 _bf(平衡因子)更改后的操作:

平衡因子_bf操作解析
1或-1向其父节点更新_bf子树的高度改变
0停止更新左右子树高度相等(平衡)
2或-2停止更新,继续旋转违反规则,需要进行调整
bool Insert(const K& key)
{
    // 插入第一个节点
    if (nullptr == _root)
    {
        _root = new Node(key);
        return true;
    }

    // 插入新节点
    Node* cur = _root;
    Node* parent = nullptr;
    //寻找新节点位置(即cur到空时)
    while (cur)
    {
        parent = cur;
        if (cur->_key > key) // 在左子树
        {				
            cur = cur->_left;
        }
        else if (cur->_key < key) // 在右子树
        {				
            cur = cur->_right;
        }
        else // 已存在,退出
        {
            return false;
        }
    }


    cur = new Node(key);

    if (parent->_key < key)//新节点的值大于parent
        parent->_right = cur;//放在parent的右边
    else 
        parent->_left = cur;//左边

    cur->_parent = parent;

    //更新平衡因子
    while (cur != _root) // 最坏情况下更新到根节点
    {
        if (parent->_left == cur) //cur在parent左边,平衡因子--
            parent->_bf--;
        else                      //cur在parent右边,平衡因子++
            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)//违反规则
        {				
            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)
            {
                RotateRL(parent); //右左双旋
            }
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                RotateLR(parent); //左右双旋
            }
            break;
        }
        else
        {
            assert(false);//出现问题,终止程序
        }
    }
    return true;
}

旋转的情况详情在下面


对于节点的 _bf=2或-2 时,旋转的情况大致分为以下4种

b. 左单旋

在这里插入图片描述

构成左单旋的条件是:parent=2 && subR=1

左旋:即节点11与节点22的结构 逆时针(向左)旋转了一下

步骤:

  1. 让subRL成为parent的右子树
  2. 将parent作为subR的左子树
  3. subR成为根(子树/树)
  4. 更新parent与subR的平衡因子
void RotateL(Node* parent)
{
    Node* pParent = parent->_parent;//parent的父节点
    Node* subR = parent->_right;//parent的右孩子
    Node* subRL = subR->_left;//subR的左孩子

    //让subRL成为parent的右子树
    parent->_right = subRL;
    if (subRL)//如果subRL不为空,更改其父指针
    {
        subRL->_parent = parent;
    }

    //将parent作为subR的左子树
    subR->_left = parent;
    parent->_parent = subR;

    //subR成为根(树/子树)
    if (_root == parent)//parent为根节点
    {
        _root = subR;
        _root->_parent = nullptr;
    }
    else //parent为一棵子树
    {
        //将subR链接到pParent上
        if (pParent->_right == parent)
            pParent->_right = subR;
        else
            pParent->_left = subR;

        subR->_parent = pParent;
    }

    // 更新parent与subR的平衡因子
    parent->_bf = subR->_bf = 0;
}

c. 右单旋

在这里插入图片描述

产生左单旋的条件是:parent=-2 && subL=-1

右旋:即节点23与节点20的结构 顺时针(向右)旋转了一下

步骤:

  1. 让subLR成为parent的左子树
  2. 将parent作为subL的右子树
  3. subL成为根(子树/树)
  4. 更新parent与subL的平衡因子
void RotateR(Node* parent)
{
    Node* pParent = parent->_parent;//parent的父节点
    Node* subL = parent->_left;//parent的左孩子
    Node* subLR = subL->_right;//subL的右孩子

    //让subLR成为parent的左子树
    parent->_left = subLR;
    if (subLR)//如果subLR不为空,更改其父指针
    {
        subLR->_parent = parent;
    }

    // 将parent作为subL的右子树
    subL->_right = parent;
    parent->_parent = subL;

    // subL成为根(子树/树)
    if (parent == _root)//parent为根节点
    {
        _root = subL;
        _root->_parent = nullptr;
    }
    else  //parent为一棵子树
    {
        //将subR链接到pParent上
        if (pParent->_right == parent)
            pParent->_right = subL;
        else
            pParent->_left = subL;

        subL->_parent = pParent;
    }

    // 更新parent与subL的平衡因子
    subL->_bf = parent->_bf = 0;
}

d. 左右双旋

在这里插入图片描述

由图左右双旋分为三种情况

在这里插入图片描述

可以发现插入位置不同,subLR->_bf=-1/1/0不同,导致右左旋转后平衡因子不同。

步骤:

  1. 将subL为根的子树左旋
  2. 将parent为根的子树右旋
  3. 更新parent、subL和subLR的平衡因子
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;//parent的左孩子
    Node* subLR = subL->_right;//subL的右孩子

    int bf = subLR->_bf; // 旋转前subLR的平衡因子

    RotateL(subL);//左旋
    RotateR(parent);//右旋

    // 更新平衡因子
    if (bf == -1)
    {
        parent->_bf = 1;			
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == 1)
    {
        parent->_bf = 0;			
        subL->_bf = -1;
        subLR->_bf = 0;
    }
    else if (bf == 0)
    {
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else
        assert(false); //错误
}

e. 右左双旋

在这里插入图片描述

由图右左双旋分为三种情况,触发条件为parent=2 && subR=-1

在这里插入图片描述

可以发现插入位置不同,subRL->_bf=-1/1/0不同,导致右左旋转后平衡因子不同。

步骤:

  1. 将subL为根的子树左旋
  2. 将parent为根的子树右旋
  3. 更新parent、subR和subRL的平衡因子
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;//parent的右孩子
    Node* subRL = subR->_left;//subR的左孩子

    int bf = subRL->_bf; //旋转前subRL的平衡因子

    RotateR(subR);//右旋
    RotateL(parent);//左旋

    // 更新三种情况的平衡因子
    if (bf == -1)
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else if (bf == 1)
    {
        parent->_bf = -1;
        subR->_bf = 0;			
        subRL->_bf = 0;
    }
    else if (bf == 0)
    {
        parent->_bf = 0;
        subR->_bf = 0;			
        subRL->_bf = 0;
    }
    else
        assert(false);//错误
}

3. 删除节点(Erase)

和插入类似,删除节也可能会引起子树高度的变化,从而改变平衡因子,导致其不再符合AVL树的规则。

步骤:

  1. 按照搜索二叉的操作删除节点
  2. 更新AVL树节点的平衡因子,当不再平衡时,进行旋转调整平衡

a. 更新平衡因子

平衡因子更新规则:

子树的高度改变,则需要更新其父节点的平衡因子;

直到更新到根节点或左右子树平衡(_bf=0)或者违反规则( _bf=2/-2)停止。

  • 删除节点在父节点的左边,父节点的平衡因子++
  • 删除节点在父节点的右边,父节点的平衡因子–
    在这里插入图片描述

节点 _bf(平衡因子)更改后的操作:

平衡因子_bf操作解析
1或-1停止更新删除前_bf=0,其左右子树高度H相等。删除后,该节点为根的子树高度为H+1不变
0向其父节点更新_bf删除前_bf=-1/1,左子树高或右子树高。删除后左右子树高度一样,即该节点为根的子树高度变小(减1)
2或-2停止更新,继续旋转违反规则,需要进行调整

b. 旋转

对于节点的 _bf=2或-2 时,旋转的情况大致分为以下6种

在这里插入图片描述

1)parent的_bf为 2 , subR的 _bf为 1----》左旋

2)parent的_bf为 -2 , subL的 _bf为 -1----》右旋

3)parent的_bf为 2 , subR的 _bf为 0----》左旋

4)parent的_bf为 -2 , subL的 _bf为 0----》右旋

5)parent的_bf为 -2 , subL的 _bf为 -1----》左右双旋

6)parent的_bf为 2 , subR的 _bf为 -1----》右左双旋

在这里插入图片描述

3)左单旋后,需更新平衡因子parent=1、subR=-1

4)右单旋后,需更新平衡因子parent=-1、subL=1

除3)和4)外,其他4种旋转和插入的旋转相同。3)和4)旋转后新子树根的_bf=-1/1,停止更新;但是其他4种情况中,旋转后新子树根的 _bf=0,因此旋转后任需向上更新。

c. 代码

bool Erase(const K& key)
{
    if (_root == nullptr)// 空树,退出
        return false;

    Node* cur = _root;//当前节点
    Node* parent = nullptr;//当前节点的父节点

    Node* del = nullptr;    // 要删除的节点
    Node* pDel = nullptr; // 要删除节点的父节点

    //查找要删除的点
    while (cur)
    {
        if (cur->_key < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_key > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else // 找到
        {

            if (cur->_left == nullptr) //所删除节点的左子树为空
            {
                if (cur == _root) //如果是删除根节点
                {
                    _root = cur->_right;

                    if (_root)//如果cur->_right不为空
                        _root->_parent = nullptr;

                    delete cur;
                    return true;
                }
                else
                {
                    // 记录要删除的节点和其父节点
                    del = cur;
                    pDel = parent;
                }
            }
            else if (cur->_right == nullptr) // 待删除节点的右子树为空
            {
                if (cur == _root) //如果是删除根节点
                {
                    _root = cur->_left;

                    if (_root)//如果cur->_left不为空
                        _root->_parent = nullptr;

                    delete cur;
                    return true;
                }
                else
                {
                    // 记录要删除的节点和其父节点
                    del = cur;
                    pDel = parent;
                }
            }
            else //左右都不为空
            {
                // 找到待删除节点右子树中的最左节点进行替换删除
                Node* minRight = cur->_right;//右子树的最小值节点
                Node* minParent = cur;//minRight的父节点

                while (minRight->_left)//即右子树的最左节点
                {
                    minParent = minRight;
                    minRight = minRight->_left;
                }

                // 交换值
                std::swap(cur->_key, minRight->_key);

                // 记录要删除的节点和其父节点
                del = minRight;
                pDel = minParent;
            }
            break;
        }
    }

    // 遍历结束,没有找到待删除节点
    if (cur == nullptr)
        return false;

    //更新平衡因子
    cur = del;
    parent = pDel;

    while (cur != _root)// 最坏情况下更新到根节点
    {
        if (cur == parent->_left)//cur在parent左边,平衡因子++
        {
            parent->_bf++;
        }
        else if (cur == parent->_right)//cur在parent右边,平衡因子--
        {
            parent->_bf--;
        }

        if (parent->_bf == 0)//继续向上更新
        {
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            break;//停止更新
        }
        else if (parent->_bf == 2 || parent->_bf == -2)//旋转
        {
            if (parent->_bf == 2 && parent->_right->_bf == 1)
            {
                Node* tmp = parent->_right; //记录左单旋转后的根节点
                RotateL(parent);//左单旋
                parent = tmp; // 更新parent
            }
            else if (parent->_bf == -2 && parent->_left->_bf == -1)
            {
                Node* tmp = parent->_left;//记录右单旋转后的根节点
                RotateR(parent);//右单旋
                parent = tmp;
            }
            else if (parent->_bf == 2 && parent->_right->_bf == 0)
            {
                Node* tmp = parent->_right; //记录左单旋转后的根节点
                RotateL(parent);//左单旋
                parent = tmp;

                //更新平衡因子
                parent->_bf = -1;
                parent->_left->_bf = 1;

                break;//停止更新
            }
            else if (parent->_bf == -2 && parent->_left->_bf == 0)
            {
                Node* tmp = parent->_left;//记录右单旋转后的根节点
                RotateR(parent);//右单旋
                parent = tmp;

                //更新平衡因子
                parent->_bf = 1;
                parent->_right->_bf = -1;

                break;//停止更新
            }
            else if (parent->_bf == -2 && parent->_left->_bf == 1)
            {
                Node* tmp = parent->_left->_right;//记录左右双旋转后的根节点
                RotateLR(parent);//左右双旋
                parent = tmp;
            }
            else if (parent->_bf == 2 && parent->_right->_bf == -1)
            {
                Node* tmp = parent->_right->_left;//记录右左双旋转后的根节点
                RotateRL(parent);//右左双旋转
                parent = tmp;
            }
            else
            {					
                assert(false);// 出现错误
            }
            //继续向上更新
            cur = parent;
            parent = parent->_parent;
        }
    }

    if (del->_left == nullptr)//所删除节点的左子树为空
    {
        //将cur的右子树链接到父节点
        if (del == pDel->_left)
        {
            pDel->_left = del->_right;
            if (del->_right)
                del->_right->_parent = pDel;
        }
        else
        {
            pDel->_right = del->_right;
            if (del->_right)
                del->_right->_parent = pDel;
        }
    }
    else// 待删除节点的右子树为空
    {
        //将cur的左子树链接到父节点
        if (del == pDel->_left)
        {
            pDel->_left = del->_left;
            if (del->_left)
                del->_left->_parent = pDel;
        }
        else
        {
            pDel->_right = del->_left;
            if (del->_left)
                del->_left->_parent = pDel;
        }
    }

    delete del;// 删除节点
    return true;
}

4. 测试

为了验证上述的插入(Insert)、删除(Erase)函数编写的正确性,可以检查操作后的AVL树是否还符合规则。

//是否平衡yes:1,no:0
bool IsBalance()
{
    return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
    if (root == nullptr)
    {
        return true;//空树是AVL树
    }

    int leftHT = Height(root->_left);
    int rightHT = Height(root->_right);

    //平衡因子=右子树高度-左子树高度
    int bf = rightHT - leftHT;

    if (bf != root->_bf)//如果不同,则报错
    {
        cout << root->_key << "平衡因子异常" << endl;
        return false;
    }

    return abs(bf) < 2		//平衡因子的绝对值小于2
        && _IsBalance(root->_left)	//左右子树也为AVL树
        && _IsBalance(root->_right);
}

int Height(Node* root)//求树高
{
    if (root == nullptr)
        return 0;

    return max(Height(root->_left), Height(root->_right)) + 1;
}

示例:

void TestAVLTree3()
{
	size_t N = 10000;
	srand(time(0));
	AVLTree<int> t1;

	//插入N次随机数
	int count = 0;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand();
		if(t1.Insert(x)) ++count;
	}
	cout << "插入成功次数:" << count << endl;

	//删除N次随机数
	count = 0;
	for (int i = 0; i < N; ++i)
	{
		int x = rand();
		if (t1.Erase(x)) ++count;
	}
	cout << "删除成功次数:" << count << endl;

	//是否仍是AVL树
	cout << "IsBalance:" << t1.IsBalance() << endl;
}

int main()
{
	TestAVLTree();
	return 0;
}

在这里插入图片描述


🦀🦀观看~

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值