C++笔记:二叉搜索树(Binary Search Tree)

news2024/10/5 14:32:04

文章目录

  • 二叉搜索树的概念
  • 二叉搜索树操作
    • 1. 框架搭建
    • 2. 遍历
    • 3. 查找
      • 迭代实现
      • 递归实现
    • 4. 插入
      • 迭代实现
      • 递归实现
    • 5. 删除
      • 迭代实现
      • 递归实现
    • 6. 析构与销毁
    • 7. 拷贝构造与赋值重载
  • 二叉搜索树的应用
  • 二叉搜索树的性能分析
  • 二叉搜索树模拟实现源码

二叉搜索树的概念

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

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

在这里插入图片描述

二叉搜索树操作

1. 框架搭建

// struct BinarySearchTreeNode - 结点类
template<class K>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;
	K _key;

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

// class BinarySearchTreeNode - 树类
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;

public:

protected:
	Node* _root;
};

【说明】

  1. BSTreeNode 类使用 struct 定义,其成员受默认访问限定符 public 修饰,BSTree 类能够直接访问结点的成员而不需要提供 Get 系列的接口。
  2. BSTree 类的主要功能是维护树的结构,包括插入、删除、搜索等操作,这些操作都是基于根节点展开的。因此,只需要一个根节点的指针就可以代表和维护整棵树。
  3. typedef 操作只是为了简化类型和规范命名,无特别深意。
  4. new 一个新节点时,编译器肯定要调用结点类的构造函数,默认生成的构造函数无法满足要求,所以要显示实现。
  5. 为什么用的是protected而不是private,在不涉及继承的情况下,二者并无区别,如何涉及继承protected的使用优于private

2. 遍历

我们都知道二分查找是一个十分厉害的算法,它能够在 O ( l o g n ) O(logn) O(logn) 的时间复杂度内找到一个目标值,但是它同时又是一个不实用的算法,① 二分查找的前提是要求数据是有序的,对数据预排序会带来额外开销,特别是大型数据集;② 二分查找依赖于顺序表结构的,顺序表结构的头部和中间插入删除开销大,而且插入删除之后需要重新排序,维护成本极高。

但是二叉搜索树规避了这些问题,如果对二叉搜索树进行中序遍历之后就会发现,它从某种意义上来说就是一个天然有序的结构,而且由于其性质的规定,二叉搜索树的插入删除不会影响结构,维护成本低。

public:
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}

protected:
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		
		cout << root->_key << " ";
		
		_Inorder(root->_right);
	}

【说明】

  • 将中序遍历写成子函数然后再封装一层的原因是在类外调用函数要传根节点指针作为参数,但由于成员变量 _root 是私有的,类外无法访问,后面操作的递归实现由于这个原因,也是要封装上一层。
  • 因为子函数_Inorder仅仅只是给Inorder调用,为了保证封装性,使用protected访问限定符修饰。
  • 至于为什么用的是protected而不是private,在不涉及继承的情况下,二者并无区别,如何涉及继承protected的使用优于private

3. 查找

查找到具体过程如下:

  1. 从根结点开始比较、查找。
  2. 目标值比结点的值大则往右边走查找,目标值比结点的值小则往左边走查找。
  3. 找到返回true,走到到空,还没找到,说明值不存在,返回false
  4. 最多查找高度次。

在这里插入图片描述

迭代实现

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;
}

递归实现

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

4. 插入

插入到具体过程如下:

  1. 树为空,则直接新增节点,赋值给 _root 指针,返回true
  2. 树不空,按二叉搜索树性质查找插入位置,插入新节点,返回true
  3. 如果待插入的值已存在,按插入失败处理,返回false

迭代实现

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

	// 树不空,查找何时位置再插入
	Node* cur = _root;
	Node* parent = cur;

	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(key);

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

	return false;
}

递归实现

public:
bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
protected:
	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			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
		{
			return false;
		}
	}

【说明】

  1. 递归实现不像迭代器实现那样要分情况,只要root是空就直接插入。
  2. Node*& root能够如此简单的实现,多亏引用运用,
    如果不加引用,root只是一个临时变量指向待插入位置,还要想办法去找双亲结点;
    加了引用之后,root就是待插位置的双亲结点的孩子指针的别名。

5. 删除

搜索二叉树的删除操作比较复杂,首先,待删结点有五种可能性:
1、待除结点不存在。
2、待删结点是叶子结点。
3、待删结点只存在左子树。
4、待删结点只存在右子树。
5、待删结点左、右子树都存在。

而实际情况中,可能性 2 可以与可能性 3 或者可能性 4 合并起来,因此真正的删除过程如下:

情况1:二叉搜索树为空,或者找不到待删结点,函数返回false,表示删除失败。

情况2:待删结点只存在左子树,先保存待删结点,然后判断待删结点是不是整棵树的根节点:

  • 是根节点:使左子树的根节点作为整棵树的根节点,再删除结点。
  • 不是根节点:使待删节点的双亲结点指向待删节点的左孩子结点,再删除结点。
  • 待删节点有可能是其双亲结点左孩子或者有孩子,这个需要额外判断。

在这里插入图片描述

在这里插入图片描述

情况3:待删结点只存在右子树,先保存待删结点,然后判断待删结点是不是整棵树的根节点:

  • 根节点:使右子树的根节点作为整棵树的根节点,再删除结点。
  • 非根节点:使待删节点的双亲结点指向待删节点的右孩子结点,再删除结点。
  • 待删节点有可能是其双亲结点左孩子或者有孩子,这个需要额外判断。

在这里插入图片描述
在这里插入图片描述

情况4:待删结点左、右子树都存在,先找到待删结点的右子树的最小结点(或者左子树的最大结点),然后用它来替换待删结点(这里选取右子树的最小结点作为替换方案),然后删除找到的最小结点,删除结点时需要加判断:

  • 右子树的最小结点既有可能是其双亲结点的左孩子,也有可能是右孩子。

在这里插入图片描述
在这里插入图片描述

迭代实现

bool Erase(const K& key)
{
	if (_root == nullptr)
		return false;

	Node* cur = _root;
	Node* parent = cur;

	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else // cur->_key == key,执行删除操作
		{
			// 处理只存在右子树
			if (cur->_left == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_right;
					delete cur;
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}

					delete cur;
				}

				return true;
			}
			// 处理只存在左子树
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_left;
					delete cur;
				}
				else
				{
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}

					delete cur;
				}

				return true;
			}
			// 处理左右子树都存在,将待删结点替换成右子树的最小结点
			// 然后转换成删除右子树的最小结点
			else
			{
				Node* rightMinParent = cur;
				Node* rightMin = cur->_right;

				while (rightMin->_left)
				{
					rightMinParent = rightMin;
					rightMin = rightMin->_left;
				}

				cur->_key = rightMin->_key;
				
				if (rightMinParent->_left == rightMin)
					rightMinParent->_left = rightMin->_right;
				else
					rightMinParent->_right = rightMin->_right;

				delete rightMin;

			}
		}
	}

	// cur == nullptr
	return false;
}

递归实现

public:
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
protected:
	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 // root->_key == key,执行删除操作
		{
			Node* del = root;
			// 左为空,右不为空,待删结点只存在右子树
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			// 右为空,左不为空,待删结点只存在左子树
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			// 左右都不为空,左右子树都存在
			else
			{
				Node* rightMin = root->_right;

				while (rightMin->_left)
				{
					rightMin = rightMin->_left;
				}

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

				// 为什么不能传rightMin?
				return _EraseR(root->_right, key);
			}

			delete del;
			return true;
		}
	}

6. 析构与销毁

public:
	~BSTree()
	{
		clear();
	}

	void clear()
	{
		_Destroy(_root);
		_root = nullptr;
	}
protected:
	void _Destroy(Node* root)
	{
		if (root == nullptr)
			return;

		_Destroy(root->_left);
		_Destroy(root->_right);

		delete root;
	}

【说明】

  1. 在某些情况下,我们需要将一颗树清空,按照STL的常规做法该提供一个clear(),清空之后为了避免野指针问题,需要将作为树的入口的_root置空,避免野指针。
  2. 析构的作用是回收对象内部的资源,这个功能恰好可以复用clear()接口。
  3. 清空这棵树采取的做法是后续遍历删除,目的是为了避免内存泄漏。
  4. 后续遍历采用递归实现,需要再封装。

7. 拷贝构造与赋值重载

public:
	// default 关键字强制让编译器生成默认的构造函数
	BSTree() = default;
	
	BSTree(const BSTree<K>& t)
	{
		_root = _Copy(t._root);
	}
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);

		return *this;
	}
protected:
	Node* _Copy(const Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* newRoot = new Node(root->_key);
		newRoot->_left = _Copy(root->_left);
		newRoot->_right = _Copy(root->_right);

		return newRoot;
	}

【说明】

  1. 二叉搜索树的拷贝构造和赋值运算符重载涉及到深拷贝问题,编译器默认生成的函数无法满足要求得自己实现。
  2. 拷贝过程决定采用后序递归构建,由于是递归,所以实现一个子函数_Copy()来完成。
  3. 拷贝构造函数算是构造函数的重载,显式定义拷贝构造函数之后编译器不再会自己生成默认构造函数,这里使用关键字default强制让编译器生成默认构造函数。
  4. 赋值运算符重载参数为BSTree<K> t,对于该写法,编译器会自动调用拷贝构造生成一个临时对象,然后调用库中的swap函数互换_root内容。
  5. 赋值运算符要求支持连续赋值,所以要返回*this

二叉搜索树的应用

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

二叉搜索树的性能分析

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

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

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

在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

二叉搜索树模拟实现源码

#include <iostream>

using namespace std;

namespace ljh
{
	// struct BinarySearchTreeNode - 结点类
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode* _left;
		BSTreeNode* _right;
		K _key;

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

	// class BinarySearchTreeNode - 树类
	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;

	public:
		// default 关键字强制让编译器生成默认的构造函数
		BSTree() = default;

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

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

			return *this;
		}

		~BSTree()
		{
			clear();
		}

		void clear()
		{
			_Destroy(_root);
			_root = nullptr;
		}

		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 Insert(const K& key)
		{
			// 树空,直接作为根结点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			// 树不空,查找何时位置再插入
			Node* cur = _root;
			Node* parent = cur;

			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key);

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

			return false;
		}

		bool Erase(const K& key)
		{
			if (_root == nullptr)
				return false;

			Node* cur = _root;
			Node* parent = cur;

			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else // cur->_key == key,执行删除操作
				{
					// 左为空,右不为空,待删结点只存在右子树
					if (cur->_left == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_right;
							delete cur;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}

							delete cur;
						}

						return true;
					}
					// 右为空,左不为空,待删结点只存在左子树
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
							delete cur;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}

							delete cur;
						}

						return true;
					}
					// 处理左右子树都存在,将待删结点替换成右子树的最小结点
					// 然后转换成删除右子树的最小结点
					else
					{
						Node* rightMinParent = cur;
						Node* rightMin = cur->_right;

						while (rightMin->_left)
						{
							rightMinParent = rightMin;
							rightMin = rightMin->_left;
						}

						cur->_key = rightMin->_key;
						
						if (rightMinParent->_left == rightMin)
							rightMinParent->_left = rightMin->_right;
						else
							rightMinParent->_right = rightMin->_right;

						delete rightMin;

					}
				}
			}

			// cur == nullptr
			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);
		}

	protected:
		void _Inorder(Node* root)
		{
			if (root == nullptr)
				return;

			_Inorder(root->_left);
			
			cout << root->_key << " ";
			
			_Inorder(root->_right);
		}

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

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

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

		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 // root->_key == key,执行删除操作
			{
				Node* del = root;
				// 左为空,右不为空,待删结点只存在右子树
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				// 右为空,左不为空,待删结点只存在左子树
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				// 左右都不为空,左右子树都存在
				else
				{
					Node* rightMin = root->_right;

					while (rightMin->_left)
					{
						rightMin = rightMin->_left;
					}

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

					// 为什么不能传rightMin?
					return _EraseR(root->_right, key);
				}

				delete del;
				return true;
			}
		}

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

			_Destroy(root->_left);
			_Destroy(root->_right);

			delete root;
		}

		Node* _Copy(const Node* root)
		{
			if (root == nullptr)
				return nullptr;

			Node* newRoot = new Node(root->_key);
			newRoot->_left = _Copy(root->_left);
			newRoot->_right = _Copy(root->_right);

			return newRoot;
		}

	protected:
		Node* _root = nullptr;
	};
}

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

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

相关文章

5G网络(接入网+承载网+核心网)

5G网络&#xff08;接入网承载网核心网&#xff09; 一、5G网络全网架构图 这张图分为左右两部分&#xff0c;右边为无线侧网络架构&#xff0c;左边为固定侧网络架构。 无线侧&#xff1a;手机或者集团客户通过基站接入到无线接入网&#xff0c;在接入网侧可以通过RTN或者IP…

欢迎 Gemma: Google 最新推出开源大语言模型

今天&#xff0c;Google 发布了一系列最新的开放式大型语言模型 —— Gemma&#xff01;Google 正在加强其对开源人工智能的支持&#xff0c;我们也非常有幸能够帮助全力支持这次发布&#xff0c;并与 Hugging Face 生态完美集成。 Gemma 提供两种规模的模型&#xff1a;7B 参数…

js之事件代理/事件委托

事件代理也叫事件委托&#xff0c;原理&#xff1a;利用DOM元素的事件冒泡&#xff0c;指定一个事件的处理程序就可以管理某一类型的所有事件。 事件冒泡和事件捕获 如上图所示&#xff0c;事件传播分成三个阶段&#xff1a; 捕获阶段&#xff1a;从window对象传导到目标节点&…

【Qt】信号和槽机制

目录 一、认识信号和槽 二、connect函数 三、自定义槽函数 四、自定义信号 五、带参数的信号和槽 六、信号和槽断开连接 七、信号和槽存在的意义 八、Lambda表达式定义槽函数 一、认识信号和槽 概述 在Qt中&#xff0c;用户和控件的每次交互过程称为一个事件。如"…

【Spring】SpringBoot 单元测试

目 录 一.什么是单元测试&#xff1f;二.单元测试有哪些好处&#xff1f;三.Spring Boot 单元测试使用单元测试的实现步骤 一.什么是单元测试&#xff1f; 单元测试&#xff08;unit testing&#xff09;&#xff0c;是指对软件中的最小可测试单元进行检查和验证的过程就叫单元…

如何查看电脑使用记录?保障个人隐私和安全

查看电脑使用记录是了解电脑活动的一种重要方式&#xff0c;可以帮助用户追踪应用程序的使用情况、登录和关机时间、文件的访问记录等。在本文中&#xff0c;我们将介绍如何查看电脑使用记录的三个方法&#xff0c;以分步骤详细说明如何查看电脑使用记录&#xff0c;帮助用户更…

07 MyBatis之高级映射 + 懒加载(延迟加载)+缓存

1. 高级映射 例如有两张表, 分别为班级表和学生表 自然, 一个班级对应多个学生 像这种数据 , 应该如果如何映射到Java的实体类上呢? 这就是高级映射解决的问题 以班级和学生为例子 , 因为一个班级对应多个学生 , 因此学生表中必定有一个班级编号字段cid 但我们在学生的实体…

MT8791迅鲲900T联发科5G安卓核心板规格参数_MTK平台方案定制

MT8791安卓核心板是一款搭载了旗舰级配置的中端手机芯片。该核心板采用了八核CPU架构设计&#xff0c;但是升级了旗舰级的Arm Cortex-A78核心&#xff0c;两个大核主频最高可达2.4GHz。配备了Arm Mali-G68 GPU&#xff0c;通过Mali-G88的先进技术&#xff0c;图形处理性能大幅提…

PyTorch:transforms.Normalize()函数详解

PyTorch&#xff1a;transforms.Normalize()函数详解 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得到您的订阅和…

华为配置WDS手拉手业务示例

配置WDS手拉手业务示例 组网图形 图1 配置WDS手拉手业务示例组网图 业务需求组网需求数据规划配置思路配置注意事项操作步骤配置文件 业务需求 企业用户通过WLAN接入网络&#xff0c;以满足移动办公的最基本需求。但企业考虑到AP通过有线部署的成本较高&#xff0c;所以通过建立…

智慧公厕是什么?智慧公厕是构建智慧城市的环境卫生基石

随着城市化进程的不断加速&#xff0c;城市人口密度和流动性也逐渐增大&#xff0c;对城市公共设施的需求与日俱增。而在这些公共设施中&#xff0c;公厕作为城市基础设施中不可或缺的一环&#xff0c;对城市的环境卫生和市民生活质量起着举足轻重的作用。如何提高公厕的管理效…

ChatGPT plus 的平替:9个可以联网的免费AI搜索引擎

ChatGPT plus 的平替&#xff1a;9个可以联网的免费AI搜索引擎。 由于ChatGPT 训练数据截止到2021年9月&#xff0c;在该时间点之后发生的事件&#xff0c;ChatGPT均无法给出答复。所以&#xff0c;大家现在都非常期待ChatGPT能够联网&#xff0c;访问实时的信息。 ChatGPT pl…

谷歌gemma2b windows本地cpu gpu部署,pytorch框架,模型文件百度网盘下载

简介 谷歌DeepMind发布了Gemma,这是一系列灵感来自用于Gemini相同研究和技术的开放模型。开放模型适用于各种用例,这是谷歌非常明智的举措。有2B(在2T tokens上训练)和7B(在6T tokens上训练)模型,包括基础和指令调整版本。在8192个token的上下文长度上进行训练。允许商业使…

Linux之ACL权限管理

文章目录 1.ACL权限介绍二、操作步骤1. 添加测试目录、用户、组&#xff0c;并将用户添加到组2. 修改目录的所有者和所属组3. 设定权限4. 为临时用户分配权限5. 验证acl权限6. 控制组的acl权限 1.ACL权限介绍 每个项目成员有一个自己的项目目录&#xff0c;对自己的目录有完全…

Java Stream API的二度深入

Java Stream API的二度深入 前言 为什么会写这样一篇文章呢&#xff1f; 1.面试的时候&#xff0c;一位前辈对我这方面有过一次提问&#xff0c;我随口回答&#xff0c;前辈很信任我&#xff0c;以此文致敬前辈&#xff01; 2.去回顾&#xff0c;去扎实&#xff0c;对得起前辈的…

Spring及工厂模式概述

文章目录 Spring 身世什么是 Spring什么是设计模式工厂设计模式什么是工厂设计模式简单的工厂设计模式通用的工厂设计 总结 在 Spring 框架出现之前&#xff0c;Java 开发者使用的主要是传统的 Java EE&#xff08;Java Enterprise Edition&#xff09;平台。Java EE 是一套用于…

SwiftUI 支持拖放功能的集合视图(Grid)如何捕获手指按下并抬起这一操作

功能需求 假设我们开发了一款 SwiftUI 应用,其中用户可以通过拖放 Grid 中的 Cell 来完成一些操作。现在,我们希望用户在某个 Cell 被按下并随后抬起手指时得到通知,这能够实现吗? 如上图所示,我们准确地捕获到了手指在 Grid 的 Cell 上按下再抬起这一操作!那么它是如何…

算法沉淀——穷举、暴搜、深搜、回溯、剪枝综合练习四(leetcode真题剖析)

算法沉淀——穷举、暴搜、深搜、回溯、剪枝综合练习四 01.解数独02.单词搜索03.黄金矿工04.不同路径 III 01.解数独 题目链接&#xff1a;https://leetcode.cn/problems/sudoku-solver/ 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&am…

亿道丨三防平板丨加固平板丨为零售业提供四大优势

随着全球经济的快速发展&#xff0c;作为传统行业的零售业也迎来了绝佳的发展机遇&#xff0c;在互联网智能化的大环境下&#xff0c;越来越多的零售企业选择三防平板电脑作为工作中的电子设备。作为一种耐用的移动选项&#xff0c;三防平板带来的不仅仅是坚固的外壳。坚固耐用…

第十四章[面向对象]:14.9:定制类

一,__len__()方法返回长度 1,len()函数 len()函数: 功能:len() 函数返回对象(字符、列表、元组等)长度或项目个数 语法: len( s ) 参数:s : 要查询长度的对象 返回值: 返回对象长度 2,没有定义__len__()方法时,对实例应用len()函数会引发TypeError class Student: …