当前位置: 首页 > news >正文

数据结构进阶 AVL树

作者:@小萌新
专栏:@数据结构进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:介绍高阶数据结构:AVL树
在这里插入图片描述

AVL树

  • AVL树的概念
  • AVL树节点类的定义
  • AVL树的插入
  • AVL树的旋转
    • 左单旋
    • 右单旋
    • 左右双旋
    • 右左单旋
  • AVL树的验证
  • AVL树的查找
  • AVL树的修改
  • AVL树的删除
  • AVL树的性能

AVL树的概念

首先抛出一个问题 为什么AVL树会诞生呢?

还记不记得我们在学习二叉搜索树最后总结的时候说的一句话

在二叉搜索树是完全二叉树的情况下效率最高 此时的效率是 LogN

在二叉搜索时是单边树的情况下效率最差 此时效率退化至 N

为了解决这个退化的效率问题 AVL树诞生了

AVL树是由两位俄国科学家G.M. Adelson-Velsky和E.M.Landis在1962年发明的。他们在一篇论文中提出了AVL树这一概念,并将其命名为"AVL树",以纪念他们的姓氏的首字母。
AVL树是一种自平衡二叉搜索树,它能够保证插入、删除和查找操作的时间复杂度都是O(log n),因此在很多场合下都很有用。

AVL树可以是一颗空树 也可以是具有以下性质的一颗二叉搜索树

  1. 它的左右子树都是AVL树
  2. 树的左右子树高度差不超过一 (以下样例均为右子树减左子树)

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

蓝色数字表示的是左右子树的高度差值(以下简称平衡因子) 它们都是不超过1的

红色的数字表示储存数据的值 符合二叉搜索树的性质

我们可以发现 当一颗二叉搜索树是AVL树的时候 假设它一共有N个节点 那么它的高度非常接近LogN 同样的搜索效率也接近LogN

这也就解决了单边树的低效问题

AVL树节点类的定义

我们这里使用KV模型来实现AVL树

为了方便后续的操作 使用三叉链结构来完成它

在这里插入图片描述

一个节点除了指向它的左右两个子节点之外还指向它的父节点 一共三个指针 所以称为三叉链

此外 我们还增加一个int类型的数据 来记录平衡因子

代码表示如下

template<class K, class V>
struct AVLTreeNode
{
	// 三叉连
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	// 平衡因子
	int _bf;

	// 储存的键值对
	pair<K, V> _kv;


	// 构造函数
	AVLTreeNode(const pair<K , V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0) //因为构造出来之后没有左右子树 所以说平衡因子为0
	{}

};

AVL树的插入

AVL树的插入满足下面几个步骤

  1. 遵循二叉搜索树的插入原则 找到待插入位置
  2. 找到插入位置之后 插入节点
  3. 更新平衡因子 如果平衡因子不满足AVL树的条件 则使之满足

第一步第二步很简单 如果还有不明白的同学可以参考这篇博客

二叉搜索树的实现

接下来我们重点学习第三步

由于我们插入一个节点之后 只有可能是这个节点的父节点的高度受到影响

所以说我们只需要更新它的祖先节点 这也是我们为什么设计成三叉链结构的原因 - 方便找祖先节点

在这里插入图片描述
(这个图稍微有点漏洞 我们这里假设新增的节点是6.5 前面的节点都是浮点数)

新增节点之后我们开始更新平衡因子 遵循下面的规则

  1. 如果新增节点在父节点的右边 则平衡因子++
  2. 如果新增节点在父节点的左边 则平衡因子–

在这里插入图片描述
这里我们可以发现 如果有一个父节点的平衡因子更新成了0 则无需向上继续更新了

怎么解释这个现象呢?

我们都知道 平衡因子是根据父节点左右子树的高度来确定的

如果父节点的平衡因子被更新成了0 那么肯定就说明 原来有一边的高度是大于另一边的 而且是大于1

插入一个新的节点之后两边的高度差就被填平了

那么对于父节点的父节点来说 这边子树的高度并没有变化 所以说我们不需要向上更新了

在这里插入图片描述
所以说我们有具体的规则如下

  • 如果父节点的平衡因子更新之后结果为1或者-1 则需要继续向上更新祖先节点的平衡因子
  • 如果父节点的平衡因子更新之后结果为0 则不需要继续向上更新祖先节点的平衡因子
  • 如果父节点的平衡因子更新之后结果为2或者-2 则需要进行旋转操作来处理异常

我们假设目前的节点是cur 它的父节点是parent

因为我们执行的是插入操作 所以说我们的节点肯定是增加的 就看增加的是父节点的左子树还是右子树

因为我们不能确定是左子树还是右子树 所以要执行判断

  • 如果cur是parent的左节点 则平衡因子–
  • 如果cur是parent的右节点 则平衡因子++

但是到这里还没完 我们要需要继续向上更新 执行下面的代码

cur = parent;
parent = parent->_parent;

最差的情况下 我们一直要更新到根节点

按照上面的规则 我们写出代码

	bool insert(const pair <K,V>& kv)
	{
		if (_root == nullptr) // 如果AVL树为空 则直接为其根节点
		{
			_root = new Node(kv);
			return true;
		}

		// 如果不为空 则按照二叉树的插入方式 通过key值 即first来比较
		Node* cur = _root; // cur确定当前位置
		Node* parent = nullptr; // parent最后连接用 
		while (cur) // 当cur不为空的时候一直走
		{
			if (kv.first > cur->_kv.first) // 如果插入值大于当前节点 向右移
			{
				cur = cur->_right;
				parent = cur;
			}
			else if (kv.first < cur->_kv.first) // 如果插入值小于当前节点 向右移
			{
				cur = cur->_left;
				parent = cur;
			}
			else // 到这里这种情况说明等于 则插入失败 返回false
			{
				return false;
			}
		}

		// 将待插入节点插入到树中
		cur = new Node(kv);
		// 根据kv的key值来分辨插入的是左子树还是右子树
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent; // 因为是三叉链 所以要更新父节点
		}
		else // 因为走到这里了kv的key值不可能相等 所以说else就表示小于
		{
			parent->_left = cur;
			cur->_parent = parent; // 因为是三叉链 所以要更新父节点
		}


		// 更新平衡因子
		while (cur != _root) // 最坏的情况 一直更新到根节点 但是cur不可能到根节点
		{
			// 如果是右则平衡因子++ 反之则--
			if (cur = parent->_left)
			{
				parent->_bf--;
			}
			else if (cur = parent->_right)
			{
				parent->_bf++;
			}

			// 平衡因子更新完毕 此时开始查看父亲节点的平衡因子是多少 是否结束 是否需要旋转
			if (parent->_bf == 0) // 如果父亲的平衡因子是0 则停止更新 直接返回
			{
				return true;
			}
			else if (parent->_bf == 1 || parent->_bf == -1) // 如果父亲的平衡因子是1或-1 则继续往上更新
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) // 如果父亲的平衡因子是2或-2 则进行旋转操作
			{
				// 旋转
			}


		}
	}

省略了旋转操作的插入代码就在上面 接下来我们重点研究下AVL的旋转操作

AVL树的旋转

首先我们要明确一点 当parent节点的平衡因子是2或者-2时候 cur的平衡因子必然是1或者-1

因为如果cur的平衡因子是0 有两种情况

  1. cur为新增节点 这个时候由于我们插入的是AVL树 所以其父节点只有可能是1 -1 或者0
  2. cur为更新后的节点 此时cur为其子节点的父节点 其平衡因子更新到了0 不会继续向上更新 所以cur的父节点一定是有正常的平衡因子 1 -1 或者0

所以说我们这里就有四种情况

  • parent的平衡因子是2 cur的平衡因子是1
  • parent的平衡因子是2 cur的平衡因子是-1
  • parent的平衡因子是-2 cur的平衡因子是1
  • parent的平衡因子是-2 cur的平衡因子是-1

然后我们根据这四种情况来讨论如何旋转

左单旋

在这里插入图片描述
从上图我们可以观察发现 parent的平衡因子被更新为了2 此时肯定要触发旋转

旋转后的图片大概是这样子

在这里插入图片描述

经过旋转后 平衡因子全部恢复正常 并且此时将这颗二叉树作为一个整体 它的高度还是h+2

所以不用继续往上更新平衡因子了

那么我们具体来看看实现左单旋要分几步

  1. 让 subr 左子树 subrl 成为 parent 的右子树
  2. 让 parent 成为 subr 的左子树
  3. 让 subr 作为整个子树的根 (如果parent不是根节点的话)
  4. 更新平衡因子

此时 subr 和 parent 的平衡因子都更新为0

而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的

此外 观察第一张图我们可以知道

左单旋对应着 parent的平衡因子是2 cur的平衡因子是1 这种情况

我们左单旋的代码表示如下

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


		// 建立subR和parent的联系
		subR->_left = parent;
		parent->_parent = subR;

		// 建立parent和subRL的联系
		parent->_right = subRL;
		if (subRL) // 有可能subRL为空
		{
			subRL->_parent = parent;
		}

		// 建立grandparent和sunR的联系
		// 这里要分grandparent是否存在以及parent是grandparent的左还是右
		if (grandparent) // 如果进去了 则说明grandparent存在 
		{
			// 如果存在 要分清楚parent在左边还是右边
			if (grandparent->_left == parent)
			{
				grandparent->_left = subR;
			}
			else
			{
				grandparent->_right = subR;
			}
			subR->_parent = grandparent;
		}
		// 否则就是不存在 我们更新_root 并且将subR的父节点置空
		else
		{
			_root = subR;
			subR->_parent = nullptr;
		}


		// 更新平衡因子
		subR->_bf = 0;
		parent->_bf = 0;
	}

只要按照上面的步骤一行行写 应该是不难理解的

这里尤其要注意的是 我们使用的是不熟悉的三叉链结构

右单旋

在这里插入图片描述
从上图我们可以观察发现 parent的平衡因子被更新为了-2 此时肯定要触发旋转

旋转后的图片大概是这样子

在这里插入图片描述
经过旋转后 平衡因子全部恢复正常 并且此时将这颗二叉树作为一个整体 它的高度还是h+2

所以不用继续往上更新平衡因子了

那么我们具体来看看实现右单旋要分几步

  1. 让 subl 的右子树 sublr 成为 parent 的左子树
  2. 让 parent 成为 subl 的左子树
  3. 让 subr 作为整个子树的根 (如果parent不是根节点的话)
  4. 更新平衡因子

此时 subl 和 parent 的平衡因子都更新为0

而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的

此外 观察第一张图我们可以知道

右单旋对应着 parent 的平衡因子是-2 cur 的平衡因子是-1 这种情况

我们右单旋的代码表示如下

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


		// 建立subL和parent之间的联系
		subL->_right = parent;
		parent->_parent = subL;

		// 建立parent和subLR之间的联系 
		parent->_left = subLR;
		if (subLR) // subLR可能不存在 
		{
			subLR->_parent = parent;
		}

		// 建立grandparent和subl之间的联系
		// 这里要分grandparent是否存在以及parent是grandparent的左还是右
		if (grandparent)
		{
			// 如果存在 要分清楚parent在左边还是右边
			if (grandparent->_left == parent)
			{
				grandparent->_left = subL;
			}
			else
			{
				grandparent->_right = subL;
			}

			subL->_parent = grandparent;
		}
		else // 这里就是表示parent就是根节点
		{
			_root = subL;
			subL->_parent = nullptr;
		}


		// 更新平衡因子
		subL->_bf = 0;
		parent->_bf = 0;
	}

左右双旋

在这里插入图片描述

左右双旋一般分三步走 图片表示如下

第一步 插入新增节点 触发旋转机制

在这里插入图片描述

第二步 以30为旋转点进行左单旋

在这里插入图片描述
第三步 以90为旋转点进行右单旋

在这里插入图片描述
左右双旋的步骤如下

  1. 以subL为旋转点进行左单旋
  2. 以parent为旋转点进行右单旋
  3. 更新平衡因子

而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的

此外 观察第二张图我们可以知道

左右双旋对应着 parent 的平衡因子是-2 cur 的平衡因子是1 这种情况

左右双旋后 平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况

  1. 当sublr的平衡因子是-1时 左右双旋后parent subl sublr的平衡因子分别更新为 1 0 0
  2. 当sublr的平衡因子是 1时 左右双旋后parent subl sublr的平衡因子分别更新为 0 -1 0
  3. 当sublr的平衡因子是 0时 左右双旋后parent subl sublr的平衡因子分别更新为 0 0 0

产生这三种情况的原因分别是

插入的节点可能是sublr 的左子树 右子树 或者是sublr本身

代码表示如下

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = parent->_left->_right;
		int bf = subLR->_bf;

		// 以subL作为旋转点左单旋
		RotateL(subL);


		// 以parent作为旋转点右单旋
		RotateR(parent);

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

右左单旋

在这里插入图片描述

右左双旋一般分三步走 图片表示如下

第一步 插入新增节点 触发旋转机制

在这里插入图片描述

第二步 以90作为节点进行右单旋

在这里插入图片描述

第三步 以30作为节点进行左单旋

在这里插入图片描述

右左双旋的步骤如下

  1. 以subR为旋转点进行右单旋
  2. 以parent为旋转点进行左单旋
  3. 更新平衡因子

而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的

此外 观察第二张图我们可以知道

右左双旋对应着 parent 的平衡因子是2 cur 的平衡因子是-1 这种情况

右左双旋后 平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况

  1. 当subrl的平衡因子是-1时 左右双旋后parent subr subrl的平衡因子分别更新为 1 0 0
  2. 当subrl的平衡因子是-1时 左右双旋后parent subr subrl的平衡因子分别更新为 0 -1 0
  3. 当subrl的平衡因子是-1时 左右双旋后parent subr subrl的平衡因子分别更新为 0 0 0

产生这三种情况的原因分别是

插入的节点可能是subrl 的左子树 右子树 或者是subrl本身

代码表示如下

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		//  以subR为轴进行右单旋
		RotateR(subR);

		//  以parent为轴进行左单旋
		RotateL(parent);

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

AVL树的验证

首先AVL树是一颗二叉搜索树 所以说它满足中序遍历后是有序的这个原则 我们可以使用中序遍历来验证

代码表示如下

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


		_Inorder(root->_left);
		cout << root->_kv.first << " ";
		_Inorder(root->_right);
	}

	void Inorder()
	{
		_Inorder(_root);
	}

但是中序遍历只能证明这棵树是二叉搜索树 要证明它是AVL树我们必须还要证明它的平衡因子全部无异常

我们可以从根节点开始 往上遍历每个子树来判断它是否是AVL树 如果全部是AVL树 则可以验证

如果有一颗不是 则证明不是AVL树

//判断是否为AVL树
bool IsAVLTree()
{
	int hight = 0; //输出型参数
	return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
	if (root == nullptr) //空树是平衡二叉树
	{
		hight = 0; //空树的高度为0
		return true;
	}
	//先判断左子树
	int leftHight = 0;
	if (_IsBalanced(root->_left, leftHight) == false)
		return false;
	//再判断右子树
	int rightHight = 0;
	if (_IsBalanced(root->_right, rightHight) == false)
		return false;
	//检查该结点的平衡因子
	if (rightHight - leftHight != root->_bf)
	{
		cout << "平衡因子设置异常:" << root->_kv.first << endl;
	}
	//把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
	hight = max(leftHight, rightHight) + 1;
	return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}

AVL树的查找

AVL树的查找方式与二叉搜索树相同

  1. 如果树为空树 则查找失败 返回nullptr
  2. 如果key值小于当前节点 则进入当前节点的子树查找
  3. 如果key值大于当前节点 则进入当前节点的子树查找
  4. 如果key值等于当前节点 则返回当前节点

代码表示如下

	Node* find(const K& key)
	{
		Node* cur = _root;

		while (cur) // 一直查找到空为止
		{
			if (key < cur->_kv.first) // 小于
			{
				cur = cur->_left;
			}
			else if (key < cur->_kv.first) // 大于
			{
				cur = cur->_right;
			}
			else // 等于
			{
				return cur
			}
		}
		// 走到这里就是没找到
		return nullptr;
	}

AVL树的修改

我们所说的修改一般是值对于value值的修改

一般步骤如下

  1. 找到对应的key值的结点
  2. 对其value值进行修改

代码表示如下

	bool Modify(const K& key, const V& value)
	{
		Node* ret = find(key);
		// 有可能不存在
		if (ret == nullptr)
		{
			return false;
		}
		ret->_kv.second = value;
		return true;
	}

AVL树的删除

AVL树的删除满足下面几个步骤

  1. 遵循二叉搜索树的删除原则 找到待删除位置
  2. 找到删除位置之后 删除节点
  3. 更新平衡因子 如果平衡因子不满足AVL树的条件 则使之满足

由于我们学习AVL树主要是为了后面的红黑树做铺垫 需要重点掌握的是AVL树的插入

并且AVL树的删除在平时的笔试面试中考察的很少 或者说几乎没有考察 所以同学们可以跳过这一部分的学习

AVL树的性能

AVL树是一颗近乎完全二叉树的二叉搜索树 所以说它查询的时间复杂度非常低 为Log N

但是如果要对于AVL树做一些结构修改的操作 它的性能就会十分低下 比如在插入和删除时 可能要经过大量的旋转操作

因此 如果是需要一种查询高效的数据结构 AVL树是十分合适的
但是 如果它经常要被修改 AVL树的性能就可能不太匹配了

相关文章:

  • 正确的清理内存方式,才能让你的空间更加充裕
  • 关于sql注入这一篇就够了(适合入门)
  • 【Linux学习】进程控制
  • Springboot中如何优雅的写好controller层代码
  • Elasticsearch:Go 客户端简介 - 8.x
  • Vue--》详解状态管理工具——Vuex
  • 基于Android的物业管理app
  • C++STL——list类与模拟实现
  • 基于java springboot+mybatis学生学科竞赛管理管理系统设计和实现
  • 分布式理论协议与算法 第三弹 BASE理论
  • Ubuntu18.04下安装编译文件压缩相关的zlib和quazip库文件
  • 新年快到了,教大家用汇编语言显示Happy New Year!
  • CSS 这个就叫优雅 | 多行文本溢出省略
  • 【Linux】Linux调试器——gdb的使用以及一些指令
  • STM32MP157驱动开发——Linux下的单总线驱动
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • 0基础学习移动端适配
  • CODING 缺陷管理功能正式开始公测
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • Java面向对象及其三大特征
  • leetcode46 Permutation 排列组合
  • PermissionScope Swift4 兼容问题
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • spring + angular 实现导出excel
  • spring-boot List转Page
  • Spring框架之我见(三)——IOC、AOP
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 理解IaaS, PaaS, SaaS等云模型 (Cloud Models)
  • 聊聊flink的TableFactory
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • hi-nginx-1.3.4编译安装
  • 阿里云移动端播放器高级功能介绍
  • 湖北分布式智能数据采集方法有哪些?
  • #《AI中文版》V3 第 1 章 概述
  • #我与Java虚拟机的故事#连载12:一本书带我深入Java领域
  • (2022 CVPR) Unbiased Teacher v2
  • (二)c52学习之旅-简单了解单片机
  • (仿QQ聊天消息列表加载)wp7 listbox 列表项逐一加载的一种实现方式,以及加入渐显动画...
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (十二)springboot实战——SSE服务推送事件案例实现
  • (转)Windows2003安全设置/维护
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
  • .NET Core 2.1路线图
  • .net core 微服务_.NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端
  • .net core 依赖注入的基本用发
  • .NET 动态调用WebService + WSE + UsernameToken
  • .Net(C#)自定义WinForm控件之小结篇
  • @Autowired多个相同类型bean装配问题
  • [2013][note]通过石墨烯调谐用于开关、传感的动态可重构Fano超——
  • [20150629]简单的加密连接.txt