哈希/散列--哈希表[思想到结构]

news2024/12/23 23:34:35

文章目录

  • 1.何为哈希?
    • 1.1百度搜索
    • 1.2自身理解
    • 1.3哈希方法/散列方法
    • 1.4哈希冲突/哈希碰撞
    • 1.5如何解决?
      • 哈希函数的设计
  • 2.闭散列和开散列
    • 2.1闭散列/开放定址法
    • 2.2开散列/链地址法/开链法
      • 1.概念
      • 2.容量问题
  • 3.代码实现[配备详细注释]
    • 3.1闭散列
    • 3.2开散列

在这里插入图片描述

1.何为哈希?

1.1百度搜索

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

1.2自身理解

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应关系
在查找一个元素时,必须要经过key的多次比较。
顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN).

有没有这样一种方法 不经过任何比较 直接从表中得到要查找的元素。

大佬神作: 构造一种存储结构,通过某种函数使元素的存储位置与key之间建立一一映射的关系,在查找时通过该函数找到该元素.

1.3哈希方法/散列方法

插入元素:
将待插入元素的key,以某个函数[哈希函数/散列函数]计算出该元素的存储位置并按此位置存放,构造出一个结构[哈希表/散列表]
搜索元素:
对元素的key进行同样的计算,求得的函数值即为元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
在这里插入图片描述

1.4哈希冲突/哈希碰撞

  • 不同关键码通过哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞.
  • 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

在这里插入图片描述

1.5如何解决?

哈希函数的设计

设计一个合理的哈希函数

哈希函数设计原则:

  1. 简单方便
  2. 哈希函数要使得关键码均分分布
  3. 定义域为所有key 值域为[0, n)

常见哈希函数

  • 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:Hashi(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况 否则导致—数据量小 但是需要的空间极大 例如 :数据-1 2 3 9999下标: 1 2 3 9999
    使用场景:数值小且分布集中
  • 除留余数法–(常用)
    设散列表中允许地址数为n,除数p的取值规则: 小于等于n 接近/等于n的质数
    哈希函数:Hashi(key) = key % p(p <= n)
  • 平方取中法–(了解)
    假设关键码为6392,它的平方为40857664,抽取中间的3位857或576作为hashi;
    使用场景:不知道关键码的分布 位数不是很大
  • 折叠法–(了解)
    将关键码从左到右分割成位数近似相等的几部分 将这几部分叠加求和
    按散列表表长 取后几位作为hashi
    使用场景:无所谓关键码的分布 位数较大
  • 随机数法–(了解)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即Hashi(key) = random(key)
    使用场景:关键字长度不等
  • 数学分析法–(了解)
    假设关键字为某一地区的手机号,大部分前几位都相同的 取后面的四位作为hashi
    还出现冲突–对抽取数字进行反转(如1234改成4321)、右环移位(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等.
    使用场景:关键字位数比较大 事先知道关键字的分布 关键字的若干位分布较均匀
    == 注意:哈希冲突只可缓解 不可避免 ==

2.闭散列和开散列

2.1闭散列/开放定址法

当发生哈希冲突时,如果哈希表未被装满,把key存放到冲突位置的==“下一个” ==空位置
寻找空位置:

  1. 线性探测
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置
    在这里插入图片描述

插入:

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 该位置没有元素直接插入新元素
  • 该位置中有元素 使用线性探测找到下一个空位置 插入新元素
    在这里插入图片描述

删除:

  • 线性探测采用标记的伪删除法来删除一个元素
  • 若直接删除 会影响其他元素的搜索
    例如上图: 删除对象是33 直接删除 hash6 这个位置应该怎么办? 需要搞一个东西让哈希表的使用者知道这里原来有元素 现在被删除了 置成null? 如果置成空 当想要查找 1002/43即33后面的元素时 遇到hash6 使用者被告知这里是空 停止查找 此时使用者得到的信息是 哈希表中并不存在此元素 这与现实违背 那怎么办? 答案是置成"删除"状态 使得使用者知道何为空何为删除 这就是伪标记删除法

负载因子

哈希表的负载因子定义为: α = 表中的元素个数[ _n ] / 哈希表长[ _tables.size() ]
负载因子a: 哈希表装满程度的标志因子
表长是定值,α与_n成正比
α越大 填入表中的元素越多 哈希冲突可能性越大
α越小 填入表中的元素越少 哈希冲突可能性越小
哈希表的平均查找长度是负载因子α的函数 处理冲突方法不同函数不同
负载因子越大,冲突的概率越高,查找效率越低,空间利用率越高
负载因子越小,冲突的概率越低,查找效率越高,空间利用率越低

容量问题: 1.size是实际能够访问数据的范围 2.capacity是存储数据的空间大小

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

优点:

实现简单

缺点;

x占据y的位置 y就得放到y+1的位置
冲突累计 产生数据堆积
本意是要减缓哈希冲突
虽然使得有相同hashi的不同数据有位置存放
但是数据堆积时 会使得寻找某关键码的位置需要许多次比较
导致搜索效率降低。

  1. 二次探测
    线性探测寻找空位置的方法[逐个后移]导致线性探测的缺陷[产生冲突的数据堆积]
    修改探测的方法:
    在这里插入图片描述

2.2开散列/链地址法/开链法

1.概念

对关键码用哈希函数计算哈希地址,具有相同地址的不同关键码归于同一子集合,每一个子集合称为一个各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

2.容量问题

桶的个数有限[即哈希表的表长有限]
随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容
某种情况下 每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,我们假定在元素个数刚好等于桶的个数时,进行扩容 即在这里插入图片描述
需要了解到是 我们最然控制α为1时进行扩容 并不代表此时哈希桶都挂了一个结点 更为普遍的情况是 一些桶为空 一些桶有许多结点 只不过结点总个数为哈希表长度大小

在这里插入图片描述

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

3.代码实现[配备详细注释]

3.1闭散列

//闭散列/开放地址法
namespace OpenAddressing
{
	//状态枚举
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	//哈希数据元素
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _pair;
		State _state = EMPTY;
	};

	//哈希表
	template<class K, class V>
	class HashTable
	{
	public:
		//插入函数
		bool Insert(const pair<K, V>& pair)
		{
			//值已存在 插入错误
			if (Find(pair.first))
				return false;

			//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();

			//(double)_n / (double)_tables.size() >= 0.7
			//_n * 10 / _tables.size() >= 7

			//使得扩容发生的条件: size为0 负载因子达到阈值
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				/  低级代码  /
				/*
				//先更新size 由size作为参数扩容 解决只改容量 不更新访问范围的问题
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;

				//调用vector的有参构造[有参构造里调用reserve] 创建一个新表
				vector<HashData<K,V>> newtables(newsize);

				//遍历旧表  由哈希函数更新数据位置
				for (auto& e :  _tables)
				{
					if (e._state == EXIST)
					{
						//哈希函数计算出的下标
						size_t hashi = pair.first % newtables.size();

						//重新线性探测
						size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
						for (size_t i = 1; newtables[index]._state == EXIST; ++i)
						{
							index = hashi + i;        //从原始下标不断 +1 +2 +3 ...
							index %= newtables.size();//防止越界 只在表内定位index
						}

						//将数据放入合适位置
						newtables[index]._pair = e._pair;
						newtables[index]._state = EXIST;
					}
				}

				//新表的数据才是我们想要的数据 交换后 newtables中存放的变为旧数据
				//newtables是个局部变量 让其"自生自灭"
				_tables.swap(newtables);
				*/

				//  高级代码 //
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> other;
				other._tables.resize(newsize);

				for (auto& e : _tables)
				{
					if (e._state == EXIST)
						other.Insert(e._pair);
				}

				_tables.swap(other._tables);
				/

				//以上高级代码实际是对下面的线性探测进行了复用
			}

			//哈希函数计算出的下标
			size_t hashi = pair.first % _tables.size();

			// 线性探测
			size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
			for (size_t i = 1; _tables[index]._state == EXIST; ++i)
			{
				index = hashi + i;      //从原始下标不断 +1 +2 +3 ...
				index %= _tables.size();//防止越界 只在表内定位index
			}
			//将数据放入合适位置
			_tables[index]._pair = pair;
			_tables[index]._state = EXIST;
			_n++; //数据个数++

			return true;
		}

		//查找函数
		HashData<K, V>* Find(const K& key)
		{
			//哈希表为空 返回nullptr
			if (_tables.size() == 0)
				return nullptr;

			//哈希函数计算出的下标
			size_t hashi = key % _tables.size();

			// 线性探测
			size_t index = hashi;
			for (size_t i = 1; _tables[index]._state != EMPTY; ++i)
			{
				//obj是key的前提是obj存在
				if (_tables[index]._state == EXIST
					&& _tables[index]._pair.first == key)
				{
					return &_tables[index];
				}

				index = hashi + i;
				index %= _tables.size();

				//当表中元素状态非exist即delete时 
				//for循环判断条件一直为真 死循环
				//解决: 当找了一圈还未找到
				//表中无此key 返回false
				if (index == hashi)
					break;
			}
			return nullptr;
		}

		//删除函数
		bool Erase(const K& key)
		{
			HashData<K, V>* pos = Find(key);
			if (pos)
			{
				pos->_state = DELETE;
				--_n; //虽然已标记删除 仍然要使数据个数减减 防止有用数据未达到阈值就执行扩容
				return true;
			}
			else
				return false;
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;//存储的数据个数
	};
	//  测试函数  ///
	void TestHashTable()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		HashTable<int, int> ht;
		//插入
		for (auto& e : a)
			ht.Insert(make_pair(e, e));
		//插入第8个数据 达到阈值  测试扩容
		ht.Insert(make_pair(15, 15));

		//查找 + 删除
		int tmp = 12;
		if (ht.Find(tmp))
			cout << tmp << "在" << endl;
		else
			cout << tmp << "不在" << endl;

		ht.Erase(tmp);

		if (ht.Find(tmp))
			cout << tmp << "在" << endl;
		else
			cout << tmp << "不在" << endl;
	}
}

3.2开散列

//开散列/链地址法
namespace ChainAddressing
{
	//结点类
	template<class K, class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _pair;

		HashNode(const pair<K, V>& pair)
			:_next(nullptr)
			, _pair(pair)
		{
		
		}
	};

	//哈希表类
	template<class K, class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		//析构函数
		~HashTable()
		{
			for (auto& ptr : _tables)
			{
				while (ptr)
				{
					//记录下一个结点
					Node* next = ptr->_next;
					//释放当前结点
					delete ptr;
					//更新ptr
					ptr = next;
				}

				ptr = nullptr;
			}
		}

		//查找函数
		Node* Find(const K& key)
		{
			//为空不查找 返回nullptr
			if (_tables.size() == 0)
				return nullptr;

			//哈希函数计算的下标
			size_t hashi = key % _tables.size();
			//首先得到表里的指针 即相当于每一个桶的头指针
			//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
			Node* ptr = _tables[hashi];

			while (ptr)
			{
				if (ptr->_pair.first == key)
					return ptr;
				ptr = ptr->_next;
			}

			return nullptr;
		}

		//删除函数
		bool Erase(const K& key)
		{
			//哈希函数计算的下标
			size_t hashi = key % _tables.size();
			//首先得到表里的指针 即相当于每一个桶的头指针
			//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
			Node* ptr = _tables[hashi];
			Node* prv = nullptr;

			while (ptr)
			{
				//当前值为目标值 执行删除操作
				if (ptr->_pair.first == key)
				{
					if (prv == nullptr)
						_tables[hashi] = ptr->_next;
					else
						prv->_next = ptr->_next;

					delete ptr;
					return true;
				}
				//当前值不为目标值 继续向下遍历
				else
				{
					prv = ptr;
					ptr = ptr->_next;
				}
			}
			return false;
		}

		//插入函数
		bool Insert(const pair<K, V>& pair)
		{
			//表中已有 返回false
			if (Find(pair.first))
				return false;

			//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();
			//负载因子 == 1时扩容
			if (_n == _tables.size())
			{
				///  高级代码1.0  /
				/*
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newht;
				newht.resize(newsize);
				for (auto& ptr : _tables)
				{
					while (ptr) 
					{
						newht.Insert(ptr->_pair);
						ptr = ptr->_next;
					}
				}

				_tables.swap(newht._tables);
				*/

				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newtables(newsize, nullptr);
				//遍历旧表 取出旧表里每一个指针
				for (auto& ptr : _tables)
				{
					//ptr是旧表里存储的每一个指针
					//它指向当前哈希桶的首结点 即他存储的是首结点的地址
					while (ptr)
					{
						//算出 当前结点根据新哈希函数计算的新下标
						size_t hashi = ptr->_pair.first % newtables.size();

						//ptr是首结点的地址 ptr->_next为第二个结点的地址
						Node* next = ptr->_next;
						
						// 头插到新表
						ptr->_next = newtables[hashi];
						newtables[hashi] = ptr;
						
						//更新ptr 即向下遍历
						ptr = next;
					}
				}

				_tables.swap(newtables);
			}

			//哈希函数计算出的下标
			size_t hashi = pair.first % _tables.size();
			//链表头插
			Node* newnode = new Node(pair);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;        
			++_n;

			return true;
		}

	private:
		vector<Node*> _tables; // 指针数组
		size_t _n = 0;         // 存储有效数据个数
	};
  测试函数  //
	void TestHashTable1()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		HashTable<int, int> ht;
		for (auto& e : a)
		{ 
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(15, 15));
		ht.Insert(make_pair(25, 25));
		ht.Insert(make_pair(35, 35));
		ht.Insert(make_pair(45, 45));
	}

	void TestHashTable2()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		HashTable<int, int> ht;
		for (auto& e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Erase(12);
		ht.Erase(3);
		ht.Erase(33);
	}
}

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

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

相关文章

【工具】idea 设置自动渲染注释

前言 需求&#xff1a;自动渲染文档注释&#xff0c;看源码更加舒服。 已知 crtl alt Q 可以 设置 尝试搜索 render&#xff0c;发现有启用 “渲染文档注释” 的地方 坐标 &#xff1a; Settings -> Editor-> Appearance

CSS3与HTML5

box-sizing content-box&#xff1a;默认&#xff0c;宽高包不含边框和内边距 border-box&#xff1a;也叫怪异盒子&#xff0c;宽高包含边框和内边距 动画&#xff1a;移动translate&#xff0c;旋转、transform等等 走马灯&#xff1a;利用动画实现animation&#xff1a;from…

分布式锁:jvm本地加锁解决商品超卖的方案

一 分布式锁 1.1 分布式锁的作用 在多线程高并发场景下&#xff0c;为了保证资源的线程安全问题&#xff0c;jdk为我们提供了synchronized关键字和ReentrantLock可重入锁&#xff0c;但是它们只能保证一个工程内的线程安全。在分布式集群、微服务、云原生横行的当下&#xff…

python二次开发CATIA:根据已知数据点创建曲线

已知数据点存于Coords.txt文件如下&#xff1a; 8.67155477658819,20.4471021292557,0 41.2016126836927,20.4471021292557,0 15.9568941320569,-2.93388599177698,0 42.2181532110364,-6.15301746150354,0 43.0652906622083,-26.4843096139083,0 -31.6617679595947,-131.1513…

Java基本数据类型和变量

目录 一、基本数据类型 1.1 整型 1.1.1 byte 1.1.2 short 1.1.3 int 1.1.4 long 1.2 浮点型 1.2.1 float 1.2.2 double 1.3 字符型 1.4 布尔型 二、变量 2.1 变量的概念 2.2 语法格式 2.3 整型变量 2.3.1 整型变量 2.3.2 长整型变量 2.3.3 短整型变量 2.3.…

【Unity2022】Unity实现在两个物体之间连出一条线

文章目录 Line Renderer组件添加Line Renderer组件重要属性Positions&#xff08;位置&#xff09;Width &#xff08;宽度&#xff09;Material&#xff08;材质&#xff09;其他属性 使用脚本绘制直线绳子运行结果其他文章 Line Renderer组件 我们可以使用LineRenderer组件来…

【GO 编程语言】面向对象

指针与结构体 文章目录 指针与结构体一、OOP 思想二、继承三、方法 一、OOP 思想 Go语言不是面向对象的语言&#xff0c;这里只是通过一些方法来模拟面向对象&#xff0c;从而更好的来理解面向对象思想 面向过程的思维模式 1.面向过程的思维模式是简单的线性思维&#xff0c;…

苹果电脑壁纸软件Irvue for mac激活

Irvue是一款Mac上的壁纸软件&#xff0c;里面包含了数千张来精彩照片&#xff0c;方便用户将喜欢的照片设置为壁纸。以下是Irvue软件的一些主要特点和功能&#xff1a; 丰富的壁纸资源&#xff1a;Irvue提供了数千张来自Unsplash的高分辨率照片&#xff0c;涵盖了风景、建筑、…

【前段基础入门之】=>元素定位布局

导语&#xff1a; CSS 元素定位&#xff0c;是目前 CSS 页面布局的一种主要方式。 文章目录 相对定位开启相对定位相对定位的参考点相对定位的特点 绝对定位开启绝对定位绝对定位的参考点绝对定位的特点 固定定位开启固定定位固定定位的参考点固定位的特点 粘性定位开启粘性定位…

详解C语言—编译与链接

目录 1、程序的翻译环境 2、C语言程序的编译链接 3、程序执行的过程&#xff1a; 1、程序的翻译环境 在ANSI C标准的任何一种实现中&#xff0c;存在两个不同的环境。 第1种是翻译环境&#xff0c;在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境&#xff0…

基于SSM的电动车上牌管理系统(有报告)。Javaee项目。

演示视频&#xff1a; 基于SSM的电动车上牌管理系统&#xff08;有报告&#xff09;。Javaee项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringM…

【3】c++设计模式——>UML表示类之间的关联关系

关联关系 关联&#xff08;Assocition&#xff09;关系是类与类之间最常见的一种关系&#xff0c;它是一种结构化的关系&#xff0c;表示一个对象与另一个对象之间有联系&#xff0c;如汽车和轮胎、师傅和徒弟、班级和学生等。在UML类图中&#xff0c;用&#xff08;带接头或不…

JVM篇---第一篇

系列文章目录 文章目录 系列文章目录一、知识点汇总二、知识点详解:三、说说类加载与卸载一、知识点汇总 JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高. 其中内存模型,类加载机制,GC是重点方面.性能调优部分更偏向应用,重点突出实践…

专业图标制作软件 Image2icon 最新中文 for mac

Image2Icon是一款用于Mac操作系统的图标转换工具。它允许用户将常见的图像文件&#xff08;如PNG、JPEG、GIF等&#xff09;转换为图标文件&#xff08;.ico格式&#xff09;&#xff0c;以便在Mac上用作应用程序、文件夹或驱动器的自定义图标。 以下是Image2Icon的一些主要功…

基于vc6+sdk51开发简易文字识别转语音的程序

系统&#xff1a;window7 软件&#xff1a;vc6.0 目的&#xff1a;简易文字转语音真人发声 利用2023国庆小长假&#xff0c;研究如何将文言转语音&#xff0c;之前在网上查询相关知识&#xff0c;大致了解微信语音转换&#xff0c;翻译官之类软件的原理&#xff0c;但要加入神…

python二次开发CATIA:旋转楼梯

旋转楼梯&#xff0c;也称为螺旋形或螺旋式楼梯&#xff0c;是一种围绕单柱或中心轴旋转而上的楼梯类型。由于其流线造型美观、典雅&#xff0c;并且能够节省空间&#xff0c;因此受到很多人的喜爱。这种楼梯最早可以追溯到公元前1000年左右&#xff0c;当时在所罗门王的宫殿中…

【4】c++设计模式——>UML表示类之间的聚合关系

聚合关系表示整体与部分的关系&#xff0c;在聚合关系中&#xff0c;成员对象时整体的一部分&#xff0c;但是成员对象可以脱离整体对象独立存在&#xff0c;当整体被析构销毁的时候&#xff0c;组成整体的这些子对象是不会被销毁的&#xff0c;是可以继续存活&#xff0c;并在…

Matlab论文插图绘制模板第117期—气泡云图

之前的文章中&#xff0c;分享了Matlab气泡图的绘制模板&#xff1a; 进一步&#xff0c;分享一种特殊的气泡图&#xff1a;气泡云图&#xff0c;先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自行下载。有…

【STL】用哈希表(桶)封装出unordered_set和unordered_map

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C进阶 ⭐代码仓库&#xff1a;C进阶 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们的支持是我…

如何在企业网站里做好网络安全

在当今数字时代&#xff0c;网站不仅仅是企业宣传和产品展示的平台&#xff0c;更是日常生活和商业活动中不可或缺的一部分。然而&#xff0c;随着网络技术不断发展&#xff0c;网站的安全问题日益凸显。保护网站和用户数据的安全已经成为至关重要的任务&#xff0c;以下是一些…