STL库 —— unordered_set与unordered_map的封装

news2024/10/5 20:20:18

这里要对 unordered_set 与 unordered_map 进行封装,封装时使用的是上一篇中学的 HashBucket 。不仅要完成封装,同时要写入迭代器。

一、HashBucket 的修改

1.1 节点的修改 T

首先来认识一下使用 unordered_set 和 ordered_map 时的区别:

unordered_set 存储唯一的键值。你只需要传入要插入的值。

#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_set<int> mySet;
    mySet.insert(10);
    mySet.insert(20);
    
    for (const auto& elem : mySet) {
        std::cout << elem << " ";
    }
    return 0;
}

unordered_map 存储键值对。你需要传入键和值。 

#include <unordered_map>
#include <iostream>

int main() {
    std::unordered_map<int, std::string> myMap;
    myMap.insert({1, "one"});
    myMap.insert({2, "two"});
    
    for (const auto& pair : myMap) {
        std::cout << pair.first << ": " << pair.second << " ";
    }
    return 0;
}

因为 unordered_set 只存储值,而 unordered_map 存储键值对并提供键到值的映射,所以当它们底层使用同一容器进行封装时,要求该容器可以兼容这两种数据类型

template<class T>//
struct HashNode
{
	T _data;//
	HashNode* _next;
	HashNode(const T& data) :_data(data), _next(nullptr)//
	{}
};

节点模板的修改是为了满足两者的需要,满足双方的供求

1.2 类的修改 KeyOfT

因为 unordered_map 传入的是键值对,但是并不知道键值对的键值是什么,所以在定义类模板时,会多传入一个参数 KeyOfT ,作为键值。但肯定有很多人会疑惑,传入的本身就是键值对了,  pair<K, V> 中的 K 不就是键值对的键值吗?这么做不是多此一举吗?

事实上,传入 KeyOfT 而不是直接使用键 Key 的主要原因是为了提高代码的灵活性和通用性,尤其是在存储复杂对象时。以下是一个具体的例子来说明这种情况:

示例场景

假设我们有一个存储复杂对象的哈希表,这些对象有多个属性,其中一个属性作为键。

struct Employee 
{
    int employee_id;
    std::string name;
    std::string department;

    Employee(int id, const std::string& n, const std::string& d) 
        : employee_id(id), name(n), department(d) {}
};
KeyOfT 提取键的机制

我们需要一种机制来从 Employee 对象中提取 employee_id 作为键。为此,我们定义一个 KeyOfEmployee 函数对象。

struct KeyOfEmployee
{
    int operator()(const Employee& emp) const
    {
        return emp.employee_id;
    }
};
键值对
  • employee_id(例如,1)
  • Employee 对象(例如,Employee(1, "Alice", "HR")
传入示例
bool Insert(const T& obj)
{
    K key = KeyOfT()(obj);
    size_t index = Hash()(key) % _bucket.size();
    _bucket[index] = new T(obj);
    return true;
}
HashBucket<int, Employee, KeyOfEmployee, HashFunc> hb;
hb.Insert(Employee(1, "Alice", "HR"));
hb.Insert(Employee(2, "Bob", "IT"));

由上面的案例就不难看出,传入的对象不一定是键值对,有可能是自定义的类对象,此时就只需要添加 KeyOfT 的模板,就可以在类内部找到键值。

类模板添加KeyOfT

template<class K, class T, class KeyOfT, class Hash = Hashfunc<K>>
class HashBucket
{
private:
	vector<Node*> _bucket;
	size_t _n;
};

unordered_set 来说,KeyOfT 的方式有些多此一举。unordered_set 本质上是一个存储唯一元素的集合,没有键值对的概念。所以这里的修改是为了将就 unordered_map

    //unordered_set
    template<class K>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& Key)
			{
				return Key;
			}
		};
	public:
	private:
		HashBucket<K, K, SetKeyOfT> _ht;//注意传参要对应HashBucket
	};
    //unordered_map
    template<class K, class V>
    class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
	private:
		HashBucket<K, pair<K, V>, MapKeyOfT> _ht;//注意传参要对应HashBucket
	};

1.3 类的修改 HashFunc

下面先来看一下这三个类与其对应的类模板:

template<class K>
class unordered_set	
{
private:
	HashBucket<K, K, SetKeyOfT> _ht;

};
    
template<class K, class V>
class unordered_map
{
private:
    HashBucket<K, pair<K, V>, MapKeyOfT> _ht;
};

template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashBucket
{};

 在使用时,用户是直接使用 unordered_set 与 unordered_map ,所以应该希望在 unordered_set 与 unordered_map 层有一个默认的 HashFunc ,这样用户不仅可以自定义,也可以使用默认的 HashFunc ,提高了代码的灵活性,而在 HashBucket 层,只需要按照上层的指令来即可,所以就需要把默认的 HashFunc 提前到上层。

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
private:
    HashBucket<K, K, SetKeyOfT, Hash> _ht;
};

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
private:
    HashBucket<K, pair<K, V>, MapKeyOfT, Hash> _ht;
};

template<class K, class T, class KeyOfT, class Hash>
class HashBucket;
{};

二、迭代器

2.1 定义迭代器成员

首先,迭代器要知道自己的位置,这就需要定义一个节点指针,另外,当在哈希桶中使用自增直到遍历完 vector 的某一节点时,因为迭代器另一个单独的类,所以需要让迭代器直到自己所处的哈希桶的结构,才好寻找下一个存在值的节点,这就需要定义一个哈希桶的指针

这样不仅得到了迭代器的成员,也得到了迭代器的构造函数。 

template<class K, class T, class KeyOfT, class Hash>
struct __HtIterator
{
	typedef HashNode<T> Node;
	Node* _node;
	HashBucket<K, T, KeyOfT, Hash>* _pht;

	__HtIterator(Node* node, HashBucket<K, T, KeyOfT, Hash>* pht)
		:_node(node), _pht(pht)
	{}
};

问题1:

因为迭代器中存在了哈希桶的指针来指向哈希桶,那么当遍历哈希桶的数组时,不可避免地会使用到哈希桶的 _bucket ,但是这又是个私有成员,如何解决呢?

可以使用友元来帮助解决(省略不必要的部分):

template<class K, class T, class KeyOfT, class Hash>
class HashBucket
{
public:
	template<class K, class T, class KeyOfT, class Hash>
	friend struct __HtIterator;//友元
};

问题2:

在迭代器中,存在了哈希桶;在哈希桶中,又用到了迭代器。那么又有一个问题,编译器访问某一个的时候,必然会访问不到另一个,这是代码顺序的问题,这个问题怎么解决呢?和函数声明类似,可以在迭代器前加上哈希桶的类声明:

template<class K, class T, class KeyOfT, class Hash>//类声明
class HashBucket;

template<class K, class T, class KeyOfT, class Hash>
struct __HtIterator
{};

2.2 begin 与 end 函数

2.2.1 begin 函数

如下图, begin 返回的是哈希桶第一个存值的迭代器,所以只需要挨个遍历即可,但是返回节点的地址容易,那么哈希桶的地址怎么办呢?这可是在哈希桶类中返回自己的地址,应该怎么办?

return iterator(cur, this);

其实答案很久之前就已经学过了,this指针代表的不就是本身吗?

若没有找到,可以直接返回 end ,下面只需要对 end 继续做优化即可。

template<class K, class T, class KeyOfT, class Hash>
class HashBucket
{
    typedef HashNode<T> Node;
public:
    typedef __HtIterator<K, T, KeyOfT, Hash> iterator;
    iterator begin()
    {
        for (size_t i = 0; i < _bucket.size(); i++)
        {
            Node* cur = _bucket[i];
            if (cur)
            {
                return iterator(cur, this);
            }
        }
        return end();
    }
private:
    vector<Node*> _bucket;
	size_t _n;
};

2.2.2 end 函数

end 返回的是最后一个存值的节点的下一个位置,所以直接可以使用空指针来构造:

	iterator end()
	{
		return iterator(nullptr, this);
	}

2.3 operator重载

2.3.1 自增的重载

这里有两种情况:

1.当前桶还为遍历完,那么迭代器可以直接指向当前节点的下一个。

2.当前桶已经遍历完,那么就需要遍历整个 vector ,直到找到下一个不为空的桶。

template<class K, class T, class KeyOfT, class Hash>
struct __HtIterator
{
	typedef HashNode<T> Node;
	typedef __HtIterator<K, T, KeyOfT, Hash> Self;

	Node* _node;
	HashBucket<K, T, KeyOfT, Hash>* _pht;

	__HtIterator(Node* node, HashBucket<K, T, KeyOfT, Hash>* pht)
		:_node(node), _pht(pht)
	{}

	Self& operator++()
	{
		if (_node->_next)
		{
			//当前桶未遍历完,取桶的下一个节点
			_node = _node->_next;
		}
		else
		{
			//当前桶已遍历完,找下一个不为空的桶
			KeyOfT kot;
			Hash hs;
			size_t i = hs(kot(_node->_data)) % _pht->_bucket.size();
			++i;
			for (; i < _pht->_bucket.size(); i++)
			{
				if (_pht->_bucket[i])
					break;
			}
			if (i == _pht->_bucket.size()) _node = nullptr;//没找到下一个不为空的桶
			else _node = _pht->_bucket[i];//找到了下一个不为空的桶
		}
		return *this;
	}
};

2.3.2 解引用和不等于的重载

这两个比较简单,就直接放在一起上代码了:

    T& operator*()
	{
		return _node->_data;
	}
	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

2.4 完整代码

下面就可以来测试一下迭代器了,测试之前先看一下完整的代码:

2.4.1 完整的迭代器

template<class K, class T, class KeyOfT, class Hash>//
class HashBucket;

template<class K, class T, class KeyOfT, class Hash>
struct __HtIterator
{
	typedef HashNode<T> Node;
	typedef __HtIterator<K, T, KeyOfT, Hash> Self;

	Node* _node;
	HashBucket<K, T, KeyOfT, Hash>* _pht;

	__HtIterator(Node* node, HashBucket<K, T, KeyOfT, Hash>* pht)
		:_node(node), _pht(pht)
	{}

	Self& operator++()
	{
		if (_node->_next)
		{
			//当前桶未遍历完,取桶的下一个节点
			_node = _node->_next;
		}
		else
		{
			//当前桶已遍历完,找下一个不为空的桶
			KeyOfT kot;
			Hash hs;
			size_t i = hs(kot(_node->_data)) % _pht->_bucket.size();
			++i;
			for (; i < _pht->_bucket.size(); i++)
			{
				if (_pht->_bucket[i])
					break;
			}
			if (i == _pht->_bucket.size()) _node = nullptr;//没找到下一个不为空的桶
			else _node = _pht->_bucket[i];//找到了下一个不为空的桶
		}
		return *this;
	}
	T& operator*()
	{
		return _node->_data;
	}
	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}
};

2.4.2 完整的HashBucket

template<class K, class T, class KeyOfT, class Hash>//
class HashBucket
{
	typedef HashNode<T> Node;//
public:
	template<class K, class T, class KeyOfT, class Hash>
	friend struct __HtIterator;
	 
	typedef __HtIterator<K, T, KeyOfT, Hash> iterator;
	iterator begin()
	{
		for (size_t i = 0; i < _bucket.size(); i++)
		{
			Node* cur = _bucket[i];
			if (cur)
			{
				return iterator(cur, this);
			}
		}
		return end();
	}
	iterator end()
	{
		return iterator(nullptr, this);
	}
	HashBucket()
	{
		_bucket.resize(10, nullptr);
		_n = 0;
	}
	~HashBucket()
	{
		for (size_t i = 0; i < _bucket.size(); i++)
		{
			Node* cur = _bucket[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_bucket[i] = nullptr;
		}
	}
	bool Insert(const T& data)
	{
		KeyOfT kot;

		if (Find(kot(data))) return false;//Find(kv.first)->Find(kot(data))
		Hash hs;
		if (_n == _bucket.size())
		{
			vector<Node*> newBucket(_bucket.size() * 2, nullptr);
			for (size_t i = 0; i < _bucket.size(); i++)
			{
				Node* cur = _bucket[i];
				while (cur)
				{
					Node* next = cur->_next;

					size_t index = hs(kot(cur->_data)) % newBucket.size();//
					cur->_next = newBucket[index];
					newBucket[index] = cur;

					cur = next;
				}
				_bucket[i] = nullptr;
			}
			_bucket.swap(newBucket);
		}
		size_t index = hs(kot(data)) % _bucket.size();
		Node* newnode = new Node(data);
		newnode->_next = _bucket[index];
		_bucket[index] = newnode;
		++_n;
		return true;
	}
	bool Erase(const K& Key)
	{
		KeyOfT kot;
		Hash hs;
		size_t index = hs(kot(Key)) % _bucket.size();
		Node* cur = _bucket[index];
		Node* prev = nullptr;
		while (cur)
		{
			if (kot(cur->_data) == Key)
			{
				//删除的是第一个节点
				if (prev == nullptr)
				{
					_bucket[index] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}
	Node* Find(const K& Key)
	{
		KeyOfT kot;
		if (_bucket.empty()) return nullptr;
		Hash hs;
		size_t index = hs(Key) % _bucket.size();
		Node* cur = _bucket[index];
		while (cur)
		{
			if (kot(cur->_data) == Key)/**/
				return cur;
			else cur = cur->_next;
		}
		return nullptr;
	}
	
private:
	vector<Node*> _bucket;
	size_t _n;
};

三、迭代器的测试

3.1 重命名

迭代器的测试其实就是在 unordered_set 与 ordered_map 中复用 HashBucket 的函数,在两个类中对迭代器进行重命名,注意一定不要错了!

//unordered_set中
typedef typename HashBucket<K, K, SetKeyOfT, Hash>::iterator iterator;
//unordered_map中
typedef typename HashBucket<K, pair<K, V>, MapKeyOfT, Hash>::iterator iterator;

此外,说明一下 typename 在这里的作用:明确指出某个标识符是一个类型,从而避免编译器将其解释为非类型名称。而且,迭代器的重命名要定义在 public 域中。

3.2 unordered_set

    template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& Key)
			{
				return Key;
			}
		};
	public:
		typedef typename HashBucket<K, K, SetKeyOfT, Hash>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		bool insert(const K& Key)
		{
			return _ht.Insert(Key);
		}
	private:
		HashBucket<K, K, SetKeyOfT, Hash> _ht;
	};
	void Test_unordered_set()
	{
		unordered_set<int> s;
		s.insert(31);
		s.insert(23);
		s.insert(19);
		s.insert(6);
		s.insert(22);
		s.insert(37);
		for (auto e : s)
		{
			cout << e << endl;
		}
	}

3.3 unordered_map

	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename HashBucket<K, pair<K, V>, MapKeyOfT, Hash>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		bool insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}
	private:
		HashBucket<K, pair<K, V>, MapKeyOfT, Hash> _ht;
	};
	void Test_unordered_map()
	{
		unordered_map<int, int> m;
		m.insert(make_pair(31, 31));
		m.insert(make_pair(23, 23));
		m.insert(make_pair(19, 19));
		m.insert(make_pair(6, 6));
		m.insert(make_pair(22, 22));
		m.insert(make_pair(37, 37));
		for (auto e : m)
		{
			cout << e.first << ":" << e.second << endl;
		}
		cout << endl;
	}

在 main 函数中进行测试时,可以看到两者都可以跑起来:

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

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

相关文章

基于物联网架构的电子小票服务系统

1.电子小票物联网架构 采用感知层、网络层和应用层的3层物联网体系架构模型&#xff0c;电子小票物联网的架构见图1。 图1 电子小票物联网架构 感知层的小票智能硬件能够取代传统的小票打印机&#xff0c;在不改变商家原有收银系统的前提下&#xff0c;采集收音机待打印的购物…

SDK——如何快速上手一个接口驱动任务(以iic为例)

如何快速上手一个接口驱动任务&#xff1a;&#xff08;这里以iic为例&#xff09; 文章目录 一、注意这里有一些基本概念需要知道&#xff1a;1.关于主从模式的选择(以iic为例)2.关于外设的中断模式&#xff08;intr&#xff09;和轮询模式&#xff08;polled&#xff09;2.1…

【学习心得】回归任务的评估指标决定系数R^2

一、决定系数是什么&#xff1f; scikit-learn库在进行回归任务的时候&#xff0c;进行模型评估时的score()方法&#xff0c;默认采取的是计算的是决定系数&#xff08;Coefficient of Determination&#xff09;&#xff0c;通常表示为得分。这个值衡量了模型预测值与实际观测…

系统思考—问题分析与持续改进

刚刚为一家500强企业完成了《系统思考—问题分析与持续改进》的课程。学员们开始意识到&#xff0c;不能仅仅停留在冰山上层事件去解决问题&#xff0c;而是要深入观察隐藏在背后的趋势变化。学会如何识别系统中的深层次原因&#xff0c;并从全局视角来制定更加有效的改进策略。…

DockerNetwork

Docker Network Docker Network 是 Docker 引擎提供的一种功能&#xff0c;用于管理 Docker 容器之间以及容器与外部网络之间的网络通信。它允许用户定义和配置容器的网络环境&#xff0c;以便容器之间可以相互通信&#xff0c;并与外部网络进行连接。 Docker Network 提供了以…

vulnhub靶场之FunBox-8

一.环境搭建 1.靶场描述 Its a box for beginners and can be pwned in the lunch break. This works better with VirtualBox rather than VMware 2.靶场下载 Funbox: Lunchbreaker ~ VulnHub 3.靶场启动 二.信息收集 1.寻找靶场真实IP地址 nmap -sP 192.168.2.0/24 arp-…

正运动控制器:视觉纠偏和找孔

一、用户主界面CCD参数设置 通过主界面CCD参数设置&#xff0c;学习如何操作计算相机中心与电批中心的偏移量&#xff0c;以及相机标定的功能。 1、相机中心与电批中心的偏移量计算 1.1、在用户主界面点击CCD参数按钮&#xff0c;进入CCD设置界面。 主界面 CCD参数设置界面 1…

Python中tkinter入门编程9

在《Python中tkinter编程入门8-CSDN博客》中提到&#xff0c;tkinter中的Canvas表示画布&#xff0c;可以在画布中显示文字和图片。除了以上功能外&#xff0c;还可以在Canvas中添加对鼠标或键盘的响应。 1 为Canvas添加事件响应 可以通过Canvas的bind()方法添加对鼠标或键盘…

蓝桥杯-班级活动

题目描述 小明的老师准备组织一次班级活动。班上一共有 ( n ) 名&#xff08;( n ) 为偶数&#xff09;同学&#xff0c;老师想把所有的同学进行分组&#xff0c;每两名同学一组。为了公平&#xff0c;老师给每名同学随机分配了一个 ( n ) 以内的正整数作为 id&#xff0c;第 …

UCOSII_STM32F1移植详细过程(一)

UCOSII_STM32F1移植详细过程&#xff08;一&#xff09; 1、概述2、关于C/OS3、移植过程&#xff08;文件描述与提取&#xff09;1.软件工程文件夹描述2.提取工程中有用的文件3.提取ST标准外设库有用的文件2.新建、修改文件 1、概述 该文写针对初学C/OS的朋友&#xff0c;基于…

python数据分析-CO2排放分析

导入所需要的package import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import datetime %matplotlib inline plt.rcParams[font.sans-serif] [KaiTi] #中文 plt.rcParams[axes.unicode_minus] False #负号 数据清洗…

RedHat9 | DNS剖析-建立子域并进行区域委派

一、实验环境 1、委派DNS服务器 域名空间由多个域构成&#xff0c;DNS提供了将域名空间划分为1个或多个区域的方法&#xff0c;这样使得管理更加方便。在域的规模增大后&#xff0c;可以为域添加附加域&#xff0c;上级域为父域&#xff0c;下级域为子域&#xff0c;下列案例…

00Java准备工作

目录 JDK的安装目录 JAVA环境变量的配置 JDK的安装目录 目录名称说明bin该路径下存放了JDK的各种工具命令,javac和java就放在这个目录conf该路径下存放了JDK的相关配置文件include该路径下存放了一些平台特定的头文件jmods该路径下存放了JDK的各种模块legal该路径下存放了JD…

Windows下安装配置深度学习环境

Windows下安装配置深度学习环境 1. 准备工作 1.1 环境准备 操作系统&#xff1a;win10 22H2 GPU&#xff1a;Nvidia GeForce RTX 3060 12G 1.2 安装Nvidia驱动、cuda、cuDNN 下载驱动需要注册并登录英伟达账号。我这里将下面用到的安装包放到了百度网盘&#xff0c;可以关注微信…

【MATLAB】信号的熵

近似熵、样本熵、模糊熵、排列熵|、功率谱熵、奇异谱熵、能量熵、包络熵 代码内容&#xff1a; 获取代码请关注MATLAB科研小白的个人公众号&#xff08;即文章下方二维码&#xff09;&#xff0c;并回复信号的熵本公众号致力于解决找代码难&#xff0c;写代码怵。各位有什么急需…

JavaRedis-主从集群-分片-数据结构-回收处理-缓存问题

一、主从集群 1.主从集群 主从集群读写分离&#xff0c;主能读能写&#xff0c;从只能读&#xff0c;读的数据是同步主的 docker搭建&#xff1a; docker-compose 这里设置网络模式为model&#xff0c;就直接暴露在了宿主机中&#xff0c;就不用映射端口了 不改就是默认的桥…

Java进阶学习笔记30——BigDecimal

BigDecimal&#xff1a; 用于解决浮点型运算的&#xff0c;出现结果失真的问题。 运行结果&#xff1a; package cn.ensource.d4_bigdecimal;import java.math.BigDecimal;public class Test {public static void main(String[] args) {// 目标&#xff1a;了解BigDecimal类do…

蓝桥楼赛第30期-Python-第三天赛题 统计学习数据题解

楼赛 第30期 Python 模块大比拼 统计学习数据 介绍 JSON&#xff08;JavaScript Object Notation, /ˈdʒeɪsən/&#xff09;是一种轻量级的数据交换格式&#xff0c;最初是作为 JavaScript 的子集被发明的&#xff0c;但目前已独立于编程语言之外&#xff0c;成为了通用的…

【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【00】补充

持续学习&持续更新中… 守破离 【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【00】补充 WindowsCMD插件IDEAVsCode MavenvagrantDocker解决MySQL连接慢问题启动&#xff08;自动&#xff09;Docker注意切换到root用户远程访问MySQL MyBatisPlus代码地址参考 WindowsC…

分布式专题

一&#xff1a;分布式事务 1、理论基础 分布式事务主要区分本地事务 什么是本地事务&#xff08;Local Transaction&#xff09;&#xff1f;本地事务也称为数据库事务或传统事务&#xff08;相对于分布式事务而言&#xff09;。尤其对于数据库而言&#xff0c;为了数据安全…