手撕AVL树

什么是AVL树

AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是logn。
它是基于二叉搜索树的以及一系列旋转来实现的。

二叉搜索树

二叉搜索树的概念

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

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
  • 它的左右子树也分别为二叉搜索树。
    在这里插入图片描述

二叉搜索树的操作以及实现

搜索

对于二叉搜索树的搜索,我们一般采用的是中序遍历,因为这种方式遍历出来的结果,一定是有序的。
举个例子:

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

它的二叉搜索树是
在这里插入图片描述
下面先用递归来实现一遍:


void InOrder()
		{
			//为什么要封装这一层是因为我们调用的时候可以不用传入参数,且_root是私有变量,
			//在类外我们也不好获取,就算获取到了,也会感觉写法不够优雅;
			//_root设置为私有的话,又会破坏其封装性
			_InOrder(_root);
			cout << endl;
			return;
		}

void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	//先遍历左边,在回到自己,再去右边
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

当然,也可以通过非递归来实现:

void InOrderNR()const
{
	Node* cur = _root;
	std::stack<Node*> st;
	while (cur != nullptr || !st.empty())
	{
		//左子树入栈,走到右子树且左右为空的时候出栈
		while (cur)
		{
			st.push(cur);
			//先访问左子树
			cur = cur->_left;
		}
		//访问根
		cout << st.top()->_key << " ";
		//访问右子树
		cur = st.top()->_right;
		st.pop();
	}
	cout << endl;
}

再顺便写上前序遍历、后序遍历的非递归版本:

//前序遍历非递归
void PreOrderNR()const
{
	Node* cur = _root;
	std::stack<Node*> st;
	while (cur != nullptr || !st.empty())
	{
		//将cur这课树的左路节点全部入栈,在入栈的同时访问cur节点
		//访问栈顶元素的右子树,走到右子树且左右为空的时候出栈
		while (cur)
		{
			st.push(cur);
			//先访问根
			std::cout << cur->_key << " ";
			//再访问左子树
			cur = cur->_left;
		}
		//开始访问右子树
		cur = st.top()->_right;
		st.pop();
	}
	cout << endl;
}
//后序遍历非递归
void PostOrderNR()const
{
	Node* cur = _root;
	Node* prev = nullptr;
	//后序遍历需要记录上一次访问的节点
	//因为要确定返回路径
	std::stack<Node*> st;
	while (cur != nullptr || !st.empty())
	{
		//左子树入栈,访问栈顶元素的右子树
		//判断右子树是否访问
		//如果前一个节点是右子树,代表从右子树回来的
		while (cur)
		{
			st.push(cur);
			//访问左子树
			cur = cur->_left;
		}
		//开始访问栈顶元素的右子树
		//1、如果右子树为空,或者被访问过了,那就直接访问根
		if (st.top()->_right == nullptr || prev == st.top()->_right)
		{
			prev = st.top();
			std::cout << st.top()->_key << " ";
			st.pop();
		}
		else
		{
			//2、如果右子树不为空,也没有被访问过,那么就开始访问右子树
			cur = st.top()->_right;
		}
	}
	std::cout << std::endl;
}

需要注意的是:如果栈顶元素的右子树为空树(st.top()->_right ==nullptr) 或者 说栈顶元素的右子树我们已经访问过了(prev ==st.top()->_right),那么说明栈顶元素的右子树,已经不值得我们再去访问了,我们直接访问栈顶元素就好了,然后弹栈。
反之说明栈顶元素的右子树还没有被访问过,我们就需要先去访问栈顶元素的右子树(cur=st.top()->_right);

插入

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针。
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点。
在这里插入图片描述
递归版:

bool insertreeR(const K& key)
{
	return _insertreeR(_root, key);
}

bool _insertreeR(Node*& root, const K& key)
{
	//找到要插入的位置
	if (root == nullptr)
	{
		Node* newNode = new Node(key);
		root = newNode;
		return true;
	}
	//插入失败
	if (root->_key == key)
		return false;
		//插在cur的左子树
	if (root->_key > key)
		return _insertreeR(root->_left, key);
	else//插在cur的右子树
		return _insertreeR(root->_right, key);
}

非递归版本:

bool insertree(const K& key)
{
	//如果是首次插入,那么直接更新_root
	if (_root == nullptr)
	{
		_root= new Node(key);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	//记录一下cur的父节点
	while (cur)
	{
		//找到要插入的节点,比cur->_key小,插在cur的左子树,反之右子树
		//要插入的节点的左右子树一定是空
		parent = cur;
		if (cur->_key > key)
			cur = cur->_left;
		else if (cur->_key < key)树
			cur = cur->_right;
		else //二叉搜索树不允许插入重复元素,插入失败 
		return false;
	}
	//找到了插入位置后,判断是左还是右
	Node* NewNode = new Node(key);
	//判断一下该插入parent的左孩子,还是右孩子
	if (key < parent->_key)
	{
		//插入parent的左子树
		parent->_left = NewNode;
	}
	else
	{
		//插入parent的右子树
		parent->_right = NewNode;
	}
	return true;
}

删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点->直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点->直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题->替换法删除

举个例子:
在这里插入图片描述

当我们删除3号节点的时候,就去右边找到最小的(左边的最大)节点,即4号节点,然后替换到3号节点的位置。
当然如果四号节点还有子树,那四号节点一定属于并,c两种情况,因为他是最小的,不会有左子树,然后把4号节点的右子树按情况c处理。
思路:
1.先找到cur这个节点的右子树,记作CurRight。
2.在CurRight这课树中,找到这课树中的最小节点及其父亲节点,分别记作:RightMIn、PRightMin,然后在保存一下RightMin这个节点的右子树,暂且记作RightMin_child;
3.交换cur节点与RightMin_child节点的_key值;
4.将RightMin_child这课树托孤给PRightMin这课树;
5.释放RightMin这个节点;

代码如下:

//递归
bool eraseR(const K&key)
{
	return _eraseR(_root,key);
}

bool _eraseR(Node*& root, const K&key)
{
	if (root == nullptr)
		return false;
	if (root->_key == key)
	{
		//a,b,c三种情况
		if (root->_left == nullptr || root->_right == nullptr)
		{
			Node* del = root;
			Node* child = nullptr;
			//左孩子为空,右孩子不为空
			if (root->_left == nullptr)
				child = root->_right;
			//右孩子为空,左孩子不为空
			else
				child = root->_left;
			root = child;
			delete del;
		}
		//情况d
		else
		{
			//找到右子树中最小的节点
			Node* RightMin = root->_right;
			Node* PRightMin = root;
			while (RightMin->_left)
			{
				PRightMin = RightMin;
				RightMin = RightMin->_left;
			}
			//交换节点数据
			std::swap(root->_key, RightMin->_key);
			//将我们要删除掉值转换到右子树中,这样问题就变成了删除右子树中的key
			_eraseR(root->_right,key);
		}
		return true;
	}
	if (root->_key > key)
		return _eraseR(root->_left, key);
	else
		return _eraseR(root->_right,key);
}

当然,也可以用非递归实现:

//非递归
bool erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else//找到要被删除的节点
		{
			if (cur->_left == nullptr || cur->_right == nullptr)
			{
				Node* child = nullptr;
				//右孩子为空,左孩子不为空
				if (cur->_right == nullptr)
					child = cur->_left;
				//左孩子为空,右孩子不为空
				else
					child = cur->_right;
				//cur如果没有父亲节点呢就说明是根节点,child直接成为root
				if (cur == _root)//被删除节点是根节点
					_root = child;
				//被删除节点不是根节点
				else
				{
					//child放在parent的右边
					if (cur->_key > parent->_key)
						parent->_right = child;
					//child放在parent的左边
					else
						parent->_left = child;
				}
				delete cur;//释放cur节点
			}
			else
			{
				//cur左右节点都不为空
				//记录cur的右子树中的最小节点
				Node* RightMin = cur->_right;
				//记录cur的右子树中的最小节点的父亲节点
				Node* PRightMin = cur;
				while (RightMin->_left)
				{
					PRightMin = RightMin;
					RightMin = RightMin->_left;
				}
				//找到了右子树最小的节点后交换cur节点和RightMin的节点的值
				swap(cur->_key, RightMin->_key);
				//记录RightMin的右孩子
				Node* RightMin_child = RightMin->_right;
				//将右子树最小节点的右孩子放在父节点右边
				if (PRightMin == cur)
				//RightMin没有左子树
					PRightMin->_right = RightMin_child;
				else
					PRightMin->_left = RightMin_child;
				delete RightMin;
			}
			return true;
		}
	}
	return false;
}

二叉搜索树的应用

1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。

附上KV代码

// 改造二叉搜索树为KV结构
template<class K, class V>
struct BSTNode
{
	BSTNode(const K& key = K(), const V& value = V())
		: _pLeft(nullptr), _pRight(nullptr), _key(key), _Value(value)
	{}
	BSTNode<T>* _pLeft;
	BSTNode<T>* _pRight;
	K _key;
	V _value
};

template<class K, class V>
class BSTree
{
	typedef BSTNode<K, V> Node;
	typedef Node* PNode;
public:
	BSTree() : _pRoot(nullptr) {}
	PNode Find(const K& key);
	bool Insert(const K& key, const V& value)
		bool Erase(const K& key)
private:
	PNode _pRoot;
	比特就业课
		2.5 二叉搜索树的性能分析
		插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
};
void TestBSTree3()
{
	// 输入单词,查找单词对应的中文翻译
	BSTree<string, string> dict;
	dict.Insert("string", "字符串");
	dict.Insert("tree", "树");
	dict.Insert("left", "左边、剩余");
	dict.Insert("right", "右边");
	dict.Insert("sort", "排序");
	// 插入词库中所有单词
	string str;
	while (cin >> str)
	{
		BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret == nullptr)
		{
			cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
		}
		else
		{
			cout << str << "中文翻译:" << ret->_value << endl;
		}
	}
}
void TestBSTree4()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
   "苹果", "香蕉", "苹果", "香蕉" };
	BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的节点中水果对应的次数++
		//BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.InOrder();
}

二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,这个时候AVL树和红黑树就可以上场了。

AVL树

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

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

在这里插入图片描述
如果一棵二叉搜索树是高度平衡的,它就是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树节点的定义

template<class T>
struct AVLTreeNode
{
 AVLTreeNode(const T& data)
     : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
 , _data(data), _bf(0)
 {}
 AVLTreeNode<T>* _pLeft;   // 该节点的左孩子
 AVLTreeNode<T>* _pRight;  // 该节点的右孩子
 AVLTreeNode<T>* _pParent; // 该节点的双亲
 T _data;
 int _bf;                  // 该节点的平衡因子
};

AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么
AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

我们插入一个新节点过后,我们必须调整新插入节点的祖先节点的平衡因子,对于其他节点的平衡因子,我们不必调节,因为平衡因子=右子树高度-左子树高度,对于这些除了NewNode节点及其祖先节点之外的节点来说,插入节点前后,这些节点的左子树、右子树的高度根本没有发生变化,平衡因子自然也就不会发生变化,那么我们自然也就不用去关心这些节点的平衡因子;
我们只管调整新插入的节点的祖先节点的平衡因子,由于我们在插入之前这课树就已经是一颗AVL树,因此每一个节点的平衡因子范围是[-1,1],那么在插入节点过后,我们的祖先节点的平衡因子可能的范围就变成了:[-2,2]。
双亲的平衡因子为正负2,违反了AVL树的平衡性,就要进行旋转。

验证平衡节点

我们插入的节点,在parent这课树的左边的话,那么parent的左子树的高度++,也就是parent->_bf–(因为平衡因子=右-左,现在左边高度+1,自然平衡因子=右 --(左+1))。
如果,我们插入的节点,在parent这课树的右边的话,那么parent的右子树的高度++,也就是parent->_bf++(因为平衡因子=右-左,现在右边高度+1,自然平衡因子=右 +1–左)。
因此,根据parent的更新过后_bf,就会有三种情况,我们就对这三种情况进行讨论:
1、更新过后parent->_bf 的绝对值等于1;
跟新过后parent->_bf 的绝对值等于1,那么说明未插入节点之前,也就是未更新parent节点的平衡因子时,parent节点的平衡因子,一定是0。

在这里插入图片描述
这时还要继续跟新parent节点的parent节点的平衡因子,为什么,还是根据平衡因子的定义,平衡因子=右子树高度-左子树高度,现在爷爷节点的其中一颗树的高度是肯定不会变的,但parent的平衡因子在更新过后变为了-1或1,这说明parent这棵树的高度发生了变化!因此我们需要继续向上调整爷爷节点,我们需要让parent往上走,向上继续调整平衡因子。

2、 更新过后parent->_bf 的绝对值等于0;
在插入节点过后parent节点的_bf变为了0,那么说明在插入节点之前,parent节点的平衡因子一定是-1或1(证明方法与前面无异),我们在推导一下:在插入节点前后,parent这课树右子树-左子树的高度差由-1或1变成了0,那么说明插入的这个节点一定是插入在parent的左右子树中最矮的一颗,你细品,仔细品!
那么说明,在插入节点前后,parent这课树的高度并没有发生变化,那么说明我们不必再更新爷爷节点及其祖先节点的平衡因子了!此次插入非常成功!很nice!我们可以提前结束本次插入了!
3、 更新过后parent->_bf 的绝对值等于2;
在插入节点过后parent节点的_bf变为了-2或2,那么说明在插入节点之前,parent节点的平衡因子一定是-1或1(证明方法与前面无异),那是不是说明,在插入节点前后,parent这课树右子树-左子树的高度由-1或1变成了-2或2,我们在深入挖掘一下,这个插入的节点是不是一定是插在左右子树中高的一颗子树中的!同时由于此时parent节点的平衡因子已经不满足,我们对于AVL树的要求了,我们必须将parent这课树调整为AVL树,我们所采取的办法就是:旋转。

AVL树的大框架:

bool Insert(const T& data)
{
	// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
	// ...

	// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树
		//   的平衡性

	 /*
	 pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
	 的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
	  1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
	  2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
	  
	 此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
	  1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
	成0,此时满足
	     AVL树的性质,插入成功
	  2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
	新成正负1,此
	     时以pParent为根的树的高度增加,需要继续向上更新
	  3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进
	行旋转处理
	 */
		while (pParent)
		{
			// 更新双亲的平衡因子
			if (pCur == pParent->_pLeft)
				pParent->_bf--;
			else
				pParent->_bf++;
			// 更新后检测双亲的平衡因子
			if (0 == pParent->_bf)
			{
				break;
			}
			else if (1 == pParent->_bf || -1 == pParent->_bf)
			{
				//插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,
				//说明以双亲为根的二叉树
				//的高度增加了一层,因此需要继续向上调整
				pCur = pParent;
				pParent = pCur->_pParent;
			}
			else
			{
				// 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
				// 为根的树进行旋转处理
				//AVL树的旋转
				if (2 == pParent->_bf)
				{
					// ...
				}
				else
				{
					// ...
				}
			}
		}
	return true;
}

AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,
使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

右单旋

新节点插入较高左子树的左侧—左左:右单旋。
在这里插入图片描述

上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左
子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在。
  2. 60可能是根节点,也可能是子树。
    如果是根节点,旋转完成后,要更新根节点。
    如果是子树,可能是某个节点的左子树,也可能是右子树。
左单旋

类似还有左单旋
在这里插入图片描述
新节点插入较高左子树的右侧—左右:先左单旋再右单旋

先左单旋再右单旋

在这里插入图片描述
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再
考虑平衡因子的更新。

先右单旋再左单旋

在这里插入图片描述
总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

  1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
    当pSubR的平衡因子为1时,执行左单旋
    当pSubR的平衡因子为-1时,执行右左双旋
  2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
    当pSubL的平衡因子为-1是,执行右单旋
    当pSubL的平衡因子为1时,执行左右双旋
    旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

话不多说,直接上代码:

bool insert(const std::pair<K, V>& data)
{
	Node* parent = nullptr;
	Node* cur = _root;
	if (_root == nullptr)//空树
	{
		_root = new Node(data);
		return true;
	}
	while (cur)
	{
		//找到要添加节点的位置
		parent = cur;
		if (cur->_data.first == data.first)
			return false;
		else if (cur->_data.first > data.first)
			cur = cur->_left;
		else
			cur = cur->_right;
	}
	cur = new Node(data);
	cur->_parent = parent;
	if (parent->_data.first > cur->_data.first)
		parent->_left = cur;
	else
		parent->_right = cur;
	//插入一个节点过后,必定导致,cur这个节点的祖宗节点的平衡因子改变,
	//我们需要跟新cur的父节点的平衡因子
	while (parent)
	{
		//cur是parent的右孩子,parent右孩子_bf++
		if (cur->_data.first > parent->_data.first)
			parent->_bf++;
		else
			parent->_bf--;
		//检查更新过后的parent的平衡因子,然后做出不同的反应
		if (parent->_bf == 0)
		{
			//更新过后parent的平衡因子为0,那么更新之前parent的平衡因子一定是 ±1 
			//这说明新插入的节点,是插在parent矮的一颗子树上的,
			//parent这棵树的高度不变,无需在更新祖宗节点的平衡因子;
			break;
		}
		else if (abs(parent->_bf) == 1)
		{
			//更新过后parent的平衡因子是 ±1 那么说明更新之前parent的_bf一定是0
			//这说明在此次插入过后,parent这棵树的高度增加了,必须更新祖宗节点的_bf
			//parent、cur直接往上更新
			parent = parent->_parent;
		}
		else if (abs(parent->_bf) == 2)
		{
			//更新过后parent的平衡因子是 ±2 ,那么说明更新之前parent的_bf一定是±1;
			//这说明,此时parent这课树已经失衡了,需要旋转parent这课树
			//不平衡又有四种情况,这四种情况,对应不同的旋转方法
			//要先比较parent->_bf,再比较parent->_left/parent->_right
			//不能交换比较顺序,不然会出现错误
			if (parent->_bf == 2 && parent->_right->_bf == 1)//(RR型)
			{
				//左单旋
				RotateL(parent);
			}
			else if (parent->_bf == -2 && parent->_left->_bf == -1)//(LL型)
			{
				//右单旋
				RotateR(parent);
			}
			else if (parent->_bf == 2 && parent->_right->_bf == -1)//(RL型)
			{
				//先记录一下parent的右节点的左节点的_bf
				Node* SubR = parent->_right;
				Node* SubRL = SubR->_left;//放心这个节点一定存在,可证明
				int bf = SubRL->_bf;
				//1、先右单旋
				RotateR(parent->_right);
				//2、再左单旋
				RotateL(parent);
				//这里我们需要重新更新一下平衡因子
				//单单的左旋、右旋只会将平衡因子置0,这是不正确的,需要我们手动调节
				if (bf == -1)
				{
					SubRL->_bf = 0;
					parent->_bf = 0;
					SubR->_bf = 1;
				}
				else if (bf == 1)
				{
					SubRL->_bf = 0;
					SubR->_bf = 0;
					parent->_bf = -1;
				}
				else
				{
					assert(false);
				}
			}
			else if (parent->_bf == -2 && parent->_left->_bf == 1)//(LR型)
			{
				Node* SubL = parent->_left;
				Node* SubLR = SubL->_right;
				//这个节点一定存在
				int bf = SubLR->_bf;
				//先左单旋
				RotateL(parent->_left);
				//再右单旋
				RotateR(parent);
				//平衡因子调节
				if (bf == -1)
				{
					SubLR->_bf = 0;
					SubL->_bf = 0;
					parent->_bf = 1;
				}
				else if (bf == 1)
				{
					SubLR->_bf = 0;
					SubL->_bf = -1;
					parent->_bf = 0;
				}
				else
				{
					assert(false);
				}
			}
			else
			{
				assert(false);
			}
			//无论哪种旋转,旋转完毕过后,头结点平衡因子都为0了,
			//这课树已经平衡了,不需要在向上调整了
			break;
		}
		else
		{
     	assert(false);
		}
	}
	return true;
}
void RotateR(Node* parent)
{
	Node* SubL = parent->_left;
	Node* SubLR = SubL->_right;
	Node* ppNode = parent->_parent;

	parent->_left = SubLR;
	if (SubLR)//SubLR节点存在
		SubLR->_parent = parent;

	SubL->_right = parent;
	parent->_parent = SubL;
	if (ppNode == nullptr)//parent是根节点
	{
		SubL->_parent = nullptr;
		_root = SubL;
	}
	else
	{
		SubL->_parent = ppNode;
		if (ppNode->_data.first > SubL->_data.first)
			ppNode->_left = SubL;
		else
			ppNode->_right = SubL;
	}
	SubL->_bf = 0;
	parent->_bf = 0;
}
void RotateL(Node* parent)
{
	Node* SubR = parent->_right;//这个节点一定存在,可以证明
	Node* SubRL = parent->_right->_left;//这个节点就不一定存在了
	Node* ppNode = parent->_parent;//提前记录一下parent的父亲
	//开始链接SubRL节点
	parent->_right = SubRL;
	if (SubRL)//只有当这个节点存在时,才需要维护器=其父亲节点
		SubRL->_parent = parent;
	//开始链接parent节点
	SubR->_left = parent;
	parent->_parent = SubR;

	//开始链接SubR节点
	if (ppNode == nullptr)//如果parent就是根,那么需要更新根节点
	{
		SubR->_parent = nullptr;
		_root = SubR;
	}
	else//parent不是根节点
	{
		SubR->_parent = ppNode;
		if (ppNode->_data.first > SubR->_data.first)
			ppNode->_left = SubR;
		else
			ppNode->_right = SubR;
	}
	//更新平衡因子
	SubR->_bf = 0;
	parent->_bf = 0;
}

AVL树的性能

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值