c++进阶--哈希表的实现

news2025/3/26 10:22:05

大家好,今天我们来学习ubordered_set和unordered_map的底层哈希表。

目录

哈希表实现

1. 哈希概念

  1.1 直接定址法

1.2 哈希冲突

1.3 负载因⼦

1.4 将关键字转为整数 

1.5 哈希函数 

下面我们介绍几种哈希函数:1.5.1 除法散列法/除留余数法 

1.5.2 乘法散列法 

1.5.3 全域散列法 

1.6 处理哈希冲突 

1.7 开放地址法

1.7.1 线性探测

1.7.2 二次探测

1.7.3 双重探测(了解)

1.7.4 开放定址法代码实现 

1.8 链地址法

解决冲突的思路

扩容

极端场景

1.8.1 链地址法代码实现 


哈希表实现

1. 哈希概念
哈希(hash)⼜称散列,是⼀种组织数据的⽅式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进⾏快速查找。
  1.1 直接定址法

求映射的方法有很多,最直接的就是直接定址法:

当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在[a,z]的⼩写字⺟,那么我们开⼀个26个数的数组,每个关键字acsii码-a ascii码就是存储位置的下标。

 也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置。这个⽅法我们在计数排序部分已经⽤过了,其次在string章节的下⾯OJ也⽤过了。

387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

 s是由小写字母构成的字符串,所以我们使用一个长度为26的数组,遍历字符串,e - ' a ' 就是字符串中每个字符在数组中的映射位置,字符每出现一次,在该映射位置就增加一次,最后再进行查找。

1.2 哈希冲突
直接定址法的缺点也⾮常明显,当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤。假设我们只有数据范围是[0, 9999]的N个值,我们要映射到⼀个M个空间的数组中(⼀般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这⾥要注意的是h(key)计算出的值必须在[0, M)之间。

 所以在数据范围大时,就要换一种映射方式,

这⾥存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。

1.3 负载因⼦
假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么 ,负载因⼦有些地⽅也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;
1.4 将关键字转为整数 

我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数,这个细节我们后⾯代码实现中再进⾏细节展⽰。下⾯哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。
1.5 哈希函数 

哈希函数就是求映射的方法,

⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却很难做到,但是我们要尽量往这个⽅向去考量设计。
下面我们介绍几种哈希函数:
1.5.1 除法散列法/除留余数法 
1. 除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。
2. 当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 ,那么key %
本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:{63 , 31}看起来没有关联的值,如果M是16,也就是 ,那么计算出的哈希值都是15,因为63的⼆进制后8位是 00111111,31的⼆进制后8位是 00011111。如果是 ,就更明显了,保留的都是10进值的后x位,如:{112, 12312},如果M是100,也就是 ,那么计算出的哈希值都是12。 所以 当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)。
3. 需要说明的是,Java的HashMap采⽤除法散列法时就是2的整数次幂做哈希表的⼤⼩M,这样的话就不⽤取模,⽽可以直接位运算,相对⽽⾔位运算⽐模更⾼效⼀些。但是他不是单纯的去取模,⽐如M是2^16次⽅,本质是取后16位,那么⽤key’ =key>>16,然后把key和key' 异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围内,但是尽量让key所有的位都参与计算,这样映射出的哈希值更均匀⼀些即可。所以我们上⾯建议M取不太接近2的整数次幂的⼀个质数的理论是⼤多数数据结构书籍中写的理论,但是实践中,灵活运⽤,抓住本质。
1.5.2 乘法散列法 

对于除法散列法,对于M的选择是有要求的,下面介绍的乘法散列法则对M没有要求:

1. 乘法散列法对哈希表⼤⼩M没有要求,他的⼤思路第⼀步:⽤关键字 K 乘上常数 A (0<A<1),并抽取出 k*A 的⼩数部分。第⼆步:后再⽤M乘以k*A 的⼩数部分,再向下取整。
2. h ( key ) = floor ( M × (( A × key )%1.0)),其中floor表⽰对表达式进⾏下取整,A∈(0,1),这⾥
最重要的是A的值应该如何设定,Knuth认为A = ( 5^1/2 − 1)/2 = 0.6180339887.... (⻩⾦分割点])⽐较好。
3. 乘法散列法对哈希表⼤⼩M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A*key= 762.6539420558,取⼩数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =669.6366651392,那么h(1234) = 669。
1.5.3 全域散列法 

对于上面两种方法,由于求映射的函数公式是相同的,所以在面对特定的数据时,会产生严重的数据冲突,下面这种全域散列法可以解决这种问题:

1. 如果存在⼀个恶意的对⼿,他针对我们提供的散列函数,特意构造出⼀个发⽣严重冲突的数据集,⽐如,让所有关键字全部落⼊同⼀个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决⽅法⾃然是⻅招拆招,给散列函数增加随机性,攻击者就⽆法找出确定可以导致最坏情况的数据。这种⽅法叫做全域散列。
2. h ab ( key ) = (( a × key + b )% P )%M,P需要选⼀个⾜够⼤的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组。假设P=17,M=6,a = 3, b = 4, 则 h 34 (8) = ((3 × 8 + 4)%17)%6  = 5。
3. 需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查改都固定使⽤这个散列函数,否则每次哈希都是随机选⼀个散列函数,那么插⼊是⼀个散列函数,查找⼜是另⼀个散列函数,就会导致找不到插⼊的key了。
1.6 处理哈希冲突 

介绍了几种方法,可见无论哪种方法都绕不开哈希冲突这个难题,实践中哈希表⼀般还是选择除法散列法作为哈希函数,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。

下面我们来一一学习这两种方法:

1.7 开放地址法
在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于1的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。
1.7.1 线性探测
1. 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。
2. h ( key ) = hash 0 = key % M, hash0位置冲突了,则线性探测公式为:hc ( key , i ) = hashi = ( hash 0 + i ) % M i = {1, 2, 3, ..., M − 1},因为负载因⼦⼩于1, 则最多探测M-1次,⼀定能找到⼀个存储key的位置。
3. 线性探测的⽐较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1,hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积。下⾯的⼆次探测可以⼀定程度改善这个问题。
下⾯演⽰ {19,30,5,36,13,20,21,12} 等这⼀组值映射到M=11的表中。
h(19) = 8 h(30) = 8 h(5) = 5 h(36) = 3 h(13) = 2 h(20) = 9 h(21) = 10, h(12) = 1

 首先根据hash0进行计算,计算结果如上,可见19和30的映射值是相同的,对于线性探测来说,对于映射值相同的数据,则从该映射值开始向后查找,在未插入值的地方进行插入。此时19被插入到了映射值为8的地方,30被插入到映射值为9的地方,那么当20进行插入的时候,由于映射值为9的位置已经被插入了,所以认为20也冲突,也需要向后查找一个空的位置进行插入,以次类推。

但是在相同映射值的数据很多的情况下,会导致数据的堆积。 

1.7.2 二次探测
1. 从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置;
2. h ( key ) = hash 0 = key % M , hash0位置冲突了,则⼆次探测公式为: hc ( key , i ) = hashi = ( hash 0 ± i ^  2 ) % M i = {1, 2, 3, ...,M/2 }
3. ⼆次探测当 hashi = ( hash 0 − i ^ 2 )% M 时,当hashi<0时,需要hashi += M
下⾯演⽰ {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中。

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0 

 二次探测和线性探测的不同点就是,线性探测是从一个方向开始查找位置,到末尾了再从开头查找,而二次探测是从左右两个方向交替进行查找,每次跳过i^2个位置,这样,在相同映射值的数据很多的时候,不会形成像线性探测那样大规模的数据堆积。

1.7.3 双重探测(了解)
1. 第⼀个哈希函数计算出的值发⽣冲突,使⽤第⼆个哈希函数计算出⼀个跟key相关的偏移量值,不断往后探测,直到寻找到下⼀个没有存储数据的位置为⽌。
2. h 1 ( key ) = hash 0 = key % M , hash0位置冲突了,则双重探测公式为:
hc ( key , i ) = hashi = ( hash 0 + i h 2 ( key )) % M i = {1, 2, 3, ..., M }
3. 要求h 2 ( key ) < Mh 2 ( key )和M互为质数,有两种简单的取值⽅法:1、当M为2整数幂时,h2 (key)从[0,M-1]任选⼀个奇数;2、当M为质数时, h 2 ( key ) = key % ( M − 1) + 1
4. 保证h 2 ( key)与M互质是因为根据固定的偏移量所寻址的所有位置将形成⼀个群,若最⼤公约数p = gcd ( M , h 1 ( key)) > 1,那么所能寻址的位置的个数为M / P < M,使得对于⼀个关键字来说⽆法充分利⽤整个散列表。举例来说,若初始探查位置为1,偏移量为3,整个散列表⼤⼩为12,那么所能寻址的位置为{1, 4, 7, 10},寻址个数为12/gcd (12, 3) = 4
1.7.4 开放定址法代码实现 
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

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

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

template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 131;
		}

		return hash;
	}
};

inline unsigned long __stl_next_prime(unsigned long n)
{
	// Note: assumes long is at least 32 bits.
	static const int __stl_num_primes = 28;
	static const unsigned long __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
	};
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	HashTable()
		:_tables(__stl_next_prime(0))
		, _n(0)
	{}

	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
			return false;
		// 负载因子 >= 0.7扩容
		if (_n * 10 / _tables.size() >= 7)
		{
			HashTable<K, V, Hash> newht;
			//newht._tables.resize(_tables.size() * 2);
			newht._tables.resize(__stl_next_prime(_tables.size() + 1));
			for (auto& data : _tables)
			{
				// 旧表的数据映射到新表
				if (data._state == EXIST)
				{
					newht.Insert(data._kv);
				}
			}
			_tables.swap(newht._tables);
		}
		Hash hash;
		size_t hash0 = hash(kv.first) % _tables.size();
		size_t hashi = hash0;
		size_t i = 1;
		int flag = 1;
		while (_tables[hashi]._state == EXIST)
		{
			// 线性探测
			hashi = (hash0 + i) % _tables.size();
			++i;
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}

	HashData<K, V>* Find(const K& key)
	{
		Hash hash;
		size_t hash0 = hash(key) % _tables.size();
		size_t hashi = hash0;
		size_t i = 1;
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._state == EXIST
				&& _tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];
			}
			// 线性探测
			hashi = (hash0 + i) % _tables.size();
			++i;
		}

		return nullptr;
	}

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			return true;
		}
		else
		{
			return false;
		}
	}

private:
	vector<HashData<K, V>> _tables;
	size_t _n;  // 记录数据个数
};
1.8 链地址法

对于开放定址法解决冲突不管使⽤哪种⽅法,占⽤的都是哈希表中的空间,始终存在互相影响的问题。但链地址法不会占用哈希表表内的空间,下面我们来看一下:
解决冲突的思路
开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。
下⾯演⽰ {19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中。

 

h(19) = 8 h(30) = 8 h(5) = 5 h(36) = 3 h(13) = 2 h(20) = 9 h(21) =10, h(12) = 1,h(24) = 2,h(96) = 88

 

扩容
开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;stl中unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。
极端场景
如果极端场景下,某个桶特别⻓怎么办?其实我们可以考虑使⽤全域散列法,这样就不容易被针对了。但是假设不是被针对了,⽤了全域散列法,但是偶然情况下,某个桶很⻓,查找效率很低怎么办?这⾥在Java8的HashMap中当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下,不断扩容,单个桶很⻓的场景还是⽐较少的,下⾯我们实现就不搞这么复杂了,这个解决极端场景的思路,⼤家了解⼀下。
1.8.1 链地址法代码实现 
template<class K>
struct HashFunc {
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
struct HashFunc<string> {
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;
		}
		return hash;
	}
};

template<class K,class V>
struct HashNode {
	pair<K,V> _kv;
	HashNode<K, V>* _next;
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
};
template<class K,class V,class Hash=HashFunc<K>>
class HashTable {
public:
	inline unsigned long __stl_next_prime(unsigned long n)
	{
		// Note: assumes long is at least 32 bits.
		static const int __stl_num_primes = 28;
		static const unsigned long __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
		};
		const unsigned long* first = __stl_prime_list;
		const unsigned long* last = __stl_prime_list + __stl_num_primes;
		const unsigned long* pos = lower_bound(first, last, n);
		return pos == last ? *(last - 1) : *pos;
	}
	typedef HashNode<K, V> Node;
	HashTable()
		:_tables(__stl_next_prime(0))
		,_n(0)
	{}

	~HashTable()
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}

	HashTable(const HashTable& table)
		:_tables(__stl_next_prime(table.size()))
		,_n(0)
	{
		Hash hs;
		for (size_t i = 0; i < table.size(); i++)
		{
			Node* cur = table[i];
			while (cur)
			{					
				Node* newnode = new Node(cur->_kv);
				size_t hashi = hs(cur->_kv.first) % _tables.size();
				newnode->_next = _tables[hashi];
				_tables[hashi] = newnode;
				cur = cur->_next;
			}
		}
	}

	HashTable& operator=(const HashTable& table)
	{
		this->swap(table);
		return *this;
	}

	void swap(HashTable& table)
	{
		swap(_tables, table._tables);
		swap(_n, table._n);
	}

	bool Insert(const pair<K,V>& kv)
	{
		if (Find(kv.first))
			return false;
		Hash hs;
		if (_n == _tables.size())
		{
			vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					size_t hashi = hs(cur->_kv.first) % newtables.size();
					cur->_next = newtables[hashi];
					newtables[hashi] = cur;
					cur = cur->_next;
				}
				_tables[i] = nullptr;
			}
			_tables.swap(newtables);
		}
		size_t hashi = hs(kv.first) % _tables.size();
		Node* newnode = new Node(kv);
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_n;
		return true;
	}

	Node* Find(const K& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				return cur;
			}
			else {
				cur = cur->_next;
			}
		}
		return nullptr;
	}

	bool Erase(const K& key)
	{
		if (Find(key))
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (hs(cur->_kv.first) == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else {
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				else {
					prev = cur;
					cur = cur->_next;
				}
			}
		}
		else {
			return false;
		}
	}
private:
	vector<Node*> _tables;
	size_t _n;
};

今天就讲到这里,对于哈希表是不是有了初步的认识了呢,下一期我们来学习哈希表的封装:ubordered_set和unordered_map的实现,我们下次再见。

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

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

相关文章

颠覆传统:SaaS 品牌如何通过 SEO 策略引爆市场!

SaaS 商业模式提供了令人难以置信的可扩展性和盈利能力——但前提是与正确的营销增长策略相结合。 SaaS 品牌知道&#xff0c;托管基于云的应用程序的成本会随着用户量的增加而降低&#xff0c;因此必须专注于订阅者的快速增长&#xff0c;以保持竞争力并降低成本。 许多 CMO…

特殊行车记录仪DAT视频丢失的恢复方法

行车记录仪是一种常见的车载记录仪&#xff0c;和常见的“小巧玲珑”的行车记录仪不同&#xff0c;一些特种车辆使用的记录仪的外观可以用“笨重”来形容。下边我们来看看特种车载行车记录仪删除文件后的恢复方法。 故障存储: 120GB存储设备/文件系统:exFAT /簇大小:128KB 故…

数据库中不存在该字段

mybatisplus 定义的类中某些字段是数据库里面没有的&#xff0c;我们可用tablefield(existfalse)来注解&#xff0c;演示如下&#xff1a;

吾爱出品,文件分类助手,高效管理您的 PC 资源库

在日常使用电脑的过程中&#xff0c;文件杂乱无章常常让人感到困扰。无论是桌面堆积如山的快捷方式&#xff0c;还是硬盘中混乱的音频、视频、文档等资源&#xff0c;都急需一种高效的整理方法。文件分类助手应运而生&#xff0c;它是一款文件管理工具&#xff0c;能够快速、智…

关于瑞芯微开发工具(RKDevTool)刷机下载Boot失败原因的研究

昨天发了文章《网心云OEC/OEC-turbo刷机问题——刷机教程、救砖方法、技术要点及下载boot失败异常解决尝试》&#xff0c;其中有关于刷机各种问题的一些解决方法。 网心云OEC/OEC-turbo刷机问题——刷机教程、救砖方法、技术要点及下载boot失败异常解决尝试-CSDN博客文章浏览阅…

web爬虫笔记:js逆向案例十一 某数cookie(补环境流程)

web爬虫笔记:js逆向案例十一 某数cookie(补环境流程) 一、获取网页数据请求流程 二、目标网址、cookie生成(逐步分析) 1、目标网址:aHR0cHM6Ly9zdWdoLnN6dS5lZHUuY24vSHRtbC9OZXdzL0NvbHVtbnMvNy9JbmRleC5odG1s 2、快速定位入口方法 1、通过脚本监听、hook_cookie等操作可…

Excel多级联动下拉菜单的自动化设置(使用Python中的openpyxl模块)

1 主要目的 在Excel中&#xff0c;经常会遇到需要制作多级联动下拉菜单的情况&#xff0c;要求单元格内填写的内容只能从指定的多个选项中进行选择&#xff0c;并且需要设置多级目录&#xff0c;其中下级目录的选项内容要根据上级目录的填写内容确定&#xff0c;如下图所示&am…

excalidraw画图工具——背景画布有无格子设置

服啦找了大半天&#xff0c;愣是没找到 toggle grid &#xff1a; 切换格子… Excalidraw的背景格子 只要右键&#xff0c;将这个勾取消就好了&#xff1f;

计算机组成原理———I\O系统精讲<1>

本篇文章主要介绍输入输出系统的发展概况 一.输入输出系统的发展概况 1.早期阶段 该阶段的特点是I/O设备与主存交换信息都必须通过CPU 当时的I/O设备有如下几个特点&#xff1a; &#xff08;1&#xff09;每个I\O设备都必须配有一套独立的逻辑电路与CPU相连&#xff0c;用来…

ENSP学习day9

ACL访问控制列表实验 ACL&#xff08;Access Control List&#xff0c;访问控制列表&#xff09;是一种用于控制用户或系统对资源&#xff08;如文件、文件夹、网络等&#xff09;访问权限的机制。通过ACL&#xff0c;系统管理员可以定义哪些用户或系统可以访问特定资源&#x…

【C++动态规划 数学】1039. 多边形三角剖分的最低得分|2130

本文涉及知识点 C动态规划 数学 LeetCode1039. 多边形三角剖分的最低得分 你有一个凸的 n 边形&#xff0c;其每个顶点都有一个整数值。给定一个整数数组 values &#xff0c;其中 values[i] 是第 i 个顶点的值&#xff08;即 顺时针顺序 &#xff09;。 假设将多边形 剖分 …

5.go切片和map

切片的概念 数组和切片相比较切片的长度是不固定的&#xff0c;可以追加元素&#xff0c;在追加时可能会使切片的容量增大&#xff0c;所以可以将切片理解成 "动态数组"&#xff0c;但是&#xff0c;它不是数组&#xff0c;而是构建在数组基础上的更高级的数据结构。…

【Linux网络-多路转接select】

代码&#xff1a;https://gitee.com/nanyi-c/linux/tree/master/day50 一、I/O多路转接之select 1.初始select 系统提供select函数来实现多路复用输入/输出模型 select系统调用是用来让我们的程序监视多个文件描述符的状态变化的程序会停在select这里等待&#xff0c;直到被…

cmd命令查看电脑的CPU、内存、存储量

目录 获取计算机硬件的相关信息的命令分别的功能结果展示结果说明获取计算机硬件的相关信息的命令 wmic cpu get name wmic memorychip get capacity wmic diskdrive get model,size,mediaType分别的功能 获取计算机中央处理器(CPU)的名称 获取计算机内存(RAM)芯片的容量…

LVS的 NAT 模式实现 3 台RS的轮询访问

使用LVS的 NAT 模式实现 3 台RS的轮询访问 1.配置 RS&#xff08;NAT模式&#xff09;2. 配置 LVS 主机&#xff08;仅主机、NAT模式&#xff09;2.1 配置仅主机网卡&#xff08;192.168.66.150/24 VIP &#xff09;2.2 配置 NAT 网卡&#xff08;192.168.88.6/24 DIP&#xff…

phpcms版AI自动发文插件,自动创作,自动配图,自动发布,支持多种大模型

phpcms版本的AI自动发文插件1.0.0版&#xff0c;支持自动写文章&#xff0c;自动配图&#xff0c;自动发布。目前支持DeepSeek&#xff0c;豆包&#xff0c;通义千问&#xff0c;文心一言&#xff0c;讯飞星火&#xff0c;KIMI&#xff0c;腾讯混元登大模型AI。同时有自定义字段…

C语言判断闰年相关问题

一、简单闰年问题引入 写一个判断年份是否为闰年的程序? 运行结果: 二、闰年问题进阶 使用switch语句根据用户输入的年份和月份,判断该月份有多少天? 第一种写法(判断年份写在switch的case的里面): 运行结果: 第二种解法(先判断闰年): 运行结果: 三、补充 switch中的ca…

数模转换电路(D/A转换器)

将数字信号转换成模拟信号称为数/模转换, 简称D/A&#xff08;Digital to Analog&#xff09;转换&#xff0c;实现 D/A 转换的器件称为D/A转换器&#xff0c;简称 DAC&#xff08;Digital-Analog Converter&#xff09;。 将模拟信号转换成数字信号称为模/数转换, 简称A/D&a…

网络基础-路由器和交换机工作配置

三、路由器和交换机的工作原理配置以及华为体系下的小型网络的搭建 3.1路由基础 3.1.1数据转发 通过链路层交换机和网络层路由器进行数据转发 交换机&#xff08;链路层&#xff09;mac地址表的数据转发路由器&#xff08;网络层&#xff09; ip路由表的数据转发 隔离广播域…

uv包简单使用案例

uv由Charlie Marsh开发&#xff0c;是Astral Tool的一个快速Python包安装器和解析器。它类似于pip和pip-tools&#xff0c;但速度更快。此外&#xff0c;uv还支持虚拟环境管理&#xff0c;替代venv和virtualenv。 参考&#xff1a;https://github.com/astral-sh/uv 安装&#x…