【数据结构进阶】AVL树深度剖析 + 实现(附源码)

🌟🌟作者主页:ephemerals__
🌟🌟所属专栏:数据结构

目录

前言

一、AVL树的概念

二、AVL树底层解析及实现

1. 节点的定义

2. 接口声明

3. AVL树的插入

3.1 更新平衡因子

3.2 旋转(重点)

右单旋

左单旋

左右双旋

右左双旋

旋转种类的选取判定

3.3 总代码

4. AVL树的查找

5. 获取节点个数

6. 中序遍历、拷贝构造和析构

7. 测试

8. 程序全部代码

三、key/key_value搜索场景分析 

1. key

2. key_value

总结


前言

本篇文章主要内容:AVL树底层结构解析、c++代码实现以及key/key_value场景分析。

        之前,我们学习了一种特殊的二叉树结构——二叉搜索树。它最大的好处在于,能够在平均情况下提供O(log n)时间复杂度的查找、插入和删除操作。然而,当数据的插入顺序与理想情况大相径庭时,传统的二叉搜索树可能会退化成接近单支树的形态,导致其查找效率骤降至O(n)。为了弥补这一缺陷,计算机科学界孕育出了一种更加精巧的数据结构——AVL树。AVL树通过引入平衡因子的概念,确保在每次插入或删除操作后,左右子树的高度差达到最小,从而保证了最优的O(log n)查找效率。本文将带你深入探索AVL树的奥秘,见证它是如何在维护平衡的同时,优雅地解决了传统二叉搜索树的缺陷。

如果你不是很了解二叉搜索树,可以参阅这篇文章:

【数据结构】二叉搜索树(二叉排序树)-CSDN博客

        之前我们实现二叉搜索树时,将键(key)作为节点的数据部分。而在本次实现AVL树的过程中,我们将进行 “改进” ,使用键值对(key_value)作为节点数据域。

一、AVL树的概念

        AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,是一种自平衡二叉搜索树,能够在进行插入或删除操作后,保持平衡状态,维持较高的查找效率。

它或者是一棵空树,或者具有以下特点:

1. 它是一棵二叉搜索树

2. 它左右子树的高度差不超过1。(无法保证做到高度一致)

3. 它的每一棵子树也符合前两点特性(每一棵子树都是AVL树)。

        一般情况下,AVL树通过平衡因子来检查左右子树是否达到平衡。每一个节点都带有一个平衡因子,平衡因子的值等于该节点右子树的高度减去左子树的高度。当平衡因子的值为-1/0/1时,表明以该节点为根的树达到平衡。

        由于AVL树能够持续保持平衡(所有平衡因子的值都在合法范围内),所以其高度可以控制在log n级别,保证了最优的查找效率。

二、AVL树底层解析及实现

1. 节点的定义

//节点
template<class K, class V>
struct AVLTNode
{
	pair<K, V> _kv;//键值对--数据域
	AVLTNode<K, V>* _left;//指向左孩子的指针
	AVLTNode<K, V>* _right;//指向右孩子的指针
	AVLTNode<K, V>* _parent;//指向父亲的指针
	int _bf;//平衡因子

	//节点构造
	AVLTNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

AVL树的节点包含五个成员变量:pair(键值对,存储key值和value值)指向左孩子的指针指向右孩子的指针指向父亲节点的指针以及平衡因子bf。不难看出,AVL树的节点是一种三叉链结构,更有利于节点之间的控制访问。

        这里我们简单介绍一下pair

pair是c++标准库自带的一种类型,定义在<utility>头文件中,用于表示二元组,该类将一对不同类型的值(T1和T2)耦合在一起。单个值可以通过它的公有成员firstsecond来访问。

代码示例:

#include <iostream>
#include <utility>
using namespace std;

int main()
{
	pair<int, char> pr1;//无参构造
	pair<int, char> pr2 = { 1,'w' };//初始化器构造
	pair<int, char> pr3(pr2);//拷贝构造

	pr1 = pr2;//支持赋值重载
	pr1.swap(pr2);//支持交换
	pr1 == pr2;//支持大小比较,比较规则是 先比第一个成员,再比第二个成员

	cout << pr1.first;//访问第一个成员
	cout << pr1.second;//访问第二个成员

	return 0;
}

2. 接口声明

        需要实现的接口如下: 

//AVL树
template<class K, class V>
class AVLTree
{
	typedef AVLTNode<K, V> Node;//重命名简化代码
public:
	//强制生成无参构造
	AVLTree() = default;

	//拷贝构造
	AVLTree(const AVLTree<K, V>& t);

	//析构
	~AVLTree();

    //中序遍历
    void Inorder();

	//插入
	bool Insert(const pair<K, V>& kv);

	//查找
	Node* Find(const K& key);

	//获取节点个数
	size_t Size();
private:
	Node* _root = nullptr;//根节点指针
	size_t _size = 0;//节点个数
};

3. AVL树的插入

        AVL树插入的总体过程是:首先按照二叉搜索树的规则进行插入,然后根据插入的位置更新父亲的平衡因子。更新过程中,判断平衡因子是否合法,若不合法,则需要通过旋转来调整树的结构,使之维持平衡。

3.1 更新平衡因子

        首先探讨平衡因子的更新方法(设cur是新节点,parent是新节点的父亲):

1. 由于平衡因子的值为右子树的高度减去左子树的高度,所以:

        当cur是parent的左孩子,则parent的平衡因子 - 1;

        当cur是parent的右孩子,则parent的平衡因子 + 1。

2. parent的平衡因子更新之后,以parent为根节点的树的高度可能发生变化,此时需要根据平衡因子的值来判断是否继续向祖先方向更新平衡因子。判断规则如下:

        •  parent的平衡因子为0(则更新之前平衡因子为-1/1),以parent为根的树的高度不变,无需向上更新。

        •  parent的平衡因子为1/-1(则更新之前平衡因子为0),以parent为根的树的高度 + 1,需要向上更新,直到某个节点平衡因子为0为止。

        •  parent的平衡因子为2/-2(则更新之前平衡因子为1/-1),此时以parent为根的树已经不平衡,需要进行旋转

总结:

1. 不难发现,向上更新是一个循环的过程。停止更新的条件是:更新完根节点或某个节点平衡因子更新后的值为0

2. 向上更新时,我们需要让cur指向当前parent节点,而让parent指向其父亲节点,此时三叉链结构的便利性得到体现。

3. 更新过程当中,一旦发现某平衡因子的值为2/-2,就需要旋转

节点插入和更新平衡因子代码实现:

//插入
bool Insert(const pair<K, V>& kv)
{
	//根节点为空,直接插入
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_size++;
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	//寻找合适位置
	while (cur)
	{
		if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}

	//让cur的parent指针指向父亲
	cur->_parent = parent;

	//更新平衡因子
	while (parent)
	{
		//根据插入节点的位置更新父亲的平衡因子
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		else
		{
			parent->_bf++;
		}

		//判断平衡因子的值
		if (parent->_bf == 0)//等于0,停止更新
		{
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)//等于1,继续向上更新
		{
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -1)//等于2,需要旋转
		{
			//旋转

			break;
		}
		else //不可能有其他情况,走到这里直接断言报错
		{
			assert(false);
		}
	}
	_size++;
	return true;
}

这里需要注意两点:

1. 由于数据类型是键值对,所以需要进行比较的是键的大小,跟值无关。

2.  与之前实现二叉搜索树相同,我们默认不支持插入重复键。

3.2 旋转(重点)

        接下来主要介绍AVL树的旋转。

当某平衡因子的值为2/-2时,需要旋转。

旋转原则在保持二叉搜索树特性的基础上,尽量降低树的高度,使树保持平衡

旋转主要分为四种,分别是:右单旋、左单旋、左右双旋、右左双旋

右单旋

        当新节点位于较高子树的侧时,进行右单旋

如图,这里的abc都是抽象形式,表示的是高度为h的子树(h>=0)

由于节点2的存在,左子树比右子树高1,所以节点6的平衡因子为-1。

当插入一个新节点,使得 的高度由h变为h+1时,节点2的平衡因子就变为-1,节点6的平衡因子变为-2

新节点使得 的高度增1,其位于较高左子树的左侧,将节点6作为旋转点,进行右单旋

右单旋的过程:如图所示,将节点6标记为parent,节点2标记为subLb标记为subLR。然后将parent的左指针指向subLR,subL的右指针指向parent,subL成为该部分的新根。

(这里的subL含义为左孩子,subLR为左孩子的右孩子)

旋转完成之后,整个体系达到平衡,将subL和parent的平衡因子设置为0。

        AVL树是三叉链结构,实际代码编写过程远比描述复杂,有许多细节需要处理。

代码实现:

//右单旋
void RotateR(Node* parent)
{
	//先标记subL和subLR
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;//让parent的左指针指向subLR
	
	if (subLR) subLR->_parent = parent;//让subLR的父指针指向parent。注意subLR可能为空,需要判断

	//标记parent的父亲
	Node* ppNode = parent->_parent;

	subL->_right = parent;//让subL的右指针指向parent
	parent->_parent = subL;//parent的父指针指向subL

	//处理ppNode和subL的关系
	if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
	{
		subL->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent) ppNode->_left = subL;
		else ppNode->_right = subL;
		subL->_parent = ppNode;
	}

	//调整平衡因子
	parent->_bf = 0;
	subL->_bf = 0;
}
左单旋

        当新节点位于较高子树的侧时,进行左单旋

由于节点8的存在,右子树比左子树高1,所以节点3的平衡因子为1。

当插入一个新节点,使得 的高度由h变为h+1时,节点8的平衡因子就变为1,节点3的平衡因子变为2

新节点使得 的高度增1,其位于较高右子树的右侧,将节点3作为旋转点,进行左单旋

左单旋的过程:如图所示,将节点3标记为parent,节点8标记为subRb标记为subRL。然后将parent的右指针指向subRL,subR的左指针指向parent,subR成为该部分的新根。

(这里的subR含义为右孩子,subRL为右孩子的左孩子)

旋转完成之后,整个体系达到平衡,将subR和parent的平衡因子设置为0。

代码实现:

//左单旋
void RotateL(Node* parent)
{
	//先标记subR和subRL
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;//让parent的右指针指向subRL

	if (subRL) subRL->_parent = parent;//让subRL的父指针指向parent。注意subRL可能为空,需要判断

	//标记parent的父亲
	Node* ppNode = parent->_parent;

	subR->_left = parent;//让subR的左指针指向parent
	parent->_parent = subR;//parent的父指针指向subR

	//处理ppNode和subR的关系
	if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
	{
		subR->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent) ppNode->_left = subR;
		else ppNode->_right = subR;
		subR->_parent = ppNode;
	}

	//调整平衡因子
	parent->_bf = 0;
	subR->_bf = 0;
}
左右双旋

        当新节点位于较高子树的侧时,进行左右双旋

如图,新节点使得 的高度增1,其位于较高左子树的右侧,进行一次右单旋显然不能达到平衡。所以插入之后需要将subL作为旋转点,进行一次左单旋;然后将parent作为旋转点,进行一次右单旋

双旋转操作可以直接调用单旋转实现,但平衡因子的调节是最棘手的问题。

我们对 b 进行细化。

场景1b(subLR)的平衡因子为0b为插入的新节点)。

两次旋转之后,subL和parent的左右子树高度差相同,平衡因子均调整为0。

场景2b(subLR)的平衡因子为-1

两次旋转之后,subLR的左右子树高度一致,平衡因子调整为0;parent的右子树比左子树高1,平衡因子调整为1。

场景3b(subLR)的平衡因子为1。

两次旋转之后,subLR的左子树比右子树高1,平衡因子调整为-1;parent的左右子树高度一致,平衡因子调整为0。

左右双旋代码实现:

//左右双旋
void RotateLR(Node* parent)
{
	//先标记subL和subLR
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	//记录subLR的平衡因子
	int bf = subLR->_bf;

	RotateL(subL);//将subL作为旋转点,进行一次左单旋
	RotateR(parent);//将parent作为旋转点,进行一次右单旋

	//调整平衡因子
	if (bf == 0)//场景1
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)//场景2
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)//场景3
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else //不可能有其他情况,走到这里直接断言报错
	{
		assert(false);
	}
}
右左双旋

        当新节点位于较高子树的侧时,进行右左双旋

如图,新节点位于较高右子树的左侧时,一次左单旋显然无法达到平衡。需要将subR作为旋转点,进行一次右单旋;然后将parent作为旋转点,进行一次左单旋

与左右双旋相同,右左双旋也需考虑平衡因子调节的问题。这里的情况与左右双旋相似,不再进行详细讲解。

场景1b(subRL)的平衡因子为0b为插入的新节点)。

场景2: b(subRL)的平衡因子为-1

场景3b(subRL)的平衡因子为1。

右左双旋代码实现:

//右左双旋
void RotateRL(Node* parent)
{
	//先标记subR和subRL
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//记录subRL的平衡因子
	int bf = subRL->_bf;

	RotateR(subR);//将subR作为旋转点,进行一次右单旋
	RotateL(parent);//将parent作为旋转点,进行一次左单旋

	//调整平衡因子
	if (bf == 0)//场景1
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 0;
	}
	else if (bf == -1)//场景2
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 1)//场景3
	{
		parent->_bf = -1;
		subRL->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}
旋转种类的选取判定

        在代码中,需要根据特定条件来判断何时使用右单旋、何时使用左右双旋等操作。从四种旋转的图示可以看出,我们可以使用插入新节点之后parent节点和subL/subR节点的平衡因子作为判定原则。

右单旋:parent平衡因子为-2;subL平衡因子为-1。

左单旋:parent平衡因子为2;subR平衡因子为1。

左右双旋:parent平衡因子为-2;subL平衡因子为1。

右左双旋:parent平衡因子为2;subR平衡因子为-1。

注:这里的subL、subR在Insert函数中都可以用cur来表示。

3.3 总代码

        AVL树插入操作的全部代码如下:

//插入
bool Insert(const pair<K, V>& kv)
{
	//根节点为空,直接插入
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_size++;
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	//寻找合适位置
	while (cur)
	{
		if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}

	//让cur的parent指针指向父亲
	cur->_parent = parent;

	//更新平衡因子
	while (parent)
	{
		//根据插入节点的位置更新父亲的平衡因子
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		else
		{
			parent->_bf++;
		}

		//判断平衡因子的值
		if (parent->_bf == 0)//等于0,停止更新
		{
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)//等于1,继续向上更新
		{
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -1)//等于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);
		}
	}
	_size++;
	return true;
}

//右单旋
void RotateR(Node* parent)
{
	//先标记subL和subLR
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;//让parent的左指针指向subLR
	
	if (subLR) subLR->_parent = parent;//让subLR的父指针指向parent。注意subLR可能为空,需要判断

	//标记parent的父亲
	Node* ppNode = parent->_parent;

	subL->_right = parent;//让subL的右指针指向parent
	parent->_parent = subL;//parent的父指针指向subL

	//处理ppNode和subL的关系
	if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
	{
		subL->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent) ppNode->_left = subL;
		else ppNode->_right = subL;
		subL->_parent = ppNode;
	}

	//调整平衡因子
	parent->_bf = 0;
	subL->_bf = 0;
}

//左单旋
void RotateL(Node* parent)
{
	//先标记subR和subRL
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;//让parent的右指针指向subRL

	if (subRL) subRL->_parent = parent;//让subRL的父指针指向parent。注意subRL可能为空,需要判断

	//标记parent的父亲
	Node* ppNode = parent->_parent;

	subR->_left = parent;//让subR的左指针指向parent
	parent->_parent = subR;//parent的父指针指向subR

	//处理ppNode和subR的关系
	if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
	{
		subR->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent) ppNode->_left = subR;
		else ppNode->_right = subR;
		subR->_parent = ppNode;
	}

	//调整平衡因子
	parent->_bf = 0;
	subR->_bf = 0;
}

//左右双旋
void RotateLR(Node* parent)
{
	//先标记subL和subLR
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	//记录subLR的平衡因子
	int bf = subLR->_bf;

	RotateL(subL);//将subL作为旋转点,进行一次左单旋
	RotateR(parent);//将parent作为旋转点,进行一次右单旋

	//调整平衡因子
	if (bf == 0)//场景1
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)//场景2
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)//场景3
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else //不可能有其他情况,走到这里直接断言报错
	{
		assert(false);
	}
}

//右左双旋
void RotateRL(Node* parent)
{
	//先标记subR和subRL
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//记录subRL的平衡因子
	int bf = subRL->_bf;

	RotateR(subR);//将subR作为旋转点,进行一次右单旋
	RotateL(parent);//将parent作为旋转点,进行一次左单旋

	//调整平衡因子
	if (bf == 0)//场景1
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 0;
	}
	else if (bf == -1)//场景2
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 1)//场景3
	{
		parent->_bf = -1;
		subRL->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

4. AVL树的查找

        AVL树的查找逻辑与传统二叉搜索树完全相同。需要注意:该函数通过key值查找。

//查找
Node* Insert(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		//比较key值
		if (key < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else if (key > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else
		{
			return cur;
		}
	}
	return nullptr;
}

5. 获取节点个数

        直接返回成员_size的值。

//获取节点个数
size_t Size()
{
	return _size;
}

6. 中序遍历、拷贝构造和析构

        三个函数的实现逻辑与传统二叉搜索树相同。注意中序遍历需要打印key值和value值。

//中序遍历
void Inorder()
{
	_Inorder(_root);
}
void _Inorder(Node* root)
{
	if (root == nullptr) return;
	_Inorder(root->_left);
	cout << root->_kv.first << ' ' << root->_kv.second << endl;
	_Inorder(root->_right);
}
//拷贝构造
AVLTree(const AVLTree<K, V>& t)
{
	_root = _Copy(t._root);
    _size = t._size;
}
Node* _Copy(Node* root)
{
	if (root == nullptr) return nullptr;
	Node* NewRoot = new Node(root->_kv);
	NewRoot->_left = _Copy(root->_left);
	NewRoot->_right = _Copy(root->_right);
	return NewRoot;
}
//析构
~AVLTree()
{
	_Destroy(_root);
}
void _Destroy(Node* root)
{
	if (root == nullptr) return;
	_Destroy(root->_left);
	_Destroy(root->_right);
	delete root;
}

7. 测试

        写一段代码测试我们实现的AVL树:

int main()
{
	AVLTree<int, int> t;
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

	for (auto& e : a)
	{
		t.Insert({ e,e });
	}

	cout << "size:" << t.Size() << endl;

	t.Inorder();
	return 0;
}

运行结果:

8. 程序全部代码

        AVL树实现全部代码:

#include <iostream>
#include <cassert>
using namespace std;

//节点
template<class K, class V>
struct AVLTNode
{
	pair<K, V> _kv;//键值对--数据域
	AVLTNode<K, V>* _left;//指向左孩子的指针
	AVLTNode<K, V>* _right;//指向右孩子的指针
	AVLTNode<K, V>* _parent;//指向父亲的指针
	int _bf;//平衡因子

	//节点构造
	AVLTNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

//AVL树
template<class K, class V>
class AVLTree
{
	typedef AVLTNode<K, V> Node;//重命名简化代码
public:
	//强制生成无参构造
	AVLTree() = default;

	//拷贝构造
    AVLTree(const AVLTree<K, V>& t)
    {
	    _root = _Copy(t._root);
        _size = t._size;
    }

	//析构
	~AVLTree()
	{
		_Destroy(_root);
	}

	//中序遍历
	void Inorder()
	{
		_Inorder(_root);
	}

	//插入
	bool Insert(const pair<K, V>& kv)
	{
		//根节点为空,直接插入
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_size++;
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		//寻找合适位置
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		//让cur的parent指针指向父亲
		cur->_parent = parent;

		//更新平衡因子
		while (parent)
		{
			//根据插入节点的位置更新父亲的平衡因子
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			//判断平衡因子的值
			if (parent->_bf == 0)//等于0,停止更新
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)//等于1,继续向上更新
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -1)//等于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);
			}
		}
		_size++;
		return true;
	}

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

	//获取节点个数
	size_t Size()
	{
		return _size;
	}
private:
	//右单旋
	void RotateR(Node* parent)
	{
		//先标记subL和subLR
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;//让parent的左指针指向subLR

		if (subLR) subLR->_parent = parent;//让subLR的父指针指向parent。注意subLR可能为空,需要判断

		//标记parent的父亲
		Node* ppNode = parent->_parent;

		subL->_right = parent;//让subL的右指针指向parent
		parent->_parent = subL;//parent的父指针指向subL

		//处理ppNode和subL的关系
		if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
		{
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent) ppNode->_left = subL;
			else ppNode->_right = subL;
			subL->_parent = ppNode;
		}

		//调整平衡因子
		parent->_bf = 0;
		subL->_bf = 0;
	}

	//左单旋
	void RotateL(Node* parent)
	{
		//先标记subR和subRL
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;//让parent的右指针指向subRL

		if (subRL) subRL->_parent = parent;//让subRL的父指针指向parent。注意subRL可能为空,需要判断

		//标记parent的父亲
		Node* ppNode = parent->_parent;

		subR->_left = parent;//让subR的左指针指向parent
		parent->_parent = subR;//parent的父指针指向subR

		//处理ppNode和subR的关系
		if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
		{
			subR->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent) ppNode->_left = subR;
			else ppNode->_right = subR;
			subR->_parent = ppNode;
		}

		//调整平衡因子
		parent->_bf = 0;
		subR->_bf = 0;
	}

	//左右双旋
	void RotateLR(Node* parent)
	{
		//先标记subL和subLR
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//记录subLR的平衡因子
		int bf = subLR->_bf;

		RotateL(subL);//将subL作为旋转点,进行一次左单旋
		RotateR(parent);//将parent作为旋转点,进行一次右单旋

		//调整平衡因子
		if (bf == 0)//场景1
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)//场景2
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)//场景3
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else //不可能有其他情况,走到这里直接断言报错
		{
			assert(false);
		}
	}

	//右左双旋
	void RotateRL(Node* parent)
	{
		//先标记subR和subRL
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//记录subRL的平衡因子
		int bf = subRL->_bf;

		RotateR(subR);//将subR作为旋转点,进行一次右单旋
		RotateL(parent);//将parent作为旋转点,进行一次左单旋

		//调整平衡因子
		if (bf == 0)//场景1
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else if (bf == -1)//场景2
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)//场景3
		{
			parent->_bf = -1;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr) return;
		_Inorder(root->_left);
		cout << root->_kv.first << ' ' << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	Node* _Copy(Node* root)
	{
		if (root == nullptr) return nullptr;
		Node* NewRoot = new Node(root->_kv);
		NewRoot->_left = _Copy(root->_left);
		NewRoot->_right = _Copy(root->_right);
		return NewRoot;
	}

	void _Destroy(Node* root)
	{
		if (root == nullptr) return;
		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
	}

	Node* _root = nullptr;//根节点指针
	size_t _size = 0;//节点个数
};

三、key/key_value搜索场景分析 

1. key

       在编程中,key(键)在数组、平衡二叉树、哈希表或数据库索引结构中扮演重要角色。key的特别之处在于其唯一性,确保了高效的数据检索。

        key的搜索场景:即元素的数据域当中只存储key,key即为需要查找的值。搜索过程中,只需要判断key是否存在。在搜索树结构当中,只支持key的增、删、查,但不支持修改(会破坏搜索树结构)

举例:小区车库只允许购买车位的业主车进入,那么已购买车位的业主车牌号就会被录入后台系统,此时 “车牌号” 即为key。当车辆进入时,如果能检索到key值,则抬杆;否则无法进入车库。

2. key_value

        key-value(键值对)是一种基础且广泛使用的数据存储和检索方式。 在key_value模型中,每一个key都有与之对应的值value。

        key_value的搜索场景:元素的数据域中存储key及其对应的value。搜索过程中,不仅需要判断key是否存在,还要访问key存在时对应的value值。在搜索树结构当中,增、删、查操作需要以key为基准进行搜索,对于修改操作,只支持修改value值,不支持修改key

举例:简单的汉化词典,数据元素中存储英文单词和汉语意思,此时 “英文单词” 即为key,“汉语意思” 即为value。当输入英文单词,就能检索到对应的汉语意思;若单词不正确(如拼写错误),则搜索失败,抛出错误提示。

总结

        在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。通过引入键值对作为节点数据域,AVL树进一步丰富了数据操作的可能性,为实际应用提供了更为灵活和强大的支持。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/935268.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

黑马商城微服务复习(6)

MQ高级 1. 消息可靠性2. 发送者的可靠性1. 发送者问题2. 生产者重试机制3. 生产者确认机制4. MQ可靠性5. 消费者的可靠性 3. 延迟消息1. 定义2. 死信交换机 1. 消息可靠性 发送消息时丢失&#xff1a; 生产者发送消息时连接MQ失败生产者发送消息到达MQ后未找到Exchange生产者发…

深度优先搜索(DFS)与回溯法:从全排列到子集问题的决策树与剪枝优化

文章目录 前言&#x1f384;一、全排列✨核心思路✨实现步骤✨代码✨时间和空间复杂度&#x1f381;1. 时间复杂度&#x1f381;2. 空间复杂度 &#x1f384;二、子集✨解法一&#xff1a;逐位置决策法&#x1f381;步骤分析&#x1f381;运行示例&#x1f381;代码 ✨解法二&a…

STM32--中断

中断 中断向量表 定义一段固定的内存&#xff0c;以4字节对齐&#xff0c;存放各个中断服务函数程序的首地址。定义在启动文件中。 中断相关寄存器 内核中断不经过中断使能、除能寄存器。 中断优先级 1、抢占优先级&#xff1a;高高抢占优先级可以打断正在执行的低抢占优先…

AUTOSAR 汽车开放系统架构

AUTOSAR 官网 AUTOMOTIVE OPEN SYSTEM ARCHITECTURE AUTOSAR (AUTomotive Open System ARchitecture) is a global partnership of leading companies in the automotive and software industry to develop and establish the standardized software framework and open E/E …

《计算机视觉:瓶颈之辩与未来之路》

一、计算机视觉的崛起 计算机视觉是使用计算机模仿人类视觉系统的科学&#xff0c;让计算机拥有类似人类提取、处理、理解和分析图像以及图像序列的能力。它是一个多学科交叉的领域&#xff0c;与机器视觉、图像处理、人工智能、机器学习等领域密切相关。 计算机视觉行业可分为…

Vue 集成地图

电子地图应用广泛&#xff1a; 网约车 : 在网约车 场景中实现 准定位 、导航 、司乘同显 &#xff0c;精准计费 智慧物流、生活服务等&#xff0c;本专题课程囊括各类应用场景 学习 电子地图解决方案&#xff0c;满足学员工作学习各类需求。 基础知识 学习 集成 地图之前需…

Docker Compose实战三:轻松部署PHP

通过前面的文章&#xff08;Docker Compose基础语法与MySQL部署&#xff09;&#xff0c;你已经掌握了Docker Compose的基本语法和常用指令&#xff0c;并成功部署了一个MySQL数据库服务器。今天&#xff0c;我们将继续深入探索Docker Compose的强大功能&#xff0c;介绍如何使…

【深度学习】深刻理解“变形金刚”——Transformer

Transformer 是一种用于处理序列数据的深度学习模型架构&#xff0c;最初由 Vaswani 等人在 2017 年的论文《Attention is All You Need》中提出。它彻底改变了自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;成为许多高级任务&#xff08;如机器翻译、文本生成、问答…

基于springboot+大数据的校园数字图书馆系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了多年的设计程序开发&#xff0c;开发过上千套设计程序&#xff0c;没有什么华丽的语言&#xff0c;只有实…

Redis篇-9--数据结构篇1--五种基本结构(String,List,Set,Sorted Set,Hash,BLPOP阻塞逻辑)

Redis 是一个高性能的键值存储系统&#xff0c;支持多种数据结构。每种数据结构都有其独特的特点和适用场景。 1、String&#xff08;字符串&#xff09; &#xff08;1&#xff09;、特点 最简单的数据类型&#xff1a;字符串是最基本的数据类型&#xff0c;可以存储字符串…

优雅的@ObservedV2和@Trace装饰器

Hello&#xff0c;大家好&#xff0c;我是 V 哥。在HarmonyOS NEXT开发中&#xff0c;ObservedV2装饰器和Trace装饰器是用于状态管理的两个装饰器&#xff0c;它们在HarmonyOS应用开发中用于增强对类对象中属性的观测能力。如果你学过观察者模式的原理&#xff0c;你会更容易理…

物联网安全-ARMv8-M Trustzone 实操

前言 本文针对ARMv8m架构M23/M33 MCU安全特性使用进行介绍,以nxp LPC55xx系列和STM32L5xx系列为例,为大家阐述如何使用Trustzone技术提高物联网设备安全性,适合有一定平台安全基础的物联网设备开发人员、安全方案开发人员。 背景 为了提升平台安全性,ARM推出了ARMv8m架构…

昱感微“多维像素”多模态融合感知展示

昱感微采用“多维像素”多模态融合技术&#xff0c;将可见光摄像头、红外摄像头、4D毫米波雷达/激光雷达的探测数据以“多维像素”的数据格式输出&#xff1a;图像数据雷达探测数据红外传感器探测数据叠加&#xff0c;以摄像头像素为颗粒度组合全部感知数据&#xff0c;形成多模…

Launcher添加hotseat图标布局

Launcher的hotseat客户要求添加一些指定应用图标。 首先打开机器将要布局的图标手动移动到hotseat位置上面。 然后使用adb命令将data/data/com.android.launcher3/databases这个文件pull出来。这个文件夹是Luancher的数据库文件。里面保存了相关应用的图标信息。 使用SQLiteS…

GNSS误差源及差分定位

GNSS误差源&#xff1a; &#xff08;一&#xff09;卫星星历误差 由星历信息所得出的卫星位置坐标与实际位置坐标的偏差就是星历误差。星历信息是由 GPS 地面部分测量计算后传入空间部分的。由于卫星在运动中要受到各种摄动力的作用, 而地面部分又很难精确测量这些作用力,…

【数据结构——内排序】希尔排序(头歌实践教学平台习题)【合集】

目录&#x1f60b; 任务描述 测试说明 我的通关代码: 测试结果&#xff1a; 任务描述 本关任务&#xff1a;实现希尔排序算法。 测试说明 平台会对你编写的代码进行测试&#xff1a; 测试输入示例&#xff1a; 10 9 8 7 6 5 4 3 2 1 0 (说明&#xff1a;第一行是元素个数&a…

通俗易懂的 Nginx 反向代理 配置

通俗易懂的 Nginx 反向代理 配置 首先 root 与 alias 的区别 root 是直接拼接 root location location /i/ {root /data/w3; }当请求 /i/top.gif &#xff0c;/data/w3/i/top.gif 会被返回。 alias 是用 alias 替换 location location /i/ {alias /data/w3/images/; }当请…

网页爬虫技术全解析:从基础到实战

引言 在当今信息爆炸的时代&#xff0c;互联网上的数据量每天都在以惊人的速度增长。网页爬虫&#xff08;Web Scraping&#xff09;&#xff0c;作为数据采集的重要手段之一&#xff0c;已经成为数据科学家、研究人员和开发者不可或缺的工具。本文将全面解析网页爬虫技术&…

分页查询和事务管理

前端需要给后端传递的参数&#xff1a; page&#xff1a;当前页码&#xff0c;用于指定用户想要查看的页。pageSize&#xff1a;每页展示记录数&#xff0c;用于指定每页应显示多少条记录。 后端需要给前端返回的结果&#xff1a; total&#xff1a;总记录数&#xff0c;用于告…

MATLAB深度学习(七)——ResNet残差网络

一、ResNet网络 ResNet是深度残差网络的简称。其核心思想就是在&#xff0c;每两个网络层之间加入一个残差连接&#xff0c;缓解深层网络中的梯度消失问题 二、残差结构 在多层神经网络模型里&#xff0c;设想一个包含诺干层自网络&#xff0c;子网络的函数用H(x)来表示&#x…