详解c++---AVL树的原理和实现

news2024/11/26 3:40:15

目录标题

  • 搜索二叉树的缺点
  • 什么是AVL树
  • 平衡因子的变化规律
  • AVL树的旋转
  • 准备工作
  • insert函数模拟实现
    • 左旋转
    • 右旋转
    • 右左双旋
    • 左右双旋
  • AVL树的打印
  • AVL的查找
  • AVL树的检查

搜索二叉树的缺点

在上一篇文章的学习种我们知道了什么搜索二叉树,它让比根小的节点都在根的左边,让比根大的节点都在根的右边,这样我们在查找数据的时候就可以很快的过滤掉我们不需要的数据,以此来达到logn的查找效率,但是我们之前实现的搜索二叉树有一个非常明显的问题就是如果它的高度十分的不平衡的话时间复杂度会大量上升,比如说插入的数据都是有序的,这就会导致二叉树走向一个极端只有一边存在数据,这样他的时间复杂度就和链表是一样的了,比如说下面的图片:
在这里插入图片描述
如果一个搜索二叉树因为插入的数据是有序的而变成这样的话那么他就和链表没有什么区别了一摸一样,所以这就不符合我们之前的想法,那么为了解决搜索二叉树不平衡的问题就有了AVL树。

什么是AVL树

我们说AVL树存在的目的就是为了解决搜索二叉树不平衡的问题,那么这里的解决方法有很多种,其中有个方法就是添加平衡因子。我们往每个节点里面添加一个整型变量,这个变量的作用就是用来记录左右子树高度差,当左子树的高度加1时,该子树的根节点的整型变量就是减一,当右子树的高度加1时,该子树的根节点的整型变量就会加1,那么我们就把这个整型变量称为平衡因子,那么我们要想整个搜索二叉树是平衡的就得保证搜索二叉树的每个节点的平衡因子都在[ -1,+1]这个范围之内,比如说下面的图片:
在这里插入图片描述
由于根节点没有左子树,右边有子树且高度为1,所以根节点的平衡因子的值就为1,因为节点4既没有左子树也没有右子树所以该节点的平衡因子的值就为0,当我们往节点4的左边插入一个节点时:
在这里插入图片描述
由于4号节点左子树的高度变为1右子树的高度为0,所以4号节点的平衡因子就变成了-1,因为节点2是刚插入的节点所以它的平衡因子为0,又因为节点4的加入导致了节点3的右子树的高度增加了1,所以3的平衡因子也得加一,所以上面图片就变成了这样:
在这里插入图片描述
当我们往3的左边插入一个节点的时
在这里插入图片描述
左子树的高度就变为了1右子树的高度不变,所以这时3号节点的平衡因子就变为了1,因为1号节点是刚插入的节点它的平衡因子为0,那么上面的图片就变成了这样:
在这里插入图片描述
那么看到这里想必大家应该已经明白了平衡因子是什么已经什么是AVL树,那么接下来我们就来更细致的聊聊平衡因子的变化规律是什么样的。

平衡因子的变化规律

通过上面的讲解想必大家应该能够理解平衡因子的概念,那这里我们就来看看平衡因子的变化规律,首先来看看这个图片:
在这里插入图片描述
这棵树当前的平衡因子如上图所示,当我们往12号的节点的左边或者右边插入一个节点时,肯定会改变12号节点的平衡因子,让其由0变成-1或者1,但是不管它是变成了1还是-1,12号节点的改变势必会影响到它父节点8的平衡因子的改变,因为当一个节点由0变成1或者-1时说明这个子树的高度变了,当子树的高度发生了改变则父节点的平衡因子一定会发生改变,因为12在8的右边,当右边子树的高度变高(这里指的是高度加一)会将父节点的平衡因子加1,所以当前图片就会变成这样:
在这里插入图片描述
也就是说如果插入的节点使得父节点由0变成-1或者1的话,那么这个改变得继续影响上面的父节点 ,直到有个父节点的平衡因子变成了0或者一直改变到根节点为止,那如果我们再往12的左边插入一个节点呢?根据上面的规则我们知道12的平衡因子会由1变成了0,那这个改变会影响到12的父节点8吗?答案是不会的,在8看来往12的左边插入一个节点对它的右子树高度来说是没有影响的,所以8的平衡因子不用进行修改,那么这里的图片就变成了这样:
在这里插入图片描述
也就是说当父节点的平衡因子由0变成了1或者-1的话,这个改变会一直网上影响知道有个父节点的平衡因子变成0或者改变到根节点为止,当父节点的平衡因子由1或者-1变成0的话,它是不会继续网上影响的将自己的改变了就结束了,那么这就是平衡因子的改变规律。

AVL树的旋转

我们上面说AVL树有一个要求就是每个节点的平衡因子都得在-1和1之间,那我们在插入节点的时候肯定会出现一些情况使得某些节点的平衡因子变成了-2或者2甚至更高,那对于这些情况我们就可以通过AVL树的旋转来解决,首先解决的原则就是防范于未然,当我们发现某个节点的平衡因子为2或者-2的话就对这个节点进行旋转使得这个子树的高度差小于2,那么这里我们就将需要旋转的情况分为4种。
第一种:新节点插入在较高右子树的右侧
我们首先来看看下面的图片:
在这里插入图片描述
长方形h表示这是一个高度为h的子树,因为这里会存在许多的情况所以就用一个长方形来进行代替,因为节点60的左边和右边都有一个高度为h的子树,所以60的平衡因子就为0,因为根节点30的左边是一个高度为h子树,右边是一个节点加上一个高度为h的子树所以右边的高度为h+1所以30的平衡因子就为1,当我们往60的右边插入节点使得原本高度为h的子树变成h+1时这里的平衡因子就会变成这样:
在这里插入图片描述
这时节点60的平衡因子为1节点30的平衡因子为2,我们说当一个节点的平衡因子变成2或者-2时就得对其进行旋转,那这里是如何旋转才能使得根结点的平衡因子变成正常值呢?那么这里的步骤如下:首先将平衡因子为2(节点30)的右节点(节点60)的左子树放到平衡因子为2(节点30)的右边,因为这个子树在30的右边,所以这么放肯定是符合规则,那么这里的图片就变成了这样:
在这里插入图片描述

然后再将节点30放到节点60的左边,因为节点60原来就位于节点30的右边所以它比这些节点的值都要大,所以放到60的左边不会违反任何规则,那么这里的图片就变成了这样:
在这里插入图片描述
最后我们来改变一下节点的平衡因子,节点30的平衡因子原来为2,但是现在左右都变成了高度为h的子树所以它的平衡因子变成了0,节点60的平衡因子原来为1,但是现在左边的高度为1个节点加上高度为h的子树,右边为高度h+1的子树,两边的高度相等,所以它的平衡因子就变成0,那么这里的图片最终变成了这样:
在这里插入图片描述
那么我们就把这种调整方法称为左单旋因为这个看起来就像一个向左旋转的过程,所以对于右子树高还向右子树的右边插入节点的情况我们就使用左单旋的方法来进行调整。
情况二:左子树高插入的节点在左子树的左侧
那么这里的图片就如下:
在这里插入图片描述

这种情况就是左子树较高然后插入的节点就在左子树的左边,这里的图片如下 :
在这里插入图片描述
这个时候根节点的平衡因子就变成了-2.,所以我们得对其进行调整,上一个情况是右子树的高度高我们还往右子树的右边插入节点,我们采取的调整方法是向左旋转,那这里是左子树的高度高我们往左子树的左边插入节点,所以采取的方法就是向右旋转,向右旋转和向左旋转的方法就是相反的,先把平衡因子为-2(节点60)的左节点(节点30)的右子树放到平衡因子为-2(节点60)的左边,比如说下面的图片:
在这里插入图片描述
再把节点60放到30的右边比如说下面的图片:
在这里插入图片描述
最后修改一下每个节点的平衡因子,节点30的平衡因子变成了0,节点0的平衡因子变成了0:
在这里插入图片描述
那么这就是第二种情况的旋转方法,由于旋转过程十分的像向右边旋转所以我们把这种旋转过程称为右单旋,所以如果左子树较高插入的节点还位于左子树的左边的话我们就采用右旋转的方法来进行调整。
情况三:右子树较高插入的节点在右子树的左边
比如说下面的图片:
在这里插入图片描述

第三种情况是右子树较高,往右子树的左边的插入节点,那么这里的右子树就是以90为根节点的子树,插入的节点的就位于节点30的两侧,比如说下面的图片:
在这里插入图片描述

这时根节点30的平衡因子就变成了2,这时我们就需要对其进行调整,调整的方法为先以90为根节点进行右旋转,那么这里旋转的过程就如下:先把60的右边放到90的左边
在这里插入图片描述
再把90放到60的右边
在这里插入图片描述
最后再把60放到30的右边然后更改一下平衡因子就可以得到下面的图片,那么这样我们就完成了第一步:
在这里插入图片描述
可以看到这里出现了两个平衡因子为2的节点,这里大家不要慌我们再以30为根节点对其进行左旋转,那么这里就是将60的左边放到30的右边然后将30放到60的左边最后更新一下平衡因子,上面的图片就变成了下面这个样子:
在这里插入图片描述
可以看到此时的搜索二叉树就变平衡了,那么对于右子树较高还往根节点的右节点的左子树插入节点的情况我们采取的方法就是先对根节点的右节点进行右旋转,然后再对旋转之后的根节点进行左旋转

情况四:左子树较高插入的节点在左子树的右边
情况四的情况就于情况三相反,如果左子树较高还往左子树的右边插入节点的话我们就采用相反的方法进行调整,比如说下面的图片:
在这里插入图片描述
当我们往节点60的左边或者右边插入值时就会导致当前二叉树的不平衡,比如说下面的图片:
在这里插入图片描述
那么这里我们就要做出调整,情况三时先对根节点的右节点实行右旋转,那么这里的情况四与之相反,所以这里是先对根节点的左节点实行左旋转,那么这里的图片就如下:
在这里插入图片描述
情况三的第二步是对对根节点实行左旋转,那么情况四则相反它是对根节点实行右旋转,这里旋转的结果如下:
在这里插入图片描述
那么这就是第四种情况的旋转过程,所以对于左子树的高度高,插入的节点还位于左子树的右边的话我们采用的方法就是先对根节点的左节点实行左旋转,再对根节点实行右旋转,那么以上就是旋转的全部情况虽然这里只描述了3种情况,但是根据子树h的不同这里相当于概括了所有情况,因为这里的调整只会对平衡因子为2或者-2的节点进行调整,对于大于2或者小于-2的节点是不会进行调整的因为我们防范于未然这种情况的节点根本就不会出现,对于-1到1的节点这里也没必要进行调整因为他们符合规则,那么这就是平衡因子控制二叉树左右平衡的原理,那么接下来我们就要用代码来实现上面的功能。

准备工作

在实现AVL树之前我们首先来完成一下AVL树的准备工作,首先每个节点都有一个左指针和右指针用于指向左子树和右子树,其次就是每个节点都得有一个整型变量来记录当前节点的平衡因子,因为每插入一个节点都可能会改变父节点往上的祖父节点的平衡因子,所以这里还得添加一个指针用于指向自己的父节点,这里我们想实现一个kv结构的AVL树,所以我们在结构体里面添加一个pair,并且为了应对各种各样的数据,我们这里添加一个模板,模板中有两个参数一个表示用于查找的K一个表示存储数据的V,那么这里的代码就如下:

template<class K,class V>
struct AVLTreeNode
{
	int bf;//平衡因子
	pair<K, V> kv;//用于记录节点的数据
	AVLTreeNode<K, V>* parent;//指向父节点
	AVLTreeNode<K, V>* left;//指向左子树
	AVLTreeNode<K, V>* right;//指向右子树
};

然后我们就可以添加一个构造函数用于初始化这些变量,平衡因子我们将其初始化为0,三个指针我们可以将其初始化为nullptr,最后构造函数需要一个pair<K,V>类型的引用用来初始化内部的kv成员数据,那这里完整的代码就如下:

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K, V>& _kv)
		:bf(0)
		,parent(nullptr)
		,left(nullptr)
		,right(nullptr)
		,kv(_kv)
	{}
	int bf;//平衡因子
	pair<K, V> kv;//用于记录节点的数据
	AVLTreeNode<K, V>* parent;//指向父节点
	AVLTreeNode<K, V>* left;//指向左子树
	AVLTreeNode<K, V>* right;//指向右子树
};

描述节点的结构体实现之后就可以来准备一下AVLTree类的基础工作,首先AVLTree它要处理各种各样的数据所以它得是一个模板类,其次我们在函数里面需要对节点进行多次操作,所以在类里面首先对节点进行重命名,然后类里面得有一个指向节点的指针用于我们遍历查找整个树,最后我们再写一下构造函数将节点初始化为空指针就行,那么这里的代码就如下 :

template<typename K,typename V>
class AVLTree
{
public:
	typedef AVLTreeNode<K, V> Node;
	AVLTree()
		:root(nullptr)
	{}
private:
	Node* root;
};

insert函数模拟实现

首先insert函数寻找节点插入位置的方法跟之前的搜索二叉树是一样的,首先判断一下当前的根节点是否为空如果为空的话就直接创建节点并让类中的root指针指向新创建出来的节点并返回true即可,那么这里的代码就如下:

	bool insert(const pair<K, V>& _kv)
	{
		if (root == nullptr)
		{
			root = new Node(_kv);
			return true;
		}
	}

如果当前的root不为空的话我们就得先找到要插入的位置,那么这里我们就可以创建一个Node类型的指针cur用于找到插入的位置当cur为空的话就说明找到了,然后再创建一个Node类型的指针parent用于指向cur的父节点用于插入节点的链接,那这里找位置的方法就是根据pair中的第一个元素进行比较,如果插入的比当前的要大的话就往右边走,如果插入的要比当前的小的话就往左边走,如果相等的话就直接返回false表明当前的元素已经存在了不能再插入这个元素了,当cur为空的时候就直接退出循环表明位置已经找到了,那么这一步的代码就如下:

	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 (_kv.first > _parent->kv.first)
	{
		_parent->right = cur;
		cur->parent = _parent;
	}
	else
	{
		_parent->left = cur;
		cur->parent = _parent;
	}

节点链接完之后我们就要更新一下父亲的平衡因子,因为父节点的更新可能会影响到祖父节点,祖父节点的更新可能会继续往上影响,所以我们这里创建while循环来一直更新平衡因子,因为平衡因子的更新一直更新到根节点就停止了,而根节点的父节点是nullptr,所以我们这里可以用parent来作为结束停止的条件,那么在循环里面我们就根据当前的cur是parent的左还是右来对parent的平衡因子做出更改,如果cur是parent的左则parent的平衡因子减一,如果cur是parent的右则parent的平衡因子加一,如果更改后parent的平衡因子是0的话我们就不需要再做出修改,直接break跳出循环,如果parent的平衡因子变成了1或者-1的话我们就得继续往上更改平衡因子,那么这里就是让cur指向parent让parent指向parent的parent,parent的值变成了2或者-2的话我们就得进行旋转调整,那么这里的代码就如下:

	while (_parent)
	{
		if (_parent->right == cur)//新增在右父节点的平衡因子加一
		{
			_parent->bf++;
		}
		else//新增在左父节点的平衡因子减一
		{
			_parent->bf--;
		}
		if (_parent->bf == 0)//父节点的平衡因子为0就跳出循环
		{
			break;
		}
		else if (_parent->bf == 1 || _parent->bf == -1)
		{
			//如果父节点的平衡因子为1或者-1得继续往上调整
			cur = _parent;
			_parent = _parent->parent;
		}
		else if (_parent->bf == 2 || _parent->bf == -2)
		{
			//当父节点的平衡因子变成2或者-2就说明当前需要旋转调整了
		}
	}

平衡因子更新完之后我们就得来考虑一下如何旋转树来调整二叉树的平衡,根据上面的讲解我们知道这里的旋转分为4种情况,第一种右子树高且插入的节点位于右子树的左边,那么这种情况对应的就是parent的平衡因子为2,并且cur的平衡因子为1,这种情况采用的方法为左单旋,第二种情况就是左子树高且插入的节点位于左子树的左边,那么这种情况对应的解释parent的平衡因子为-2且cur节点为-1,这种情况采用的解决方法为右单旋,第三种情况是右子树的高度较高并且插入的节点为右子树的左边,那么这种情况对应的解释parent的平衡因子为2cur的平衡因子为-1,这种情况我们采用的方法为选右单旋再左单旋,第四种情况是左子树的高度较高插入的节点位于左子树的右边,那么这种情况就是parent的平衡因子为-2cur的平衡因子为1,这种情况采用的方法就是先左单旋再右单旋,那么这里为了看起来更加简洁我们就把这四种方法分别写到4个函数里面分别为:RototalL(左单旋)RototalR(右单旋) RototalLR(右左单旋) RototalRL(左右单旋)这四个函数都只需要传递一个_parent父类指针便可以实现它的功能那么这四种情况对应的代码就是下面这样:

		else if (_parent->bf == 2 || _parent->bf == -2)
		{
			//当父节点的平衡因子变成2或者-2就说明当前需要旋转调整了
			if (_parent->bf == 2 && cur->bf == 1)
			{
				RototalL(_parent);
			}
			else if (_parent->bf == -2 && cur->bf == -1)
			{
				RototalR(_parent);
			}
			else if (_parent->bf == 2 &&cur->bf == -1)
			{
				RototalLR(_parent);
			}
			else if (_parent->bf == -2 && cur->bf == 1)
			{
				RototalRL(_parent);
			}
			else
			{
				assert(false);
			}
		}

左旋转

那么接下来我们就要分别实现这四个函数,首先就是左旋转,这个函数的参数是Node类型的指针,然后这个函数没有返回值所以返回值为void:

	void RototalL(Node* _parent)
	{

	}

然后左旋转的规律就是先将自己右节点的左孩子放到自己的右边,然后再再将自己放到左孩子的左边,所以这里我们就先创建一个节点child用于表示孩子,然后就可以轻松的写出下面的代码:

	void RototalL(Node* _parent)
	{
		Node* child = _parent->right;
		_parent->right=child->left;
		child->left = _parent;
	}

但是这么写错的因为我们忽略掉了每个节点内部的parent指针,所以把child的左子树放到_parent的右边的时候,我们还得改变左子树的parent指针的指向,当我们把_parent放到child的左边的时候还得改变_parent的parent的指向,并且当_parent可能为其他子树的节点,所以我们还得改变祖父的left或者right的指向,当_parent为根节点的时候还得改变成员变量root的指向,并且右节点的左孩子还可能为空所以在修改的时候还得添加if语句做为判断,所以上面的代码实现的就存在很多的问题,那么我们推倒重来修改之后的代码就如下:

	void RototalL(Node* _parent)
	{
		Node* subR = _parent->right;//右孩子的节点
		Node* subRL = subR->left;//右孩子的左节点
		Node* ppNode = _parent->parent;//祖父节点
		//把subRL放到_parent的右
		_parent->right = subRL;
		if (subRL)
		{
			//如果subRL不为空则修改父节点的指向
			subRL->parent = _parent;

		}
		//把_parent放到subR的左
		subR->left = _parent;
		//修改_parent的parent的指向
		_parent->parent = subR;
		if (ppNode)//如果祖父不为空,则要改变祖父的指向
		{
			if (ppNode->right == _parent)
			{
				ppNode->right = subR;
				subR->parent = ppNode;
			}
			else//如果_parent是祖父的左
			{
				ppNode->left = subR;
				subR->parent = ppNode;
			}
		}
		else//祖父为空节点说明当前调整的是根节点
		{
			root = subR;
			_parent->parent = nullptr;
		}
	}

关系修改到这里就结束了,接下来我们就只用干一件事情就是调整当前节点的平衡因子,根据我们上面得到图片可以知道,经过左旋转之后_parent和subR的平衡因子都变成了0,所以完整的代码就如下:

	void RototalL(Node* _parent)
	{
		Node* subR = _parent->right;//右孩子的节点
		Node* subRL = subR->left;//右孩子的左节点
		Node* ppNode = _parent->parent;//祖父节点
		//把subRL放到_parent的右
		_parent->right = subRL;
		if (subRL)
		{
			//如果subRL不为空则修改父节点的指向
			sunRL->parent = _parent;
		}
		//把_parent放到subR的左
		subR->left = _parent;
		//修改_parent的parent的指向
		_parent->parent = subR;
		if (ppNode)//如果祖父不为空,则要改变祖父的指向
		{
			if (ppNode->right == _parent)
			{
				ppNode->right = chlid;
				child->parent = ppNode;
			}
			else//如果_parent是祖父的左
			{
				ppNode->left = child;
				child->parent = ppNode;
			}
		}
		else//祖父为空节点说明当前调整的是根节点
		{
			root = _parent;
			_parent->parent = nullptr;
		}
		_parent->bf = subR->bf == 0;
	}

右旋转

有了左旋转作为基础,右旋转也是差不多的原理。首先创建三个节点subL表示左孩子的节点,subLR表示左孩子的右子树节点,ppNode表示祖父节点,那么第一步就是将subLR放到_parent的左边,如果subLR不为空的话就改变它parent指针的指向,那么这里的代码就如下:

	void RototalR(Node* _parent)
	{
		Node* subL = _parent->left;
		Node* subLR = subL->right;
		Node* ppNode = _parent->parent;
		_parent->left = subLR;
		if (subLR)
		{
			subLR->parent = _parent;
		}
	}

然后我们就让_parent变成subL的右子树,并且修改_parent的parent指针的指向,最后我们就得修改ppNode的指向,如果_parent是ppNode的右我们就修改他的右指针的指向,如果_parent是ppNode的左我们就修改ppNode的左指向,如果ppNode为空的话就说明这里是根节点,那么我们这里就得改变成员变量root的指向,并把subL的parent指向nullptr,最后修改一下两个节点的平衡因子那么这里的代码就下:

	void RototalR(Node* _parent)
	{
		Node* subL = _parent->left;
		Node* subLR = subL->right;
		Node* ppNode = _parent->parent;
		_parent->left = subLR;
		if (subLR)
		{
			subLR->parent = _parent;
		}
		subL->right = _parent;
		_parent->parent = subL;
		if (ppNode != nullptr)
		{
			if (ppNode->right == _parent)
			{
				ppNode->right = subL;
				subL->parent = ppNode;
			}
			else
			{
				ppNode->left = subL;
				subL->parent = ppNode;
			}
		}
		else
		{
			root = subL;
			subL->parent=nullptr;
		}
		subL->bf = _parent->bf = 0;
	}

右左双旋

首先这个函数也是只用一个Node类型的指针作为参数,并且没有返回的参数:

	void RototalRL(Node* _parent)
	{

	}

对于这个函数我们就可以用上面的两个函数来帮我们实现,首先创建一个subL用于指向_parent的右子树,然后调用Rototal函数对该节点实行右旋转,然后调用Lototal函数对_parent节点实行左旋转便可以达到调整平衡的目的,那这里的代码就如下:

	void RototalRL(Node* _parent)
	{
		Node* subR = _parent->right;
		RototalR(subR);
		RototalL(_parent);
	}

最后我们就来看看平衡因子该如何调整,一开始树长这样:
在这里插入图片描述
经过两个旋转之后树变成了这样:

在这里插入图片描述
我们说节点是插在60的下面,那么这里就会存在三种情况一个是插入在60的左边一个是插入在60的右边最后一个就是当h的值就为1所以60本省就是插入的节点,那么这三种情况对应的修改之后的平衡因子分布是不同的,所以我们一个一个的讨论,首先我们要解决的一个问题就是我们怎么知道插入的节点位于哪边?那这个问题就很好解决,首先插入的节点肯定位于parent的右子树的左子树那边,那这里我们就可以创建一个subRL指针,如果这个节点的平衡因子为1说明插入的地方为右边,如果平衡因子为-1说明插入节点的地方为左边,如果平衡因子为0的话说明这个节点本身就是插入的,那这里的代码就如下:

	void RototalRL(Node* _parent)
	{
		Node* subR = _parent->right;
		Node* subRL = subR->left;
		int _bf = subRL->bf;
		RototalR(subR);
		RototalL(_parent);
		if (_bf == 1)
		{

		}
		else if (_bf == -1)
		{

		}
		else
		{

		}
	}

对于subRL等于1的情况我们可以通过下面的图片来查看平衡因子的改变结果:
在这里插入图片描述
原来的_parent变成了-1,subR和subRL都变成了0,对于第二种情况就是插入的节点在b的下面,那么旋转之后就是下面的这个图片:
在这里插入图片描述
所以_parent和subRL的平衡因子就变成了0,subR的平衡因子就变成了1,对于第三种情况60本身就是插入的节点,那么这种情况高度为h的树都没有节点所以h的值为0,那么旋转之后就变成了下面这样:
在这里插入图片描述

那么这时_parent的平衡因子就为0 subL的平衡因子就为0,subRL的平衡因子就变成了0,所以整理一下我们的代码就为下面这个样子:

	void RototalRL(Node* _parent)
	{
		Node* subR = _parent->right;
		Node* subRL = subR->left;
		int _bf = subRL->bf;
		RototalR(subR);
		RototalL(_parent);
		if (_bf == 1)
		{
			_parent->bf = -1;
			subR->bf = subRL->bf = 0;
		}
		else if (_bf == -1)
		{
			subR->bf = 1;
			subRL->bf = _parent->bf = 0;
		}
		else
		{
			_parent->bf = 0;
			subR->bf = 0;
			subRL->bf = 0;
		}
	}

那么这就是右左双旋的代码,希望大家可以理解。

左右双旋

有了前面的基础这里的左右双选我们就可以很好的模拟实现,首先创建两个Node类型的指针变量,一个subL指向_parent的左节点,一个subLR指向subL的右,那么这个函数的第一步就是先对subLR进行左旋转,然后再对subL进行右旋转,旋转完之后我们就要对平衡因子进行修改,同样的道理这里也是三种情况subLR的平衡因子为0,1,-1这三种情况,那么这里的代码就如下:

	void RototalLR(Node* _parent)
	{
		Node* subL = _parent->left;
		Node* subLR = subL->right;
		int _bf = subLR->bf;
		RototalL(subL);
		RototalR(_parent);
		if (_bf == 1)
		{

		}
		else if (_bf == -1)
		{

		}
		else
		{

		}
	}

在调整之前树的形状如下:
在这里插入图片描述
这里也是往60的下面插入节点,当插入的节点为于60的左边时图片变成下面这样:
在这里插入图片描述
旋转之后树的形状变成下面这样:
在这里插入图片描述
所以这种情况parent的平衡因子就为0,subLR的平衡因子就为0,subL的平衡因子就为-1,如果插入的节点在b上那么旋转之后的平衡因子就变成下面这样:
在这里插入图片描述
subL的平衡因子为0,subLR的平衡因子为0,parent的平衡因子为1,如果60本身为插入的节点的话旋转之后的图片就成为下面这样:

在这里插入图片描述

subL的平衡因子为0,parent的平衡因子为0,subLR的平衡因子为0,所以将上面的情况总结一下就可以写出我们下面的代码:

void RototalLR(Node* _parent)
	{
		Node* subL = _parent->left;
		Node* subLR = subL->right;
		int _bf = subLR->bf;
		RototalL(subL);
		RototalR(_parent);
		if (_bf == 1)
		{
			subL->bf = -1;
			subLR->bf = 0;
			_parent->bf =0 ;
		}
		else if (_bf == -1)
		{
			subL->bf =0;
			subLR->bf = 0;
			_parent->bf = 1;
		}
		else
		{
			subL->bf = 0;
			subLR->bf = 0;
			_parent->bf = 0;
		}
	}

那么写到这里我们的insert函数的实现就算完成了,那insert函数完整的代码就如下:

	bool insert(const pair<K, V>& _kv)
	{
		if (root == nullptr)
		{
			root = new Node(_kv);
			return true;
		}
		Node* cur = root;
		Node* _parent = nullptr;
		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 (_kv.first > _parent->kv.first)
		{
			_parent->right = cur;
			cur->parent = _parent;
		}
		else
		{
			_parent->left = cur;
			cur->parent = _parent;
		}
		while (_parent)
		{
			if (_parent->right == cur)//新增在右父节点的平衡因子加一
			{
				_parent->bf++;
			}
			else//新增在左父节点的平衡因子减一
			{
				_parent->bf--;
			}
			if (_parent->bf == 0)//父节点的平衡因子为0就跳出循环
			{
				break;
			}
			else if (_parent->bf == 1 || _parent->bf == -1)
			{
				//如果父节点的平衡因子为1或者-1得继续往上调整
				cur = _parent;
				_parent = _parent->parent;
			}
			else if (_parent->bf == 2 || _parent->bf == -2)
			{
				//当父节点的平衡因子变成2或者-2就说明当前需要旋转调整了
				if (_parent->bf == 2 && cur->bf == 1)
				{
					RototalL(_parent);
				}
				else if (_parent->bf == -2 && cur->bf == -1)
				{
					RototalR(_parent);
				}
				else if (_parent->bf == 2 && cur->bf == -1)
				{
					RototalRL(_parent);
				}
				else if (_parent->bf == -2 && cur->bf == 1)
				{
					RototalLR(_parent);
				}
				else
				{
					assert(false);
				}
				break;
			}
		}
		return true;
	}
	void Inorder()
	{
		_Inorder(root);
	}
private:
	void _Inorder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Inorder(root->left);
		cout << root->kv.first << ":" << root->kv.second << endl;
		_Inorder(root->right);
	}
	void RototalL(Node* _parent)
	{
		Node* subR = _parent->right;//右孩子的节点
		Node* subRL = subR->left;//右孩子的左节点
		Node* ppNode = _parent->parent;//祖父节点
		//把subRL放到_parent的右
		_parent->right = subRL;
		if (subRL)
		{
			//如果subRL不为空则修改父节点的指向
			subRL->parent = _parent;

		}
		//把_parent放到subR的左
		subR->left = _parent;
		//修改_parent的parent的指向
		_parent->parent = subR;
		if (ppNode)//如果祖父不为空,则要改变祖父的指向
		{
			if (ppNode->right == _parent)
			{
				ppNode->right = subR;
				subR->parent = ppNode;
			}
			else//如果_parent是祖父的左
			{
				ppNode->left = subR;
				subR->parent = ppNode;
			}
		}
		else//祖父为空节点说明当前调整的是根节点
		{
			root = subR;
			subR->parent = nullptr;
		}
		_parent->bf = subR->bf = 0;
	}
	void RototalR(Node* _parent)
	{
		Node* subL = _parent->left;
		Node* subLR = subL->right;
		Node* ppNode = _parent->parent;
		_parent->left = subLR;
		if (subLR)
		{
			subLR->parent = _parent;
		}
		subL->right = _parent;
		_parent->parent = subL;
		if (ppNode != nullptr)
		{
			if (ppNode->right == _parent)
			{
				ppNode->right = subL;
				subL->parent = ppNode;
			}
			else
			{
				ppNode->left = subL;
				subL->parent = ppNode;
			}
		}
		else
		{
			root = subL;
			subL->parent = nullptr;
		}
		subL->bf = _parent->bf = 0;
	}
	void RototalRL(Node* _parent)
	{
		Node* subR = _parent->right;
		Node* subRL = subR->left;
		int _bf=subRL->bf;
		RototalR(subR);
		RototalL(_parent);
		if (_bf->bf == 1)
		{
			_parent->bf = -1;
			subR->bf = subRL->bf = 0;
		}
		else if (_bf->bf == -1)
		{
			subR->bf = 1;
			subRL->bf = _parent->bf = 0;
		}
		else
		{
			_parent->bf = 0;
			subR->bf = 0;
			subRL->bf = 0;
		}
	}
	void RototalLR(Node* _parent)
	{
		Node* subL = _parent->left;
		Node* subLR = subL->right;
		int _bf=subLR->bf;
		RototalL(subL);
		RototalR(_parent);
		if (_bf == 1)
		{
			subL->bf = 0;
			subLR->bf = -1;
			_parent->bf =0 ;
		}
		else if (_bf == -1)
		{
			subL->bf = 0;
			subLR->bf = 0;
			_parent->bf = 1;
		}
		else
		{
			subL->bf = 0;
			subLR->bf = 0;
			_parent->bf = 0;
		}
	}

AVL树的打印

插入函数完成之后我们就可以来实现一下AVL树的打印函数,由于AVL树具有搜索二叉树的特性,所以我们这里采用中序遍历的方式对这个树进行打印,因为用户拿不到指向根节点的指针,所以我们这里就通过调用内部函数的方式来实现这里的打印函数,比如说下面的代码:

public:
	void Inorder()
	{
		_Inorder(root);
	}
private:
	void _Inorder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Inorder(root->left);
		cout << root->kv.first << ":" << root->kv.second << endl;
		_Inorder(root->right);
	}

那么这里我们就可以用下面的代码来进行一下测试:

int main()
{
	AVLTree<int, int> Tree;
	srand(time(0));
	const size_t N = 10000;
	for (int i = 0; i < N; i++)
	{
		size_t x = rand();
		Tree.insert(make_pair(x, x));
	}
	Tree.Inorder();
	return 0;
}

运行的结果如下:
在这里插入图片描述
可以看到这里的运行结果跟我们预测是一样的,所以我们的代码实现的可能是正确的。

AVL的查找

有了前面的基础那这里的查找想必就很好的实现了,在查找的时候就可以根据每个节点的pair的第一个元素进行比较,如果要找的元素较大的话就往右边走,如果要找的元素较小的话就往左边走,如果相等的话就返回当前节点的地址,如果没有找到的话就返回空指针,那么这里的代码就如下:

	Node* Find(const K& key)
	{
		Node* cur = root;
		while (cur)
		{
			if (key>cur->kv.first)
			{
				cur = cur->right;
			}
			else if (key<cur->kv.first)
			{
				cur = cur->left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

我们可以用下面的代码来进行一下测试:

int main()
{
	AVLTree<int, int> Tree;
	Tree.insert(make_pair(8, 8));
	Tree.insert(make_pair(14, 14));
	Tree.insert(make_pair(13, 13));
	Tree.insert(make_pair(15, 15));
	Tree.insert(make_pair(7, 7));
	if (Tree.Find(8)){cout << "存在" << endl;}
	else{cout << "不存在" << endl;}
	if (Tree.Find(10)){cout << "存在" << endl;}
	else{cout << "不存在" << endl;}
	Tree.Inorder();
	return 0;
}

那么这段代码的运行结果如下:
在这里插入图片描述
那么这就说明我们的代码实现的是正确的。

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;
	}

然后我们就可以实现一个检查函数这个函数就可以通过递归的方式一个一个检查每个节点的右左高度差是否等于平衡因子,如果不相等的话就返回false,如果每个节点的平衡因子都是正常的话就返回true,那么这里的代码就如下:

piblic:
	bool check()
	{
		return _check(root);
	}
private:
	bool _check(const Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
		int lh = Height(root->left);
		int rh = Height(root->right);
		if (rh - lh != root->bf)
		{
			cout << root->kv.first << "平衡因子错误" << endl;
			return false;
		}
		return abs(lh - rh) < 2 && _check(root->right) && _check(root->left);
	}

那么这里我们就可以用下面的代码来做测试:

int main()
{
	AVLTree<int, int> Tree;
	srand(time(0));
	const size_t N = 1000;
	for (int i = 0; i < N; i++)
	{
		size_t x = rand();
		Tree.insert(make_pair(x, x));
	}
	if (Tree.check())
	{
		Tree.Inorder();
	}
	else
	{
		cout << "出现错误" << endl;
	}
	return 0;
}

代码的运行结果如下可以看到确实是正确的:
在这里插入图片描述
那么这就是本篇文章的全部内容,希望大家能够理解。

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

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

相关文章

数字孪生水网可视化平台提高企业应急联动指挥水平

随着城市化进程的不断加快&#xff0c;给水管网的建设和维护变得越来越重要。传统的给水管网监测和管理方式通常只能通过文字和图片来进行描述和展示&#xff0c;难以直观地了解管网的结构和运行情况。而3D可视化大屏展示技术的出现&#xff0c;为给水管网的监测管控提供了以下…

可完成城市内涝一维二维耦合模拟的慧天[HTWATER]软件讲解

第一部分&#xff1a;CAD、GIS在水力建模过程中的应用 1.1复杂城市排水管网系统快速建模&#xff1a;通过标准化的步骤&#xff0c;利用CAD数据、GIS数据建立SWMM模型。在建模的不同阶段发挥不同软件的优势&#xff0c;实现高效的数据处理、准确的参数赋值、模型的快速建立。在…

【NPS 】1. 服务器端搭建

一、环境&#xff1a; 二、安装docker docker-compose 三、安装 nps 服务端 1. 在 /mnt/docker/nps 下创建 docker-compose.yaml 文件 2. 安装 nps 四、登录 1. ip:8800登录&#xff0c;默认 账号密码 admin / 123 2. 修改密码 一、环境&#xff1a; 系统 &#xff1a;C…

2023年6月中国数据库排行榜:OceanBase 连续七月踞榜首,华为阿里谋定快动占先机

群雄逐鹿&#xff0c;酣战墨坛。 2023年6月的 墨天轮中国数据库流行度排行 火热出炉&#xff0c;本月共有273个数据库参与排名。本月排行榜前十变动不大&#xff0c;可以用一句话概括为&#xff1a;OTO 组合连续两月开局&#xff0c;传统厂商GBase南大通用乘势而上&#xff0c;…

将深度学习与传统计算机视觉进行比较

原创 | 文 BFT机器人 深度学习 (DL) 在数字图像处理中用于解决难题&#xff08;例如&#xff0c;图像着色、分类、分割和检测&#xff09;。卷积神经网络 (CNN) 等深度学习方法通过使用大数据和丰富的计算资源提高预测性能&#xff0c;突破了可能性的界限。 深度学习是机器学习…

学习一年Java的程序员的C++学习记录(指针引用绕晕记)

文章目录 一 C入门二 变量和数据类型三 运算符四 流程控制五 复合数据类型六 函数七 函数高阶八 面向对象 一 C入门 标准输出流中 cout 是一个ostream对象&#xff0c;<< 和 >>是C中经过重载的运算符&#xff0c;配合cout和cin使用时表示流运算符。C中是如何重载运…

苹果头显Vision Pro深度解读1 下一个十年计算机行业的标杆

1 苹果Vision Pro是下一个十年计算机行业的标杆。 今天主要给大家说下今年WWDC发布的vision pro&#xff0c;以及后面很多期给大家做一些vision pro背后大量的技术的分享。我这次是从头到位把苹果官网上所有的文档&#xff0c;视频&#xff0c;全部学习了一遍。好几十个视频文…

Stable Diffusion web UI之X/Y/Z plot使用

一、安装环境配置 PASS CFG Scale配置的越高&#xff0c;SD生成的图会更贴用户提供的prompt来进行生成&#xff0c;AI的自由度会下降&#xff0c;生成人物的时候特别需要注意&#xff0c;对于手脚脸部&#xff0c;过高的值更容易造成过拟合还有画面崩坏。 二、X/Y/Z plot 使用…

民间最大的社区,倒闭了

看到一则不起眼的消息&#xff1a;天涯社区已经无法打开。 时代抛弃你的时候&#xff0c;都不说一声再见&#xff0c;现实就是这样残酷。 记得我读大学的时候&#xff0c;天涯社区是国内互联网行业中最具影响力的论坛之一&#xff0c;号称 " 全球华人网上家园 “。 当年&a…

Maven配置仓库

目录 Maven仓库介绍 Maven配置本地仓库 Maven配置中央仓库 Maven配置远程仓库 配置jdk Maven仓库介绍 当使用 Maven 构建项目时&#xff0c;有三种仓库起着重要作用&#xff1a; 本地仓库&#xff1a;就像你自己的书库。当你使用 Maven 下载依赖项时&#xff0c;它们会被…

某企业《IT治理管理办法》共十个章节,五十九条管理要求,适用于集团级企业、大型企业、中型企业

第二条&#xff1a;IT治理是规范公司各部门在公司IT系统建设、IT 应用中的责任与权力分配&#xff0c;主要包括原则、IT架构、基础设施、IT应用、IT投入等方面&#xff0c;明确责任人以及决策权力人&#xff0c;以提升工作效率&#xff0c;提高决策的科学性和合理性。 总则的第…

HarmonyOS元服务端云一体化开发快速入门(下)

四、关联云开发资源 为工程关联云开发所需的资源&#xff0c;即在DevEco Studio中选择您的华为开发者帐号加入的开发者团队&#xff0c;将该团队在AGC的同包名应用关联到当前工程。具体操作如下&#xff1a; 1.&#xff08;可选&#xff09;如您尚未登录DevEco Studio&#xf…

深入理解ASEMI代理光宝LTV-152光耦的特性与应用

编辑-Z 光耦LTV-152是一种广泛应用于电子设备中的光电器件&#xff0c;它的主要功能是实现电路之间的隔离和信号传输。本文将深入探讨光耦LTV-152的特性和应用&#xff0c;帮助读者更好地理解和使用这种重要的电子元件。 一、光耦LTV-152的特性 1. 高隔离电压&#xff1a;光耦…

日撸java三百行day63-65

文章目录 说明1. Day63-65 AdaBoosting算法1 AdaBoostin举例1.1数据样本1.2 举例过程 2. 理论知识3. 总结 2. 代码理解1. WeightedInstances类2. 选择基分类器并进行训练&#xff08;树桩分类器&#xff09;3. 计算误差率和误差系数&#xff08;树桩分类器&#xff09;4. 计算精…

谈谈Java高并发网站的设计思路

目前Java都在流行一个说词&#xff1a;高并发。 反正不管是不是&#xff0c;反正就是高并发。 谈高并发&#xff0c;我们需要知道几个名词&#xff1a; -响应时间(Response Time&#xff0c;RT)-吞吐量(Throughput)-每秒查询率QPS(Query Per Second)-每秒事务处理量TPS(Transa…

用单元测试读懂 vue3 中的 defineComponen

目录 前言&#xff1a; I. 测试用例 II. 一些基础类型定义 III. 官网文档中的 props V. 开发实践 VI. 全文总结 前言&#xff1a; Vue3是一种流行的JavaScript框架&#xff0c;它在组件开发方面提供了更多的表现力和灵活性&#xff0c;通过使用defineComponent高阶函数&…

腾讯安全联动头部企业携手验证“数字安全免疫力”模型框架

6月13日&#xff0c;腾讯安全联合IDC发布“数字安全免疫力”模型框架及《加强企业数字安全免疫力&#xff0c;助力数字时代下的韧性发展》白皮书&#xff0c;提出用免疫的思维应对新时期下安全建设与企业发展难以协同的挑战&#xff0c;围绕“数据”和“数字业务”建立三层由内…

视觉SLAM十四讲——ch8实践(视觉里程计2)

视觉SLAM十四讲----ch8的实践操作及避坑 0.实践前小知识介绍1. 实践操作前的准备工作2. 实践过程2.1 LK光流2.2 直接法 3. 遇到的问题及解决办法3.1 编译时遇到的问题 0.实践前小知识介绍 里程计的历史渊源是什么&#xff1f; 里程计是一种用来测量车辆或机器人行驶距离的装置…

Uniapp uni-app学习与快速上手

个人开源uni-app开源项目地址&#xff1a;准备中 在线展示项目地址&#xff1a;准备中 什么是uni-app uni&#xff0c;读 you ni&#xff0c;是统一的意思。 Dcloud即数字天堂(北京)网络技术有限公司是W3C成员及HTML5中国产业联盟发起单位&#xff0c;致力于推进HTML5发展构…

MP : Human Motion 人体运动的MLP方法

Back to MLP: A Simple Baseline for Human Motion Prediction conda install -c conda-forge easydict 简介 papercodehttps://arxiv.org/abs/2207.01567v2https://github.com/dulucas/siMLPe Back to MLP是一个仅使用MLP的新baseline,效果SOTA。本文解决了人类运动预测的问…