AVL 树的旋转

github 地址:https://github.com/duchenlong/Cpp/tree/master/avl_tree/avl_tree

什么是AVL树

对于为什么会有AVL树这个概念,名字来源于他的发明者G. M. Adelson-Velsky和E. M. Landis两位大佬。

AVL树是最先发明的平衡二叉查找树,在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树

平衡二叉树的搜索效率确实也挺高的,但是由于他的根节点是由第一次插入的元素的决定的,一旦根节点确定,那么以后这个节点就是整棵树的根了,是不会变化的,这也就出现了一些意外情况:

  • 当我们按照升序进行插入的时候,整个树就会变成一个只有右子树的结构

在这里插入图片描述

  • 另一种就是我们以降序进行插入的时候,又会出现只有左子树的情况

在这里插入图片描述

对于这样的情况,当我们进行查找的时候,其实总的效率还是O(n)的,但是由于树中的节点都是不一定连续的地址空间,然后用指针在逻辑上连接起来的,所以当时间复杂度为O(n)的时候,可能还不如直接在数组中遍历找数据。

而AVL树的出现,很好的解决了这样的情况。既然树中节点的地址都是不连续的空间,那么就可以通过移动指针变量,来对树的节点进行旋转,从而让整棵树趋于平衡的状态,让查找的效率真正达到O(log n)

AVL树的概念

AVL 树相比于平衡二叉树,除了多了一个旋转的操作,还在成员变量中多了一个平衡因子的概念。

平衡因子

当树的一个节点是根节点的时候,他的平衡因子 = 右子树的最大深度 - 左子树的最大深度
在这里插入图片描述

而一颗AVL树,他一定需要具备以下的几个特征:

  1. 他的左右子树都是AVL树
  2. 左右子树的高度之差的绝对值不超过1 (1,0,-1),也就是每一个根节点的平衡因子都是(1,0,-1)中的一个。

那么,当一颗树是AVL树的时候,就可以保证他是高度平衡的。
如果它有n个结点,其高度可保持在O(log n),搜索时间复杂度也为O(log n)

AVL树节点的定义

在AVL树的节点的结构中,为了调节平衡因子,所有就需要利用孩子节点找到父亲节点,所以说为了方便,就增加了一个父亲节点的指针,用来指向他的父亲节点。

而树中节点所保存的数据类型是一个pair的结构体,他是C++中的一个只有两个元素的结构体模板,可以通过first,second两个指针来访问两个结构体的元素。

template<class Key,class Val>
struct tree_node
{
	typedef tree_node<Key, Val> node;
	typedef pair<Key, Val> data_type;

	node* _parent;	//双亲节点
	node* _left;	//左孩子节点
	node* _right;	//右孩子节点
	data_type _data;//节点的值
	int _bf;		//平衡因子

	tree_node(const data_type& data)
		:_data(data), _bf(0)
	{
		_parent = _left = _right = nullptr;
	}
};

对于整个AVL树的结构,可以这样来

template<class key, class val>
class avl_tree
{
public:
	typedef key key_type;
	typedef val val_type;
	typedef tree_node<key_type,val_type> node;
	avl_tree()
		:root(nullptr)
	{}
private:
	node* root;//根节点
};

AVL数的插入

在这里插入图片描述

首先,我们需要明确一点,就是AVL树在插入之前,他一定是一颗平衡的二叉树。只是在插入这个新的节点之后,才可能会变得不平衡,所以说平衡因子的取值只会是(-2,-1,0,1,2)。所以说,只有出现(-2,2)中的任何一个数,就说明以该节点为根节点的子树不平衡,需要调整

对于前两步而言,程序部分都是和平衡二叉树一样的,只是多了一个更新父亲节点的操作

		//如果树中没有节点
		if (root == nullptr)
		{
			root = new node(data);
			return true;
		}

		node* parent = nullptr;
		node* cur = root;
		
		//找到要插入的位置
		while (cur)
		{
			if (cur->_data.first == data.first) return false;
			parent = cur;
			cur = (cur->_data.first < data.first) ? cur->_right : cur->_left;
		}

		cur = new node(data);
		//插入 cur 到指定位置
		if (parent->_data.first < data.first)
			parent->_right = cur;
		else
			parent->_left = cur;
		cur->_parent = parent;

在这里插入图片描述
接下来就是判断是否需要旋转,也就是插入节点后,这棵树是否平衡?

这个时候,我们维护了两个指针变量,一个是当前新插入的节点(cur),另一个是该节点的父亲节点(parent)。所以说,我们可以循环的取更新平衡因子,直到父亲节点(parent)是空的时候,就说明更新完毕了

		//更新平衡因子
		while (parent)
		{
		}

更新平衡因子 并 判断是否旋转

如何更新平衡因子?

对于插入一个节点,非三种情况:

  1. 新插入的节点就是AVL树 的跟节点,这个时候只需要更新跟节点就可以了
  2. 新插入的节点在右子树中,根节点的平衡因子 + 1
  3. 新插入的节点在左子树中,根节点的平衡因子 - 1
		//新插入的节点在右子树
		if (cur == parent->_right)
		{
			parent->_bf++;
		}
		else if (cur == parent->_left)//新插入的节点在左子树
		{
			parent->_bf--;
		}

在更新平衡因子的时候,我们选择从下向上更新,而不是在找插入节点位置时候同时更新平衡因子。

在这里插入图片描述
我们需要考虑这样一种情况,对于某一个根节点,我们插入新的节点后,并没有使得这个根节点的左右子树的高度发生变化。那么我们是不需要更新根节点(以及根节点再向上的父亲节点)的平衡因子
在这里插入图片描述
所以说,对于更新完毕的平衡因子,我们做以下的处理

  1. 当更新后,父亲节点的平衡因子为0。说明对于父亲节点向上的节点来说,他们的高度没有发生变化,更新完毕了
  2. 父亲节点的平衡因子为 (1,-1)。说明这个父亲节点的高度发生了变化,会引起上面节点的高度发生变化,但是没有失去平衡,所以需要向上迭代
  3. 父亲节点的平衡因子为(-2,2)。说明当前的父亲节点已经不平衡了,我们需要进行旋转操作
	if (parent->_bf == 0)
	{
		break;
	}
	else if (abs(parent->_bf) == 1)
	{
		cur = parent;
		parent = parent->_parent;
	}
	else if (abs(parent->_bf) == 2)
	{
		//此时parent的子树已经不平衡,需要调整
	}

旋转

对于一颗AVL树,当需要进行旋转的时候,他大体上一定是下面这四种类型:

  1. 当父亲节点的平衡因子为-2的时候,说明左子树高度比较高

在这里插入图片描述
第二种情况经过化简,其实就是这个模式
在这里插入图片描述

  1. 当父亲节点的平衡因子为2的时候,说明右子树的高度比较高

在这里插入图片描述
同样的,第三种情况化简后可以是这样
在这里插入图片描述

那么对于第一种和第四种情况,他们都是只有一条单边,那么我们只需要旋转一次就可以了,此为单旋

单旋

对於单旋而言,又根据旋转方向的不同,分为左单旋和右单旋两种

  1. 左单旋,即为向左进行旋转

在这里插入图片描述
程序的实现

	//右,右,左旋
	void Right_Right(node* parent)
	{
		//保留父亲的父亲节点
		node* pParent = parent->_parent;
		//父亲的右节点
		node* pRight = parent->_right;
		//右节点的左节点
		node* pRLeft = pRight->_left;

		//先把 pRLeft 和 parent 节点连接起来
		parent->_right = pRLeft;
		//更新 pRLeft 的父亲节点为 parent
		if (pRLeft) pRLeft->_parent = parent;

		//开始左旋
		pRight->_left = parent;
		parent->_parent = pRight;

		//pRight 此时作为新的父亲节点,需要判断是否是新的根节点
		//并连接 pRight 和 pParent 
		if (pParent == nullptr)
			root = pRight;
		else
			(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
		
		pRight->_parent = pParent;
		pRight->_bf = parent->_bf = 0;
	}
  1. 右单旋,即为向右进行旋转。

在这里插入图片描述
程序实现

	//左,左,右旋
	void Left_Left(node* parent)
	{
		// 保留父亲的父亲节点
		node* pParent = parent->_parent;
		//父亲的左节点
		node* pLeft = parent->_left;
		//左节点的右节点
		node* pLRight = pLeft->_right;

		//先把 pLRight 和 parent 节点连接起来
		parent->_left = pLRight;
		//更新 pLRight 的父亲节点为 parent
		if (pLRight) pLRight->_parent = parent;

		//开始右旋
		pLeft->_right = parent;
		parent->_parent = pLeft;

		//pLeft 此时作为新的父亲节点,需要判断是否是新的根节点
		//并连接 pLeft 和 pParent 
		if (pParent == nullptr)
			root = pLeft;
		else
			(parent == pParent->_left) ? pParent->_left = pLeft : pParent->_right = pLeft;

		pLeft->_parent = pParent;
		pLeft->_bf = parent->_bf = 0;
	}

最后,因为如果只进行单旋,那么旋转后的图形的根节点的平衡因子一定是0,并且原本不平衡的那个节点由于变成了一个叶子节点,所以说平衡因子也为0。
在这里插入图片描述
而最底下的那个节点,因为在单旋的过程中,高度没有发生变化,所以说他的平衡因子不变。

双旋

对于双旋的情况,我们重新用程序实现也是挺麻烦的,肯定还不简洁。所以可以尝试着把双旋变成单旋来试试。

  1. 我们上面所示的第二种情况,当失衡节点的平衡因子是-2

在这里插入图片描述
程序实现

	//左,右,双旋
	void Left_Right(node* parent)
	{
		node* pLeft = parent->_left;
		node* pLRight = pLeft->_right;
		int pLRbf = pLRight->_bf;
		//右,右,单旋
		Right_Right(pLeft);

		//左,左,单旋
		Left_Left(parent);
		
		//更新平衡因子
	}

对于双旋而言,我们需要再次更新平衡因子,因为我们第一次单旋的分支并不一定是一个模板的分支,他可能只是单旋分支的前两部分

在这里插入图片描述
其实这里又有三种平衡因子变化的情况,就是当根节点的左节点的右节点,的平衡因子是 (0,-1,1)这三种情况的时候,对于双旋后的平衡因子都有不同的结果

  1. 当该节点的平衡因子为0时:

在这里插入图片描述
2. 当该点的平衡因子为-1时:

在这里插入图片描述

  1. 当该点的平衡因子为1时

在这里插入图片描述
所以说,在修改平衡因子的时候,可以判断一下这个节点的情况,在进行赋值就可以了

		if (pLRbf == 1)
		{
			parent->_bf = 0;
			pLeft->_bf = -1;
			pLRight->_bf = 0;
		}
		else if (pLRbf == -1)
		{
			parent->_bf = 1;
			pLeft->_bf = 0;
			pLRight->_bf = 0;
		}
		else if (pLRbf == 0)
		{
			parent->_bf = 0;
			pLeft->_bf = 0;
			pLRight->_bf = 0;
		}

这里需要注意的是,我们在修改平衡因子的过程是在旋转完毕后的,那么旋转后这个点的平衡因子是会发生变化的,所以说我们需要在进行旋转之前就保留这个点的平衡因子的值,然后用这个变量在最后去修改。(亲测一下午)

为什么说这个点的取值一定是(0,-1,1)?

因为我们这是一颗AVL树,他在插入一个元素之前一定是一颗平衡的树,我们进行双旋的时候,这个点一定是一个平衡的节点,所以他的取值是(0,-1,1)

  1. 失衡节点的平衡因子是2的时候,也就是第三种情况

在这里插入图片描述
那么,他的旋转过程我们可以再进行一次封装,并且他调节平衡因子的过程和上面是差不多的,也是那三种情况。

void Right_Left(node* parent)
	{
		node* pRight = parent->_right;
		node* pRLeft = pRight->_left;
		int pRLbf = pRLeft->_bf;
		//左,左,单旋
		Left_Left(pRight);

		//右,右,单旋
		Right_Right(parent);

		if (pRLbf == 1)
		{
			parent->_bf = -1;
			pRight->_bf = 0;
			pRLeft->_bf = 0;
		}
		else if (pRLbf == -1)
		{
			parent->_bf = 0;
			pRight->_bf = 1;
			pRLeft->_bf = 0;
		}
		else if (pRLbf == 0)
		{
			parent->_bf = 0;
			pRight->_bf = 0;
			pRLeft->_bf = 0;
		}
	}

AVL树的验证

对于验证,我们只需要对数的结构进行遍历(前中后序都可以),然后依次判断每个节点的左右孩子节点的深度可以了。

另外,和插入操作相同,我们在遍历的时候,需要传入一个参数,也就是当前节点的指针。而这个树的节点是被封装好的私有类型,所以我们需要在类内部调用另一个接口。

public:
	//判断是否是平衡二叉树
	bool IsBF()
	{
		return _IsBF(root);
	}
private:
	//判断是否是平衡
	bool _IsBF(node* root)
	{
		if (root == nullptr) return true;

		int leH = _Height(root->_left);//左子树高度
		int riH = _Height(root->_right);//右子树高度

		if (riH - leH != root->_bf)
		{
			cout << root->_data.first << " 平衡因子异常" << endl;
			return false;
		}
			
		return _IsBF(root->_left) && _IsBF(root->_right);
	}

	//由根节点确定子树高度
	int _Height(node* root)
	{
		if (root == nullptr)
			return 0;
		return 1 + max(_Height(root->_left), _Height(root->_right));
	}

最后在测试的时候,我使用了大量的随机数来进行测试,程序一切正常
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章