<C++> 二叉搜索树

news2024/9/24 19:28:33

目录

二叉搜索树

1. 概念

2. 二叉搜索树操作

2.1 基础结构

2.2 非递归版

1. 查找

2. 插入

3. 删除

2.3 递归版

1. 查找

2. 插入

3. 删除

2.4 拷贝构造函数

2.5 赋值运算符重载

2.6 析构函数

2.7 完整代码

3. 二叉搜索树的应用

4. 二叉搜索树的性能


二叉搜索树

1. 概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

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

由于二叉搜索树的性质,该树的中序遍历就是递增序列

 

2. 二叉搜索树操作

2.1 基础结构

	template<class K>
	struct BSTreeNode
	{
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;

		BSTreeNode(const K& key)
			:_key(key)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};

    template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

	private:
		Node* _root;
	};

2.2 非递归版

1. 查找

  • key小于cur->_key,则往左子树找
  • key大于cur->_key,则往右子树找
		bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}

			return false;
		}
2. 插入

  • 插入要先找到合适的空位
  • 如果已经之前已经存在,那么return false
  • 找到空位之后,new出一个新节点,在链接时我们要获取parent节点,这就需要我们提前记录parent节点
  • 链接时还要判断 key 与 parent->_key大小关系,因为我们找到了合适的位置,但是没有记录是在左还是在右
bool Insert(const K& key)
		{
			//如果树为空,直接new一个根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			//记录父节点,最后链到父节点
			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
				{
					return false;
				}
			}

			//由于不知道插入的节点与父亲的key大小关系,所以再判断一下是插到左边还是右边
			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}
3. 删除

 

  • 先查找该节点是否存在,如果存在,再看以下几点
  • 删除节点情况分三种:该节点左孩子为空、该节点右孩子为空、该节点左右孩子都有
  • 左孩子为空:将右节点链接到parent上即可,此时还要判断cur在parent的哪一边,如果cur在parent的右边,那么cur的所有孩子必然比parent的key大,所以将cur的right链接到parent的right即可
  • 右孩子为空:同理
  • 左右孩子都存在:此时我们可以找该节点左子树的最右节点,或右子树的最左节点,这两个节点都是最接近根节点key值的节点(因为二叉搜索树的性质,当节点无穷时,这两个节点的key值将从左和从右边无限趋近于根节点的key值),找到节点后与根节点交换key值,此时如果是左子树的最右节点,那么该节点是绝不可能存在右子树,所以此时将该节点的左子树链接到parent节点即可(提前记录parent,在判断cur在parent的哪一边)
  • 最后不要忘了delete被删除的节点
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)
						{
							_root = cur->_right;
						}
						else  //再看看cur在父节点parent的哪一边,在哪一边就把孤儿链接到哪一边
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}
					}
					else if (cur->_right == nullptr)	//该节点右为空
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_left;
							}
							else
							{
								parent->_left = cur->_left;
							}
						}
					}
					else  //该节点左右都不为空
					{
						Node* parent = cur;
						Node* leftMax = cur->_left;

						while (leftMax->_right)
						{
							parent = leftMax;
							leftMax = leftMax->_right;
						}

						swap(leftMax->_key, cur->_key);

						if (parent->_left == leftMax)
						{
							parent->_left = leftMax->_left;
						}
						else
						{
							parent->_right = leftMax->_left;
						}

						cur = leftMax;
					}
					delete cur;
					return true;
				}
			}

			return false;
		}

2.3 递归版

        由于用户不传递root参数,所以FindR内层封装一个获取root参数的函数,其他函数同理

1. 查找

  • 如果找不到返回false,找到了返回true。比key小,往右子树递归找;比key大,往左子树递归找。
		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return true;
			}
		}
2. 插入

  • insert函数神之一手的地方就是参数类型 Node*&,当它递归下去时 root 其实是它的父节点的 left 或 right 的引用!它可以不用提前保存parent的信息,直接链接新节点!
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}

		//Node*& root,这里的引用是神之一手,因为在连接的时候可以不用再去记录parent
		//在链接的时候,那个root是引用的父亲的root->_right或root->_left !!
		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				//这个root是引用的父亲的root->_right或root->_left,这一步是直接链接了!
				root = new Node(key);
				return true;
			}

			if (root->_key < key)
			{
				return _InsertR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key);
			}
			else
			{
				//已经有该节点了,返回false
				return false;
			}
		}
3. 删除

  • 先找到要删除的节点,然后再提前保存该节点,为了后续的delete
  • 同样的,erase的参数也是Node*&,它极大方便了节点的链接
  • 找到节点后,如果该节点的左节点为空,那么直接root = root->_right;如果右节点为空,那么 root = root->_left,这就是Node* &的强大之处。那么可能有疑问,非递归版本为什么不能用引用?循环版本不能使用引用,是因为引用不能改变指向!递归可以使用引用是因为每次都是一个新的栈帧
  • 如果左右节点都存在,找左子树最右节点,交换key值,再erase掉key的节点(注意,不能从root开始找,因为root此时已经被交换key值了,递归会往右边去找,这就会导致找错了
  • 最后Erase()这里不能传leftMax->_left,因为leftMax是局部变量,最后子节点会链接不上真正的root

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

        bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key < key)
			{
				return _EraseR(root->_left, key);
			}
			else  //找到了,开始删除
			{
				//提前记录
				Node* del = root;

				//左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//左右都不为空
				else
				{
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(leftMax->_key, root->_key);

					//这里不能传leftMax->_left,因为是别名,leftMax是局部变量,最后子节点会链接不上真正的root
					return _EraseR(root->_left, key);
				}

				delete del;
				return true;
			}
		}

2.4 拷贝构造函数

  • 根据前序序列递归拷贝
  • 该递归就是从最左边开始链接,再从最底层往上

		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}

		Node* Copy(Node* root)
		{
			//前序遍历拷贝
			if (root == nullptr)
				return nullptr;

			Node* copyroot = new Node(root->_key);
			copyroot->_left = Copy(root->_left);
			copyroot->_right = Copy(root->_right);

			return copyroot;
		}

2.5 赋值运算符重载

  • 现代写法
		BSTree<K>& operator=(BSTree<K> t)
		{
			swap(_root, t._root);
			return *this;
		}

2.6 析构函数

  • 递归式析构
		~BSTree()
		{
			Destory(_root);
		}

		void Destory(Node*& root)
		{
			if (root == nullptr)
				return;

			Destory(root->_left);
			Destory(root->_right);
			delete root;
			root = nullptr;
		}

2.7 完整代码

namespace key
{
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;

		BSTreeNode(const K& key)
			:_key(key)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};

	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}

		BSTree<K>& operator=(BSTree<K> t)
		{
			swap(_root, t._root);
			return *this;
		}

		~BSTree()
		{
			Destory(_root);
		}

		bool Insert(const K& key)
		{
			//如果树为空,直接new一个根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			//记录父节点,最后链到父节点
			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
				{
					return false;
				}
			}

			//由于不知道插入的节点与父亲的key大小关系,所以再判断一下是插到左边还是右边
			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}

		bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}

			return false;
		}

		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)
						{
							_root = cur->_right;
						}
						else  //再看看cur在父节点parent的哪一边,在哪一边就把孤儿链接到哪一边
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}
					}
					else if (cur->_right == nullptr)	//该节点右为空
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_left;
							}
							else
							{
								parent->_left = cur->_left;
							}
						}
					}
					else  //该节点左右都不为空
					{
						Node* parent = cur;
						Node* leftMax = cur->_left;

						while (leftMax->_right)
						{
							parent = leftMax;
							leftMax = leftMax->_right;
						}

						swap(leftMax->_key, cur->_key);

						if (parent->_left == leftMax)
						{
							parent->_left = leftMax->_left;
						}
						else
						{
							parent->_right = leftMax->_left;
						}

						cur = leftMax;
					}
					delete cur;
					return true;
				}
			}

			return false;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		Node* Copy(Node* root)
		{
			//前序遍历拷贝
			if (root == nullptr)
				return nullptr;

			Node* copyroot = new Node(root->_key);
			copyroot->_left = Copy(root->_left);
			copyroot->_right = Copy(root->_right);

			return copyroot;
		}

		void Destory(Node*& root)
		{
			if (root == nullptr)
				return;

			Destory(root->_left);
			Destory(root->_right);
			delete root;
			root = nullptr;
		}

		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key < key)
			{
				return _EraseR(root->_left, key);
			}
			else  //找到了,开始删除
			{
				//提前记录
				Node* del = root;

				//左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//左右都不为空
				else
				{
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(leftMax->_key, root->_key);

					//这里不能传leftMax->_left,因为是别名,leftMax是局部变量,最后子节点会链接不上真正的root
					return _EraseR(root->_left, key);
				}

				delete del;
				return true;
			}
		}

		//Node*& root,这里的引用是神之一手,因为在连接的时候可以不用再去记录parent
		//在链接的时候,那个root是引用的父亲的root->_right或root->_left !!
		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				//这个root是引用的父亲的root->_right或root->_left,这一步是直接链接了!
				root = new Node(key);
				return true;
			}

			if (root->_key < key)
			{
				return _InsertR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key);
			}
			else
			{
				//已经有该节点了,返回false
				return false;
			}
		}

		bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return true;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}
	private:
		Node* _root;
	};

	void TestBSTree1()
	{
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		BSTree<int> t;
		for (auto e : a)
		{
			t.Insert(e);
		}

		t.InOrder();

		t.EraseR(4);
		t.InOrder();

		t.EraseR(6);
		t.InOrder();

		t.EraseR(7);
		t.InOrder();

		t.EraseR(3);
		t.InOrder();

		for (auto e : a)
		{
			t.Erase(e);
		}
		t.InOrder();
	}

}

3. 二叉搜索树的应用

1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值

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

  • 以单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对

比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:

  • <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较
  • Key查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
namespace key_value
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;

		BSTreeNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
	};

	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key, const V& value)
		{
			return _InsertR(_root, key, value);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				Node* del = root;

				// 1、左为空
				// 2、右为空
				// 3、左右都不为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(root->_key, leftMax->_key);

					return _EraseR(root->_left, key);
				}

				delete del;
				return true;
			}
		}

		bool _InsertR(Node*& root, const K& key, const V& value)
		{
			if (root == nullptr)
			{
				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 == 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;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root;
	};


	void TestBSTree1()
	{
		//BSTree<string, Date> carTree;

		BSTree<string, string> dict;
		dict.InsertR("insert", "插入");
		dict.InsertR("sort", "排序");
		dict.InsertR("right", "右边");
		dict.InsertR("date", "日期");

		string str;
		while (cin >> str)
		{
			BSTreeNode<string, string>* ret = dict.FindR(str);
			if (ret)
			{
				cout << ret->_value << endl;
			}
			else
			{
				cout << "无此单词" << endl;
			}
		}
	}

	void TestBSTree2()
	{

		// 统计水果出现的次数
		string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		BSTree<string, int> countTree;
		for (auto& str : arr)
		{
			auto ret = countTree.FindR(str);
			if (ret == nullptr)
			{
				countTree.InsertR(str, 1);
			}
			else
			{
				ret->_value++;
			}
		}

		countTree.InOrder();
	}
}

相比只有key版本,key_value只是在结构体内多加了value而已,部分函数原理也没有变

4. 二叉搜索树的性能

        插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

        对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

        但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

 

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

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

相关文章

机器学习算法那些事 | 这是我见过最通俗易懂的SVD(奇异值分解)算法介绍

本文来源公众号“机器学习算法那些事”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;这是我见过最通俗易懂的SVD&#xff08;奇异值分解&#xff09;算法介绍 线性代数是机器学习领域的基础&#xff0c;其中一个最重要的概念是奇…

手机号归属地查询如何用Java进行调用

一、什么是手机号归属地查询接口&#xff1f; 手机号归属地查询接口又叫手机号归属地、手机号信息查询、手机号查询&#xff0c;通过手机号查询归属地信息、是否虚拟运营商等。该接口可支持三大运营商&#xff0c;移动、电信、联通等。 二、手机号归属地查询接口适用场景有哪…

OpenCV+Python识别机读卡

背景介绍 正常机读卡是通过读卡机读取识别结果的&#xff0c;目前OpenCV已经这么强大了&#xff0c;尝试着用OpenCVPython来识别机读卡。要识别的机读卡长这样&#xff1a; 我们做以下操作&#xff1a; 1.识别答题卡中每题选中项结果。 不做以下操作&#xff1a; 1.不识别准…

【数据分析:RFM客户价值度模型】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索大数据技术RFM客户价值度模型&#xff0c;本篇文章主要讲述了&#xff1a;RFM客户价值度模型等等。欢迎大家一起探索讨论&#xff01;&#xff01;&#xff01…

大数据-99 Spark 集群 Spark Streaming DStream 文件数据流、Socket、RDD队列流

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

GATK ReadsPathDataSource类介绍

GATK(Genome Analysis Toolkit)是一个广泛使用的基因组分析工具包,它的核心库之一是htsjdk,用于处理高通量测序数据。在GATK中,ReadsPathDataSource类是负责管理和提供读取高通量测序数据文件(如BAM、SAM、CRAM)的类。 常见使用场景 数据加载:在GATK的基因组分析工具链…

MySQL的MRR(Multi-Range Read)优化原理详解

❃博主首页 &#xff1a; 「码到三十五」 &#xff0c;同名公众号 :「码到三十五」&#xff0c;wx号 : 「liwu0213」 ☠博主专栏 &#xff1a; <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 &#xff1a…

LeetCode:反转区间内的链表

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;力扣刷题日记 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 文章目录 反转区间内的链表题目链接方法一&#xff1a;拆开反转…

【TB作品】PIC16F1719单片机,EEPROM,PFM,读写

对于PIC16F1719单片机&#xff0c;没有直接的EEPROM&#xff0c;而是使用高耐久度的程序闪存&#xff08;PFM&#xff09;作为非易失性数据存储区域。这个区域特别适合存储那些需要频繁更新的数据。读写这个内存区域需要操作一些特殊功能寄存器&#xff0c;比如用于地址的PMADR…

2.K8s集群搭建

K8s搭建 搭建方案kubeadm搭建系统初始化操作k8s Master节点初始化将node节点加入集群安装网络插件Calico集群测试 搭建方案 minikube&#xff1a;轻量化的Kubernetes集群&#xff0c;为了能够更好学习和体验k8s功能而推出的&#xff0c;借助个人PC的虚拟化环境就可以实现Kuber…

如何使用ssm实现基于java web的网上书城系统的设计与实现+vue

TOC ssm123基于java web的网上书城系统的设计与实现vue JAVA简介 Java主要采用CORBA技术和安全模型&#xff0c;可以在互联网应用的数据保护。它还提供了对EJB&#xff08;Enterprise JavaBeans&#xff09;的全面支持&#xff0c;java servlet API&#xff0c;JSP&#xff…

【Redis】Redis客户端——Jedis(Java)

Redis Java使用案例 环境配置引入依赖配置端⼝转发连接 Redis Server Java基础代码操作Redisset 和 getexsits 和 del 环境配置 引入依赖 Java 操作 redis 的客⼾端有很多. 其中最知名的是 jedis. 创建 maven 项⽬, 把 jedis 的依赖拷⻉到 pom.xml 中. <!-- https://mvnr…

ssrf--web-ssrfme例题

将web-ssrfme.zip解压缩在Ubuntu下 Docker-compose up -d 更新后的镜像重新启动容器 可以看到已经拉取成功ssrfme镜像 我们使用端口访问文件&#xff0c;可以看到有一个过滤条件&#xff0c;它限制了file&#xff0c;dict协议&#xff0c;127.0.0.1和localhost 也不能用&…

【55-90】结构型模式

目录 一.结构型模式概述 二.代理模式 2.1 概述 2.2 结构 2.3 静态代理 2.4 JDK动态代理 2.5 CGLIB动态代理 2.6 三种代理的对比 2.7 优缺点 三.适配器模式 3.1 概述 3.2 结构 3.3 类适配器模式 3.4 对象适配器模式 3.5 应用场景 四.装饰者模式 4.1 概述 4.2 结…

从并发20到并发120之laravel性能优化

调优成果 遇到问题 单台服务并发20&#xff0c;平均响应时间1124ms&#xff0c;通过htop观察&#xff0c;发现cpu占用率达到100%&#xff08;包括sleep的进程&#xff09;&#xff0c;内存几乎没怎么用。 调优后 单机最大吞吐量达到120 响应时长不超过1000ms 硬件信息 …

数学建模----线性回归分析(引入热力图的绘制方法)

目录 0.直击重点 1.一元线性回归分析 1.1散点图的绘制 1.2相关性的分类 1.3计算相关系数 1.4模型的检验 1.5模型的预测 2.多重线性回归分析&#xff08;上&#xff09; 2.1多重线性的概念 2.2散点图的分类 2.3热力图的绘制 2.4根据结果确定新的变量 3.多重线性…

【开端】 如何判断手机号码属于哪个国家(手机号判断正则)汇总

import org.apache.commons.lang3.StringUtils; /** * 手机号判断正则 */ public enum MobileRegularExp { /** * 国家 正则 */ CN("中国", 86, "^(\\?0?86\\-?)?1[3456789]\\d{9}$"), TW("中国台湾", 886, "…

第七节 循环结构;goto语句

目录 7.1 while循环 7.1.1 if 和 while的对⽐ 7.1.2 while的执行流程 7.1.3 while的练习 7.2 for循环 7.2.1 语法形式 7.2.2 for循环的执⾏流程 7.2.3 for 循环的练习 7.3 while 和 for 循环的对比 7.4 do while 循环 7.4.1 do while 的语法形式 7.4.2 do while循…

Jamba前生今世:1.5开源来袭

AI21服务于企业&#xff0c;为企业构建基础模型和AI系统以加速GenAI在生产中的使用。AI21 成立于2017年&#xff0c;已从NVIDIA、Intel、Google等公司共筹集了3.36亿美元。它是最早将生成式AI推向大众的公司之一&#xff0c;借助AI21平台&#xff0c;企业可以构建自己的生成式A…

菲菲更名宝贝:批量处理,文件命名不再繁琐

你是否有这样的经历&#xff1f;曾几何时&#xff0c;在堆积如山的文件中迷失方向&#xff0c;为了一个个手动重命名文件而加班到深夜&#xff1f;是否渴望有一种魔法&#xff0c;能瞬间让你的文件整理得井井有条&#xff0c;让繁琐的命名工作变得轻松愉快&#xff1f;那么&…