我们前面已经说过了搜索二叉树,和map,set的用法,我们通过学习可以知道, 搜索二叉树的实现还是有一些问题的,比如果所有值都在一条线上,拿遍历的效率就低;也就是搜索二叉树不好控制,这里引进AVL树,在二叉搜索树之后,优化而成的;

在之后会在学习红黑树,那才是map set的真正的底层;

AVL的简介

  1. AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,他们在1962年的论⽂《An algorithm for the organization of information》中发表了它。
  2. AVL树是最先发明的是平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AV树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制⾼度差去控制平衡。
  3. AVL树实现这⾥我们引入一个平衡因⼦(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡。
  4. AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 ,那么增删查改的效率也可以控制在O(logN) (虽然但是,也是接近,但是计算机的计算速度是很快的相差的拿几百几千也是可以忽略为 logN),相⽐⼆叉搜索树有了本质的提升。
  5. 思考⼀下为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。⽐如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法作为⾼度差是0。

 AVL树的实现

 AVL树的结构

template<class K, class V>
	struct AVLTreeNode {

		pair<K, V> _kv;
		AVLTreeNode<K, V>* _left = nullptr;
		AVLTreeNode<K, V>* _right = nullptr;
		// 需要parent指针,后续更新平衡因⼦可以看到
		AVLTreeNode<K, V>* _parent = nullptr;
		int _bf = 0;     // balance factor

			AVLTreeNode(pair<K, V> kv)
			:_left()
			,_right()
			,_parent()
			,_bf()
			,_kv(kv)
		{}
	};
template<class K, class V>
	class AVLTree {
		typedef AVLTreeNode<K, V> Node;
……………………
	public:
        Node*_root;
}

AVL树的插⼊(平衡因子的方法)


大致过程:

  1. 平衡因子 = 右子树⾼度 - 左子树⾼度
  2. 只有子树⾼度变化才会影响当前结点平衡因子。
  3. 插⼊结点,会增加⾼度,所以新增结点在parent的右子树,parent的平衡因⼦++,新增结点parent的左子树,parent平衡因⼦--
  4. parent所在子树的⾼度是否变化决定了是否会继续往上更新

更新停止条件:
 

  1. 更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0 或者 1->0,说明更新前parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插⼊后parent所在的⼦树⾼度不变,不会影响parent的⽗亲结点的平衡因⼦,更新结束。
  2. 更新后parent的平衡因⼦等于1 或 -1,更新前更新中parent的平衡因⼦变化为0->1 或者 0->-1,说明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所在的⼦树符合平衡要求,但是⾼度增加了1,会影响arent的⽗亲结点的平衡因⼦,所以要继续向上更新。
  3. 更新后parent的平衡因⼦等于2 或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在⾼的那边,parent所在的⼦树⾼的那边更⾼了,破坏了平衡,parent所在的⼦树不符合平衡要求,需要旋转处理。
  4. 旋转的⽬标有两个:1、把parent⼦树旋转平衡。2、降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不需要继续往上更新,插⼊结束。(旋转后,parent 平衡因子是0)

过程演示

更新到10结点,平衡因⼦为2,10所在的⼦树已经不平衡,需要旋转处理
 

 更新到中间结点,3为根的⼦树⾼度不变,不会影响上⼀层,更新结束:

最坏更新到根停⽌:


插⼊结点及更新平衡因⼦的代码实现 

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.second > kv.second)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_kv.second < kv.second)
				{
					parent = cur;
					cur = cur->_right;
				}
				else {
					return false;
				}
			}

			cur = new Node(kv);
			if (parent->_kv.second > cur->_kv.second)
			{
				parent->_left = cur;

			}
			else if (parent->_kv.second < cur->_kv.second)
			{
				parent->_right = cur;
			}
			else {
				return false;
			}

			// 链接父亲
			cur->_parent = parent;

			//控制平衡
			//改变平衡因子

			while (parent)
			{
				if (parent->_right == cur)
				{
					parent->_bf++;
				}
				else if (parent->_left == cur)
				{
					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)
					{
						RotateR(parent);
					}
					else if (parent->_bf == 2 && cur->_bf == 1)
					{
						RotateL(parent);
					}
					else if (parent->_bf == -2 && cur->_bf == 1)
					{
						RotateLR(parent);
					}
					else if (parent->_bf == 2 && cur->_bf == -1)
					{
						RotateRL(parent);
					}
					else 
					{
						assert(false);
					}
					break;
				}
				else {
					assert(false);
				}
			}

			return true;
		}

会看到。插入出现了旋转,那么旋转怎么实现呢?

旋转
 

旋转的原则

  1. 保持搜索树的规则
  2. 让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度
  3. 旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。

右单旋
 

  • 下面的图片展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。

这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种,但都属于这种形态

解读: 

在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要往右边旋转,控制两棵树的平衡。

旋转核心步骤:

  1. 因为5 < b⼦树的值 < 10,
  2. 将b变成10的左⼦树,10变成5的右⼦树,
  3. 5变成这棵树新的根,符合搜索树的规则,

控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。
 

右单旋代码实现
 

		void RotateR(Node* parent)
		{
			Node* subL = parent->_left;
			Node* subLR = subL->_right;

			parent->_left = subLR;
			if (subLR)
				subLR->_parent = parent;

			Node* pParent = parent->_parent;

			subL->_right = parent;
			parent->_parent = subL;

			if (parent == _root)
			{
				_root = subL;
				subL->_parent = nullptr;
			}
			else
			{
				if (pParent->_left == parent)
				{
					pParent->_left = subL;
				}
				else
				{
					pParent->_right = subL;
				}

				subL->_parent = pParent;
			}

			subL->_bf = 0;
			parent->_bf = 0;
		}

 左单旋

  • 下面图展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。

这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有左单旋的场景,实际左单旋形态有很多种,具体跟上⾯右旋类似这里就不在多说

 解读: 

在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从1变成2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树右边太⾼了,需要往左边旋转,控制两棵树的平衡。

旋转核心步骤:

  1. 因为10 < b⼦树的值 < 15,
  2. 将b变成10的右⼦树,10变成15的左⼦树,
  3. 15变成这棵树新的根,

符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转

原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

左单旋代码实现
 

		void RotateL(Node* parent)
		{
			Node* subR = parent->_right;
			Node* subRL = subR->_left;
			parent->_right = subRL;
			if (subRL)
				subRL->_parent = parent;

			Node* parentParent = parent->_parent;
			subR->_left = parent;
			parent->_parent = subR;
			if (parentParent == nullptr)
			{
				_root = subR;
				subR->_parent = nullptr;
			}
			else
			{
				if (parent == parentParent->_left)
				{
					parentParent->_left = subR;
				}
				else
				{
					parentParent->_right = subR;
				}
				subR->_parent = parentParent;
			}

			parent->_bf = subR->_bf = 0;
		}

左右双旋
 

  • 下面的两个图可以看到,左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。
  • 右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的子树不再是单纯的左边高,对于10是左边⾼,但是对于5是右边高,需要⽤两次旋转才能解决,
  1. 以5为旋转点进⾏⼀个左单旋
  2. 以10为旋转点进⾏⼀个右单旋,这棵树这棵树就平衡了。

  • 上面两个图分别为左右双旋中h==0和h==1具体场景分析,下⾯我们将a/b/c⼦树抽象为高度h的AVL子树进行分析,另外我们需要把b的子树的细节进一步展开为8和左子树高度为h-1的e和f子树
  • 因为我们要对b的⽗亲5为旋转点进⾏左单旋,左单旋需要动b树中的左⼦树。b⼦树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察上图的平衡因⼦不同,这⾥我们要分三个场景讨论。
  1. h >= 1时,新增结点插⼊在e⼦树,e⼦树⾼度从h-1并为h并不断更新8->5->10平衡因⼦,引发旋转,其中8的平衡因⼦为-1,旋转后8和5平衡因⼦为0,10平衡因⼦为1。
  2. h >= 1时,新增结点插⼊在f⼦树,f⼦树⾼度从h-1变为h并不断更新8->5->10平衡因⼦,引发旋转,其中8的平衡因⼦为1,旋转后8和10平衡因⼦为0,5平衡因⼦为-1。
  3. h == 0时,a/b/c都是空树,b⾃⼰就是⼀个新增结点,不断更新5->10平衡因⼦,引发旋转,其中8的平衡因⼦为0,旋转后8和10和5平衡因⼦均为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)
			{
				subLR->_bf = 0;
				subL->_bf = 0;
				parent->_bf = 1;
			}
			else if (bf == 1)
			{
				subLR->_bf = 0;
				subL->_bf = -1;
				parent->_bf = 0;
			}
			else if (bf == 0)
			{
				subLR->_bf = 0;
				subL->_bf = 0;
				parent->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

右左双旋

先把左右双旋,了解明白那么右左双旋 就很好明白了 !!!!!

跟左右双旋类似,下⾯我们将a/b/c⼦树抽象为⾼度h的AVL⼦树进⾏分析,另外我们需要把b⼦树的细节进⼀步展开为12左⼦树⾼度为h-1的e和f⼦树。

因为:

  1. 我们要对b的⽗亲15为旋转点进⾏右单旋
  2. 右单旋需要动b树中的右⼦树。

b⼦树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察12的平衡因⼦不同,这⾥我们要分三个场景讨论。

  1. h >= 1时,新增结点插⼊在e⼦树,e⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦,引发旋转,其中12的平衡因⼦为-1,旋转后10和12平衡因⼦为0,15平衡因⼦为1。
  2. h >= 1时,新增结点插⼊在f⼦树,f⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦,引发旋转,其中12的平衡因⼦为1,旋转后15和12平衡因⼦为0,10平衡因⼦为-1。
  3.  h == 0时,a/b/c都是空树,b⾃⼰就是⼀个新增结点,不断更新15->10平衡因⼦,引发旋转,其中12的平衡因⼦为0,旋转后10和12和15平衡因⼦均为0。

 

右左双旋代码实现
 


		void RotateRL(Node* parent)
		{
			Node* subR = parent->_right;
			Node* subRL = subR->_left;
			int bf = subRL->_bf;
			RotateR(parent->_right);
			RotateL(parent);
			if (bf == 0)
			{
				subR->_bf = 0;
				subRL->_bf = 0;
				parent->_bf = 0;
			}
			else 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
			{
				assert(false);
			}
		}

 AVL树的查找

那⼆叉搜索树逻辑实现即可,搜索效率为 O(logN)
 比较十分接近于完全二叉树;

		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_kv.first > key)
				{
					cur = cur->_left;
				}
				else if (cur->_kv.first < key)
				{
					cur = cur->_right;
				}
				else {
					return cur;
				}
			}
			return nullptr;
		}

AVL树平衡检测
 

我们实现的AVL树是否合格,我们通过检查左右⼦树⾼度差的的程序进⾏反向验证,同时检查⼀下结点的平衡因⼦更新是否出现了问题。

注意:
不能检查  节点的 bf 是否 大于-2 小于 2;毕竟这和  自己该自己的卷子差不多;要用算的左子树高度减右子树高度  和 bf 对比; 

AVL树的删除
 

想要了解的同学可参考:《殷⼈昆 数据结构:⽤⾯向对象⽅法与C++语⾔描述》中讲解。

这里就不介绍了0;
 

		int Height()
		{
			return _Height(_root);
		}

		
		void InOrder()
		{
			_InOrder(_root);

			cout << endl;
		}

		bool IsBalanceTree()
		{
			return _IsBalanceTree(_root);
		}
	private:

		bool _IsBalanceTree(Node* root)
		{
			// 空树也是AVL树
			if (nullptr == root)
				return true;
			// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
			int leftHeight = _Height(root->_left);
			int rightHeight = _Height(root->_right);
			int dif = rightHeight - leftHeight;

			if (dif != root->_bf)
			{
				cout << root->_kv.first << "平衡因子异常" << dif << ":" << root->_bf << endl;
				return false;
			}

			if (abs(dif) >= 2)
			{
				cout << root->_kv.first << "高度差异常" << endl;
				return false;
			}

				return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
		}

		void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;

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

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

			int HeightL = _Height(root->_left);
			int HeightR = _Height(root->_right);

			return HeightL > HeightR ? HeightL + 1 : HeightR + 1;
		}

更多推荐