直击AVL树要害:插入过程详解(大厂面试常考:建议收藏)

直击AVL树要害:插入过程详解(大厂面试常考:建议收藏)

前言

AVL树是数据结构中效率比较高的结构,应用非常广泛,相比于顺序表和链表,在一亿个单词查找一个单词平均需要一亿次,AVL树只需要查找几十次,就能快速高效地在一亿个单词获得想要的单词。在实现AVL树数据结构的时候,我们会遇到很多的难题,这一节,我会就AVL树中插入数据过程所会遇到的难题进行详细分析。


本节重点

一、什么是AVL树?

二、AVL树的性质

三、AVL树结点的类型

四、查找

五、重头戏:插入(分类讨论)


一、什么是AVL树?

在这里插入图片描述

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。


二、AVL树的性质

  1. 它的左右子树都是AVL树
  2. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  3. 这里需要注意,空树也属于AVL树,所以在实现AVL树的接口的时候,需要记得对空树的情况进行判断
示意图

在这里插入图片描述
上图就是一棵AVL树


三、AVL树结点的类型(K_Val模型)

//结点类型
template <class K,class V>
struct AVLTreeNode
{
	//三叉链结构
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//数据域
	pair<K, V> _kv;

	//平衡因子:控制左右子树高度平衡
	int _bf;
    //构造函数
	AVLTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{
//新节点由于没有左右子树,因此左右高度肯定是平衡的,那么平衡因子自然就是0
	}
};
  1. 三叉链模型就是有三个指针,方便在使用的过程中可以很直接地找到当前结点的左孩子和右孩子和父亲结点
  2. K_Val模型:使用比Key模型好,其中的数据域是使用一个pair<K,V>类型,pair<K,V>本质是一个结构体,里面包含有first(key)second(value),应用比较广泛,value是一个与key有一定关系的数据

四、查找

查找操作和二叉搜索树非常相似

代码示例
	bool Find(const pair<K,V>& kv)//插入一个kv类型的数据
	{
	//const:对kv进行保护,防止kv被改变
	//&:减少拷贝
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

注意:

  1. 这里可以不对树进行判空操作,因为,如果树为空,那么_root = nullptr,则cur = nullptr,那么下面的循环就不会进去,函数会直接返回false
  2. 在比较的过程中不是比较kv的大小,而是比较kv中的first,因为kv中的first才是每个结点的关键字

五、重头戏:插入(分类讨论)

  1. 按照二叉搜索树的原则进行插入
if (_root == nullptr)
		{
			//根为空:空树
			_root = new Node(kv);
			_root->_bf = 0;
			return true;
		}

		//非空:找到插入的位置
		//(利用二叉搜索树中讲到的典型的遍历方式:双指针法)
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)//注意比较的是kv中的first
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)//注意比较的是kv中的first
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				//数据已经存在,不支持插入
				return false;
			}
		}

		//到这里,说明找到了插入的位置,就是cur
		cur = new Node(kv);
		cur->_bf = 0;
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
在继续下面的操作之前,首先需要学会几种常见的情况及对应的旋转方式

情况一:当新插入的结点的位置是在较高左树的左侧时,采用右旋
如图
在这里插入图片描述
在右旋的过程中需要记录几个重要的结点:ppNode,subL,subLR(如图)
在这里插入图片描述
一般情况的旋转过程:(右旋)
在这里插入图片描述

分析:

  1. 将b给60结点的左边
  2. 将60结点给30的右边

这里需要注意两个问题:

  1. 当前的parent可能为根节点,也可能为子树中的结点
  2. subLR可能为空,可能不为空
代码实现

	void RotateR(Node* parent)
	{
		//1.记录关键的结点
		Node* ppNode = parent->_parent;
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//2.处理parent和subLR的关系
		parent->_left = subLR;
		if (subLR)//这里一定要判断subLR是否为空,如果为空则不需要处理
		//否则,就对空指针指向的内容进行非法访问了
		{
			subLR->_parent = nullptr;
		}

		//3.处理parent和subL的关系
		subL->_right = parent;
		parent->_parent = subL;

		//4.在处理subL->_parent的时候,需要判断原来的parent是否为根
		if (ppNode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
			//_root->_parent = ppNode;此时ppNode = nullptr
		}
		else
		{
			// 5.判断原来的parent是ppNode的左孩子还是右孩子
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subl->_parent = ppNode;
		}
	}
平衡因子的更新
parent->_bf = subL->_bf = 0;

情况二:当新插入的结点的位置是在较高右树的右侧时,采用左旋(如图)
在这里插入图片描述
一般情况的左旋
在这里插入图片描述

分析:

  1. 将b给30的右边
  2. 将30给60的左边

在左旋的过程中需要记录几个重要的结点:ppNode,subR,subRL
在这里插入图片描述
这里需要注意两个问题:

  1. 当前的parent可能为根节点,也可能为子树中的结点
  2. subRL可能为空,可能不为空
代码实现
void RotateL(Node* parent)
	{
		//1.记录关键结点
		Node* ppNode = parent->_parent;
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//2. 处理parent和subRL的关系
		parent->_right = subRL;
		if (subRL)
		{
			subRL->_parent = parent;
		}

		//3. 处理parent和subR的关系
		subR->_left = parent;
		parent->_parent = subR;

		//在更新subR->_parent时,需要判断原来的parent是否为根
		if (ppNode == nullptr)
		{
			//如果为根,则需要更新根的情况
			_root = subR;
			_root->_parent = nullptr;
			//_root->_parent = ppNode;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
	}
平衡因子的更新
parent->_bf = subR->_bf = 0;

情况三:在较高的左子树的右侧插入新节点(双旋:左旋+右旋)
在这里插入图片描述
这种情况则需要先对左子树进行左旋,转化成左子树比右子树高的情况,再对整棵树进行右旋,这里主要是要对subLR->_bf的情况进行分类以更新后续的平衡因子

  1. 向上面的图这样的情况,subLR为新插入的结点,则其平衡因子为0
  2. subLR不是新插入的结点,新插入的结点的位置是subLR的左边,subLR->_bf = -1
    在这里插入图片描述
  3. subLR不是新插入的结点,新插入的结点的位置是subLR的右边,subLR->_bf = 1
    在这里插入图片描述
    一般情况的旋转过程:
    在这里插入图片描述
代码实现
void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(subL);
		RotateR(parent);

		//更新平衡因子
		if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

情况四:在较高的右子树的左侧插入新节点(双旋:右旋+左旋)

  1. 最简单的情况:subRL不存在,在该位置插入新节点,插入后subRL->_bf = 0
    在这里插入图片描述
  2. subRL不是新插入的结点,新插入的结点的位置是subRL的左边,subRL->_bf = -1
    在这里插入图片描述
  3. subRL不是新插入的结点,新插入的结点的位置是subRL的右边,subRL->_bf = 1
    在这里插入图片描述
    处理方法:先对右子树进行右旋,转化为整棵树的右子树比左子树高的情况,再对整棵树进行左旋
    在这里插入图片描述
代码实现
void RotateRL(Node* parent)
	{
		//记录关键的结点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		//旋转
		RotateR(subR);
		RotateL(parent);

		//分类讨论
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
调整
代码实现与分析
while (parent)
		{
		//调整平衡因子
			if (cur == parent->_left)
			{
			//插入左树,平衡因子--
				parent->_bf--;
			}
			else
			{
			//插入右树,平衡因子++
				parent->_bf++;
			}

			if (parent->_bf == 0)
			{
			//高度不变,不需要进行调整,退出循环
				break;
			}
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
			//出现1或者-1,只有可能是0变成1或者0变成-1
			//也就是说原来的左右高度是一样的,平衡因子为0
			//插入左树后,平衡因子变成-1
			//插入右树后,平衡因子变成1
			//需要继续向上进行调整
				cur = parent;
				parent = cur->_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)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);
				}
				break;
			}
		}
  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
二叉搜索树: 二叉搜索树是一种特殊的二叉树,它的左子树中所有节点的值都小于它的根节点的值,而右子树中所有节点的值都大于它的根节点的值。二叉搜索树支持快速的插入、查找和删除操作,时间复杂度为O(logn)。 插入和删除: 插入操作是将一个新节点插入到二叉搜索树中,找到对应的位置并插入即可。删除操作比插入操作复杂一些,需要考虑到删除节点后二叉搜索树的结构是否仍然满足二叉搜索树的性质。如果需要删除的节点是叶子节点,直接删除即可。如果需要删除的节点只有一个子节点,可以将其子节点上移,删除该节点。如果需要删除的节点有两个子节点,可以找到该节点右子树中最小的节点,将其值复制到需要删除的节点中,然后删除该最小节点。 树遍历: 树的遍历是指按照某种顺序依次访问树中的所有节点。常用的树遍历方式有前序遍历、中序遍历和后序遍历。前序遍历是先访问根节点,然后访问左子树和右子树;中序遍历是先访问左子树,然后访问根节点和右子树;后序遍历是先访问左子树和右子树,然后访问根节点。 AVL树: AVL树是一种自平衡二叉搜索树,它保证了树的任意节点的左右子树高度差不超过1。当插入或删除节点时,AVL树会通过旋转操作来保持树的平衡。AVL树的查找、插入、删除操作的时间复杂度均为O(logn)。 红黑树: 红黑树也是一种自平衡二叉搜索树,它的插入和删除操作比AVL树更快,但查找操作稍慢一些。红黑树通过染色和旋转操作来保持树的平衡,它要求树中任意一条从根到叶子的路径上,红色节点和黑色节点的个数相同。红黑树的时间复杂度也为O(logn)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值