哈希表实现
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define HASHTABLE_CAPACITY 20
// === File: array_hash_map.c ===
/* 键值对 int->string */
typedef struct {
int key;
char* val;
} Pair;
typedef struct {
void* set;
int len;
} MapSet;
/* 基于数组简易实现的哈希表 */
typedef struct {
Pair* buckets[HASHTABLE_CAPACITY];
} ArrayHashMap;
/* 构造函数 */
ArrayHashMap* newArrayHashMap() {
ArrayHashMap* hmap = malloc(sizeof(ArrayHashMap));
return hmap;
}
/* 析构函数 */
void delArrayHashMap(ArrayHashMap* hmap) {
for (int i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
free(hmap->buckets[i]->val);
free(hmap->buckets[i]);
}
}
free(hmap);
}
/* 添加操作 */
void put(ArrayHashMap* hmap, const int key, const char* val) {
Pair* Pair = malloc(sizeof(Pair));
Pair->key = key;
Pair->val = malloc(strlen(val) + 1);
strcpy(Pair->val, val);
int index = hashFunc(key);
hmap->buckets[index] = Pair;
}
/* 删除操作 */
void removeItem(ArrayHashMap* hmap, const int key) {
int index = hashFunc(key);
free(hmap->buckets[index]->val);
free(hmap->buckets[index]);
hmap->buckets[index] = NULL;
}
/* 获取所有键值对 */
void pairSet(ArrayHashMap* hmap, MapSet* set) {
Pair* entries;
int i = 0, index = 0;
int total = 0;
/* 统计有效键值对数量 */
for (i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
total++;
}
}
entries = malloc(sizeof(Pair) * total);
for (i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
entries[index].key = hmap->buckets[i]->key;
entries[index].val = malloc(strlen(hmap->buckets[i]->val + 1));
strcpy(entries[index].val, hmap->buckets[i]->val);
index++;
}
}
set->set = entries;
set->len = total;
}
/* 获取所有键 */
void keySet(ArrayHashMap * hmap, MapSet * set) {
int* keys;
int i = 0, index = 0;
int total = 0;
/* 统计有效键值对数量 */
for (i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
total++;
}
}
keys = malloc(total * sizeof(int));
for (i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
keys[index] = hmap->buckets[i]->key;
index++;
}
}
set->set = keys;
set->len = total;
}
/* 获取所有值 */
void valueSet(ArrayHashMap* hmap, MapSet* set) {
char** vals;
int i = 0, index = 0;
int total = 0;
/* 统计有效键值对数量 */
for (i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
total++;
}
}
vals = malloc(total * sizeof(char*));
for (i = 0; i < HASHTABLE_CAPACITY; i++) {
if (hmap->buckets[i] != NULL) {
vals[index] = hmap->buckets[i]->val;
index++;
}
}
set->set = vals;
set->len = total;
}
/* 打印哈希表 */
void print(ArrayHashMap* hmap) {
int i;
MapSet set;
pairSet(hmap, &set);
Pair* entries = (Pair*)set.set;
for (i = 0; i < set.len; i++) {
printf("%d -> %s\n", entries[i].key, entries[i].val);
}
free(set.set);
}
一种常见的实现方式是使用数组和链表的组合来构建哈希表。具体步骤如下:
- 创建一个固定大小的数组,作为哈希表的桶(bucket)。
- 使用哈希函数将键(key)转换为数组的索引,即桶的位置。
- 如果发生碰撞(collisions),也就是两个不同的键生成相同的索引,可以在桶的位置上使用链表或其他数据结构来存储冲突的键值对。
- 为了插入、查找或删除键值对,你需要实现相应的操作,并对桶中的链表进行遍历。
哈希冲突
多个输入对应同一输出的情况
容易想到,哈希表容量越大,多个 key 被分配到同一个桶中的概率就越低,冲突就越少。因此,我们可以
通过扩容哈希表来减少哈希冲突。
类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时。并且由于哈希表容量
capacity 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步提高了扩容过程的计算
开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。
「负载因子 load factor」是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希
冲突的严重程度,也常被作为哈希表扩容的触发条件。例如在 Java 中,当负载因子超过 0.75 时,系统会将
哈希表容量扩展为原先的 2 倍。
通常情况下哈希函数的输入空间远大于输出空间,因此理论上哈希冲突是不可避免的。比如,输
入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一桶索引。
哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为解决该问题,我们可以每当遇到哈希冲突时就
进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量
的数据搬运与哈希值计算。为了提升效率,我们可以采用以下策略。
- 改良哈希表数据结构,使得哈希表可以在存在哈希冲突时正常工作。
- 仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。
哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。
链式地址
// === File: hash_map_chaining.c ===
/* 链表节点 */
typedef struct Node {
Pair* pair;
struct Node* next;
} Node;
/* 链式地址哈希表 */
typedef struct {
int size; // 键值对数量
第 6 章 哈希表 hello‑algo.com 122
int capacity; // 哈希表容量
double loadThres; // 触发扩容的负载因子阈值
int extendRatio; // 扩容倍数
Node** buckets; // 桶数组
} HashMapChaining;
/* 构造函数 */
HashMapChaining* newHashMapChaining() {
HashMapChaining* hashMap = (HashMapChaining*)malloc(sizeof(HashMapChaining));
hashMap->size = 0;
hashMap->capacity = 4;
hashMap->loadThres = 2.0 / 3.0;
hashMap->extendRatio = 2;
hashMap->buckets = (Node**)malloc(hashMap->capacity * sizeof(Node*));
for (int i = 0; i < hashMap->capacity; i++) {
hashMap->buckets[i] = NULL;
}
return hashMap;
}
/* 析构函数 */
void delHashMapChaining(HashMapChaining* hashMap) {
for (int i = 0; i < hashMap->capacity; i++) {
Node* cur = hashMap->buckets[i];
while (cur) {
Node* tmp = cur;
cur = cur->next;
free(tmp->pair);
free(tmp);
}
}
free(hashMap->buckets);
free(hashMap);
}
/* 哈希函数 */
int hashFunc(HashMapChaining* hashMap, int key) {
return key % hashMap->capacity;
}
/* 负载因子 */
double loadFactor(HashMapChaining* hashMap) {
return (double)hashMap->size / (double)hashMap->capacity;
}
/* 查询操作 */
char* get(HashMapChaining* hashMap, int key) {
int index = hashFunc(hashMap, key);
// 遍历桶,若找到 key 则返回对应 val
Node* cur = hashMap->buckets[index];
while (cur) {
if (cur->pair->key == key) {
return cur->pair->val;
}
cur = cur->next;
}
return ""; // 若未找到 key 则返回空字符串
}
/* 添加操作 */
void put(HashMapChaining* hashMap, int key, const char* val) {
// 当负载因子超过阈值时,执行扩容
if (loadFactor(hashMap) > hashMap->loadThres) {
extend(hashMap);
}
int index = hashFunc(hashMap, key);
// 遍历桶,若遇到指定 key ,则更新对应 val 并返回
Node* cur = hashMap->buckets[index];
while (cur) {
if (cur->pair->key == key) {
strcpy(cur->pair->val, val); // 若遇到指定 key ,则更新对应 val 并返回
return;
}
cur = cur->next;
}
// 若无该 key ,则将键值对添加至尾部
Pair* newPair = (Pair*)malloc(sizeof(Pair));
newPair->key = key;
strcpy(newPair->val, val);
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->pair = newPair;
newNode->next = hashMap->buckets[index];
hashMap->buckets[index] = newNode;
hashMap->size++;
}
/* 扩容哈希表 */
void extend(HashMapChaining* hashMap) {
// 暂存原哈希表
int oldCapacity = hashMap->capacity;
Node** oldBuckets = hashMap->buckets;
// 初始化扩容后的新哈希表
hashMap->capacity *= hashMap->extendRatio;
hashMap->buckets = (Node**)malloc(hashMap->capacity * sizeof(Node*));
for (int i = 0; i < hashMap->capacity; i++) {
hashMap->buckets[i] = NULL;
}
hashMap->size = 0;
// 将键值对从原哈希表搬运至新哈希表
for (int i = 0; i < oldCapacity; i++) {
Node* cur = oldBuckets[i];
while (cur) {
put(hashMap, cur->pair->key, cur->pair->val);
Node* temp = cur;
cur = cur->next;
// 释放内存
free(temp->pair);
free(temp);
}
}
free(oldBuckets);
}
/* 删除操作 */
void removeItem(HashMapChaining* hashMap, int key) {
int index = hashFunc(hashMap, key);
Node* cur = hashMap->buckets[index];
Node* pre = NULL;
while (cur) {
if (cur->pair->key == key) {
// 从中删除键值对
if (pre) {
pre->next = cur->next;
}
else {
hashMap->buckets[index] = cur->next;
}
// 释放内存
free(cur->pair);
free(cur);
hashMap->size--;
return;
}
pre = cur;
cur = cur->next;
}
}
/* 打印哈希表 */
void print(HashMapChaining* hashMap) {
for (int i = 0; i < hashMap->capacity; i++) {
Node * cur = hashMap->buckets[i];
printf("[");
while (cur) {
printf("%d -> %s, ", cur->pair->key, cur->pair->val);
cur = cur->next;
}
printf("]\n");
}
}
在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图 6‑5 展示了一个链式地址哈希表的
例子。
基于链式地址实现的哈希表的操作方法发生了以下变化。
- ‧ 查询元素:输入 key ,经过哈希函数得到桶索引,即可访问链表头节点,然后遍历链表并对比 key 以查
- 找目标键值对。
- ‧ 添加元素:先通过哈希函数访问链表头节点,然后将节点(即键值对)添加到链表中。
- ‧ 删除元素:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标节点,并将其删除。
链式地址存在以下局限性。
- ‧ 占用空间增大,链表包含节点指针,它相比数组更加耗费内存空间。
- ‧ 查询效率降低,因为需要线性遍历链表来查找对应元素。
以下代码给出了链式地址哈希表的简单实现,需要注意两点。
- ‧ 使用列表(动态数组)代替链表,从而简化代码。在这种设定下,哈希表(数组)包含多个桶,每个桶都是一个列表。
- ‧ 以下实现包含哈希表扩容方法。当负载因子超过 2/3 时,我们将哈希表扩容至 2 倍。
值得注意的是,当链表很长时,查询效率 O(n) 很差。此时可以将链表转换为“AVL 树”或“红黑树”,从而
将查询操作的时间复杂度优化至 O(log n ) 。
开放寻址
「开放寻址 open addressing」不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主
要包括线性探测、平方探测、多次哈希等。
下面将主要以线性探测为例,介绍开放寻址哈希表的工作机制与代码实现。
1.线性探测
线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。
‧ 插入元素:通过哈希函数计算桶索引,若发现桶内已有元素,则从冲突位置向后线性遍历(步长通常为
1 ),直至找到空桶,将元素插入其中。
‧ 查找元素:若发现哈希冲突,则使用相同步长向后线性遍历,直到找到对应元素,返回 value 即可;如
果遇到空桶,说明目标元素不在哈希表中,返回 None 。
图 6‑6 展示了开放寻址(线性探测)哈希表的键值对分布。根据此哈希函数,最后两位相同的 key 都会被映
射到相同的桶。而通过线性探测,它们被依次存储在该桶以及之下的桶中。
然而,线性探测容易产生“聚集现象”。具体来说,数组中连续被占用的位置越长,这些连续位置发生哈希冲
突的可能性越大,从而进一步促使该位置的聚堆生长,形成恶性循环,最终导致增删查改操作效率劣化。
值得注意的是,我们不能在开放寻址哈希表中直接删除元素。这是因为删除元素会在数组内产生一个空桶
None ,而当查询元素时,线性探测到该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序
可能误判这些元素不存在。
为了解决该问题,我们可以采用「懒删除 lazy deletion」机制:它不直接从哈希表中移除元素,而是利用一
个常量 TOMBSTONE 来标记这个桶。在该机制下,None 和 TOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 TOMBSTONE 时应该继续遍历,因为其之下可能还存在键值对。
然而,懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记,随着 TOMBSTONE
的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 TOMBSTONE 才能找到目标元素。
为此,考虑在线性探测中记录遇到的首个 TOMBSTONE 的索引,并将搜索到的目标元素与该 TOMBSTONE 交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从
而优化查询效率。
以下代码实现了一个包含懒删除的开放寻址(线性探测)哈希表。为了更加充分地使用哈希表的空间,我们
将哈希表看作是一个“环形数组”,当越过数组尾部时,回到头部继续遍历。
// === File: hash_map_open_addressing.c ===
/* 开放寻址哈希表 */
typedef struct {
int size; // 键值对数量
int capacity; // 哈希表容量
double loadThres; // 触发扩容的负载因子阈值
int extendRatio; // 扩容倍数
Pair **buckets; // 桶数组
Pair *TOMBSTONE; // 删除标记
} HashMapOpenAddressing;
/* 构造函数 */
HashMapOpenAddressing *newHashMapOpenAddressing() {
HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));
hashMap->size = 0;
hashMap->capacity = 4;
hashMap->loadThres = 2.0 / 3.0;
hashMap->extendRatio = 2;
hashMap->buckets = (Pair **)malloc(sizeof(Pair *) * hashMap->capacity);
hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));
hashMap->TOMBSTONE->key = -1;
hashMap->TOMBSTONE->val = "-1";
return hashMap;
}
/* 析构函数 */
void delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {
for (int i = 0; i < hashMap->capacity; i++) {
Pair *pair = hashMap->buckets[i];
if (pair != NULL && pair != hashMap->TOMBSTONE) {
free(pair->val);
free(pair);
}
}
}
/* 哈希函数 */
int hashFunc(HashMapOpenAddressing *hashMap, int key) {
return key % hashMap->capacity;
}
/* 负载因子 */
double loadFactor(HashMapOpenAddressing *hashMap) {
return (double)hashMap->size / (double)hashMap->capacity;
}
/* 搜索 key 对应的桶索引 */
int findBucket(HashMapOpenAddressing *hashMap, int key) {
int index = hashFunc(hashMap, key);
int firstTombstone = -1;
// 线性探测,当遇到空桶时跳出
while (hashMap->buckets[index] != NULL) {
// 若遇到 key ,返回对应桶索引
if (hashMap->buckets[index]->key == key) {
// 若之前遇到了删除标记,则将键值对移动至该索引
if (firstTombstone != -1) {
hashMap->buckets[firstTombstone] = hashMap->buckets[index];
hashMap->buckets[index] = hashMap->TOMBSTONE;
return firstTombstone; // 返回移动后的桶索引
}
return index; // 返回桶索引
}
// 记录遇到的首个删除标记
if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {
firstTombstone = index;
}
// 计算桶索引,越过尾部返回头部
index = (index + 1) % hashMap->capacity;
}
// 若 key 不存在,则返回添加点的索引
return firstTombstone == -1 ? index : firstTombstone;
}
/* 查询操作 */
char *get(HashMapOpenAddressing *hashMap, int key) {
// 搜索 key 对应的桶索引
int index = findBucket(hashMap, key);
// 若找到键值对,则返回对应 val
if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {
return hashMap->buckets[index]->val;
}
// 若键值对不存在,则返回空字符串
return "";
}
/* 添加操作 */
void put(HashMapOpenAddressing *hashMap, int key, char *val) {
// 当负载因子超过阈值时,执行扩容
if (loadFactor(hashMap) > hashMap->loadThres) {
extend(hashMap);
}
// 搜索 key 对应的桶索引
int index = findBucket(hashMap, key);
// 若找到键值对,则覆盖 val 并返回
if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {
free(hashMap->buckets[index]->val);
hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val + 1)));
strcpy(hashMap->buckets[index]->val, val);
hashMap->buckets[index]->val[strlen(val)] = '\0';
return;
}
// 若键值对不存在,则添加该键值对
Pair *pair = (Pair *)malloc(sizeof(Pair));
pair->key = key;
pair->val = (char *)malloc(sizeof(strlen(val + 1)));
strcpy(pair->val, val);
pair->val[strlen(val)] = '\0';
hashMap->buckets[index] = pair;
hashMap->size++;
}
/* 删除操作 */
void removeItem(HashMapOpenAddressing *hashMap, int key) {
// 搜索 key 对应的桶索引
int index = findBucket(hashMap, key);
// 若找到键值对,则用删除标记覆盖它
if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {
Pair *pair = hashMap->buckets[index];
free(pair->val);
free(pair);
hashMap->buckets[index] = hashMap->TOMBSTONE;
hashMap->size--;
}
}
/* 扩容哈希表 */
void extend(HashMapOpenAddressing *hashMap) {
// 暂存原哈希表
Pair **bucketsTmp = hashMap->buckets;
int oldCapacity = hashMap->capacity;
// 初始化扩容后的新哈希表
hashMap->capacity *= hashMap->extendRatio;
hashMap->buckets = (Pair **)malloc(sizeof(Pair *) * hashMap->capacity);
hashMap->size = 0;
// 将键值对从原哈希表搬运至新哈希表
for (int i = 0; i < oldCapacity; i++) {
Pair *pair = bucketsTmp[i];
if (pair != NULL && pair != hashMap->TOMBSTONE) {
put(hashMap, pair->key, pair->val);
free(pair->val);
free(pair);
}
}
free(bucketsTmp);
}
/* 打印哈希表 */
void print(HashMapOpenAddressing *hashMap) {
for (int i = 0; i < hashMap->capacity; i++) {
Pair *pair = hashMap->buckets[i];
if (pair == NULL) {
printf("NULL\n");
} else if (pair == hashMap->TOMBSTONE) {
printf("TOMBSTONE\n");
} else {
printf("%d -> %s\n", pair->key, pair->val);
}
}
}
2.平方探测
平方探测与线性探测类似,都是开放寻址的常见策略之一。当发生冲突时,平方探测不是简单地跳过一个固
定的步数,而是跳过“探测次数的平方”的步数,即 1, 4, 9, … 步。
- ‧ 平方探测通过跳过平方的距离,试图缓解线性探测的聚集效应。
- ‧ 平方探测会跳过更大的距离来寻找空位置,有助于数据分布得更加均匀。
然而,平方探测也并不是完美的。
- ‧ 仍然存在聚集现象,即某些位置比其他位置更容易被占用。
- ‧ 由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。
3.多次哈希
多次哈希使用多个哈希函数 f1(x�)、�f2(x�)、f�3(x�)、… 进行探测。
‧ 插入元素:若哈希函数 �f1(x�)出现冲突,则尝试 �f2(x�) ,以此类推,直到找到空桶后插入元素。
‧ 查找元素:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;或当遇到空桶或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 None 。
与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会增加额外的计算量。
哈希算法
在上两节中,我们了解了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链地址法,它
们只能保证哈希表可以在发生冲突时正常工作,但无法减少哈希冲突的发生。
如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如图 6‑8 所示,对于链地址哈希表,理想情况下键值
对平均分布在各个桶中,达到最佳查询效率;最差情况下所有键值对都被存储到同一个桶中,时间复杂度退
化至 �O(n�) 。
键值对的分布情况由哈希函数决定。回忆哈希函数的计算步骤,先计算哈希值,再对数组长度取模:
index = hash(key) % capacity
观察以上公式,当哈希表容量 capacity 固定时,哈希算法 hash() 决定了输出值,进而决定了键值对在哈希表中的分布情况。
这意味着,为了减小哈希冲突的发生概率,我们应当将注意力集中在哈希算法 hash() 的设计上。
哈希算法的目标
为了实现“既快又稳”的哈希表数据结构,哈希算法应包含以下特点。
- ‧ 确定性:对于相同的输入,哈希算法应始终产生相同的输出。这样才能确保哈希表是可靠的。
- ‧ 效率高:计算哈希值的过程应该足够快。计算开销越小,哈希表的实用性越高。
- ‧ 均匀分布:哈希算法应使得键值对平均分布在哈希表中。分布越平均,哈希冲突的概率就越低。
实际上,哈希算法除了可以用于实现哈希表,还广泛应用于其他领域中。
- ‧ 密码存储:为了保护用户密码的安全,系统通常不会直接存储用户的明文密码,而是存储密码的哈希
值。当用户输入密码时,系统会对输入的密码计算哈希值,然后与存储的哈希值进行比较。如果两者匹
配,那么密码就被视为正确。 - ‧ 数据完整性检查:数据发送方可以计算数据的哈希值并将其一同发送;接收方可以重新计算接收到的
数据的哈希值,并与接收到的哈希值进行比较。如果两者匹配,那么数据就被视为完整的。
对于密码学的相关应用,为了防止从哈希值推导出原始密码等逆向工程,哈希算法需要具备更高等级的安全
特性。
- ‧ 单向性:无法通过哈希值反推出关于输入数据的任何信息。
- ‧ 抗碰撞性:应当极其困难找到两个不同的输入,使得它们的哈希值相同。
- ‧ 雪崩效应:输入的微小变化应当导致输出的显著且不可预测的变化。
请注意,“均匀分布”与“抗碰撞性”是两个独立的概念,满足均匀分布不一定满足抗碰撞性。例如,在随机
输入 key 下,哈希函数 key % 100 可以产生均匀分布的输出。然而该哈希算法过于简单,所有后两位相等的
key 的输出都相同,因此我们可以很容易地从哈希值反推出可用的 key ,从而破解密码。
- ‧ 加法哈希:对输入的每个字符的 ASCII 码进行相加,将得到的总和作为哈希值。
- ‧ 乘法哈希:利用了乘法的不相关性,每轮乘以一个常数,将各个字符的 ASCII 码累积到哈希值中。
- ‧ 异或哈希:将输入数据的每个元素通过异或操作累积到一个哈希值中。
- ‧ 旋转哈希:将每个字符的 ASCII 码累积到一个哈希值中,每次累积之前都会对哈希值进行旋转操作。
哈希算法的设计是一个需要考虑许多因素的复杂问题。然而对于某些要求不高的场景,我们也能设计一些简
单的哈希算法。
// === File: simple_hash.c ===
/* 加法哈希 */
int addHash(char *key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash = (hash + (unsigned char)key[i]) % MODULUS;
}
return (int)hash;
}
/* 乘法哈希 */
int mulHash(char *key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash = (31 * hash + (unsigned char)key[i]) % MODULUS;
}
return (int)hash;
}
/* 异或哈希 */
int xorHash(char *key) {
int hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash ^= (unsigned char)key[i];
}
return hash & MODULUS;
}
/* 旋转哈希 */
int rotHash(char *key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (int i = 0; i < strlen(key); i++) {
hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;
}
return (int)hash;
}
观察发现,每种哈希算法的最后一步都是对大质数 1000000007 取模,以确保哈希值在合适的范围内。值得
思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。
先抛出结论:当我们使用大质数作为模数时,可以最大化地保证哈希值的均匀分布。因为质数不会与其他数
字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
举个例子,假设我们选择合数 9 作为模数,它可以被 3 整除。那么所有可以被 3 整除的 key 都会被映射到 0、3、6 这三个哈希值。
modulus = 9
key = {0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, … }
hash =
如果输入 key 恰好满足这种等差数列的数据分布,那么哈希值就会出现聚堆,从而加重哈希冲突。现在,假设
将 modulus 替换为质数 13 ,由于 key 和 modulus 之间不存在公约数,输出的哈希值的均匀性会明显提升。
modulus = 13
key = {0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, … }
hash =
值得说明的是,如果能够保证 key 是随机均匀分布的,那么选择质数或者合数作为模数都是可以的,它们都
能输出均匀分布的哈希值。而当 key 的分布存在某种周期性时,对合数取模更容易出现聚集现象。
总而言之,我们通常选取质数作为模数,并且这个质数最好足够大,以尽可能消除周期性模式,提升哈希算
法的稳健性。
常见哈希算法
小结
重点回顾