[数据结构 C++] AVL树的模拟实现

在这里插入图片描述

问题引入:
在上一篇文章中,我们提到了二叉搜索树在插入时,可能会形成单边树,会降低二叉搜索的性能。因此我们需要平衡二叉搜索树,降低二叉搜索树的高度,使得二叉搜索树趋于一颗完全二叉树的样子,这样就可以提高二叉搜索树的性能。本篇文章就来介绍一种平衡二叉树,AVL树。
在这里插入图片描述

1、AVL树

1.1 AVL树的概念

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
    在这里插入图片描述
    如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log N),搜索时间复杂度O(log N)。
    我们了解了AVL树的基本规则后,下面我们来实现一下AVL树。

2、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)
	{}
};

3、AVL树的插入和旋转

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

当某个节点的平衡因子被修改为2的时候,就需要旋转来调节,因此就存在一下四种旋转方式:

3.1 左单旋

我们将 左单旋的情况抽象出来,如下图所示:
在这里插入图片描述

当 h >= 0,且parent->_bf == 2 && subR->_bf == 1时,触发左旋。
在这个图中,只能是在 c 子树新增,才能触发左旋的条件parent->_bf == 2 && subR->_bf == 1。此时进行左旋。
如果是在 b 子树新增,那么仅仅左旋是不够的,
旋转步骤:将60的左树变为30的右树,将60的左树变为30,最后将parent和subR的平衡因子变为0就完成了左旋。

左旋代码实现

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

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

    if (_root == parent) // 父节点就是根节点
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else // 子树情况
    {
        if (parentParent->_left == parent)
        {
            parentParent->_left = subR;
        }
        else
        {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }
    // 修改平衡因子
    parent->_bf = subR->_bf = 0;
}

3.2 右单旋

我们将 右单旋的情况抽象出来,如下图所示:
在这里插入图片描述
当 h >= 0,且 parent->_bf == 2 && subL->_bf == -1时,触发右旋。
在这个图中,只能是在 a子树新增,才能触发右旋的条件parent->_bf == -2 && subL->_bf == -1。此时进行右旋。
如果是在 b 子树新增,那么仅仅右旋是不够的。
旋转步骤:将30的右树接到60的左树并断开与30的链接,再将60接到30的右树,并将60的父节点改为3,最后再调整parent与SubL的平衡因子为0,就完成整个右旋。

右旋代码实现

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

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

    if (_root == parent) // 父节点是根节点
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else // 子树情况
    {
        if (parentParent->_left == parent)
        {
            parentParent->_left = subL;
        }
        else
        {
            parentParent->_right = subL;
        }
        subL->_parent = parentParent;
    }
    // 修改平衡因子
    parent->_bf = subL->_bf = 0;
}

3.3 右左双旋

我们将 右左双旋的所有情况抽象出来,如下图所示:
在这里插入图片描述

右左双旋的本质是先将子树右旋,让右侧一侧高,再进行整体的左旋,这样就完成了高度的调整。
双旋的插入位置可以是 b/c 子树,此类型插入之后就会触发右左双旋。
旋转步骤:直接复用右旋,再复用左旋即可。不过旋转的基点不同,右旋是以subR为基点,左旋是以parent为基点旋转的。旋转就完成了,难点在于平衡因子的调节。
平衡因子的调节:
这里主要是 记下subRL最初的平衡因子它的平衡因子就代表了插入节点是在subRL的左边还是右边插入的,由此可以推出最终的parent与subR的平衡因子。

  • 当subRL->_bf = -1时,最后parent->_bf = 0,subR->_bf = 1,subRL->_bf = 0;
  • 当subRL->_bf = 0时,最后parent->_bf = 0,subR->_bf = 0,subRL->_bf = 0;
  • 当subRL->_bf = 1时,最后parent->_bf = -1,subR->_bf = 0,subRL->_bf = 0;

右左双旋的代码实现

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

    RotateR(subR);
    RotateL(parent);

    if (bf == 0) // subRL 就是插入的
    {
        parent->_bf = subR->_bf = subRL->_bf = 0;
    }
    else if (bf == 1) // subRL 右边边插入
    {
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if (bf == -1) // subRL 左边插入
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

3.4 左右双旋

我们将 右左双旋的所有情况抽象出来,如下图所示:
在这里插入图片描述

左右双旋与右左双旋的思路是差不多的,我们来看看。
左右双旋的本质是先将子树左旋,让左侧一侧高,在进行整体的右旋,这样就完成了高度的调整。
双旋的插入位置可以是 b/c 子树,此类型插入之后就会触发左右双旋。
旋转步骤:直接复用左旋,再复用右旋即可。不过旋转的基点不同,右旋是以subR为基点,左旋是以parent为基点旋转的。旋转就完成了,难点也是在于平衡因子的调节。
平衡因子的调节:
这里主要是 记下subLR最初的平衡因子它的平衡因子就代表了插入节点是在subLR的左边还是右边插入的,由此可以推出最终的parent与subL的平衡因子。

  • 当subLR->_bf = -1时,最后parent->_bf = 1,subL->_bf = 0,subLR->_bf = 0;
  • 当subLR->_bf = 0时,最后parent->_bf = 0,subL->_bf = 0,subLR->_bf = 0;
  • 当subLR->_bf = 1时,最后parent->_bf = 0,subL->_bf = -1,subLR->_bf = 0;

左右双旋的代码实现

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

    RotateL(subL);
    RotateR(parent);

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

3.5 insert接口实现

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

    Node* parent = nullptr;
    Node* cur = _root;
    // 1、先找到插入的位置
    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;
        }
    }
    // 2、new一个节点,并与parent链接起来
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }
    // 3、调平横 —— 旋转 + 平衡因子的调节
    while (parent)
    {
        if (parent->_left == cur)
        {
            parent->_bf--;
        }
        else
        {
            parent->_bf++;
        }

        if (0 == parent->_bf)
        {
            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)
            {
                RotateRL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                RotateLR(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == -1)
            {
                RotateR(parent);
            }

            // 1、旋转让这颗子树平衡了
            // 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,所以对上一层没有影响,不用继续更新
            break;
        }
        else
        {
            assert(false);
        }
    }
    return true;
}

4、判断是否为AVL树

AVL树的本质是搜索二叉树 + 平衡机制,所以验证步骤:
1、首先判断是否为搜索树,写一个中序遍历,看看是不是升序即可;
2、按照AVL树的性质来判断:

  • 每个节点的左右子树高度差绝对值小于等于1;
  • 节点的平衡因子是否正确;

判断AVL树的代码实现

bool _IsBalance(Node* pRoot)
{
    if (pRoot == nullptr)
        return true;

    int leftHeight = _Height(pRoot->_left);
    int rightHeight = _Height(pRoot->_right);
    if (rightHeight - leftHeight != pRoot->_bf)
    {
        cout << pRoot->_kv.first << "平衡因子异常" << endl;
        return false;
    }

    return rightHeight - leftHeight < 2
        && _IsAVLTree(pRoot->_left)
        && _IsAVLTree(pRoot->_right);
}

size_t _Height(Node* pRoot)
{
    if (pRoot == nullptr)
        return 0;

    int leftHeight = _Height(pRoot->_left);
    int rightHeight = _Height(pRoot->_right);

    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

void _InOrder(Node* pRoot)
{
    if (pRoot == nullptr)
        return;

    _InOrder(pRoot->_left);
    cout << pRoot->_kv.first << " ";
    _InOrder(pRoot->_right);
}

5、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(log N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
AVL树的实现代码放在代码仓库:https://gitee.com/xiaobai-is-working-hard-jy/data-structure/tree/master/AVLTree

  • 79
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 54
    评论
### 回答1: 山东大学数据结构课程设计要求实现AVL树AVL树是一种自平衡二叉搜索树,具有良好的平衡性。在AVL树中,每个节点的左子树和右子树的高度差不超过1。这种平衡性使得AVL树的查找、插入和删除操作的时间复杂度都为O(logn),相较于其他平衡二叉搜索树如红黑树而言,AVL树的查找性能更好。 实现AVL树主要包括以下几个步骤: 1. 定义AVL树的节点结构,包含数据域和左右子节点指针。 2. 实现AVL树的插入操作。插入操作首先按照二叉搜索树的规则找到要插入的位置,然后进行平衡调整。插入操作的平衡调整包括修改各个节点的平衡因子,根据不同的情况进行树的旋转操作,以保证树的平衡性。 3. 实现AVL树的删除操作。删除操作首先按照二叉搜索树的规则找到要删除的节点,然后进行平衡调整。删除操作的平衡调整包括修改各个节点的平衡因子,并根据不同的情况进行树的旋转操作,以保证树的平衡性。 4. 实现AVL树的查找操作。查找操作按照二叉搜索树的规则进行,即根据节点的大小关系不断在左子树或右子树中查找,直到找到目标节点或者为空节点。 在实现AVL树的过程中,需要注意保持树的平衡性,并根据旋转操作的具体情况进行适当的调整。此外,还可以实现一些辅助函数,如计算节点的高度、更新节点的平衡因子等,以提高代码的可读性和维护性。 总之,山东大学数据结构课程设计要求实现AVL树,通过定义节点结构和实现插入、删除和查找操作,可以实现一个具有良好平衡性和高效性能的AVL树。 ### 回答2: 山东大学的数据结构课程设计中,我实现AVL树AVL树是一种平衡二叉搜索树,它的目的是保持树的平衡,以避免在搜索、插入和删除操作中产生较高的时间复杂度。AVL树通过在每个节点上维护一个平衡因子(即左子树高度减去右子树高度),来确保树的平衡。 在我的实现中,首先我定义了一个AVLNode结构体,它包含了存储在树节点中的数据以及两个指向左右子节点的指针。然后,我实现了一些基本操作函数,包括实现了插入函数、删除函数、查找函数等等。 在插入函数中,我通过递归地将新节点插入到树中,并在插入完成后更新每个节点的平衡因子。这样,如果平衡因子超过了允许的范围(例如-1到1之外),我就需要进行相应的旋转操作来恢复树的平衡。 在删除函数中,我首先找到要删除的节点,并按照BST的规则进行删除操作。删除后,我还需要更新其父节点及其祖先节点的平衡因子,并通过旋转操作来保持树的平衡性。 除了插入和删除操作,我还实现了一些其他的功能,例如查找最小值、查找最大值、查找后继节点、查找前驱节点等。 在整个实现过程中,我注重了代码的可读性和效率。我使用了递归来处理树的节点,利用平衡因子来判断树的平衡性,采用适当的旋转操作来维持树的平衡。通过测试样例,我验证了实现的正确性和性能。 ### 回答3: AVL树是一种自平衡的二叉搜索树,它的设计旨在解决二叉搜索树在插入、删除等操作过程中可能导致不平衡的问题。 在山东大学数据结构课程设计中,我们可以采用以下步骤实现AVL树: 1. 首先,我们需要定义AVL树的节点结构,该结构包括左右孩子指针、平衡因子和关键字等信息。可以使用结构体来表示节点。 2. 接着,我们实现插入操作。当插入一个新的节点时,我们需要按照二叉搜索树的规则找到插入位置,并将节点插入到相应的位置。插入完成后,我们需要逐级向上更新每个节点的平衡因子,并检查是否需要进行旋转操作。 3. 为了保持树的平衡性,我们需要定义旋转操作。主要有四种旋转操作:左单旋、右单旋、左-右双旋和右-左双旋。这些旋转操作能够通过改变节点之间的链接关系,使树重新平衡。 4. 我们还需要实现删除操作。删除节点时,我们首先找到要删除的节点,并根据二叉搜索树的规则调整树的结构。删除完成后,同样需要逐级向上更新每个节点的平衡因子,并检查是否需要进行旋转操作。 5. 最后,我们需要实现一些辅助函数,如计算树的高度、查找最小值和最大值等。 在实现AVL树时,需要注意维护树的平衡性,并确保插入和删除操作的正确性。此外,可在实现过程中添加必要的错误处理、输入验证和合理的注释,以提高代码的稳定性和可读性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 54
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在努力jy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值