数据结构——二叉搜索树(附带C++实现版本)

news2025/1/16 11:17:17

文章目录

  • 二叉搜索树
    • 概念
  • 二叉树的实际应用
  • 二叉树模拟实现
    • 存储结构
    • 二叉搜索树构成
    • 二叉搜索树的查找
    • 插入操作
    • 中序遍历
    • 二叉树的删除
      • 循环(利用左子树最右节点)
      • 递归(利用右子树根节点)
    • 二叉树拷贝
    • 二叉树资源的销毁
  • 二叉树实现完整代码
  • 总结

二叉搜索树

概念

二叉搜索树又叫二叉排序树,二叉搜索树也是一种树形结构。
它是一课满足以下性质的搜索树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别是二叉搜索树

注意,二叉搜索树对于值相同的节点只能存储一个

在这里插入图片描述

优点:这样的结构能够平均用logn的时间复杂度找到我们想要的值,但是其最坏时间复杂度是O(n), 这是由于搜索二叉树不一定是平衡的,如下图所示:
在这里插入图片描述
这个结构也满足二叉搜索树的性质,但是其查找所需要的复杂度是O(n)

二叉树的实际应用

  1. K模型:K模型只以key为关键码,结构中只需要存储K即可,关键码即为需要搜索道的值。

例如: 给一个单词word,判断该单词是否拼写正确,具体方法如下:

  • 以词库中的所有单词集合中的每一个单词作为key,构建一颗二叉搜索树。
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误
  • KV模型: 每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方 式在现实生活中非常常见:
  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出 现次数就是<word, count>就构成一种键值对

二叉树模拟实现

在这里为大家以kv模型为例模拟实现二叉搜索树,只要稍作修改即可变成k模型。

存储结构

首先,二叉搜索树中存储的是节点,所以我们需要定义一个表示节点的结构体,如下:

	template<typename K, typename V>
	struct BSTree_node
	{
		K _key = K();
		V _value = V();
		BSTree_node* _left = nullptr;
		BSTree_node* _right = nullptr;
		BSTree_node() {}
		BSTree_node(const K& key, const V& value)
			:_key(key)
			, _value(value)
		{}
	};

K模型和K,V模型差别就在k,v模型里面还存储了键所对应的值的内容,而k模型没有

二叉搜索树构成

对于二叉搜索树来说,我们想要找到其中的全部成员,和二叉树一样,我们只需要存储它的根节点即可。

template<typename K, typename V>
class BSTree
{
	//typedef减少代码长度
	typedef BSTree_node<K, V> node;
private:
	node* _root = nullptr;

二叉搜索树的查找

由于二叉搜索树的性质,想要从中查找就很简单了。
a. 从根开始比较查找,比根大往右走,比根小往左走。
b. 最多查找高度次,如果走到空还没找到,则这个值不存在

//循环操作
node* find(const K& key)
{
	//find不需要查找值,只需要查找键
	node* cur = _root;
	while (cur)
	{
		if (cur->_key > key) cur = cur->_left;
		else if (cur->_key < key) cur = cur->_right;
		else return cur;
	}
	return nullptr;
}
//递归的方式
//由于递归需要传递this指针来找到根节点,而方法不能在外面使用this指针,因此我们需要写一个子函数来完成任务
private:
			node* _findR(node* root, const K& key)
		{
			if (!root) return nullptr;
			if (root->_key < key) return _findR(root->_right, key);
			else if (root->_key > key) return _findR(root->_left, key);
			else return root;
		}
public:
	node* findR(const K& key)
	{
		return _findR(_root,key);
	}

插入操作

插入操作同样两个要点:
a. 如果树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树的位置不断进行比较找到插入位置,如果找到相同值的节点则插入失败,否则插入新节点
同样提供递归和循环两种方法:

//循环
//对于循环,由于当找到空节点的时候并不知道其在其双亲的左侧还是右侧,所以每次变换时需要记录其是在左侧还是右侧
		//成功插入返回true,插入失败返回false
		bool insert(const K& key, const V& value)
		{
			node* tmp = new node(key, value);
			if (!_root)
			{
				_root = tmp;
				return true;
			}
			//需要存储parent,还要存储所在的方向
			node* parent = nullptr;
			node* cur = _root;
			bool is_right = false;
			while (cur)
			{
				if (cur->_key < key) 
				{
					is_right = true;
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key) 
				{
					is_right = false;
					parent = cur;
					cur = cur->_left;
				}
				else return false;
			}
			//出循环时,cur已经指向空指针
			//如果直接对cur赋值,是在对该拷贝赋值,并没有修改其双亲的指向
			if (is_right) parent->_right = tmp;
			else parent->_left = tmp;
			return true;
		}
		
		//递归
		//同样,我们需要一个子函数来传递this指针
		private:
			//这里使用引用是为了解决循环中无法知道新节点在其双亲的左边还是右边的问题
			bool _insertR(node*& root, const K& key, const V& value)
{
	//引用解决了找不到父亲的问题
	if (!root)
	{
	//由于使用的是引用,这里其实是在修改双亲的指向
		root = new node(key, value);
		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;
}		

中序遍历

由于二叉树的特殊性质,其中序遍历一定是有序的,因此我们可以写一个inorder函数输出存储结果用于检验我们操作是否正确实现

private:
			void _inorder(node* root)
		{
			if (!root) return;
			_inorder(root->_left);
			std::cout << root->_key << ":" << root->_value << std::endl;
			_inorder(root->_right);	
		}
public:
			void inorder()
		{
			_inorder(_root);
			std::cout << std::endl;
		}

二叉树的删除

  1. 首先查找元素是否在二叉树中,如果不存在,则返回
  2. 如果存在,则删除节点还需要分以下几种情况:
  • 要删除的节点无左孩子
  • 要删除的节点只有左孩子节点
  • 要删除的节点只有右孩子节点
  • 要删除的节点有左,右孩子节点

在实际删除过程中,可以将情况1和情况2或情况3合并起来,删除过程如下:

  • 情况1: 删除该节点且使被删除节点的双亲节点指向被删除节点的左孩子——直接删除
    情况2: 删除该节点且使被删除节点的双亲指向被删除节点的右孩子
    情况3: 寻找和节点的值最相近的节点(左子树最右节点或者该节点右子树的根节点),用它的值填补道被删除节点,然后再来处理该节点的删除问题。

节点的删除操作实现是二叉搜索树中最困难的一个,由于其的细节很多,这里同样还是给出递归和循环各一种方法。

循环(利用左子树最右节点)

对于循环,博主选择了和左子树的最右节点进行交换的方法,这是由于只要找到了左子树的最右节点,和此时的根节点交换,然后就只需要进行左右都没孩子的删除操作即可。
但是,和插入有着同样的问题,我们在查找待删除节点的时候并不知道它在左侧还是右侧,所以我们仍然要保存其双亲节点的位置,但是,还有例外。
先看情况一和情况二
在这里插入图片描述
对于这种情况来说,如果要删除的是根节点,那么其双亲节点的指针就是nullptr,如果不加以判断,就会出错,当待删除节点为根节点时,我们直接将树的_root节点改成_root->left/_root->right即可
情况三:
对于情况三来说,虽然我们需要找的是左子树的最右节点,但是一定不要认为左子树最右节点一定在其双亲的右边,有一种情况是例外的。如下:
在这里插入图片描述
如果此时要删除的是根节点,那么其左子树的最右节点就在其双亲的左边,所以我们仍然需要一个标记来判断该节点是在双亲的左边还是右边。

对于二叉树的删除操作来说,循环需要考虑的细节较多,递归虽然也有细节,但是相对更简单一些,但是循环的好处就是不会爆栈,因此在数据量非常大的时候还是使用循环更合适。
循环模拟代码如下:

bool erase(const K& key)
{
	//可以分为两种大情况
	//无子节点,有一个子节点 -> 采用托孤处理
	//托孤要注意删根节点的情况
	//两个子节点都有 -> 将其与左树最大节点或者右数最小节点相交换,然后删除
	//第一步先找到要删除的值的位置
	node* parent = nullptr;
	node* cur = _root;
	//保存节点位于其双亲的位置
	bool is_right = false;
	while (cur)
	{
		if (cur->_key < key)
		{
			is_right = true;
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			is_right = false;
			parent = cur;
			cur = cur->_left;
		}
		else break;
	}
	if (!cur) return false;
	node* del = cur;
	//这里需要找到父节点的本质原因是引用不能改变指向
	if (!cur->_left)
	{
		//特殊情况,删除的是根节点
		if (!parent) _root = _root->_right;
		else
		{
			if (is_right) parent->_right = cur->_right;
			else parent->_left = cur->_right;
		}
	}
	else if (!cur->_right)
	{
		//同理
		if (!parent) _root = _root->_left;
		else
		{
			if (is_right) parent->_right = cur->_left;
			else parent->_left = cur->_left;
		}
	}
	else
	{
		//左右两边都不为空
		//这里循环找左边最大更方便
		node* leftMax = cur->_left;
		//bool is_up = true;
		//出现 特殊情况的本质是下面这个循环没有生效
		while (leftMax->_right)
		{
			parent = leftMax;
			leftMax = leftMax->_right;
			//is_up = false;
		}
		std::swap(leftMax->_key, cur->_key);
		//如果左子树最大节点就是初始的leftMax,则将待删除节点的左指向leftMax的左
		//if(is_up)
		if (leftMax == cur->_left) cur->_left = leftMax->_left;
		else parent->_right = leftMax->_left;
		//转换待释放的节点
		del = leftMax;
	}
	delete del;
	return true;
}

递归(利用右子树根节点)

对于递归来说,如果我们选择右子树的根节点进行操作,整个删除过程就可以变成子问题解决。

首先,由于递归可以使用引用作为参数,我们不需要纠结双亲以及其位于双亲左还是右的问题,因此对于循环中情况1,2删除根节点的问题就不需要考虑了
另外,对于情况3来说,选择右子树的根节点也使得情况简单了许多,因为将右子树节点与待删除节点的值交换后,就变成了删除其右子树的根的子问题,完美符合递归的逻辑,直到根只有一个孩子的时侯就变成情况1了,不需要考虑其他特殊情况。
代码如下:

private:
	bool _erase(node*& root, const K& key)
{
	if (!root) return false;
	if (root->_key < key) return _erase(root->_right, key);
	else if (root->_key > key) return _erase(root->_left, key);
	else
	{
		//用引用就不需要考虑父亲指向的问题了
		if (!root->_left) 
		{
			root = root->_right;
			return true;
		}
		else if (!root->_right)
		{
			root = root->_left;
			return true;
		}
		else
		{
			swap(root->_key, root->_right->_key);
			return _erase(root->_right, key);
		}
	}
}
public:
	bool eraseR(const K& key)
	{
		_erase(_root, key);
	}

二叉树拷贝

二叉树的拷贝构造也可以利用递归的性质来实现,先拷贝根节点,然后拷贝左子树,最后拷贝右子树,拷贝左子树和右子树的逻辑与主逻辑相同。
代码:

private:
		node* _copy(node*& root, node* copy)
		{
			if (!copy) return nullptr;
			root = new node(copy->_key, copy->_value);
			root->_left = _copy(root->_left, copy->_left);
			root->_right = _copy(root->_right, copy->_right);
			return root;
		}
public:
		BSTree(const BSTree& t)
		{
			_copy(_root, t._root);
		}

二叉树资源的销毁

由于二叉树的节点都是new出来的节点,所以我们在结束使用时也需要释放资源,否则就会导致内存泄漏的问题,对于释放资源,我们可以放在析构函数解决,运用递归后序遍历的思路,先释放左子树的资源,然后释放右子树的资源,最后释放根节点的资源,代码如下:

private:
		void _destroy(node* root)
		{
			if (!root) return;
			_destroy(root->_left);
			_destroy(root->_right);
			delete root;
		}

public:
		~BSTree()
		{
			_destroy(_root);
		}

二叉树实现完整代码

#pragma once
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;

namespace key_value
{
	template<typename K, typename V>
	struct BSTree_node
	{
		K _key = K();
		V _value = V();
		BSTree_node* _left = nullptr;
		BSTree_node* _right = nullptr;
		BSTree_node() {}
		BSTree_node(const K& key, const V& value)
			:_key(key)
			, _value(value)
		{}
	};
	template<typename K, typename V>
	class BSTree
	{
		typedef BSTree_node<K, V> node;
	private:
		node* _root = nullptr;
		void _inorder(node* root)
		{
			if (!root) return;
			_inorder(root->_left);
			std::cout << root->_key << ":" << root->_value << std::endl;
			_inorder(root->_right);	
		}
		bool _insertR(node*& root, const K& key, const V& value)
		{
			//引用解决了找不到父亲的问题
			if (!root)
			{
				root = new node(key, value);
				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;
		}
		node* _findR(node* root, const K& key)
		{
			if (!root) 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 _erase(node*& root, const K& key)
		{
			if (!root) return false;
			if (root->_key < key) return _erase(root->_right, key);
			else if (root->_key > key) return _erase(root->_left, key);
			else
			{
				//用引用就不需要考虑父亲指向的问题了
				if (!root->_left) 
				{
					root = root->_right;
					return true;
				}
				else if (!root->_right)
				{
					root = root->_left;
					return true;
				}
				else
				{
					swap(root->_key, root->_right->_key);
					return _erase(root->_right, key);
				}
			}
		}
		void _destroy(node* root)
		{
			if (!root) return;
			_destroy(root->_left);
			_destroy(root->_right);
			delete root;
		}
		node* _copy(node*& root, node* copy)
		{
			if (!copy) return nullptr;
			root = new node(copy->_key, copy->_value);
			root->_left = _copy(root->_left, copy->_left);
			root->_right = _copy(root->_right, copy->_right);
			return root;
		}
	public:
		BSTree() {}
		//循环
		/
		//插入,如果已经存在就不用插入
		bool insert(const K& key, const V& value)
		{
			node* tmp = new node(key, value);
			if (!_root)
			{
				_root = tmp;
				return true;
			}
			//需要存储parent,还要存储所在的方向
			node* parent = nullptr;
			node* cur = _root;
			bool is_right = false;
			while (cur)
			{
				if (cur->_key < key) 
				{
					is_right = true;
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key) 
				{
					is_right = false;
					parent = cur;
					cur = cur->_left;
				}
				else return false;
			}
			//出循环时,cur已经指向空指针
			if (is_right) parent->_right = tmp;
			else parent->_left = tmp;
			return true;
		}
		node* find(const K& key)
		{
			//find不需要查找值,只需要查找键
			node* cur = _root;
			while (cur)
			{
				if (cur->_key > key) cur = cur->_left;
				else if (cur->_key < key) cur = cur->_right;
				else return cur;
			}
			return nullptr;
		}
		bool erase(const K& key)
		{
			//可以分为两种大情况
			//无子节点,有一个子节点 -> 采用托孤处理
			//托孤要注意删根节点的情况
			//两个子节点都有 -> 将其与左树最大节点或者右数最小节点相交换,然后删除
			//第一步先找到要删除的值的位置
			node* parent = nullptr;
			node* cur = _root;
			//保存节点位于其双亲的位置
			bool is_right = false;
			while (cur)
			{
				if (cur->_key < key)
				{
					is_right = true;
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					is_right = false;
					parent = cur;
					cur = cur->_left;
				}
				else break;
			}
			if (!cur) return false;
			node* del = cur;
			//这里需要找到父节点的本质原因是引用不能改变指向
			if (!cur->_left)
			{
				//特殊情况,删除的是根节点
				if (!parent) _root = _root->_right;
				else
				{
					if (is_right) parent->_right = cur->_right;
					else parent->_left = cur->_right;
				}
			}
			else if (!cur->_right)
			{
				//同理
				if (!parent) _root = _root->_left;
				else
				{
					if (is_right) parent->_right = cur->_left;
					else parent->_left = cur->_left;
				}
			}
			else
			{
				//左右两边都不为空
				//这里循环找左边最大更方便
				node* leftMax = cur->_left;
				//bool is_up = true;
				//出现 特殊情况的本质是下面这个循环没有生效
				while (leftMax->_right)
				{
					parent = leftMax;
					leftMax = leftMax->_right;
					//is_up = false;
				}
				std::swap(leftMax->_key, cur->_key);
				//如果左子树最大节点就是初始的leftMax,则将待删除节点的左指向leftMax的左
				//if(is_up)
				if (leftMax == cur->_left) cur->_left = leftMax->_left;
				else parent->_right = leftMax->_left;
				//转换待释放的节点
				del = leftMax;
			}
			delete del;
			return true;
		}
		//递归
		/
		//中序遍历
		void inorder()
		{
			_inorder(_root);
			std::cout << std::endl;
		}
		bool insertR(const K& key, const V& value)
		{
			return _insertR(_root, key, value);
		}
		node* findR(const K& key) { return _findR(_root, key); }
		bool eraseR(const K& key)
		{
			_erase(_root, key);
		}
		~BSTree()
		{
			_destroy(_root);
		}
		BSTree(const BSTree& t)
		{
			_copy(_root,t._root);
		}
	};

	//测试用例1
	void test_BSTree()
	{
		int a[] = { 8,3,1,10,6,4,7,14,13 };
		BSTree<int, int> bst;
		for (auto e : a) bst.insert(e,e);
		std::cout << bst.find(8)->_key << std::endl;
		std::cout << bst.find(13)->_key << std::endl;
		//std::cout << bst.find(18)->_key << std::endl;
		BSTree<int, int> t1(bst);
		t1.inorder();
		bst.erase(4);
		bst.inorder();

		bst.erase(6);
		bst.inorder();

		bst.erase(7);
		bst.inorder();

		bst.erase(3);
		bst.inorder();

		for (auto e : a)
		{
			bst.erase(e);
		}
		bst.inorder();
	}
	//测试用例2
	void TestBSTree()
	{
		BSTree<string, string> dict;
		dict.insertR("insert", "插入");
		dict.insertR("erase", "删除");
		dict.insertR("left", "左边");
		dict.insertR("string", "字符串");

		string str;
		while (cin >> str)
		{
			auto ret = dict.findR(str);
			if (ret)
			{
				cout << str << ":" << ret->_value << endl;
			}
			else
			{
				cout << "单词拼写错误" << endl;
			}
		}

		string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
		// 统计水果出现的次
		BSTree<string, int> countTree;
		for (auto str : strs)
		{
			auto ret = countTree.findR(str);
			if (ret == nullptr)
			{
				countTree.insertR(str, 1);
			}
			else
			{
				ret->_value++;
			}
		}
		BSTree<string, int> t1(countTree);
		countTree.inorder();
		t1.inorder();
	}
}

总结

前面提到了对于二叉查找树来说,

  • 最好情况下二叉树为完全二叉树(或接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
  • 最坏情况下,二叉搜索树有可能退化成单支树(或类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

那么就有一个问题了,如果退化成单支树,二叉搜索树的性能就消失了,那么是否能够改进,不论按什么次数插入关键码,二叉搜索树的性能都能达到最优呢?
那么就需要用到AVL树和红黑树了,这两种树都是特殊的搜索二叉树,但是底层相对于普通的二叉搜索树又复杂了许多,加入了翻转二叉树等操作来达到最有优效率!
STL中的mapset底层就是用红黑树实现的,其做到了查找最坏时间复杂度为 O ( l o g 2 N ) O(log_2 N) O(log2N),这样大家就感受到红黑树的强大了把!

对于AVL树和红黑树的知识,博主会在之后的博客中讲解,大家敬请期待!


以上就是二叉树实现的增删查改的相关知识内容,完整代码以及其作用和性能分析的总结了,希望大家看完能够有所收获!如果对博主的内容有疑惑或者博主内容有误的话,欢迎评论区指出!

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

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

相关文章

LVS-DR+keepalived实现高可用负载群集

VRRP 通信原理&#xff1a; VRRP就是虚拟路由冗余协议&#xff0c;它的出现就是为了解决静态路由的单点故障。 VRRP是通过一种竞选的一种协议机制&#xff0c;来将路由交给某台VRRP路由。 VRRP用IP多播的方式&#xff08;多播地址224.0.0.18&#xff09;来实现高可用的通信&…

opencv运动目标检测-背景建模

背景建模 帧差法 由于场景中的目标在运动&#xff0c;目标的影像在不同图像帧中的位置不同。该类算法对时间上连续的两帧图像进行差分运算&#xff0c;不同帧对应的像素点相减&#xff0c;判断灰度差的绝对值&#xff0c;当绝对值超过一定阈值时&#xff0c;即可判断为运动目…

Java虚拟机(JVM):垃圾收集算法

目录 一、分代收集理论 二、标记-清除算法 三、标记-复制算法 四、标记-整理算法 一、分代收集理论 分代收集理论建立在两个分代假说之上&#xff1a; 1、弱分代假说&#xff1a;绝大多数对象都是朝生夕灭的。 2、强分代假说&#xff1a;熬过越多次垃圾收集过程的对象就…

Python中数据结构列表详解

列表是最常用的 Python 数据类型&#xff0c;它用一个方括号内的逗号分隔值出现&#xff0c;列表的数据项不需要具有相同的类型。 列表中的每个值都有对应的位置值&#xff0c;称之为索引&#xff0c;第一个索引是 0&#xff0c;第二个索引是 1&#xff0c;依此类推。列表都可…

C语言之指针进阶篇(1)

目录​​​​​​​ 引言 字符指针 指针数组 数组指针 数组指针的定义 &数组名vs数组名 数组指针的使用 一维数组使用 二维数组使用 一维数组传参 二维数组传参 总结 数组参数 一维数组传参 二维数组传参 指针参数 一级指针传参 二级指针传参 引言 今…

Jmeter对websocket进行测试

JMeterWebSocketSampler-1.0.2-SNAPSHOT.jar下载 公司使用websocket比较奇怪&#xff0c;需要带认证信息进行长连接&#xff0c;通过websocket插件是请求失败&#xff0c;如下图&#xff0c;后面通过代码实现随再打包jar包完成websocket测试 本地实现代码如下&#xff1a; pa…

总结,由于顺丰的问题,产生了电脑近期一个月死机问题集锦

由于我搬家&#xff0c;我妈搞顺丰发回家&#xff0c;但是没有检查有没有坏&#xff0c;并且我自己由于不可抗力因素&#xff0c;超过了索赔时间&#xff0c;反馈给顺丰客服&#xff0c;说超过了造成了无法索赔的情况&#xff0c;现在总结发生了损坏配件有几件&#xff0c;显卡…

Java 项目日志实例基础:Log4j

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ 介绍几个日志使用方面的基础知识。 1 Log4j 1、Log4j 介绍 Log4j&#xff08;log for java&#xff09;是 Apache 的一个开源项目&#xff0c;通过使用 Log4j&#xff0c;我…

奥威BI数据可视化工具:个性化定制,打造独特大屏

每个人都有自己独特的审美&#xff0c;因此即使是做可视化大屏&#xff0c;也有很多人希望做出不一样的报表&#xff0c;用以缓解审美疲劳的同时提高报表浏览效率。因此这也催生出了数据可视化工具的个性化可视化大屏制作需求。 奥威BI数据可视化工具&#xff1a;个性化定制&a…

nginx代理webSocket链接响应403

一、场景 使用nginx代理webSocket链接&#xff0c;nginx响应403 1、nginx访问日志响应403 [18/Aug/2023:09:56:36 0800] "GET /FS_WEB_ASS/webim_api/socket/message HTTP/1.1" 403 5 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit…

opencv-dnn

# utils_words.txt 标签文件 import osimage_types (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff")def list_images(basePath, containsNone):# return the set of files that are validreturn list_file…

机器学习|决策树:数学原理及代码解析

机器学习&#xff5c;决策树&#xff1a;数学原理及代码解析 决策树是一种常用的监督学习算法&#xff0c;适用于解决分类和回归问题。在本文中&#xff0c;我们将深入探讨决策树的数学原理&#xff0c;并提供 Python 示例代码帮助读者更好地理解和实现该算法。 决策树数学原…

大语言模型-RLHF(七)-PPO实践(Proximal Policy Optimization)原理实现代码逐行注释

从open AI 的论文可以看到&#xff0c;大语言模型的优化&#xff0c;分下面三个步骤&#xff0c;SFT&#xff0c;RM&#xff0c;PPO&#xff0c;我们跟随大神的步伐&#xff0c;来学习一下这三个步骤和代码实现&#xff0c;本章介绍PPO实践。 生活中&#xff0c;我们经常会遇到…

数字化时代,数据仓库和商业智能BI系统演进的五个阶段

数字化在逐渐成熟的同时&#xff0c;社会上也对数字化的性质有了进一步认识。当下&#xff0c;数字化除了前边提到的将复杂的信息、知识转化为可以度量的数字、数据&#xff0c;在将其转化为二进制代码&#xff0c;引入计算机内部&#xff0c;建立数据模型&#xff0c;统一进行…

Java数据库连接池原理及spring boot使用数据库连接池(HikariCP、Druid)

和线程池类似&#xff0c;数据库连接池的作用是建立一些和数据库的连接供需要连接数据库的业务使用&#xff0c;避免了每次和数据库建立、销毁连接的性能消耗&#xff0c;通过设置连接池参数可以防止建立连接过多导致服务宕机等&#xff0c;以下介绍Java中主要使用的几种数据库…

IP 地址监控工具

地址监控实用程序是一套 IP 工具&#xff0c;包括 IP 地址监控工具、流氓检测工具和 MAC 地址解析器&#xff0c;用于日常监控和管理 DNS 名称、IP和 MAC 地址。地址监控工具用于 IP监控&#xff0c;用于管理 DNS 名称、网络的 IP 和 MAC 地址&#xff0c;并跟踪 IP 地址。 IP…

基于基于springboot+vue+B2C模式的电子商务平台【源码+论文+演示视频+包运行成功】

博主介绍&#xff1a;✌csdn特邀作者、博客专家、java领域优质创作者、博客之星&#xff0c;擅长Java、微信小程序、Python、Android等技术&#xff0c;专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推…

Unity 物体的运动之跟随鼠标

你想让鼠标点击哪里&#xff0c;你的运动的对象就运动到哪里吗&#xff1f; Please follow me ! 首先&#xff0c;你要先添加一个Plane ,以及你的围墙&#xff0c;你的移动的物体 想要实现跟随鼠标移动&#xff0c;我们先创建一个脚本 using System.Collections; using Syst…

Coremail参与编制|《信创安全发展蓝皮书——系统安全分册(2023年)》

信创安全发展蓝皮书 近日&#xff0c;Coremail参与编制的《信创安全发展蓝皮书—系统安全分册&#xff08;2023年&#xff09;》重磅发布。 此次信创安全发展蓝皮书由工业和信息化部电子第五研究所联合大数据协同安全技术国家工程研究中心重磅共同发布。 本次蓝皮书涵盖信创系…

关于路由器和DNS解析的一些新理解

其实我本人对于交换机和路由器这些网络硬件是比较感兴趣的&#xff0c;也在一点一点的学习相关知识&#xff0c;每次解决一个问题&#xff0c;就让我对一些事情有新的思考。。 今天前台同事&#xff0c;的机器突然上不了网&#xff0c;&#xff0c;和领导一起去看了一波&#…