探索数据结构:AVL树的分析与实现

news2025/1/11 14:00:15

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog

1. AVL树的介绍

在前面我们学习二叉搜索树时知道,在数据有序或接近有序时二叉搜索树会退化为单边树,查找时相当于在单链表中查找,效率低下。

img

为了解决这个问题,1962 年,两位俄罗斯数学家G.M.Adelson-VelskiiE.M.Landis提出了一种新方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。而这颗新的树我们平衡二叉搜索(排序)树,简称AVL树。其具有以下特点:

  1. AVL树本质就是一颗二叉搜索树。
  2. AVL树左右两棵子树的高度差的绝对值(平衡因子)不超过1。
  3. AVL树的左右两棵子树也是一颗AVL树。

其中特别注意:空树也是一颗AVL树,并且由于AVL树也是二叉搜索树,所以AVL树的中序遍历是有序的

img

AVL树通过特殊的构造有效的避免了二叉搜索树在数据有序或接近有序时退化为单边树的情况。这使得AVL树在查找、插入和删除操作上,能够保持较为稳定的时间复杂度,而不会因为数据的特殊分布导致性能急剧下降。

2. AVL树的功能

以下是常见的AVL树的功能:

  1. AVL树的插入。
  2. AVL树的查找。
  3. AVL树的删除。

3. AVL树的结构

3.1. AVL树的节点

AVL树的节点本质与二叉搜索树的节点差不多,所以肯定有三个成员变量:左子树_left,右子树_right,键值_val,并且键值我们采用KV模型的形式。而为了后续调整我们还需一个平衡因子_bf(默认为右子树的高度-左子树的高度)以及一个父节点_parent。当然为了适配不同的类型,我们可以定义一个模版.。

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K, V>& val = pair<K, V>())
		:_kv(val)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
	pair<K, V> _kv;//kv模型
	int _bf;//平衡因子
	AVLTreeNode* _left;//左子树
	AVLTreeNode* _right;//右子树
	AVLTreeNode* _parent;//指向父节点
};

3.2. AVL树

然后我们就可以通过节点来定义AVL树,并将根节点初始化为空。

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//...具体功能
private:
	Node* _root = nullptr;//根节点
};

4. AVL树的初始化与销毁

4.1. 构造函数/拷贝构造/赋值重载

首先我们直接定义一个无参的构造函数,因为我们在定义拷贝构造之后编译器就不会在生成默认的构造函数了。

AVLTree()
{}

之后我们可以利用递归来实现一个拷贝构造函数。

AVLTree(const AVLTree& t)
{
    _root = copy(t._root);
}
Node* copy(Node* root)
{
    // 如果传入的根节点为空,直接返回空指针,表示拷贝结束
    if (root == nullptr)
        return nullptr;
    // 为新树创建一个与原始根节点值相同的新节点
    Node* newnode = new Node(root->_kv);
    // 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针
    newnode->_left = copy(root->_left);
    // 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针
    newnode->_right = copy(root->_right);
    // 新节点的父节点默认为空
    newnode->_parent = nullptr;
    // 如果新节点的左子节点存在,设置其父节点为新节点
    if (newnode->_left)
    {
        newnode->_left->_parent = newnode;
    }
    // 如果新节点的右子节点存在,设置其父节点为新节点
    if (newnode->_right)
    {
        newnode->_right->_parent = newnode;
    }
    // 返回新树的根节点指针
    return newnode;
}

最后我们通过一个简单的方式实现赋值重载——通过形参调用拷贝构造出一个临时变量,然后交换this所指向的变量,这样原本this所指向的对象出了作用域就会销毁,间接实现了实现赋值重载。

AVLTree<K,V>& operator=(const AVLTree <K,V> t)
{
    //赋值重载
    this->swap(_root, t._root);
    return *this;
}

4.2. 析构函数

析构函数需要借助递归释放所有节点,而为了方便我们传参我们可以定义子函数来帮助我们解决。

~AVLTree()
{
    Destroy(_root);
}
void Destroy(Node*& root)
{
	if (root == nullptr)
	{
		return;
	}
	//递归销毁左子树
	Destroy(root->_left);
	//递归销毁右子树
	Destroy(root->_right);
	//销毁根节点
	delete root;
	root = nullptr;
}

5. AVL树的功能实现

5.1. AVL树的旋转

我们知道AVL树左右子树的高度差绝对值要保证不超过1,也就是说AVL树的平衡因子_bf只能取-101三个值。而无论插入还是删除都可能破坏原有的结构,导致AVL树失衡。为了重新平衡AVL树,我们需要重新对其调整。

首先我们可以将AVL树被破坏的情形可以抽象成以下四种场景,其中红色字体代表该节点的平衡因子,蓝色节点代表破坏AVL树平衡的节点。

5.1.1. 右单旋

首先第一种情况就是破坏AVL树平衡的节点位于较高左子树的左侧。

img

对于这种情况我们需要右单旋的方式,设失衡节点为_parent,其左子树节点为subL,而左子树的右子树为subLR。其调整规则如下:

  1. subLR链接到parent的左边。
  2. parent链接到subL的右边。
  3. parentsubL的平衡因子置为0。

img

void RotateR(Node*&parent)
{
	// 获取父节点的左子节点
	Node* subL = parent->_left;
	// 获取父节点左子节点的右子节点
	Node* subLR = subL->_right;

	// 1.将subLR链接到parent的左边。
	parent->_left = subLR;
	// 如果父节点左子节点的右子节点存在,设置其父节点为当前父节点
	if (subLR)
	{
		subLR->_parent = parent;
	}
	// 获取父节点的父节点
	Node* ppNode = parent->_parent;
	// 2.将parent链接到subL的右边。
	subL->_right = parent;
	//parent的父节点指向subL
	parent->_parent = subL;
	// 如果父节点存在父节点
	if (ppNode)
	{
		// 如果父节点是其父亲节点的左子节点
		if (ppNode->_left == parent)
		{
			// 将父节点的父亲节点的左子节点设置为当前父节点的左子节点
			ppNode->_left = subL;
		}
		// 如果父节点是其父亲节点的右子节点
		else
		{
			// 将父节点的父亲节点的右子节点设置为当前父节点的左子节点
			ppNode->_right = subL;
		}
		// 设置父节点左子节点的父节点为父节点的父节点
		subL->_parent = ppNode;
	}
	// 如果父节点为根节点
	else
	{
		// 将根节点设置为父节点的左子节点
		_root = subL;
		// 将父节点左子节点的父节点设置为空
		subL->_parent = nullptr;
	}
	//3.将parent与subL的平衡因子置为0。
	subL->_bf = parent->_bf = 0;
	//改变parent节点的指向,方便erase中的node返回
	parent = subL;
}

其中需要特别注意的就是:判断父节点**_parent**到底是根节点,还是某个节点的子节点。

5.1.2. 左单旋

第二种情况就是破坏AVL树平衡的节点位于较高右子树的右侧。

img

对于这种情况我们需要左单旋的方式,设失衡节点为_parent,其右子树节点为subR,而有右子树的左子树为subRL。其调整规则如下:

  1. subRL链接到parent的右边。
  2. parent链接到subR的左边。
  3. parentsubR的平衡因子置为0。

img

void RotateL(Node* &parent)
{
	// 获取父节点的右子节点
	Node* subR = parent->_right;
	// 获取父节点右子节点的左子节点
	Node* subRL = subR->_left;
	//1.将subRL链接到parent的右边。
	parent->_right = subRL;
	// 如果父节点右子节点的左子节点存在,设置其父节点为当前父节点
	if (subRL)
	{
		subRL->_parent = parent;
	}
	// 获取父节点的父节点
	Node* ppNode = parent->_parent;
	//2.将parent链接到subR的左边。
	subR->_left = parent;
	// 设置父节点的父节点为右子节点
	parent->_parent = subR;

	// 如果父节点存在父节点
	if (ppNode)
	{
		// 如果父节点是其父亲节点的左子节点
		if (ppNode->_left == parent)
		{
			// 将父节点的父亲节点的左子节点设置为当前父节点的右子节点
			ppNode->_left = subR;
		}
		// 如果父节点是其父亲节点的右子节点
		else
		{
			// 将父节点的父亲节点的右子节点设置为当前父节点的右子节点
			ppNode->_right = subR;
		}
		// 设置父节点右子节点的父节点为父节点的父节点
		subR->_parent = ppNode;
	}
	// 如果父节点为根节点
	else
	{
		// 将根节点设置为父节点的右子节点
		_root = subR;
		// 将父节点右子节点的父节点设置为空
		subR->_parent = nullptr;
	}
	// 3.将parent与subR的平衡因子置为0。
	subR->_bf = parent->_bf = 0;
	//改变parent节点的指向,方便erase中的node返回
	parent = subR;
}

其中需要特别注意的就是:判断父节点**_parent**到底是根节点,还是某个节点的子节点。

5.1.3. 先左单旋,再右单旋

第三种情况就是破坏AVL树平衡的节点位于较高左子树的右侧。

img

对于这种情况我们需要先左单旋,再右单旋的方式,设失衡节点为_parent,其左子树节点为subL,而有左子树的右子树为subLR。其调整规则如下:

  1. 先对subL进行左单旋。
  2. 在对parent进行右单旋。
  3. 调整平衡因子。

img

img

但是平衡因子的调整又可以分别三种情况:

  1. subLR = -1时,如上图所示,调整后subL = 0subLR = 0parent = 1
  2. subLR = 0,即h = 0时:

比如依次插入三个节点,10,5,6,如下图所示:

img

调整后subL = 0subLR = 0parent = 0

  1. subLR = 1时,具体情况如下图:

imgimg

调整后subL = -1subLR = 0parent = 0

最后我们可以总结一下:

  • subLR = 0时:调整为subLR = 0,subL = 0,parent = 0
  • subLR = -1时:调整为subLR = 0,subL = 0,parent = 1
  • subLR = 1时:调整为subLR = 0,subL = -1,parent = 0
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;
    //先左旋
    RotateL(parent->_left);
    //再右旋
    RotateR(parent);
    //1.情况1
    if (bf == 0)
    {
        parent->_bf = subL->_bf = subLR->_bf = 0;
    }
    //2.情况2
    else if (bf == -1)
    {
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    //3.情况3
    else if (bf == 1)
    {
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    }
    else
    {
        //走到这里说明AVL树有问题
        assert(false);
    }
}
5.1.4. 先右单旋,再左单旋

最后一种情况就是破坏AVL树平衡的节点位于较高右子树的左侧。

img

对于这种情况我们需要先右单旋,再左单旋的方式,设失衡节点为_parent,其右子树节点为subR,而有右子树的左子树为subRL。其调整规则如下:

  1. 先对subR进行右单旋。
  2. 在对parent进行左单旋。
  3. 调整平衡因子。

img

img

同理,平衡因子的调整又可以分别三种情况:

  1. subRL = -1时,如上图所示,调整后subR = 1subRL = 0parent = 0
  2. subRL = 0,即h = 0时:

比如依次插入三个节点,5,10,6,如下图所示:

img

调整后subR = 0subRL = 0parent = 0

3.当subRL = 1时,具体情况如下图:

img

img

调整后subR = 0subRL = 0parent = -1

最后我们可以总结一下:

  • subRL = 0时:调整为subRL = 0,subR = 0,parent = 0
  • subRL = -1时:调整为subRL = 0,subR = 1,parent = 0
  • subRL = 1时:调整为subRL = 0,subL = 0,parent = -1
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;
    //先右单旋
    Rotate(parent->_right);
    Rotate(parent);
    //1.情况一
    if (bf == 0)
    {
        parent->_bf = subRL->_bf = subR->_bf = 0;
    }
    //2.情况二
    else if (bf == -1)
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    //3.情况三
    else if (bf == 1)
    {
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else
    {
        //走到这里说明AVL树出错
        assert(false);
    }
}

5.2. AVL树的插入

AVL树进行插入,首先是先找到需要插入的位置,这个逻辑与二叉搜索树类似,这里就不在赘述。找到之后对父节点的平衡因子进行更新,更新平衡因子就可以分为以下三种大的情况:

  1. 父节点平衡因子为 0,整体高度不变,不需要再向上调整平衡因子。

img

  1. 父节点平衡因子为1或-1,整体高度改变,需要再向上调整平衡因子。

img

  1. 父节点平衡因子为2或-2,平衡被破坏,需要进行旋转。

平衡被破坏同样可以分为四种情况,与旋转的方式想对应,其中蓝色为插入节点。首先第一种往较高左子树的左侧插入,此时parent = -2subL = -1,进行右单旋。

img

第二种往较高右子树的右侧插入,此时parent = 2subL = 1,进行左单旋。

img

第三种往较高左子树的右侧插入,此时parent = -2subL = 1,先左旋再右旋。

imgimg

第四种往较高右子树的右侧插入,先右旋再左旋,此时parent = 2subR = -1,先右旋再左旋。

imgimg

最后我们可以归纳总结出:

  1. 父节点平衡因子为 0,整体高度不变,不需要再向上调整平衡因子。
  2. 父节点平衡因子为1或-1,整体高度改变,需要再向上调整平衡因子。
  3. 父节点平衡因子为2或-2,平衡被破坏,需要进行旋转。设父节点为parent,插入方向子节点为cur
  • parent = -2cur = -1进行右单旋。
  • parent = 2cur = 1进行左单旋。
  • parent = -2cur = 1进行先进行左单旋,再进行右单旋。
  • parent = 2cur = -1进行先进行右单旋,再进行左单旋。
	bool insert(const pair<K, V>& kv)
	{
		// 如果根节点为空,创建新节点作为根节点并设置平衡因子为 0,然后返回插入成功
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_bf = 0;
			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->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;

		// 更新平衡因子
		while (parent)
		{
			// 如果插入在左边,父节点的平衡因子减 1
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			// 如果插入在右边,父节点的平衡因子加 1
			else
			{
				parent->_bf++;
			}
			// 如果父节点平衡因子为 0,整体高度不变,不需要再向上调整
			if (parent->_bf == 0)
			{
				break;
			}
			// 平衡因子为 1 或 -1,需要继续向上调整
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			// 平衡因子为 -2 或 2,需要进行旋转操作以保持平衡
			else if (parent->_bf == -2 || parent->_bf == 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);
				}

				break;
			}
			// 插入之前 AVL 树就已经不平衡了,断言
			else
			{
				assert(false);
			}
		}

		return true;
    }

5.3. AVL树的查找

AVL树的查找逻辑就与二叉搜索树一样了,这里就不在赘述。

Node* Find(const K& val)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first > val)
		{
			//左子树中查找
			cur = cur->_left;
		}
		else if (cur->_kv.first < val)
		{
			//右子树中查找
			cur = cur->_right;

		}
		else
		{
			//找到了
			return cur;
		}
	}
	return nullptr;
}

5.4. AVL树的删除

AVL树的删除逻辑其实整体与二叉搜索树的删除逻辑类似,可以分为三种情况讨论:删除节点的左子树节点为空

,删除节点的右子树节点为空,删除节点的左右子树都不为空。其中删除节点的左右子树都不为空可以经过伪删除法,即寻找到左子树的最右节点即左子树的最大值,或者是右子树的最左节点即右子树的最小值。然后赋值,转换为在其左子树或者右子树删除节点。而我们的AVL树本质是为了保持平衡,所以尽量选择删除子树较高的一方。

因为删除节点的左右子树都不为空的情况一定会被转换另外两种情况,所以我们只需要讨论删除节点的左子树节点为空,删除节点的右子树节点为空这两种情况。

当然这里在上面基础上再重新分类可分为:

  1. 删除节点在父节点的左侧:

当节点数少于三个时,删除后父节点平衡因子为1或0时,正常删除。

img

当父节点parent的右子树的左右子树高度相等,且删除后父节点平衡因子为2时,进行左单旋。

img

当父节点parent的右子树的右子树高度大于其左子树高度,且删除后父节点平衡因子为2时,进行左单旋。

img

当父节点parent的右子树的右子树高度小于其左子树高度,且删除后父节点平衡因子为2时,先进行右单旋,再进行左单旋。

img

  1. 删除节点在父节点右侧:

当节点数少于三个时,删除后父节点平衡因子为-1或0时,正常删除。

img

当父节点parent的左子树的左右子树高度相等,且删除后父节点平衡因子为-2时,进行右单旋。

img

当父节点parent的左子树的左子树高度大于其右子树高度,且删除后父节点平衡因子为-2时,进行右单旋。

img

当父节点parent的左子树的左子树高度小于其右子树高度,且删除后父节点平衡因子为-2时,先进行左单旋,再进行右单旋。

img

最后我们可以总结出以下结论:

  1. 当删除节点在父节点的左侧时
  • 父节点的平衡因子为2时,如果根节点的右子树的右子树高度大于等于其左子树高度,进行左单旋,否则进行先右旋再左旋。
  • 父节点的平衡因子不为2时,正常删除。
  1. 当删除节点在父节点的右侧时
  • 父节点的平衡因子为-2时,如果根节点的左子树的左子树高度大于等于其右子树高度,进行右单旋,否则进行先左旋再右旋。
  • 父节点的平衡因子不为-2时,正常删除。
//求高度
int Hegiht(Node* root)
{
    if (root == nullptr)
        return 0;

    int leftHegiht = Hegiht(root->_left);//先计算左子树高度
    int rightHegiht = Hegiht(root->_right);//再计算右子树高度

    return leftHegiht > rightHegiht ? leftHegiht + 1 : rightHegiht + 1;
}
void erase(const K& val)
{
    //递归删除
    _root = _erase(_root, val);
}
Node* _erase(Node* node, const K& val)
{
    // 如果当前节点为空,直接返回空指针
    if (node == nullptr)
    {
        return nullptr;
    }

    // 如果当前节点的键值大于要删除的键值,在左子树中删除
    if (node->_kv.first > val)
    {
        node->_left = _erase(node->_left, val);
        //更新节点的父节点
        if (node->_left)
        node->_left->_parent = node;
        // 调整节点的平衡因子
        node->_bf = Hegiht(node->_right) - Hegiht(node->_left);
        int bf = node->_bf;
        //情况一删除节点都在左边
        if (bf == 2)
        {
            
            int rightHegiht = Hegiht(node->_right->_right);
            int leftHegiht = Hegiht(node->_right->_left);
            if (rightHegiht >= leftHegiht)
            {
                RotateL(node);
            }
            else
            {
                RotateRL(node);
            }
        }
        
    }
    // 如果当前节点的键值小于要删除的键值,在右子树中删除
    else if (node->_kv.first < val)
    {
        node->_right = _erase(node->_right, val);
        //更新节点的父节点
        if(node->_right)
        node->_right->_parent = node;
        // 调整节点平衡因子
        node->_bf = Hegiht(node->_right) - Hegiht(node->_left);
        int bf = node->_bf;
        //情况二删除节点都在右边
        if (bf == -2)
        {
            int rightHegiht = Hegiht(node->_left->_right);
            int leftHegiht = Hegiht(node->_left->_left);
            if (leftHegiht >= rightHegiht)
            {
                RotateR(node);
            }
            else
            {
                RotateLR(node);
            }
        }
    }
    // 找到要删除的节点
    else
    {
        // 如果节点有两个孩子
        if (node->_left != nullptr && node->_right != nullptr)
        {
            // 如果左子树高度大于等于右子树高度
            if (Hegiht(node->_left) >= Hegiht(node->_right))
            {
                // 找到左子树中的最大节点
                Node* prev = node->_left;
                while (prev->_right)
                    prev = prev->_right;
                int target = prev->_kv.first;
                // 将当前节点的值替换为左子树中的最大节点的值
                node->_kv = prev->_kv;
                // 在左子树中删除该最大节点
                node->_left = _erase(node->_left,target);
            }
            else
            {
                // 找到右子树中的最小节点
                Node* post = node->_right;
                while (post->_left)
                    post = post->_left;
                // 将当前节点的值替换为右子树中的最小节点的值
                int target = post->_kv.first;
                node ->_kv = post->_kv ;
                // 在右子树中删除该最小节点
                node->_right = _erase(node->_right, target);
            }
        }
        // 如果节点最多有一个孩子
        else
        {
            // 如果有左孩子
            if (node->_left != nullptr)
            {
                // 保存左孩子指针
                Node* left = node->_left;
                // 删除当前节点
                delete node;
                // 返回左孩子指针
                return left;
            }
            // 如果有右孩子
            else if (node->_right != nullptr)
            {
                // 保存右孩子指针
                Node* right = node->_right;
                // 删除当前节点
                delete node;
                // 返回右孩子指针
                return right;
            }
            // 如果没有孩子
            else
            {
                // 返回空指针
                delete node;
                return nullptr;
            }
        }
    }

    // 返回调整后的节点指针
    return node;
}

6. 判断是否为AVL树

在判断一棵树是否为 AVL 树时,其核心在于检查每一个节点的左右子树高度差的绝对值是否小于 1 。由于需要对整棵树的所有子树进行这样的判断,所以采用递归的方法比较合适的。

具体实现时,定义一个函数来进行判断。然后,分别递归地获取左子树和右子树的高度。接着,进行条件判断。如果左子树或右子树中存在高度标记为 -1 的情况(意味着该子树不平衡),或者当前节点左右子树高度差的绝对值大于 1 ,那么就将当前子树的高度标记为 -1 并返回。如果当前节点的子树都是平衡的,那么就返回最高的高度。

最后,通过一个总函数来调用这个递归函数,并根据返回结果是否大于 0 来确定整棵树是否为 AVL 树。如果大于 0 ,则表示整棵树是AVL 树;否则,表示不是AVL树。

//判断是否平衡
bool isBalanced()
{
    return _isBalanced(_root) >= 0;
}
int _isBalanced(Node* root)
{
    if (root == nullptr)
    {
        return 0;
    }
    //左平衡高度
    int left_isBalanced = _isBalanced(root->_left);
    //右平衡高度
    int right_isBalanced = _isBalanced(root->_right);
    //如果右平衡-左平衡则返回-1,并且防止两边同时返回-1相减等于0的情况,需要单独判断
    if (left_isBalanced == -1 || right_isBalanced == -1 || abs(right_isBalanced - left_isBalanced) > 1)
    {
        return -1;
    }
    //返回最高高度
    return max(left_isBalanced, right_isBalanced) + 1;
}

7. 复杂度分析

下是对上述AVL 树代码中主要操作的时间复杂度和空间复杂度分析:

时间复杂度

  • insert 操作:平均和最坏情况下的时间复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)。在插入过程中,通过不断调整平衡因子和进行旋转操作来保持树的平衡,每次调整和旋转的操作时间都是常数级,而查找插入位置的过程类似于二叉搜索树,时间复杂度为
    O ( log ⁡ n ) O(\log n) O(logn)
  • Find 操作:平均和最坏情况下的时间复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)。与二叉搜索树的查找过程类似,每次比较都将搜索范围缩小一半。
  • erase 操作:平均和最坏情况下的时间复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)。删除节点时需要查找节点位置,然后进行调整和可能的旋转操作,时间复杂度类似于插入操作。

空间复杂度

  • insert 操作:空间复杂度为 O ( 1 ) O(1) O(1)。在插入过程中,主要的额外空间消耗在于创建新节点以及一些临时变量来存储指针和平衡因子等信息,这些都是常数级的空间消耗。
  • Find 操作:空间复杂度为 O ( 1 ) O(1) O(1)。在查找过程中,仅使用了一些固定数量的指针和临时变量,没有额外的大规模空间分配。
  • erase 操作:空间复杂度为 O ( 1 ) O(1) O(1)。删除操作中,主要的空间消耗在于临时变量和指针的存储,没有动态分配大规模的额外空间。

总的来说,上述各个操作的主要空间复杂度都为 O ( 1 ) O(1) O(1),整个 AVL 树的空间复杂度主要取决于树中节点的数量,即 O ( n ) O(n) O(n),其中 n n n 是节点的数量。

8. 源码

#pragma once
#include<utility>
#include<assert.h>
template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K, V>& val = pair<K, V>())
		:_kv(val)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
	pair<K, V> _kv;//kv模型
	int _bf;//平衡因子
	AVLTreeNode* _left;//左子树
	AVLTreeNode* _right;//右子树
	AVLTreeNode* _parent;//指向父节点
};
template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//...具体功能
	AVLTree()
	{}
	AVLTree(const AVLTree& t)
	{
		_root = copy(t._root);
	}
	AVLTree<K,V>& operator=(const AVLTree <K,V> t)
	{
		//赋值重载
		this->swap(_root, t._root);
		return *this;
	}
	void RotateR(Node*&parent)
	{
		// 获取父节点的左子节点
		Node* subL = parent->_left;
		// 获取父节点左子节点的右子节点
		Node* subLR = subL->_right;

		// 1.将subLR链接到parent的左边。
		parent->_left = subLR;
		// 如果父节点左子节点的右子节点存在,设置其父节点为当前父节点
		if (subLR)
		{
			subLR->_parent = parent;
		}
		// 获取父节点的父节点
		Node* ppNode = parent->_parent;
		// 2.将parent链接到subL的右边。
		subL->_right = parent;
		//parent的父节点指向subL
		parent->_parent = subL;
		// 如果父节点存在父节点
		if (ppNode)
		{
			// 如果父节点是其父亲节点的左子节点
			if (ppNode->_left == parent)
			{
				// 将父节点的父亲节点的左子节点设置为当前父节点的左子节点
				ppNode->_left = subL;
			}
			// 如果父节点是其父亲节点的右子节点
			else
			{
				// 将父节点的父亲节点的右子节点设置为当前父节点的左子节点
				ppNode->_right = subL;
			}
			// 设置父节点左子节点的父节点为父节点的父节点
			subL->_parent = ppNode;
		}
		// 如果父节点为根节点
		else
		{
			// 将根节点设置为父节点的左子节点
			_root = subL;
			// 将父节点左子节点的父节点设置为空
			subL->_parent = nullptr;
		}
		//3.将parent与subL的平衡因子置为0。
		subL->_bf = parent->_bf = 0;
		//改变parent节点的指向,方便erase中的node返回
		parent = subL;
	}
	void RotateL(Node* &parent)
	{
		// 获取父节点的右子节点
		Node* subR = parent->_right;
		// 获取父节点右子节点的左子节点
		Node* subRL = subR->_left;
		//1.将subRL链接到parent的右边。
		parent->_right = subRL;
		// 如果父节点右子节点的左子节点存在,设置其父节点为当前父节点
		if (subRL)
		{
			subRL->_parent = parent;
		}
		// 获取父节点的父节点
		Node* ppNode = parent->_parent;
		//2.将parent链接到subR的左边。
		subR->_left = parent;
		// 设置父节点的父节点为右子节点
		parent->_parent = subR;

		// 如果父节点存在父节点
		if (ppNode)
		{
			// 如果父节点是其父亲节点的左子节点
			if (ppNode->_left == parent)
			{
				// 将父节点的父亲节点的左子节点设置为当前父节点的右子节点
				ppNode->_left = subR;
			}
			// 如果父节点是其父亲节点的右子节点
			else
			{
				// 将父节点的父亲节点的右子节点设置为当前父节点的右子节点
				ppNode->_right = subR;
			}
			// 设置父节点右子节点的父节点为父节点的父节点
			subR->_parent = ppNode;
		}
		// 如果父节点为根节点
		else
		{
			// 将根节点设置为父节点的右子节点
			_root = subR;
			// 将父节点右子节点的父节点设置为空
			subR->_parent = nullptr;
		}
		// 3.将parent与subR的平衡因子置为0。
		subR->_bf = parent->_bf = 0;
		//改变parent节点的指向,方便erase中的node返回
		parent = subR;
	}
	void RotateLR(Node*&parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//先左旋
		RotateL(parent->_left);
		//再右旋
		RotateR(parent);
		int bf = subLR->_bf;
		//1.情况1
		if (bf == 0)
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		//2.情况2
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		//3.情况3
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else
		{
			//走到这里说明AVL树有问题
			assert(false);
		}
	}
	
	void RotateRL(Node*&parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		//先右单旋
		RotateR(parent->_right);
		//再左单旋
		RotateL(parent);
		int bf = subRL->_bf;
		//1.情况一
		if (bf == 0)
		{
			parent->_bf = subRL->_bf = subR->_bf = 0;
		}
		//2.情况二
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		//3.情况三
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			//走到这里说明AVL树出错
			assert(false);
		}
	}
	bool insert(const pair<K, V>& kv)
	{
		// 如果根节点为空,创建新节点作为根节点并设置平衡因子为 0,然后返回插入成功
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_bf = 0;
			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->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;

		// 更新平衡因子
		while (parent)
		{
			// 如果插入在左边,父节点的平衡因子减 1
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			// 如果插入在右边,父节点的平衡因子加 1
			else
			{
				parent->_bf++;
			}
			// 如果父节点平衡因子为 0,整体高度不变,不需要再向上调整
			if (parent->_bf == 0)
			{
				break;
			}
			// 平衡因子为 1 或 -1,需要继续向上调整
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			// 平衡因子为 -2 或 2,需要进行旋转操作以保持平衡
			else if (parent->_bf == -2 || parent->_bf == 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);
				}

				break;
			}
			// 插入之前 AVL 树就已经不平衡了,断言
			else
			{
				assert(false);
			}
		}

		return true;
    }
	Node* Find(const K& val)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first > val)
			{
				//左子树中查找
				cur = cur->_left;
			}
			else if (cur->_kv.first < val)
			{
				//右子树中查找
				cur = cur->_right;

			}
			else
			{
				//找到了
				return cur;
			}
		}
		return nullptr;
	}
	//求高度
	int Hegiht(Node* root)
	{
		if (root == nullptr)
			return 0;

		int leftHegiht = Hegiht(root->_left);//先计算左子树高度
		int rightHegiht = Hegiht(root->_right);//再计算右子树高度

		return leftHegiht > rightHegiht ? leftHegiht + 1 : rightHegiht + 1;
	}
	void erase(const K& val)
	{
		//递归删除
		_root = _erase(_root, val);
	}
	//判断是否平衡
	bool isBalanced()
	{
		return _isBalanced(_root) >= 0;
	}
	~AVLTree()
	{
		Destroy(_root);
	}

private:
	void Destroy(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		//cout << root->_kv.first << " ";
		//递归销毁左子树
		Destroy(root->_left);
		//递归销毁右子树
		Destroy(root->_right);
		//销毁根节点
		delete root;
		//root = nullptr;
	}
	Node* _erase(Node* node, const K& val)
	{
		// 如果当前节点为空,直接返回空指针
		if (node == nullptr)
		{
			return nullptr;
		}

		// 如果当前节点的键值大于要删除的键值,在左子树中删除
		if (node->_kv.first > val)
		{
			node->_left = _erase(node->_left, val);
			//更新节点的父节点
			if (node->_left)
			node->_left->_parent = node;
			// 调整节点的平衡因子
			node->_bf = Hegiht(node->_right) - Hegiht(node->_left);
			int bf = node->_bf;
			//情况一删除节点都在左边
			if (bf == 2)
			{
				int rightHegiht = Hegiht(node->_right->_right);
				int leftHegiht = Hegiht(node->_right->_left);
				if (rightHegiht >= leftHegiht)
				{
					RotateL(node);
				}
				else
				{
					RotateRL(node);
				}
			}
			
		}
		// 如果当前节点的键值小于要删除的键值,在右子树中删除
		else if (node->_kv.first < val)
		{
			node->_right = _erase(node->_right, val);
			//更新节点的父节点
			if(node->_right)
			node->_right->_parent = node;
			// 调整节点平衡因子
			node->_bf = Hegiht(node->_right) - Hegiht(node->_left);
			int bf = node->_bf;
			//情况二删除节点都在右边
			if (bf == -2)
			{
				int rightHegiht = Hegiht(node->_left->_right);
				int leftHegiht = Hegiht(node->_left->_left);
				if (leftHegiht >= rightHegiht)
				{
					RotateR(node);
				}
				else
				{
					RotateLR(node);
				}
			}
		}
		// 找到要删除的节点
		else
		{
			// 如果节点有两个孩子
			if (node->_left != nullptr && node->_right != nullptr)
			{
				// 如果左子树高度大于等于右子树高度
				if (Hegiht(node->_left) >= Hegiht(node->_right))
				{
					// 找到左子树中的最大节点
					Node* prev = node->_left;
					while (prev->_right)
						prev = prev->_right;
					int target = prev->_kv.first;
					// 将当前节点的值替换为左子树中的最大节点的值
					node->_kv = prev->_kv;
					// 在左子树中删除该最大节点
					node->_left = _erase(node->_left,target);
				}
				else
				{
					// 找到右子树中的最小节点
					Node* post = node->_right;
					while (post->_left)
						post = post->_left;
					// 将当前节点的值替换为右子树中的最小节点的值
					int target = post->_kv.first;
					node ->_kv = post->_kv ;
					// 在右子树中删除该最小节点
					node->_right = _erase(node->_right, target);
				}
			}
			// 如果节点最多有一个孩子
			else
			{
				// 如果有左孩子
				if (node->_left != nullptr)
				{
					// 保存左孩子指针
					Node* left = node->_left;
					// 删除当前节点
					delete node;
					// 返回左孩子指针
					return left;
				}
				// 如果有右孩子
				else if (node->_right != nullptr)
				{
					// 保存右孩子指针
					Node* right = node->_right;
					// 删除当前节点
					delete node;
					// 返回右孩子指针
					return right;
				}
				// 如果没有孩子
				else
				{
					// 返回空指针
					delete node;
					return nullptr;
				}
			}
		}

		// 返回调整后的节点指针
		return node;
	}
	int _isBalanced(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		//左平衡高度
		int left_isBalanced = _isBalanced(root->_left);
		//右平衡高度
		int right_isBalanced = _isBalanced(root->_right);
		//如果右平衡-左平衡则返回-1,并且防止两边同时返回-1相减等于0的情况,需要单独判断
		if (left_isBalanced == -1 || right_isBalanced == -1 || abs(right_isBalanced - left_isBalanced) > 1)
		{
			return -1;
		}
		//返回最高高度
		return max(left_isBalanced, right_isBalanced) + 1;
	}
	Node* copy(Node* root)
	{
		// 如果传入的根节点为空,直接返回空指针,表示拷贝结束
		if (root == nullptr)
			return nullptr;
		// 为新树创建一个与原始根节点值相同的新节点
		Node* newnode = new Node(root->_kv);
		// 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针
		newnode->_left = copy(root->_left);
		// 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针
		newnode->_right = copy(root->_right);
		// 新节点的父节点默认为空
		newnode->_parent = nullptr;
		// 如果新节点的左子节点存在,设置其父节点为新节点
		if (newnode->_left)
		{
			newnode->_left->_parent = newnode;
		}
		// 如果新节点的右子节点存在,设置其父节点为新节点
		if (newnode->_right)
		{
			newnode->_right->_parent = newnode;
		}
		// 返回新树的根节点指针
		return newnode;
	}
	Node* _root = nullptr;//根节点
};
			}
				// 如果有右孩子
				else if (node->_right != nullptr)
				{
					// 保存右孩子指针
					Node* right = node->_right;
					// 删除当前节点
					delete node;
					// 返回右孩子指针
					return right;
				}
				// 如果没有孩子
				else
				{
					// 返回空指针
					delete node;
					return nullptr;
				}
			}
		}

		// 返回调整后的节点指针
		return node;
	}
	int _isBalanced(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		//左平衡高度
		int left_isBalanced = _isBalanced(root->_left);
		//右平衡高度
		int right_isBalanced = _isBalanced(root->_right);
		//如果右平衡-左平衡则返回-1,并且防止两边同时返回-1相减等于0的情况,需要单独判断
		if (left_isBalanced == -1 || right_isBalanced == -1 || abs(right_isBalanced - left_isBalanced) > 1)
		{
			return -1;
		}
		//返回最高高度
		return max(left_isBalanced, right_isBalanced) + 1;
	}
	Node* copy(Node* root)
	{
		// 如果传入的根节点为空,直接返回空指针,表示拷贝结束
		if (root == nullptr)
			return nullptr;
		// 为新树创建一个与原始根节点值相同的新节点
		Node* newnode = new Node(root->_kv);
		// 递归地拷贝原始根节点的左子树,并将结果赋给新节点的左指针
		newnode->_left = copy(root->_left);
		// 递归地拷贝原始根节点的右子树,并将结果赋给新节点的右指针
		newnode->_right = copy(root->_right);
		// 新节点的父节点默认为空
		newnode->_parent = nullptr;
		// 如果新节点的左子节点存在,设置其父节点为新节点
		if (newnode->_left)
		{
			newnode->_left->_parent = newnode;
		}
		// 如果新节点的右子节点存在,设置其父节点为新节点
		if (newnode->_right)
		{
			newnode->_right->_parent = newnode;
		}
		// 返回新树的根节点指针
		return newnode;
	}
	Node* _root = nullptr;//根节点
};

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

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

相关文章

鸿蒙开发APP应用UX体验标准

基础体验 应用导航 3.1.1.1 系统返回 页面布局 3.1.2.1 布局基础要求 3.1.2.2 挖孔区适配 人机交互 3.1.3.1 避免与系统手势冲突3.1.3.2 典型手势时长设计3.1.3.3 点击热区 视觉风格 3.1.4.1 色彩对比度3.1.4.2 字体大小 3.1.4.3 图标 3.1.4.3.1 应用图标3.1.4.3.2 界…

统一响应结果封装,Result类的实现【后端 06】

统一响应结果封装&#xff0c;Result类的实现 在开发Web应用或API接口时&#xff0c;如何优雅地处理并返回响应结果是每个开发者都需要考虑的问题。统一响应结果封装&#xff08;Unified Response Encapsulation&#xff09;作为一种广泛采用的实践&#xff0c;不仅提高了API的…

快讯 | OpenAI 找回场子:chatgpt-4o-latest 刷新多项AI跑分纪录

在数字化浪潮的推动下&#xff0c;人工智能&#xff08;AI&#xff09;正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展&#xff0c;捕捉行业动态&#xff1b;提供深入的新闻解读&#xff0c;助您洞悉技术背后的逻辑&#xff1b;汇聚行业专家的见解&#xff0c;…

LeetCode 205 同构字符串

题目 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符的顺序。不同字符不能映射到同一个字符上&#xff0c…

边缘智能:让每一个温室都成为计算中心

&#xff08; 于景鑫 国家农业信息化工程技术研究中心&#xff09;当人工智能的浪潮席卷全球&#xff0c;大语言模型&#xff08;LLM&#xff09;引领智能风潮之时&#xff0c;"智慧农业"也摩拳擦掌跃跃欲试。设施农业作为现代农业的翘楚&#xff0c;正站在数智化变革…

C语言典型例题38

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 例题3.5 写程序&#xff0c;判断某一年是否为闰年 代码&#xff1a; //《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 //例题3.5 写程序&#xff0c;判断某一年是否为闰年//相关知识&#xff1a;如果…

观存储历史,论数据未来

数据存储 这几天我反复观看了腾讯云社区的《中国数据库前世今生》纪录片&#xff0c;每次的感受都大相径庭。以下是我在这段时间里对纪录片的两个不同感想&#xff0c;希望感兴趣的小伙伴们也能去观看一番。 一个是关于国产数据库的发展趋势的探讨&#xff1a;https://blog.c…

使用 C# 反射查询程序集的元数据 (LINQ)

文章目录 1. 反射概述2. LINQ 概述3. 使用反射和 LINQ 查询程序集的元数据4. 扩展&#xff1a;查询字段和属性5. 扩展示例&#xff1a;查询公共类及其属性和方法6. 总结 在 C# 中&#xff0c;反射是一个强大的工具&#xff0c;它允许我们在运行时检查程序集、类型、方法等的元数…

机器学习速成第二集——监督学习之回归(理论部分)!

目录 回归算法 线性回归与非线性回归在实际应用中的优缺点比较是什么&#xff1f; 线性回归的优缺点 非线性回归的优缺点 优点&#xff1a; 缺点&#xff1a; 多项式回归模型如何选择最佳的多项数以提高预测准确性&#xff1f; 岭回归和套索回归在防止过拟合方面的具体…

【屏驱MCU】实现文件路径的的挂载

说明&#xff1a;本文涉及到一些底层的 .py 编译脚本以及编辑原理&#xff0c;笔者也不是完全明白&#xff0c;本文的主要目的是介绍一下流程&#xff0c;供小白使用。 接上文&#xff1a;【屏驱MCU】RT-Thread 文件系统接口解析 屏驱MCU系列文章 【屏显MCU】多媒体接口总结&am…

【Python学习-UI界面】PyQt5 小部件6- QComboBox

样式如下: 一个 QComboBox 对象呈现一个下拉列表供选择。它在表单上占用的屏幕空间最小&#xff0c;仅显示当前选定项。 可以将组合框设置为可编辑&#xff1b;还可以存储像素映射对象。 常用方法如下&#xff1a; 序号方法描述1addItem将字符串添加到集合中2addItems在列…

第十一章、 Java常用类

第十一章、 Java常用类 11.1 包装类 11.1.1 包装类的分类 针对八种基本数据类型相应的引用类型-包装类有了类的特点&#xff0c;就可以调用类中的方法。 11.1.2 包装类和基本数据的转换 Jdk5前的手动装箱和拆箱方式&#xff0c;装箱&#xff1a;基本类型->包装类型&am…

Mysql——对数据基本操作(增删查改)——操纵语言(DML)

之前的创建数据库和创建表&#xff0c;类型、约束都是用的DDL【data definition language】 数据定义语言&#xff0c;用来维护存储数据的结构 代表指令: create, drop, alter 那么现在我们来学习数据操纵语言 DML【data manipulation language】 数据操纵语言&#xff0c;用来…

现网/生产/一线问题记录

为信息安全考虑&#xff0c;涉及到公司保密信息的&#xff0c;用某来代替 文章目录 问题现象定位过程查看节点日志分析重启原因发现kafka消息积压分析dump追踪代码 定位结论经验总结&#xff0c;编码教训 问题现象 凌晨升级微服务&#xff0c;维护通知升级后某微服务频繁重启…

【Python开发】Python环境安装(Python3.8.0)与VS Code配置相应环境

一、安装Python环境 Python3.8.0下载连接 下载好后同意用户协议并点击安装 等待安装 安装完成 二、检查Python环境 按住键盘上的【Win】键【R】键&#xff0c;并在弹出窗口输入cmd 在弹出界面输入“Python”后&#xff0c;按下键盘回车键 若提示如下则Python环境安装成功 三、…

深入理解 iOS 中的 AutoLayout(一)

目录 1.前言 一、AutoLayout 基本概念 1.AutoLayout的概念 1.外部的变化 2.内部的变化 3.AutoLayout和基于frame的布局 2.不使用约束进行自动布局 1.xib使用UIStackView 2.纯代码方式设置UIStackView 3.AutoLayout中的约束 1.Auto Layout中的属性 1.边距约束 2.宽…

软件架构设计师-UML知识导图

软件架构设计师-UML知识导图&#xff0c;包含如下内容&#xff1a; 结构化设计&#xff0c;包含结构化设计的概念、结构化设计的主要内容、概要设计、详细设计及模块设计原则&#xff1b;UML是什么&#xff1a;介绍UML是什么&#xff1b;UML的结构&#xff1a;构造块、公共机制…

【SpringCloud】RabbitMQ——五种方式实现发送和接收消息

SpringAMQP SpringAMQP是基于RabbitMQ封装的一套模板&#xff0c;并且还利用SpringBoot对其实现了自动装配。 SpringAmqp的官方地址&#xff1a;https://spring.io/projects/spring-amqp SpringAMQP提供了三个功能&#xff1a; 自动声明队列、交换机及其绑定关系基于注解的…

Docker基本语法

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、更新yum镜像仓库&#xff08;一&#xff09;查看本地yum镜像源地址&#xff08;二&#xff09;设置docker的镜像仓库&#xff08;1&#xff09;安装必要工具…

安卓相关环境配置

安卓相关环境配置 偶尔更新。。。 JEB&#xff08;动态调试好用&#xff09; JEB动态调试Smali-真机/模拟器&#xff08;详细&#xff0c;新手必看&#xff09; 夜步城 JADX官网&#xff08;静态分析&#xff09; https://github.com/skylot/jadx/releases/tag/v1.5.0 雷…