数据结构——搜索二叉树

文章目录

  • 一. 概念
  • 二. 二叉搜索树的操作
    • 1.查找
    • 2.插入
    • 3.删除(重点)
    • 4.遍历
    • 5.拷贝构造与析构
  • 三.二叉搜索树的递归实现
    • 1.递归查找
    • 2.递归插入
    • 3.递归删除
  • 四.二叉搜索树的性能分析
  • 五.二叉树搜索的应用
  • 六.源码

前言:
本章我们将认识一种新的二叉树——搜索二叉树。这棵树有个神奇的功能就是会对数据自动排序且有着非常高的查找效率。搜索二叉树作为set、map的基础结构,同样又是接下来将要学到的AVL树以及红黑树的实现基础非常值得我们去深入学习~

一. 概念

叉搜索树本质上也是一种二叉树,只不过多了一个约束规则——

若一棵二叉树不为空,则:

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

所以构建一个搜索二叉树,只需要在插入结点时满足约束规则即可。

二. 二叉搜索树的操作

与二叉树相同,二叉搜索树由一个个结点链接而成。每个结点包含三个成员——

template <class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;  //左孩子
	BSTreeNode<K>* _right; //右孩子
	K _key;				   //键值

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

所以再定义出BSTNode(Binary Search Tree简写)结构体——

template <class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	// 成员函数的实现
	// 插入、删除、查找...
private:
	Node* _root = nullptr;
};

接着就是各种成员函数的实现了

1.查找

搜索二叉树的查找比较简单而且更容易帮助我们理解搜索二叉树的性质,所以先从查找入手。
在这里插入图片描述

以上图为例,倘若我们要查找 7,具体的思路是这样的——

  • 7 < 8,因此去 8 的左子树去查找
  • 7 > 3,因此去 3 的右子树去查找
  • 7 > 6,因此去 6 的右子树去查找
  • 7 = 7,找到了,返回true

于是我们试着着手实现一个Find函数

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; // 找到返回true
	}
	return false; // 未找到返回false
}

2.插入

理解了如何查找,插入也就非常简单。

在这里插入图片描述

还是以此图为例,倘若我们要插入 9 ,具体步骤为——

  • 首先确定cur的位置,并随时更新parent
  • 最终,cur走到10的左节点的位置,即cur = nullptr,循环结束
  • 此时patent = Node*(10)
  • 最后一步,new一个结Node*(key)并赋值给parent->_left即可。
bool Insert(const K& key)
{
	// 如果是第一次插入,直接new一个新结点给_root
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}
	
	Node* cur = _root; // cur用来定位插入的位置
	Node* parent = cur; // 记录parent的父亲
	
	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 true;
}

3.删除(重点)

二叉搜索树的删除是最精华的部分。对与叶子节点,例如4、7、13,删除非常简单,只需将自身的位置替换为nullptr即可。

在这里插入图片描述

如果要删除14或者10,也是比较简单的,因为10的左右子树只有一方为nullptr(10的左子树为空),所以只需要载删除的时候让父结点接管自己不为空的子树即可。

倘若要删除6或者3,由于它们的左右子树都不为空,删除时无法将两个子树都交给父结点,情况就较为复杂。

所以此种情况,我们只能想办法请一个人来接替自己的位置,但是并不是谁来都能胜任这个位置的。这个接替者必须满足二叉搜索树的条件——左子树都比它小,右子树都比它大。那么这个接替者的人选只能有这两个——

  • 左子树的最大(最右)节点
  • 或右子树的最小(最左)节点

例如,倘若要删除3,此时有两种做法都可行——

  • 用1替换3
  • 用7替换3

综上所述,删除操作共分为一下几种情况——

  1. 左子树为空
  2. 右子树为空
  3. 左右子树都不为空
  4. (左右子树都为空其实可以归并到1或2的情况中)
bool Erase(const K& key)
{
	Node* cur = _root;
	Node* parent = cur;
	while (cur)
	{
		// 找到值为key的结点
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else // 找到了
		{	
			// 删除
			if (cur->_left == nullptr) // 1.左子树为空
			{
				if (cur == _root) // 根节点的删除
				{
					_root = cur->_right;
					return true;
				}
				else
				{
					if (parent->_left == cur)
						parent->_left = cur->_right;
					else
						parent->_right = cur->_right;
					delete cur;
				}
			}
			else if (cur->_right == nullptr) // 2.右子树为空
			{
				if (cur == _root) // 根节点的删除
				{
					_root = cur->_left;
					return true;
				}
				else
				{
					if (parent->_left == cur)
						parent->_left = cur->_left;
					else
						parent->_right = cur->_left;
					delete cur;
				}
			}
			else // 左右子树都不为空
			{
				// 找左子树的最大结点 或者 右子树的最小结点
				Node* minRight = cur->_right;
				Node* pminRight = cur;

				while (minRight->_left)
				{
					pminRight = minRight;
					minRight = minRight->_left;
				}

				cur->_key = minRight->_key; // 替换
				
				if (pminRight->_left == minRight)
				{
					pminRight->_left = minRight->_right;
				}
				else
				{
					pminRight->_right = minRight->_right;
				}

				delete minRight;
			}
			return true;
		}
	}
	return false;
}

4.遍历

二叉搜索树的遍历非常简单,就是之前学习过的二叉树的中序遍历

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

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

注:由于调用函数时C++封装的特性,需设计两个函数,InOrder接口对外提供,_InOrder不对外提供。

5.拷贝构造与析构

	//采用前序遍历拷贝构造
	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;
	}
	//采用后序遍历的方式来删除,从下往上删。
	void _Destroy(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}

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

	~BSTree()
	{
		_Destroy(_root);
	}

因为拷贝构造二叉搜索树时要保证树的结构与原来树的结构一致,因此采用前序遍历进行拷贝构造。

但如果写了拷贝构造之后编译器就不会生成默认的构造函数了,因为拷贝构造也属于构造,因此可以利用一下C++11的特性,强制编译器生成一个默认的拷贝构造

//强制编译器生成一个默认的拷贝构造
BSTree() = default;

三.二叉搜索树的递归实现

对于搜索二叉树来说,上面实现的非递归版本是比递归版本更优的。此处的递归实现完全属于多余了,但是作为拓展内容看一看也未尝不可。

1.递归查找

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 true;
	if (root->_key > key)
		_FindR(root->_left, key);
	else
		_FindR(root->_right, key);
}

2.递归插入

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

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

3.递归删除

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;
		//1.右为空
		if (root->_right == nullptr)
			root = root->_left;
		//2.左为空
		else if (root->_left)
			root = root->_right;
		//3.左右都不为空
		else
		{
			//找左子树最大节点交换
			Node* maxleft = root->_left;
			while (maxleft->_right)
				maxleft = maxleft->_right;
			
			//找到后先交换要删除的值与左子树最大节点的值
			swap(root->_key, maxleft->_key);
			//再递归到左子树中去删除
			return _EraseR(root->_left, key);
		}
		delete del;

		return true;
	}
}

四.二叉搜索树的性能分析

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

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

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

在这里插入图片描述

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

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

五.二叉树搜索的应用

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

六.源码

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

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

	template <class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		//强制编译器生成构造函数
		BSTree() = default;
		//拷贝构造
		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}
		//赋值重载
		BSTree<k>& operator=(BSTree<K> t)
		{
			swap(_root, t._root);
			return *this;
		}
		~BSTree()
		{
			Destroy(_root);
		}
		//插入节点
		bool Insert(const K& key)
		{
			//直接插入根
			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
				{
					returen false;
				}
			}

			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->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					returen 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
				{
					//1.左为空
					if (cur->_left == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right == cur->_right;
							}
						}
						delete cur;
					}

					//2.右为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right == cur->_left;
							}
						}
						delete cur;
					}

					//3.左右都不为空,找右树最小节点或左树最大节点替代cur
					else
					{
						Node* pminRight = cur;
						Node* minRight = cur->_right;

						while (minRight->_left)
						{
							pminRight = minRight;
							minRight = minRight->_left;
						}
						cur->_key = minRight->_key;
						if (pminRight->_left == minRight)
						{
							pminRight->_left = minRight->_right;
						}
						else
						{
							pminRight->_right = minRight->_right;

						}
						delete minRight
					}
					return true;
				}
			}
			return false;
		}
		//递归版
		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
		bool Erase(const K& key)
		{
			return _Erase(_root, key);
		}
		bool Insorder()
		{
			_Insorder(_root);
			cout << endl;
		}
	protected:
		Node* Copy(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;
		}
		void Destroy(Node*& root)
		{
			if (root == nullptr)
				return;
			Destroy(root->_left);
			Destroy(root->_right);

			delete root;
			root = nullptr;
		}

		bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;
			if (root->_key == key)
				return true;
			if (root->_key < key)
				return _FindR(root->_right, key);
			else 
				return _FindR(root->_left, key);
		}
		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
			{
				Node* del = root;
				//1.右为空
				if (root->_right == nullptr)
					root = root->_left;
				//2.左为空
				else if (root->_left)
					root = root->_right;
				//3.左右都不为空
				else
				{
					//找左子树最大节点交换
					Node* maxleft = root->_left;
					while (maxleft->_right)
						maxleft = maxleft->_right;
					
					//找到后先交换要删除的值与左子树最大节点的值
					swap(root->_key, maxleft->_key);
					//再递归到左子树中去删除
					return _EraseR(root->_left, key);
				}
				delete del;

				return true;
			}
		}
	
	private:
		Node* _root;
	};
}

本文到此结束,码文不易,还请多多支持!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot配置文件与日志文件

1. Spring Boot 配置文件 我们知道, 当我们创建一个Spring Boot项目之后, 就已经有了配置文件存在于目录结构中. 1. 配置文件作用 整个项目中所有重要的数据都是在配置文件中配置的&#xff0c;比如: 数据库的连接信息 (包含用户名和密码的设置) ;项目的启动端口;第三方系统的调…

【chrome扩展开发】vue-i18n使用问题及解决方案

记录chrome扩展开发时调用vue-i18n的一些问题和解决方法 环境 vue: ^3.3.4vue-i18n: ^9.2.2vite: ^4.4.8 错误1 Uncaught (in promise) EvalError: Refused to evaluate a string as JavaScript because unsafe-eval is not an allowed source of script in the following Con…

vi 编辑器入门到高级

vi 编辑器的初级用法vi 编辑器的工作模式1. 命令模式2. 文本输入模式3. 状态行vi 工作模式切换存储缓冲区 vi 编辑器命令1. 启动 vi2. 文本输入3. 退出 vi4. 命令模式下的 光标移动5. 命令模式下的 文本修改6. 从 命令模式 进入 文本输入模式7. 搜索字符串8. vi 在线帮助文档 v…

云原生Kubernetes:阿里云托管k8s集群ACK创建和使用

目录 一、理论 1.容器服务Kubernetes版 2.ACK Pro版集群概述 3.CKA版本说明 二、实验 1.创建专有版Kubernetes集群 三、问题 1.依赖检查未通过 一、理论 1.容器服务Kubernetes版 &#xff08;1&#xff09;概念 阿里云容器服务Kubernetes版&#xff08;Alibaba Cloud…

mysql转sqlite3

在项目中需要将mysql迁移到sqlite3中&#xff0c;此时需要作数据转换 准备工作 下载mysql2sqlite转换工具 https://github.com/dumblob/mysql2sqlite/archive/refs/heads/master.zip 下载sqlite3 https://www.sqlite.org/download.html 转换 命令行中输入如下命令 1、cd …

Vue——webpack

webpack 一、Install1.全局安装2.局部安装 二、总结1.打包2.定义脚本3.配置文件定义&#xff08;webpack.config.js)4.项目重新加载依赖5.webpack打包Css6.style-loader 一、Install 1.全局安装 npm install webpack webpack-cli -g2.局部安装 以项目为单位&#xff0c;一个项…

skywalking日志收集

文章目录 一、介绍二、添加依赖三、修改日志配置1. 添加链路表示traceId2. 添加链路上下文3. 异步日志 四、收集链路日志 一、介绍 在上一篇文章skywalking全链路追踪中我们介绍了在微服务项目中使用skywalking进行服务调用链路的追踪。 本文在全链路追踪的基础上&#xff0c…

QT生成Debug和Release发布版后,运行exe缺少dll问题

在QT Creator生成debug和release的exe执行文件后&#xff0c;运行时&#xff0c;报错缺少*.dll.解决办法1&#xff1a; 在系统环境变量中添加D:\Qt\Qt5.13.2\Tools\mingw730_64\bin后&#xff0c;即可运行。 当使用此方法时&#xff0c;将exe拷贝到其他电脑中运行时&#xff0c…

科技感响应式管理系统后台登录页ui设计html模板

做了一个科技感的后台管理系统登录页设计&#xff0c;并且尝试用响应式布局把前端html写了出来&#xff0c;发现并没有现象中的那么容易&#xff0c;chrome等标准浏览器都显示的挺好&#xff0c;但IE11下面却出现了很多错位&#xff0c;兼容起来还是挺费劲的&#xff0c;真心不…

《吐血整理》高级系列教程-吃透Fiddler抓包教程(36)-掌握Fiddler中FiddlerScript用法你会有多牛逼-上

1.简介 Fiddler是一款强大的HTTP抓包工具&#xff0c;它能记录所有客户端和服务器的http和https请求&#xff0c;允许你监视&#xff0c;设置断点&#xff0c;甚至修改输入输出数据. 使用Fiddler无论对开发还是测试来说&#xff0c;都有很大的帮助。Fiddler提供的功能基本上能…

全网最全Linux 运行jar包的几种方式

一、Linux 运行jar包的几种方式 方式一&#xff1a; java -jar xxx.jar 最常用的启动jar包命令&#xff0c;特点&#xff1a;当前ssh窗口被锁定&#xff0c;可按CTRL C打断程序运行&#xff0c;或直接关闭窗口&#xff0c;程序退出 方式二&#xff1a; java -jar xxx.jar &…

复原 IP 地址——力扣93

文章目录 题目描述回溯题目描述 回溯 class Solution{public:static constexpr int seg_count=4<

使用Python + Flask搭建web服务

示例脚本 from flask import Flask# 获取一个实例对象 app Flask(__name__)# 1、注册 app.route(/reg, methods[get]) def reg():return {code: 200,msg: reg ok!}# 2、登录 app.route(/login, methods[get]) def login():return login ok&#xff01;if __name__ __main__:…

防火墙第五次作业

1. 什么是恶意软件&#xff1f; 恶意软件官方的一个定义&#xff1a;恶意软件 (Malware) 从“恶意”(malicious) 和“软件”(software) 这两个词合并而来&#xff0c;是一个通用术语&#xff0c;可以指代病毒、蠕虫、特洛伊木马、勒索软件、间谍软件、广告软件和其他类型的有害…

【vue】vue基础知识

1、插值表达式&属性绑定 <!--template展示给用户&#xff0c;相当于MVVM模式中的V--> <template><div class"first_div">//插值表达式<p>{{ message }}</p>//这里的参数是从父组件的template里传过来的<p>{{data_1}}</p…

深度学习——全维度动态卷积ODConv

ODConv(OMNI-DIMENSIONAL DYNAMIC CONVOLUTION)是一种关注了空域、输入通道、输出通道等维度上的动态性的卷积方法&#xff0c;因此被称为全维度动态卷积。 part1. 什么是动态卷积 动态卷积就是对卷积核进行线性加权 第一篇提出动态卷积的文章也是在SE之后&#xff0c;他提出…

uni-app:实现数字文本框,以及左右加减按钮

效果 代码 <template><view><view classline3><view classline3_position><view classleft>数量<text>*</text></view> <view class"right"><view class"quantity_btn"><view class"…

【知网检索稳定】第八届现代管理和教育技术国际学术会议(MMET2023)

第八届现代管理和教育技术国际学术会议&#xff08;MMET 2023&#xff09;将于2023年09月22-24日在中国上海召开。会议由四川大学、泰国程逸皇家大学、泰国程逸皇家大学中泰同文同学国际交流中心主办、乐山师范学院、四川职业技术学院、AEIC学术交流中心协办。会议主要围绕会议…

TechTool Pro for mac(硬件监测和系统维护工具)

TechTool Pro 是为 Mac OS X 重新设计的全新工具程序&#xff0c;不但保留旧版原有的硬件侦测功能&#xff0c;还可检查系统上其他重要功能&#xff0c;如&#xff1a;网络连接&#xff0c;区域网络等。 TechTool Pro for mac随时监控和保护您的电脑&#xff0c;并可预设定期检…

【Linux取经路】冯诺依曼结构体系与操作系统的碰撞

文章目录 一、冯诺依曼体系结构1.1 硬件介绍1.2 内存的重要性 二、操作系统2.1 设计操作系统的目的2.2 操作系统是如何进行管理的&#xff1f; 一、冯诺依曼体系结构 我们现在常见的计算机&#xff0c;如笔记本&#xff0c;以及我们不常见的计算机&#xff0c;如服务器&#x…
最新文章