C++ 红黑树插入详解

news2024/11/24 9:12:52

前言

        在之前,我们学习了AVL树,知道了AVL树是一个平衡二叉搜索树,如果没学过AVL树,这篇文章看起来会很吃力,不清楚如何旋转的,建议可以先看AVL树的内容。

        今天我们要学习的红黑树,他也是一颗平衡二叉搜索树,只不过他不像AVL树那样是绝对平衡的二叉搜索树,而是一种近视平衡,是不是知道这一点后,感觉红黑树也没有那么神秘了。

目录

一、红黑树概念

二、红黑树性质

三、红黑树插入结点

1.红黑树的结点

2.找到应该插入的位置

3.插入结点

4.判断是否需要处理

4.1叔叔存在且为红色

4.2叔叔不存在或者存在是黑色

4.2.1叔叔不存在

4.2.2叔叔存在且为黑 

4.2.3总结

四、判断是否为红黑树

五、中序遍历

六、总代码


一、红黑树概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的

二、红黑树性质

1. 每个结点不是红色就是黑色

2. 根节点是黑色的

3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 

4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

第1点和第2点都很好理解。不是红就是黑,根为黑。

对于第3点:“如果一个节点是红色,那么它的两个孩子的结点是黑色的”,这句话我们可以翻译一下,他代表着不可能出现两个连续的红色结点

对于第4点,从某一个结点开始到其所有后代叶节点的路径上,黑色结点的数量相同。这里指的叶子结点是空节点

如下左图,一共有7条路径。是红黑色       下右图,最右路径只有两个黑色结点,不是红黑树

  • 那么,有了这几点性质,为何就能保证我们概念里所说到的最长路径不超过最短路径的两倍呢? 

因为,最长路径为一黑一红(从第3点可知,不能有连续的红色结点),最短路径为全为黑色(因为有了红色结点就会增加长度)。并且由第4点可知,每一条路径上的黑色结点都相同,如果每条路径上的黑色节点为n个,那么最长路径可以为2n,最短路径可以为n。这样就可以保证最长路径不超过最短路径的两倍。

由此我们可以发现,红黑树的查找效率并不会比AVL树差很多,他们的查找时间复杂度都是log(n) ,只是常数的差距。那么何为Map和Set的底层要用红黑树而不是AVL树呢?因为红黑树查找效率不差,插入效率会高一些,(没有严格的高度差,首先考虑变色,不得已才旋转)。

三、红黑树插入结点

1.红黑树的结点

红黑树的结点也要用到三叉链,就是有左右子树结点,还有父亲节点,还有一个_key值存储数据,和一个_col存放结点是什么颜色(_col用了枚举来方便我们管理)

enum color
{
	RED,
	BLACK
};

template<class K>
struct RBTreeNode
{
	RBTreeNode<K>* _parent;
	RBTreeNode<K>* _left;
	RBTreeNode<K>* _right;
	K _key;
	color _col;
	RBTreeNode(const K& key)
		:_key(key)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_col(RED)
	{}
};

对于RBTreeNode的构造函数,_key、_parent、_left、_right的初始化没问题,也一直都能理解,那为什么要将结点的颜色初始化为红色呢? 黑色不行吗

这也要涉及到红黑树的性质3和4

3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 

4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 

如果新节点为红色,我们可能就会破坏掉第3条性质,如果新节点的父亲为黑,则不需要处理,如果父亲为红,才需要处理。并且我们只会破坏当前路径的规则,对于其他路径并不涉及

如果新节点为黑色,我们会破坏掉第4条的性质,使得每条路径的黑色结点不同,似乎是对整棵树都会有影响,这样非常不方便我们处理。

因此在选择新结点颜色为红(得罪一个)还是为黑(得罪一群),我们选择为红(得罪一个)

2.找到应该插入的位置

 我们将红黑树封装为类,他有私有成员根节点_root。对于应该插入的位置,由于红黑树是一颗二叉搜索树,因此我们可以通过Key值去判断他应该存放在左子树还是右子树,具体情况二叉搜索树中有讲到,可以直接Copy一份过来。

template<class K>
class RBTree
{
	typedef RBTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		Node* cur = _root;
		Node* parent = _root;
		if (_root == nullptr)
		{
			_root = new Node(key);
			_root->_col = BLACK;
			return true;
		}
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
    }
private:
    Node* _root;
};

3.插入结点

从上面的代码可知,现在已经有了parent的位置了。但不知道应该插在parent的左还是右,这里可以通过parent的_key和我们插入结点所传递的key作比较,父亲的_key比插入的key大就插入在父亲的左边父亲的_key比插入的key小就插入在父亲的右边。

代码如下

        if (parent->_key > key)
		{
			parent->_left = new Node(key);
			cur = parent->_left;
		}
		else
		{
			parent->_right = new Node(key);
			cur = parent->_right;
		}
		cur->_parent = parent;

4.判断是否需要处理

我们所插入的新节点是红色的,如果parent为黑色,则不需要处理,parent是红色,才需要处理。(规则3:不能有连续的红节点)那么我们应该如何处理呢??先说答案(看叔叔

4.1叔叔存在且为红色

如下图,我们插入了cur结点或者cur结点是由下面的节点变色而来,首先应该将父节点p变黑(规则3,不能有连续的红色结点),那么我p变黑了,如果叔叔u的颜色为红色,那么叔叔u也得变成黑色(规则4,每条路径黑色节点相同),那么还得将祖父g变红,目的是让这条路径黑色结点个数没有变(如果不变红,黑色结点个数就从1变成了2),由于g被变红了,可能g的父亲也是红色结点,因此我们要继续往上更新

最后需要注意一点,如果祖父g是根节点,那么我们将他变成红色就违反了规则2(根节点是黑色),为防止这种情况,可以在while循环结束后,添加上"_root->_col=BLACK;"代码,保证无论如何根节点都给他置为黑色即可。

叔叔存在且为红代码如下

while (parent && parent->_col == RED)
{
	Node* grandparent = parent->_parent;

	if (grandparent->_left == parent)
	{
		//     g
		//   p   u
		// c
		Node* uncle = grandparent->_right;
		if (uncle && uncle->_col == RED)
		{
			//变颜色
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandparent->_col = RED;
			//往上更新
			cur = grandparent;
			parent = grandparent->_parent;
		}
    }
}
_root->_col=BLACK;

4.2叔叔不存在或者存在是黑色

4.2.1叔叔不存在

如果叔叔不存在,证明cur就为新增结点,并且树的结构一定如下图所示,因为无论在哪个结点左右添加上任意结点,都会破坏红黑树的性质。

这个时候,如果你只通过变色来处理,是不能够保持红黑树的性质的。因为,如果你将父亲p变黑,祖父g变红,如下图,会使得g往右边走的那条路径少一个结点,因此这种处理不可取。

  • 那我们应该如何处理呢?

需要用到之前AVL树提到的旋转,如下图,对祖结点g节点进行右旋。最后再将p变黑色,g变红色就完成了,这就保持了红黑树的性质,并且由于p是黑色结点,p的父亲无论是什么颜色,都不影响。可以选择break;防止出错,至于为什么会出错,看下面的。

  • 上图中cur为parent的左边,如果cur在parent的右边呢? 

先将p结点左旋成为上图的样子,再重复上图的步骤即可。只需要注意这样旋转后cur变成了当前树的根节点,变色应该将cur变黑,g变红,再break退出循环即可(一定要break)。

  • 为什么一定要break呢?

一是因为旋转后,已经满足红黑树的性质,不需要再次处理。

二是因为我们外层的循环条件 "parent && parent->_col == red" 已经不适用这种情况了,parent被旋转过,不在是cur的父节点

4.2.2叔叔存在且为黑 

叔叔存在且为黑,他跟上面的叔叔不存在是一样的,区别在于图是抽象图。如下图。

abcde不一定为空,对于祖父结点g来说,如果每条路径有两个黑色结点那么d和e可能有一个红色结点,或者没有结点c一定有一个黑色结点为根这个根的左子树和右子树一样要么不存在要么为红a和b为空节点。p结点为新增结点(为什么会这样,因为只有这样才不会破坏红黑树的性质)这一段分析应该不难理解(理解不了也没关系)

当然如果p是由下面a,b子树变色而来,情况就很复杂了,因此我们只画了抽象图。

对于上图这种情况依然是跟叔叔不存在一样进行旋转,对祖父g节点进行右旋,这时候p就变成了当前树的根结点,再将p变黑色,g变红色。一样尽量break。

对于cur在parent的右边,跟上面分析的叔叔不存在一样,对父节点p左旋,再对祖结点g右旋,再将g变红色,cur变黑色即可,看下图。

代码如下,旋转代码后续给出。

Node* grandparent = parent->_parent;

if (grandparent->_left == parent)
{
	//     g
	//   p   u
	// c
	Node* uncle = grandparent->_right;
	if (uncle && uncle->_col == RED)
	{
		//变颜色
		parent->_col = BLACK;
		uncle->_col = BLACK;
		grandparent->_col = RED;
		//往上更新
		cur = grandparent;
		parent = grandparent->_parent;
	}
	else
	{
		//     g
		//   p   u
		// c
		if(cur == parent->_left)
		{
			RotateR(grandparent);
			grandparent->_col = RED;
			parent->_col = BLACK;
			break;
		}
		//      g
		//   p     u
		//    c
		else
		{
			RotateL(parent);
			RotateR(grandparent);
			cur->_col = BLACK;
			grandparent->_col = RED;
			break;
		}
	}
}
4.2.3总结

刚刚我们讲解了如下grandparent->_left = parent ,父节点在祖父的左,叔叔在右的情况

对于如下grandparent->_right = parent ,父节点在祖父的右,叔叔在左的情况,跟上图刚刚好相反,代码也是类似的,就不再过多赘述了

附上代码

else  //grandparent->_right == parent
{
	//     g
	//   u   p
	//         c
	Node* uncle = grandparent->_left;
	if (uncle && uncle->_col == RED)
	{
		parent->_col = uncle->_col = BLACK;
		grandparent->_col = RED;
		cur = grandparent;
		parent = grandparent->_parent;
	}
	else
	{
		//     g
		//   u   p
		//         c
		if (parent->_right == cur)
		{
			RotateL(grandparent);
			parent->_col = BLACK;
			grandparent->_col = RED;
			break;
		}
		else
		{
			//     g
			//   u   p
			//      c
			RotateR(parent);
			RotateL(grandparent);

			cur->_col = BLACK;
			grandparent->_col = RED;
			break;
		}
	}
}

四、判断是否为红黑树

要判断是否为红黑树,我们需要根据它的性质来进行判断。

性质1:非红即黑                                  ------ 不需要判断,肯定非红即黑

性质2:根为黑                                     ------- 简单,直接if判断即可

难点在于判断他的性质3和性质4

性质3:不能有连续的红色节点

        由于二叉树有左右子树,如果当前结点颜色为红色,我们去判断他的孩子是否为红,因为有两个孩子,并且孩子可能还不存在,这样情况会比较复杂。如果我们选择判断当前结点的父亲是否为红色,这样可以节省很多时间,并且只有根节点没有父亲,其他结点都有父亲,这样遍历较好。

性质4:每条路径黑色结点的个数相同

        这个似乎乍眼一看,还挺难判断的,但是也有技巧,我们可以随便找出一条路径的黑色结点个数,记为valRef,再传递给递归函数去判断。同时传递当前黑色结点的个数,记为BlackNum,一开始为0,如果递归函数遇到一个黑结点,就将BlackNum+1,直到走到了空,证明这条路径走完了,可以将valRef和BlackNum进行相等判断,相等就return true,继续遍历,不相等就return false,会递归回来结束遍历。

 具体代码如下:

bool IsBalance()
{
	if (_root == nullptr)
		return true;
	if (_root->_col == RED)
		return false;
    //valRef为路径上黑色结点的个数
	int valRef = 0;
	Node* cur = _root;
	while (cur)   //一直往某条路径走,找出这条路径黑色结点的个数
	{
		if (cur->_col == BLACK)
			valRef++;
		cur = cur->_left;
	}
	int BlackNum = 0;
	return Check(_root,BlackNum,valRef);
}

bool Check(Node* root,int BlackNum,int valRef)
{
	if (root == nullptr)
	{
		if(BlackNum == valRef)
		{
			return true;
		}
		else
		{
			cout << "每条路径黑色结点个数不等" << endl;
			return false;
		}
	}
	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << "有连续的红色节点" << endl;
		return false;
	}
	if (root->_col == BLACK)
	{
		BlackNum++;
	}
	return Check(root->_left,BlackNum,valRef) && Check(root->_right, BlackNum, valRef);
}

五、中序遍历

还有中序递归遍历,这是我们老生常谈的东西,不对赘述了,附上代码 

void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

六、总代码

 附上总代码(左旋右旋也在里面)RBTree.h

#pragma once

enum color
{
	RED,
	BLACK
};

template<class K>
struct RBTreeNode
{
	RBTreeNode<K>* _parent;
	RBTreeNode<K>* _left;
	RBTreeNode<K>* _right;
	K _key;
	color _col;
	RBTreeNode(const K& key)
		:_key(key)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_col(RED)
	{}
};

template<class K>
class RBTree
{
	typedef RBTreeNode<K> Node;
public:
	bool Insert(const K& key)
	{
		Node* cur = _root;
		Node* parent = _root;
		if (_root == nullptr)
		{
			_root = new Node(key);
			_root->_col = BLACK;
			return true;
		}
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		if (parent->_key > key)
		{
			parent->_left = new Node(key);
			cur = parent->_left;
		}
		else
		{
			parent->_right = new Node(key);
			cur = parent->_right;
		}
		cur->_parent = parent;
		while (parent && parent->_col == RED)
		{
			Node* grandparent = parent->_parent;

			if (grandparent->_left == parent)
			{
				//     g
				//   p   u
				// c
				Node* uncle = grandparent->_right;
				if (uncle && uncle->_col == RED)
				{
					//变颜色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandparent->_col = RED;
					//往上更新
					cur = grandparent;
					parent = grandparent->_parent;
				}
				else
				{
					//     g
					//   p   u
					// c
					if(cur == parent->_left)
					{
						RotateR(grandparent);
						grandparent->_col = RED;
						parent->_col = BLACK;
						break;
					}
					//      g
					//   p     u
					//    c
					else
					{
						RotateL(parent);
						RotateR(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
						break;
					}
				}
			}
			else  //grandparent->_right == parent
			{
				//     g
				//   u   p
				//         c
				Node* uncle = grandparent->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandparent->_col = RED;
					cur = grandparent;
					parent = grandparent->_parent;
				}
				else
				{
					//     g
					//   u   p
					//         c
					if (parent->_right == cur)
					{
						RotateL(grandparent);
						parent->_col = BLACK;
						grandparent->_col = RED;
						break;
					}
					else
					{
						//     g
						//   u   p
						//      c
						RotateR(parent);
						RotateL(grandparent);

						cur->_col = BLACK;
						grandparent->_col = RED;
						break;
					}
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool Check(Node* root,int BlackNum,int valRef)
	{
		if (root == nullptr)
		{
			if(BlackNum == valRef)
			{
				return true;
			}
			else
			{
				cout << "每条路径黑色结点个数不等" << endl;
				return false;
			}
		}
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "有连续的红色节点" << endl;
			return false;
		}
		if (root->_col == BLACK)
		{
			BlackNum++;
		}
		return Check(root->_left,BlackNum,valRef) && Check(root->_right, BlackNum, valRef);
	}

	bool IsBalance()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
			return false;
		int valRef = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				valRef++;
			cur = cur->_left;
		}
		int BlackNum = 0;
		return Check(_root,BlackNum,valRef);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		subL->_right = parent;
		Node* grandparent = parent->_parent;
		parent->_parent = subL;
		if (grandparent == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
			return;
		}
		if (grandparent->_left == parent)
		{
			grandparent->_left = subL;
		}
		else
		{
			grandparent->_right = subL;
		}
		subL->_parent = grandparent;
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;
		subR->_left = parent;
		Node* grandparent = parent->_parent;
		parent->_parent = subR;
		if (grandparent == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
			return;
		}
		if (grandparent->_left == parent)
		{
			grandparent->_left = subR;
		}
		else
		{
			grandparent->_right = subR;
		}
		subR->_parent = grandparent;
	}
	Node* _root = nullptr;
};

 test.cpp文件

#include<iostream>
#include<vector>
#include<ctime>
using namespace std;
#include"RBTree.h"

//int main()
//{
//	RBTree<int> rbt;
//	int arr[] = { 7, 6, 2, 1, 3, 6, 8, 9, 7, 10 };
//	for(auto e : arr)
//	{
//		rbt.Insert(e);
//	}
//	rbt.InOrder();
//	cout << rbt.IsBalance() << endl;
//	return 0;
//}

int main()
{
	const int N = 1000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}
	RBTree<int> rbt;
	for (auto e : v)
	{
		if (e == 24473)
		{
			int i = 0;
		}
		rbt.Insert(e);
		rbt.IsBalance();
	}
	rbt.InOrder();
	cout << rbt.IsBalance() << endl;
	return 0;
}

测试 

感谢大家观看!!!

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

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

相关文章

串口通信 HAL库+cubeMX

一.通信的基本概念 1.串行通信和并行通信 2.全双工、半双工和单工 3.针对串行通信的同步通信和异步通信 4.通信速率 二.UART配置 UART常用HAL库函数 //UART_HandleTypeDef *huart是句柄typedef struct {//初始化看前面两个就可以了USART_TypeDef *Instance; /* UART 寄存器…

软件提示vcruntime140_1.dll文件丢失解决方法,和vcruntime140_1.dll丢失原因分析

vcruntime140_1.dll是Visual C Redistributable Packages的一部分&#xff0c;它是Microsoft Visual C 2015 Update 3运行时库文件。它包含了许多C标准库函数的实现&#xff0c;这些函数在运行使用C编写的程序时会被调用。所以我们在打开运行软件时候如果计算机中的vcruntime14…

(十三)Flask之特殊装饰器详解

目录&#xff1a; Flask中用作装饰器的特殊的函数第一部分&#xff1a;before_request和after_request一、 before_request装饰器&#xff1a;二、after_request装饰器&#xff1a;三、多个before_request和after_request执行流程分析&#xff1a; 首先—理论讲解&#xff1a;然…

Java核心知识点整理大全18-笔记

Java核心知识点整理大全-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全2-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全3-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全4-笔记-CSDN博客 Java核心知识点整理大全5-笔记-CSDN博客 Java核心知识点整理大全6…

沈阳陪诊系统|陪诊软件开发功能

陪诊小程序的出现它可以帮助患者或家属解决就医过程中的各种问题。根据数据显示&#xff0c;2021年中国陪诊市场规模约为36.7亿元&#xff0c;预计到2025年将达到100亿元。同时&#xff0c;在医疗行业数字化转型的大背景下&#xff0c;陪诊微信小程序作为一种创新的医疗服务模式…

ABAP: JSON 报文解析——/ui2/cl_json

1、JSON数组 报文格式如下&#xff0c;是JSON 数组类型的。 [{"I_TYPE":"V","I_BUSINESSSCOPE":"1001"},{"I_TYPE":"V","I_BUSINESSSCOPE":"1002"} ] json转换为SAP内表&#xff1a; TYP…

二 使用GPIO的复用功能 利用USART 实现printf()

参考这篇&#xff1a; STM32串口通信详解 1. 关于USART USART ( universal synchronous / asynchronous receiver /transmitter) 是一种串行通讯协议 , 允许设备通过串行端口进行数据传输&#xff0c; USART 能够以同步或者异步的方式进行工作&#xff0c;在实际的运用中&…

机器学习的复习笔记4-岭回归与多项式回归

一、岭回归 在简单的线性回归中&#xff0c;一味追求平方误差最小化&#xff0c;R2值尽可能大&#xff0c;可能会受到噪声的严重干扰。噪声&#xff0c;即偶发的错误的值。 如图&#xff0c;若为满足所有点的拟合&#xff08;虚线&#xff09;&#xff0c;表面上看R2值小&…

虚拟偶像的商业化

虚拟偶像的商业化主要通过以下几种方式实现&#xff1a; 直播与内容&#xff1a;虚拟主播在各大平台进行直播&#xff0c;提供音乐、游戏、教育等内容。收益主要包括Super Chat&#xff08;直播打赏&#xff09;、收费会员&#xff08;支付月费后成为该频道会员&#xff0c;可…

如何使用WMS仓储管理系统实现流程优化

随着企业对于物流管理的需求日益增长&#xff0c;自动化WMS仓储管理系统已经成为了现代企业的核心工具之一。通过引入信息化技术&#xff0c;我们可以实现仓库管理流程的不断调整和优化&#xff0c;从而更好地满足客户的多样化需求。 一、信息化技术的引领 在现代仓库管理中&a…

Docker和Kubernetes:区别与优势对比

在现代软件开发和部署中&#xff0c;Docker和Kubernetes是两个备受关注的技术。本文将对Docker和Kubernetes进行比较&#xff0c;探讨它们的区别和各自的优势。 引言 在过去的几年中&#xff0c;容器技术得到了迅速的发展&#xff0c;并且在现代软件交付和部署中扮演着越来越重…

如何获取抖音订单列表

怎么获取订单列表接口 请求地址&#xff1a;响应参数以及示例

为什么我用 AI 做不出好看的图?好说给你划重点

最近一个月&#xff0c;我们介绍了些实用的 AI 绘画教程&#xff0c;不知道看过的你是学会了吗&#xff1f;有不少朋友和小编说&#xff0c;有了这些教程&#xff0c;用好说 AI 的指令做起图来比 SD 方便了不少。 不过也有朋友就和小编抱怨&#xff1a;“你教程里的和我做出来…

Flask 实现Token认证机制

在Flask框架中&#xff0c;实现Token认证机制并不是一件复杂的事情。除了使用官方提供的flask_httpauth模块或者第三方模块flask-jwt&#xff0c;我们还可以考虑自己实现一个简易版的Token认证工具。自定义Token认证机制的本质是生成一个令牌&#xff08;Token&#xff09;&…

dubbo框架技术文档-《spring-boot整合dubbo框架搭建+配置文件》框架的本地基础搭建

阿丹&#xff1a; 目前流行的微服务更多的就是dubbo和springcould微服务。之前阿丹没有出过dubbo相关的文章&#xff0c;因为之前接触springcould的微服务概念比较多一点&#xff0c;但是相对于springcould来说&#xff0c;springcould服务之间的调用是大多是使用了nacos&#…

Cascader 级联选择器动态加载数据的回显

如果后端没有只返回第三级的id,而是同时把第三级的名字一起返回了&#xff0c;那么就可以通过下面的方法来实现 1.在级联选择器里面加上这句代码 placeholder"请选择" 2.注册一个字符串 pleasett:"" 3.赋值 如过后端返回的有第三级的选项名 直接进行赋…

记录一个mqtt错误

在vue-admin-template 中引入mqtt 安装不报错&#xff0c;引入试过 import mqtt from mqtt import * as mqtt from mqtt/dist/mqtt.min; import {connect} from mqtt 一直报错&#xff1a; 就表示不理解&#xff0c;网上也没查到相应的资料&#xff0c;请告诉我我不是第一个遇…

pytest系列——pytest_runtest_makereport钩子函数获取测试用例执行结果

前言 pytest测试框架提供的很多钩子函数方便我们对测试框架进行二次开发&#xff0c;可以根据自己的需求进行改造。 例如&#xff1a;钩子方法&#xff1a;pytest_runtest_makereport &#xff0c;可以更清晰的了解测试用例的执行过程&#xff0c;并获取到每个测试用例的执行…

Python基础:字符串(String)详解

1. 字符串定义 在Python中&#xff0c;字符串是一种数据类型&#xff0c;用于表示文本数据。字符串是由字符组成的序列&#xff0c;可以包含字母、数字、符号和空格等字符。在Python中&#xff0c;你可以使用单引号&#xff08;&#xff09;或双引号&#xff08;"&#xf…

【Python】巧用tkinter设计秒表计时器

秒表计时器是一种用于计算时间间隔的计时器。它通常由一个开始按钮、一个停止按钮和一个重置按钮组成。 使用方法&#xff1a; 单击“开始”按钮开始计时。单击“停止”按钮停止计时。单击“重置”按钮将计时器归零。 注意事项&#xff1a; 没有对计时器误差进行校准&#…