草庐IT

【C++】AVL树

野猪佩奇` 2023-05-27 原文

文章目录

一、什么是 AVL 树

我们在前面学习二叉搜索树时提到,二叉搜索树的查找效率为 O(N),因为当数据有序或接近有序时,构建出来的二叉搜索树是单分支或接近单分支的结构,此时树的高度接近 n,所以最坏情况下二叉搜索树的查找效率为 O(N);

为了应对上面这种情况,两位俄罗斯的数学家 G.M.Adelson-Velskii 和 E.M.Landis 在1962年提出了一种解决办法 – 当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1 (需要对树中的结点进行调整来实现),即可降低树的高度,从而减少平均搜索长度

通过上面这种方法构建出来的树就是平衡二叉搜索树,也叫 AVL 树 (由提出它的两个科学家名字的首字母组成);AVL 树具有以下特性:

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

如上,如果一棵二叉搜索树是高度平衡 (每个节点的平衡因子的绝对值都小于等于1) 的,那它就是AVL树;对于 n 个节点的 AVL 树,其高度为 O(logN),查找的效率也为 O(logN)。

注意事项:

1、引入平衡因子只是控制一棵树为平衡树的其中一种方法,我们也可以通过其他方法来控制;在平衡因子控制中,平衡因子是用来评估树的状态的变量;

2、有的同学可能会疑惑这里的平衡因子的值为什么是 -1/0/1,而不直接调整为0,让 AVL 树变成一棵绝对平衡的树;这是因为在某些节点数下不可能将 AVL 树调整到绝对平衡,比如节点数为 2/4/6 …,在这些情况下不管怎么调整都始终会有一棵子树高度高1;

3、由于平衡二叉搜索树是通过高度保持平衡的,所以有的地方也把它叫做高度平衡二叉搜索树。


二、AVL 树的节点结构

和二叉搜索树不同,AVL 树我们需要增加一个变量 bf 即平衡因子来控制树的状态;同时,我们需要将节点定义为三叉链结构,即增加一个节点指针指向父节点,这是为了方便后面插入节点时修改父节点的平衡因子。

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

	std::pair<K, V> _kv;  //键值对
	int _bf;         //balance factor

	AVLTreeNode(const std::pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

三、AVL 树的插入

AVL 树的插入前面部分和二叉搜索树的插入很类似 – 比当前节点大就往右边走,小就往左边走,相等就插入失败,走到空位就插入,不过二叉搜索树插入的是键值 key,而我们这里插入的是键值对 pair<K, V>,所以比较的时候是 cur->_kv.first 和 kv.first 进行比较;同时,在链接节点时需要注意修改节点父节点指针的指向,因为 AVL 树的节点是三叉链结构;

AVL 树插入的难点在于平衡因子的更新以及平衡因子非法时如何进行旋转

1、更新父节点的平衡因子 – 我们每插入一个新节点后都需要更新父节点的平衡因子 – 由于平衡因子 = 左子树高度 - 右子树高度,所以往左插入–父节点平衡因子,往右插入++父节点平衡因子即可;

2、更新祖先节点的平衡因子 – 祖先节点的更新则比较复杂,一共有三种情况:

  • 如果更新后父节点的平衡因子为0,说明插入前父节点的平衡因子为1/-1,左右子树高度不同,且此次插入刚好插入的是矮的那边,此时子树整体高度不变,不用继续向上更新祖先节点的平衡因子
  • 如果更新后父节点的平衡因子为1/-1,说明插入前父节点的平衡因子为0,左右子树高度相同,插入后改变了左子树/右子树的高度,子树整体高度也变了,此时需要继续向上更新祖先节点的平衡因子,且最多会更新到根节点的平衡因子;
  • 祖先节点平衡因子更新过程中,如果祖先节点的平衡因子变为0或者更新到了根节点,则停止更新;如果祖先平衡因子变为2/-2,此时这棵子树不再是 AVL树,我们需要对其进行旋转将其重新调整为 AVL树

具体案例如下:

注:如上,在某些情况下我们需要更新祖先节点的平衡因子,这也是为什么我们要将 AVL 树的节点结构定义为三叉链的原因,即为了能够更方便的找到父节点的地址。

代码如下:

bool insert(const std::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->_left;
        }
        else if (kv.first > cur->_kv.first) {
            parent = cur;
            cur = cur->_right;
        }
        else {
            return false;  //不允许重复节点
        }
    }

    //走到空开始插入,注意这里还需要修改父节点的指向
    Node* newNode = new Node(kv);
    if (kv.first < parent->_kv.first)
        parent->_left = newNode;
    else
        parent->_right = newNode;
    newNode->_parent = parent;  //修改父节点指向

    //更新平衡因子:
    //因为平衡因子是左子树高度-右子树高度,所以往左插入平衡因子--,往右插入平衡因子++
    //平衡因子更新后有三种情况:
    //1.更新后父节点的平衡因子为0,说明插入前父节点的平衡因子为1/-1,左右子树高度不同,且此次插入刚好插入的是矮的那边,此时子树高度不变,不用继续向上更新祖先节点的平衡因子
    //2.更新后父节点的平衡因子为1/-1,说明插入前父节点的平衡因子为0,左右子树高度相同,插入后改变了左右子树的高度,此时需要向上更新祖先节点的平衡因子,且最多可以更新到根节点的平衡因子
    //3.更新祖先节点平衡因子过程中,祖先平衡因子变为2/-2,此时这棵子树不再是AVL树,需要进行旋转将其调整为AVL树
    cur = newNode;
    while (parent) {  //最多更新到根节点
        //更新平衡因子
        if (cur == parent->_left)
            parent->_bf--;
        else
            parent->_bf++;

        //根据更新后的平衡因子的值判断是否要往上继续更新
        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说明结构出现问题,需要旋转进行调整
            //旋转分为四类
            //...
        }
        else {  //走到这里说明abs(parent->_bf)>=3,此时插入前就不是一棵AVL树,直接断言错误
            assert(false);
        }
    }

    return true;
}

四、AVL 树的旋转

当某一个节点的平衡因子为 2/-2 时,我们要对以这个节点为根节点的子树进行旋转,让这课子树重新变为 AVL 树,也就是说,旋转的目标如下

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

根据节点插入位置的不同,AVL 树的旋转可以总结为四类:

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

下面我们来具体探讨这四类情况。

1、左单旋

左单旋的抽象图如下,其中 a b c 都是高度为 h 的三棵 AVL 子树,30 是这棵子树的根,当满足 “右子树比左子树高1且在右子树的右边插入节点时 – 右右 (右边高右边插)” 进行左单旋,左单旋的过程很简单 – 让右子树的左子树链接到根的左子树、让根链接到右子树的左子树、让右子树成为根、将根节点和右子树的根节点的平衡因子置0即可:

可以看到,左单旋其实是完成了选择的四个目标的 – 1、旋转后左右子树高度都为 h+1,高度差不超过1;2、由于右子树的左子树都比60小,比30及其左子树大,所以链接到30的右边,30及其左右子树都比60小,所以链接到60左边,保持了搜索树的结构;3、旋转后原来的根节点和现在的根节点的平衡因子都变为0;4、插入前和插入后子树的整体高度都为 h+1,不会再往上影响。

注意:这里给定的图是抽象图,也就是说它可以表示无数种情况,只是这些情况都可以被归为右右这一类;比如当 h == 0 时,插入情况和子树类型合计有1种;h == 1 时插入情况和子树类型合计有 2 种;当 h == 2 时,插入情况和子树类型合计有1种;h == 1 时插入情况和子树类型合计有 36 种;… …

代码实现如下 – 代码实现时要比我们上面分析的复杂的多,因为要考虑父节点指针的指向问题、h == 0 的情况、parent 是整棵树的根节点与子树根节点的情况等等:

//左单旋--右右
void rotateL(Node* parent) {
    Node* subR = parent->_right;  //右子树
    Node* subRL = subR->_left;  //右子树的左子树

    //右子树的左子树链接到parent的右边
    parent->right = subRL;
    if (subRL)  //subR可能为空(h==0)
        subRL->_parent = parent;

    //parent链接到右子树的左边
    subR->_left = parent;
    Node* pparent = parent->_parent;  //保存parent的父节点
    parent->_parent = subR:

    //让subR成为子树的根
    if (pparent) {
        if (pparent->_left == parent) {
            pparent->_left = subR;
            subR->_parent = pparent;
        }
        else {
            pparent->_right == subR;
            subR->_parent = pparent;
        }
    }
    else {  //如果parent的parent为空,说明parent是整棵树根节点,此时subR成为新的根节点
        subR->_parent = nullptr;
        _root = subR;
    }

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

2、右单旋

右单旋的抽象图如下,当满足 “左子树比右子树高1且在左子树的左边插入节点时 – 左左 (左边高左边插)” 进行右单旋:

由于右单旋和左单旋非常类似,所以这里我就直接给出代码了:

//右单旋--左左
void rotateR(Node* parent) {
    Node* subL = parent->_left;  //左子树
    Node* subLR = subL->_right;  //左子树的右子树

    //将左子树的右子树链接到根的左边
    parent->_left = subLR;
    if (subLR)  //应对h==0的情况
        subLR->_parent = parent;

    //将根链接到左子树的右边
    subL->_right = parent;
    Node* pparent = parent->_parent;  //记录父节点的父节点
    parent->_parent = subL;

    //让subL成为子树的根
    if (pparent) {
        if (pparent->_left == parent) {
            pparent->_left = subL;
            subL->_parent = pparent;
        }
        else {
            pparent->_right = subL;
            subL->_parent = pparent;
        }
    }
    else {  //如果parent的parent为空,说明parent是整棵树的根节点,此时subL成为新的根节点
        subL->_parent = nullptr;
        _root = subL;
    }

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

3、左右双旋

我们上面讨论在左单旋和右单旋都是插入后为直线的情况,那么如果插入后是折线呢?如下:

可以看到,如果插入后形成的是一条直线,即在较高右子树的右侧插入或在较高左子树的左侧插入的话,那么经过一次左单旋或右单旋就能解决问题;但是如果插入后是一条折线,即在较高右子树的左侧插入或在较高左子树的右侧插入的话,单纯的左单旋或右单旋并不能解决问题;

如上图所示,情况3经过左单旋其实是变成了情况4,而情况4经过右单旋又变成了情况3;所以在这种情况我们需要左单旋和右单旋配合从而使子树变为 AVL 树,即进行左右双旋或右左双旋。

左右双旋的抽象图如下,其中 a d 是高度为 h 的 AVL 子树,b c 是高度为 h-1 的 AVL 子树,90是这棵树的根,当满足 “左子树比右子树高1且在左子树的右侧插入节点时 – 左右 (左边高右边插)” 就先进行左单旋,再进行右单旋:

注:其实 h == 0 的情况就是我们前面画的第四种情况,此时 60 为新增节点,新增位置为较高左节点的右侧,我们需要先进行左单旋、再进行右单旋,而不能像情况4中那样只进行右单旋。

关于旋转,我们可以直接在左右双旋中调用左单旋和右单旋来实现,左右双旋的难点在于平衡因子的更新:我们不能依靠左单旋和右单旋的代码来更新左右双旋的平衡因子,因为它们是直接将平衡因子变为0,对于平衡因子我们需要自己来单独处理;

为了更好的理解左右双旋平衡因子的变化,我们可以不将它划分为先左再右的两步,而是直接将它看成一步,此时相当于60的左子树被链接到了30的右子树,60的右子树被链接到了90的左子树,然后30和90及其子树分别成为了60的左右子树,那么60的平衡因子不管怎样都是0,但是30和90的平衡因子会被分为三种情况

  1. 当新节点插入到60的左子树时,旋转后60的平衡因子为0,30的平衡因子为0,90的平衡因子为1;(因为60的右子树高度并没有加1,而它最终被链接到了90的左子树)
  2. 当新节点插入到60的由子树时,旋转后60的平衡因子为0,30的平衡因子为-1,90的平衡因子为0;(因为60的左子树高度并没有加1,而它最终被链接到了30的右子树)
  3. 当 h == 0 时,此时60自己为新增节点,旋转后30、60、90的平衡因子都为0。

那么,我们该如何判断插入是属于下面哪种情况呢?其实通过旋转前60的平衡因子的值判断即可 – 如果60的平衡因子为0,说明60本身为新增节点,属于情况3;如果60的平衡因子为-1,说明在60的左子树插入,属于情况1;如果为1,说明在60的右子树插入,属于情况3。

所以最终代码如下:

//左右双旋--左右
void rotateLR(Node* parent) {
    Node* subL = parent->_left;  //左子树--30
    Node* subLR = subL->_right;  //左子树的右子树--60
    int bf = subLR->_bf;  //记录subLR平衡因子的值

    //先以左子树为轴进行左单旋--30
    rotateL(parent->_left);  
    //再进行右单旋--90
    rotateR(parent);

    //更新平衡因子
    if (bf == 0) {
        parent->_bf = subL->_bf = subLR->_bf = 0;
    }
    else if (bf == -1) {
        subLR->_bf = 0;
        subL->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1) {
        subLR->_bf = 0;
        subL->_bf = -1;
        parent->_bf = 0;
    }
    else {
        assert(false);  //旋转前subLR的平衡因子的绝对值不可能大于1
    }
}

4、右左双旋

右左双旋的抽象图如下,当满足 “右子树比左子树高1且在右子树的左侧插入节点时 – 右左 (右边高左边插)” 就先进行右单旋,再进行左单旋:

由于上面详细讲解了左右双旋的过程,所以右左双旋我也是直接给出代码了:

//右左双旋--右左
void rotateRL(Node* parent) {
    Node* subR = parent->_right;  //右子树--60
    Node* subRL = subR->_left;  //右子树的左子树--90
    int bf = subRL->_bf;  //记录subRL的平衡因子

    //先以subR为轴进行右单旋
    rotateR(parent->_right);
    //再进行左单旋	
    rotateL(parent);

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

5、总结

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

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

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


五、VAL 树的验证

在完善了 AVL 树的插入之后,我们可以验证一下通过我们的程序构建出来的树到底是不是一棵 AVL 树,验证一共分为两部分:

  1. 验证是否为二叉搜索树;
  2. 验证二叉搜索树是否平衡;

验证二叉搜索树很简单,我们向树中插入数据,然后实现一个 InOrder 函数看遍历得到的数据是否有序即可。

//中序遍历
void InOrder() {
    return _inOrder(_root);
}

void _inOrder(Node* root) {
    if (root == nullptr)
        return;

    _inOrder(root->_left);
    std::cout << root->_kv.first >> root->_kv.second >> std::endl;
    _inOrder(root->_right);
}

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

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

    int leftH = Height(root->_left);
    int rightH = Height(root->_right);

    return (leftH > rightH ? leftH : rightH) + 1;
}

//判断是否为平衡二叉树
bool IsBanlance() {
    return _IsBanlance(_root)
}

bool _IsBanlance(Node* root) {
    if (root == nullptr)
        return true;

    //左右高度差的绝对值是否小于等于1
    int leftH = Height(root->_left);
    int rightH = Height(root->_right);

  	//如果不平衡我们可以打印一下发生错误的节点的key值
    if (rightH - leftH != root->_bf) {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }

    //不仅要当前子树为平衡二叉树,子树的左右子树也要为平衡二叉树
    return abs(rightH - leftH) < 2 && _IsBanlance(root->_left) && _IsBanlance(root->_right);
}

如果我们用几千上万甚至几十上百万个随机数测试出来的结果都是真,那么我们的 AVL 就基本上是平衡的了,当然前提是你的 IsBanlance 函数没写错。


六、AVL 树的删除

因为 AVL 树是一棵二叉搜索树,所以它的删除过程和二叉搜索树其实差不多,先找到删除的节点,然后删除分三种情况:

  1. 删除的节点左边为空,则托孤后直接删除;
  2. 删除的节点右边为空,则托孤后直接删除;
  3. 删除的节点左右都不为空,则替换左边最大节点或右边最小节点进行删除;

删除后我们也需要调整父节点的平衡因子,只是删除后父节点及祖先节点平衡因子的变化和插入时平衡因子的变化一定程度上可以说是相反的 – 删除左孩子父节点平衡因子++,删除右孩子父节点平衡因子–;

如果删除后父节点的平衡因子为 1/-1,说明删除前平衡因子为0,则删除不会改变子树的高度,不需要继续往上调整;如果删除后父节点平衡因子变为0,说明删除前为 1/-1,则此次删除改变了子树的高度,需要向上调整祖先节点的平衡因子,最坏情况下调整到根;

如果祖先的平衡因子调整后变为 2/-2,则需要进行旋转,旋转仍然分为四类:左单旋、右单旋、左右双旋和右左双旋。

所以 AVL 树的删除仅仅是比 AVL 树的插入复杂一些,但是原理都是一样的,所以这里我们不对它进行过多探讨,仅仅作为了解;如果对这个特别感兴趣的同学可以看一看殷人昆老师的 《数据结构-用面向对象方法与C++描述》,里面有 AVL 树删除的具体思路讲解和代码实现。


七、AVL 树的性能

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

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


八、AVL 树的代码实现

注意:这里我们只给出 AVL 树部分功能的代码,其中主要是插入和旋转的代码,其他的包括删除、构造、赋值重载等都没有给出,这是因为 AVL 树并不是需要我们重点学习的数据结构,红黑树才是,我们将在红黑树部分给出红黑树完整模拟实现,包括迭代器的封装。

AVLTree.h:

#pragma once
#include <assert.h>

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

	std::pair<K, V> _kv;  //键值对--注意pair作用域的问题
	int _bf;         //balance factor

	AVLTreeNode(const std::pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

template<class K, class V>
class AVLTree {
	typedef AVLTreeNode<K, V> Node;
public:
	bool insert(const std::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->_left;
			}
			else if (kv.first > cur->_kv.first) {
				parent = cur;
				cur = cur->_right;
			}
			else {
				return false;  //不允许重复节点
			}
		}

		//走到空开始插入,注意这里还需要修改父节点的指向
		Node* newNode = new Node(kv);
		if (kv.first < parent->_kv.first)
			parent->_left = newNode;
		else
			parent->_right = newNode;
		newNode->_parent = parent;  //修改父节点指向

		//更新平衡因子:
		//因为平衡因子是左子树高度-右子树高度,所以往左插入平衡因子--,往右插入平衡因子++
		//平衡因子更新后有三种情况:
		//1.更新后父节点的平衡因子为0,说明插入前父节点的平衡因子为1/-1,左右子树高度不同,且此次插入刚好插入的是矮的那边,此时子树高度不变,不用继续向上更新祖先节点的平衡因子
		//2.更新后父节点的平衡因子为1/-1,说明插入前父节点的平衡因子为0,左右子树高度相同,插入后改变了左右子树的高度,此时需要向上更新祖先节点的平衡因子,且最多可以更新到根节点的平衡因子
		//3.更新祖先节点平衡因子过程中,祖先平衡因子变为2/-2,此时这棵子树不再是AVL树,需要进行旋转将其调整为AVL树
		cur = newNode;
		while (parent) {  //最多更新到根节点
			//更新平衡因子
			if (cur == parent->_left)
				parent->_bf--;
			else
				parent->_bf++;

			//根据更新后的平衡因子的值判断是否要往上继续更新
			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说明结构出现问题,需要旋转进行调整
				//旋转分为四类
				//1.新节点插入较高右子树的右侧--左单旋
				if (parent->_bf == 2 && cur->_bf == 1) {
					rotateL(parent);
				}

				//2.新节点插入较高左子树的左侧--右单旋
				else if (parent->_bf == -2 && cur->_bf == -1) {
					rotateR(parent);
				}

				//3.新节点插入较高左节点的右侧--左右双旋
				else if (parent->_bf == -2 && cur->_bf == 1) {
					rotateLR(parent);
				}

				//4.新节点插入较高右子树的左侧--右左双旋
				else if (parent->_bf == 2 && cur->_bf == -1) {
					rotateRL(parent);
				}
				else {  //如果都不属于上面这四类情况,说明树有问题
					assert(false);
				}

				break;
			}
			else {  //走到这里说明abs(parent->_bf)>=3,此时插入前就不是一棵AVL树,直接断言错误
				assert(false);
			}
		}

		return true;
	}

	//左单旋--右右
	void rotateL(Node* parent) {
		Node* subR = parent->_right;  //右子树
		Node* subRL = subR->_left;  //右子树的左子树

		//右子树的左子树链接到parent的右边
		parent->_right = subRL;
		if (subRL)  //subR可能为空(h==0)
			subRL->_parent = parent;

		//parent链接到右子树的左边
		subR->_left = parent;
		Node* pparent = parent->_parent;  //保存parent的父节点
		parent->_parent = subR;

		//让subR成为子树的根
		if (pparent) {
			if (pparent->_left == parent) {
				pparent->_left = subR;
				subR->_parent = pparent;
			}
			else {
				pparent->_right = subR;
				subR->_parent = pparent;
			}
		}
		else {  //如果parent的parent为空,说明parent是整棵树根节点,此时subR成为新的根节点
			subR->_parent = nullptr;
			_root = subR;
		}

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

	//右单旋--左左
	void rotateR(Node* parent) {
		Node* subL = parent->_left;  //左子树
		Node* subLR = subL->_right;  //左子树的右子树

		//将左子树的右子树链接到根的左边
		parent->_left = subLR;
		if (subLR)  //应对h==0的情况
			subLR->_parent = parent;

		//将根链接到左子树的右边
		subL->_right = parent;
		Node* pparent = parent->_parent;  //记录父节点的父节点
		parent->_parent = subL;

		//让subL成为子树的根
		if (pparent) {
			if (pparent->_left == parent) {
				pparent->_left = subL;
				subL->_parent = pparent;
			}
			else {
				pparent->_right = subL;
				subL->_parent = pparent;
			}
		}
		else {  //如果parent的parent为空,说明parent是整棵树的根节点,此时subL成为新的根节点
			subL->_parent = nullptr;
			_root = subL;
		}

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

	//左右双旋--左右
	void rotateLR(Node* parent) {
		Node* subL = parent->_left;  //左子树
		Node* subLR = subL->_right;  //左子树的右子树
		int bf = subLR->_bf;  //记录subLR平衡因子的值

		//先以左子树为轴进行左单旋
		rotateL(parent->_left);
		//再进行右单旋
		rotateR(parent);

		//更新平衡因子
		if (bf == 0) {
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else if (bf == -1) {
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1) {
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else {
			assert(false);  //旋转前subLR的平衡因子的绝对值不可能大于1
		}
	}

	//右左双旋--右左
	void rotateRL(Node* parent) {
		Node* subR = parent->_right;  //右子树
		Node* subRL = subR->_left;  //右子树的左子树
		int bf = subRL->_bf;  //记录subRL的平衡因子

		//先以subR为轴进行右单旋
		rotateR(parent->_right);
		//再进行左单旋
		rotateL(parent);

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

	//中序遍历
	void InOrder() {
		return _inOrder(_root);
	}

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

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

		int leftH = Height(root->_left);
		int rightH = Height(root->_right);

		return (leftH > rightH ? leftH : rightH) + 1;
	}

private:
	void _inOrder(Node* root) {
		if (root == nullptr)
			return;

		_inOrder(root->_left);
		std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;
		_inOrder(root->_right);
	}

	bool _IsBanlance(Node* root) {
		if (root == nullptr)
			return true;

		//左右高度差的绝对值是否小于等于1
		int leftH = Height(root->_left);
		int rightH = Height(root->_right);

		//如果不平衡我们可以打印一下发生错误的节点的key值
		if (rightH - leftH != root->_bf) {
			std::cout << root->_kv.first << "平衡因子异常" << std::endl;
			return false;
		}

		//不仅要当前子树为平衡二叉树,子树的子树也要为平衡二叉树
		return abs(rightH - leftH) < 2 && _IsBanlance(root->_left) && _IsBanlance(root->_right);
	}

private:
	Node* _root = nullptr;
};

test.cpp:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <time.h>
//特别注意这里std展开的位置,如果AVLTree.h里面的pair或者cout、endl没有指定std::,那么std必须展开在它的前面,
//否则预处理之后AVLTree.h里面的pair、cout、endl会找不到或者找到的不是我们想要的
using namespace std;
#include "AVLTree.h"

//验证搜索树
void AVLTree_test1() {
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> avl;
	for (auto e : a)
		avl.insert(std::make_pair(e, e));

	avl.InOrder();
}

//验证平衡树
void AVLTree_test2() {
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int, int> avl;
	for (auto e : a)
		avl.insert(std::make_pair(e, e));

	avl.InOrder();

	cout << avl.IsBanlance() << endl;
}

//增大测试用例--采用N个随机数来验证平衡树
void AVLTree_test3() {
	srand((size_t)time(0));
	const int N = 10000;
	AVLTree<int, int> avl;
	for (int i = 0; i < N; i++) {
		int x = rand();
		avl.insert(make_pair(x, x));
	}

	cout << avl.IsBanlance() << endl;
}

int main() {
	AVLTree_test3();

	return 0;
}

特别提醒:

test.cpp 中 std 展开的位置要特别注意 – 如果 AVLTree.h 里面的 pair 或者 cout、endl 等在使用时没有指定 std 作用域,那么 std 必须展开在 AVLTree.h 的前面,否则预处理之后 AVLTree.h 里面的 pair、cout、endl 会找不到或者找到的不是我们想要的那个,这时编译器会报一大堆奇怪的语法错误。(别问我为什么要特别提它,问就是我在这里折腾了半天 QAQ)


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3adig03x6w6cc


有关【C++】AVL树的更多相关文章

  1. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  2. 【开卷数据结构 】平衡二叉树(AVL) - 2

    目录平衡二叉树的定义平衡二叉树的插入LL平衡旋转(右单旋转)RR平衡旋转(左单旋转)LR平衡旋转(先左后右双旋转)RL平衡旋转(先右后左双旋转)平衡二叉树的构建平衡二叉树的查找平衡二叉树的定义Q:什么是二叉排序树A:二叉排序树或者是一棵空树,或者是具有如下性质的二叉树1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值3)左、右子树也分别是一棵二叉排序树Q:什么是平衡二叉树A:它或者是一颗空树,或者是具有以下性质的二叉排序树:它的左子树和右子树的深度之差的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。定义结点

  3. java - Java 中的红黑树或 AVL 树实现 - 2

    Javacollections/Guava/ApacheCommons库中是否有RedBlackTree/AVLTreedata结构实现?如果是的话,你能把它们指给我看吗?基本上我正在寻找一种数据结构,查询应该在O(lgn)time内发生。数据结构也会有一些更新,但不会像查询那样频繁。 最佳答案 BasicallyIamlookingforadatastructurewherethequeriesshouldhappeninO(lgn)time使用TreeMap.它由Red-Blacktree支持所以它的访问时间是O(logN)(我

  4. c++ - 合并 2 个不同的 AVL 树 - 2

    假设我有两棵AVL树并且我知道它们各自的大小。但是,我不知道是否有重复的节点,或任何其他信息。将它们合并到新的AVL树中的最有效方法是什么?原来的树木可以被摧毁。 最佳答案 将树T1和T2转换为排序列表L1和L2将L1和L2合并成一个排序列表L再次将L转换为树T。IIRC所有这些操作都是O(N),所以完全合并也将是O(N)。如果您对AVL树的表示允许高效地迭代它们(例如,使用反向指针、延续、惰性求值等),您也应该能够在没有中间列表的情况下执行此操作。更新:由于您的编程语言似乎是C/C++,您可以暂时滥用AVL节点结构作为链表中的节点

  5. c++ - 没有父指针的AVL树如何实现插入? - 2

    看到一些关于AVL的rebalance()函数实现的文章。每次插入后,我们应该检查插入节点的祖先是否平衡。所以我想,为了检查祖先的余额,我了解了插入节点的父节点。但是,我想知道有没有其他方法可以做到这一点而不必使用父指针?例如,节点结构:structNode{intdata;structNode*lchild,*rchild;//*parent;}; 最佳答案 遍历树的时候可以维护一个到当前节点的栈stacknodeStack;当你遍历到一个新的节点时,将它添加到堆栈中,然后你就有了你的祖先。处理完节点后,将其从堆栈中弹出。**编辑

  6. 二叉树、平衡二叉树AVL、红黑树、B树、B+树 - 2

    image.pngB树的阶数等于叶节点最大关键字数量+1(因为关键字两边都有指向子节点的指针-分叉)在m阶(m叉)B树中除根结点外,任何节点至少[m/2]个分叉,即至少[m/2]-1个关键字,[]代表向上取整。节点内的关键字采用顺序查找或二分查找。因为关键字太少会导致树变高,降低查找效率。另外就是保证同级子树的高度相同-平衡。image.pngB+树的阶数与叶节点最大关键字数量相同,有与分块查找相似的地方;分支节点中只包含它的叶子结点所有关键字中的最大值。查找失败:关键字的记录(信息)为空,指向null文章知识点与官方知识档案匹配,可进一步学习相关知识

  7. 二叉搜索树:AVL平衡 - 2

    文章目录一、二叉搜索树1.1概念1.2操作1.3代码实现二、二叉搜索树的应用K模型和KV模型三、二叉搜索树的性能分析四、AVL树4.1AVL树的概念4.2AVL树的实现原理4.3旋转4.4AVL树最终代码一、二叉搜索树1.1概念二叉搜索树(BinarySearchTree,BST)是一种特殊的二叉树,它可以是空树,也可以是满足以下性质的一颗二叉树:若左子树不为空,左子树中所有节点的键值都小于根节点的值。若右子树不为空,右子树中所有节点的键值都大于根节点的值。左右子树也分别为二叉搜索树。因此,二叉搜索树的中序遍历结果是一个有序序列。这个特性使得二叉搜索树在搜索、插入和删除操作时具有高效性能。📝二

  8. 二叉搜索树:AVL平衡 - 2

    文章目录一、二叉搜索树1.1概念1.2操作1.3代码实现二、二叉搜索树的应用K模型和KV模型三、二叉搜索树的性能分析四、AVL树4.1AVL树的概念4.2AVL树的实现原理4.3旋转4.4AVL树最终代码一、二叉搜索树1.1概念二叉搜索树(BinarySearchTree,BST)是一种特殊的二叉树,它可以是空树,也可以是满足以下性质的一颗二叉树:若左子树不为空,左子树中所有节点的键值都小于根节点的值。若右子树不为空,右子树中所有节点的键值都大于根节点的值。左右子树也分别为二叉搜索树。因此,二叉搜索树的中序遍历结果是一个有序序列。这个特性使得二叉搜索树在搜索、插入和删除操作时具有高效性能。📝二

  9. 【C++】AVL树 - 2

    AVL树文章目录AVL树一、底层结构二、AVL树的概念三、AVL树节点的定义四、AVL树的基本框架五、AVL树的插入六、AVL树的旋转1.左单旋2.右单旋3.左右双旋4.右左双旋七、AVL树的验证八、AVL树的修改九、AVL树的查找十、AVL树的删除(了解)十一、AVL树的性能一、底层结构前面对map、multimap、set、multiset进行了简单的介绍,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行

  10. 【C++】AVL树 - 2

    AVL树文章目录AVL树一、底层结构二、AVL树的概念三、AVL树节点的定义四、AVL树的基本框架五、AVL树的插入六、AVL树的旋转1.左单旋2.右单旋3.左右双旋4.右左双旋七、AVL树的验证八、AVL树的修改九、AVL树的查找十、AVL树的删除(了解)十一、AVL树的性能一、底层结构前面对map、multimap、set、multiset进行了简单的介绍,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行

随机推荐