【数据结构】C++实现AVL平衡树

news2024/11/25 0:58:12

文章目录

  • 1.AVL树的概念
  • 2.AVL树的实现
    • AVL树结点的定义
    • AVL树的插入
      • AVL树的旋转
        • 左单旋
        • 右单旋
        • 左右双旋
        • 右左双旋
        • 插入代码
    • AVL树的验证
    • AVL树的查找
    • AVL树的修改
    • AVL树的删除
    • AVL树的性能
  • AVL树的代码测试

1.AVL树的概念

二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的

因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

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

在这里插入图片描述

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

注意: 这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之差的绝对值不超过1,因为只有满二叉树才能做到每个结点左右子树高度之差均为0。

2.AVL树的实现

AVL树结点的定义

我们这里直接实现KV模型的AVL树,为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。

// AVL平衡树,左右高度差不超过1
template<class K, class V>
struct AVLTreeNode {
    AVLTreeNode<K, V> *_left;
    AVLTreeNode<K, V> *_right;
    AVLTreeNode<K, V> *_parent;
    pair<K, V> _kv;// 存储键值对的pair对象;pair是一个模板类,它可以用来存储两个不同类型的值,头文件utility

    int _bf;// balance factor 平衡因子,用于AVL树的平衡操作。 -1 0 1是正常的平衡因子

    AVLTreeNode(const pair<K, V> &kv)// 构造函数传参为一个pair对象
        : _left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0) {
    }
};

注意: 给每个结点增加平衡因子并不是必须的,只是实现AVL树的一种方式,不引入平衡因子也可以实现AVL树,只不过会麻烦一点。

AVL树的插入

AVL树插入结点时有以下三个步骤:

  1. 按照二叉搜索树的插入方法,找到待插入位置。
  2. 找到待插入位置后,将待插入结点插入到树中。
  3. 更新平衡因子,如果出现不平衡,则需要进行旋转。

因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:

  1. 待插入结点的key值比当前结点小就插入到该结点的左子树。
  2. 待插入结点的key值比当前结点大就插入到该结点的右子树。
  3. 待插入结点的key值与当前结点的key值相等就插入失败。

如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。

与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。

由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

在这里插入图片描述

所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

  1. 新增结点在parent的右边,parent的平衡因子+1。
  2. 新增结点在parent的左边,parent的平衡因子-1。

每更新完一个结点的平衡因子后,都需要进行以下判断:

  • 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断理由说明:

parent更新后的平衡因子分析
-1或1只有0经过–/++操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。
0只有-1/1经过++/–操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。
-2或2此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。

注意: parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。

而在最坏情况下,我们更新平衡因子时会一路更新到根结点。例如下面这种情况:

在这里插入图片描述

说明一下: 由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。当然,我们也可以不用三叉链结构,可以在插入结点时将路径上的结点存储到一个栈当中,当我们更新平衡因子时也可以通过这个栈来更新祖先结点的平衡因子,但是相对较麻烦。

若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:

我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,那么我们必定要执行以下逻辑:

cur = parent;
parent = parent->_parent;

这里我想说明的是:当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

理由如下:
若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。

而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:

  1. 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
  2. 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

根据此结论,我们可以将旋转处理分为以下四类:

  1. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
  2. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
  3. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
  4. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。

并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。

AVL树的旋转

左单旋

旋转示意图如下:

在这里插入图片描述

左单旋的步骤如下:

  1. 让subR的左子树作为parent的右子树。
  2. 让parent作为subR的左子树。
  3. 让subR作为整个子树的根。
  4. 更新平衡因子。

左单旋后满足二叉搜索树的性质:

  1. subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
  2. parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。

平衡因子更新如下:

在这里插入图片描述

可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左单旋后无需继续往上更新平衡因子。

代码如下:

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

    // 旋转链接
    parent->_right = subRL;
    // subRL不为nullptr
    if (subRL) {
        subRL->_parent = parent;
    }

    // 需要记录要旋转的树还有没有父亲
    Node *ppnode = parent->_parent;

    subR->_left = parent;
    parent->_parent = subR;

    // 如果ppnode为nullptr,说明parent一开始为根,旋转后subR为根
    if (ppnode == nullptr) {
        // 更新根节点
        _root = subR;
        _root->_parent = nullptr;
    } else {
        if (ppnode->_left == parent) {
            ppnode->_left = subR;
        } else {
            ppnode->_right = subR;
        }
        subR->_parent = ppnode;
    }
    // 更新平衡因子
    parent->_bf = subR->_bf = 0;
}

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

右单旋

旋转示意图如下:

在这里插入图片描述

右单旋的步骤如下

  1. 让subL的右子树作为parent的左子树。
  2. 让parent作为subL的右子树。
  3. 让subL作为整个子树的根。
  4. 更新平衡因子。

右单旋后满足二叉搜索树的性质

  1. subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
  2. parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。

平衡因子更新如下:

在这里插入图片描述

可以看到,经过右单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右单旋后无需继续往上更新平衡因子。

代码如下:

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

    parent->_left = subLR;//将subLR的右数作为parent的左数
    if (subLR) {
        subLR->_parent = parent;//更新subLR的父亲
    }

    Node *ppnode = parent->_parent;//记录祖先结点

    subL->_right = parent; //subL的右数更新为parent
    parent->_parent = subL;//更新parent的父亲

    //祖先结点链接更新后的父亲(subL)
    if (ppnode == nullptr) {
        _root = subL;
        _root->_parent = nullptr;
    } else {
        if (ppnode->_left == parent) {
            ppnode->_left = subL;
        } else {
            ppnode->_right = subL;
        }
        subL->_parent = ppnode;
    }
    //更新平衡因子
    parent->_bf = subL->_bf = 0;
}

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

左右双旋

左右双旋的步骤如下:

1、插入新结点。

在这里插入图片描述

2、以30为旋转点进行左单旋。

在这里插入图片描述

3、以90为旋转点进行右单旋。

在这里插入图片描述

左右双旋的步骤如下:

  1. 以subL为旋转点进行左单旋。
  2. 以parent为旋转点进行右单旋。
  3. 更新平衡因子。

左右双旋后满足二叉搜索树的性质

左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。

subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。

subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。

经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。

左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。

在这里插入图片描述

2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。

在这里插入图片描述

3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。

在这里插入图片描述

可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左右双旋后无需继续往上更新平衡因子。

代码如下

// 双旋(左右旋转)
void RotateLR(Node *parent) {
    Node *subL = parent->_left;
    Node *subLR = subL->_right;
    // subLR的平衡因子决定了,在哪里插入
    int bf = subLR->_bf;

    RotateLeft(parent->_left);
    RotateRight(parent);

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

右左双旋

右左双旋的步骤如下

1、插入新结点。

在这里插入图片描述

2、以90为旋转点进行右单旋。从subR的位置开始右单旋转

在这里插入图片描述

3、以30为旋转点进行左单旋。以parent开始右单旋

在这里插入图片描述

4.更新平衡因子

右左双旋后满足二叉搜索树的性质

右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。

  • subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
  • subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。
  • 经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。

右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
1、当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。

在这里插入图片描述

2、当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。

在这里插入图片描述

3、当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。

在这里插入图片描述

可以看到,经过右左双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右左双旋后无需继续往上更新平衡因子。

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

    int bf = subRL->_bf;
    //右单旋从parent->right开始旋转
    RotateRight(parent->_right);
    RotateLeft(parent);

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

插入代码

bool Insert(const pair<K, V> &kv) {
    //空树就new一个
    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走到了合适的位置
    cur = new Node(kv);
    // 选择插入到parent的左边还是右边
    if (kv.first < parent->_kv.first) {
        parent->_left = cur;
    } else {
        parent->_right = cur;
    }
    // cur链接parent
    cur->_parent = parent;

    // 更新平衡因子
    // 1.如果更新完以后,平衡因子没有出现问题(|bf|<=1),不需要处理
    // 2.如果更新完以后,平衡出现问题(|bf|>1),需要处理(旋转)

    // 插入新增节点。会影响祖先的平衡因子(全部或者部分)
    // 1、cur == parent->right  parent->bf++
    // 2、cur == parent->left   parent->bf--

    // 什么决定了是否继续往上更新祖先节点,取决于parent所在的子树高度是否变化?变了继续更新,不变则不再更新
    // a、parent->bf==1 || parent->bf == -1   parent所在的子树变了,需要继续更新祖先节点
    // b、parent->bf==2 || parent->bf == -1   parent所在的子树不平衡,需要处理这颗子树(旋转)
    // c、parent->bf==0, parent所在的子树高度不变,不用继续网上更新。插入结束 为什么?
    //  说明插入前parent->bf==1 or -1,插入之前一边高,一边低。插入节点插入矮的那边,它的高度不变

    while (parent) {
        //插入后,判断孩子插入到左边还是右边 进行++ --平衡因子
        if (cur == parent->_right) {
            parent->_bf++;
        } else {
            parent->_bf--;
        }

        if (parent->_bf == 1 || parent->_bf == -1) {
            // 更新祖先节点的bf
            parent = parent->_parent;
            cur = cur->_parent;
        } else if (parent->_bf == 0) {
            // 平衡,不需要处理
            break;
        } else if (parent->_bf == 2 || parent->_bf == -2) {
            // 需要旋转处理
            // 当parent->bf == 2 && cur->bf == 1 右边高,需要左单旋
            // 当parent->_bf == -2 && cur->_bf == -1 左边高,需要右单旋
            // 当parent->_bf == -2 && cur->_bf == 1 需要左右双旋转
            // 当parent->_bf == 2 && cur->_bf == -1 需要右左双旋转
            if (parent->_bf == 2 && cur->_bf == 1) {
                RotateLeft(parent);
            } else if (parent->_bf == -2 && cur->_bf == -1) {
                RotateRight(parent);
            } else if (parent->_bf == -2 && cur->_bf == 1) {
                RotateLR(parent);
            } else if (parent->_bf == 2 && cur->_bf == -1) {
                RotateRL(parent);
            }

            break;
        }
    }
    return true;
}

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。

代码如下:

void _InOrder(Node *root) {
    if (root == nullptr) {
        return;
    }
    _InOrder(root->_left);
    cout << root->_kv.first << " ";
    _InOrder(root->_right);
}

void InOrder() {
    _InOrder(this->_root);
    cout << endl;
}

但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,变量步骤如下:

  1. 从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
  2. 先判断左子树是否是平衡二叉树。
  3. 再判断右子树是否是平衡二叉树。
  4. 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

在这里插入图片描述

代码如下

// 判断是否是平衡树
bool _IsBalanace(Node *root) {
    if (root == nullptr) {
        return true;
    }

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

    if (rightHeight - leftHeight != root->_bf) {
        cout << root->_kv.first << "节点平衡因子异常" << endl;
        return false;
    }

    return abs(leftHeight - rightHeight) < 2 && _IsBalanace(root->_left) && _IsBalanace(root->_right);
}


bool IsBalanace() {
    return _IsBalanace(this->_root);
}

AVL树的查找

AVL树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

  1. 若树为空树,则查找失败,返回nullptr。
  2. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  3. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  4. 若key值等于当前结点的值,则查找成功,返回对应结点。

代码如下:

Node *Find(const K &key) {
    Node *cur = _root;
    while (cur) {
        //key值小于该结点的值
        if (key < cur->_kv.first) {
            //在该结点的左子树当中查找
            cur = cur->_left;
        } else if (key > cur->_kv.first) {
            //key值大于该结点的值
            cur = cur->_right;//在该结点的右子树当中查找
        } else {
            //找到了目标结点
            return cur;//返回该结点
        }
    }
    return nullptr;
}

AVL树的修改

实现修改AVL树当中指定key值结点的value,我们可以实现一个Modify函数,该函数当中的逻辑如下:

  1. 调用查找函数获取指定key值的结点。
  2. 对该结点的value进行修改。

代码如下:

bool Modify(const K &key, const V &value) {
    Node *ret = Find(key);
    //没找到key则返回false
    if (ret == nullptr) {
        return false;
    }

    ret->_kv.second = value;
    return true;
}

AVL树的删除

要进行结点的删除,首先需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同:

  1. 先找到待删除的结点。
  2. 若找到的待删除结点的左右子树均不为空,则需要使用替换法进行删除。

替换法删除指的是:让待删除结点左子树当中key值最大的结点,或待删除结点右子树当中值最小的结点代替待删除结点被删除(代码中以后者为例),然后再将待删除结点的key值以及value值都改为代替其被删除的结点的值即可。

在找到实际需要被删除的结点后,我们先不进行实际的删除,而是先进行平衡因子的更新,不然后续更新平衡因子时特别麻烦,而更新平衡因子时的规则与插入结点时的规则是相反的,

更新规则如下:

  • 删除的结点在parent的右边,parent的平衡因子–
  • 删除的结点在parent的左边,parent的平衡因子++

并且每更新完一个结点的平衡因子后,都需要进行以下判断:

  • 如果parent的平衡因子等于-1或者1,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于0,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断理由说明

parent更新后的平衡因子分析
-1或1只有0经过–/++操作后会变成-1/1,说明原来parent的左子树和右子树高度相同,现在我们删除一个结点,并不会影响以parent为根结点的子树的高度,从而变化影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。
0只有-1/1经过–/++操作后会变成0,说明本次删除操作使得parent的左右子树当中较高的一棵子树的高度降低了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。
-2或2此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。

注意:parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。

而在最坏情况下,删除结点后更新平衡因子时也会一路更新到根结点。例如下面这种情况:

在这里插入图片描述

在更新完平衡因子后,我们再进行实际删除结点的操作,因为实际删除结点的左右子树当中至少有一个为空树,因此我们实际删除结点时的逻辑如下:

  • 若实际删除结点的左子树为空,则让其parent链接到实际删除结点的右子树,最后再删除结点即可。
  • 若实际删除结点的右子树为空,则让其parent链接到实际删除结点的左子树,最后再删除结点即可。
  • 但是要注意,因为结点是三叉链结构,因此在链接结点的过程中需要建立两个结点之间的双向关系。

在进行旋转处理时,我们将其分为以下六种情况

  1. 当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋
  2. 当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋
  3. 当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋
  4. 当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋
  5. 当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋
  6. 当parent的平衡因子为2,parent的右孩子的平衡因子为0时,也进行左单旋

与插入结点不同的是,删除结点时若是进行了旋转处理,那么在进行旋转处理后我们必须继续往上更新平衡因子,因为旋转的本质就是降低树的高度,旋转后树的高度降低了,就会影响其父结点的平衡因子,因此我们还需要继续往上更新平衡因子。

注意: 上述旋转处理的六种情况当中,若属于情况三或情况六,那么在旋转后无需继续往上更新平衡因子,因为这两种情况旋转后树的高度并没有发生变化。

代码如下

bool Erase(const K &key) {
    //遍历二叉树
    Node *parent = nullptr;
    Node *cur = _root;
    //用于标记实际的删除结点及其父结点
    Node *delParentPos = nullptr;
    Node *delPos = nullptr;
    //找key值
    while (cur) {
        if (key < cur->_kv.first) {
            parent = cur;
            cur = cur->_left;
        } else if (key > cur->_kv.first) {
            parent = cur;
            cur = cur->_right;
        } else {//找到待删除结点
            //待删除结点的左子树为空
            if (cur->_left == nullptr) {
                //待删除结点是根结点,让根结点的右子树作为新的根结点
                if (cur == _root) {
                    _root = _root->_right;
                    if (_root) {
                        _root->_parent = nullptr;
                    }
                    delete cur;
                } else {
                    //带删除结点不是根结点
                    delParentPos = parent;//标记实际删除结点的父结点
                    delPos = cur;         //标记实际删除的结点
                }
                break;                          跳出循环,之后更新平衡因子
            } else if (cur->_right == nullptr) {//待删除结点的右子树为空
                //待删除结点是根结点,让根结点的左子树作为新的根结点
                if (cur == _root) {
                    _root = _root->_left;
                    if (_root) {
                        _root->_parent = nullptr;
                    }
                    delete cur;
                    return true;
                } else {
                    //带删除结点不是根结点
                    delParentPos = parent;
                    delPos = cur;
                }
                break;//跳出循环,之后更新平衡因子
            } else {  //待删除结点左右两边都不为空
                //使用替换法进行删除
                //找右子树中的最小结点成为待删除结点作为子树的根
                Node *minParent = cur;
                Node *minRight = cur->_right;
                //最小结点在最左边
                while (minRight->_left) {
                    minParent = minRight;
                    minRight = minRight->_left;
                }

                //更新待删除结点
                cur->_kv.first = minRight->_kv.first;
                cur->_kv.second = minRight->_kv.second;
                //更新待删除的结点,此时待删除的结点就是minRight
                delParentPos = minParent;
                delPos = minRight;
                //跳出循环,更新平衡因子
                break;
            }
        }
    }

    //key值不对,走到了nullptr,还没有找到key,说明要找的key不存在,返回false
    if (delPos == nullptr) {
        //删除结点没有修改过,则返回false
        return false;
    }

    //记录待删除结点及其父结点(用于后续实际删除)
    Node *del = delPos;
    Node *delParent = delParentPos;

    //更新平衡因子
    //待删除的结点不是根
    while (delPos != _root) {
        //删除结点在父结点左边需要++,在右边需要--
        if (delPos == delParentPos->_left) {
            delParentPos->_bf++;
        } else if (delPos == delParent->_right) {
            delParentPos->_bf--;
        }

        //判断是否更新结束或需要进行旋转
        //_bf == 0 需要向上更新
        //_bf == -1/1 不需要更新
        //_bf == -2/2 需要旋转
        if (delParentPos->_bf == 0) {
            //delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
            delPos = delParentPos;
            delParentPos = delParentPos->_parent;
        } else if (delParentPos->_bf == -1 || delParentPos->_bf == 1) {
            //delParent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
            break;
        } else if (delParentPos->_bf == -2 || delParentPos->_bf == 2) {
            //6种旋转
            if (delParentPos->_bf == -2) {
                if (delParentPos->_left->_bf == -1) {
                    Node *tmp = delParentPos->_left;//记录delParentPos右旋转后新的根结点
                    RotateRight(delParentPos);      //右单旋
                    delParentPos = tmp;             //更新根结点
                } else if (delParentPos->_left->_bf == 1) {
                    Node *tmp = delParentPos->_left->_right;//记录delParentPos左右双旋后新的根结点
                    RotateLR(delParentPos);                 //左右双旋
                    delParentPos = tmp;
                } else {
                    Node *tmp = delParentPos->_left;
                    RotateRight(delParentPos);//右单旋
                    delParentPos = tmp;       //更新根结点

                    //平衡因子调整
                    delParentPos->_bf = 1;
                    delParentPos->_right->_bf = -1;
                    break;
                }
            } else if (delParentPos->_bf == 2) {
                if (delParentPos->_right->_bf = -1) {
                    Node *tmp = delParentPos->_right->_left;//记录delParentPos右左旋转后新的根结点
                    RotateRL(delParentPos);                 //右左双旋
                    delParentPos = tmp;
                } else if (delParentPos->_right->_bf == 1) {
                    Node *tmp = delParentPos->_right;//记录delParentPos左旋转后新的根结点
                    RotateLeft(delParentPos);        //左旋转
                    delParentPos = tmp;
                } else {//bf==0
                    Node *tmp = delParentPos->_right;
                    RotateLeft(delParentPos);
                    delParentPos = tmp;

                    //更新平衡因子
                    delParentPos->_bf = -1;
                    delParentPos->_bf = 1;
                    break;
                }
            }
            //旋转后delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
            delPos = delParentPos;
            delParentPos = delParentPos->_parent;
        } else {
            //在删除前树的平衡因子就有问题
            assert(false);
        }
    }

    //进行实际删除
    if (del->_left == nullptr) {      //实际删除结点的左子树为空
        if (del == delParent->_left) {//实际删除结点是其父结点的左孩子
            //让父亲的左孩子指向被删除结点的有孩子
            delParent->_left = del->_right;
            //如果有孩子不为空,绑定右孩子的父亲
            if (del->_right) {
                del->_right->_parent = delParent;
            }
        } else {//实际删除结点是其父结点的右孩子
            //直接领养被删除结点的右孩子,右孩子不为空则绑定父亲
            delParent->_right = del->_right;
            if (del->_right) {
                del->_right->_parent = delParent;
            }
        }
    } else {//实际删除结点的右子树为
        //实际删除结点是其父结点的左孩子
        if (del == delParent->_left) {
            //被删除结点父亲领养被删除结点的左孩子
            delParent->_left = del->_left;
            //绑定父亲
            if (del->_left) {
                del->_left->_parent = delParent;
            }

        } else {//实际删除结点是其父结点的右孩子
            //领养被删除结点左孩子,并绑定父亲
            delParent->_right = del->_left;
            if (del->_left) {
                del->_left->_parent = delParent;
            }
        }
    }
    delete del;

    return true;
}

AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。

AVL树的代码测试

AVLTree.h文件

#pragma once
#include <assert.h>
#include <iostream>
#include <utility>
using namespace std;

// AVL平衡树,左右高度差不超过1
template<class K, class V>
struct AVLTreeNode {
    AVLTreeNode<K, V> *_left;
    AVLTreeNode<K, V> *_right;
    AVLTreeNode<K, V> *_parent;
    pair<K, V> _kv;// 存储键值对的pair对象;pair是一个模板类,它可以用来存储两个不同类型的值,头文件utility

    int _bf;// balance factor 平衡因子,用于AVL树的平衡操作。 -1 0 1是正常的平衡因子

    AVLTreeNode(const pair<K, V> &kv)// 构造函数传参为一个pair对象
        : _left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0) {
    }
};

template<class K, class V>
class AVLTree {
public:
    typedef AVLTreeNode<K, V> Node;
    bool Insert(const pair<K, V> &kv) {
        //空树就new一个
        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走到了合适的位置
        cur = new Node(kv);
        // 选择插入到parent的左边还是右边
        if (kv.first < parent->_kv.first) {
            parent->_left = cur;
        } else {
            parent->_right = cur;
        }
        // cur链接parent
        cur->_parent = parent;

        // 更新平衡因子
        // 1.如果更新完以后,平衡因子没有出现问题(|bf|<=1),不需要处理
        // 2.如果更新完以后,平衡出现问题(|bf|>1),需要处理(旋转)

        // 插入新增节点。会影响祖先的平衡因子(全部或者部分)
        // 1、cur == parent->right  parent->bf++
        // 2、cur == parent->left   parent->bf--

        // 什么决定了是否继续往上更新祖先节点,取决于parent所在的子树高度是否变化?变了继续更新,不变则不再更新
        // a、parent->bf==1 || parent->bf == -1   parent所在的子树变了,需要继续更新祖先节点
        // b、parent->bf==2 || parent->bf == -1   parent所在的子树不平衡,需要处理这颗子树(旋转)
        // c、parent->bf==0, parent所在的子树高度不变,不用继续网上更新。插入结束 为什么?
        //  说明插入前parent->bf==1 or -1,插入之前一边高,一边低。插入节点插入矮的那边,它的高度不变

        while (parent) {
            //插入后,判断孩子插入到左边还是右边 进行++ --平衡因子
            if (cur == parent->_right) {
                parent->_bf++;
            } else {
                parent->_bf--;
            }

            if (parent->_bf == 1 || parent->_bf == -1) {
                // 更新祖先节点的bf
                parent = parent->_parent;
                cur = cur->_parent;
            } else if (parent->_bf == 0) {
                // 平衡,不需要处理
                break;
            } else if (parent->_bf == 2 || parent->_bf == -2) {
                // 需要旋转处理
                // 当parent->bf == 2 && cur->bf == 1 右边高,需要左单旋
                // 当parent->_bf == -2 && cur->_bf == -1 左边高,需要右单旋
                // 当parent->_bf == -2 && cur->_bf == 1 需要左右双旋转
                // 当parent->_bf == 2 && cur->_bf == -1 需要右左双旋转
                if (parent->_bf == 2 && cur->_bf == 1) {
                    RotateLeft(parent);
                } else if (parent->_bf == -2 && cur->_bf == -1) {
                    RotateRight(parent);
                } else if (parent->_bf == -2 && cur->_bf == 1) {
                    RotateLR(parent);
                } else if (parent->_bf == 2 && cur->_bf == -1) {
                    RotateRL(parent);
                }

                break;
            }
        }
        return true;
    }

    void InOrder() {
        _InOrder(this->_root);
        cout << endl;
    }

    bool IsBalanace() {
        return _IsBalanace(this->_root);
    }

    int Height() {
        return _Height(this->_root);
    }

    int _Height(Node *root) {
        if (root == nullptr)
            return 0;

        int leftHeight = _Height(root->_left) + 1;
        int rightHeight = _Height(root->_right) + 1;

        return leftHeight > rightHeight ? leftHeight : rightHeight;
    }

    // 判断是否是平衡树
    bool _IsBalanace(Node *root) {
        if (root == nullptr) {
            return true;
        }

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

        if (rightHeight - leftHeight != root->_bf) {
            cout << root->_kv.first << "节点平衡因子异常" << endl;
            return false;
        }

        return abs(leftHeight - rightHeight) < 2 && _IsBalanace(root->_left) && _IsBalanace(root->_right);
    }

    bool Modify(const K &key, const V &value) {
        Node *ret = Find(key);
        //没找到key则返回false
        if (ret == nullptr) {
            return false;
        }

        ret->_kv.second = value;
        return true;
    }

    bool Erase(const K &key) {
        //遍历二叉树
        Node *parent = nullptr;
        Node *cur = _root;
        //用于标记实际的删除结点及其父结点
        Node *delParentPos = nullptr;
        Node *delPos = nullptr;
        //找key值
        while (cur) {
            if (key < cur->_kv.first) {
                parent = cur;
                cur = cur->_left;
            } else if (key > cur->_kv.first) {
                parent = cur;
                cur = cur->_right;
            } else {//找到待删除结点
                //待删除结点的左子树为空
                if (cur->_left == nullptr) {
                    //待删除结点是根结点,让根结点的右子树作为新的根结点
                    if (cur == _root) {
                        _root = _root->_right;
                        if (_root) {
                            _root->_parent = nullptr;
                        }
                        delete cur;
                    } else {
                        //带删除结点不是根结点
                        delParentPos = parent;//标记实际删除结点的父结点
                        delPos = cur;         //标记实际删除的结点
                    }
                    break;                          跳出循环,之后更新平衡因子
                } else if (cur->_right == nullptr) {//待删除结点的右子树为空
                    //待删除结点是根结点,让根结点的左子树作为新的根结点
                    if (cur == _root) {
                        _root = _root->_left;
                        if (_root) {
                            _root->_parent = nullptr;
                        }
                        delete cur;
                        return true;
                    } else {
                        //带删除结点不是根结点
                        delParentPos = parent;
                        delPos = cur;
                    }
                    break;//跳出循环,之后更新平衡因子
                } else {  //待删除结点左右两边都不为空
                    //使用替换法进行删除
                    //找右子树中的最小结点成为待删除结点作为子树的根
                    Node *minParent = cur;
                    Node *minRight = cur->_right;
                    //最小结点在最左边
                    while (minRight->_left) {
                        minParent = minRight;
                        minRight = minRight->_left;
                    }

                    //更新待删除结点
                    cur->_kv.first = minRight->_kv.first;
                    cur->_kv.second = minRight->_kv.second;
                    //更新待删除的结点,此时待删除的结点就是minRight
                    delParentPos = minParent;
                    delPos = minRight;
                    //跳出循环,更新平衡因子
                    break;
                }
            }
        }

        //key值不对,走到了nullptr,还没有找到key,说明要找的key不存在,返回false
        if (delPos == nullptr) {
            //删除结点没有修改过,则返回false
            return false;
        }

        //记录待删除结点及其父结点(用于后续实际删除)
        Node *del = delPos;
        Node *delParent = delParentPos;

        //更新平衡因子
        //待删除的结点不是根
        while (delPos != _root) {
            //删除结点在父结点左边需要++,在右边需要--
            if (delPos == delParentPos->_left) {
                delParentPos->_bf++;
            } else if (delPos == delParent->_right) {
                delParentPos->_bf--;
            }

            //判断是否更新结束或需要进行旋转
            //_bf == 0 需要向上更新
            //_bf == -1/1 不需要更新
            //_bf == -2/2 需要旋转
            if (delParentPos->_bf == 0) {
                //delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
                delPos = delParentPos;
                delParentPos = delParentPos->_parent;
            } else if (delParentPos->_bf == -1 || delParentPos->_bf == 1) {
                //delParent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
                break;
            } else if (delParentPos->_bf == -2 || delParentPos->_bf == 2) {
                //6种旋转
                if (delParentPos->_bf == -2) {
                    if (delParentPos->_left->_bf == -1) {
                        Node *tmp = delParentPos->_left;//记录delParentPos右旋转后新的根结点
                        RotateRight(delParentPos);      //右单旋
                        delParentPos = tmp;             //更新根结点
                    } else if (delParentPos->_left->_bf == 1) {
                        Node *tmp = delParentPos->_left->_right;//记录delParentPos左右双旋后新的根结点
                        RotateLR(delParentPos);                 //左右双旋
                        delParentPos = tmp;
                    } else {
                        Node *tmp = delParentPos->_left;
                        RotateRight(delParentPos);//右单旋
                        delParentPos = tmp;       //更新根结点

                        //平衡因子调整
                        delParentPos->_bf = 1;
                        delParentPos->_right->_bf = -1;
                        break;
                    }
                } else if (delParentPos->_bf == 2) {
                    if (delParentPos->_right->_bf = -1) {
                        Node *tmp = delParentPos->_right->_left;//记录delParentPos右左旋转后新的根结点
                        RotateRL(delParentPos);                 //右左双旋
                        delParentPos = tmp;
                    } else if (delParentPos->_right->_bf == 1) {
                        Node *tmp = delParentPos->_right;//记录delParentPos左旋转后新的根结点
                        RotateLeft(delParentPos);        //左旋转
                        delParentPos = tmp;
                    } else {//bf==0
                        Node *tmp = delParentPos->_right;
                        RotateLeft(delParentPos);
                        delParentPos = tmp;

                        //更新平衡因子
                        delParentPos->_bf = -1;
                        delParentPos->_bf = 1;
                        break;
                    }
                }
                //旋转后delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
                delPos = delParentPos;
                delParentPos = delParentPos->_parent;
            } else {
                //在删除前树的平衡因子就有问题
                assert(false);
            }
        }

        //进行实际删除
        if (del->_left == nullptr) {      //实际删除结点的左子树为空
            if (del == delParent->_left) {//实际删除结点是其父结点的左孩子
                //让父亲的左孩子指向被删除结点的有孩子
                delParent->_left = del->_right;
                //如果有孩子不为空,绑定右孩子的父亲
                if (del->_right) {
                    del->_right->_parent = delParent;
                }
            } else {//实际删除结点是其父结点的右孩子
                //直接领养被删除结点的右孩子,右孩子不为空则绑定父亲
                delParent->_right = del->_right;
                if (del->_right) {
                    del->_right->_parent = delParent;
                }
            }
        } else {//实际删除结点的右子树为
            //实际删除结点是其父结点的左孩子
            if (del == delParent->_left) {
                //被删除结点父亲领养被删除结点的左孩子
                delParent->_left = del->_left;
                //绑定父亲
                if (del->_left) {
                    del->_left->_parent = delParent;
                }

            } else {//实际删除结点是其父结点的右孩子
                //领养被删除结点左孩子,并绑定父亲
                delParent->_right = del->_left;
                if (del->_left) {
                    del->_left->_parent = delParent;
                }
            }
        }
        delete del;

        return true;
    }

private:
    void _InOrder(Node *root) {
        if (root == nullptr) {
            return;
        }
        _InOrder(root->_left);
        cout << root->_kv.first << " ";
        _InOrder(root->_right);
    }

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

        // 旋转链接
        parent->_right = subRL;
        // subRL不为nullptr
        if (subRL) {
            subRL->_parent = parent;
        }

        // 需要记录要旋转的树还有没有父亲
        Node *ppnode = parent->_parent;

        subR->_left = parent;
        parent->_parent = subR;

        // 如果ppnode为nullptr,说明parent一开始为根,旋转后subR为根
        if (ppnode == nullptr) {
            // 更新根节点
            _root = subR;
            _root->_parent = nullptr;
        } else {
            if (ppnode->_left == parent) {
                ppnode->_left = subR;
            } else {
                ppnode->_right = subR;
            }
            subR->_parent = ppnode;
        }
        // 更新平衡因子
        parent->_bf = subR->_bf = 0;
    }

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

        parent->_left = subLR;//将subLR的右数作为parent的左数
        if (subLR) {
            subLR->_parent = parent;//更新subLR的父亲
        }

        Node *ppnode = parent->_parent;//记录祖先结点

        subL->_right = parent; //subL的右数更新为parent
        parent->_parent = subL;//更新parent的父亲

        //祖先结点链接更新后的父亲(subL)
        if (ppnode == nullptr) {
            _root = subL;
            _root->_parent = nullptr;
        } else {
            if (ppnode->_left == parent) {
                ppnode->_left = subL;
            } else {
                ppnode->_right = subL;
            }
            subL->_parent = ppnode;
        }
        //更新平衡因子
        parent->_bf = subL->_bf = 0;
    }

    // 双旋(左右旋转)
    void RotateLR(Node *parent) {
        Node *subL = parent->_left;
        Node *subLR = subL->_right;
        // subLR的平衡因子决定了,在哪里插入
        int bf = subLR->_bf;

        RotateLeft(parent->_left);
        RotateRight(parent);

        //更新平衡因子
        if (bf == 1) {
            parent->_bf = 0;
            subLR->_bf = 0;
            subL->_bf = -1;
        } else if (bf == -1) {
            parent->_bf = 1;
            subLR->_bf = 0;
            subL->_bf = 0;
        } 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;
        //右单旋从parent->right开始旋转
        RotateRight(parent->_right);
        RotateLeft(parent);

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

    Node *Find(const K &key) {
        Node *cur = _root;
        while (cur) {
            //key值小于该结点的值
            if (key < cur->_kv.first) {
                //在该结点的左子树当中查找
                cur = cur->_left;
            } else if (key > cur->_kv.first) {
                //key值大于该结点的值
                cur = cur->_right;//在该结点的右子树当中查找
            } else {
                //找到了目标结点
                return cur;//返回该结点
            }
        }
        return nullptr;
    }


private:
    Node *_root = nullptr;
};

AVL树测试代码

#include "AVLTree.h"
#include <cstdlib>
#include <ctime>
#include <iostream>
using namespace std;

void Test_AVLTree1() {
    int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};
    AVLTree<int, int> t1;
    for (auto e: a) {
        // 条件断点
        if (e == 14) {
            int x = 0;// 不能为空语句
        }
        t1.Insert(make_pair(e, e));
        cout << e << "插入:" << t1.IsBalanace() << endl;// 为了测试插入哪个异常所以加上了打印
                                                        // 屏蔽右左旋转的平衡因子更新观察现象
    }
    t1.Erase(1);
    t1.InOrder();
    cout << t1.IsBalanace() << endl;
}

void Test_AVLTree2() {
    srand(time(nullptr));
    const size_t n = 1000000;
    AVLTree<int, int> t;
    for (size_t i = 0; i < n; i++) {
        size_t x = rand();
        t.Insert(make_pair(x, x));
    }
    //t.InOrder();
    cout << t.IsBalanace() << endl;
    cout << t.Height() << endl;
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1015282.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计网第四章(网络层)(八)(最短路径优先协议OSPF)

在第七节&#xff08;计网第四章&#xff08;网络层&#xff09;&#xff08;七&#xff09;_永无魇足的博客-CSDN博客&#xff09;我们总结了路由信息协议RIP。在最后我们提到了RIP协议有坏消息传的慢的问题&#xff0c;这是距离向量算法的本质决定的&#xff0c;所以这种问题…

c语言练习59:深入理解char类型的取值范围

深入理解char类型的取值范围 例如&#xff1a; #include <stdio.h> int main() {char a[1000];int i;for(i0; i<1000; i){a[i] -1-i;}printf("%d",strlen(a));return 0; }结果为255 ab以%d的形式打印结果为&#xff1a;300 而c由于unsigned char的取值范…

【CMU15-445 Part-9】Multi-Threaded Index Concurrency Control

Part09-Multi-Threaded Index Concurrency Control 多线程下索引的并发控制 Concurrency Control 强制所有访问数据结构的线程都使用某种协议或者某种方式。并发控制协议的概念&#xff1a;并发控制协议是一种当并发操作作用在一个共享对象上时DBMS用来确保correct的method。…

【杂记】git管理工具的相关应用

这里记录一些用git管理工具进行开发的命令&#xff0c;便于自己查看&#xff0c;我认为下面两篇博客写的很详细&#xff0c;但是为了自己方便查看&#xff0c;所以自己写了一些命令供自己进一步理解。gitee相对git来说更方便一些&#xff08;毕竟国内的不用担心墙&#xff09;&…

JavaEE 网络原理——TCP的工作机制(初篇 包含 UDP 协议的再次阐述)

文章目录 一、再次简述 UDP 协议二、再次简述 TCP 协议三、描述部分 TCP 内部的工作机制1. 确认应答2. 超时重传 前提&#xff1a; 在前面的文章中&#xff0c;我向大家分别简单介绍了 TCP 协议和 UDP 包装一个数据形成数据报发送信息。 除此之外&#xff0c;还通过代码编写了 …

QT中摄像头的使用

QT中摄像头相关类 摄像头的使用 QT中摄像头的使用主要分为三个方面&#xff0c;显示画面、抓取图片和视频录制。这三个方面对应着摄像模块的三种模式。模式如下&#xff1a; ConstantValueDescriptionQCamera::CaptureViewfinder0相机仅配置为显示取景器。QCamera::CaptureSt…

React入门

一、react开始 1、react是什么 用于构建用户界面的JavaScript库 操作DOM呈现页面 &#xff08;发送请求获取数据和处理数据不由react处理&#xff09;fessbook开发 2、为什么要学 原生js操作DOM繁琐、效率低 使用原生js直接操作DOM&#xff0c;浏览器会进行大量重绘重排 原…

计算机视觉面试题整理

1、介绍目标检测网络yolo系列以及ssd系列的原理&#xff0c;yolo对小目标检测不好的原因&#xff0c;除了缩小anchor外还可以如何改善&#xff1f; Yolo目标检测&#xff1a;YOLO是一种实时目标检测算法&#xff0c;其核心思想是将目标检测问题归为一个回归问题&#xff0c;直…

Android毕业设计,基于Android 语音朗读书籍管理系统

视频演示&#xff1a; 基于Android 语音朗读书籍管理系统 基于 Android 的语音朗读书籍管理系统可以提供用户管理书籍、朗读书籍的功能。以下是一个简单的步骤和功能列表&#xff1a; 用户注册和登录功能&#xff1a; 用户可以注册新账号或使用现有账号登录系统。用户信息可以包…

【rust/egui】(十一)使用rfd选择文件并使用serde_json进行序列化

说在前面 rust新手&#xff0c;egui没啥找到啥教程&#xff0c;这里自己记录下学习过程环境&#xff1a;windows11 22H2rust版本&#xff1a;rustc 1.71.1egui版本&#xff1a;0.22.0eframe版本&#xff1a;0.22.0上一篇&#xff1a;这里 rfd-Rusty File Dialogs 一个跨平台的…

只需3步部署Django项目到Kubernetes上

1. 目标 本文讲述了如何通过3步&#xff0c;把Django项目部署在K8S上。 本文适用读者&#xff1a; 了解Django项目的开发。了解K8S的用途。 2. 具体步骤 把一个Django项目部署在Kubernete环境上&#xff0c;只需以下3步&#xff1a; 创建镜像部署在Kubernetes环境上配置MyS…

linux命令查看谁在使用服务器的GPU

命令&#xff1a;查看GPU使用情况 nvidia-smi 可以知悉GPU占用情况和主要使用GPU的进程&#xff0c;如下图所示&#xff1a; 实时查看gpu使用&#xff1a; nvidia-smi -l 1 表示每隔1s刷新一下&#xff0c;数字可更改。 查看进程的归属者 方法一&#xff1a;ps -f -p pid…

360极速浏览器X终极奥义之——更改划词工具条的搜索为百度搜索 2023更新版

原文为2019版本&#xff0c;具体已失效&#xff0c;2023更新。 1.需要将 https://www.so.com/s?q%s&src360csex_zoned字符串对应的十六进制码替换为 https://www.baidu.com/s?wd%s&src360csex_z对应的十六进制码。 2.需要删除后面的"oned"以保证转换出来…

冒泡排序~

1、对应长度len 数组&#xff0c;需要进行 len -1 趟冒泡&#xff0c;每趟冒泡&#xff0c;将最大&#xff08;小&#xff09;元素排列到最后无序位置 2、每趟冒泡从第一个元素开始&#xff0c;邻近两两比较&#xff0c;找出最大元素 每一趟冒泡&#xff0c;都进行元素交换&am…

基于频谱信息的图像去噪与恢复——使用约束最小二乘方滤波法

大家好&#xff0c;我是带我去滑雪&#xff01; 随着科学技术的不断发展&#xff0c;信息的交流和获取已不再受到时空的限制&#xff0c;已经成为人们日常生活中不可或缺的一部分。图像作为人类信息交流中的重要载体&#xff0c;起着不可替代的作用。频谱图像去噪复原方法是一种…

Hive【Hive(一)DDL】

前置准备 需要启动 Hadoop 集群&#xff0c;因为我们 Hive 是在 Hadoop 集群之上运行的。 从DataGrip 或者其他外部终端连接 Hive 需要先打开 Hive 的 metastore 进程和 hiveserver2 进程。 Hive DDL 数据定义语言 1、数据库&#xff08;database&#xff09; 创建数据库 c…

YOLOv8『小目标』检测指南

前言 目前博主课题组在进行物体部件的异常检测项目&#xff0c;项目中需要先使用 YOLOv8 进行目标检测&#xff0c;然后进行图像切割&#xff0c;最后采用 WinCLIP 模型 进行部件异常检测 但是在实际操作过程中出现问题&#xff0c; YOLOv8 模型目标检测在大目标精确度不错&a…

Vue的详细教程--入门

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Vue的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Vue是什么 二. Vue的特点及优势 三.使用…

IT运维:利用数据分析平台采集Windows event log数据

概述 本文将介绍如何借助Winlogbeat和Vector在鸿鹄里采集Windows event log数据&#xff0c;使技术人员能够在鸿鹄里更便捷和高效地分析Windows event log数据。 操作步骤 Winlogbeat是一个开源的日志数据采集器&#xff0c;专门用于采集Windows操作系统中的event log数据。它可…

【JAVA数据结构】包装类与认识泛型

作者主页&#xff1a;paper jie 的博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《JAVA数据结构》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精…