哈希表又称散列表,其通过建立键key和值value之间的映射以实现高效的元素查询。与数组和链表对比,其添加元素、查询元素和删除元素的时间均为$O(1)$.

哈希表常用操作

初始化、查询操作、添加键值对和删除键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 初始化哈希表
unordered_maps<int,string>map;

// 在哈希表中添加键值对
map[12345]="凉宫";
map[12346]="长门";
map[15532]="朝比奈";
map[11451]="阿虚";
map[18946]="古泉";

// 查询操作
// 向哈希表中输入键 key ,得到值 value
string name = map[12346];

// 删除操作
// 在哈希表中删除键值对 (key, value)
map.erase(12346);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 初始化哈希表
hmap: dict = {}

# 添加操作
# 在哈希表中添加键值对 (key, value)
hmap[12345]="凉宫"
hmap[12346]="长门"
hmap[15532]="朝比奈"
hmap[11451]="阿虚"
hmap[18946]="古泉"

# 查询操作
# 向哈希表中输入键 key ,得到值 value
name: str = hmap[15937]

# 删除操作
# 在哈希表中删除键值对 (key, value)
hmap.pop(10583)

遍历

哈希表有三种常用的遍历方式:遍历键值对、遍历键和遍历值

1
2
3
4
5
6
7
8
9
/* 遍历哈希表 */
// 遍历键值对 key->value
for (auto kv: map) {
cout << kv.first << " -> " << kv.second << endl;
}
// 使用迭代器遍历 key->value
for (auto iter = map.begin(); iter != map.end(); iter++) {
cout << iter->first << "->" << iter->second << endl;
}
1
2
3
4
5
6
7
8
9
10
11
# 遍历键值对
for key,value in hmap.items():
print(key,"->",value)

# 遍历键
for key in hmap.keys():
print(key)

# 遍历值
for value in hmap.values():
print(value)

注:C语言未提供内置的哈希表类型

哈希表的实现

在哈希表中,我们将数组中的每个空位称为,每个桶可存储一个键值对。因此,查询操作就是找到键对应的桶,并在桶中获取值。通过键定位到桶是通过哈希函数实现的。

哈希函数的计算过程一般分为如下两步:

  1. 将键值通过某种哈希算法hash()计算得到哈希值
  2. 将哈希值对桶数量取模,从而获取键值对应的桶,即存储该哈希表的数组的索引。
    1
    index = hash(key) % capacity

如下代码实现了一个简单哈希表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
typedef struct{
int key;
char *val;
}Pair;

typedef struct{
Pair *buckets{MAX_SIZE};
}ArrayHashMap;

// 构造函数
ArrayHashMap *newArrayHashMap(){
ArrayHashMap *hmap=malloc(sizeof(ArrayHashMap));
for(int i=0;i<MAX_SIZE;i++){
hmap->buckets[i]=NULL;
}
}return hmap;

// 析构函数
void delArrayHashMap(ArrayHashMap *hmap){
for(int i=0;i<MAX_SIZE;i++){
if(hmap->buckets[i]!=NULL){
free(hmap->buckets[i]->val);
free(hmap->buckets[i]);
}
}
}

// 添加操作
void put(ArrayHashMap *hmap,const int key,const char *val){
Pair*Pair=malloc(sizeof(Pair));
Pair->key=key;
Pair->val=malloc(strlen(val+1)+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 < MAX_SIZE; i++) {
if (hmap->buckets[i] != NULL) {
total++;
}
}
entries = malloc(sizeof(Pair) * total);
for (i = 0; i < MAX_SIZE; 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 < MAX_SIZE; i++) {
if (hmap->buckets[i] != NULL) {
total++;
}
}
keys = malloc(total * sizeof(int));
for (i = 0; i < MAX_SIZE; 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 < MAX_SIZE; i++) {
if (hmap->buckets[i] != NULL) {
total++;
}
}
vals = malloc(total * sizeof(char *));
for (i = 0; i < MAX_SIZE; 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;
parSet(hmap,&set);
Pair *entries=(Pair*)set.set;
for(i=0;i<set.len;i++){
printf("%d -> %s\n",entries[i].key,entries.val);
}
free(set.set);
}

哈希冲突与扩容

从本质上看,哈希函数的作用是将所有键构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,理论上一定存在“多个输入对应相同输出”的情况,这种情况我们称之为哈希冲突。(哈希冲突是不可避免的)

易知哈希表容量越大,多个 key 被分配到同一个桶中的概率就越低,冲突就越少。故可以通过扩容哈希表来减少哈希冲突。

负载因子(load factor)是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件。

哈希冲突会导致查询结果错误,严重影响哈希表的可用性。可在发生冲突时进行扩容,直到冲突消失,但是由于哈希表扩容需要进行大量的数据搬运和哈希值计算使得该方法效率较低。故有以下两种思路改良哈希表结构:

  1. 改良结构,使得哈希表可以在出现哈希冲突时正常工作。
  2. 仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。

而改良结构的方法又分为“链式地址”和“开放寻址”两种。

链式地址

在原始哈希表中,每个桶仅能存储一个键值对。链式地址(separate chaining)将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。

此时对链表的操作发生如下变化:

  1. 查询元素:输入 key ,经过哈希函数得到桶索引,即可访问链表头节点,然后遍历链表并对比 key 以查找目标键值对。
  2. 添加元素:首先通过哈希函数访问链表头节点,然后将节点(键值对)添加到链表中。
  3. 删除元素:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标节点并将其删除。

链式地址有占用空间增大(因为链表包含节点指针)和查询效率降低(因为需要线性遍历链表来查找对应元素)的缺陷。

如下代码是链式地址哈希表的一个简单实现,其使用列表(动态数组)代替链表,从而简化代码,且以下实现包含哈希表扩容方法(当负载因子超过2/3时将哈希表扩容至原先的2倍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
typedef struct Node {
Pair *pair; // Pair的实现在上文提及
struct Node *next;
} Node;

typedef struct {
int size; // 键值对数量
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->entendRatio=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);
Node *cur=hashMap->buckets[index];
while(cur){
if(cur->pair->key==key){
return cur->pair->val;
}
cur=cur->next;
}
return "";
}

// 添加操作
void put(HashMapChaining *hasMap,int key,const char *val){
if(loadFactor(hashMap)>hashMap->loadThres){
extend(hashMap);
}
int index=hashFunc(hashMap,key);
Node *cur=hashMap->buckets[index];
while(cur){
// 若遇到指定 key ,则更新对应 val 并返回
if(cur->pair->key==key){
strcpy(cur->pair->val,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->bucksts[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");
}
}

值得注意的是,当链表很长时,查询效率$O(n)$很差。此时可以将链表转换为“AVL树”或“红黑树”,从而将查询操作的时间复杂度优化至$O(\log n)$。

开放寻址

开放寻址不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等。

线性探测为例,介绍开放寻址哈希表的工作机制:

  1. 插入元素:通过哈希函数计算桶索引,若发现桶内已有元素,则从冲突位置向后线性遍历,直至找到空桶,将元素插入其中。
  2. 查找元素:若发现哈希冲突,则使用相同步长向后进行线性遍历,直到找到对应元素,返回value即可;如果遇到空桶,说明目标元素不在哈希表中,返回None。

但是线性探测容易产生“聚集现象”。数组中连续被占用的位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促使该位置的聚堆生长,导致增删查改操作效率劣化。而且不能在开放寻址哈希表中直接删除元素。这是因为删除元素会在数组内产生一个空桶 None ,而当查询元素时,线性探测到该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序可能误判这些元素不存在。

为了解决该问题,我们可以采用懒删除(lazy deletion)机制:它不直接从哈希表中移除元素,而是利用一个常量 TOMBSTONE 来标记这个桶。在该机制下,None 和 TOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 TOMBSTONE 时应该继续遍历,因为其之下可能还存在键值对。然而,懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记,随着 TOMBSTONE 的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 TOMBSTONE 才能找到目标元素。为此,考虑在线性探测中记录遇到的首个 TOMBSTONE 的索引,并将搜索到的目标元素与该 TOMBSTONE 交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从而优化查询效率。以下代码实现了一个包含懒删除的开放寻址(线性探测)哈希表。为了更加充分地使用哈希表的空间,我们将哈希表看作一个“环形数组”,当越过数组尾部时,回到头部继续遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/* 开放寻址哈希表 */
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 **)calloc(hashMap->capacity, sizeof(Pair *));
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);
}
}
free(hashMap->buckets);
free(hashMap->TOMBSTONE);
free(hashMap);
}

/* 哈希函数 */
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 **)calloc(hashMap->capacity, sizeof(Pair *));
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);
}
}
}

平方探测与线性探测类似,都是开放寻址的常见策略之一。当发生冲突时,平方探测不是简单地跳过一个固定的步数,而是跳过“探测次数的平方”的步数,即1,4,9,……步。其相较于线性探测能通过跳过探测次数平方的距离,试图缓解线性探测的聚集效应和使数据分布得更加均匀的作用。

但其仍然存在聚集现象,即某些位置比其他位置更容易被占用。且由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它。

多次哈希
多次哈希方法使用多个哈希函数进行探测。若哈希函数1出现冲突,则尝试下一个,以此类推,直到找到空位后插入元素。与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会带来额外的计算量。且不能直接删除元素的问题依然存在。

哈希算法

键值对的分布情况由哈希函数决定。为了降低哈希冲突的发生概率,我们应当将注意力集中在哈希算法 hash() 的设计上。哈希算法应具备以下特点:

  1. 确定性 对于相同的输入,哈希算法应始终产生相同的输出(以确保其可靠性)。
  2. 效率高 计算哈希值的过程应该足够快。计算开销越小,哈希表的实用性越高。
  3. 均匀分布 哈希算法应使得键值对均匀分布在哈希表中。分布越均匀,哈希冲突的概率就越低。

哈希算法除了可以用于实现哈希表,还广泛应用于其他领域中。,如

  1. 密码存储 为了保护用户密码的安全,系统通常不会直接存储用户的明文密码,而是存储密码的哈希值。当用户输入密码时,系统会对输入的密码计算哈希值,然后与存储的哈希值进行比较。
  2. 数据完整性检查 数据发送方可以计算数据的哈希值并将其一同发送;接收方可以重新计算接收到的数据的哈希值,并与接收到的哈希值进行比较。如果两者匹配,那么数据就被视为完整。

为了防止从哈希值推导出原始密码等逆向工程,哈希算法需要具备更高等级的安全特性:

  1. 单向性 无法通过哈希值反推出关于输入数据的任何信息
  2. 抗碰撞性 应当极难找到两个不同的输入,使得它们的哈希值相同
  3. 雪崩效应 输入的微小变化应当导致输出的显著且不可预测的变化

自行设计哈希算法时最后一步常常是对一个大素数取模以使哈希值在一个合适范围内,这是因为使用大质数作为模数,可以最大化地保证哈希值的均匀分布。当 key 的分布存在某种周期性时,对合数取模更容易出现聚集现象。

注:本文是作者阅读《hello算法》一书的读书笔记,内容会与原书有重合,特此声明。