说在前面

今天这篇博客,是博主今年以来最最用心的一篇博客。我们也很久没有更新数据结构系列了,几个月前博主用心深入的学习了这颗二叉平衡搜索树,博主被它的查找效率深深吸引。

AVL树出自1962年中的一篇论文《An_algorithm_for_the_organization_of_information》,它解决了普通二叉搜索树退化的问题,这个博主稍后会详细解释。AVL树放到现在,它的查找效率是很少数据结构能够比拟的。

博主为了这篇博客,做了很多准备,试了很多画图软件,就是为了让大家看得明白!希望大家不要吝啬一键三连啊!!

前言

那么这里博主先安利一下一些干货满满的专栏啦!

手撕数据结构https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏https://blog.csdn.net/yu_cblog/category_11464817.html这里是STL源码剖析专栏,这个专栏将会持续更新STL各种容器的模拟实现。

STL源码剖析https://blog.csdn.net/yu_cblog/category_11983210.html?spm=1001.2014.3001.5482


什么是AVL树?

tips:博主在最后会放一份整体代码,供大家参考!

首先,它是一颗二叉搜索树。

什么是二叉搜索树:

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

如图所示:这就是一颗二叉搜索树。

当我们需要在这颗搜索树里面查找节点4的时候,我们从3开始,比3大,往右走,比5小,往左走。这样我们就找到了4这个节点。

一颗二叉搜索树最高的查找复杂度是O(h),h为树的高度,这样其实我们也可以得到较好的查找效率了。但是,搜索树可能会退化。如图所示:

比如第一棵树,我们如果要查找最下面那个节点,我们要找n次。比如第二棵树,我们找最下面节点要找n/2次,假设我们要查10亿个数据,我们要找5亿次,其实还是O(n)级别。在这种情况下,查找就没有优势了。导致这种情况的主要原因就是,高度不平衡!我们设想一颗满二叉树,它就是平衡的,我们查找一个值,即高度次,复杂度是O(logn)。因此我们需要在插入的同时,不断变换这颗树,使它的高度平衡,这样我们的查找性能才能得到质的提升!一颗二叉平衡搜索树,在10亿个数据中查找一个值,最多查找31次,这种优化,是非常大的!

因此我们引出二叉平衡搜索树,常见的二叉平衡搜索树有AVL树和红黑树。

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

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

AVL树的结构定义

AVL树通常使用三叉链进行构造,我们在处理指针的时候要记得_parent也要处理。

template<class K,class V>
struct AVLNode {
public:
AVLNode<K, V>* _left;
AVLNode<K, V>* _right;
AVLNode<K, V>* _parent;
pair<K, V>_kv;
int _bf; //balance factor
public:
AVLNode(const pair<K, V>& kv)
:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0) {}
}; template<class K, class V>
struct AVL {
typedef AVLNode<K, V>Node;
private:
Node* _root = nullptr;
public:
//成员函数
//...
}

AVL树节点的插入(重点)

AVL树的节点插入可以分为三个步骤:

  1. 新节点的插入
  2. 平衡因子的更新
  3. 通过平衡因子确定平衡性是否被打破,若平衡性被打破,进行旋转

AVL树的旋转:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋

AVL树的旋转是本篇博客最最最最最重点的地方,博主将会详细解释这部分!

一、新节点的插入

新节点的插入步骤和普通二叉搜索树的插入步骤一样,找到插入的位置,直接插入即可!

	bool insert(const pair<K, V>& kv) {
if (_root == nullptr) {
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
}
else return false;
}
cur = new Node(kv);
if (parent->_kv.first < kv.first) {
parent->_right = cur;
}
else {
parent->_left = cur;
}
cur->_parent = parent; //先更新平衡因子
//...
//平衡因子更新后,判断是否需要旋转 return true;
}

二、更新平衡因子

当一个新节点被插入之后,该节点的祖先路径上的节点有可能会受到影响,我们要看情况进行更新。如图所示:插入节点后,只会影响祖先路径上节点的平衡因素。

我们只需要利用_parent指针,不断迭代向上就行了。

  • 如果插入在「新节点父亲」的右边,父亲的平衡因子++
  • 如果插入在「新节点父亲」的左边,父亲的平衡因子 - -

tips:博主在最后会放一份整体代码,供大家参考!

如果平衡因子更新后是1/-1,说明子树的高度被改变了,需要继续向上迭代。如果平衡因子更新后是0,说明新节点只是把子树不齐的地方补齐了(这个很好理解,如果大家不明白可以简单画个图),如果更新后平衡因子是2/-2,说明平衡被打破了,需要旋转

        while (parent) {//只有根没有父亲
//最坏可能需要更新到根
if (cur == parent->_right) {
parent->_bf++;
}
else {
parent->_bf--;
} if (parent->_bf == 0) {
//高度不变 -- 停止更新
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) {
//说明parent所在的子树已经不平衡了 -- 需要旋转
}
else {
assert(false);//理论上不能走到这里
}
}

三、旋转

AVL树的旋转:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋

首先我们来看几个例子,大概了解一下旋转是一个什么样的操作,看下旋转式怎么让不平衡的树便平衡的:

这里只展示了旋转中的其中一种情况,左单旋,现在博主将分四种情况给大家详细讲解

左单旋:新节点插入较高右子树的右侧

大家注意,这里的触发条件是,较高的是右子树,而且插入的是在该右子树的右侧

其实就是把30拿下来,60替代它的位置即可

此时我们要注意,这里我们发现平衡因子异常的点,也就是30这个节点,它不一定是整棵树的根,它有可能只是一个子树的根,但是在旋转过程中,我们不需要关心它上面的结构是什么,我们旋转完成之后,重新链接上去就行了.

给重要的节点标上名字,怎么旋转的,我们就一目了然了,因为是三叉链条,我们直接操作指针即可!

我们把思路转化成代码:

首先,右右触发左单旋,即parent->_bf==2&&cur->_bf==1(在上图中cur是parent的右孩子),这个时候触发左单旋.

if (parent->_bf == 2 && cur->_bf == 1) {
//注意这里肯定是bf==1的情况 -- 才是单旋
//parent->_bf==2说明是左单旋
rotate_left(parent);//旋转就动了6个指针 -- O(1)
}
	void rotate_left(Node* parent) {
//当然我们还要注意处理parent指针
//parent和subR不可能为空 -- ,但是subRL可能为空
//1.parent是整颗树根
//2.parent是子树的根
//最后更新一下平衡因子
//只有subR和parent的平衡因子受到了影响
Node* subR = parent->_right;
Node* subRL = subR->_left; parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
Node* ppNode = parent->_parent;//记录一下原先parent的parent
subR->_left = parent;
parent->_parent = subR; if (_root == parent) {
_root = subR;
subR->_parent = nullptr;
}
else {
//如果ppNode==nullpt,是不会进来这里的
if (ppNode->_left == parent) {
ppNode->_left = subR;
}
else {
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
//更新一下平衡因子
subR->_bf = parent->_bf = 0;//这个看图就行了
}

右单旋:新节点插入较高左子树的左侧

大家注意,这里的触发条件是,较高的是左子树,而且插入的是在该左子树的左侧

这里其实就是左单旋的一个镜像,博主把图画给大家,代码相信我们已经可以自己写出来了

由图片我们可以得知,触发条件时parent->_bf==-2&&cur->_bf==-1

代码如下:

else if (parent->_bf == -2 && cur->_bf == -1) {
//右单旋
rotate_right(parent);
}
//右单旋 -- 思路和左单旋是镜像 -- 很简单
void rotate_right(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent) {
_root = subL;
subL->_parent = nullptr;
}
else {
if (ppNode->_left == parent) {
ppNode->_left = subL;
}
else {
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
//更新平衡因子
subL->_bf = parent->_bf = 0;
}

双旋:

看到这里,难道这两种单旋就可以解决所有问题了吗?答案是否定的,我们举个例子:

我们可以看到这种情况,无论怎么单旋,我们都不能使树便平衡.

由此我们引出双旋:

左右双旋:新节点插入较高左子树的右侧:先左旋后右旋

双旋的过程会比单旋的过程复杂一些

其中,左右双旋分别有三种插入的情况,如图所示:

其中情况3:h==0,此时60就是新插入的节点

下面是旋转的过程:

相信上面的图已经把旋转过程解释得非常清晰了,其实就是两次单旋的组合

现在我们要重点讨论旋转后的平衡因子的更新:

我们可以发现最后的平衡因子取决于初始状态下subLR的平衡因子

情况1: subLR->_bf==-1

  • 旋转后parent,subL,subLR的平衡因子分别为1,0,0

情况2: subLR->_bf==1

  • 旋转后parent,subL,subLR的平衡因子分别为0,-1,0

情况3: subLR->_bf==0

  • 旋转后parent,subL,subLR的平衡因子分别为0,0,0

现在我们只需要对照着图片,对照着调整后的平衡因子,就可以很快的写出代码:

首先,通过图片我们可以知道,左右双旋的触发条件是:

parent->_bf == -2 && cur->_bf == 1

else if (parent->_bf == -2 && cur->_bf == 1) {
//左右双旋
rotate_left_right(parent);
}
    void rotate_left_right(Node* parent) {
//要在单旋之前记录一下,因为单旋之后平衡因子会变
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//记录一下subLR的平衡因子
rotate_left(parent->_left);//先最左边进行一个左旋
rotate_right(parent);//再对自己进行一个右旋转
//如何区分三种情况的平衡因子更新呢? subLR->_bf = 0;//一定要画图!三种情况的subLR最终都是0
if (bf == 1) {
//情况1
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1) {
//情况2
parent->_bf = 1;
subL->_bf = 0;
}
else if (bf == 0) {
//情况3
parent->_bf = 0;
subL->_bf = 0;
}
else assert(false);
}

右左双旋:新节点插入较高右子树的左侧:先右旋后左旋

同样,右左双旋也有三种情况.

右左双旋其实就是左右双旋的一个镜像,搞明白了左右双旋,右左双旋直接画一下图,总结一下三种情况的平衡因子,直接写代码就行了.

触发条件:

parent->_bf == 2 && cur->_bf == -1

else if (parent->_bf == 2 && cur->_bf == -1) {
//右左双旋
rotate_right_left(parent);
}
    void rotate_right_left(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
rotate_right(parent->_right);
rotate_left(parent);
subRL->_bf = 0;
if (bf == 1) {
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1) {
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0) {
subR->_bf = 0;
parent->_bf = 0;
}
else assert(false);
}

四、AVL树的检验

写到这里,我们就可以尝试插入一些节点,检查AVL树是否平衡了

当然,我们可以通过调试,打断点去检查这棵树,但是这样很麻烦.

同时,我们是不能通过检查中序遍历是否有序去判断AVL树的合法性的.因为所有的搜索树中序遍历都是有序的.

我们要通过AVL树的性质去检查,检查每颗子树的左右子树高度差是否小于等于1:

当然现在写出这种代码对于我们来说其实很简单了,这里也提供一道里扣题的传送门,其实就是AVL树的检验,大家可以顺便完成它

面试题 04.04. 检查平衡性https://leetcode.cn/problems/check-balance-lcci/这里博主也一起提供中序遍历的代码:

class AVL {
//...
//...
//...
public:
void inorder() {
_inorder(this->_root);
}
bool is_balance() {
return _is_balance(this->_root);
}
private:
void _inorder(Node* root) {
if (root == nullptr) {
return;
}
_inorder(root->_left);
cout << (root->_kv).first << ":" << (root->_kv).second << endl;
_inorder(root->_right);
}
int _height(Node* root) {
if (root == nullptr)return 0;
int leftHT = _height(root->_left);
int rightHT = _height(root->_right);
return max(leftHT, rightHT) + 1;
}
bool _is_balance(Node* root) {
if (root == nullptr)return true;
int leftHT = _height(root->_left);//左子树高度
int rightHT = _height(root->_right);//右子树高度
int diff = rightHT - leftHT;
//把平衡因子也检查一下
if (diff != root->_bf) {
cout << root->_kv.first << "的平衡因子异常" << endl;
return false;
}
return abs(diff) < 2
&& _is_balance(root->_left)//判断一下左子树是否平衡
&& _is_balance(root->_right);//判断一下右子树是否平衡
}
}

五、删除等接口

讲到这里有伙伴可能会问,为什么讲AVL,不讲删除那些接口呢?

因为,校招,公司面试,以后工作中都基本不会考察到AVL树的删除接口,红黑树也是一样,我们只需要掌握插入接口就行了.

AVL树,红黑树我们都是做了解性学习,我们并不需要去手撕它的全部代码,这样时间成本很大,意义不大.我们学习AVL树,我们需要深入的去理解它的结构,学习一个插入接口,我们已经可以很好的做到这一点了.

这里博主大概讲一下删除的步骤:

一开始也是像搜索树一样找到要删除的节点,用替换法删除,删除后也是同样,检查平衡性.如果平衡性被破坏,进行旋转,这个过程其实就大概是插入的反方向操作

六、AVL.h整体代码

#pragma once

#include<map>
#include<set>
#include<algorithm>
#include<assert.h>
#include<time.h>
using namespace std; template<class K,class V>
struct AVLNode {
public:
AVLNode<K, V>* _left;
AVLNode<K, V>* _right;
AVLNode<K, V>* _parent;
pair<K, V>_kv;
int _bf; //balance factor
public:
AVLNode(const pair<K, V>& kv)
:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0) {}
}; //如何更新平衡因子
//如何旋转 template<class K, class V>
struct AVL {
typedef AVLNode<K, V>Node;
private:
Node* _root = nullptr;
private:
//左单旋
void rotate_left(Node* parent) {
//当然我们还要注意处理parent指针
//parent和subR不可能为空 -- ,但是subRL可能为空
//1.parent是整颗树根
//2.parent是子树的根
//最后更新一下平衡因子
//只有subR和parent的平衡因子受到了影响
Node* subR = parent->_right;
Node* subRL = subR->_left; parent->_right = subRL;
if (subRL) {
subRL->_parent = parent;
}
Node* ppNode = parent->_parent;//记录一下原先parent的parent
subR->_left = parent;
parent->_parent = subR; if (_root == parent) {
_root = subR;
subR->_parent = nullptr;
}
else {
//如果ppNode==nullpt,是不会进来这里的
if (ppNode->_left == parent) {
ppNode->_left = subR;
}
else {
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
//更新一下平衡因子
subR->_bf = parent->_bf = 0;//这个看图就行了
}
//右单旋
void rotate_right(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) {
subLR->_parent = parent;
}
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (_root == parent) {
_root = subL;
subL->_parent = nullptr;
}
else {
if (ppNode->_left == parent) {
ppNode->_left = subL;
}
else {
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
//更新平衡因子
subL->_bf = parent->_bf = 0;
}
//左右双旋
void rotate_left_right(Node* parent) {
//要在单旋之前记录一下,因为单旋之后平衡因子会变
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//记录一下subLR的平衡因子
rotate_left(parent->_left);//先最左边进行一个左旋
rotate_right(parent);//再对自己进行一个右旋转
//如何区分三种情况的平衡因子更新呢? subLR->_bf = 0;//一定要画图!三种情况的subLR最终都是0
if (bf == 1) {
//情况1
parent->_bf = 0;
subL->_bf = -1;
}
else if (bf == -1) {
//情况2
parent->_bf = 1;
subL->_bf = 0;
}
else if (bf == 0) {
//情况3
parent->_bf = 0;
subL->_bf = 0;
}
else assert(false);
}
//右左双旋
void rotate_right_left(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
rotate_right(parent->_right);
rotate_left(parent);
subRL->_bf = 0;
if (bf == 1) {
subR->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1) {
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 0) {
subR->_bf = 0;
parent->_bf = 0;
}
else assert(false);
}
public:
//我们先不返回pair,到时候我们封装map的时候在搞
bool insert(const pair<K, V>& kv) {
if (_root == nullptr) {
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
}
else return false;
}
cur = new Node(kv);
if (parent->_kv.first < kv.first) {
parent->_right = cur;
}
else {
parent->_left = cur;
}
cur->_parent = parent;
//控制平衡
//先更新平衡因子
while (parent) {//只有根没有父亲
//最坏可能需要更新到根
if (cur == parent->_right) {
parent->_bf++;
}
else {
parent->_bf--;
} if (parent->_bf == 0) {
//高度不变 -- 停止更新
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//继续更新
parent = parent->_parent;
cur = cur->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) {
//说明parent所在的子树已经不平衡了 -- 需要旋转
//左旋
if (parent->_bf == 2 && cur->_bf == 1) {
//注意这里肯定是bf==1的情况 -- 才是单旋
//parent->_bf==2说明是左单旋
rotate_left(parent);//旋转就动了6个指针 -- O(1)
}
else if (parent->_bf == -2 && cur->_bf == -1) {
rotate_right(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1) {
//左右双旋
rotate_left_right(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1) {
//右左双旋
rotate_right_left(parent);
}
else assert(false);
break;
}
else {
assert(false);//理论上不能走到这里
}
}
return true;
}
public:
void inorder() {
_inorder(this->_root);
}
bool is_balance() {
return _is_balance(this->_root);
}
private:
void _inorder(Node* root) {
if (root == nullptr) {
return;
}
_inorder(root->_left);
cout << (root->_kv).first << ":" << (root->_kv).second << endl;
_inorder(root->_right);
}
int _height(Node* root) {
if (root == nullptr)return 0;
int leftHT = _height(root->_left);
int rightHT = _height(root->_right);
return max(leftHT, rightHT) + 1;
}
bool _is_balance(Node* root) {
if (root == nullptr)return true;
int leftHT = _height(root->_left);//左子树高度
int rightHT = _height(root->_right);//右子树高度
int diff = rightHT - leftHT;
//把平衡因子也检查一下
if (diff != root->_bf) {
cout << root->_kv.first << "的平衡因子异常" << endl;
return false;
}
return abs(diff) < 2
&& _is_balance(root->_left)//判断一下左子树是否平衡
&& _is_balance(root->_right);//判断一下右子树是否平衡
}
}; void test1() {
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
AVL<int, int>t1;
for (auto e : a) {
t1.insert(make_pair(e, e));
}
t1.inorder();
cout << "is_balance():" << t1.is_balance() << endl;
}
void test2(){
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
AVL<int, int>t1;
for (auto e : a) {
t1.insert(make_pair(e, e));
}
t1.inorder();
cout << "is_balance():" << t1.is_balance() << endl;
}
void test3(){
size_t N = 10000;
srand(time(nullptr));
AVL<int, int>t1;
for (size_t i = 0; i < N; ++i) {
int x = rand();
t1.insert(make_pair(x, i));
}
cout << "is_balance():" << t1.is_balance() << endl;
}

七、总结

看到这里,大家应该对AVL树的实现,重点是它的旋转有了比较深入的了解了。这篇博客博主花了很多心思在画图上,也投入了很多时间到画图上。下期给大家带来红黑树的内容。希望大家可以多多支持,一键三连,点赞关注收藏评论后在离开哦!

( 转载时请注明作者和出处。未经许可,请勿用于商业用途 )
更多文章请访问我的主页

@背包https://blog.csdn.net/Yu_Cblog?type=blog

万字手撕AVL树 | 上百行的旋转你真的会了吗?【超用心超详细图文解释 | 一篇学会AVL】的更多相关文章

  1. jQuery Cookie (内附 上百行的中文使用手册,与 所有的注释中文翻译)

    jQuery Cookie (内附 上百行的中文使用手册,与 所有的注释中文翻译) 博主亲自翻译. 大家多多捧场. 更多资源请点击"查看TA 的资源" .全场通通 2积分. htt ...

  2. 【SecureCRT配置】修改默认卷屏行数当做一个操作,屏幕输出有上百行,当需要将屏幕回翻时,这个设置会有很大帮助,默认为500行,可以改为10000行,不用担心找不到了。 选项 => 全局选项 => Default Session => Edit Default Settings => Terminal => Emulation => Scrollback 修改为32000。

    SecureCRT配置屏幕内容输出到log文件 SecureCRT看不到前几分钟操作的内容,或者想把通过vi命令查看的日志输出到log文件(在懒得下载日志文件的情况下),所以接下来就这样操作: 文件保 ...

  3. AVL树的插入操作(旋转)图解

    =================================================================== AVL树的概念       在说AVL树的概念之前,我们需要清楚 ...

  4. 手写AVL 树(上)

    平衡二叉树 左旋,右旋,左右旋,右左旋 具体原理就不说了,网上教程很多.这里只实现了建树的过程,没有实现删除节点的操作. 下一篇会实现删除节点的操作. // // main.cpp // AVL // ...

  5. AVL树(三)之 Java的实现

    概要 前面分别介绍了AVL树"C语言版本"和"C++版本",本章介绍AVL树的Java实现版本,它的算法与C语言和C++版本一样.内容包括:1. AVL树的介绍 ...

  6. AVL树(一)之 图文解析 和 C语言的实现

    概要 本章介绍AVL树.和前面介绍"二叉查找树"的流程一样,本章先对AVL树的理论知识进行简单介绍,然后给出C语言的实现.本篇实现的二叉查找树是C语言版的,后面章节再分别给出C++ ...

  7. AVL树(二)之 C++的实现

    概要 上一章通过C语言实现了AVL树,本章将介绍AVL树的C++版本,算法与C语言版本的一样. 目录 1. AVL树的介绍2. AVL树的C++实现3. AVL树的C++测试程序 转载请注明出处:ht ...

  8. AVL树的实现——c++

    一.概念 AVL树是根据它的发明者G.M. Adelson-Velsky和E.M. Landis命名的.它是最先发明的自平衡二叉查找树,也被称为高度平衡树.相比于"二叉查找树",它 ...

  9. 006-数据结构-树形结构-二叉树、二叉查找树、平衡二叉查找树-AVL树

    一.概述 树其实就是不包含回路的连通无向图.树其实是范畴更广的图的特例. 树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合. 1.1.树的特性: 每个结点有零个或多个子 ...

  10. linux 内核数据结构之 avl树.

    转载: http://blog.csdn.net/programmingring/article/details/37969745 https://zh.wikipedia.org/wiki/AVL% ...

随机推荐

  1. Educational Codeforces Round 99 (Rated for Div. 2) (A ~ F)个人题解

    Educational Codeforces Round 99 (Rated for Div. 2) A. Strange Functions 读懂题即可(或者快速看一下样例解释),直接输出字符串长度 ...

  2. vue 状态管理 四、Action用法

    系列导航 vue 状态管理 一.状态管理概念和基本结构 vue 状态管理 二.状态管理的基本使用 vue 状态管理 三.Mutations和Getters用法 vue 状态管理 四.Action用法 ...

  3. mybatis-plus-QueryWrapper like的用法

    mybatis-plus 中想写like的语句 一.直接用 QueryWrapper 中的 like String deptLevelCodeTemp = "1000010001" ...

  4. vue3调用高德地图,实现地址,经纬度填写

    父组件引用高德地图: 1 <template> 2 <div class="wrapper"> 3 <div class="box" ...

  5. 图解 HTTP 缓存

    HTTP 的缓存机制,可以说这是前端工程师需要掌握的重要知识点之一.本文将针对 HTTP 缓存整体的流程做一个详细的讲解,争取做到大家读完整篇文章后,对缓存有一个整体的了解. HTTP 缓存分为 2 ...

  6. excel如何自动获取本周工作日的日期显示作为标题(周一至周五)

    一.背景: 每周发周报的标题格式为:本周一到本周五的日期内容,如下:但是每隔一周发送的时候需要改一下周报标题里面的日期,比较麻烦目前的需求是通过函数自动化生成,根据当前的日期去定位出本周一的日期以及本 ...

  7. Vue第三篇 Vue组件

    01-组件的全局注册 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  8. 远程复制文件-scp

  9. 海思Hi35xx 通过uboot 读取U盘文件进行固件升级

    前言 基本过程为:uboot 启动后,通过命令将U盘的的文件读取到内存中,再通过uboot 的flash 写入命令将读取到内存中的升级文件写入到flash的固定位置. (一)usb常用命令 uboot ...

  10. [转帖]FIO磁盘性能测试工具

    https://www.cnblogs.com/lyhabc/p/16708771.html 简介 一般我们测试硬盘或者存储的性能的时候,会用Linux系统自带的dd命令,因为是自带命令,简单易使用, ...