【C++修炼之路】19.AVL树

news2024/11/15 15:55:28

在这里插入图片描述
每一个不曾起舞的日子都是对生命的辜负

AVL树

  • 前言:
  • 一.AVL树的概念
  • 二.AVL树的结构
    • 2.1 AVL树节点的定义
    • 2.2 AVL树的结构
    • 2.3 AVL树的插入
    • 2.4 AVL树的验证
    • 2.5 AVL树的删除(了解)
  • 三.AVL树的旋转(重要)
    • 3.1 左单旋
    • 3.2 右单旋
    • 3.3 左右双旋
    • 3.4 右左双旋
  • 四.AVL树完整代码
    • AVLTree.h
    • Test.c
  • 五. AVL树的性能

前言:

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

平衡树有AVL树、红黑树,本篇就来了解一下AVL树。

一.AVL树的概念

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

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

平衡因子 = right - left

image-20230210143537772

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

二.AVL树的结构

2.1 AVL树节点的定义

对于AVL树,相比普通的二叉搜索树,最主要的就是多了一个平衡因子保持AVL高度平衡的结构。而为了能够更加便捷的操作平衡因子,除了左右节点的指针,还要新增一个父亲节点指针,即三叉链的结构,因为左右子树的增加节点就会导致父亲节点平衡因子的变化:

template<class K, class V>
struct AVLTreeNode//三叉链
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	int _bf;    // balance factor (平衡因子)
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

2.2 AVL树的结构

#pragma once
template<class K, class V>
struct AVLTreeNode//三叉链
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	int _bf;    // balance factor (平衡因子)
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

template<class K, class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
    //一系列的成员函数……
    // 1.插入
    // 2.
private:
	Node* _root;
};

2.3 AVL树的插入

对于AVL树的插入来说,本质上还是二叉搜索树,因此大致的插入逻辑还是像普通的搜索树一样,即:比根小向左遍历,比根大向右遍历,如果遇到相同节点,就插入失败,返回false,如果没遇到,遇到空的地方就直接插入。

但与普通二叉搜索树不同的是,我们在插入节点的过程中要时时刻刻注意AVL树的结构,即通过我们新增的成员:平衡因子_bf,而为了便于访问这个平衡因子相比较普通的搜索树也就增加了指向父亲节点的指针,即三叉链的结构。但需要注意的是,如果插入了新节点,这样不仅会导致父亲节点平衡因子的变化,同样也有可能父亲的父亲节点也会跟着变化,下面试着举2个例子:

例1:image-20230210155630757

对于这种情况来说,新增了一个cur节点,恰好能够使parent的平衡因子变成0,我们知道平衡因子=右子树的高度-左子树的高度,我们可以看出,parent的值从-1或者1变成0,整颗树没有任何一个子树的高度发生了变化,因此插入一个节点不影响任何子树的高度,即parent的平衡因子变成0,就不需要继续向上遍历检索上面父亲节点的平衡因子了。

例2:

image-20230210160506145

新增插入节点时,都必须去检索。对于这种情况,向上遍历的平衡因子并且根据变化的结构去修改平衡因子,如果平衡因子变成了不属于[-1, 1]的范围,那就说明这颗子树的结构出现了问题,此时就需要将这颗子树进行旋转,使其结构满足所有平衡因子都属于[-1, 1]的范畴。如何旋转?由于旋转过于复杂,后面会单独展示。

+++

那再缕清一下插入的思路:

第一步:寻找插入点

  • 此步骤与普通的二叉搜索树的规则几乎相同,唯一区别是由于多了一个parent指针,parent初始化为nullptr,因此在搜索的时候parent也需要不断变化。

第二步:更新平衡因子

  • parent的平衡因子会随着插入节点而发生变化,parent一旦变化,那parent的parent也会发生变化,因此需要一个循环使平衡因子顺着自己的parent指针不断遍历,并且判断是否需要更改。

第三步:判断平衡因子的范围

  • 平衡因子如果变成0,说明之前的parent->_bf是1或-1,说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新,可以跳出循环。
  • 平衡因子如果变成1或者-1,说明插入之前parent->_bf = 0,两遍一样高,现在插入后一边更高了,parent所在子树高度变了,需要继续往上更新
  • 平衡因子如果变成 2或者-2,说明之前parent->_bf是1或-1,现在插入后严重不平衡,违反了左右高度差不超过1的规则,就需要就地处理,即旋转这颗子树,旋转需要注意:
    1. 让这颗子树旋转之后的高度差不超过1。
    2. 旋转过程中需要保持仍是搜索树。
    3. 更新调整孩子节点的平衡因子。
    4. 让这颗子树的高度和插入前保持一致。

Insert代码:

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;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//更新平衡因子
		while (parent)
		{
			//新增在右:parent->_bf++
			//新增在左:parent->_bf--
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			//判断平衡因子的范围
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//需要旋转这个子树:代码和情况过多,在三.AVL树的旋转中展示:

			}
			else
			{
				assert(false);
			}
		}

		return true;

	}

看完这部分代码直接看下一个一级标题:AVL树的旋转。看完旋转之后再回来按照顺序依次看。

2.4 AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  • 验证其为二叉搜索树
    • 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  • 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    • 节点的平衡因子是否计算正确

代码如下:

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

    int lh = Height(root->_left);
    int rh = Height(root->_right);

    return lh > rh ? lh + 1 : rh + 1;
}

bool IsBalance()
{
    return IsBalance(_root);
}

bool IsBalance(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(rightHeight - leftHeight) < 2
        && IsBalance(root->_left)
        && IsBalance(root->_right);
}

2.5 AVL树的删除(了解)

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。可以想象成插入的反向思考,具体实现可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。

删除太难了,别学了。

三.AVL树的旋转(重要)

旋转就是降低高度。

在2.3的插入中,我们说到了一旦平衡因子超出了指定的范围就会导致子树左右高度差发生变化,导致结构不再是高度平衡的状态,此时这个子树就需要旋转,旋转到没插入前的高度。对于旋转,有很多种情况,因此我把他单独拿出来作为一个大标题的形式来描述。

先不进行分类,随便举个例子看看:image-20230211172928922

可以看出,旋转后的特征:

  1. 让这颗子树旋转之后的高度差不超过1。
  2. 旋转过程中需要保持仍是搜索树。
  3. 更新调整孩子节点的平衡因子。
  4. 让这颗子树的高度和插入前保持一致。

但实际上,我们的AVL树可能会非常的复杂,因此并不像上面的例子那么简单。

因此,如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转根据不同的插入情况分为四种:左单旋、右单旋、先左单旋再右单旋、先右单旋再左单旋。上面的例子就属于左单旋。

  • 注:插入的节点名字为cur。

3.1 左单旋

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

左旋转

a, b, c都为AVL树,且高度为h.

image-20230211185444803

对于此图,实际上是一个抽象图,即a,b,c的高度都不是一个确切的数字。但从图中我们可以看出,在c插入节点,导致高度变化为h+1,这个时候30的平衡因子就变成了2。那为什么会左旋,从抽象的角度来看,对于高度平衡的AVL树,右边过高,我们就需要考虑不让右面高,从绳子的角度来说,右边过长,那就将中间节点再往右移动,对于这个模型也一样,我们考虑根节点往右移动,即将60作为根节点,此时左子树就会往下,可以看出,这是一个将左子树往下压的过程,通过这种角度的思考,就不难对这种树进行旋转。

既然有了抽象的左旋,从学习的角度同样要将这种AVL树的旋转具象化:

  1. 如果h=0,那情况就正如我们一开始随便举的例子一样,如果是根节点的1平衡因子不符合条件,那就将右孩子通过旋转变成根节点,右孩子的左孩子给到原来根节点的右孩子节点,这样就完成了旋转,同时满足了条件。

  2. 如果h=1,唯一与上面的情况不同的是,平衡因子首先不满足条件的节点可能不会是根节点,因此这种情况,我们只需将这个不满足条件的节点作为需要旋转的子树的根,就和上面的步骤一样。最后这个子树的新根的parent指针再连接回去。需要注意的细节问题是节点的parent指针。

  3. 如果h=2,那么情况就会变的很复杂,因此上述抽象的结构我们提前将c确定形状,在这我们具体实例化一下:image-20230211204551497

    对于红色的a, b来说,都有三种的选择,因此当h=2时插入之前的组合情况就会有3*3=9种的结果,在这种情况之下,在c的任何一个子树下插入都会引发30这个节点的旋转,而c的孩子节点有四个位置可以插入,那一共就是9*4=36种情况。h继续增加只会更多,因此也没必要将这么多种情况都画出来,因为这些情况都属于上面抽象图的衍生,都可以调用左旋转。

+++

左单旋平衡因子的条件:

if (parent->_bf == 2 && cur->_bf == 1)
{
    RotateL(parent);//左单旋
}

知道旋转的方法之后,我们还需要将这种情况的具体步骤给总结出来:image-20230212140621221

左单旋代码:

void RotateL(Node* parent)//左单旋
{
    //1.记录subR, subRL
    Node* subR = parent->_right;
    Node* subRL = subR->_left;


    parent->_right = subRL;
    if (subRL)//subRL不为空则需要连接到parent
    {
        subRL->_parent = parent;
    }

    subR->_left = parent;

    Node* ppNode = parent->_parent;//记录保存
    parent->_parent = subR;

    if (ppNode == nullptr)//说明根节点变化
    {
        _root = subR;
        _root->_parent = nullptr;
    }
    else//如果是局部子树
    {
        //判断ppNode之前是左连接还是右连接
        if (ppNode->_left == parent)
        {
            ppNode->_left = subR;
        }
        else
        {
            ppNode->_right = subR;
        }
        subR->_parent = ppNode;
    }

    //最后更新平衡因子:一定都为0
    parent->_bf = subR->_bf = 0;
}

3.2 右单旋

右旋转

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

image-20230212142206528

右单旋平衡因子的条件:

if (parent->_bf == -2 && cur->_bf == -1)
{
    RotateR(parent);//右单旋
}

和左单旋的思想是一样的,只不过是将赋值的左右反过来。这里就直接上代码了:

void RotateR(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 (ppNode == nullptr)
    {
        _root = subL;
        _root->_parent == nullptr;
    }
    else
    {
        if (ppNode->_left == parent)
        {
            ppNode->_left = subL;
        }
        else
        {
            ppNode->_right == subL;
        }
        subL->_parent = ppNode;
    }
    //最后更新平衡因子
    subL->_bf = parent->_bf = 0;
}

可以看出,左单旋和右单旋是非常类似的,都是类似于这样:image-20230212154039503

那如果不是按照上面的样子插入,即有拐点呢?

image-20230212154353836

这种折线似的结构似乎更加常见,但如果仍用山上面的左旋或者右旋,只会让其左右转一下,并不能减少其任意子树的高度。这样就无法用到上面任何一个单旋了,因此下面还有两种旋转处理的情况。

3.3 左右双旋

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

image-20230212155811385

就如上面的抽象图,我们仍然需要对其有所见解才能进一步分析并写出代码,通过观察发现对具象的三个节点,原本是折线,通过左旋变成直线后,发现就可以通过右旋从而实现AVL树的结构了。当然,这么说还是过于敷衍,下面将h具象化看看例子:

  1. 如果h=0,则h-1的部分为-1,但我们可以同样的认为他是不存在节点的:image-20230212160925372
  2. 如果h=1,和上面的情况几乎相似,同3.1左单旋叙述的一样,只是子树部分旋转,需要注意parent指针,这种情况是最普遍的。
  3. 如果h=2,情况就如左单旋的h=2的情况一样,有非常多的情况出现,因此,这里只需明白抽象的具体树的旋转规则,就可以了。

+++

左右双旋平衡因子的条件:

if (parent->_bf == -2 && cur->_bf == 1)
{
    RotateLR(parent);
}

下面来看看左右双旋的具体步骤:

image-20230212161240046

  1. 对于左右双旋,上面的步骤不难看出,先左旋parent的左孩子,之后再右单旋旋转parent,复用前面的左单旋和右单旋的代码即可。

  2. 但是关键还要修改旋转节点对应的平衡因子,由于左单旋和右单旋改变了原有的平衡因子,因此我们需要在左右单旋之前将需要改变的节点及对应的平衡因子的值给保留起来,保留的目的是需要根据原有的平衡因子的值将旋转后对应的值进行改变。

    • 如果插入后subLR->_bf == -1,即subLR的左子树插入,上图就是这样,旋转后的parent->_bf = 1subL->_bf = 0subLR->_bf = 0

    • 如果插入后subLR->_bf == 1,即subLR的右子树插入,那么旋转后的parent->_bf = 0subL->_bf = -1subLR->_bf = 0image-20230212185707978

    • 如果插入后的subLR->_bf == 0,说明subLR本身就是新增节点,则这三个平衡因子都为0。image-20230212190215652

代码:

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

    RotateL(parent->_left);//左节点左旋
    RotateR(parent);//右旋

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

可以看出,左右双旋的平衡因子的更新才是关键。

3.4 右左双旋

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

image-20230212191103183

正如右单旋按照左单旋的思路,右左双旋就按照左右双旋的思路。

按照不同的情况画图就能准确的判断平衡因子的变化。

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

    RotateR(parent->_right);//右节点右旋
    RotateL(parent);//左旋

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

四.AVL树完整代码

AVLTree.h

#pragma once

template<class K, class V>
struct AVLTreeNode//三叉链
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	int _bf;    // balance factor (平衡因子)

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

template<class K, class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	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;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//更新平衡因子
		while (parent)
		{
			//新增在右:parent->_bf++
			//新增在左:parent->_bf--
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			//判断平衡因子的范围
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//需要旋转这个子树

				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);//左旋
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);//右旋
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					cout << parent->_kv.first << ":" << parent->_bf << ":" << cur->_bf << endl;
					assert(false);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}

		return true;

	}

	void RotateL(Node* parent)//左单旋
	{
		//1.记录subR, subRL
		Node* subR = parent->_right;
		Node* subRL = subR->_left;


		parent->_right = subRL;
		if (subRL)//subRL不为空则需要连接到parent
		{
			subRL->_parent = parent;
		}

		Node* ppNode = parent->_parent;//记录保存
		subR->_left = parent;
		parent->_parent = subR;

		if (ppNode == nullptr)//说明根节点变化
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else//如果是局部子树
		{
			//判断ppNode之前是左连接还是右连接
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}

		//最后更新平衡因子:一定都为0
		parent->_bf = subR->_bf = 0;
	}

	void RotateR(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 (ppNode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
		//最后更新平衡因子
		subL->_bf = parent->_bf = 0;
	}

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

		RotateL(parent->_left);//左节点左旋
		RotateR(parent);//右旋

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

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

		RotateR(parent->_right);//右节点右旋
		RotateL(parent);//左旋

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

	void Inorder()
	{
		_Inorder(_root);
	}
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}

	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(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(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
	}


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

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

	Node* _root = nullptr;
};

void TestAVLTree()
{
	//int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}

	t.Inorder();

}

Test.c

#include<iostream>
#include<map>
#include<string>
#include<assert.h>
using namespace std;
#include"AVLTree.h"

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

image-20230212210808675

五. AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

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

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

相关文章

2023年 ChatGPT 研究报告

第一章 行业概况 ChatGPT是由OpenAI 团队研发创造&#xff0c;OpenAI是由创业家埃隆马斯克、美国创业孵化器Y Combinator总裁阿尔特曼、全球在线支付平台PayPal联合创始人彼得蒂尔等人于2015年在旧金山创立的一家非盈利的AI研究公司&#xff0c;拥有多位硅谷重量级人物的资金支…

最简单得方法解决TCP分包粘包问题

如何用最简单的方法解决TCP传输中的分包粘包问题&#xff1f; 首先需要说明一点&#xff0c;分包粘包等等一系列的问题并不是协议本身存在的问题&#xff0c;而是程序员在写代码的时候&#xff0c;没有搞清楚数据的边界导致的。 看个简单的例子&#xff0c;TCP客户端不断的向服…

SAP 解析固定资产的减值功能

SAP固定资产的减值功能 若固定资产出现减值迹象&#xff0c;也就是固定资产的可收回金额小于账面价值时&#xff0c;就要计提固定资产减值准备。 分录&#xff1a; 借&#xff1a;资产减值损失&#xff08;损益科目&#xff09; 贷&#xff1a;固定资产减值准备&#xff08;资…

骨传导耳机是不是智商税?骨传导耳机真的不伤耳吗?

很多人对骨传导耳机是具有一定的了解&#xff0c;但是对骨传导耳机还是有一定的刻板印象&#xff0c;那么骨传导耳机到底是不是智商税呢&#xff1f;主要还是要从骨传导耳机传声原理上讨论。 骨传导耳机是属于固体传声的一种方式&#xff0c;通过骨骼传递声音&#xff0c;在使用…

一种基于强化学习的自动变道机动方法

文章目录摘要前言相关的工作方法论动作空间奖励函数设计Q学习仿真结果结论摘要 变道是一项至关重要的车辆操作&#xff0c;需要与周围车辆协调。建立在基于规则的模型上的自动换道功能可能在预定义的操作条件下表现良好&#xff0c;但在遇到意外情况时可能容易失败。在我们的研…

谈一谈正向代理和反向代理?

谈一谈正向代理和反向代理&#xff1f;什么是代理服务器&#xff08;Proxy Serve&#xff09;&#xff1f;为什么使用代理服务器&#xff1f;什么是正向代理什么是反向代理正向代理和反向代理的区别正向代理的应用反向代理的应用什么是代理服务器&#xff08;Proxy Serve&#…

android kotlin 协程(四) 协程间的通信

android kotlin 协程(四) 协程间的通信 学完本篇你将会了解到: channelproduceactorselect 先来通过上一篇的简单案例回顾一下挂起于恢复: fun main() {val waitTime measureTimeMillis {runBlocking<Unit> {println("main start") // 1 // …

学会这些Jmeter插件,才能设计出复杂性能测试场景

为什么要使用jmeter线程组插件呢&#xff1f; jmeter自带的线程组插件模拟的压测场景非常有限&#xff0c;当需要模拟复杂压测场景的时候&#xff0c;推荐大家使用jmeter线程组插件。 如何下载jmeter线程组插件呢&#xff1f; 早期版本的jmeter可以针对我们需要的扩展功能&a…

软考案例分析题精选

试题一&#xff1a;阅读下列说明&#xff0c;回答问题1至问题4&#xff0c;将解答填入答题纸的对应栏内。某公司中标了一个软件开发项目&#xff0c;项目经理根据以往的经验估算了开发过程中各项任务需要的工期及预算成本&#xff0c;如下表所示&#xff1a;任务紧前任务工期PV…

大规模 IoT 边缘容器集群管理的几种架构-1-Rancher+K3s

前文回顾 大规模 IoT 边缘容器集群管理的几种架构-0-边缘容器及架构简介 &#x1f4da;️Reference: IoT 边缘计算系列文章 Rancher K3s 简介 Rancher&#xff1a; Kubernetes 统一管理平台&#xff0c; Rancher 是为采用容器的团队提供的一个完整的软件栈。它解决了管理多个…

PCI设备驱动初探(仅仅是内核部分,不是具体设备驱动)

在操作系统中&#xff0c;声卡、网卡之类的设备驱动并不像硬盘、鼠标、键盘等等驱动直接编写就行了。它们是建立在内核PCI驱动基础上的&#xff0c;也就是说这类设备通过PCI总线与系统通信。所以要编写这类的驱动首先要构造一个PCI设备的内核驱动&#xff0c;这样我们才能继续正…

Hive学习——DDLDML语句

目录 一、Hive数据类型 (一)Hive基本数据类型 (二)Hive的基本数据类型转换 (三)Hive集合数据类型 (四)文本文件数据编码 (五)读时模式 (六)Hive数据结构 二、DDL&DML命令 (一)数据库操作 1.创建数据库 2.查看数据库 3.修改数据库 4.删除数据库 5.切换(使用)指…

【LeetCode】No.225. 用队列实现栈 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/implement-stack-using-queues/ 1. 题目介绍&#xff08;225. 用队列实现栈&#xff09; 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0c;并支持普通栈的全部四种操作&#xff08;push、t…

回文子串的数量[寻找回文子串的完整思路过程]

寻找回文子串的完整思路过程前言一、回文串的数量二、动态规划1、完整思考过程2、go总结参考文献前言 回文字符串&#xff0c;就是从左遍历和从右遍历的字符是相同顺序的&#xff0c;转换一下&#xff0c;就是该字符串是对称的。寻找回文子串面临两个直接的问题&#xff0c;1-…

pytorch深度学习案例(二)——航拍街道语义分割

数据集 使用的数据集是kaggle的Semantic segmentation of aerial imagery 其数据的组织形式为 项目结构 utils dataConvert.py dataConvert中主要包含数据的变换过程 函数作用loadColorMap用于加载标签的颜色映射voc_colormap2label获取颜色标签到数值标签的映射关系voc_…

黑马Spring学习笔记(二)——注解开发

目录 一、纯注解开发 1.1 实现步骤 1.2 小结 二、注解开发依赖注入 2.1 自动装配 2.1.1 Autowired——按照类型注入 2.1.2 Qualifier——按照名称注入 2.1.3 Value——简单类型注入 2.2 注解读取properties配置文件——PropertySource 三、注解开发管理第三方B…

【基础算法】差分

&#x1f339;作者:云小逸 &#x1f4dd;个人主页:云小逸的主页 &#x1f4dd;Github:云小逸的Github &#x1f91f;motto:要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前…

Kotlin 33. CompileSdkVersion 和 targetSdkVersion 有什么区别?

CompileSdkVersion 和 targetSdkVersion 有什么区别&#xff1f; 在 build.gradle (Module) 文件中&#xff0c;我们通常会看到 CompileSdkVersion 和 targetSdkVersion 的使用&#xff0c;比如下面是一个完整的 build.gradle (Module) 文件&#xff1a; plugins {id com.and…

JavaScript随手笔记---比较两个数组差异

&#x1f48c; 所属专栏&#xff1a;【JavaScript随手笔记】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#…