AVL树实现

上篇文章介绍了搜索二叉树,这篇文章介绍一下它的升级版,平衡搜索二叉树,也就是AVL树了。

在这里插入图片描述

AVL树是在二叉搜索树的基础上演变而来的主要是为了解决 当树中的值单调性很明显的时候二叉树就变成了单支树,效率变得低下了,所以AVL树在此基础上对树进行了平衡处理,可以做到将一颗二叉树在插入随意数据(数据单调也好,不单调也好)都能使该树时刻都是绝对平衡的!这就解决了当二叉搜索树中的值是单调递增或递减的时候不会形成单支树,降低了树的平均高度,效率得到了提升,增删查改的时间复杂度都是log(N) 而普通的搜索树因为存在单支树的情况,时间复杂度是0(N)

在这里插入图片描述
上图中提到了平衡因子这个概念,拿出来讲一下

平衡因子是AVL树每个结点都具有的属性,它的定义是该结点的右子树减左子树的高度差 当平衡因子为0 1 -1 是都说明这个结点的左右子树的高度差不超过1 就时平衡因子的绝对值不超过1说明该结点是平衡的
平衡因子是判断一棵树是否平衡的重要依据,只有每个节点的平衡因子都是不超过1才能说明整棵树是平衡的
可以总结为一棵二叉搜索树每个结点都遵循左右子树高度差不超过一,那么它可以称为AVL树

AVL树的结点定义成一个单独的类
每个结点包含一个键值对key val(只有key也可以,就是set了,值设为键值对的话就是map),左右孩子结点,父亲结点,和上面的平衡因子。

template<class K, class V>//模板参数
struct AVLTreeNode
{
	//为了方便找到父亲 构造三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//为了构造出具有平衡特性的树 加入了平衡因子balance factor
	int _bf;
	pair<K, V> _kv;
	
	//构造函数
	AVLTreeNode()
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(make_pair(K(),V()))//匿名对象的好处
	{}
	//带参构造函数
	AVLTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
		, _kv(kv)
	{}
};

下面就来介绍AVL树的实现

一、插入结点

插入新结点的时候需要注意的是:每次插入完新结点都需要向上迭代着更新平衡因子,因为新增了结点会影响上面原本是平衡的结点的平衡状态,所以要往上更新每个结点的平衡因子,直到走到空或者某个结点已经出现不平衡了才停止更新,然后进行旋转处理使不平衡的结点变的平衡即可。

情况一:直接插入不用旋转

当根结点为空的时候插入新的结点就可以直接插入,并把新插入的结点作为根,不需要做旋转处理;
当一个结点的左右子树的高度差为一的时候新增结点插入到高度较低的一边的时候也可以直接插入,不用做旋转处理;
在这里插入图片描述

重点:单旋和双旋的使用场景指出

下面是需要进行旋转处理的情况:单旋和双旋
单旋和双旋的区别是看是新增结点与要进行旋转的结点之间形成的路径(具体看下面的图,语言不好描述)是一条直线还是一条折线,直线就是单旋,折线就是双旋;其中如果是 \ 型直线就是左单旋,/型直线就是右单旋;如果是 < 型折线就是左右双旋,> 型就是右左双旋;

情况二:插入新结点之后要进行单旋

什么时候才需要进行单旋呢?
当一个结点的左右子树的高度差为1的时候,再次新增结点到高度较高的那一边,那么高度差就变成了2,就需要进行旋转处理。至于是左单旋还是有单旋就需要判断是该结点的左边高还是右边高了,左边高进行右单旋,右边高进行左单旋。

1.左单旋

在这里插入图片描述
解读:其中新增之前 10的平衡因子是1 (右边高度h+1 左边高度h),右边的高度高,新增的时候还是往右边新增,那么右边的高度就是h+2了,此时10的平衡因子就是2,不满足平衡条件(平衡因子绝对值不超过1),所以要对10结点进行单旋,因为这条路径是**\型直线,所以是右边高要进行左单旋,先把15的左孩子作为10的右孩子,再把10作为15的左孩子,这样就压低了树右边的高度,使得左右平衡,高度相等。**

平衡因子的更新: 这个情况下只要旋转后就是平衡的,直接把10和15的平衡因子设为0即可。

代码:

//左单旋(rotatel) rotate旋转
	//当一个结点的左右两边高度相差1 右边比左边的高度高1 如果此时还是继续往右边插入新元素 那么就会导致右边的高度比左边的高度高2
	//这就导致了当前结点的_bf是2了 就得进行左单旋,把左边的树旋转到右边的左边 使得右边的树左右两边的高度相同 这样既不会破坏
	//平衡二叉树的物理结构,还成功地插入了新元素
	//void RotateL(const Node* parent)
	void RotateL( Node* parent)//不要用const parent需要被修改
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		parent->_right = SubRL;//让右结点的左结点做父亲结点的右结点

		if (SubRL)//防止SubRL为空 造成对空指针的访问
		SubRL->_parent = parent;//更新右结点的左节点的父亲结点 指向父亲结点
		
		SubR->_left = parent;//让原来父亲结点的右结点的左指向父亲结点指向
		Node* parent_parent = parent->_parent;//在父亲结点的父亲结点改变之前保存父亲结点的父亲结点
		parent->_parent = SubR;//上面让父亲结点的左节点为父亲结点 那么就要更新父亲结点的父亲结点 让它指向其左结点
		if (parent == _root)//接下来就是判断父亲结点是不是根结点 如果是根节点就让SubR当新的根 并且让SubR的父亲结点指向空
		{
			_root = SubR;
			SubR->_parent = nullptr;
		}
		else//如果最开始传进来的结点不是根节点 那么就让SubR的父亲结点指向原来父亲结点的父亲结点但是要搞清楚原来的父亲结点是他父亲的左孩子还是右孩子
		{
			if (parent_parent->_left == parent)//这里还有当前结点是其他结点的子树结点 就得判断该结点是父亲结点的左节点还是右结点 然后把父亲结点的子结点设置为subr
			{
				parent_parent->_left = SubR;//更新开始父亲结点的父亲结点的孩子结点
			}
			else
				parent_parent->_right = SubR;
			SubR->_parent = parent_parent;//更新SubR的父亲结点
		}
		
		parent->_bf = SubR->_bf=0;//走到这里就说明调整完成了 就让开始的parent和SubR的_bf等于0 因为通过调整使其左右子树高度都相等了

	}

2.右单旋

在这里插入图片描述

解读:其中新增之前 10的平衡因子是-1 (右边高度h 左边高度h+1),左边的高度高,新增的时候还是往左边新增,那么左边的高度就是h+2了,此时10的平衡因子就是2,不满足平衡条件(平衡因子绝对值不超过1),所以要对10结点进行单旋,因为这条路径是**/型直线,所以是左边高要进行右单旋,先把15的右孩子作为10的左孩子,再把10作为15的右孩子,这样就压低了树左边的高度,使得左右平衡,高度相等。**

平衡因子的更新: 这个情况下只要旋转后也是平衡的,也是直接把10和15的平衡因子设为0即可。

代码:

//右旋(RotateR) 
	//当一个结点的左树比右树的高度高1 然后继续在左树插入新元素就会使得该结点的左树高度比右树高度高2 就要进行旋转调整  这时就是右旋了
	//让左子树的右子树当父亲结点的左子树 然后让父亲结点当左树的右子树 这样就即保证了平衡二叉树的结构不变的同时有插入了数据 然后最开始的
	//父亲结点的左结点就成了这些结点的根节点 然后更新这个结点的父亲结点 并且更新最开始父亲结点的父亲结点的子节点 让新的那个结点做父亲结点的父亲结点的左结点
	//整体思路与左旋基本一样
	//void RotateR(const Node* parent)
	void RotateR( Node* parent)//不要用const 因为parent需要修改
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		parent->_left = SubLR;
		if (SubLR)
		{
			SubLR->_parent = parent;
		}
		SubL->_right = parent;
		Node* parent_parent = parent->_parent;
		parent->_parent = SubL;
		if (parent == _root)
		{
			_root = SubL;
			SubL->_parent = nullptr;
		}
		else
		{
			if (parent_parent->_left == parent)
			{
				parent_parent->_left = SubL;
			}
			else
			{
				parent_parent->_right = SubL;
			}
			SubL->_parent = parent_parent;
		}
		SubL->_bf = parent->_bf = 0;//跟左旋一样 更新平衡因子
	}

情况三:插入新结点后要进行双旋

上面说到如果路径是折线型就需要进行双旋,双旋其实就是旋转两次,其中第一次旋转是将折线型变成直线型,也就是将双旋的情况转化为单旋的情况,然后再用单旋的情况来处理即可。

1.左右双旋

在这里插入图片描述
解读:新增之前20的平衡因子是-1说明左子树的高度比右子树的高度高1但是,只要高度差不超过1就是平衡的,之后在20的左子树(10)的右子树(15)的右边新增结点,就使得10的右边的高度比左边高了1,然后从下往上进行平衡因子的更新,就会使得10的平衡因子变成了1,20的平衡因子变成了-2 不满足平衡条件就需要进行旋转,这里是需要进行双旋的,因为路径是折线型的,先对10进行左单旋 ,然后让15链接上20的左,再对20进行右单旋,就使得这颗树平衡了。

平衡因子的更新:这里因为是进行了两次旋转,所以平衡因子的更新是跟单旋不一样的,上面这种新增在15的右边情况完成两次旋转后15 和 20的平衡因子变成了0,10的平衡因子变成了-1要根据图中情况来进行更新。因为下面同样是左右双旋,但是最后平衡因子的更新是不一样的,所以要分清是哪种情况!

在这里插入图片描述
解读:新增之前20的平衡因子是-1说明左子树的高度比右子树的高度高1但是,只要高度差不超过1就是平衡的,之后在20的左子树(10)的右子树(15)的左边新增结点,就使得10的左边的高度比左边高了1,然后从下往上进行平衡因子的更新,就会使得10的平衡因子变成了1,20的平衡因子变成了-2 不满足平衡条件就需要进行旋转,这里是需要进行双旋的,因为路径是折线型的,先对10进行左单旋 ,然后让15链接上20的左,再对20进行右单旋,就使得这颗树平衡了。

平衡因子的更新:这里因为是进行了两次旋转,所以平衡因子的更新是跟单旋不一样的,上面这种新增在15的左边情况完成两次旋转后,15 和 10的平衡因子变成了0,20的平衡因子变成了1**,虽然都是左右双旋但是两种情况的平衡因子的更新是截然不同的**,所以要根据图中情况来进行更新。要分清是哪种情况!

//接下来就是双旋了 分左右双旋和右左双旋
	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		int SubLR_bf = SubLR->_bf;
		RotateL(SubL);//先左旋后右旋 之后再分情况进行平衡因子的更新 参考上面画的图来看
		RotateR(parent);
		//接下来就是更新这几个结点的平衡因子了
		//但是这里有有两种情况 因为左右双旋有两种新增的情况 一左一右 两种情况走完后的平衡衡因子的数目又不同 要分情况来更新
		if (SubLR_bf == 1)
		{
			SubLR->_bf = 0;
			SubL->_bf = -1;
			parent->_bf = 0;
		}
		else if(SubLR_bf==-1)
		{
			SubLR->_bf = 0;
			SubL->_bf = 0;
			parent->_bf = 1;
		}
		else if (SubLR_bf == 0)
		{
		SubLR->_bf = 0;
		SubL->_bf = 0;
		parent->_bf = 0;
		}
		else
		{
		assert(false);//出现其他情况说明在双旋之前就出错了 断言断死
		}
	}

2.右左双旋

在这里插入图片描述
同上! 就是和左右双旋的第一种情况类似,只不过是跟那种情况就是相当于左右互换了一下罢了,就不多解释了。

在这里插入图片描述
同上左右双旋的第二种情况类似!

代码:

	//右左双旋
	void RotateRL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		int SubRL_bf = SubRL->_bf;
		RotateR(SubR);
		RotateL(parent);
		//更新平衡因子
		if (SubRL_bf == 1)
		{
			SubRL->_bf = 0;
			SubR->_bf = 0;
			parent->_bf = -1;

		}
		else if(SubRL_bf==-1) 
		{
			SubRL->_bf = 0;
			SubR->_bf =	1;
			parent->_bf = 0;
		}
		else if (SubRL_bf == 0)
		{
			SubRL->_bf = 0;
			SubR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);//出现其他情况说明在双旋之前就出错了 断言断死
		}

	}

整体的插入的方法:值得细读!

//插入 如果要重载[]运算符 就把返回值改成 pair
	pair<Node*,bool> Insert(const pair<K, V>& kv)
	{
		//先去找合适的插入位置
		if (_root == nullptr)//第一次插入的情况,根为空,新增节点做根
		{
			_root = new Node(kv);
			//return true;
			return make_pair(_root, true);
		}
		//Node* parent = nullptr;
		Node* parent = _root;

		Node* cur = _root;
		/*Node* newnode = new Node(kv);*/
		while (cur)
		{
			if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				//return false;
				//make_pair(_root, false);
				return make_pair(cur, false);//新增节点的值已经存在,返回该节点的地址,和一个false
				//如果是为了封装出multimap就不需要这样,因为multimap允许有重复的值存在
			}
		}
		//走到这里说明找到了正确的插入位置 这里是要根据值来判断是插入到左边还是右边
		//if (parent->_kv.first > newnode->_kv.first)
		cur = new Node(kv);
		Node* newnode = cur;
		if (parent->_kv.first > kv.first)

		{
			parent->_left = newnode;
			//cur->_bf--;//插入到左边就让bf减减因为bf是右边高度减左边高度 错误的代码 忽略掉
			cur->_parent = parent;
		}
		else 
		{
			parent->_right = newnode;
			//cur->_bf++;
			cur->_parent = parent;

		}  
		//接下来就要判断parent的bf是多少了
		//三种情况:
		// 1、如果parent的bf是1或者-1就说明高度变了 当前结点高度还会影响到其上面的节点(同一路径上的,父亲结点)
		//    就要往上更新平衡因子
		// 2、如果是当前结点的平衡因子是0了就说明当前结点左右子树的高度平衡了 就停止更新平衡因子
		// 3、如果是当前结点的平衡因子是2或者-2就说明当前结点已经不平衡了,就要开始进行旋转调整了 
		// 
	
		while (parent)//只要父亲节点没有走到空就一直往上换代更新
		{
			if (parent->_left == cur)//一上来就进行平衡因子的更新
			{
				--parent->_bf;
			}
			else
			{
				++parent->_bf;
			}
			if (parent->_bf == 0)//对应第一种直接插入的操作不需要进行旋转
			{
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				if (parent->_bf == 2)
				{
					if (cur->_bf == 1)//说明要进行左单旋
					{
						RotateL(parent);
					}
					else//cur->_bf==-1 说明要进行右左双旋
					{
						RotateRL(parent);
					}
				}
				else 
				{
					if (cur->_bf == -1)//进行右单旋即可
					{
						RotateR(parent);
					}
					else//cur->_bf==1 说明要进行左右双旋
					{
						RotateLR(parent);
					}

				}
				break;//到此就完成了双旋调整 直接跳出循环
				
			}
			else if (parent->_bf == 1 || parent->_bf == -1)//只要当前节点的平衡因子不为0就往上迭代父子关系去更新平衡因子
			{
				cur = parent;
				parent = parent->_parent;
			}
			else
			{
				assert(false);//走到这里说明前面的情况都不符合,说明再插入之前就出错了
			}
		}
		return make_pair(newnode, true);//为了对应stl中map和set的插入 如果插入成功就返回一个pair存着新增节点的地址和一个布尔值,成功就是返回true
		
	}

二、删除结点(就不多介绍了,掌握了插入就能理解AVL树的原理了,删除了解一下即可)

三、查找结点

查找和二叉搜索树一样,分为递归法和非递归法 相比插入就简单很多了 不多介绍了

//查找
	Node* _FindR(const Node* root, const K& key)
	{
		if (root == nullptr)
			return nullptr;
		if (key > root->_kv.first)
		{
			return _FindR(root->_right, key);
		}
		else if (key < root->_kv.first)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return root;
		}
		//这句是多余的
		/*return nullptr;*/
	}
	Node* _Find(const Node* root,const K& key)
	{
		if (root == nullptr)
			return nullptr;
		Node* cur = root;
		while (cur)//这里是要有循环的
		{
			if (key > root->_kv.first)
			{
				root = root->_right;
			}
			else if (key < root->_kv.first)
			{
				root = root->_left;
			}
			else
			{
				return root;
			}
		}
		return nullptr;
		
	}
	
	Node* Find(const K& key)
	{
		return _Find(_root, key);
		//return _FindR(_root,key);
	}

四、拷贝构造(深拷贝)与赋值运算符重载

递归构造 不多介绍

//拷贝构造(深拷贝)
	Node* _AVLTree( Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}
		Node* newroot = new Node(make_pair(root->_kv.first, root->_kv.second));
		newroot->_left = _AVLTree(root->_left);
		newroot->_right = _AVLTree(root->_right);
		return newroot;
	}
	AVLTree(const AVLTree& tree)
	{
		_AVLTree(tree->_root);
	}

析构:

//析构函数
	void Destory (Node* root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
	}
	~AVLTree()
	{
		Destory(_root);
	}

赋值运算符重载:

//两行代码搞定
	//赋值运算符重载
	AVLTree& operator=(AVLTree tree)
	{
		swap(_root, tree._root);
		return *this;
	}

[ ]的重载

在这里插入图片描述
在这里插入图片描述

stl中的map和set的插入都是支持如果是插入单个值的话其返回值是一个pair<iterator,bool> 如果插入了一个容器中没有的值这个返回的pair的first就是那个节点的迭代器,second就是一个布尔值,插入未插入过的值返回true ,否则返回false;
在这里插入图片描述
其中map还支持运算符[ ]的重载 就是可以调用insert来辅助实现可以对对应的key的值进行修改,也可以用它来实现插入元素
因为其是调用insert 来插入传过去的值,不管插入成功与否都是返回pair<iterator,bool>中的iterator这个迭代器指向的pair型节点的second引用 即val的地址 ,那么如果要插入的值(键值对类型的pair<key,val>)在容器中没有出现过通过这个运算符重载就会被插入到容器中,并且返回该值的second的引用,那么可以对其通过 [key]=具体的值 的形式来修改该键值对的值

//stl中的写法
	V& operator[](const K& key)
	{
		return (*((this->insert(make_pair(k,mapped_type()))).first)).second
	}
	//上面是连起来的 下面是简化版本 更容易看懂
	V& operator[](const K& key)
	{
		pair<Node*, bool> ret = Insert(make_pair(key, V()));
		return ret.first->_kv.second;
	}
	

五、判断是否为AVL树

判断是否是AVL树就是判断其每个节点的平衡因子是否有超过二的,即一个节点的左右子树的高度差是否有超过1的,可以用递归来干这个事。

	//获取某个结点的高度的方法
	int _GetHight(Node* root)
	{
		if (root == nullptr)
			return 0;
		return 1 + max(_GetHight(root->_left), _GetHight(root->_right));//不为空 返回左右子树中高度高的加上自己的高度1
	}
	
	bool IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;//空不影响平衡
		}
		int heighL = _GetHight(root->_left);//获取左右子树的高度
		int heighR = _GetHight(root->_right);
		int dv = heighR - heighL;
		if (dv != root->_bf)//判断高度差是否和平衡因子对应相等
		{
			cout << root->_kv.first << ":" << "平衡因子异常" << endl;
			return false;
		}
		/*if (root->_bf > 2)
			return false;*/
			//高度差和平衡因子相等后 还要判断平衡因子是否不超过1,然后递归判断左右子树是否是符合AVL树的规则
		return dv < 2 && IsBalance(root->_left) && IsBalance(root->_right);
	}
	bool IsAVLTree()
	{
		return IsBalance(_root);
	}

六、模拟map

用AVL树来模拟map的功能 红黑树和AVL树的结构类似,但是调整的规则不同,set map就是红黑树封装出来的,可以说map
set就是一个壳子罢了,都是用kv类型的红黑树通过实现传的v的类型的不同,如果树的val是一个键值对就是封装的map,如果val也是一个key类型那么封装的就是set,然后套用红黑树的功能就可以实现map和set。至于为啥不用AVL树来封装出map和set是因为对于STL中的set和map来说,需要进行频繁的插入和删除,而AVL这种严格平衡二叉树,插入删除太频繁会导致左旋右旋操作频繁,影响性能,AVL只适合查找较多但插入、删除不多的操作。而红黑树也是一种平衡二叉树,但只要求最长路径不超过最短路径的两倍,因此,更适合插入、删除操作较多的结构
在这里插入图片描述

本文到此就结束了~

在这里插入图片描述
98.png)
在这里插入图片描述

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值