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

目录标题

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

搜索二叉树的缺点

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

什么是AVL树

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

平衡因子的变化规律

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

AVL树的旋转

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

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

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

第三种情况是右子树较高,往右子树的左边的插入节点,那么这里的右子树就是以90为根节点的子树,插入的节点的就位于节点30的两侧,比如说下面的图片:
详解c++---AVL树的原理和实现_第21张图片

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

情况四:左子树较高插入的节点在左子树的右边
情况四的情况就于情况三相反,如果左子树较高还往左子树的右边插入节点的话我们就采用相反的方法进行调整,比如说下面的图片:
详解c++---AVL树的原理和实现_第26张图片
当我们往节点60的左边或者右边插入值时就会导致当前二叉树的不平衡,比如说下面的图片:
详解c++---AVL树的原理和实现_第27张图片
那么这里我们就要做出调整,情况三时先对根节点的右节点实行右旋转,那么这里的情况四与之相反,所以这里是先对根节点的左节点实行左旋转,那么这里的图片就如下:
详解c++---AVL树的原理和实现_第28张图片
情况三的第二步是对对根节点实行左旋转,那么情况四则相反它是对根节点实行右旋转,这里旋转的结果如下:
详解c++---AVL树的原理和实现_第29张图片
那么这就是第四种情况的旋转过程,所以对于左子树的高度高,插入的节点还位于左子树的右边的话我们采用的方法就是先对根节点的左节点实行左旋转,再对根节点实行右旋转,那么以上就是旋转的全部情况虽然这里只描述了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类型的引用用来初始化内部的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);
	}

最后我们就来看看平衡因子该如何调整,一开始树长这样:
详解c++---AVL树的原理和实现_第30张图片
经过两个旋转之后树变成了这样:

详解c++---AVL树的原理和实现_第31张图片
我们说节点是插在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的情况我们可以通过下面的图片来查看平衡因子的改变结果:
详解c++---AVL树的原理和实现_第32张图片
原来的_parent变成了-1,subR和subRL都变成了0,对于第二种情况就是插入的节点在b的下面,那么旋转之后就是下面的这个图片:
详解c++---AVL树的原理和实现_第33张图片
所以_parent和subRL的平衡因子就变成了0,subR的平衡因子就变成了1,对于第三种情况60本身就是插入的节点,那么这种情况高度为h的树都没有节点所以h的值为0,那么旋转之后就变成了下面这样:
详解c++---AVL树的原理和实现_第34张图片

那么这时_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
		{

		}
	}

在调整之前树的形状如下:
详解c++---AVL树的原理和实现_第35张图片
这里也是往60的下面插入节点,当插入的节点为于60的左边时图片变成下面这样:
详解c++---AVL树的原理和实现_第36张图片
旋转之后树的形状变成下面这样:
详解c++---AVL树的原理和实现_第37张图片
所以这种情况parent的平衡因子就为0,subLR的平衡因子就为0,subL的平衡因子就为-1,如果插入的节点在b上那么旋转之后的平衡因子就变成下面这样:
详解c++---AVL树的原理和实现_第38张图片
subL的平衡因子为0,subLR的平衡因子为0,parent的平衡因子为1,如果60本身为插入的节点的话旋转之后的图片就成为下面这样:

详解c++---AVL树的原理和实现_第39张图片

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

运行的结果如下:
详解c++---AVL树的原理和实现_第40张图片
可以看到这里的运行结果跟我们预测是一样的,所以我们的代码实现的可能是正确的。

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

那么这段代码的运行结果如下:
详解c++---AVL树的原理和实现_第41张图片
那么这就说明我们的代码实现的是正确的。

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

代码的运行结果如下可以看到确实是正确的:
详解c++---AVL树的原理和实现_第42张图片
那么这就是本篇文章的全部内容,希望大家能够理解。

你可能感兴趣的:(c++详解,c++,数据结构,开发语言)