【C++指南】哈希驱动的封装:如何让unordered

06-01 1279阅读

【C++指南】哈希驱动的封装:如何让unordered

🌟 各位看官好,我是egoist2023!

🌍 种一棵树最好是十年前,其次是现在!

💬 注意:本文在哈希函数中主讲除法散列法,乘法散列法、全域散列法、双重散列等自行了解。

🚀 今天来学习哈希表的相关知识,为之后unordered_map/set的封装打下基础。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

引入 :直接定址法

在现实生活中,我们往往会将一类东西跟另一种东西进行绑定,且这种 关系具有一定的联系 。 在计算机当中也是必然,如“left”的中文意思是“左边”,“string”的中文意思是“字符串”等等。而对于每个数字都有对应存储的下标。 当 关键字的范围⽐较集中 时,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。 但是如果 一组关键字比较分散 ,如只出现了1、20、99时,此时要开100空间的数组有97个空间会被浪费,这显然不是我们期望的。因此,关于一段 哈希的故事就此展开 。

哈希

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

哈希函数

⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却很难做到。因此我们要尽量往这个⽅向去考量设计。

除法散列法(除留余数法)

当数据比较分散的情况下,用直接定址法是无法很好地处理问题的,那是否能仅用较小地空间让保证所有的值都映射到该空间上来呢(保证空间大于值数量)?于是有人提出了除法散列法的概念并对此进行了说明。

除法散列法也叫做除留余数法,假设哈希表的大小为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。(这样即能保证所有的值都在这个空间上)

哈希冲突和负载因子

当使⽤除法散列法时,要 尽量避免M为某些值 ,如2的幂,10的幂等。 如果是 ,那么key %2 ^X 本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:{63 , 31}看起来没有关联的值,如果M是16,即2^4,保留后4位,因为63的⼆进制后8位是 00111111,31的⼆进制后8位是 00011111,后四位都是相同的,那么都会映射到同一个空间上去,这样就产生了冲突,即哈希冲突。 因此 当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数) 。 负载因子: 假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,M一定要大于,那么负载因子 = N/M,保证负载因子小于1。 负载因⼦越⼤,说明M是接近于N的,则空间利⽤率越⾼,相对地哈希冲突的概率越⾼; 负载因⼦越⼩,说明M的空间很大,则空间利用率低,相对地哈希冲突的概率越低。

处理哈希冲突

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表 ⽆论选择什么哈希函数也避免不了冲突 ,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。
开放定址法
在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的。这⾥的规则有三种: 线性探测、⼆次探测、双重探测(自行了解) 。(哈希表有三种状态表示: 存在、空、删除 ) 开放定址法的哈希表结构
enum Status
{
	EMPTY,
	EXIST,
	DELETE
};
template
struct HashData
{
	pair _kv;
	Status _status = EMPTY;
};
template
class HashTable
{
public:
    HashTable(size_t size = 11)
		:_tables(size)
		, _n(0)
	{}
    //...
private:
	vector _tables;
	size_t _n;
};
优化

但是上面的实现并不是那么好,如果当映射的元素_n/_tables.size()大于负载因子时,显然是需要扩容的,如果选择2倍扩容,原本的11空间变为22空间,看似没有毛病。但前面我们说过,使用除法散列法时,要尽量避免M为某些值,即取不太接近2的整数次幂的⼀个质数。因此,不能直接地选择2倍扩容地方式来放大空间。那代码又该如何实现呢?

  1. 提前造好一些数据,这些数据得保证是质数,且是不太接近2的整数次幂的⼀个质数;
  2. 每次扩容的时候都往我们造好的数据上进行扩容。
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
};
inline unsigned long __stl_next_prime(unsigned long n)
{
	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;
}
enum Status
{
	EMPTY,
	EXIST,
	DELETE
};
template
struct HashData
{
	pair _kv;
	Status _status = EMPTY;
};
template
class HashTable
{
public:
	HashTable(size_t size = __stl_num_primes)
		:_tables(size)
		, _n(0)
	{}
    //...
private:
	vector _tables;
	size_t _n;
};

在上面这段程序中,哈希表默认初始空间是28,当大于负载因子时进行扩容,第一次扩容是53,第二次是97…… ,可以发现,每次扩容都是以近乎2倍扩容且满足不太接近2的整数次幂的⼀个质数。这种手动造数据的方法看似笨拙,实则这难道不是利用其特性下的一种取巧吗?

线性探测
  • 在映射数据的时候可能会存在哈希冲突,此时从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。
    • h(key) = hash0 = key % M,hash0位置冲突了,则线性探测公式为:hc(key,i) = hashi = (hash0 + i) % Mi = {1, 2, 3, ..., M − 1},保证线性探测时能从队尾走到队头,且因为负载因⼦小于1,则最多探测M-1次,⼀定能找到⼀个存储key的位置。

      【C++指南】哈希驱动的封装:如何让unordered

      • 可以发现线性探测的问题会占用其他值可能映射到的空间,会导致原本不冲突的值产生哈希冲突。严重的话肯呢个会使多个hash0,hash1,hash2,hash3的值都争夺hash3位置,这种现象叫做群集/堆积。
        二次探测(了解)
        存在哈希冲突是必然的,但有什么比较好的方法可以减少哈希冲突的现象呢?
        • 从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为止,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置;
          • h(key) = hash0 = key % M , hash0位置冲突了,则⼆次探测公式为:hc(key,i) = hashi = (hash0 ± i^2 ) % Mi = {1, 2, 3, ...,M/2 };
          • ⼆次探测当 hashi = (hash0 − i 2 )%M 时,当hashi 线性探测 while (_tables[hashi]._status == EXIST) { hashi = (hashi + i) % _tables.size(); i++; } _tables[hash0]._kv = kv; _tables[hash0]._status = EXIST; ++_n; return true; }
            扩容

            方案一:新建一个哈希表,遍历旧表让里面的数据重新映射到新表当中;

            方案二:采用复用的手段将旧表数据映射到新表中。

            if ((double)_n / (double)_tables.size() > 0.7)
            {
            	HashTable newHT(__stl_next_prime(_tables.size() + 1));
            	for (size_t i = 0;i  
             
            查找和删除

            查找规律:巧妙利用线性探测的特性

            【C++指南】哈希驱动的封装:如何让unordered

            删除规律:采用Find函数寻找该值是否存在,如果存在标记为del,返回true;否则,返回false。

            	HashData* Find(const K& key)
            	{
            		size_t hash0 = key % _tables.size();
            		size_t hash1 = hash0;
            		size_t i = 1;
            		while (_tables[hash1]._status != EMPTY)
            		{
            			if (_tables[hash1]._kv.first == key && _tables[hash1]._status != DELETE)
            			{
            				return &_tables[hash1];
            			}
            			hash1 = (hash1 + i) % _tables.size();
            			++i;
            		}
            		return nullptr;
            	}
            	bool Erase(const K& key)
            	{
            		HashData* ret = Find(key);
            		if (ret)
            		{
            			ret->_status = DELETE;
            			return true;
            		}
            		else
            		{
            			return false;
            		}
            	}
            代码实现
            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
            };
            inline unsigned long __stl_next_prime(unsigned long n)
            {
            	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;
            }
            enum Status
            {
            	EMPTY,
            	EXIST,
            	DELETE
            };
            template
            struct HashData
            {
            	pair _kv;
            	Status _status = EMPTY;
            };
            template
            struct HashFunc
            {
            	size_t operator()(const K& key)
            	{
            		return (size_t)key;
            	}
            };
            template
            class HashTable
            {
            public:
            	HashTable(size_t size = __stl_num_primes)
            		:_tables(size)
            		, _n(0)
            	{}
            	bool Insert(const pair& kv)
            	{
            		//扩容 --> 负载因子大于0.7
            		if ((double)_n / (double)_tables.size() > 0.7)
            		{
            			HashTable newHT(__stl_next_prime(_tables.size() + 1));
            			for (size_t i = 0;i  线性探测
            		while (_tables[hashi]._status == EXIST)
            		{
            			hashi = (hashi + i) % _tables.size();
            			i++;
            		}
            		_tables[hash0]._kv = kv;
            		_tables[hash0]._status = EXIST;
            		++_n;
            		return true;
            	}
            	HashData* Find(const K& key)
            	{
            		size_t hash0 = key % _tables.size();
            		size_t hash1 = hash0;
            		size_t i = 1;
            		while (_tables[hash1]._status != EMPTY)
            		{
            			if (_tables[hash1]._kv.first == key && _tables[hash1]._status != DELETE)
            			{
            				return &_tables[hash1];
            			}
            			hash1 = (hash1 + i) % _tables.size();
            			++i;
            		}
            		return nullptr;
            	}
            	bool Erase(const K& key)
            	{
            		HashData* ret = Find(key);
            		if (ret)
            		{
            			ret->_status = DELETE;
            			return true;
            		}
            		else
            		{
            			return false;
            		}
            	}
            private:
            	vector _tables;
            	size_t _n;
            };
            链地址法
            开放定址法无论怎样势必会造成哈希冲突,因其 占⽤的 都是哈希表中的空间,始终存在互相影响问题。那如何解决这种弊端呢? 链地址法为了解决这种冲突,不是再将所有的数据直接存储在哈希表中,而是通过指针的方式让每一个值都映射到对应空间,即使产生冲突会用指针的方式将它们悬挂起来,挂在哈希表该位置下面,当然我们形象地称这种方法为拉链法或哈希桶。 【C++指南】哈希驱动的封装:如何让unordered

            但是在一些极端场景下,如有人恶意造出一段数据让它都映射到同一个位置,导致某个桶特别长,查询时间复杂度达到了O(N)。有大佬提出,如果这个桶的长度超过⼀定阀值(8)时就把链表转换成红⿊树。(用全域散列法也可)

            链地址法的结构实现

                //手动造数据见上面
            	template
            	struct HashNode
            	{
            		pair _kv;
            		HashNode* _next;
            		HashNode(const pair& kv)
            			:_next(nullptr)
            			,_kv(kv)
            		{}
            	};
            	template
            	class HashTable
            	{
            		typedef HashNode Node;
            	public:
            		HashTable(size_t size = __stl_next_prime(0))
            			:_tables(size, nullptr)
            			,_n(0)
            		{}
            	private:
            		vector _tables;
            		size_t _n;
            	};
            特殊情况:插入元素不是数字

            如果插入元素是浮点数、负数情况呢?

            仿函数的作用再次体现出来了,无论是整数、浮点数、负数,统一强转成正数来处理,找到该映射的位置,对查找、删除等操作都不会受到影响,因为我们同样需要将目标值强制转成正数映射到对应位置。

            	//仿函数
            	template
            	struct HashFunc
            	{
            		size_t operator()(const K& key)
            		{
            			return (size_t)key; // --> 针对浮点、负数等情况
            		}
            	};

            针对字符串

            但是字符串的处理,并不能直接强转成正数来处理,这并不被允许。因此,在这里采用特化的思想进行特殊处理,实现如下。

            	//针对字符串
            	template
            	struct HashFunc
            	{
            		size_t operator()(const string& key)
            		{
            			size_t hash0 = 0;
            			for (auto& ch : key)
            			{
            				//对不同字符串但hash0相同的处理,减少冲突
            				hash0 *= 131;
            				hash0 += ch;
            			}
            			return hash0;
            		}
            	};
            改动
            template
            
            扩容
            开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了。 在stl中 的实现 最⼤负载因⼦基本控制在1,⼤于1就扩容,我们同样采用此逻辑进行扩容。 扩容操作时,开一个新的哈希表,需要将旧表上的值都重新映射到新表中。
            1. 方法一:由于是链地址法的实现,一个位置下可能会挂着多个值(即哈希桶),那么就需要遍历该位置中桶的每个结点,将每个结点的值重新映射到新表中,而每次操作都需要申请新结点,并把旧表中该结点释放掉。显然,这种方式是非常浪费的,且效率非常低效。
            2. 方法二:正是由于采用的是链地址法的实现,由于哈希表中每个位置都是一个指针,那么我们只需要遍历旧表中结点映射在新表中的位置,使其指向旧表中该结点。最后,将旧表置为空,交换新旧链表,完成扩容操作。
            		bool Insert(const pair& kv)
            		{
            			//不允许冗余
            			if (Find(kv.first))
            				return false;
            			Hash hs;
            			//需要扩容
            			if (_n == _tables.size())
            			{
            				// 也可以,但是扩容新开辟节点,释放旧节点,有点浪费
            				/*HashTable newHT(__stl_next_prime(_tables.size() + 1));
            				for (size_t i = 0; i _kv);
            						cur = cur->_next;
            					}
            				}
            				_tables.swap(newHT._tables);*/
            				vector newtables(__stl_next_prime(_tables.size() + 1), nullptr);
            				//遍历旧表
            				for (size_t i = 0;i _next;
            						size_t hash0 = hs(cur->_kv.first) % newtables.size();
            						cur->_next = newtables[hash0];
            						newtables[hash0] = cur;
            						cur = next;
            					}
            					_tables[i] = nullptr;
            				}
            				_tables.swap(newtables);
            			}
            			size_t hash0 = hs(kv.first) % _tables.size();
            			Node* newnode = new Node(kv);
            			//头插
            			newnode->_next = _tables[hash0];
            			_tables[hash0] = newnode;
            			++_n;
            			return true;
            		}
            删除

            【C++指南】哈希驱动的封装:如何让unordered

                    bool Erase(const K& key)
            		{
            			Hash hs;
            			size_t hash0 = hs(key) % _tables.size();
            			Node* prev = nullptr;
            			Node* cur = _tables[hash0];
            			while (cur)
            			{
            				if (cur->_kv.first == key)
            				{
            					if (prev == nullptr)
            					{
            						_tables[hash0] = cur->_next;
            					}
            					else
            					{
            						prev->_next = cur->_next;
            					}
            					--_n;
            					delete cur;
            					return true;
            				}
            				prev = cur;
            				cur = cur->_next;
            			}
            			return false;
            		}
            代码实现
            	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
            	};
            	inline unsigned long __stl_next_prime(unsigned long n)
            	{
            		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
            	struct HashNode
            	{
            		pair _kv;
            		HashNode* _next;
            		HashNode(const pair& kv)
            			:_next(nullptr)
            			,_kv(kv)
            		{}
            	};
            	//仿函数
            	template
            	struct HashFunc
            	{
            		size_t operator()(const K& key)
            		{
            			return (size_t)key; // --> 针对浮点、负数等情况
            		}
            	};
            	//针对字符串
            	template
            	struct HashFunc
            	{
            		size_t operator()(const string& key)
            		{
            			size_t hash0 = 0;
            			for (auto& ch : key)
            			{
            				//对不同字符串但hash0相同的处理,减少冲突
            				hash0 *= 131;
            				hash0 += ch;
            			}
            			return hash0;
            		}
            	};
            	template
            	class HashTable
            	{
            		typedef HashNode Node;
            	public:
            		HashTable(size_t size = __stl_next_prime(0))
            			:_tables(size, nullptr)
            			,_n(0)
            		{}
            		bool Insert(const pair& kv)
            		{
            			//不允许冗余
            			if (Find(kv.first))
            				return false;
            			Hash hs;
            			//需要扩容
            			if (_n == _tables.size())
            			{
            				// 也可以,但是扩容新开辟节点,释放旧节点,有点浪费
            				/*HashTable newHT(__stl_next_prime(_tables.size() + 1));
            				for (size_t i = 0; i _kv);
            						cur = cur->_next;
            					}
            				}
            				_tables.swap(newHT._tables);*/
            				vector newtables(__stl_next_prime(_tables.size() + 1), nullptr);
            				//遍历旧表
            				for (size_t i = 0;i _next;
            						size_t hash0 = hs(cur->_kv.first) % newtables.size();
            						cur->_next = newtables[hash0];
            						newtables[hash0] = cur;
            						cur = next;
            					}
            					_tables[i] = nullptr;
            				}
            				_tables.swap(newtables);
            			}
            			size_t hash0 = hs(kv.first) % _tables.size();
            			Node* newnode = new Node(kv);
            			//头插
            			newnode->_next = _tables[hash0];
            			_tables[hash0] = 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;
            				cur = cur->_next;
            			}
            			return nullptr;
            		}
            		bool Erase(const K& key)
            		{
            			Hash hs;
            			size_t hash0 = hs(key) % _tables.size();
            			Node* prev = nullptr;
            			Node* cur = _tables[hash0];
            			while (cur)
            			{
            				if (cur->_kv.first == key)
            				{
            					if (prev == nullptr)
            					{
            						_tables[hash0] = cur->_next;
            					}
            					else
            					{
            						prev->_next = cur->_next;
            					}
            					--_n;
            					delete cur;
            					return true;
            				}
            				prev = cur;
            				cur = cur->_next;
            			}
            			return false;
            		}
            		~HashTable()
            		{
            			for (size_t i = 0;i _next;
            					delete cur;
            					cur = next;
            				}
            				_tables[i] = nullptr;
            			}
            		}
            	private:
            		vector _tables;
            		size_t _n;
            	};

            【C++指南】哈希驱动的封装:如何让unordered

            【C++指南】哈希驱动的封装:如何让unordered

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码