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

news2024/12/20 3:12:59

🌟🌟作者主页: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() const;
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() const
{
	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() const
	{
		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树进一步丰富了数据操作的可能性,为实际应用提供了更为灵活和强大的支持。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java_断点调试(debug)

按照如下配置好后&#xff0c;即可点击“F7”,进入相应的方法&#xff0c;查看源码 package com.hspedu.debug_;//debug对象创建的过程&#xff0c;加深对调试的理解 public class Debug01 {public static void main(String[] args) {//创建对象的流程//&#xff08;1&#xff…

YOLOv11融合[CVPR2024]Starnet中的star block特征提取模块

YOLOv11v10v8使用教程&#xff1a; YOLOv11入门到入土使用教程 YOLOv11改进汇总贴&#xff1a;YOLOv11及自研模型更新汇总 《Rewrite the Stars》 一、 模块介绍 论文链接&#xff1a;https://arxiv.org/abs/2403.19967 代码链接&#xff1a;https://github.com/ma-xu/Rewri…

【kubernetes】k8s集群的简述与搭建

简述 Kubernetes&#xff08;简称 K8s&#xff09;是一个开源的容器编排平台&#xff0c;用于自动化部署、扩展和管理容器化应用程序 关键特性 自动化部署和回滚&#xff1a;Kubernetes 可以自动化地部署和回滚应用程序&#xff0c;确保应用程序始终处于预期的状态。服务发现…

SpringBoot+vue实现WebSocket通信

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务器主动向客户端推送数据。 WebSocket的主要特点&#xff1a; 全双工通信&#xff1a;客户端和服务器之间的数据可以同时双向传输低延迟&…

【BUG】记一次context canceled的报错

文章目录 案例分析gorm源码解读gin context 生命周期context什么时候cancel的什么时候context会被动cancel掉呢&#xff1f; 野生协程如何处理 案例分析 报错信息 {"L":"ERROR","T":"2024-12-17T11:11:33.0050800","file"…

H5 中 van-popup 的使用以及题目的切换

H5 中 van-popup 的使用以及题目的切换 在移动端开发中&#xff0c;弹窗组件是一个常见的需求。vant 是一个轻量、可靠的移动端 Vue 组件库&#xff0c;其中的 van-popup 组件可以方便地实现弹窗效果。本文将介绍如何使用 van-popup 实现题目详情的弹窗展示&#xff0c;并实现…

【第一节】Git的简介和安装

目录 一、git的介绍 二、git 的安装 2.1 Linux 平台安装 2.2 源码安装 2.3 Windows 平台安装 2.4 Mac 平台安装 2.5 Git 配置 2.5.1 配置文件 2.5.2 用户信息配置 2.5.3 文本编辑器配置 2.5.4 差异分析工具配置 2.5.5 查看配置信息 一、git的介绍 Git 是一种开源的…

奇怪的知识又增加了,ESP32下的Lisp编程:ULisp--Lisp for microcontrollers

ESP32下有MicroPython&#xff0c;那么我就在想&#xff0c;有Lisp语言支持吗&#xff1f;答案是果然有&#xff01;有ULisp&#xff0c;专门为MCU设计的Lisp&#xff01; 网址&#xff1a;uLisp - Lisp for microcontrollers 介绍&#xff1a;用于微控制器的 Lisp 适用于 Ar…

决策树的生成与剪枝

决策树的生成与剪枝 决策树的生成生成决策树的过程决策树的生成算法 决策树的剪枝决策树的损失函数决策树的剪枝算法 代码 决策树的生成 生成决策树的过程 为了方便分析描述&#xff0c;我们对上节课中的训练样本进行编号&#xff0c;每个样本加一个ID值&#xff0c;如图所示…

51c嵌入式~单片机~合集2

我自己的原文哦~ https://blog.51cto.com/whaosoft/12362395 一、不同的电平信号的MCU怎么通信&#xff1f; 下面这个“电平转换”电路&#xff0c;理解后令人心情愉快。电路设计其实也可以很有趣。 先说一说这个电路的用途&#xff1a;当两个MCU在不同的工作电压下工作&a…

Kerberos实验

kdc&#xff1a;192.168.72.163 客户端&#xff08;机器账户win10&#xff09;&#xff1a;192.168.72.159 用户&#xff1a;administrator 抓包&#xff1a;开机登录win10&#xff0c;使用administrator域用户凭据登录。 生成 Kerberos 解密文件 抓取 krbtgt 用户和 win1…

AI一键分析小红书对标账号‼️

宝子们&#xff0c;AI小助手近期发现了一款宝藏AI工具&#xff0c;拥有对标账号AI分析功能&#xff0c;只需10秒就能全面掌握对标账号的运营情况&#xff0c;并且可以根据分析结果提供创作方向和灵感&#xff0c;轻松助力1:1复刻起号&#xff01; 功能亮点&#xff1a; &…

大腾智能CAD:国产云原生三维设计新选择

在快速发展的工业设计领域&#xff0c;CAD软件已成为不可或缺的核心工具。它通过强大的建模、分析、优化等功能&#xff0c;不仅显著提升了设计效率与精度&#xff0c;还促进了设计思维的创新与拓展&#xff0c;为产品从概念构想到实体制造的全过程提供了强有力的技术支持。然而…

VMware虚拟机 Ubuntu没有共享文件夹的问题

在虚拟机的Ubuntu系统中&#xff0c;共享文件目录存放在 mnt/hgfs 下面&#xff0c;但是我安装完系统并添加共享文件后发现&#xff0c;在mnt下连/hgfs目录都没有。 注意&#xff1a;使用共享文件目录需要已安装VMtools工具。 添加共享文件目录 一&#xff1a;在超级用户下 可…

OpenGL ES 01 渲染一个四边形

项目架构 着色器封装 vertex #version 300 es // 接收顶点数据 layout (location 0) in vec3 aPos; // 位置变量的属性位置值为0 layout (location 1) in vec4 aColors; // 位置变量的属性位置值为1 out vec4 vertexColor; // 为片段着色器指定一个颜色输出void main() {gl…

leetcode二叉搜索树部分笔记

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 二叉搜索树 1. 二叉搜索树的最小绝对差2. 二叉搜索树中第 K 小的元素3. 验证二叉搜索树 1. 二叉搜索树的最小绝对差 给你一个二叉搜索树的根节点 root &#xff0c;返回 树中…

推送本地仓库到远程git仓库

目录 推送本地仓库到远程git仓库1.1修改本地仓库用户名1.2 push 命令1.3远程分支查看 推送本地仓库到远程git仓库 删除之前的仓库中的所有内容&#xff0c;从新建库&#xff0c;同时创建一个 A.txt 文件 清空原有的远程仓库内容&#xff0c;重新创建一个新的仓库&#xff0c;…

暂停一下,给Next.js项目配置一下ESLint(Next+tailwind项目)

前提 之前开自己的GitHub项目&#xff0c;想着不是团队项目&#xff0c;偷懒没有配置eslint&#xff0c;后面发现还是不行。eslint的存在可以帮助我们规范代码格式&#xff0c;同时 ctrl s保存立即调整代码格式是真的很爽。 除此之外&#xff0c;团队使用eslint也是好处颇多…

基于微信小程序的小区疫情防控ssm+论文源码调试讲解

第2章 程序开发技术 2.1 Mysql数据库 为了更容易理解Mysql数据库&#xff0c;接下来就对其具备的主要特征进行描述。 &#xff08;1&#xff09;首选Mysql数据库也是为了节省开发资金&#xff0c;因为网络上对Mysql的源码都已进行了公开展示&#xff0c;开发者根据程序开发需…