【数据结构】C++实现AVL平衡树
文章目录
- 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是一个模板类,它可以用来存储两个不同类型的值,头文件utilityint _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树插入结点时有以下三个步骤:
- 按照二叉搜索树的插入方法,找到待插入位置。
- 找到待插入位置后,将待插入结点插入到树中。
- 更新平衡因子,如果出现不平衡,则需要进行旋转。
因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:
- 待插入结点的key值比当前结点小就插入到该结点的左子树。
- 待插入结点的key值比当前结点大就插入到该结点的右子树。
- 待插入结点的key值与当前结点的key值相等就插入失败。
如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。
与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。
由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。
所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:
- 新增结点在parent的右边,parent的平衡因子+1。
- 新增结点在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,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:
- 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
- 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。
综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
根据此结论,我们可以将旋转处理分为以下四类:
- 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
- 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
- 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
- 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。
AVL树的旋转
左单旋
旋转示意图如下:
左单旋的步骤如下:
- 让subR的左子树作为parent的右子树。
- 让parent作为subR的左子树。
- 让subR作为整个子树的根。
- 更新平衡因子。
左单旋后满足二叉搜索树的性质:
- subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
- parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。
平衡因子更新如下:
可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左单旋后无需继续往上更新平衡因子。
代码如下:
// 左单旋
void RotateLeft(Node *parent) {Node *subR = parent->_right;// 要旋转的parent的右子树Node *subRL = subR->_left; // 子树的左子树// 旋转链接parent->_right = subRL;// subRL不为nullptrif (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;
}
注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。
右单旋
旋转示意图如下:
右单旋的步骤如下:
- 让subL的右子树作为parent的左子树。
- 让parent作为subL的右子树。
- 让subL作为整个子树的根。
- 更新平衡因子。
右单旋后满足二叉搜索树的性质:
- subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
- 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的右数更新为parentparent->_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为旋转点进行右单旋。
左右双旋的步骤如下:
- 以subL为旋转点进行左单旋。
- 以parent为旋转点进行右单旋。
- 更新平衡因子。
左右双旋后满足二叉搜索树的性质:
左右双旋后,实际上就是让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链接parentcur->_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) {// 更新祖先节点的bfparent = 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)
- 先判断左子树是否是平衡二叉树。
- 再判断右子树是否是平衡二叉树。
- 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)
代码如下:
// 判断是否是平衡树
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树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:
- 若树为空树,则查找失败,返回nullptr。
- 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
- 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
- 若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函数,该函数当中的逻辑如下:
- 调用查找函数获取指定key值的结点。
- 对该结点的value进行修改。
代码如下:
bool Modify(const K &key, const V &value) {Node *ret = Find(key);//没找到key则返回falseif (ret == nullptr) {return false;}ret->_kv.second = value;return true;
}
AVL树的删除
要进行结点的删除,首先需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同:
- 先找到待删除的结点。
- 若找到的待删除结点的左右子树均不为空,则需要使用替换法进行删除。
替换法删除指的是:让待删除结点左子树当中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链接到实际删除结点的左子树,最后再删除结点即可。
- 但是要注意,因为结点是三叉链结构,因此在链接结点的过程中需要建立两个结点之间的双向关系。
在进行旋转处理时,我们将其分为以下六种情况:
- 当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋
- 当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋
- 当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋
- 当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋
- 当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋
- 当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;//更新待删除的结点,此时待删除的结点就是minRightdelParentPos = minParent;delPos = minRight;//跳出循环,更新平衡因子break;}}}//key值不对,走到了nullptr,还没有找到key,说明要找的key不存在,返回falseif (delPos == nullptr) {//删除结点没有修改过,则返回falsereturn 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==0Node *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是一个模板类,它可以用来存储两个不同类型的值,头文件utilityint _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链接parentcur->_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) {// 更新祖先节点的bfparent = 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则返回falseif (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;//更新待删除的结点,此时待删除的结点就是minRightdelParentPos = minParent;delPos = minRight;//跳出循环,更新平衡因子break;}}}//key值不对,走到了nullptr,还没有找到key,说明要找的key不存在,返回falseif (delPos == nullptr) {//删除结点没有修改过,则返回falsereturn 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==0Node *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不为nullptrif (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的右数更新为parentparent->_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;
}
相关文章:
【数据结构】C++实现AVL平衡树
文章目录 1.AVL树的概念2.AVL树的实现AVL树结点的定义AVL树的插入AVL树的旋转左单旋右单旋左右双旋右左双旋插入代码 AVL树的验证AVL树的查找AVL树的修改AVL树的删除AVL树的性能 AVL树的代码测试 1.AVL树的概念 二叉搜索树虽然可以提高我们查找数据的效率,但如果插…...
图神经网络系列之序章
文章目录 一、为什么需要图神经网络?二、图的定义1.图的定义和种类2.一些关于图的重要概念2.1 子图2.2 连通图2.3 顶点的度、入度和出度2.4 边的权和网2.5 稠密图、稀疏图 3.图的存储结构3.1 邻接矩阵3.2 邻接表3.3 边集数组3.4 邻接多重表3.5 十字链表3.6 链式前向…...
Unity中 UI Shader的基本功能
文章目录 前言一、实现思路1、暴露一个 2D 类型的属性来接受UI的纹理2、设置shader的层级为TransParent半透明渲染层级,一般UI都是在这个渲染层级3、更改混合模式,是 UI 使用的纹理,该透明的地方透明 二、代码实现 前言 Unity中 UI Shader的…...
【自学开发之旅】Flask-标准化返回-连接数据库-分表-orm-migrate-增删改查(三)
业务逻辑不能用http状态码判断,应该有自己的逻辑判断。想要前端需要判断(好多if…else),所以需要标准化,标准化返回。 json标准化返回: 最外面:data,message,code三个字段。 data:返回的数据 co…...
numpy增删改查
NumPy是一个用于科学计算的Python库,它提供了一个多维数组对象以及许多用于操作这些数组的函数。下面是关于如何在NumPy中进行增删改查操作的一些基本示例: 创建NumPy数组: import numpy as np # 创建一个一维数组 arr np.array([1, 2, 3, …...
【kafka】kafka重要的集群参数配置
如何规划Kafka 对于实际应用的生产环境中,需要尽量先规划设计好集群,避免后期业务上线后费力调整。在考量部署方案时需要通盘考虑,不能仅从单个维度上进行评估,下面是几个重要的维度的考量和建议: 这里重点说说操作系…...
cs224w_colab3_2023 And cs224w_colab4_2023学习笔记
class GNNStack(torch.nn.Module):def __init__(self, input_dim, hidden_dim, output_dim, args, embFalse):super(GNNStack, self).__init__() #这里的继承表示参见 https://blog.csdn.net/wanzew/article/details/106993425 # 继承时运行继承类别的函数 总之 __mro__的目的…...
Cannot find module ‘prop-types‘
把这个import删了。...
LeetCode-63-不同路径Ⅱ-动态规划
题目描述: 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。 现在考虑网格中有障碍物。那…...
unity 使用Photon进行网络同步
Pun使用教程 第一步:请确保使用的 Unity 版本等于或高于 2017.4(不建议使用测试版)创建一个新项目。 第二步:打开资源商店并找到 PUN 2 资源并下载/安装它。 导入所有资源后,让 Unity 重新编译。 第三步…...
大数据课程M1——ELK的概述
文章作者邮箱:yugongshiyesina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解ELK的定义; ⚪ 掌握ELK的使用; 一、什么是ELK 1. 简介 ELK 是elastic公司提供的一套完整的日志收集以及展示的解决方案,是三个…...
C# byte[] 如何转换成byte*
目标:将byte[]转成byte*以方便使用memcpy [DllImport("kernel32.dll", EntryPoint "RtlCopyMemory", CharSet CharSet.Ansi)] public extern static long CopyMemory(IntPtr dest, IntPtr source, int size); private void butTemp_Click(object…...
MySQL与Oracle的分页
MySQL与Oracle的分页 当我们通过SQL去查询一个结果集的时候,并不需要查看所有行,可能只是查看前几行,或者中间的几行。则需要像MySQL的limit或Oracle的ROWNUM与FETCH NEXT来实现。 MySQL 语法 SELECT * FROM table_name LIMIT [offset,] ro…...
git基本手册
Git and GitHub for Beginners Tutorial - YouTube Kevin Stratvert git config --global user.name “xxx” git config --global user.email xxxxx.com 设置默认分支 git config --global init.default branch main git config -h查看帮助 详细帮助 git help config 清除 cl…...
每日一题(两数相加)
每日一题(两数相加) 2. 两数相加 - 力扣(LeetCode) 思路 思路: 由于链表从头开始向后存储的是低权值位的数据,所以只需要两个指针p1和p2,分别从链表的头节点开始遍历。同时创建一个新的指针new…...
恒运资本:沪指震荡涨0.28%,医药板块强势拉升,金融等板块上扬
15日早盘,沪指盘中震荡上扬,科创50指数表现强势;北向资金小幅净流入。 到午间收盘,沪指涨0.28%报3135.31点,深成指、创业板指涨均0.11%,科创50指数涨1.04%;两市合计成交4357亿元,北…...
【计算机网络】Tcp详解
文章目录 前言Tcp协议段格式TCP的可靠性面向字节流应答机制超时重传流量控制滑动窗口(重要)拥塞控制延迟应答捎带应答标志位具体标志位三次握手四次挥手粘包问题TCP异常情况listen的第二个参数 前言 前面我们学习了传输层协议Udp,今天我们一…...
最简单的laravel不使用任何扩展导出csv
php导出csv是非常常用的操作,网上也有灰常多的扩展。如果只是单纯的导出csv数据,完全没有必要去用扩展。现在做项目,都是代码能少就少,扩展能不用就不用。好了,不废话了,开干! 直接搞一个方法&…...
Android studio 断点调试、日志断点
目录 参考文章参考文章1、运行调试2、调试操作3、断点类型行断点的使用场景属性断点的使用场景异常断点的使用场景方法断点的使用场景条件断点日志断点 4、断点管理区 参考文章 参考文章 1、运行调试 开启 Debug 调试模式有两种方式: Debug Run:直接…...
服务器数据恢复-热备盘同步过程中硬盘离线的RAID5数据恢复案例
服务器数据恢复环境: 华为OceanStor某型号存储,11块硬盘组建了一组RAID5阵列,另外1块硬盘作为热备盘使用。基于RAID5阵列的LUN分配给linux系统使用,存放Oracle数据库。 服务器故障: RAID5阵列1块硬盘由于未知原因离线…...
Python 使用input获取用户输入
视频版教程 Python3零基础7天入门实战视频教程 input()函数用于向用户生成一条提示,然后获取用户输入的内容。由于input()函数总会将用户输入的内容放入字符串中,因此用户可以输入任何内容,input()函数总是返回一个字符串。我们可以通过int(…...
Python 可迭代对象、迭代器、生成器
可迭代对象 定义 在Python的任意对象中,只要它定义了可以返回一个迭代器的 __iter__ 魔法方法,或者定义了可以支持下标索引的 __getitem__ 方法,那么它就是一个可迭代对象,通俗的说就是可以通过 for 循环遍历了。Python 原生的列…...
HTML的有序列表、无序列表、自定义列表
目录 背景: 过程: 有序列表: 简介: 代码展示: 效果展示: 无序列表: 简介: 代码展示: 效果展示: 自定义列表: 简介: 代码展示: 效果展示: 总结: 背景: 1.有序列表(Ordered List): 有序列表是最早的…...
银河麒麟安装Docker-国产化-九五小庞
银河麒麟高级服务器操作系统 V10 是针对企业级关键业务,适应虚拟化、 云计算、大数据、工业互联网时代对主机系统可靠性、安全性、性能、扩展性和 实时性的需求,依据 CMMI 5 级标准研制的提供内生安全、云原生支持、国产 平台深入优化、高性能、易管理的…...
数据库与身份认证
1. 数据库的基本概念 1.1 什么是数据库 数据库(database)是用来组织、存储和管理数据的仓库。 当今世界是一个充满着数据的互联网世界,充斥着大量的数据。数据的来源有很多,比如出行记录、消费记录、浏览的网页、发送的消息…...
LabVIEW开发锅炉汽包水位的监督控制和模拟
LabVIEW开发锅炉汽包水位的监督控制和模拟 控制锅炉汽包液位对于机械的安全和设备的保护至关重要。滚筒液位控制器的工作是将滚筒液位提高到指定的设定点,并保持在那里,同时保持一致的蒸汽负荷。锅炉管可能会因该水平急剧下降而暴露,这会导致…...
2023-简单点-树莓派安装ncnn框架
not python 按照下面的步骤进行就可以了: 参考 tips: 其中有一步要用下面方法: 如果你的git clone不得行,可以按照以下操作方法: git clone --depth1 https://ghproxy.com/ https://github.com/Tencent/ncnn.git python 直接 pip install …...
Docker核心原理与实操
第一章、Docker基本概念 1、概念:Docker是一种容器技术,可以解决软件跨环境迁移问题。 2、实现原理:是一个分层复用的文件系统;每一层都是一个独立的软件; …...
虚幻引擎 UE5 增强输入系统
用人话讲!虚幻引擎 UE5 增强输入系统(蓝图篇)_酥妃大魔王i的博客-CSDN博客 UE5 -- EnhancedInput(增强输入系统) - 知乎 (zhihu.com) 简单认识 虚幻引擎中的增强输入 | 虚幻引擎5.1文档 (unrealengine.com) 文档有较详细介绍 标记一下方便…...
Mac 安装软件各种报错解决方案
Mac 安装软件各种报错解决方案 文章目录 Mac 安装软件各种报错解决方案一. 打开允许“允许任何来源”二. 无法打开"xxx",因为它不是从App Store下载三. 无法打开"xxx",因为 Apple无法检查其是否包含恶意软件。四. "xxx"已…...
长武网站建设/亚马逊查关键词搜索量的工具
报错:(CHTCollectionViewWaterfallLayout是通过pods添加进来的),然后pods中的其他库都可以编译通过,就是这个库一直找不到。最后得到同事的协助,找到了原因。 ld: library not found for -lCHTCollectionVi…...
武汉外贸网站建设/搜索引擎优化技巧
在最新的MIUI V5中的短信界面,如果我们按“菜单”键已经看不到曾经在这里出现的“私密短信”字样了。那它到底跑哪里去了呢?既然是私密,当然要在更隐蔽更不容易被发现的地方了。官方日志中给出的答案是----“在短信界面努力下拉即可开启”。说白了&…...
做网站工作怀孕/百度用户服务中心人工24小时电话
转自百度百科 数据库防火墙 系统,串联部署在数据库服务器之前,解决数据库应用侧和运维侧两方面的问题,是一款基于 数据库 协议分析与控制技术的数据库安全防护系统。DBFirewall基于主动防御机制,实现数据库的访问行为控制、危险…...
dreamweaver网页设计期末考试/电脑系统优化软件
刚换了一台新电脑,可是收藏夹都在之前的电脑上,是不是再一个一个找到网站收藏?答:当然不是!!!你遇到的问题我们优秀的浏览器开发工程师门早就已经想到啦,效率君给你提供两种解决方…...
软件上传网站/东莞seo代理
最近数码圈新机发布的少,但操作系统却打的火热。这边鸿蒙OS2.0刚刚公测,那边Android 12系统就正式登场了。谷歌正式发布Android 12(1)开放的系统风格与操作界面这么多年以来,国产手机用的基本上都是安卓系统,虽然它们在此基础上设…...
建设网站需要钱吗/seo渠道是什么意思
文档就绪函数这些是通常在jQuery中使用的不同类型的Document Ready函数 (又名jQuery DOM Ready)。 许多开发人员似乎在不知道为什么的情况下使用它们。 因此,我将尝试解释为什么您可能会选择一个版本而不是另一个版本。 可以将文档就绪功能看…...