[数据结构]二叉搜索树详解

news2025/4/13 13:55:59

目录

一、二叉搜索树的概念

二、二叉搜索树的性能分析

三、二叉搜索树的中序遍历用于排序+去重

四、二叉搜索树的查找

1、查找的非递归写法

2、查找的递归写法

五、二叉搜索树的插入

1、插入的非递归写法

2、插入的递归写法

六、二叉搜索树的删除

1、删除的非递归写法

2、删除的递归写法

七、二叉搜索树的使用场景

1、key搜索模型(节点存key)

key搜索模型整体代码

2、key/value搜索模型(节点既存key又存value)

key/value搜索模型整体代码


一、二叉搜索树的概念

        二叉搜索树又称二叉排序树。

        空树是二叉搜索树,如果一棵树不是空树,需要满足如下情况便可称其为二叉搜索树:

        1、左子树上每一个键值均小于根节点;

        2、右子树上每一个键值均大于根节点;

        3、左右子树均为二叉搜索树。


二、二叉搜索树的性能分析

它可以用来排序 – 由于二叉搜索树的左子树都小于根,右子树都大于根,所以如果对二叉搜索树进行中序遍历得到的数据天然就是有序的

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

  • 最优情况下,二叉搜索树为完全二叉树 (或者接近完全二叉树),其平均比较次数为 O(logN)
  • 最差情况下,二叉搜索树退化为单支树( 或者类似单支),其平均比较次数为 O(N)。
  • 所以,二叉搜索树进行查找的时间复杂度为 O(N)。

可能有的同学会想,既然二叉搜索树查找的时间复杂度为 O(N),那我们为什么不直接用二分查找呢?毕竟二分查找的时间复杂度可是 O(logN),这是因为二分查找存在许多限制:

  • 二分查找要求数据必须有序;
  • 二分查找使用顺序表进行数据存储,插入、删除数据效率低,而在实际开发中,我们是要经常插入删除数据的;

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插 入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


三、二叉搜索树的中序遍历用于排序+去重

通过上面那张图不难发现,用二叉搜索树走个中序,就是升序+去重排序,这也是二叉搜索树又被称为二叉排序树的原因。

        使用InOrder调用_InOrder的原因是类外面传参传不了私有的_root,所以采用多套一层的方法。

//中序遍历
void _InOrder(Node* _root)
{
    if (_root == nullptr)
    {
        return;
    }
    _InOrder(_root->_left);
    std::cout << _root->_key << " ";
    _InOrder(_root->_right);
}
void InOrder()//因为外部取不到_root,所以这里套了一层调用函数
{
    _InOrder(_root);
    std::cout << std::endl;
}

四、二叉搜索树的查找

1、查找的非递归写法

bool Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_key < key)
        {
            cur = cur->_right;
        }
        else if (cur->_key > key)
        {
            cur = cur->_left;
        }
        else//说明找到了
            return true;
    }
	return false;
}

2、查找的递归写法

Node* _FindR(Node* root,const K& key)
{
    if (root == nullptr)
        return nullptr;
    if (root->_key < key)
    {
        return _FindR(root->_right, key);
    }
    else if (root->_key > key)
    {
        return _FindR(root->_left, key);
    }
    else
        return root;
}
bool FindR(const K& key)
{
    return _FindR(_root, key) == nullptr ? false : true;
}

五、二叉搜索树的插入

        二叉搜索树的插入需要考虑插入后,需要维持二叉搜索树的形态。

1、插入的非递归写法

bool Insert(const K& key)
{
    if (_root == nullptr)
    {
        _root = new Node(key);//BSTreeNode对象中存放key值,构造一个二叉搜索树节点 
    }
    else
    {
        Node* parent = nullptr;
        Node* cur = _root;
        //cur一直走,走到要插入的位置
        while (cur)
        {
            parent = cur;
            if (cur->_key < key)
            {
                cur = cur->_right;
            }
            else if (cur->_key > key)
            {
                cur = cur->_left;
            }
            else//说明数字重复,插入失败
                return false;
        }
        cur = new Node(key);
        //判断插入节点放在parent节点的左子树还是右子树
        if (parent->_key < key)
        {
            parent->_right = cur;
        }
        else
        {
            parent->_left = cur;
        }
    }
    return true;
}

        1、如果根是空,插入的节点就是新的根;

        2、如果根不为空,就先根据二叉搜索树的性质找到该节点要插入的位置,如果路上遇到相同的数,插入失败;

        3、再判断一下,是要插入父亲的左边还是右边即可。

2、插入的递归写法

bool _InsertR(Node*& root, const K& key)//形参是root的引用
{
    if (root == nullptr)
    {
        root = new Node(key);//因为root是父节点左/右孩子的别名,直接修改别名,链接关系存在,不用考虑父子节点连接关系
        return true;
    }
    if (root->_key < key)
        return _InsertR(root->_right, key);//看到这个root->_right没,它是下一层root的别名
    else if (root->_key > key)
        return _InsertR(root->_left, key);//看到这个root->_left没,它是下一层root的别名
    else//说明相等,插入失败
        return false;
}
bool InsertR(const K& key)
{
    return _InsertR(_root, key);
}

因为函数参数是父节点的左孩子/右孩子的别名,所以被修后不需要考虑链接关系。

六、二叉搜索树的删除

        二叉搜索树的节点进行删除后,同样需要维持二叉搜索树的形态。

        二叉搜索树的删除无非是三种情况:

1、删除的非递归写法

bool Erase(const K& key)
{
    Node* parent = nullptr;
    Node* cur = _root;
    //找到要删除的节点
    while (cur)
    {
        if (cur->_key < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_key > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else//说明找到要删除的节点了
        {
            //开始分析三种情况
            if (cur->_left == nullptr)//被删除节点左孩子为空。
            {
                if (cur == _root)//需要判断cur等于根节点的情况,否则else中parent空指针解引用了
                {
                    _root = _root->_right;
                }
                else
                {
                    if (parent->_left == cur)//确定cur是parent的左还是右,再进行“托孤”
                        parent->_left = cur->_right;
                    else
                        parent->_right = cur->_right;
                }	
                delete cur;
            }
            else if (cur->_right == nullptr)//被删除节点左孩子不为空,右孩子为空
            {
                if (cur == _root)
                {
                    _root = _root->_left;
                }
                else
                {
                    if (parent->_left == cur)
                        parent->_left = cur->_left;
                    else
                        parent->_right = cur->_left;
                }	
                delete cur;
            }
            else//被删除节点左右孩子均不为空
            {
                //左右孩子均不为空,就需要左子树的最大值或右子树的最小值选出来当新根(对被删除节点进行替换)
                Node* rightMin = cur->_right;//这里选用右树的最小值进行更换
                Node* rightMinParent = cur;
                while (rightMin->_left!=nullptr)//因为找最小值,不停找左树即可
                {
                    rightMinParent = rightMin;
                    rightMin = rightMin->_left;
                }
                //std::swap(cur->_key, rightMin->key);//用std的交换对自定义类型可能比较慢
                cur->_key = rightMin->_key;//还是用赋值好一点,即使是自定义类型,肯定有写赋值重载
                //rightMin的左节点必为空,判断父节点的链接方式即可
                if (rightMinParent->_left == rightMin)//两种情况,第一种如上方图删除8,实际干掉9位置,需要将10的左连至9的右
                    rightMinParent->_left = rightMin->_right;
                else if (rightMinParent->_right == rightMin)//第二种如上方图删除10,实际干掉14,需要将10的右连至14的右
                    rightMinParent->_right = rightMin->_right;
                delete rightMin;
            }
            return true;
        }
    }
    return false;
}

         1、先通过二叉搜索树的性质找到要删除的节点;

        2、找到需要删除的节点后,分三种情况进行讨论:

        一、被删除节点的左孩子为空,除了cur等于根节点情况下,其他情况下,父节点的孩子指针由指向被删除节点转为指向被删除节点的右孩子。(如图删除9和14)

        二、被删除节点的左孩子存在但右孩子为空,除了cur等于根节点情况下,其他情况下,父节点的孩子指针由指向被删除节点转为指向被删除节点的左孩子。(如图删除9)

        三、被删除的节点均不为空,可以选用左树最大节点或者右树最小节点对被删除节点进行值替换,问题转化为第一种或第二种情况。(详见代码注释)

2、删除的递归写法

bool _EarseR(Node*& root, const K& key)//形参给了引用,意义同插入的递归写法
{
    if (root == nullptr)
    {
        return false;
    }
    if (root->_key < key)
        return _EarseR(root->_right, key);
    else if (root->_key > key)
        return _EarseR(root->_left, key);
    else//说明找到了要删除的节点,无需考虑root的父亲为空
    {
        Node* del = root;
        if (root->_left == nullptr)//被删除节点的左为空
            root = root->_right;//让root连接root的右树,因为是引用,所以父节点和root是连接的
        else if (root->_right == nullptr)//被删除节点左不为空但右为空
            root = root->_left;
        else//root左右子树均不为空
        {
            Node* rightMin = root->_right;
            while (rightMin->_left!=nullptr)//找到被删除节点的右树最小节点 
            {
                rightMin = rightMin->_left;
            }
            root->_key = rightMin->_key;//找到了交换key
            //对子树进行递归删除
            return _EarseR(root->_right, rightMin->_key);//return表示子树进行删除,结束掉递归
        }
        delete del;
        return true;
    }
}
bool EraseR(const K& key)
{
    return _EarseR(_root, key);
}

 找到节点后,同样需要分三种情况讨论。

        1、被删除节点左树为空;

        2、被删除节点左树不为空但右树为空;

        3、被删除节点左右子树均不为空。


七、二叉搜索树的使用场景

1、key搜索模型(节点存key)

        key搜索模型只用key作关键码,结构中只需存key,key即为需要搜索到的值。

        例如对英语单词拼写的检查,可以将词库中的所有单词存入二叉搜索树,通过二叉搜索树中检索单词是否存在,达到拼写报错目的。

key搜索模型整体代码
template <class K>
struct BSTreeNode
{
	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
};
template <class K>
struct BSTree
{
	typedef BSTreeNode<K> Node;
	BSTree()
		:_root(nullptr)
	{}
	//插入节点
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);//BSTreeNode对象中存放key值 
		}
		else
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				parent = cur;
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else//说明数字重复
					return false;
			}
			cur = new Node(key);
			//判断插入节点放在parent节点的左子树还是右子树
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
		}
		return true;
	}
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
	//中序遍历
	void InOrder()//因为外部取不到_root,所以这里套了一层调用函数
	{
		_InOrder(_root);
		std::cout << std::endl;
	}
	//查找
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
				return true;
		}
		return false;
	}
	bool FindR(const K& key)
	{
		return _FindR(_root, key) == nullptr ? false : true;
	}
	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//找到要删除的节点
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else//说明找到要删除的节点了
			{
				//开始分析三种情况
				if (cur->_left == nullptr)//被删除节点左孩子为空。
				{
					if (cur == _root)//需要判断cur等于根节点的情况,否则else中parent空指针解引用了
					{
						_root = _root->_right;
					}
					else
					{
						if (parent->_left == cur)//确定cur是parent的左还是右,再进行“托孤”
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}	
					delete cur;
				}
				else if (cur->_right == nullptr)//被删除节点左孩子不为空,右孩子为空
				{
					if (cur == _root)
					{
						_root = _root->_left;
					}
					else
					{
						if (parent->_left == cur)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}	
					delete cur;
				}
				else//被删除节点左右孩子均不为空
				{
					//左右孩子均不为空,就需要左子树的最大值或右子树的最小值选出来当新根
					Node* rightMin = cur->_right;//这里选用右树的最小值进行更换
					Node* rightMinParent = cur;
					while (rightMin->_left!=nullptr)
					{
						rightMinParent = rightMin;
						rightMin = rightMin->_left;
					}
					//std::swap(cur->_key, rightMin->key);//用std的交换对自定义类型可能比较慢
					cur->_key = rightMin->_key;//还是用赋值好一点,即使是自定义类型,肯定有写赋值重载
					if (rightMinParent->_left == rightMin)//两种情况,第一种如图删除8,实际干掉9位置,需要将10的左连至9的右
						rightMinParent->_left = rightMin->_right;
					else if (rightMinParent->_right == rightMin)//第二种如图删除10,实际干掉14,需要将10的右连至14的右
						rightMinParent->_right = rightMin->_right;
					delete rightMin;
				}
				return true;
			}
		}
		return false;
	}
	bool EraseR(const K& key)
	{
		return _EarseR(_root, key);
	}
private:
	Node* _root;
	void _InOrder(Node* _root)
	{
		if (_root == nullptr)
		{
			return;
		}
		_InOrder(_root->_left);
		std::cout << _root->_key << " ";
		_InOrder(_root->_right);
	}
	Node* _FindR(Node* root,const K& key)
	{
		if (root == nullptr)
			return nullptr;
		if (root->_key < key)
		{
			return _FindR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _FindR(root->_left, key);
		}
		else
			return root;
	}
	bool _InsertR(Node*& root, const K& key)//形参是root的引用
	{
		if (root == nullptr)
		{
			root = new Node(key);//因为root是父节点左/右孩子的别名,直接修改别名,链接关系存在,不用考虑父子节点连接关系
			return true;
		}
		if (root->_key < key)
			return _InsertR(root->_right, key);
		else if (root->_key > key)
			return _InsertR(root->_left, key);
		else
			return false;
	}
	bool _EarseR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_key < key)
			return _EarseR(root->_right, key);
		else if (root->_key > key)
			return _EarseR(root->_left, key);
		else//说明找到了要删除的节点,无需考虑root的父亲为空
		{
			Node* del = root;
			if (root->_left == nullptr)
				root = root->_right;
			else if (root->_right == nullptr)
				root = root->_left;
			else//root左右子树均不为空
			{
				Node* rightMin = root->_right;
				while (rightMin->_left!=nullptr)//找到右树最小节点 
				{
					rightMin = rightMin->_left;
				}
				root->_key = rightMin->_key;
				return _EarseR(root->_right, rightMin->_key);//return表示子树进行删除,结束掉递归
			}
			delete del;
			return true;
		}
	}
};

2、key/value搜索模型(节点既存key又存value)

        key/value搜索模型指每一个key值,都有与之对应的value值,例如英汉互译,一个英文单词可以对应一个翻译字符串。该模型还可以用于统计相同内容出现次数。(举例代码见下方测试函数。)

key/value搜索模型整体代码
namespace KV
{
	template <class K,class V>
	struct BSTreeNode
	{
		BSTreeNode(const K& key,const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			,_value(value)
		{}
		BSTreeNode<K,V>* _left;
		BSTreeNode<K,V>* _right;
		K _key;
		V _value;
	};
	template <class K,class V>
	struct BSTree
	{
		typedef BSTreeNode<K,V> Node;
		BSTree()
			:_root(nullptr)
		{}
		//插入节点
		bool Insert(const K& key,const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key,value);//BSTreeNode对象中存放key值 
			}
			else
			{
				Node* parent = nullptr;
				Node* cur = _root;
				while (cur)
				{
					parent = cur;
					if (cur->_key < key)
					{
						cur = cur->_right;
					}
					else if (cur->_key > key)
					{
						cur = cur->_left;
					}
					else//说明数字重复
						return false;
				}
				cur = new Node(key, value);
				//判断插入节点放在parent节点的左子树还是右子树
				if (parent->_key < key)
				{
					parent->_right = cur;
				}
				else
				{
					parent->_left = cur;
				}
			}
			return true;
		}
		bool InsertR(const K& key,const V& value)
		{
			return _InsertR(_root, key, value);
		}
		//中序遍历
		void InOrder()//因为外部取不到_root,所以这里套了一层调用函数
		{
			_InOrder(_root);
			std::cout << std::endl;
		}
		//查找
		Node* Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
					return cur;
			}
			return nullptr;
		}
		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		bool Erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			//找到要删除的节点
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else//说明找到要删除的节点了
				{
					//开始分析三种情况
					if (cur->_left == nullptr)//被删除节点左孩子为空。
					{
						if (cur == _root)//需要判断cur等于根节点的情况,否则else中parent空指针解引用了
						{
							_root = _root->_right;
						}
						else
						{
							if (parent->_left == cur)//确定cur是parent的左还是右,再进行“托孤”
								parent->_left = cur->_right;
							else
								parent->_right = cur->_right;
						}
						delete cur;
					}
					else if (cur->_right == nullptr)//被删除节点左孩子不为空,右孩子为空
					{
						if (cur == _root)
						{
							_root = _root->_left;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_left;
							else
								parent->_right = cur->_left;
						}
						delete cur;
					}
					else//被删除节点左右孩子均不为空
					{
						//左右孩子均不为空,就需要左子树的最大值或右子树的最小值选出来当新根
						Node* rightMin = cur->_right;//这里选用右树的最小值进行更换
						Node* rightMinParent = cur;
						while (rightMin->_left != nullptr)
						{
							rightMinParent = rightMin;
							rightMin = rightMin->_left;
						}
						//std::swap(cur->_key, rightMin->key);//用std的交换对自定义类型可能比较慢
						cur->_key = rightMin->_key;//还是用赋值好一点,即使是自定义类型,肯定有写赋值重载
						cur->_value = rightMin->_value;
						if (rightMinParent->_left == rightMin)//两种情况,第一种如图删除8,实际干掉9位置,需要将10的左连至9的右
							rightMinParent->_left = rightMin->_right;
						else if (rightMinParent->_right == rightMin)//第二种如图删除10,实际干掉14,需要将10的右连至14的右
							rightMinParent->_right = rightMin->_right;
						delete rightMin;
					}
					return true;
				}
			}
			return false;
		}
		bool EraseR(const K& key)
		{
			return _EarseR(_root, key);
		}
	private:
		Node* _root;
		void _InOrder(Node* _root)
		{
			if (_root == nullptr)
			{
				return;
			}
			_InOrder(_root->_left);
			std::cout << _root->_key << " "<<_root->_value;
			_InOrder(_root->_right);
		}
		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return nullptr;
			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
				return root;
		}
		bool _InsertR(Node*& root, const K& key, const V& value)//形参是root的引用
		{
			if (root == nullptr)
			{
				root = new Node(key,value);//因为root是父节点左/右孩子的别名,直接修改别名,链接关系存在,不用考虑父子节点连接关系
				return true;
			}
			if (root->_key < key)
				return _InsertR(root->_right, key,value);
			else if (root->_key > key)
				return _InsertR(root->_left, key,value);
			else
				return false;
		}
		bool _EarseR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < key)
				return _EarseR(root->_right, key);
			else if (root->_key > key)
				return _EarseR(root->_left, key);
			else//说明找到了要删除的节点,无需考虑root的父亲为空
			{
				Node* del = root;
				if (root->_left == nullptr)
					root = root->_right;
				else if (root->_right == nullptr)
					root = root->_left;
				else//root左右子树均不为空
				{
					Node* rightMin = root->_right;
					while (rightMin->_left != nullptr)//找到右树最小节点 
					{
						rightMin = rightMin->_left;
					}
					root->_key = rightMin->_key;
					root->_value = rightMin->_value;
					return _EarseR(root->_right, rightMin->_key);//return表示子树进行删除,结束掉递归
				}
				delete del;
				return true;
			}
		}
	};
}
void testKV1()//中英互译
{
	KV::BSTree<std::string, std::string> dic;
	dic.Insert("data", "数据");
	dic.Insert("algorithm", "算法");
	dic.Insert("map", "地图、映射");
	dic.Insert("Linux", "一款开源免费的操作系统");
	std::string str;
	while (std::cin >> str)
	{
		KV::BSTreeNode<std::string, std::string>* ret = dic.Find(str);
		if (ret != nullptr)
		{
			std::cout << "中文翻译:" << ret->_value << std::endl;
		}
		else
			std::cout << "查找失败!" << std::endl;
	}
}
void testKV2()//用于统计次数
{
	std::string arr[] = { "数学", "语文", "数学", "语文", "数学", 
		"数学", "英语","数学", "英语", "数学", "英语" };
	KV::BSTree<std::string, int> count;
	for (auto& e : arr)
	{
		KV::BSTreeNode<std::string, int>* ret = count.Find(e);
		if (ret != nullptr)
		{
			ret->_value++;
		}
		else
		{
			count.Insert(e,1);
		}
	}
	count.InOrder();
}

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

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

相关文章

撕碎QT面具(2):groupBox内容居中显示

问题描述&#xff1a; 当笔者在GroupBox中使用Form Layout构建图中内容时&#xff0c;不能居中显示。 解决方案&#xff1a; 1、首先在form layout左右添加横向弹簧&#xff0c;并ctrl进行选中这三个控件。点击水平布局&#xff0c;让中间的控件不变形。 2、选中groupBox&#…

SpringBoot速成(14)文件上传P23-P26

1. 什么是 multipart/form-data&#xff1f; 想象一下&#xff0c;你有一个包裹要寄给朋友&#xff0c;但包裹里有不同类型的东西&#xff1a;比如一封信&#xff08;文字&#xff09;、一张照片&#xff08;图片&#xff09;和一个小礼物&#xff08;文件&#xff09;。为了确…

图论入门算法:拓扑排序(C++)

上文中我们了解了图的遍历(DFS/BFS), 本节我们来学习拓扑排序. 在图论中, 拓扑排序(Topological Sorting)是对一个有向无环图(Directed Acyclic Graph, DAG)的所有顶点进行排序的一种算法, 使得如果存在一条从顶点 u 到顶点 v 的有向边 (u, v) , 那么在排序后的序列中, u 一定…

【iOS】SwiftUI状态管理

State ObservedObject StateObject 的使用 import SwiftUIclass CountModel: ObservableObject {Published var count: Int 0 // 通过 Published 标记的变量会触发视图更新init() {print("TimerModel initialized at \(count)")} }struct ContentView: View {State…

自制简单的图片查看器(python)

图片格式&#xff1a;支持常见的图片格式&#xff08;JPG、PNG、BMP、GIF&#xff09;。 import os import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTkclass ImageViewer:def __init__(self, root):self.root rootself.root.…

ChatGPT行业热门应用提示词案例-AI绘画类

AI 绘画指令是一段用于指导 AI 绘画工具&#xff08;如 DALLE、Midjourney 等&#xff09;生成特定图像的文本描述。它通常包含场景、主体、风格、色彩、氛围等关键信息&#xff0c;帮助 AI 理解创作者的意图&#xff0c;从而生成符合要求的绘画作品。 ChatGPT 拥有海量的知识…

Visual Studio Code的下载安装与汉化

1.下载安装 Visual Studio Code的下载安装十分简单&#xff0c;在本电脑的应用商店直接下载安装----注意这是社区版-----一般社区版就足够用了---另外注意更改安装地址 2.下载插件 重启后就是中文版本了

分词器(Tokenizer) | 有了分词器,为什么还需要嵌入模型

文章目录 什么是tokenizer有了分词器&#xff0c;为什么还需要嵌入模型分词器为什么在transformers 里Hugging Face的Tokenizer大模型不同tokenizer训练效果对比分词器库选择当前顶尖大模型所采用的 Tokenizer 方法与词典大小 参考 什么是tokenizer Tokenizers huggingface官方…

scala中 隐式转换

一、 隐式转换&#xff1a; 编译器 偷偷地&#xff0c;自动地帮我们把一种数据类型转换为另一种类型 例如&#xff1a; int --> double object test {// 复习隐式转换// 隐式转换&#xff1a; 编译器 偷偷地&#xff0c;自动地帮我们把一种数据类型转换为另一…

实战开发coze应用-姓氏头像生成器(上)

​欢迎关注【AI技术开发者】 上次&#xff0c;我们开发了一个对话形式的头像生成器智能体&#xff08;Agents&#xff09;&#xff0c;广受大家欢迎。 同时也接收到一些用户的反馈&#xff0c;生成前无法看到头像样式、初次使用不会用等等。 对此&#xff0c;我准备使用Coze开…

【Node.js】express框架

目录 1初识express框架 2 初步使用 2.1 安装 2.2 创建基本的Web服务器 2.3 监听方法 2.3.1 监听get请求 2.3.2 监听post请求 2.4 响应客户端 2.5 获取url中的参数(get) 2.5.1 获取查询参数 2.5.2 获取动态参数 2.6 托管静态资源 2.6.1 挂载路径前缀 2.6.2 托管多…

JS逆向实战三:1688工厂信息

本文说明&#xff1a;B站学习笔记整理&#xff0c;仅供学习参考~~ 网站&#xff1a;https://sale.1688.com/factory/category.html 1. 页面分析与解密 刷新页面&#xff0c;通过对关键词进行搜索&#xff0c;实现接口定位。 通过多次刷新页面或者页面翻页&#xff0c;找到变化…

Pipeline 获取 Jenkins参数

Pipeline 获取 Jenkins参数 Jenkins 提供了一系列默认的环境变量&#xff0c;这些变量在构建过程中可以被使用。以下是一些常见的 Jenkins 默认环境变量&#xff1a; WORKSPACE: 当前构建的工作目录路径 JOB_NAME: 当前构建的作业名称 BUILD_NUMBER: 当前构建的编号&#xff…

ESP32 在IDF_V5.3.1版本下实现AP无线热点模式!(带WIFI事件处理)

一、什么是ESP32的AP无线热点模式&#xff1f; ESP32 的 AP&#xff08;Access Point&#xff09;模式 是指 ESP32 作为无线接入点运行&#xff0c;它自己创建一个 Wi-Fi 网络&#xff0c;允许其他设备&#xff08;如手机、电脑、平板等&#xff09;直接连接到它上面&#xff0…

Elasticsearch:探索 CLIP 替代方案

作者&#xff1a;来自 Elastic Jeffrey Rengifo 及 Toms Mura 分析图像到图像和文本到图像搜索的 CLIP 模型的替代方案。 在本文中&#xff0c;我们将通过一个模拟房地产网站的实际示例介绍 CLIP 多模态模型&#xff0c;探索替代方案&#xff0c;并分析它们的优缺点&#xff0c…

Nginx 在Linux中安装、使用

Nginx 在Linux中安装、使用 一、官网下载Nginx 官网地址&#xff1a;http://nginx.org/en/download.html 二、上传到服务器解压 1、上传到指定的服务器地址 上传的地址自己决定&#xff0c;我上传到 /data/home/prod/nginx/ 2、解压 使用命令&#xff1a; tar -zxvf “你的N…

【Spring+MyBatis】_图书管理系统(下篇)

图书管理系统上篇、中篇如下&#xff1a; 【SpringMyBatis】_图书管理系统&#xff08;上篇&#xff09;-CSDN博客 【SpringMyBatis】_图书管理系统&#xff08;中篇&#xff09;-CSDN博客 目录 功能5&#xff1a;删除图书 6.1 约定前后端交互接口 6.2 后端接口 6.3 前端…

若依-@Excel新增注解numberFormat

Excel注解中原本的scale会四舍五入小数&#xff0c;导致进度丢失 想要的效果 显示的时候保留两个小数真正的数值是保留之前的数值 还原过程 若以中有一個專門的工具类&#xff0c;用来处理excel的 找到EXCEL导出方法exportExcel()找到writeSheet,写表格的方法找到填充数据的方法…

Cherry-Studio下载安装教程,AI面向开发者的工具或平台(付安装包)

文章目录 一、Cherry Studio是什么&#xff1f;二、功能特点 一、Cherry Studio是什么&#xff1f; Cherry Studio 是一款开源跨平台的多模型服务桌面客户端&#xff0c;集成超 300 个大语言模型&#xff0c;内置 300 多个预配置 AI 助手&#xff0c;支持多格式文件处理、全局…

多信道接收机

线性调频&#xff08;LFM&#xff09;信号&#xff0c;模拟多个目标反射的回波信号&#xff0c;并进行混频和滤波处理。 % 参数设置 c 3e8; % 光速 (m/s) f0 8.566e9; % 载波频率 (Hz) T 10e-6; % 脉冲持续时间 (s) B 100e6; % 信号带宽 (Hz) mu B / T; % 调频斜率 (Hz/s…