【C++高阶】精通AVL树:全面剖析与深度学习

news2024/12/23 22:27:07

目录

  • 🚀 前言
  • 一: 🔥 AVL树的性质
  • 二: 🔥 AVL树节点的定义
  • 三: 🔥 AVL树的插入
  • 四: 🔥 AVL树的平衡调整(附动图)
  • 五:🔥 AVL树的验证
    • 5.1 验证有序
    • 5.2 验证平衡
  • 六: 🔥 AVL树的删除
  • 七: 🔥AVL树的性能和完整代码

🚀 前言

AVL树,又称为平衡二叉树,它基于二叉搜索树并通过平衡而得到。

在前面的学习中我们提到,二叉搜索树可以提高搜索数据的效率,但在数据有序的情况下会退化为单支树,此时在树中查找元素就得遍历一整个分支,时间复杂度也会退化至O(N)。

如果有一种算法,可以使二叉搜索树时刻保持左右子树的平衡,就可以避免这种最坏情况。

因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了AVL树,它不仅解决了二叉搜索树在数据插入和删除时可能产生的失衡问题,更通过旋转操作,使得树的高度始终保持在一个相对较低的水平,从而保证了搜索的高效性。

一: 🔥 AVL树的性质

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差 ( 简称平衡因子 ) 的绝对值不超过 1 (-1 / 0 / 1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度 O( l o g 2 n log_2 n log2n)

二: 🔥 AVL树节点的定义

AVL树节点的定义通常包含以下几个关键部分:

1.基本元素
在调整失衡的AVL树时,我们需要频繁的访问父节点,所以在AVL树中我们需要使用三叉链,因此AVL树的节点除了包含左右子节点的指针,还需要一个指向父节点的指针。

  • _left:指向节点的左子节点的指针。
  • _right:指向节点的右子节点的指针。
  • _parent:指向节点的父节点的指针。
  • _kv:一个结构体或配对(pair),包含节点的键值(key)和值(value)。这取决于AVL树的具体用途,可能只包含键或包含键值对。

2. 平衡因子(_bf)

  • 一个整数,表示节点左子树和右子树的高度差。AVL树的性质要求任何节点的平衡因子的绝对值不超过1(-1, 0, 1)。
  • 如果左子树比右子树高一层,那么平衡因子就为-1;如果左右子树一样高,平衡因子就为0;如果右子树比左子树高一层,那么平衡因子就为1,这三种情况下AVL树的性质都没有被打破。
  • 按照这个规则,如果平衡因子为-2、2或其他值,则说明左右子树已经失衡,性质被打破。

另外需要说明一下,本文中,我们使用key / value模型的AVL树

AVL树节点的定义:

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	
	pair<K, V> _kv; 	//第一个数据存储key,第二个数据存储value
	int _bf; 			//平衡因子(balance factor)
 
	AVLTreeNode(const pair<const K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0) 		//新节点左右都为空,平衡因子为0
	{}
};

三: 🔥 AVL树的插入

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

  • 按照二叉搜索树的方式插入新节点
  • 树的平衡调整以及调整节点的平衡因子

新节点插入后,parent的平衡因子一定需要调整
我们先按照二叉搜索树的规则将节点插入到AVL树中,并判断插入的节点在父节点的左边还是右边

按照平衡因子的规则,如果新节点插入到了父节点的左侧,那么父节点的平衡因子-1
在这里插入图片描述
如果新节点插入到了父节点的右侧,那么父节点的平衡因子+1
在这里插入图片描述
以上,便是新增节点的父节点平衡因子可能的变化情况。

但是! 插入一个节点不但会影响父节点,还可能会影响到祖先节点。

  • 我们观察上面的四种可能,其中左边的两种情况下,插入节点后以父节点为根的子树高度发生了变化;在右边的两种情况下,插入节点后以父节点为根的子树高度没有发生变化。

  • 观察过后可以发现,当父节点的平衡因子从0变为1/-1后,子树高度发生变化;当父节点的平衡因子从1/-1变为0后,子树高度不发生变化

  • 如果以父节点为根的子树高度没有发生变化,那么就不会影响到祖先节点的平衡因子;如果高度变了就会继续向上影响到祖先节点的平衡因子

因此,我们可以通过判断节点的插入位置来计算父节点的平衡因子,进而判断子树高度是否发生变化,再进一步计算对祖先节点平衡因子的影响,来判断AVL树是否失衡。

至此,我们已经可以开始写插入新节点和更新平衡因子的代码了:

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool insert(const pair<const K, V>& kv)
	{
		if (_root == nullptr) 		//检测为空树的情况
		{
			_root = new Node(kv);
			return true;
		}
 
		Node* parent = nullptr;
		Node* cur = _root;
 
		while (cur) 				//搜索新节点的插入位置
		{
			parent = cur;
			if (kv.first > cur->_kv.first)
				cur = cur->_right;
			else if (kv.first < cur->_kv.first)
				cur = cur->_left;
			else
				return false;
		}
 
		cur = new Node(kv);
        //将父节点与新节点链接
        //比较新节点和父节点的key判断插入到左边还是右边
		if (kv.first > parent->_kv.first)   
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
 
		while (cur != _root)
		{
            //插入节点后除了对父节点造成影响还可能对祖宗节点造成影响
            //因此随着循环进行,这里的cur不一定为新节点,可以理解为高度发生变化的子树的根节点
            //更新父节点的平衡因子
			if (cur == parent->_left)
				parent->_bf--;
			else
				parent->_bf++;
 
            //更新后检测父节点的平衡因子
			if (parent->_bf == 0) //平衡因子为0说明没有打破性质,跳出循环
				break;
			else if (parent->_bf == 1 || parent->_bf == -1) //更新后平衡因子为1或-1说明高度发生变化,改变cur和parent的指向后继续向上更新
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) //更新后平衡因子为2或-2.说明已经失衡,需要调整
			{
                //不同情况的调整方法...
				if (parent->_bf == 2)
				{
					if (cur->_bf == 1)
					{
						//...
					}
					else if (cur->_bf == -1)
					{
						//...
					}
				}
				else
				{
					if (cur->_bf == 1)
					{
						//...
					}
					else if (cur->_bf == -1)
					{
						//...
					}
				}
				break;
			}
			else //平衡因子出现意外情况,报错
			{
				assert(false);
			}
		}
 
		return true;
	}
 
private:
	Node* _root = nullptr;
};

通过上述分析我们可以知道:插入后,parent的平衡因子可能有三种情况:0,正负1, 正负2

  • 如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
  • 如果parent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,此时以parent为根的树的高度增加,需要继续向上更新
  • 如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理
  • 接下来我们就重点讲解AVL树的旋转操作

四: 🔥 AVL树的平衡调整(附动图)

如果在一颗原本平衡的AVL树中插入一个新节点,可能会造成失衡,此时需要调整树的结构使之重新平衡,这种调整方法称为旋转。

根据树的原本结构和节点插入位置的不同分为四种情况和四种旋转方式:

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

在这里插入图片描述
问题来了:如何判断插入的新节点的方位呢?

很简单,以上面的情况为例,插入新节点后60的平衡因子变成-2,说明左子树更高,而30的平衡因子变成-1,说明新节点插入到了30的左子树。后面左单旋以及双旋中都同理,我们使用平衡因子就可以判断新节点插入的位置

右单旋代码如下:

void RotateRight(Node *parent) 		//parent为平衡因子发生失衡的节点
{
    Node *subL = parent->_left; 	//subL为parent的左子节点
    Node *subLR = subL->_right; 	//subLR为subL的右子节点
    //parent,subL和subLR三个节点是旋转中唯三需要进行操作的三个节点
 
    // 将parent与subLR节点进行链接
    parent->_left = subLR;
    if (subLR) //subLR可能为空
        subLR->_parent = parent;
 
    Node *parentParent = parent->_parent; 	//记录parent的父节点
 
    if (parent != _root)
    {
        subL->_parent = parentParent; 	//将subL与parent的父节点链接
        if (parent == parentParent->_left)
            parentParent->_left = subL;
        else
            parentParent->_right = subL;
    }
    else 		//如果parent为根,旋转后subL成为新的根
    {
        _root = subL;
        subL->_parent = nullptr;
    }
 
    //将subL与parent链接
    subL->_right = parent;
    parent->_parent = subL;
 
    parent->_bf = subL->_bf = 0; 		//更新平衡因子
}

通过右单旋我们可以看到,原本的_parent、_left以及_right的平衡因子都变成了0

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

在这里插入图片描述
因为左单旋的原理和右单旋是类似的,只要理解了右单旋,加上动图的配合,左单旋和后面的双旋都是很好理解的

左单旋代码如下:

void RotateLeft(Node *parent)
{
    Node *subR = parent->_right;
    Node *subRL = subR->_left;
 
    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;
 
    Node *parentParent = parent->_parent;
 
    if (parent != _root)
    {
        subR->_parent = parentParent;
        if (parent == parentParent->_left)
            parentParent->_left = subR;
        else
            parentParent->_right = subR;
    }
    else
    {
        _root = subR;
        subR->_parent = nullptr;
    }
 
    subR->_left = parent;
    parent->_parent = subR;
 
    parent->_bf = subR->_bf = 0;
}

同样:原本的_parent、_left以及_right的平衡因子都变成了0

(3)新节点插入较高左子树的右侧:先左单旋再右单旋(左右双旋)

这种情况又可以分为两种情况:

在这里插入图片描述

不过这两种情况都属于在较高左子树的右侧插入,处理方式都是相同的,唯一的区别在于最后旋转完成后,更新平衡因子时的值不同。

  • 还存在一种情况就是30本身就是新插入的节点没有bc子树,这点会在代码里体现比较容易理解这里就不赘述了

接下来我们以上面的那个情况为例展示左右双旋的过程:
在这里插入图片描述
而下面的情况和上面的情况唯一的区别在于,最后更新的平衡因子不同
在这里插入图片描述
如何去决定每个节点更新后的平衡因子呢?可以看到这两种情况中,如果在b下面插入新节点,那么旋转过后30和60的平衡因子更新成0,90的平衡因子更新成1;如果在c下面插入新节点,则是60和90的平衡因子更新成0,30的平衡因子更新成-1

关键一点:而新节点究竟插入到了b下面还是在c下面,我们可以通过插入节点后60的平衡因子来判断
在这里插入图片描述
左右双旋代码如下:

void RotateLR(Node *parent)
{
    Node *subL = parent->_left;
    Node *subLR = subL->_right;
    int bf = subLR->_bf; //记录插入节点后subLR的平衡因子
 
    RotateLeft(subL); //先左单旋
    RotateRight(parent); //再右单旋
 
    //更新平衡因子
    //通过前面记录的平衡因子判断更新的情况
    if (bf == 0)
    {
        parent->_bf = subL->_bf = subLR->_bf = 0;
    }
    else if (bf == 1)
    {
        subL->_bf = -1;
        parent->_bf = subLR->_bf = 0;
    }
    else if (bf == -1)
    {
        parent->_bf = 1;
        subL->_bf = subLR->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

(4)新节点插入较高右子树的左侧:先右单旋再左单旋(右左双旋)

这种情况和左右双旋的情况原理一样,我们直接上动图和代码
右左双旋的代码如下:
在这里插入图片描述

void RotateRL(Node *parent)
{
    Node *subR = parent->_right;
    Node *subRL = subR->_left;
    int bf = subRL->_bf;
 
    RotateRight(subR);
    RotateLeft(parent);
 
    if (bf == 0)
    {
        parent->_bf = subR->_bf = subRL->_bf = 0;
    }
    else if (bf == 1)
    {
        parent->_bf = -1;
        subR->_bf = subRL->_bf = 0;
    }
    else if (bf == -1)
    {
        subR->_bf = 1;
        parent->_bf = subRL->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

现在四个旋转的函数都实现了,完整的插入函数代码如下:

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) {
		_root = new Node(kv);
		return true;
	}

	Node* cur = _root, *parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first == kv.first) return false;
		else if (cur->_kv.first < kv.first) {
			parent = cur;
			cur = cur->_right;
		}
		else {
			parent = cur;
			cur = cur->_left;
		}
	}
	cur = new Node(kv);
	if (kv.first > parent->_kv.first) parent->_right = cur;
	else parent->_left = cur;

	cur->_parent = parent;										// 链接父亲

	//更新平衡因子
	while (parent)
	{
		if (cur == parent->_left) parent->_bf--;
		else 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
			{
				RotateLR(parent);						   //  先左旋再右旋  (左边高,孩子右边高, 父子异号)
			}

			break;
		}
		else {
			assert(false);
		}
	}

	return true;
}

总结:

假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑

  1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
  • 当subR的平衡因子为1时,执行左单旋
  • 当subR的平衡因子为-1时,执行右左双旋
  1. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL
  • 当subL的平衡因子为-1是,执行右单旋
  • 当subL的平衡因子为1时,执行左右双旋

五:🔥 AVL树的验证

5.1 验证有序

最重要的插入节点部分完成了,不过在验证是否符合AVL树性质前,我们首先需要验证其是否是一棵二叉搜索树

在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树

中序遍历代码如下:

void InOrder()
{
    _InOrder(_root);
    cout << endl;
}
 
void _InOrder(Node *root)
{
    if (root == nullptr)
        return;
    _InOrder(root->_left);
    cout << root->_kv.first << " "; // key/value模型,我们只打印key即可
    _InOrder(root->_right);
}

5.2 验证平衡

要验证是否符合AVL树性质,只需要检测它的所有节点的子树高度差不超过1即可

需要注意的是,这里不可以直接通过判断平衡因子的绝对值是否大于1来验证平衡,因为平衡因子是不客观的,可以被修改

因此,我们通过递归来得到每棵子树的高度并进行判断即可

bool IsBalance()
{
    return _IsBalance(_root);
}
 
bool _IsBalance(Node *root)
{
    if (root == nullptr)
        return true;
    int leftHeigit = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    if (rightHeight - leftHeigit != root->_bf)
    {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }
 
    return abs(rightHeight - leftHeigit) <= 1 
        && _IsBalance(root->_left) 
        && _IsBalance(root->_right);
}
 
int Height()
{
    return _Height(_root);
}
 
int _Height(Node *root)
{
    if (root == nullptr)
        return 0;
    int higher = max(_Height(root->_left), _Height(root->_right));
    return higher + 1;
}

六: 🔥 AVL树的删除

AVL树的删除并不是本文的重点,因为其原理我们在前面已经学习过了

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置

七: 🔥AVL树的性能和完整代码

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

相对于AVL树的严格平衡,红黑树则追求一种相对平衡,因此会略胜一筹,后面的文章中会对红黑树进行讲解。

AVL树的完整代码如下:

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<const K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{}
};
 
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool insert(const pair<const K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
 
		Node* parent = nullptr;
		Node* cur = _root;
 
		while (cur)
		{
			parent = cur;
			if (kv.first > cur->_kv.first)
				cur = cur->_right;
			else if (kv.first < cur->_kv.first)
				cur = cur->_left;
			else
				return false;
		}
 
		cur = new Node(kv);
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
 
		while (cur != _root)
		{
			if (cur == parent->_left)
				parent->_bf--;
			else
				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)
				{
					if (cur->_bf == 1)
					{
						RotateLeft(parent);
					}
					else if (cur->_bf == -1)
					{
						RotateRL(parent);
					}
				}
				else
				{
					if (cur->_bf == 1)
					{
						RotateLR(parent);
					}
					else if (cur->_bf == -1)
					{
						RotateRight(parent);
					}
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
 
		return true;
	}
 
	void RotateLeft(Node* parent) //新节点插入较高右子树的右侧:左单旋
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
 
		parent->_right = subRL;
		if(subRL)
			subRL->_parent = parent;
 
		Node* parentParent = parent->_parent;
 
		if (parent != _root)
		{
			subR->_parent = parentParent;
			if (parent == parentParent->_left)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
		}
		else
		{
			_root = subR;
			subR->_parent = nullptr;
		}
 
		subR->_left = parent;
		parent->_parent = subR;
 
		parent->_bf = subR->_bf = 0;
	}
 
	void RotateRight(Node* parent) //新节点插入较高左子树的左侧:右单旋
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
 
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
 
		Node* parentParent = parent->_parent;
 
		if (parent != _root)
		{
			subL->_parent = parentParent;
			if (parent == parentParent->_left)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
		}
		else
		{
			_root = subL;
			subL->_parent = nullptr;
		}
 
		subL->_right = parent;
		parent->_parent = subL;
 
		parent->_bf = subL->_bf = 0;
	}
 
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
 
		RotateRight(subR);
		RotateLeft(parent);
 
		if (bf == 0)
		{
			parent->_bf = subR->_bf = subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			subR->_bf = 1;
			parent->_bf = subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
 
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
 
		RotateLeft(subL);
		RotateRight(parent);
 
		if (bf == 0)
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
 
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
 
	bool IsBalance()
	{
		return _IsBalance(_root);
	}
 
	int Height()
	{
		return _Height(_root);
	}
 
	size_t Size()
	{
		return _Size(_root);
	}
 
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_kv.first)
				cur = cur->_right;
			else if (key < cur->_kv.first)
				cur = cur->_left;
			else
				return cur;
		}
		return nullptr;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
 
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		int leftHeigit = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		if (rightHeight - leftHeigit != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
 
		return abs(rightHeight - leftHeigit) <= 1 
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
 
	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int higher = max(_Height(root->_left), _Height(root->_right));
		return higher + 1;
	}
 
	size_t _Size(Node* root)
	{
		if (root == nullptr)
			return 0;
		return _Size(root->_left) + _Size(root->_right) + 1;
	}
private:
	Node* _root = nullptr;
};

以上就是AVL搜索树的图解与完整实现过程,欢迎在评论区留言,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述

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

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

相关文章

防御保护课-防火墙接口配置实验

一、实验拓扑 &#xff08;我做实验用的图如下&#xff09; 二、实验要求 1.防火墙向下使用子接口分别对应生产区和办公区 2.所有分区设备可以ping通网关 三、实验思路 配IP&#xff1b; 划分vlan并配置vlan&#xff1b; 配置路由和安全策略。 四、实验配置 1、画图并…

C++与lua联合编程

C与lua联合编程 一、环境配置二、lua基本语法1.第一个lua和C程序2.基本数据类型和变量2.1 Nil2.2 Booleans2.3 Numbers2.4 String(最常用) 3. 字符串处理3.1 错误处理3.2 字符串长度:string.len3.3 字符串子串 :string.sub3.4 字符串查找: string.find3.5字符串替换: string.gs…

Evil-WinRM一键测试主机安全情况(KALI工具系列四十四)

目录 1、KALI LINUX 简介 2、Evil-WinRM 3、信息收集 3.1 目标IP 3.2 kali的IP 4、操作步骤 4.1 用户访问 4.2 使用哈希值 4.3 文件处理 5、总结 1、KALI LINUX 简介 Kali Linux 是一个功能强大、多才多艺的 Linux 发行版 &#xff0c;广泛用于网络安全社区。它具有全…

swiftui使用ScrollView实现左右滑动和上下滑动的效果,仿小红书页面

实现的效果如果所示&#xff0c;顶部的关注用户列表可以左右滑动&#xff0c;中间的内容区域是可以上下滚动的效果&#xff0c;点击顶部的toolbar也可以切换关注/发现/附近不同页面&#xff0c;实现翻页效果。 首页布局 这里使用了NavigationStack组件和tabViewStyle样式配置…

在项目服务器部署git 并实现自动提交

以下场景适合在服务器当中使用git 方便提交代码&#xff0c;同时不需要外部的git仓库&#xff08;码云gitee或者github作为管理平台&#xff09;。依靠服务器本身ssh 连接协议做为git提交的地址&#xff0c;同时利用钩子自动同步项目代码 首先下载git sudo apt update sudo a…

Linux最直观的性能分析(热点分析)-编译perf并生成火焰图

本文先介绍了linux下perf工具的使用场景&#xff0c;然后对命令行安装和源码编译安装两种方式做了说明&#xff0c;接下来通过最简单的perf top命令给出perf的直观印象&#xff0c;最后通过perf record生成火焰图的方式说明如何发现进程中的函数热点。 一、perf工具介绍 per…

00 JavaWeb

学习资料&#xff1a;B站视频-黑马程序员JavaWeb基础教程 文章目录 JavaWeb1、JavaWeb简介2、 JavaWeb主要内容3、JavaWeb技术栈4、JavaWeb课程安排5、Web核心课程安排 JavaWeb 1、JavaWeb简介 Web&#xff1a;全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏…

C++: 链表回文结构/分割链表题解

目录 1.链表的回文结构 分析 代码 2.链表分割 ​编辑分析 代码 1.链表的回文结构 分析 这道题的难点是空间复杂度为O&#xff08;1&#xff09; 结合逆置链表找到链表的中间节点就可以解决了。 先找到链表的中间节点&#xff0c;再对中间节点的下一个节点进行逆置&…

macbook pro大模型推理

安装与配置 参考github ollama 链接安装ollama。安装完成后,安装常用的模型,下载速度超快。 性能测试 在进行实际测试之前,我首先对模型进行了预处理,以确保其在 M3 Max 上能够高效运行。测试过程中,我主要关注了以下几个方面: 模型加载时间 加载大型模型通常需要较…

Python WebUIAPI:打造交互式Web界面的利器

Python WebUIAPI&#xff1a;打造交互式Web界面的利器 引言&#xff1a;交互式Web界面的革新 在当今快速发展的互联网时代&#xff0c;Web界面的交互性已成为衡量用户体验的重要标准。Python作为一门流行的编程语言&#xff0c;其生态中涌现出许多强大的库来帮助开发者构建交互…

组队学习——贝叶斯分类器

前言 本次数据继续沿用上一次主题的【组队学习——支持向量机-CSDN博客】 数据处理部分延续【组队学习——支持向量机】主题的处理办法对应划分训练集和验证集 模型选择 本次贝叶斯分类器模型的较多&#xff0c;常用的为高斯朴素贝叶斯分类器、多项式朴素贝叶斯分类器、伯努…

2024年超好用的4款PDF阅读器推荐

PDF文件已经是我们平时常常会接触到的文件&#xff0c;但是无论是阅读和编辑都需要依赖一些工具&#xff0c;所以今天给大家介绍的是4个很多人都在使用的PDF阅读器。 &#xff11;、福昕PDF阅读软件 这款PDF编辑器是一个大厂其他的产品&#xff0c;功能非常的强大&#xff0c;…

Build a Large Language Model (From Scratch)GPT-4o翻译和代码每行中文注释Ch4

目录 4 Implementing a GPT model from Scratch To Generate TextThis chapter covers4.1 Coding an LLM architectureListing 4.1 A placeholder GPT model architecture class4.2 Normalizing activations with layer normalization4.3 Implementing a feed forward network …

STM32CUBEMX_SPI_驱动WS2811灯带

STM32CUBEMX_SPI_驱动WS2811灯带 前言&#xff1a; 关于这种带芯片的之前我都是使用GPIO模拟时序&#xff0c;但是带来一个很大的弊端&#xff0c;那就是严重占用CPU资源&#xff0c;使得其他代码逻辑没办法正常执行了&#xff0c;想办法搞一个单片机的外设使用DMA功能&#xf…

LeetCode-day21-1186. 删除一次得到子数组最大和

LeetCode-day21-1186. 删除一次得到子数组最大和 题目描述示例示例1&#xff1a;示例2&#xff1a;示例3&#xff1a; 思路代码 题目描述 给你一个整数数组&#xff0c;返回它的某个 非空 子数组&#xff08;连续元素&#xff09;在执行一次可选的删除操作后&#xff0c;所能得…

【学术研究、研究热点、最新前沿】如何跟踪最新的论文

1.跟踪arxiv 使用https://www.arxivdaily.com/接收每天的推送。 2.跟踪热点文章的引用 使用semantic scholar。 3.跟踪某个学术大佬或者主题 3.1 使用web of science。 3.2 使用文献鸟 4.跟踪某个期刊

pico+unity3d 射线交互教程

前期配置&#xff1a;环境配置参考教程一&#xff0c;手部模型参考教程二&#xff0c;场景基于上一篇搭建。 最终效果&#xff1a;手部射线&#xff08;初始不可见&#xff09;对准 UI 显示&#xff0c;按下手柄 Trigger 键与可交互 UI&#xff08;如 Button、Toggle、Slider …

Android APP 基于RecyclerView框架工程(知识体系积累)

说明&#xff1a;这个简单的基于RecyclerView的框架作用在于自己可以将平时积累的一些有效demo整合起来&#xff08;比如音视频编解码的、opengles的以及其他也去方向的、随着项目增多&#xff0c;工程量的增加&#xff0c;后期想高效的分析和查找并不容易&#xff09;&#xf…

vscode 环境

这张截图显示的是在VS Code&#xff08;Visual Studio Code&#xff09;中选择Python解释器的界面。不同的Python解释器及其虚拟环境列出了可选项&#xff0c;用户可以根据需要选择合适的解释器来运行Python代码。以下是对截图中信息的详细解释&#xff1a; 解释器选择界面 当…

Java小技能:多级组织机构排序并返回树结构(包含每个层级的子节点和业务数据集合)

文章目录 引言I 实体定义1.1 部门1.2 用户组织机构中间表1.3 树状DTOII 抽取组织机构排序方法2.1 树状排序方法2.2 案例III 查询条件构建3.1 根据部门进行权限控制3.2 注入风险引言 需求: 根据组织机构进行数据授权控制,例如控制船舶、船舶设备、摄像头、港区查看权限。 一…