哈希表与hashtable
1. 定义
如果查找关键字时不需要比较就可以获得需要记录的存储位置,这就称作【散列技术】。它是在记录的存储位置和它的关键字之间建立一个确定的对应关系 f,使得每个关键字 key 对应一个存储位置 f(key)。把这种对应关系 f 称为【散列函数】,又称【哈希(hash)函数】,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为【散列表】或【哈希表(hash table)】。那么关键字对应的记录存储位置就称为【散列地址】。
优点:适用于快速的查找,其时间复杂度为 。
缺点:占用内存空间比较大。
1.1 散列函数
设计特点:
- 计算简单(复杂的计算会降低查找的时间)
- 散列地址分布均匀(减少哈希冲突)
1.1.1 直接定址法
直接定址法就是取关键字的某个线性函数作为散列地址,即
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是要事先知道关键字的分布情况,适合查找表较小且连续的情况。
1.1.2 数字分析法
如果关键字是位数较多的数字,比如 11 位的手机号,其前3位是接入号,对应不同运营商公司的子品牌;中间四位是 HLR 识别号,表示用户归属地;最后四位才是真正的用户号。
那么如果采用用户手机号作为关键字,很可能前 7 位都是相同的。那么可以选择后四位成为散列地址。同时,为了解决冲突问题,还可以对抽取出来的数字再进行反转、右环移位、左环移位、甚至前两数与后两数叠加等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,可以考虑此方法。
1.1.3 平方取中法
假如关键字为 1234,那么对其平方,得到 1522756,再抽取中间的 3 位就是 227,用做散列地址。
平方取中法比较适合不知道关键字的分布,而位数又不是很大的情况。
1.1.4 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如关键字为 9876543210,散列表表长为三位,将其分为四组,987|654|321|0,然后对其叠加求和,即 987+654+321+0=1962,再求后 3 位得到散列地址为 962。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
1.1.5 除留余数法
此方法为最常用的构造散列函数的方法。对于散列表长为 m 的散列函数公式为:,其中,mod 是取模(求余数)的意思。
根据经验,若散列表表长为 m,通常 p 为小于或等于表长(最好接近 m)的最小质数或不包含小于 20 质因子的合数。
1.1.6 随机数法
选择一个随机数,取关键字的随机函数值作为它的散列表地址。当关键字长度不等时,采用这个方法较为合适。
1.2 散列冲突
1.2.1 线性探测法
线性探测法也称为【开放定址法】,即一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列表地址总能找到,并将记录存入,即 。
比如,对于如下关键字集合,表长为 12。采用散列函数 。
12 | 67 | 56 | 16 | 25 | 37 | 22 | 29 | 15 | 47 | 48 | 34 |
---|
当计算前5个数时,都没有冲突的散列地址,直接存入:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 16 | 67 | 56 |
当计算 37 时,发现,此时就与 25 所在位置产生冲突。于是应用上面的公式。于是 37 就存入了下标为 2 的位置:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 37 | 16 | 67 | 56 |
接下来的 22、29、15、47 都没有冲突:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 37 | 15 | 16 | 29 | 67 | 56 | 22 | 47 |
当计算 48 时,有 ,与 12 所在的位置冲突了,然后 ,此时又与 25 所在位置产生冲突,于是 ,还是冲突,重复如此计算,直到结果为 6 时才有空位:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
关键字 | 12 | 25 | 37 | 15 | 16 | 29 | 48 | 67 | 56 | 22 | 47 |
将这种解决冲突的开放定址法称为【线性探测法】。对于上述例子中 48 和 37 这种本来都不是同义词却需要争夺一个地址的情况,称这种现象为【堆积】。
开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是常用的解决冲突的方法。
1.2.2 二次探测法
同样的对于上述的例子,当计算 34 时,,与 22 所在位置冲突,但是 22 后面没有空位了,反而其前面存在空位,虽然可以不断求余数得到结果,但是效率很差。
因此改进 ,使得可以双向寻找可能的空位置。称这种方法为【二次探测法】。
还有一种方法,在冲突时,对于位移量 采用随机函数计算得到,称之为【随机探测法】。
1.2.3 链地址法
将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。

2.代码实现
2.1 线性探测哈希表
线性探测哈希表的代码实现如下:
// 桶的状态 enum State { UNUSED = 0, // 未曾使用 USING, // 正在使用 REMOVE, // 已被删除 }; struct Bucket { Bucket(int key = 0, State state = UNUSED) : val_(key) , state_(state) { } int val_; // 存储的数据 State state_; // 桶的当前状态 }; class HashTable { public: HashTable(int size = primes_[0], double loadfactor = 0.75); ~HashTable(); // 插入元素 bool insert(int key); // 删除元素 bool erase(int key); // 查询 bool find(int key) const; // 打印 void print() const; private: // 扩容 void expand(); private: Bucket* table_; // 可动态扩容的数组 int tabSize_; // 桶的总数量 int usedCnt_; // 已使用的桶数量 double loadfactor_; // 装载因子 int primeIdx_; // 素数表下标 static const int PRIME_SIZE = 10; // 素数表的大小 static int primes_[PRIME_SIZE]; // 素数表 }; int HashTable::primes_[PRIME_SIZE] = { 3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773 }; HashTable::HashTable(int size, double loadfactor) : usedCnt_(0) , loadfactor_(loadfactor) , primeIdx_(0) { // 将其调整到最近的素数 if (size != primes_[0]) { for (; primeIdx_ < PRIME_SIZE; primeIdx_++) { if (primes_[primeIdx_] > size) { break; } } } // 如果size过大 已超过最后一个素数 则调整为最后一个素数 if (primeIdx_ == PRIME_SIZE) { primeIdx_--; } tabSize_ = primes_[primeIdx_]; table_ = new Bucket[tabSize_]; } HashTable::~HashTable() { delete[] table_; table_ = nullptr; } bool HashTable::insert(int key) { double factor = usedCnt_ * 1.0 / tabSize_; if (factor > loadfactor_) { // 哈希表扩容 expand(); } int idx = key % tabSize_; int i = idx; do { if (table_[i].state_ != USING) { table_[i].state_ = USING; table_[i].val_ = key; usedCnt_++; return true; } i = (i + 1) % tabSize_; } while (i != idx); return false; } bool HashTable::erase(int key) { int idx = key % tabSize_; int i = idx; do { if (table_[i].state_ == USING && table_[i].val_ == key) { table_[i].state_ = REMOVE; usedCnt_--; } i = (i + 1) % tabSize_; } while (table_[i].state_ != UNUSED && i != idx); return true; } bool HashTable::find(int key) const { int idx = key % tabSize_; int i = idx; do { if (table_[i].state_ == USING && table_[i].val_ == key) { return true; } i = (i + 1) % tabSize_; } while (table_[i].state_ != UNUSED && i != idx); return false; } void HashTable::print() const { for (int i = 0; i < tabSize_; i++) { if (table_[i].state_ == USING) { cout << table_[i].val_ << " "; } else { cout << "x "; } } cout << endl; } void HashTable::expand() { ++primeIdx_; if (primeIdx_ == PRIME_SIZE) { throw "HashTable is too large! Can't expand anymore!"; } Bucket* newTable = new Bucket[primes_[primeIdx_]]; for (int i = 0; i < tabSize_; i++) { if (table_[i].state_ == USING) { int idx = table_[i].val_ % primes_[primeIdx_]; int k = idx; do { if (newTable[k].state_ != USING) { newTable[k].state_ = USING; newTable[k].val_ = table_[i].val_; break; } k = (k + 1) % primes_[primeIdx_]; } while (k != idx); } } delete[] table_; table_ = newTable; tabSize_ = primes_[primeIdx_]; }
线性探测哈希表的缺陷如下:
- 发生哈希冲突时,需要接近 的时间复杂度来解决冲突;
- 在多线程环境中,线性探测所用到的基于数组实现的哈希表,只能给全局的表用互斥锁来保证哈希表的原子操作,以确保线程安全。
2.2 链地址法哈希表
在链地址法哈希表中,当发生哈希冲突时,将发生哈希冲突的元素采用链表数据结构连接起来。但是如果发生哈希冲突的元素过多,则会导致链表过长,那么其搜索的时间复杂度就会增加。
其常见的优化手段如下:
- 当链表长度过长时,可以将链表转化为红黑树数据结构的形式;
- 链式哈希表每个桶都可以创建自己的互斥锁,不同的桶之间可以进行并发操作;
链地址法哈希表的代码实现如下:
class HashTable { public: HashTable(int size = primes_[0], double loadFactor = 0.75); ~HashTable() = default; // 增加元素 void insert(int key); // 删除元素 void remove(int key); // 查找元素 bool find(int key) const; // 打印 void print() const; private: // 扩容 void expand(); private: vector<list<int>> table_; // 哈希表 int useCnt_; // 已使用的桶数量 double loadfactor_; // 装载因子 int primeIdx_; // 素数表下标 static const int PRIME_SIZE = 10; // 素数表的大小 static int primes_[PRIME_SIZE]; // 素数表 }; int HashTable::primes_[PRIME_SIZE] = { 3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773 }; HashTable::HashTable(int size, double loadFactor) : useCnt_(0) , loadfactor_(loadFactor) , primeIdx_(0) { if (size != primes_[0]) { for (; primeIdx_ < PRIME_SIZE; primeIdx_++) { if (primes_[primeIdx_] >= size) break; } if (primeIdx_ == PRIME_SIZE) { primeIdx_--; } } table_.resize(primes_[primeIdx_]); } void HashTable::insert(int key) { double factor = useCnt_ * 1.0 / table_.size(); if (factor > loadfactor_) { expand(); } int idx = key % table_.size(); if (table_[idx].empty()) { useCnt_++; table_[idx].emplace_front(key); } else { auto it = ::find(table_[idx].begin(), table_[idx].end(), key); if (it == table_[idx].end()) { table_[idx].emplace_front(key); } } } void HashTable::remove(int key) { int idx = key % table_.size(); auto it = ::find(table_[idx].begin(), table_[idx].end(), key); if (it != table_[idx].end()) { table_[idx].erase(it); if (table_[idx].empty()) { useCnt_--; } } } bool HashTable::find(int key) const { int idx = key % table_.size(); auto it = ::find(table_[idx].begin(), table_[idx].end(), key); return it != table_[idx].end(); } void HashTable::print() const { cout << "==================" << endl; for (int i = 0; i < table_.size(); i++) { cout << "[" << i << "] -> "; for (auto key : table_[i]) { cout << key << " -> "; } cout << endl; } cout << "==================" << endl; } void HashTable::expand() { if (primeIdx_ + 1 == PRIME_SIZE) { throw "Hashtable cannot expand anymore!"; } primeIdx_++; useCnt_ = 0; vector<list<int>> oldTable; table_.swap(oldTable); table_.resize(primes_[primeIdx_]); for (auto list : oldTable) { for (auto key : list) { int idx = key % table_.size(); if (table_[idx].empty()) { useCnt_++; } table_[idx].emplace_front(key); } } }
3. STL实现
3.1 hashtable
SGI 的 hashtable 实现了哈希表的数据结构,其采用除留余数法来构造散列函数,同时通过开链法来解决哈希冲突的问题。它称 hashtable 表格内的元素为【桶子(bucket)】。

如下为 hashtable 的节点定义:
template<class Value> struct __hashtable_node { __hashtable_node* next; Value val; };
注意,bucket 所维护的 linked list,并不采用 STL 的 list 或 slist,而是自行维护上述的 hashtable node。至于 buckets 结构体,则以 vector 完成,以便有动态扩充能力。
对于 hashtable 的迭代器来说,其前进操作是首先尝试从目前所指的节点出发,前进一个节点,由于节点被安置在 list 内,所以利用节点的 next 指针即可轻易达成前进操作。如果目前节点正巧是 list 的尾端,就跳至下一个 bucket 身上,那正是指向下一个 list 的头部节点。
注意:hashtable 的迭代器没有后退操作,也没有定义所谓的逆向迭代器。
虽然开链法并不要求表格大小必须为质数,但 SGI STL 仍然以质数来设计表格大小,并且先将 28 个质数(逐渐呈现大约 2 倍关系)计算好,以备随时访问,同时提供一个函数__stl_next_prime()
,用来查询在这 28 个质数之中,最接近某数并大于某数的质数。
注意,是否重建表格的依据是拿元素个数和 bucket vector 的大小来比,如果前者大于后者,就重建表格。因此,每个 bucket(list) 的最大容量和 bucktes vector 的大小相同。
对于哈希函数来说,由于某些元素型别无法对其进行取模运算,因此 SGI 对内建的所有 hash functions 包装了一层,通过函数 bkt_num()
来获取元素落脚的位置。
在 <stl_hash_fun.h>
中定义有数个现成的 hash functions,全部都是仿函数。通过【模板特化】机制来计算相应的哈希函数。其中,针对 char、int、long 等整数型别,大部分的 hash functions 什么都不做。但是对于字符串 const char*,则设计有相应的转换函数:
inline size_t __stl_hash_string(const char* s) { unsigned long h = 0; for(; *s; ++s) h = 5 * h + s; return size_t(h); }
而对于其他的型别,例如 string、double、float 等,用户都必须自行为它们定义 hash function。
3.2 hash_set&hash_multiset
hash_set 以 hashtable 作为底层机制。与 set 不同的是,set 的底层机制 RB-tree 有自动排序功能而 hashtable 没有,反映出来的结果就是,set 的元素有自动排序功能而 hash_set 没有。
注意:hash_set 有一些无法处理的型别,除非用户为那些型别撰写 hash function。凡是 hashtable 无法处理者,hash_set 也无法处理。
hash_set 的构造函数在缺省情况下,默认采用大小为 100 的表格,然后将被 hash table 调整为最接近且较大的质数。其插入操作则采用 hashtable 的 insert_unique()
操作,不允许键值重复。
hash_multiset 的特性与 multiset 完全相同,唯一的差别在于它的底层机制是 hashtable。也因此,hash_multiset 的元素并不会被自动排序。
hash_multiset 和 hash_set 实现上唯一的差别在于,前者的元素插入操作采用底层机制 hashtable 的 insert_equal()
,而后者采用的是 insert_unique()
。
3.3 hash_map&hash_multimap
hash_map 以 hashtable 作为底层机制。与 map 不同的是,map 的底层机制 RB-tree 有自动排序功能而 hashtable 没有,反映出来的结果就是,map 的元素有自动排序功能而 hash_map 没有。
map 的特性是,每一个元素都同时拥有一个实值(value)和一个键值(key),这一点在 hash_map 中也是一样的。hash_map 的使用方式与 map 完全相同。
注意:hash_map 有一些无法处理的型别,除非用户为那些型别撰写 hash function。凡是 hashtable 无法处理者,hash_map 也无法处理。
hash_map 的构造函数在缺省情况下,默认采用大小为 100 的表格,然后将被 hash table 调整为最接近且较大的质数。其插入操作则采用 hashtable 的 insert_unique()
操作,不允许键值重复。
hash_multimap 的特性与 multimap 完全相同,唯一的差别在于它的底层机制是 hashtable。也因此,hash_multimap 的元素并不会被自动排序。
hash_multimap 和 hash_map 实现上唯一的差别在于,前者的元素插入操作采用底层机制 hashtable 的 insert_equal()
,而后者采用的是 insert_unique()
。
本文作者:Leaos
本文链接:https://www.cnblogs.com/tuilk/p/17017110.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步