【1++的数据结构】之哈希(一)

news2024/11/17 17:51:07

👍作者主页:进击的1++
🤩 专栏链接:【1++的数据结构】


文章目录

  • 一,什么是哈希?
  • 二,哈希冲突
    • 哈希函数
    • 哈希冲突解决
  • unordered_map与unordered_set

一,什么是哈希?

首先我们要知道的是哈希是一种思想----一 一映射。在以前我们讲过的容器中,查找效率最高的就是二叉平衡搜索树,由于其关键码与存储位置之间没有对应的关系,而是通过多次比较关键码的大小来查找,查找的效率取决于比较次数,查找的时间复杂度可以达到O(logN) 。最理想的查找便是不经过任何的比较,直接能够锁定查找值的位置,因此,如果能够构建一种结构,通过某种函数能够使得关键码与存储位置之间一一映射,便能够快速的找到要查找的值。其实,有点类似与通信中的调制与解调哈。
在这里插入图片描述
在这里插入图片描述
如上图:
我们根据插入元素的关键码,将其通过哈希函数的计算后,便可以得到该元素的存储位置。
其是一一映射的关系。
当我们要查找某元素时,我们便可以根据该元素的关键码,再通过哈希函数的计算后,得出该元素的位置。效率是不是感觉高的起飞!!!!!!
我们将该方法称为哈希方法,将转换函数称为哈希函数,将该结构称为哈希表。
这么牛逼的想法,我怎么现在才知道???难道就仅仅这么简单吗???
在这里插入图片描述
哈哈哈哈哈哈!!!当然不会这么容易。一刚才的图为例,当我们插入44,11这样的值后,我们就会发现其产生了冲突,位置被占用了(我们把这种冲突称为哈希冲突)!!!这该怎么办呢? 我们下一节来进行讲解。

二,哈希冲突

我们把不同关键字通过哈希函数计算得出相同的地址的这种现象称为哈希冲突。
既然元素的存储地址是通过哈希函数的计算得出的,那么哈希冲突当然也与哈希函数的设计有关,好的哈希函数,可以减少哈希冲突的发生。

哈希函数

哈希函数的设计原则:

  1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  2. 哈希函数计算出来的地址能均匀分布在整个空间中
  3. 哈希函数应该比较简单

常见的几种哈希函数:

直接定址法: 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
其优点是简单,均匀;缺点是要提前知道关键字的分布情况。

除留余数法: 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
那为什么最好要是质数呢?
有以下结论:
如果有一个数列s,间隔为1,那么不管模数为几,都是均匀分布的,因为间隔为1是最小单位

如果一个数列s,间隔为模本身,那么在哈希表中的分布仅占有其中的一列,也就是处处冲突

数列的冲突分布间隔为因子大小,同样的随机数列,因子越多,冲突的可能性就越大。
具体验证过程大家可以看下面这篇文章:
除留余数法为什么选择质数取模

平方取中法:
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。

折叠法:
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

随机数法:
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法。

哈希函数设计的越好,冲突就越少,但冲突无法避免。

哈希冲突解决

最常见的两种解决方法:闭散列与开散列。
我们先来说闭散列:
当发生哈希冲突时,若哈希表未满,则将元素放到下一个空位置。
寻找空位置,也有两种方法:线性探测和二次探测。
什么叫线性探测呢?从冲突的位置开始,一次向后找,知道找到下一个空位置。
哈希表中元素的删除:
在这里插入图片描述
如上图所示,我们采用线性探测法来寻找空位置,当我们要删除元素4时,便会出现一个问题,若我们直接将4从表中物理删除后,当再去查找44时,就会出现找不到的情况。因此我们采用伪删除来解决。就是我们给哈希表中的每个位置给一个标记:EMPTY / FILLED / DELETE。这样就可以避免上面的问题了。
为了使冲突尽可能的少,我们哈希表的空间会被插入的元素个数要大。我们将 插入的元素个数/哈希表的大小 之比称为载荷因子。

enum State
	{
		EMPTY,
		DELETE,
		FILL

	};
	
	template<class K, class V>
	struct Hash_Node
	{
		pair<K, V> _kv;
		State _state = EMPTY;

	};

		bool Insert(const pair<K, V>& kv)
		{
			//查找
			if (Find(kv))
			{
				return false;
			}
			//扩容
			if (_size == 0 ||(_size * 10)/ _table.size() >= 7)
			{
				size_t newsize = _table.size() == 0 ? 5 : _table.size() * 2;
				Hash_table<K, V> tmp;
				tmp._table.resize(newsize);
				for (auto& e : _table)
				{
					if (e._state == FILL)
					{
						tmp.Insert(e._kv);
					}
				}
				_table.swap(tmp._table);

			}

			Hash _hash;
			size_t tablei = _hash(kv.first) % _table.size();
			while (_table[tablei]._state == FILL)
			{
				++tablei;
				tablei %= _table.size();
			}
			_table[tablei]._kv = kv;
			_table[tablei]._state = FILL;
			++_size;
			return true;

		}

当超过载荷因子后,要进行扩容,在扩容时我们申请一个容量更大的哈希表,然后将旧表的数据移到新表中,这里我们采用了一个相对较聪明的写法,我们将旧表遍历一遍,调用插入函数进行插入。最后,哈希表的底层是vector,我们再调用vector的交换函数,可以将两个指向不同vector对象的指针进行交换。这样就扩容完成了。

线性探测的缺点:当多个哈希冲突集中在一起,会发生数据堆积,这样在查找时比较的次数就会增多,影响效率。而二次探测就可以避免这样的问题。
二次探测在这里我们就简单了解:
当我们发生冲突时,(设发生冲突的位置为x)我们去找(x+1^2)%表长 这个位置,若还是冲突则找(x-1^2)%表长 这个位置,若仍冲突则找(x+2^2)%表长 这个位置…直到没有冲突。

开散列:
开散列是指:将关键码集合通过哈希函数计算后得出地址,将具有相同地址的关键码集中在一个子集合。每一个子集合称为桶。桶中的元素通过链表链接起来,哈希表中存储的是链表的头结点。
代码如下:

template<class K,class V,class Hash=HashFunc<K>>
	class Hash_Bucket
	{
		typedef Hash_Node<K,V> Node;

	public:
		bool Insert(const pair<K, V>& kv)
		{
			Hash hash;
			//查找
			if (Find(kv))
			{
				return false;
			}
			//扩容
			if (_size == _Bucket.size())//这里有点小问题:当桶的数量等于元素数量时就扩容,这样冲突小。
			{
				if (_size == 0)
				{
					_Bucket.resize(5);
				}
				else
				{
					vector<Node*> tmp;
					tmp.resize(_Bucket.size() * 2);
					size_t i = 0;
					for (i = 0; i < _Bucket.size(); i++)
					{
						Node* cur = _Bucket[i];
						while (cur)
						{
							size_t tmpi = hash(cur->_kv.first) % tmp.size();
							Node* next = cur->_next;
							cur->_next = tmp[tmpi];
							tmp[tmpi]=cur;
							cur = next;
							
						}
						//_Bucket[i] = nullptr;

					}
					_Bucket.swap(tmp);
				}

			}
			//插入

			size_t Bucketi = hash(kv.first) % _Bucket.size();
			Node* cur = new Node(kv);
			cur->_next = _Bucket[Bucketi];
			_Bucket[Bucketi]=cur;
			_size++;
			return true;
		}

		bool Find(const pair<K, V>& kv)
		{
			if (_size == 0)
			{
				return false;
			}

			Hash hash;
			size_t Bucketi = hash(kv.first) % _Bucket.size();
			Node* cur = _Bucket[Bucketi];
			while (cur)
			{
				if (cur->_kv == kv)
				{
					return true;
				}
				else
				{
					cur = cur->_next;
				}
			}
			return false;
		}

		
	private:
		vector<Hash_Node<K, V>*> _Bucket;
		size_t _size=0;

	};

这里我们重点讲一下开散列的扩容!!!
首先就是什么时候扩容比较好,开散列性能最好当然就是一个桶直挂一个元素的时候是最好的,因此当桶的数量等于元素的数量时扩容是比较好的。接着我们讲一下扩容的具体操作。与闭散列不同的是,闭散列是直接再实例化了一个哈希表对象,再进行插入操作,最后交换了两个指向vector的指针。而我们的开散列这样做会比较浪费空间,因为我们的元素存储再一个一个的结点中,因此我们不妨将这些结点再利用起来,让其插入再新的哈希表中,再将指向新旧哈希表的指针进行交换。

我们的哈希表只能存储key为整型的元素,那么如何存储其他元素呢?
我们可以提供一个能够将key转化为整型的仿函数。

template<class K>
	struct HashFunc
	{
		size_t operator() (const K& key)
		{
			return (size_t)key;
		}
	};

	template<>
	struct HashFunc<string>//特化
	{
		size_t operator() (const string& key)
		{
			size_t ret = 0;
			for (auto e : key)
			{
				ret += e;
			}
			return ret;
		}
	};

unordered_map与unordered_set

unordered_map与unordered_set的底层结构为哈希表,其封装过程及其对哈希表的改造与map与set相似这里我们就不过多阐述,我们直接看代码:

改造后的哈希表

template<class T>
	struct Hash_Node
	{
		Hash_Node* _next;
		T _data;

		Hash_Node(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
		
		
	};

	template<class K, class T, class Hash, class KeyOfT>
	class Hash_Bucket;

	template<class K,class T,class Hash,class KeyOfT>
	struct _Iterator
	{
	
		typedef Hash_Node<T> Node;
		typedef _Iterator Self;
		typedef Hash_Bucket<K, T, Hash, KeyOfT> _Ht;
		Node* _node;
		_Ht* _pht;
		Hash hash;
		KeyOfT kot;

		_Iterator(Node* node,_Ht* pht)
			:_node(node)
			,_pht(pht)
		{}

		T& operator*()
		{
			return _node->_data;
		}

		T* operator->()
		{
			return &_node->_data;
		}

		bool operator!=(const Self& s)const
		{
			return _node != s._node;
		}

		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				size_t i = hash(kot(_node->_data)) % _pht->_Bucket.size();
				++i;
				for (; i < _pht->_Bucket.size(); i++)
				{
					if (_pht->_Bucket[i])
					{
						_node = _pht->_Bucket[i];
						break;
					}
				}
				if (i == _pht->_Bucket.size())
				{
					_node = nullptr;
				}


			}
			return *this;
			
		}


	};

	template<class K,class T,class Hash,class KeyOfT>
	class Hash_Bucket
	{
		typedef Hash_Node<T> Node;

		template<class K, class T, class Hash, class KeyOfT>
		friend struct _Iterator;

	public:

		typedef typename _Iterator<K, T, Hash, KeyOfT> iterator;
		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}


		pair<iterator,bool> Insert(const T& data)
		{
			KeyOfT kot;
			Hash hash;
			//查找
			if (Find(kot(data)).second)
			{
				return Find(kot(data));
			}
			//扩容
			if (_size ==_Bucket.size())
			{
					vector<Node*> tmp;
					tmp.resize(__stl_next_prime(_Bucket.size()),nullptr);
					size_t i = 0;
					for (i = 0; i < _Bucket.size(); i++)
					{
						Node* cur = _Bucket[i];
						while (cur)
						{
							size_t tmpi = hash(kot(cur->_data)) % tmp.size();
							Node* next = cur->_next;
							cur->_next = tmp[tmpi];
							tmp[tmpi]=cur;
							cur = next;
							
						}
						//_Bucket[i] = nullptr;

					}
					_Bucket.swap(tmp);
				

			}
			//插入

			size_t Bucketi = hash(kot(data)) % _Bucket.size();
			Node* cur = new Node(data);
			cur->_next = _Bucket[Bucketi];
			_Bucket[Bucketi]=cur;
			_size++;
			return make_pair(iterator(cur,this),true);
		}

		size_t Erase(const K& key)
		{
			Hash hash;
			KeyOfT kot;
			size_t Bucketi = hash(key) % _Bucket.size();
			Node* cur = _Bucket[Bucketi];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					//可能为头结点,也可能为中间结点
					if (kot(_Bucket[Bucketi]->_data) == key)
					{
						_Bucket[Bucketi]=cur->_next;
						delete cur;
						_size--;

							return 1;
					}
					else
					{
						prev->_next=cur->_next;
						delete cur;
						_size--;
						return 1;

					}
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}

			return 0;
		}
			

		pair<iterator,bool> Find(const K& key)
		{
			KeyOfT kot;
			if (_size == 0)
			{
				return make_pair(iterator(nullptr,this),false);
			}

			Hash hash;
			size_t Bucketi = hash(key) % _Bucket.size();
			Node* cur = _Bucket[Bucketi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return make_pair(iterator(cur, this), true);
				}
				else
				{
					cur = cur->_next;
				}
			}
			return make_pair(iterator(nullptr, this), false);
		}

		iterator begin()
		{
			for (size_t i = 0; i < _Bucket.size(); i++)
			{
				if (_Bucket[i])
				{
					return iterator(_Bucket[i], this);
				}
				
			}
			return end();

		}

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


		
	private:
		vector<Hash_Node<T>*> _Bucket;
		size_t _size=0;

	};

封装的unordered_map

template<class K>
	struct HashFunc
	{
		size_t operator() (const K& key)
		{
			return (size_t)key;
		}
	};

	template<>
	struct HashFunc<string>
	{
		size_t operator() (const string& key)
		{
			size_t ret = 0;
			for (auto e : key)
			{
				ret += e;
			}
			return ret;
		}
	};

	template<class K, class V,class Hash= HashFunc<K>>
	class unordered_map
	{
		struct KeyOfM
		{
			const K& operator() (const pair<K, V>& kv)
			{
				return kv.first;
			}

		};
	public:
		typedef typename Hash_Bucket<K, pair<K, V>, Hash, KeyOfM>::iterator iterator;

		pair<iterator,bool> Insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

		size_t Erase(const K& key)
		{
			return _ht.Erase(key);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

	private:
		Hash_Bucket<K, pair<K,V>,Hash, KeyOfM> _ht;
	};

封装的unordered_set

template<class K, class Hash=HashFunc<K>>
	class unordered_set
	{ 
		struct KeyOfS
		{
			const K& operator() (const K& key)
			{
				return key;
			}
		};

	public:
		
		typedef typename Hash_Bucket<K,K,Hash,KeyOfS>::iterator iterator;
		pair<iterator, bool> Insert(const K& key)
		{
			return _ht.Insert(key);
		}

		size_t Erase(const K& key)
		{
			return _ht.Erase(key);
		}

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}


	private:
		Hash_Bucket<K, K, Hash, KeyOfS> _ht;

	};

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

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

相关文章

(3)MyBatis-Plus待开发

常用注解 TableName MyBatis-Plus在确定操作的表时&#xff0c;由BaseMapper的泛型决定即实体类型决定&#xff0c;且默认操作的表名和实体类型的类名一致,如果不一致则会因找不到表报异常 //向表中插入一条数据 Test public void testInsert(){User user new User(null, &…

Android 10.0 禁用adb shell input输入功能

1.前言 在10.0的产品开发中,在进行一些定制开发中,对于一些adb shell功能需要通过属性来控制禁止使用input 等输入功能,比如adb shell input keyevent 响应输入事件等,所以就需要 熟悉adb shell input的输入事件流程,然后来禁用adb shell input的输入事件功能,接下来分…

yolov7添加注意力机制

yolov7结构图 方法:直接在common里改,在相关的后面加上就行 1、接受通道数的注意力机制 1、目的:在三个输出地方添加注意力 yolov7.yaml文件,换成其他模块 注意力链接 2、models下建SE.py 3、common.py下,先找class Conv,再复制一份修改,把模块导进来 4、yolo.…

生成式AI时代的新基础设施

生成式人工智能席卷了科技行业。 2023 年第一季度&#xff0c;随着数亿用户采用 ChatGPT 和 GitHub CoPilot 等应用程序&#xff0c;对新一代 AI 初创公司的投资高达 1.7B 美元。 技术领先的公司正在争先恐后地制定自己的生成式AI策略&#xff0c;许多公司都在努力将应用程序投…

基于Java+SpringBoot+Vue前后端分离校园资产管理设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

45位主播带货破亿,单日热销100w+单,8月榜单有哪些看点?

根据抖音官方数据&#xff0c;截至2021年1月&#xff0c;抖音在全球范围内的日活用户已经超过7亿。 从娱乐到学习&#xff0c;从社交到购物&#xff0c;抖音成为了人们生活中不可或缺的一部分。 那么&#xff0c;8月有哪些主播表现突出&#xff0c;哪些商品在畅销&#xff0c;哪…

【内存管理】C与C++的内存管理异同点

C/C程序内存区域划分 栈又称堆栈&#xff1a;存放非静态局部变量/函数参数/返回值等等&#xff0c;栈是向下增长的。内存映射段&#xff1a;高效的I/O映射方式&#xff0c;用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存&#xff0c;做进程间通信。堆&…

NATAPP使用详细教程(免费隧道内网映射)

NATAPP - https://natapp.cn/tunnel/lists NATAPP 在开发时可能会有将自己开发的机器上的应用提供到公网上进行访问&#xff0c;但是并不想通过注册域名、搭建服务器&#xff1b;由此可以使用natapp&#xff08;内网穿透&#xff09; 购买免费隧道 修改隧道配置 看自己的web…

RTMP流媒体服务器EasyDSS视频点播平台在不关闭防火墙的情况下平稳部署的具体步骤

EasyDSS视频直播点播平台提供了视频转码、点播、直播、推拉流、录像、回放等功能&#xff0c;可应用在AR、VR、无人机推流、虚拟直播、教育培训、远程会议等多样化的场景中。 通常我们在部署EasyDSS时都建议用户关闭防火墙的&#xff0c;但是也有无需关闭防火墙的部署办法&…

4、nginx 配置实例-反向代理

文章目录 4、nginx 配置实例-反向代理4.1 反向代理实例一4.1.1 实验代码 4.3 反向代理实例二4.3.1 实验代码 【尚硅谷】尚硅谷Nginx教程由浅入深 志不强者智不达&#xff1b;言不信者行不果。 4、nginx 配置实例-反向代理 4.1 反向代理实例一 实现效果&#xff1a;使用 nginx…

为什么有的测试员路越走越窄?原因在这里

常常在思考&#xff1a;同样背景&#xff0c;同样学历的人&#xff0c;为什么有的人路越走越宽&#xff0c;而有的人路越走越窄&#xff1f; 不能简单归结于性格和运气&#xff0c;看似偶然实则必然。 不善学习 学习才能使我们内心强壮&#xff0c;充满自信。 然而&#xff…

[machine Learning]强化学习

强化学习和前面提到的几种预测模型都不一样,reinforcement learning更多时候使用在控制一些东西上,在算法的本质上很接近我们曾经学过的DFS求最短路径. 强化学习经常用在一些游戏ai的训练,以及一些比如火星登陆器,月球登陆器等等工程领域,强化学习的内容很简单,本质就是获取状…

C++信息学奥赛1191:流感传染

一开始的代码自己运行测试代码怎么测试都是正确&#xff0c;但是一直提示答案错误 #include <iostream> using namespace std; int main() {int n;cin >> n;char arr[n][n];for (int i 0; i < n; i){for (int j 0; j < n; j){cin >> arr[i][j];}}in…

Netty—EventLoop

文章目录 一、EventLoopGroup 是什么&#xff1f;&#x1f914;️二、NioEventLoop 有哪些重要组成部分&#xff1f;&#x1f50d;三、NioEventLoop 的 thread 在何时启动&#xff1f;三、 run() 方法中线程在干嘛&#xff1f; 一、EventLoopGroup 是什么&#xff1f;&#x1f…

纯源码程序的执行

QT Creator本身是个IDE安装的时候根据自己需要配置的又有对应的编译器&#xff0c;因此编写普通的程序也不再话下。 选择Non-Qt Project工程&#xff0c;并在右侧根据自己的需要选择C应用还是C应用 新工程中工程管理文件和代码如下&#xff1a; 执行结果如下

驱动开发--day2

实现三盏灯的控制&#xff0c;编写应用程序测试 head.h #ifndef __HEAD_H__ #define __HEAD_H__#define LED1_MODER 0X50006000 #define LED1_ODR 0X50006014 #define LED1_RCC 0X50000A28#define LED2_MODER 0X50007000 #define LED2_ODR 0X50007014#endif mychrdev.c #inc…

浅谈数据治理中的智能数据目录

在数字化转型的战略实施中&#xff0c;很多企业都在搭建自己的业务、数据及人工智能的中台。在同这些企业合作和交流中&#xff0c;越来越体会到数据目录是中台建设的核心和基础。为了更好地提供数据服务&#xff0c;发挥数据价值&#xff0c;用户需要先理解数据和信任数据。 企…

c高级day2(9.7)shell脚本

作业: 写一个1.sh脚本&#xff0c;将以下内容放到脚本中&#xff1a; 在家目录下创建目录文件&#xff0c;dir 在dir下创建dir1和dir2 把当前目录下的所有文件拷贝到dir1中&#xff0c; 把当前目录下的所有脚本文件拷贝到dir2中 把dir2打包并压缩为dir2.tar.xz 再把dir2…

基于docker环境的tomcat开启远程调试

背景&#xff1a; Tomcat部署在docker环境中&#xff0c;使用rancher来进行管理&#xff0c;需要对其进行远程调试。 操作步骤&#xff1a; 1.将容器中的catalina.sh映射出来&#xff0c;便于对其修改&#xff0c;添加远程调试相关参数。 注意&#xff1a;/data/produce2201…

【计算机网络】HTTP(下)

本文承接上文的代码进行改造&#xff0c;上文链接&#xff1a;HTTP上 文章目录 1. 实现网站跳转实现 自己的网站跳转 2. 请求方法(get) && 响应方法(post)GET方法POST方法GET与POST的应用场景 3. HTTP状态码在自己设计的代码中发现4043开头的状态码(重定向状态码)永久…