<C++> AVLTree

目录

1. AVL概念

2. AVL树节点的定义

3. AVL树的插入

4. AVL树的旋转

5. AVL树的验证

6. AVL树的删除

7. AVL树的性能

暴力搜索、二分搜索、二叉搜索树、二叉平衡搜索树(AVL、红黑树)、多叉平衡搜索树(B树)、哈希表 

1. AVL概念

        二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

        因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差 (简称平衡因子的绝对值不超过1 (-1 / 0 / 1) ,不是绝对的0是因为做不到绝对的平衡(例如,非满二叉树)

平衡因子:右子树高度 - 左子树高度

        如果一棵二叉搜索树是高度平衡的,它就是AVL 树。如果它有 n 个结点,其高度可保持在
,搜索时 间复杂度 O( log N  )
        
        满二叉树:2^h - 1 = N
        
                                                        -> 约等于logN        
        
        AVL树:2^h - X = N        (X范围 [ 1, 2^(h - 1) - 1 ] 

2. AVL树节点的定义

        直接写key—value版本,因为它与单key版本,只是多了一个对应value(pair就解决了),可以看二叉搜索树一文再理解两个版本的关系

        加入平衡因子会更加方便

template<class K, class V>
struct AVLTreeNode
{
	int _bf;    //balance factor
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

3. AVL树的插入

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

  1. 按照二叉搜索树的方式找到合适点插入新节点

  2. 调整节点的平衡因子

  • 新增在左边,parent平衡因子--
  • 新增在右边,parent平衡因子++
  • 如果插入后,节点的平衡因子变成了0,那么说明这个节点的左右子树高度平衡了,高度没有增大或减小,所以不会对往上的祖先产生影响,就不需要往上更新祖先的平衡因子
  • 如果插入后,节点的平衡因子变成了1 或 -1,那么说明这个节点的左右子树高度不平衡,高度增大了或减小了,会对其祖先的平衡因子产生影响,所以使用 parent 指针向上更新平衡因子(这就体现了 parent 指针的用武之地),直到某一个祖先的平衡因子变为 0 或根节点就停止!!!(因为如果一个节点的高度没变,即平衡因子为0,那么它就不影响parent节点)
  • 如果插入后,节点的平衡因子变成了2 或 -2,说明parent所在的子树的高度变化且不平衡了!这时就要体现 AVL 平衡的特点,对 parent 所在子树进行旋转
  • 不会出现3 或 -3以上情况,因为它们必然会经历2 或 -2的情况,那时我们早就已经处理完了
bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		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 (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;

		// ... 控制平衡
		// 更新平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else // if (cur == parent->_right)
			{
				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)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}

				break;
			}
			else  //正常代码不会执行到这里,所以抛异常
			{
				assert(false);
			}
		}
		return true;
	}

4. AVL树的旋转

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

新节点插入较高右子树的右侧 --- 右右:左单旋

        cur 的 left 链接到 parent 的 right,再将 parent 链接到 cur 的 left

        在操作时,不仅要更新_bf平衡因子,还要额外注意父节点指针,一不留心就会出现三叉链,而且还要注意,parent 是不是 root,如果不是根节点,那么还要提前记录 parent 的 parent,以便使cur 的 parent 指向修改

符合左单旋的场景无穷尽,下图直接概括了左单旋的抽象总结图: 

         但是无论h高度是多少,节点的操作是一样的

//左单旋
	void RotateL(Node* parent)
	{
		++_rotateCount;

		Node* cur = parent->_right;
		Node* curleft = cur->_left;

		//开始链,注意父亲链
		parent->_right = curleft;
		if (curleft)	//这里判断cur的right是不是为空,为空的话就不能使用指针链接
		{
			curleft->_parent = parent;
		}

		cur->_left = parent;

		Node* ppnode = parent->_parent;

		parent->_parent = cur;

		//判断parent的父亲的情况
		if (parent == _root)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;

			}
			//记得父亲链
			cur->_parent = ppnode;
		}
		//不要忘了平衡因子
		parent->_bf = cur->_bf = 0;
	}

新节点插入较高左子树的左侧---左左:右单旋 

        右单旋与左单旋完全对称

简单分析后即可总结出上图:

	//右单旋
	void RotateR(Node* parent)
	{
		++_rotateCount;
		
		Node* cur = parent->_left;
		Node* curright = cur->_right;

		//开始链,注意父亲链
		parent->_left = curright;
		if (curright)	//这里判断cur的right是不是为空,为空的话就不能使用指针链接
		{
			curright->_parent = parent;
		}

		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;

		//判断parent的父亲的情况
		if (ppnode == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			//记得父亲链
			cur->_parent = ppnode;
		}

		//不要忘了平衡因子
		parent->_bf = cur->_bf = 0;
	}

新节点插入较高右子树的左侧--- 右左:先右单旋再左单旋
         上面的两种情况,都是极端的左或右,新增节点与祖先处于一条直线。而双旋的情况更复杂,新增节点与祖先节点的连线是一条折线
双旋本质相当于第一次的旋转是预处理:将树修正为左单旋情况或右单旋情况,第二次就是简单的单旋情况
       
        那么只需复用上面的左右单旋函数即可解决问题,但是复用两个函数后,30、90、60这三个节点的平衡因子都会成为0!这是不正确的,因为h大小分情况,并且在60的左右插入位置不同,导致结果的平衡因子也不同。
分类讨论:
        
所以对于不同情况,要进行分类的处理平衡因子
	//右左双旋
	void RotateRL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		int bf = curleft->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		//分情况更新平衡因子
		if (bf == 0)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			cur->_bf = 1;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

        代码解析:在插入一个节点之后,向上更新平衡因子,直到遇见以上四种情况,开始旋转处理

新节点插入较高左子树的右侧 --- 左右:先左单旋再右单旋
        同理
	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		//分情况更新平衡因子
		if (bf == 0)
		{
			parent->_bf = 0;
			cur->_bf = 0;
			curright->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			cur->_bf = 0;
			curright->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			cur->_bf = -1;
			curright->_bf = 0;
		}
	}
        如果程序出错了,数据量很大调试就很难受了,这时候可以一段一段注释调试,对于不同场景可能不合适,所以还可以手写一个判断函数,让计算机协助你找到错误,打开监视窗口、打断点,要带着思路、有预测结果的去调试,中间可以使用条件断点(可以手写if,里面写一些语句,程序走到这里时停下)

总结:

        假如以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为根的子树个高度降低,已经平衡,不需要再向上更新。

5. AVL树的验证

	//递归求高度,测试代码是否正确
	int Height()
	{
		return Height(_root);
	}

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

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	//测试代码是否正确,是不是AVL结构
	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHight = Height(root->_left);
		int rightHight = Height(root->_right);

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

		return abs(rightHight - leftHight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
	}

我们使用随机生成的数来测试 

#include "AVLTree.h"
#include <vector>

const int N = 1000000;

int main()
{
	AVLTree<int, int> at;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (int i = 0; i < N; i++)
	{
		v.push_back(rand());
	}

	for (auto e : v)
	{
		at.Insert(make_pair(e, e));
	}

	cout << at.IsBalance() << endl;
	return 0;
}

6. AVL树的删除

        按搜索树的规则查找节点进行删除,与插入时对节点的平衡因子更新相反,如果删除节点之后 parent 平衡因子为0,那么需要继续往上更新,如果是 1 或 -1,那么不需要往上更新

        而且删除时,双旋情况更多,平衡因子的更新更复杂

7. AVL树的性能

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 logN 但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

        因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值