1. 哈希概念哈希(hash)⼜称散列是⼀种组织数据的⽅式。从译名来看有散乱排列的意思。本质就是通过哈希 函数把关键字Key跟存储位置建⽴⼀个映射关系查找时通过这个哈希函数计算出Key存储的位置进 ⾏快速查找。1.1 直接定址法当关键字的范围⽐较集中时直接定址法就是⾮常简单⾼效的⽅法⽐如⼀组关键字都在[0,99]之间 那么我们开⼀个100个数的数组每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在 [a,z]的⼩写字⺟那么我们开⼀个26个数的数组每个关键字acsii码-a ascii码就是存储位置的下标。 也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置。1.2 哈希冲突直接定址法的缺点也⾮常明显当关键字的范围⽐较分散时就很浪费内存甚⾄内存不够⽤。假设我 们只有数据范围是[0,9999]的N个值我们要映射到⼀个M个空间的数组中(⼀般情况下MN)那么 就要借助哈希函数(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.5.2 乘法散列法了解1.5.3 全域散列法了解1.6 处理哈希冲突实践中哈希表⼀般还是选择除法散列法作为哈希函数当然哈希表⽆论选择什么哈希函数也避免不了 冲突那么插⼊数据时如何解决冲突呢主要有两种两种⽅法开放定址法和链地址法。1.6.1 开放定址法在开放定址法中所有的元素都放到哈希表⾥当⼀个关键字key⽤哈希函数计算出的位置冲突了则按 照某种规则找到⼀个没有存储数据的位置进⾏存储开放定址法中负载因⼦⼀定是⼩于的。这⾥的规 则有三种线性探测、⼆次探测、双重探测。线性探测二次探测双重散列1.6.2 开放定址法代码实现开放定址法的哈希表结构enum State { EXIST, EMPTY, DELETE }; templateclass K, class V struct HashData { pairK, V _kv; State _state EMPTY; }; templateclass K, class V class HashTable { private: vectorHashDataK, V _tables; size_t _n 0; // 表中存储数据个数 };要注意的是这⾥需要给每个存储值的位置加⼀个状态标识否则删除⼀些值以后会影响后⾯冲突的 值的查找扩容这⾥我们哈希表负载因⼦控制在0.7当负载因⼦到0.7以后我们就需要扩容了我们还是按照2倍扩 容但是同时我们要保持哈希表⼤⼩是⼀个质数第⼀个是质数2倍后就不是质数了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; } if ((double)_n / _tables.size() 0.7) { HashTableK, V, Hash newHT; newHT._tables.resize(__stl_next_prime(_tables.size()1)); // 遍历旧表将所有值映射到新表 for (auto data : _tables) { if (data._status EXIST) { newHT.Insert(data._kv); } } _tables.swap(newHT._tables); }key不能取模的问题当key是string/Date等类型时key不能取模那么我们需要给HashTable增加⼀个仿函数这个仿函 数⽀持把key转换成⼀个可以取模的整形如果key可以转换为整形并且不容易冲突那么这个仿函数 就⽤默认参数即可如果这个Key不能转换为整形我们就需要⾃⼰实现⼀个仿函数传给这个参数实 现这个仿函数的要求就是尽量key的每值都参与到计算中让不同的key转换出的整形值不同。templateclass K struct HashFunc { size_t operator()(const K key) { return (size_t)key; } }; template struct HashFuncstring { size_t operator()(const string str) { size_t hash 0; for (auto ch : str) { hash ch; hash * 131; } return hash; } };1.6.3 链地址法扩容开放定址法负载因⼦必须⼩于1链地址法的负载因⼦就没有限制了可以⼤于1。负载因⼦越⼤哈 希冲突的概率越⾼空间利⽤率越⾼负载因⼦越⼩哈希冲突的概率越低空间利⽤率越低stl中 unordered_xxx的最⼤负载因⼦基本控制在1⼤于1就扩容极端场景如果极端场景下某个桶特别⻓怎么办其实我们可以考虑使⽤全域散列法这样就不容易被针对 了。但是假设不是被针对了⽤了全域散列法但是偶然情况下某个桶很⻓查找效率很低怎么 办这⾥在Java8的HashMap中当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下 不断扩容单个桶很⻓的场景还是⽐较少的下⾯我们实现就不搞这么复杂了这个解决极端场景的 思路⼤家了解⼀下namespace hash_bucket { templateclass K,class V struct HashNode { pairK, V _kv; HashNodek, V* _next; HashNode(const pairK,V kv) :_kv(kv) ,next(nullptr) { } }; templateclass K, class V, class Hash HashFuncK class HashTable { typedef HashNodeK, V Node; public: HashTable() :_tables(__stl_next_prime(1), nullptr) , _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; } } bool Insert(const pairK, V kv) { if (Find(kv.first)) return false; Hash hs; if (_n _tables.size()) { vectorNode* newtables(__stl_next_prime(_tables.size() 1)); for (size_t i 0; i _tables.size(); i) { Node* cur _tables[i]; while (cur) { Node* next cur-_next; size_t hashi hs(cur-_kv.first) % newtables.size(); cur-_next newtables[hashi]; newtables[hashi] 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; cur cur-_next; } return nullptr; } bool Erase(const K key) { Hash hs; size_t hashi hs(key) % _tables.size(); Node* prev nullptr; Node* cur tables[hashi]; while (cur) { if (cur-_kv.first key) { if (prev nulptr) { _tables[hashi] cur-_next; } else { prev-_next cur-_next; } delete cur; return true; } prev cur; cur cur-_next; } return false; } private: //vectorlistpairK, V _tables; vectorNode* _tables; size_t _n 0; // 实际存储的数据个数 }; }