From 3008ac7e0a1428c4e0477403b6b1c31fba29b9b2 Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Wed, 5 Sep 2018 19:21:50 +0800 Subject: [PATCH 01/10] add 01-introduction chinese translation --- .translations/zh-cn/01-introduction/README.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .translations/zh-cn/01-introduction/README.md diff --git a/.translations/zh-cn/01-introduction/README.md b/.translations/zh-cn/01-introduction/README.md new file mode 100644 index 0000000..b9bad76 --- /dev/null +++ b/.translations/zh-cn/01-introduction/README.md @@ -0,0 +1,90 @@ +# 介绍 + +A hash table is a data structure which offers a fast implementation of the +associative array [API](#api). As the terminology around hash tables can be +confusing, I've added a summary +哈希表是一种数据结构,它提供了关联数组 [API](#api) 的快速实现,关于哈希表的相关知识大家可能会感觉困惑和不解,没关系,我在[下面](#摘要).添加了一些摘要 + +A hash table consists of an array of 'buckets', each of which stores a key-value +pair. In order to locate the bucket where a key-value pair should be stored, the +key is passed through a hashing function. This function returns an integer which +is used as the pair's index in the array of buckets. When we want to retrieve a +key-value pair, we supply the key to the same hashing function, receive its +index, and use the index to find it in the array. +哈希表是由一系列的"桶"组成,每个桶都存储一组键值对。为了确定该键值对的桶应该放置在哪个位置, +我们可以通过将键值对的键作为参数传递到哈希函数中,然后此哈希函数生成一个整型索引值作为数组索引, +当需要检索数据时,也是同样的通过此哈希函数生成相应的索引来定位此键值对存放的位置。 + +Array indexing has algorithmic complexity `O(1)`, making hash tables fast at +storing and retrieving data. +由于数组的复杂度为 `O(1)`,使得哈希表能够快速的存储和检索数据 + +Our hash table will map string keys to string values, but the principals +given here are applicable to hash tables which map arbitrary key types to +arbitrary value types. Only ASCII strings will be supported, as supporting +unicode is non-trivial and out of scope of this tutorial. +以下将要实现的哈希表仅仅是存储字符类型的键值对, 其实原则上应该是实现任意类型的键值对, +但由于支持 unicode 并不简单而且超出了本教程的范围,所以本教程只支持 ASCII 字符串。 + +## API + +Associative arrays are a collection of unordered key-value pairs. Duplicate keys +are not permitted. The following operations are supported: +关联数组是无序键值对的集合,故重复的键是无效的,且支持以下的操作: + +- `search(a, k)`: 数组 `a` 中通过键 `k` 返回值 `v`,如果该键对应的值不存在,则返回 NULL +- `insert(a, k, v)`: 在数组 `a` 中存储键值对 `k:v` +- `delete(a, k)`: 通过键 `k` 删除数组 `a` 中对应的键值对,若键不存在,则不做任何操作 + +## 起始 + +To set up C on your computer, please consult [Daniel Holden's](@orangeduck) +guide in the [Build Your Own +Lisp](http://www.buildyourownlisp.com/chapter2_installation) book. Build Your +Own Lisp is a great book, and I recommend working through it. +由于本教程是由 C 编写的,如果你还不清楚 C,可以参考 [Daniel Holden's](@orangeduck) 编写的 +《[Build Your Own Lisp](http://www.buildyourownlisp.com/chapter2_installation)》, +这是一本不错的书籍,如果可以的话,我建议你读一读 + +## 代码结构 + +Code should be laid out in the following directory structure. +代码结构由以下目录和文件组成 + +``` +. +├── build +└── src + ├── hash_table.c + ├── hash_table.h + ├── prime.c + └── prime.h +``` + +`src` will contain our code, `build` will contain our compiled binaries. +`src` 文件夹里放置我们的代码,`build` 文件夹放置编译后的二进制文件 + +## 摘要 + +There are lots of names which are used interchangeably. In this article, we'll +use the following: +很多名词都有很多种不同的叫法,在本文中,我们统一用以下命名: + +- Associative array: an abstract data structure which implements the + [API](#api) described above. Also called a map, symbol table or + dictionary. +- 关联数组: 实现以上 [API](#api) 的一种抽象的数据结构。也称为映射,符号表,字典 + +- Hash table: a fast implementation of the associative array API which makes + use of a hash function. Also called a hash map, map, hash or + dictionary. +- 哈希表: 通过散列函数实现的快速存储与检索的关联数组。也称为哈希映射,映射,哈希或字典。 + +Associative arrays can be implemented with many different underlying data +structures. A (non-performant) one can be implemented by simply storing items in +an array, and iterating through the array when searching. Associative arrays and +hash tables are often confused because associative arrays are so often +implemented as hash tables. + +Next section: [Hash table structure](/02-hash-table) +[Table of contents](https://github.com/jamesroutley/write-a-hash-table#contents) From 361a8a3d352e26e622b79585356a9f7744f7d6be Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Wed, 5 Sep 2018 23:08:55 +0800 Subject: [PATCH 02/10] add 02-hash-table translation --- .translations/zh-cn/01-introduction/README.md | 73 ++++--------- .translations/zh-cn/02-hash-table/README.md | 101 ++++++++++++++++++ .translations/zh-cn/README.md | 0 3 files changed, 121 insertions(+), 53 deletions(-) create mode 100644 .translations/zh-cn/02-hash-table/README.md create mode 100644 .translations/zh-cn/README.md diff --git a/.translations/zh-cn/01-introduction/README.md b/.translations/zh-cn/01-introduction/README.md index b9bad76..697e3ef 100644 --- a/.translations/zh-cn/01-introduction/README.md +++ b/.translations/zh-cn/01-introduction/README.md @@ -1,54 +1,33 @@ # 介绍 -A hash table is a data structure which offers a fast implementation of the -associative array [API](#api). As the terminology around hash tables can be -confusing, I've added a summary -哈希表是一种数据结构,它提供了关联数组 [API](#api) 的快速实现,关于哈希表的相关知识大家可能会感觉困惑和不解,没关系,我在[下面](#摘要).添加了一些摘要 - -A hash table consists of an array of 'buckets', each of which stores a key-value -pair. In order to locate the bucket where a key-value pair should be stored, the -key is passed through a hashing function. This function returns an integer which -is used as the pair's index in the array of buckets. When we want to retrieve a -key-value pair, we supply the key to the same hashing function, receive its -index, and use the index to find it in the array. -哈希表是由一系列的"桶"组成,每个桶都存储一组键值对。为了确定该键值对的桶应该放置在哪个位置, -我们可以通过将键值对的键作为参数传递到哈希函数中,然后此哈希函数生成一个整型索引值作为数组索引, +哈希表是根据关联数组的 [Api](API) 实现的一种快速存储与检索数据的数据结构, +为了让大家更好的阅读本文,我在文章底部的[摘要](摘要)添加了一些名词的解析。 + +哈希表是由一系列的"桶"组成,每个桶都存储一组键值对。为了确定存储键值对的桶应该放置在哪个位置, +我们可以通过将键值对的键作为参数传递到哈希函数中,然后此哈希函数生成一个整型索引值作为数组的索引, 当需要检索数据时,也是同样的通过此哈希函数生成相应的索引来定位此键值对存放的位置。 -Array indexing has algorithmic complexity `O(1)`, making hash tables fast at -storing and retrieving data. -由于数组的复杂度为 `O(1)`,使得哈希表能够快速的存储和检索数据 +由于数组的存取复杂度均为 `O(1)`,而哈希表是借助数组实现的,所以哈希表具有快速存储和检索数据的特性。 -Our hash table will map string keys to string values, but the principals -given here are applicable to hash tables which map arbitrary key types to -arbitrary value types. Only ASCII strings will be supported, as supporting -unicode is non-trivial and out of scope of this tutorial. -以下将要实现的哈希表仅仅是存储字符类型的键值对, 其实原则上应该是实现任意类型的键值对, -但由于支持 unicode 并不简单而且超出了本教程的范围,所以本教程只支持 ASCII 字符串。 +本篇文章将要实现的哈希表只是存储字符类型的键值对, 原则上应该是实现任意类型的键值对, +但由于支持 unicode 并不简单而且超出了本票文章的范围,所以此处数据类型只支持 ASCII ## API -Associative arrays are a collection of unordered key-value pairs. Duplicate keys -are not permitted. The following operations are supported: -关联数组是无序键值对的集合,故重复的键是无效的,且支持以下的操作: +关联数组是一组无序键值对的集合,故重复的键是无效的,且支持以下的操作: -- `search(a, k)`: 数组 `a` 中通过键 `k` 返回值 `v`,如果该键对应的值不存在,则返回 NULL -- `insert(a, k, v)`: 在数组 `a` 中存储键值对 `k:v` -- `delete(a, k)`: 通过键 `k` 删除数组 `a` 中对应的键值对,若键不存在,则不做任何操作 +- `search(a, k)`: 关联数组 `a` 中通过键 `k` 返回值 `v`,如果该键对应的值不存在,则返回 NULL +- `insert(a, k, v)`: 在关联数组 `a` 中插入键值对 `k:v` +- `delete(a, k)`: 通过键 `k` 删除关联数组 `a` 中对应的键值对,若键不存在,则不做任何操作 ## 起始 -To set up C on your computer, please consult [Daniel Holden's](@orangeduck) -guide in the [Build Your Own -Lisp](http://www.buildyourownlisp.com/chapter2_installation) book. Build Your -Own Lisp is a great book, and I recommend working through it. -由于本教程是由 C 编写的,如果你还不清楚 C,可以参考 [Daniel Holden's](@orangeduck) 编写的 +由于本文是由 C 编写的,如果你还不清楚 C,可以参考 [Daniel Holden's](@orangeduck) 编写的 《[Build Your Own Lisp](http://www.buildyourownlisp.com/chapter2_installation)》, -这是一本不错的书籍,如果可以的话,我建议你读一读 +这是一本非常不错的书籍,如果可以的话,我建议你读一读 ## 代码结构 -Code should be laid out in the following directory structure. 代码结构由以下目录和文件组成 ``` @@ -61,30 +40,18 @@ Code should be laid out in the following directory structure. └── prime.h ``` -`src` will contain our code, `build` will contain our compiled binaries. `src` 文件夹里放置我们的代码,`build` 文件夹放置编译后的二进制文件 ## 摘要 -There are lots of names which are used interchangeably. In this article, we'll -use the following: -很多名词都有很多种不同的叫法,在本文中,我们统一用以下命名: +由于很多技术名词都有各种不同的叫法,为了便于大家的理解,在本文中,我们统一用以下命名: -- Associative array: an abstract data structure which implements the - [API](#api) described above. Also called a map, symbol table or - dictionary. -- 关联数组: 实现以上 [API](#api) 的一种抽象的数据结构。也称为映射,符号表,字典 +- 关联数组: 通过 [API](#api) 实现的一种抽象的数据结构。也称为映射,符号表,字典 -- Hash table: a fast implementation of the associative array API which makes - use of a hash function. Also called a hash map, map, hash or - dictionary. - 哈希表: 通过散列函数实现的快速存储与检索的关联数组。也称为哈希映射,映射,哈希或字典。 -Associative arrays can be implemented with many different underlying data -structures. A (non-performant) one can be implemented by simply storing items in -an array, and iterating through the array when searching. Associative arrays and -hash tables are often confused because associative arrays are so often -implemented as hash tables. +关联数组可以用许多不同的底层数据结构实现,一种比较低性能且较为简单的方法是把元素直接存储在数组中, +在检索的时候逐个遍历对比。关联数组和散列表经常被混淆,因为散列表通常都是通过关联数组来实现。 -Next section: [Hash table structure](/02-hash-table) -[Table of contents](https://github.com/jamesroutley/write-a-hash-table#contents) +下一节: [哈希表结构](../02-hash-table) +[目录](/.translations/zh-cn/README.md#contents) diff --git a/.translations/zh-cn/02-hash-table/README.md b/.translations/zh-cn/02-hash-table/README.md new file mode 100644 index 0000000..76c5410 --- /dev/null +++ b/.translations/zh-cn/02-hash-table/README.md @@ -0,0 +1,101 @@ +# 哈希表结构 + +哈希的元素存储在以下的 `ht_item` 结构, `key` 代表元素的键, `value` 代表元素的值,且均为字符类型: + +```c +// hash_table.h +typedef struct { + char* key; + char* value; +} ht_item; +``` + +下面是哈希表的结构, `items` 是指向 `ht_item` 结构的指针数组,`size` 则存储哈希表的大小,即哈希的长度 +`count` 表示当前哈希表当前已存储了多少个键值对: + +```c +// hash_table.h +typedef struct { + int size; + int count; + ht_item** items; +} ht_hash_table; +``` + +## 哈希的初始化和删除 + +首先,我们需要定义一个初始化 `ht_item` 结构的方法,并分配一块大小为 `ht_item` 的内存, +并存储字符串 `k` 和 `v` 的副本,该方法的开头需要标记为 `static` 静态方法,是为了该方法 +只供在本文件内调用 + +```c +// hash_table.c +#include +#include + +#include "hash_table.h" + +static ht_item* ht_new_item(const char* k, const char* v) { + ht_item* i = malloc(sizeof(ht_item)); + i->key = strdup(k); + i->value = strdup(v); + return i; +} +``` + +`ht_new` 方法用于初始化一个哈希表结构。 `size` 定义了该结构能存储多少元素,当前暂且 +只分配 53 的存储大小,在后面的章节中,我们会提到 [resizing](../06-resizing) 来动态调整哈希表的 +存储大小。然后我们使用 `calloc` 方法来初始化元素数组,这里用 `calloc` 而不用 `malloc` 是因为 +`calloc` 所分配的内存空间的每一位都会初始化为 `NULL`,而 `NULL` 则表示该位置未存储元素 + +```c +// hash_table.c +ht_hash_table* ht_new() { + ht_hash_table* ht = malloc(sizeof(ht_hash_table)); + + ht->size = 53; + ht->count = 0; + ht->items = calloc((size_t)ht->size, sizeof(ht_item*)); + return ht; +} +``` + +当然,为了防止[内存泄漏](https://en.wikipedia.org/wiki/Memory_leak),我们需要定义 `ht_item` +和 `ht_hash_tables` 的删除方法来进行内存的回收 + +```c +// hash_table.c +static void ht_del_item(ht_item* i) { + free(i->key); + free(i->value); + free(i); +} + + +void ht_del_hash_table(ht_hash_table* ht) { + for (int i = 0; i < ht->size; i++) { + ht_item* item = ht->items[i]; + if (item != NULL) { + ht_del_item(item); + } + } + free(ht->items); + free(ht); +} +``` + +在本章节中,我们定义了如何初始化或删除一个哈希表,虽然实现的功能并不多,但为了保证后面程序的正常运行, +需要保证编写的方法不会报错,故可以照着以下的示例代码进行调试 + +```c +// main.c +#include "hash_table.h" + +int main() { + ht_hash_table* ht = ht_new(); + ht_del_hash_table(ht); +} +``` + +下一节: [哈希的方法](../03-hashing) +[目录](/.translations/zh-cn/README.md#contents) diff --git a/.translations/zh-cn/README.md b/.translations/zh-cn/README.md new file mode 100644 index 0000000..e69de29 From 1fa8a4f9ad385a3843a340e103d21ed965089b01 Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Thu, 6 Sep 2018 13:30:07 +0800 Subject: [PATCH 03/10] add 04-collisions translation --- .translations/zh-cn/04-collisions/README.md | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .translations/zh-cn/04-collisions/README.md diff --git a/.translations/zh-cn/04-collisions/README.md b/.translations/zh-cn/04-collisions/README.md new file mode 100644 index 0000000..bbb2400 --- /dev/null +++ b/.translations/zh-cn/04-collisions/README.md @@ -0,0 +1,42 @@ +## 哈希碰撞 + +上节提到的散列函数若通过输入无限的数据映射到有限数量的输出,必然会产生同样的索引值,所以也就导致了 +存储桶的索引碰撞,所以我们需要有好的方案去处理这些冲突。 + +显然,已经有很多人遇到了这个问题,所以也有很多方案可以处理这些情况,对于其它处理碰撞的方法,就不在这里赘述,请查看[附录](../07-appendix) + +在本文中我们将使用一种称为开放寻址和再哈希的技术来处理冲突。也就是说,在发生碰撞后,再哈希会使用两个哈希函数来计算 `i` 出现碰撞后的索引值 + +## 再哈希 + +在 `i` 发生碰撞后的索引值可由以下公式算出: + +``` +index = hash_a(string) + i * hash_b(string) % num_buckets +``` + +通过以上式子我们可以发现,如果没有发生任何碰撞,即 `i = 0`,所以哈希的索引值仅仅只是 `hash_a` 的结果。 +如果碰撞发生了,即 i != 0,结果很明显会根据 `hash_b` 的返回结果进行变化 + +很显然,如果 `hash_b` 的返回结果为 0 将导致第二项的结果也为 0,哈希表会一遍又一遍的尝试插入到同一个位置, +那么我们的这项处理就毫无意义了,所以,我们可以通过在 `hash_b` 的结果后面加 1 来避免这样的情况。 + +``` +index = (hash_a(string) + i * (hash_b(string) + 1)) % num_buckets +``` + +## 代码实现 + +```c +// hash_table.c +static int ht_get_hash( + const char* s, const int num_buckets, const int attempt +) { + const int hash_a = ht_hash(s, HT_PRIME_1, num_buckets); + const int hash_b = ht_hash(s, HT_PRIME_2, num_buckets); + return (hash_a + (attempt * (hash_b + 1))) % num_buckets; +} +``` + +下一节: [哈希表的函数](../05-methods) +[Table of contents](https://github.com/jamesroutley/write-a-hash-table#contents) From 15463540946599a4df02b58590e1e5f1688549bd Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Thu, 6 Sep 2018 23:24:39 +0800 Subject: [PATCH 04/10] add readme translation --- .../{zh-cn => cn}/01-introduction/README.md | 0 .../{zh-cn => cn}/02-hash-table/README.md | 2 +- .translations/cn/03-hashing/README.md | 83 ++++++++++++++++++ .../{zh-cn => cn}/04-collisions/README.md | 2 +- .translations/cn/README.md | 36 ++++++++ .translations/flags/cn.png | Bin 0 -> 1299 bytes .translations/zh-cn/README.md | 0 7 files changed, 121 insertions(+), 2 deletions(-) rename .translations/{zh-cn => cn}/01-introduction/README.md (100%) rename .translations/{zh-cn => cn}/02-hash-table/README.md (98%) create mode 100644 .translations/cn/03-hashing/README.md rename .translations/{zh-cn => cn}/04-collisions/README.md (95%) create mode 100644 .translations/cn/README.md create mode 100644 .translations/flags/cn.png delete mode 100644 .translations/zh-cn/README.md diff --git a/.translations/zh-cn/01-introduction/README.md b/.translations/cn/01-introduction/README.md similarity index 100% rename from .translations/zh-cn/01-introduction/README.md rename to .translations/cn/01-introduction/README.md diff --git a/.translations/zh-cn/02-hash-table/README.md b/.translations/cn/02-hash-table/README.md similarity index 98% rename from .translations/zh-cn/02-hash-table/README.md rename to .translations/cn/02-hash-table/README.md index 76c5410..4523a24 100644 --- a/.translations/zh-cn/02-hash-table/README.md +++ b/.translations/cn/02-hash-table/README.md @@ -97,5 +97,5 @@ int main() { } ``` -下一节: [哈希的方法](../03-hashing) +下一节: [哈希函数](../03-hashing) [目录](/.translations/zh-cn/README.md#contents) diff --git a/.translations/cn/03-hashing/README.md b/.translations/cn/03-hashing/README.md new file mode 100644 index 0000000..e0b7eac --- /dev/null +++ b/.translations/cn/03-hashing/README.md @@ -0,0 +1,83 @@ +# 哈希函数 + +在这一节中,将编写用于计算索引值的哈希函数。 + +哈希函数应该具有以下特征: + +- 输入一个字符串,返回一个 `0` - `m` 的整型数字, `m` 必须不大于哈希的长度,即上一节中 `ht_hash_table` 的 `size` + +- 计算的索引值必须均匀分布,如果计算的索引值分布不均匀,会导致大多数键都落在同一索引,从而在哈希表插入过程中将会产生大量[碰撞](#Pathological data),大大的降低性能 + +## 算法 + +我们将使用泛型字符串散列函数,下面是该算法的伪代码。 + +``` +function hash(string, a, num_buckets): + hash = 0 + string_len = length(string) + for i = 0, 1, ..., string_len: + hash += (a ** (string_len - (i+1))) * char_code(string[i]) + hash = hash % num_buckets + return hash +``` + +从上面的函数可以看出,要生成一个 hash 值,我们需要做以下两步: + +1. 把字符串转换为整型 +2. 因转换的整型数字可能远远大于哈希表的长度,所以我们需要通过 `mod` 取余数来保证生成的哈希函数不大于哈希表长度 `m` + +变量 `a` 应当是一个大于字母表大小的素数,因为我们在计算字符串的散列值,而字符串的 ASCII 大小为 128, +所以 `a` 的大小应当是大于 128 的素数 + +`char_code` 是将单字符转换为整数的方法 + +根据上面的描述,可以举个例子计算一下: + +``` +hash("cat", 151, 53) + +hash = (151**2 * 99 + 151**1 * 97 + 151**0 * 116) % 53 +hash = (2257299 + 14647 + 116) % 53 +hash = (2272062) % 53 +hash = 5 +``` + +通过改变变量 `a` 的值,我们可以得到不同的计算结果 + +``` +hash("cat", 163, 53) = 3 +``` + +## 代码实现 + +```c +// hash_table.c +static int ht_hash(const char* s, const int a, const int m) { + long hash = 0; + const int len_s = strlen(s); + for (int i = 0; i < len_s; i++) { + hash += (long)pow(a, len_s - (i+1)) * s[i]; + hash = hash % m; + } + return (int)hash; +} +``` + +## 病理数据 + +理想的散列函数无论输入什么键,其返回值都是均匀分布的。然而,对于任意的哈希函数,都不会生成每个键唯一的哈希值, +肯定会出现一些 `奇怪` 的键,这些键生成的哈希值往往会占用其它键的哈希值。 + +一系列的病理数据集合意味着没有完美的哈希函数可以生成唯一的哈希值。所以最好的办法就是 +创建一个函数来处理这些可能会出现的数据 + +Pathological inputs also poses a security issue. If a hash table is fed a set of +colliding keys by some malicious user, then searches for those keys will take +much longer (`O(n)`) than normal (`O(1)`). This can be used as a denial of +service attack against systems which are underpinned by hash tables, such as DNS +and certain web services. + + +Next section: [处理哈希碰撞](../04-collisions) +[目录](/.translations/zh-cn/README.md#contents) diff --git a/.translations/zh-cn/04-collisions/README.md b/.translations/cn/04-collisions/README.md similarity index 95% rename from .translations/zh-cn/04-collisions/README.md rename to .translations/cn/04-collisions/README.md index bbb2400..979266c 100644 --- a/.translations/zh-cn/04-collisions/README.md +++ b/.translations/cn/04-collisions/README.md @@ -39,4 +39,4 @@ static int ht_get_hash( ``` 下一节: [哈希表的函数](../05-methods) -[Table of contents](https://github.com/jamesroutley/write-a-hash-table#contents) +[目录](/.translations/zh-cn/README.md#contents) diff --git a/.translations/cn/README.md b/.translations/cn/README.md new file mode 100644 index 0000000..288f77b --- /dev/null +++ b/.translations/cn/README.md @@ -0,0 +1,36 @@ +[](/README.md) [](/.translations/fr/README.md) [](/.translations/zh-cn/README.md) + +# 哈希表的实现(C) + +[哈希表](https://en.wikipedia.org/wiki/Hash_table) 是一种最常用的数据结构。 由于哈希表的快速存储以及插入,搜索,删除的可扩展,使得其在计算机领域有着广泛的应用 + +在本篇教程中,我们将用 C 实现基于[开放寻址](https://en.wikipedia.org/wiki/Open_addressing)以及[再哈希](https://en.wikipedia.org/wiki/Double_hashing)的哈希表。 +通过这篇教程,你可以知道: + +- 基础数据结构是如何在底层工作的 +- 深入了解到什么时候使用哈希表,而什么时候应避免使用哈希表以及为什么不能用 +- 对 C 或许会有新的认知 + +C 非常适合用于编写哈希表,主要是因为: + +- C 不需要引入任何东西就可以直接运行 +- 属于底层语言,所以可以深入的了解到在机器层面上的程序是如何运作的 + +本教程是假设你已熟悉 C 的语法。由于代码本身相对简单,大多数问题都可以通过搜索引擎来解决。 +如果你遇到其它问题,请打开 Github 的 [Issue](https://github.com/yigger/write-a-hash-table/issues) 进行提问 + +整个过程大概需要编写 200 行左右的代码,这大概会耗费你 1 ~ 2 个小时 + +## 目录 + +1. [介绍](./01-introduction) +2. [哈希表数据结构](./02-hash-table) +3. [哈希函数](./03-hashing) +4. [冲突处理](./04-collisions) +5. [哈希表的方法](./05-methods) +6. [调整哈希表大小](./06-resizing) +7. [附录: 处理哈希碰撞的方法与描述](./07-appendix) + +## Credits + +本教程是由 [James Routley](https://twitter.com/james_routley) 编写,博客地址:[routley.io](https://routley.io)。 diff --git a/.translations/flags/cn.png b/.translations/flags/cn.png new file mode 100644 index 0000000000000000000000000000000000000000..ccc90e633bf1d813ead30d53478ea31393fcbba1 GIT binary patch literal 1299 zcmb7EPi)&%826}DMwYCp-EQqx4dX(}SQ^`L?Id2a(%8 zE@@8fc489(Z31!F#07EO0f`d_G{gjxKtde&vnFn;v4Kj_c7OxoP@dbS+hLl7<>&9c zr|8*)M=0FhzmgwPhvz&LwOjkw*4*e8(#QWfuh@zvevj>u*$LlkYPp70X$KZ zXFwXr%F@CeFo+;688wr&vdK|CuNMQdqZ6nUOAw79gF}^)oSy?0o&z(g79}2j{sn=n zN|cyjlT@-KfLV25^*?!!G5UGZ!T$;Th2AC@=I+^s97X62|E8hBqR8{2o?k2i%}R(-A{^u5kzhO)rf4WBAr$8Xi4g{Z zRB$*#adZ>=BnGkt6jkA=5G#cu)If}u!ZchlHWC!W3>ykbOps-TCN`m&maOH0W6!~= zkFm@%vAkda+0u=SuFp3sKQ^mdx;d+taABOoCsa+*%cfJ5oft)6s26||H}oRDomF0a ziuJfebBq`lrO=N3FeeBxN`hI4#DoYJBX+UM|H%{yogkf7c%n6$SFqol?Ovn9!``R> z4bF!F2SGT+RN)Za85_+=_N(aHiB8++wU1-=YpC6a)_c*~QEbhR+Q-p4fz@%W{t{a6 z!)nJc+lM}wI=J;{pWTNw!rn($T^qW4wFA5NhPVDA)?nKIZM3Xn9SyGi&npKWUUmJN z@~)9+?FeSSg5EvQ@w@72u^&xMq8~fvuyD@;?Ki5_#Q2PXQ09#|xzizhP zKil?mOJ}VMt3leqcH58s;_6uK#vV+!ZEYbNRZrcAt`BIO=dN-s<+B*|Tw(u5nIso<^?d$m4-LtuQx#pj|@Ppso zwfxbok@ue;|8V8n@HNjBWAZuQ=JHDWNn}5T^c7yX@%lRjuk)KqNGb7V?EJ<50OviX ATmS$7 literal 0 HcmV?d00001 diff --git a/.translations/zh-cn/README.md b/.translations/zh-cn/README.md deleted file mode 100644 index e69de29..0000000 From 58a88ed9e041fa042a4084c14605acbca03bc46b Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Fri, 7 Sep 2018 13:43:56 +0800 Subject: [PATCH 05/10] add 05-methods translation --- .translations/cn/01-introduction/README.md | 2 +- .translations/cn/02-hash-table/README.md | 2 +- .translations/cn/03-hashing/README.md | 2 +- .translations/cn/04-collisions/README.md | 2 +- .translations/cn/05-methods/README.md | 156 +++++++++++++++++++++ .translations/cn/README.md | 4 +- 6 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 .translations/cn/05-methods/README.md diff --git a/.translations/cn/01-introduction/README.md b/.translations/cn/01-introduction/README.md index 697e3ef..93ffb33 100644 --- a/.translations/cn/01-introduction/README.md +++ b/.translations/cn/01-introduction/README.md @@ -54,4 +54,4 @@ 在检索的时候逐个遍历对比。关联数组和散列表经常被混淆,因为散列表通常都是通过关联数组来实现。 下一节: [哈希表结构](../02-hash-table) -[目录](/.translations/zh-cn/README.md#contents) +[目录](/.translations/cn/README.md#contents) diff --git a/.translations/cn/02-hash-table/README.md b/.translations/cn/02-hash-table/README.md index 4523a24..b26c2e7 100644 --- a/.translations/cn/02-hash-table/README.md +++ b/.translations/cn/02-hash-table/README.md @@ -98,4 +98,4 @@ int main() { ``` 下一节: [哈希函数](../03-hashing) -[目录](/.translations/zh-cn/README.md#contents) +[目录](/.translations/cn/README.md#contents) diff --git a/.translations/cn/03-hashing/README.md b/.translations/cn/03-hashing/README.md index e0b7eac..ff18126 100644 --- a/.translations/cn/03-hashing/README.md +++ b/.translations/cn/03-hashing/README.md @@ -80,4 +80,4 @@ and certain web services. Next section: [处理哈希碰撞](../04-collisions) -[目录](/.translations/zh-cn/README.md#contents) +[目录](/.translations/cn/README.md#contents) diff --git a/.translations/cn/04-collisions/README.md b/.translations/cn/04-collisions/README.md index 979266c..650eb8e 100644 --- a/.translations/cn/04-collisions/README.md +++ b/.translations/cn/04-collisions/README.md @@ -39,4 +39,4 @@ static int ht_get_hash( ``` 下一节: [哈希表的函数](../05-methods) -[目录](/.translations/zh-cn/README.md#contents) +[目录](/.translations/cn/README.md#contents) diff --git a/.translations/cn/05-methods/README.md b/.translations/cn/05-methods/README.md new file mode 100644 index 0000000..94f30bd --- /dev/null +++ b/.translations/cn/05-methods/README.md @@ -0,0 +1,156 @@ +# 哈希表API + +哈希表需要实现以下 API 方法: + +```c +// hash_table.h +void ht_insert(ht_hash_table* ht, const char* key, const char* value); +char* ht_search(ht_hash_table* ht, const char* key); +void ht_delete(ht_hash_table* h, const char* key); +``` + +## 插入 + +为了让一个键值对能插入到哈希表中,我们需要调用哈希函数来生成索引值,但该索引值的对应位置可能已经被使用, +也就是说发生了碰撞,则通过递增 `i` 的值来重新生成索引,直至找到合适的位置存储新增的键值对。 +在插入了新元素后,我们同样需要将哈希表的 `count` 属性加一,这是为了便于[调整哈希表大小](../06-resizing) + +```c +// hash_table.c +void ht_insert(ht_hash_table* ht, const char* key, const char* value) { + ht_item* item = ht_new_item(key, value); + int index = ht_get_hash(item->key, ht->size, 0); + ht_item* cur_item = ht->items[index]; + int i = 1; + while (cur_item != NULL) { + index = ht_get_hash(item->key, ht->size, i); + cur_item = ht->items[index]; + i++; + } + ht->items[index] = item; + ht->count++; +} +``` + +## 查找 + +查找过程跟插入过程差不多,也是通过 `key` 来生成索引值,然后通过该索引值寻找哈希表对应位置的元素, +如果找到的元素的 `key` 与传入的 `key` 一致,则当前元素便是我们需要要查找的结果。 +如果两个 `key` 不相等,则说明 `key` 在插入过程中可能发生了碰撞,需要继续通过 `ht_get_hash` 来生成索引, +然后重复上个过程,直至找到与参数 `key` 一致的元素。 +如果在查询过程中,`index` 索引返回了 `NULL`,则代表当前索引值为空,即元素不存在,结果返回 `NULL` + +```c +// hash_table.c +char* ht_search(ht_hash_table* ht, const char* key) { + int index = ht_get_hash(key, ht->size, 0); + ht_item* item = ht->items[index]; + int i = 1; + while (item != NULL) { + if (strcmp(item->key, key) == 0) { + return item->value; + } + index = ht_get_hash(key, ht->size, i); + item = ht->items[index]; + i++; + } + return NULL; +} +``` + +## 删除 + +删除元素的操作比插入或查找都要复杂。因为我们需要删除的元素的对应索引位置可能是发生了碰撞后的位置, +如果直接删除的话,就会破坏了我们处理碰撞的处理操作,从而无法查询到所有发生碰撞后的元素。 +所以,为了解决这个问题,我们不能删除这个元素,取而代之的是把这个元素做个标记,即软删除。 +(举个例子: `hello` 的索引是 `4`, `world` 的索引也是 `4`,通过碰撞处理函数,`world` 重新分配到了索引为 10 的位置。 +如果直接删除索引为 `4` 的位置,在查询 `world` 的过程中就会因为 `4` 的位置返回 `NULL`,从而结果也就为 `NULL`) + +我们需要定义一个标记位,即 `ht_item HT_DELETED_ITEM = {NULL, NULL}`,然后,把需要删除的元素的指向替换成指向 `HT_DELETED_ITEM` 元素 + +```c +// hash_table.c +static ht_item HT_DELETED_ITEM = {NULL, NULL}; + + +void ht_delete(ht_hash_table* ht, const char* key) { + int index = ht_get_hash(key, ht->size, 0); + ht_item* item = ht->items[index]; + int i = 1; + while (item != NULL) { +        if (item != &HT_DELETED_ITEM) { + if (strcmp(item->key, key) == 0) { + ht_del_item(item); + ht->items[index] = &HT_DELETED_ITEM; + } + } + index = ht_get_hash(key, ht->size, i); + item = ht->items[index]; + i++; + } + ht->count--; +} +``` + +在删除元素后,哈希表的 `count` 属性同样的需要减一 + +由于做了这个操作,必然会影响到之前写的插入与查找操作,我们还需要稍微修改一下 + +在查找的时候,我们将查询到的元素与删除的标记位进行比较,以此来判断该元素是否已经被删除,如果已经删除了, +则跳过该元素,继续寻找下一个。 +在插入过程中,如果新增元素的索引位置恰巧命中了已删除元素的位置,则直接把新元素替换到该位置即可 + + +```c +// hash_table.c +void ht_insert(ht_hash_table* ht, const char* key, const char* value) { + // ... + while (cur_item != NULL && cur_item != &HT_DELETED_ITEM) { + // ... + } + // ... +} + + +char* ht_search(ht_hash_table* ht, const char* key) { + // ... + while (item != NULL) { +        if (item != &HT_DELETED_ITEM) { + if (strcmp(item->key, key) == 0) { + return item->value; + } + } + // ... + } + // ... +} +``` + +## 更新 + +我们的哈希表目前不支持更新操作。因为当前结构不允许存在两个同样的键,假如存在同样的键, +那么在插入过程中肯定会发生碰撞,从而后一个键会插入到后一个位置。 +但是在查找过程中,由于键相同,所以生成的索引值也将是一样的,也就是说,第二个键将永远命中第一个键对应的位置。 + +为了解决这个问题,我们可以直接把待修改的元素替换成新的元素 + +```c +// hash_table.c +void ht_insert(ht_hash_table* ht, const char* key, const char* value) { + // ... + while (cur_item != NULL) { +        if (cur_item != &HT_DELETED_ITEM) { + if (strcmp(cur_item->key, key) == 0) { + ht_del_item(cur_item); + ht->items[index] = item; + return; + } + } + // ... + } + // ... +} +``` + +下一节: [调整哈希表大小](../06-resizing) +[目录](/.translations/cn/README.md#contents) diff --git a/.translations/cn/README.md b/.translations/cn/README.md index 288f77b..e27c0cf 100644 --- a/.translations/cn/README.md +++ b/.translations/cn/README.md @@ -1,8 +1,8 @@ -[](/README.md) [](/.translations/fr/README.md) [](/.translations/zh-cn/README.md) +[](/README.md) [](/.translations/fr/README.md) [](/.translations/cn/README.md) # 哈希表的实现(C) -[哈希表](https://en.wikipedia.org/wiki/Hash_table) 是一种最常用的数据结构。 由于哈希表的快速存储以及插入,搜索,删除的可扩展,使得其在计算机领域有着广泛的应用 +[哈希表](https://en.wikipedia.org/wiki/Hash_table) 是一种最常用的数据结构。由于哈希表具有的快速存储以及可扩展性的特性,使得其在计算机领域有着广泛的应用 在本篇教程中,我们将用 C 实现基于[开放寻址](https://en.wikipedia.org/wiki/Open_addressing)以及[再哈希](https://en.wikipedia.org/wiki/Double_hashing)的哈希表。 通过这篇教程,你可以知道: From f37a9676ec34b074ddf59235915e2c26544dd301 Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Sat, 8 Sep 2018 08:59:41 +0800 Subject: [PATCH 06/10] add 07 appendix translation --- .translations/cn/06-resizing/README.md | 175 +++++++++++++++++++++++++ .translations/cn/07-appendix/README.md | 52 ++++++++ 2 files changed, 227 insertions(+) create mode 100644 .translations/cn/06-resizing/README.md create mode 100644 .translations/cn/07-appendix/README.md diff --git a/.translations/cn/06-resizing/README.md b/.translations/cn/06-resizing/README.md new file mode 100644 index 0000000..ed52636 --- /dev/null +++ b/.translations/cn/06-resizing/README.md @@ -0,0 +1,175 @@ +# 调整哈希表的大小 + +到目前为止,我们的哈希表的大小都是固定在最开始的 `53` 个位置,但随着越来越多的元素被添加进来, +哈希表在有限的空间内将会被填充满,这可能会导致两个问题: + +1. 哈希表的各项性能会随着高冲突率而下降 +2. 哈希表只能存储固定大小的元素,当想要添加更多的元素到哈希表的时候,将会失败 + +如何解决以上问题?很简单,当哈希表即将要满的时候,扩大哈希表的大小就可以了。 +但需要制定策略,什么时候应该扩大,什么时候应该减小。 +这时候,在上一节我们的 `count` 属性就发挥了重要的作用,我们可以通过该属性与 `size` 属性来判断 +当前哈希表的存储状态,将它保存在 `load` 变量中,当满足以下条件时,需要进行调整大小: + +- 增大哈希表的长度, if load > 0.7 +- 减小哈希表的长度, if load < 0.1 + +为了调整大小,我们可以通过创建一个新的哈希表,其大小为现在的一半或者是两倍,并且将当前哈希表未被删除的数据迁移过去 + +待调整的哈希表为当前大小的一半或者两倍的素数,但找到新的长度并不是一件简单的事情,为此, +需要设置一个初始值大小,增加哈希表长度的时候,只需在初始值的基础上找到下一个素数即可,当需要减小的时候, +将当前哈希表的长度减半,然后基于这个数值找到下一个素数 + +哈希表的初始大小设置为 50 + +我们使用比较暴力的方法去查找下一个素数,但事实上,通过此方法查找素数并不会耗费过多的时间与性能, +因为我们需要检查的数量是非常少的,所需的时间比遍历哈希表每个元素都要少 + + +首先,我们把素数的相关方法保存在文件 `prime.h` 和 `prime.c` + +```c +// prime.h +int is_prime(const int x); +int next_prime(int x); +``` + +```c +// prime.c + +#include + +#include "prime.h" + + +/* + * 判断 x 是否素数 + * + * 返回值: + * 1 - 素数 + * 0 - 不是素数 + * -1 - 无效的输入 (i.e. x < 2) + */ +int is_prime(const int x) { + if (x < 2) { return -1; } + if (x < 4) { return 1; } + if ((x % 2) == 0) { return 0; } + for (int i = 3; i <= floor(sqrt((double) x)); i += 2) { + if ((x % i) == 0) { + return 0; + } + } + return 1; +} + + +/* + * 返回 x 基础上的下一个素数 + */ +int next_prime(int x) { + while (is_prime(x) != 1) { + x++; + } + return x; +} +``` + +下一步,需要更新 `ht_new` 方法以便支持可以根据传入的参数来生成指定大小的哈希表, +所以,需要新增一个方法 `ht_new_sized`,然后把我们之前的 `ht_new` 方法重命名为 `ht_new_sized` 并加上参数 + +```c +// hash_table.c +static ht_hash_table* ht_new_sized(const int base_size) { + ht_hash_table* ht = xmalloc(sizeof(ht_hash_table)); + ht->base_size = base_size; + + ht->size = next_prime(ht->base_size); + + ht->count = 0; + ht->items = xcalloc((size_t)ht->size, sizeof(ht_item*)); + return ht; +} + + +ht_hash_table* ht_new() { + return ht_new_sized(HT_INITIAL_BASE_SIZE); +} +``` + +到目前为止,我们已经完成了可以根据不同的参数来创建不同大小的哈希表的方法。 +下面,我们在这个方法的基础上编写调整哈希表大小的方法。 +首先,需要保证的待调整的哈希表大小不能低于预设的最小值,然后根据这个值,创建一个新的哈希表。 +,接着,遍历旧的哈希表,把所有结果不为 `NULL` 以及没有被删除的元素都迁移到新的哈希表上, +当一切工作已经完成后,要记得释放旧的哈希表的内存,以防内存泄漏 + +```c +// hash_table.c +static void ht_resize(ht_hash_table* ht, const int base_size) { + if (base_size < HT_INITIAL_BASE_SIZE) { + return; + } + ht_hash_table* new_ht = ht_new_sized(base_size); + for (int i = 0; i < ht->size; i++) { + ht_item* item = ht->items[i]; + if (item != NULL && item != &HT_DELETED_ITEM) { + ht_insert(new_ht, item->key, item->value); + } + } + + ht->base_size = new_ht->base_size; + ht->count = new_ht->count; + + // To delete new_ht, we give it ht's size and items + const int tmp_size = ht->size; + ht->size = new_ht->size; + new_ht->size = tmp_size; + + ht_item** tmp_items = ht->items; + ht->items = new_ht->items; + new_ht->items = tmp_items; + + ht_del_hash_table(new_ht); +} +``` + +为了便于调整与调用,下面简单的定义了增大哈希表以及减小哈希表的两个方法 + +```c +// hash_table.c +static void ht_resize_up(ht_hash_table* ht) { + const int new_size = ht->base_size * 2; + ht_resize(ht, new_size); +} + + +static void ht_resize_down(ht_hash_table* ht) { + const int new_size = ht->base_size / 2; + ht_resize(ht, new_size); +} +``` + +在插入以及删除的时候需要触发哈希表长度的检查,通过当前存储的容量百分比,我们可以很轻易的判断出,当容量超过 70% 的时候,需要增加哈希表长度, +当使用容量小于 10% 的时候,需要减小哈希表长度 + +```c +// hash_table.c +void ht_insert(ht_hash_table* ht, const char* key, const char* value) { + const int load = ht->count * 100 / ht->size; + if (load > 70) { + ht_resize_up(ht); + } + // ... +} + + +void ht_delete(ht_hash_table* ht, const char* key) { + const int load = ht->count * 100 / ht->size; + if (load < 10) { + ht_resize_down(ht); + } + // ... +} +``` + +下一节: [附录](../07-appendix) +[目录](/.translations/cn/README.md#contents) diff --git a/.translations/cn/07-appendix/README.md b/.translations/cn/07-appendix/README.md new file mode 100644 index 0000000..0e0e86d --- /dev/null +++ b/.translations/cn/07-appendix/README.md @@ -0,0 +1,52 @@ +# 附录: 碰撞冲突的其它处理方案 + +在哈希表的冲突问题中,有两个最为常用的方法,分别是: + +- 拉链法 +- 开放寻址法 + +### 拉链法 + +拉链法,每个哈希元素都指向一个链表,当发生碰撞时候,把冲突的元素直接新增到链表中,方法如下: + +- 插入:先通过哈希函数返回存储位置的索引值。如果当前位置还没被占用,则直接把元素添加到此位置。如果当前位置已经被占用了,则添加到链表的末端 +- 查询:通过索引值定位到待查找元素的位置,然后遍历当前位置的链表,如果找到则返回,否则,返回 `NULL` +- 删除:和查找也是同样的道理,先找到目标元素,如果命中,则把当前元素从链表中删除,如果当前链表只有一个元素,则设置索引位置为 `NULL` 即可 + +拉链法的实现虽然很简单,但是空间效率太低了。因为哈希表的每个位置都需要存储一个指向链表的指针, +然而指针是会消耗内存的,实际上可以利用这些内存来存储更多的元素 + +### 开放地址 + +开放地址可以解决拉链法所带来的空间浪费问题。当冲突发生后,冲突的元素应该被放置在哈希表其它空余的位置上。 +但是这个位置当然不是随意选取,如果随意选取的话,在查找的时候就无法查找到目标元素了。所以需要制定一套策略,供插入,查询使用 + +#### 线性探测 + +线性探测,当发生冲突的时候,顺序查看哈表表中的下一个位置,直至找到一个空的位置或遍历全表,哈希方法如下: + +- 插入:找到对应 key 的索引值,如果命中的位置为空,则把元素添加到这个位置。否则,查看冲突索引值的下个位置,直到找到一个空的位置去插入目标元素 +- 查询:通过索引值定位到相应的位置,并通过比较两个 key 的值来确定待查找的元素,如果命中,则返回,否则,递增索引,重复上个过程 +- 删除:同样的通过查询方法定位到目标位置,然后删除目标元素,但是仔细想想,直接删除该位置的元素将会导致在此位置之后的元素无法被查找,因此,需要将哈希表中被删除键的右侧的所有元素重新插入到散列表 + +线性探测提供了良好的[局部性原理](https://en.wikipedia.org/wiki/Locality_of_reference),但是会遇到键簇(一组连续的条目)的问题, +使得探测的时间成本大大增加,因为每次发生冲突后可能需要遍历很长的一段距离才能找到可用的位置 + +#### 二次探查 + +此方法与线性探测类似,不同的一点在于发生冲突后,不是直接查找下一个索引位置,而是通过具有以下规律的序列来定位下个位置的索引值:`i, i + 1, i + 4, i + 9, i + 16, ...`, +其中 `i` 为发生冲突后的索引值。哈希方法如下: + +- 插入:通过索引值以及序列方法查询到可用的位置,并将待添加的元素添加到此处 +- 查询:通过索引值定位到相应的位置,并通过比较两个 key 的值来确定待查找的元素,如果命中,则返回,否则,通过序列函数计算下一个索引值,重复以上过程 +- 删除:我们无法判断我们要删除的元素是否是冲突集的一部分,因此我们无法直接删除该元素而是使用标记的方法来实现删除操作 + +通过以上策略,二次探查比较好的缓解了线性探测的长键簇问题 + +#### 再哈希 + +二次探查仍然可能存在长键簇带来的问题,而再哈希法正是为了解决此问题。为此, +当发生冲突的时候,我们将使用第二个哈希函数作为新索引值,且索引值应该是均匀分布的。 +在大多数的哈希表的实现上都使用再哈希法来解决冲突问题,同时,也是我们本教程所使用的方法 + + From 5a3fea7d38da25656e2e95bfafcac4a708cb929b Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Sat, 8 Sep 2018 10:42:23 +0800 Subject: [PATCH 07/10] fix some wrong description from translation --- .translations/cn/01-introduction/README.md | 38 +++++++++---------- .translations/cn/02-hash-table/README.md | 26 +++++-------- .translations/cn/03-hashing/README.md | 44 +++++++++------------- .translations/cn/04-collisions/README.md | 2 +- .translations/cn/05-methods/README.md | 2 +- .translations/cn/06-resizing/README.md | 2 +- .translations/cn/README.md | 24 ++++++------ 7 files changed, 61 insertions(+), 77 deletions(-) diff --git a/.translations/cn/01-introduction/README.md b/.translations/cn/01-introduction/README.md index 93ffb33..aceebd3 100644 --- a/.translations/cn/01-introduction/README.md +++ b/.translations/cn/01-introduction/README.md @@ -1,34 +1,33 @@ # 介绍 -哈希表是根据关联数组的 [Api](API) 实现的一种快速存储与检索数据的数据结构, -为了让大家更好的阅读本文,我在文章底部的[摘要](摘要)添加了一些名词的解析。 +哈希表是基于关联数组 [API](#API) 实现的一种快速存储与检索数据的数据结构, +为了让大家更好的阅读本文,我在文章底部的[摘要](#摘要)添加了一些相关术语的解析 -哈希表是由一系列的"桶"组成,每个桶都存储一组键值对。为了确定存储键值对的桶应该放置在哪个位置, -我们可以通过将键值对的键作为参数传递到哈希函数中,然后此哈希函数生成一个整型索引值作为数组的索引, -当需要检索数据时,也是同样的通过此哈希函数生成相应的索引来定位此键值对存放的位置。 +哈希表是由一系列的"桶"组成,每个桶都存储一组键值对。为了确定存储键值对的桶放置的位置, +我们通过将键作为参数传递给哈希函数,然后哈希函数生成一个整型索引值作为数组的索引。 +当需要检索数据时,也是同样的通过此哈希函数生成相应的索引来找到对应的元素 -由于数组的存取复杂度均为 `O(1)`,而哈希表是借助数组实现的,所以哈希表具有快速存储和检索数据的特性。 +由于数组的存取复杂度均为 `O(1)`,而哈希表是借助数组实现的,所以哈希表具有快速存储和检索数据的特性 -本篇文章将要实现的哈希表只是存储字符类型的键值对, 原则上应该是实现任意类型的键值对, -但由于支持 unicode 并不简单而且超出了本票文章的范围,所以此处数据类型只支持 ASCII +本文将要实现的哈希表仅支持字符类型的键值对,原则上应当支持任意类型的键值对, +但由于 UNICODE 的编码并不容易实现且超出了本篇文章的范围,所以此处只支持 ASCII 的编码类型 ## API -关联数组是一组无序键值对的集合,故重复的键是无效的,且支持以下的操作: +关联数组是一组无序键值对的集合,故插入重复的键是无效的,且具有如下方法: -- `search(a, k)`: 关联数组 `a` 中通过键 `k` 返回值 `v`,如果该键对应的值不存在,则返回 NULL -- `insert(a, k, v)`: 在关联数组 `a` 中插入键值对 `k:v` +- `search(a, k)`: 关联数组 `a` 通过参数键 `k` 返回 `v`,如果查询失败,则返回 NULL +- `insert(a, k, v)`: 在关联数组 `a` 插入键值对 `k:v` - `delete(a, k)`: 通过键 `k` 删除关联数组 `a` 中对应的键值对,若键不存在,则不做任何操作 ## 起始 -由于本文是由 C 编写的,如果你还不清楚 C,可以参考 [Daniel Holden's](@orangeduck) 编写的 -《[Build Your Own Lisp](http://www.buildyourownlisp.com/chapter2_installation)》, -这是一本非常不错的书籍,如果可以的话,我建议你读一读 +由于本文是由 C 编写的,如果你还不清楚 C,可以参考 [Daniel Holden's](https://github.com/orangeduck) 编写的 +《[Build Your Own Lisp](http://www.buildyourownlisp.com/chapter2_installation)》,这是一本非常不错的书籍,如果可以的话,我建议你读一读 ## 代码结构 -代码结构由以下目录和文件组成 +本教程的代码结构如下: ``` . @@ -40,18 +39,17 @@ └── prime.h ``` -`src` 文件夹里放置我们的代码,`build` 文件夹放置编译后的二进制文件 +`src` 文件夹放置我们将要编写的代码,`build` 文件夹放置编译后的二进制文件 ## 摘要 -由于很多技术名词都有各种不同的叫法,为了便于大家的理解,在本文中,我们统一用以下命名: +很多技术名词都有各种不同的叫法,为了便于大家的理解,在本文中,我们统一用以下命名: - 关联数组: 通过 [API](#api) 实现的一种抽象的数据结构。也称为映射,符号表,字典 - 哈希表: 通过散列函数实现的快速存储与检索的关联数组。也称为哈希映射,映射,哈希或字典。 -关联数组可以用许多不同的底层数据结构实现,一种比较低性能且较为简单的方法是把元素直接存储在数组中, -在检索的时候逐个遍历对比。关联数组和散列表经常被混淆,因为散列表通常都是通过关联数组来实现。 +关联数组可以用许多不同的底层数据结构实现,一种比较低性能且较为简单的方法是把元素直接存储在数组中,在检索的时候逐个遍历对比。因为哈希表通常都是由关联数组来实现的,所以人们经常把关联数组和哈希表的称呼混在一起 下一节: [哈希表结构](../02-hash-table) -[目录](/.translations/cn/README.md#contents) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/02-hash-table/README.md b/.translations/cn/02-hash-table/README.md index b26c2e7..2fe6f4c 100644 --- a/.translations/cn/02-hash-table/README.md +++ b/.translations/cn/02-hash-table/README.md @@ -1,6 +1,6 @@ # 哈希表结构 -哈希的元素存储在以下的 `ht_item` 结构, `key` 代表元素的键, `value` 代表元素的值,且均为字符类型: +哈希表的元素由以下结构 `ht_item` 组成,键:`key`,值:`value`,且均为字符类型: ```c // hash_table.h @@ -10,8 +10,7 @@ typedef struct { } ht_item; ``` -下面是哈希表的结构, `items` 是指向 `ht_item` 结构的指针数组,`size` 则存储哈希表的大小,即哈希的长度 -`count` 表示当前哈希表当前已存储了多少个键值对: +哈希表结构定义, `items` 属性是指向 `ht_item` 结构的指针数组,`size` 则存储哈希表的总长度,`count` 属性代表当前哈希表的长度使用情况,即当前已用元素的数量: ```c // hash_table.h @@ -22,11 +21,9 @@ typedef struct { } ht_hash_table; ``` -## 哈希的初始化和删除 +## 哈希结构的初始化和删除方法 -首先,我们需要定义一个初始化 `ht_item` 结构的方法,并分配一块大小为 `ht_item` 的内存, -并存储字符串 `k` 和 `v` 的副本,该方法的开头需要标记为 `static` 静态方法,是为了该方法 -只供在本文件内调用 +以下是实现哈希表的初始化方法,首先需要初始化 `ht_item` 的结构体,然后将键值对参数存储到结构体对应的属性,最后,将该方法标记为 `static` 静态方法,以防外部文件调用 ```c // hash_table.c @@ -43,10 +40,9 @@ static ht_item* ht_new_item(const char* k, const char* v) { } ``` -`ht_new` 方法用于初始化一个哈希表结构。 `size` 定义了该结构能存储多少元素,当前暂且 -只分配 53 的存储大小,在后面的章节中,我们会提到 [resizing](../06-resizing) 来动态调整哈希表的 -存储大小。然后我们使用 `calloc` 方法来初始化元素数组,这里用 `calloc` 而不用 `malloc` 是因为 -`calloc` 所分配的内存空间的每一位都会初始化为 `NULL`,而 `NULL` 则表示该位置未存储元素 +`ht_new` 方法用于初始化哈希表结构。 `size` 属性定义了哈希表的长度,当前暂且只分配 53 的长度,在后面的章节中,我们将会通过 [调整哈希表大小](../06-resizing) 来动态调整哈希表的存储大小。 + +使用 `calloc` 初始化 `items` 属性,这里用 `calloc` 而不用 `malloc` 是因为 `calloc` 所分配的内存空间的每一位元素都会初始化为 `NULL`,`NULL` 代表哈希表的此位置可用 ```c // hash_table.c @@ -60,8 +56,7 @@ ht_hash_table* ht_new() { } ``` -当然,为了防止[内存泄漏](https://en.wikipedia.org/wiki/Memory_leak),我们需要定义 `ht_item` -和 `ht_hash_tables` 的删除方法来进行内存的回收 +为了防止[内存泄漏](https://en.wikipedia.org/wiki/Memory_leak),我们需要定义 `ht_item` 和 `ht_hash_tables` 结构的删除方法来进行内存的回收 ```c // hash_table.c @@ -84,8 +79,7 @@ void ht_del_hash_table(ht_hash_table* ht) { } ``` -在本章节中,我们定义了如何初始化或删除一个哈希表,虽然实现的功能并不多,但为了保证后面程序的正常运行, -需要保证编写的方法不会报错,故可以照着以下的示例代码进行调试 +在本章节中,我们定义了如何初始化或删除一个哈希表,虽然实现的功能并不多,但为了保证后面程序能正常运行,需要进行简单的测试 ```c // main.c @@ -98,4 +92,4 @@ int main() { ``` 下一节: [哈希函数](../03-hashing) -[目录](/.translations/cn/README.md#contents) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/03-hashing/README.md b/.translations/cn/03-hashing/README.md index ff18126..839a859 100644 --- a/.translations/cn/03-hashing/README.md +++ b/.translations/cn/03-hashing/README.md @@ -1,16 +1,16 @@ -# 哈希函数 +# 哈希值函数 -在这一节中,将编写用于计算索引值的哈希函数。 +在这一节中,将编写哈希值函数来计算哈希索引值 -哈希函数应该具有以下特征: +哈希值函数应当满足以下要求: -- 输入一个字符串,返回一个 `0` - `m` 的整型数字, `m` 必须不大于哈希的长度,即上一节中 `ht_hash_table` 的 `size` +- 输入一个字符串,返回一个 `0` - `m` 的整型数字, `m` 不大于哈希的长度,即上一节中 `ht_hash_table` 的 `size` -- 计算的索引值必须均匀分布,如果计算的索引值分布不均匀,会导致大多数键都落在同一索引,从而在哈希表插入过程中将会产生大量[碰撞](#Pathological data),大大的降低性能 +- 索引值结果必须均匀分布,如果计算的索引值分布不均匀,会导致大多数键都落在同一索引,且在哈希表插入过程中将会产生大量[冲突](#不符合预期的问题数据),冲突将会在很大程度上降低哈希表的性能 ## 算法 -我们将使用泛型字符串散列函数,下面是该算法的伪代码。 +我们将使用字符串来计算哈希值,下面是该算法的伪代码 ``` function hash(string, a, num_buckets): @@ -22,17 +22,16 @@ function hash(string, a, num_buckets): return hash ``` -从上面的函数可以看出,要生成一个 hash 值,我们需要做以下两步: +从上面的伪代码可以看出,要生成一个哈希值,我们需要做以下两步操作: 1. 把字符串转换为整型 -2. 因转换的整型数字可能远远大于哈希表的长度,所以我们需要通过 `mod` 取余数来保证生成的哈希函数不大于哈希表长度 `m` +2. 因转换后的数字可能远大于哈希表的长度,所以我们需要通过 `mod` 取余数来保证生成的哈希值不大于哈希表长度 `m` -变量 `a` 应当是一个大于字母表大小的素数,因为我们在计算字符串的散列值,而字符串的 ASCII 大小为 128, -所以 `a` 的大小应当是大于 128 的素数 +变量 `a` 是一个大于字母表大小的素数,因为本文的哈希值函数仅支持字符类型,而字符串的 ASCII 大小为 128,所以 `a` 的大小应是大于 128 的素数 -`char_code` 是将单字符转换为整数的方法 +`char_code` 则是将单字符转换为整数的方法 -根据上面的描述,可以举个例子计算一下: +根据上面的描述,可以举例演示计算过程: ``` hash("cat", 151, 53) @@ -43,7 +42,7 @@ hash = (2272062) % 53 hash = 5 ``` -通过改变变量 `a` 的值,我们可以得到不同的计算结果 +通过改变变量 `a` 的值,我们可以得到不同的哈希值 ``` hash("cat", 163, 53) = 3 @@ -64,20 +63,13 @@ static int ht_hash(const char* s, const int a, const int m) { } ``` -## 病理数据 +## 不符合预期的问题数据 -理想的散列函数无论输入什么键,其返回值都是均匀分布的。然而,对于任意的哈希函数,都不会生成每个键唯一的哈希值, -肯定会出现一些 `奇怪` 的键,这些键生成的哈希值往往会占用其它键的哈希值。 +理想的哈希值函数无论输入的参数是什么,其结果都是均匀分布的。然而,无论怎么优化哈希值函数,都很难生成唯一的哈希值,肯定会出现 `不符合预期` 的键,这些键生成的哈希值往往会与其它键所生成哈希值一致 -一系列的病理数据集合意味着没有完美的哈希函数可以生成唯一的哈希值。所以最好的办法就是 -创建一个函数来处理这些可能会出现的数据 +这类问题数据集的存在,意味着没有完美的哈希值函数可以生成唯一的哈希值。所以我们所能做的就是想办法处理出现问题的数据 -Pathological inputs also poses a security issue. If a hash table is fed a set of -colliding keys by some malicious user, then searches for those keys will take -much longer (`O(n)`) than normal (`O(1)`). This can be used as a denial of -service attack against systems which are underpinned by hash tables, such as DNS -and certain web services. +问题数据同时也存在安全性问题。如果某些恶意用户通过不断提供冲突的键给程序,那么当查询的时候就会比预期时间 `O(1)` 花费更多的时间 `O(n)` 才能找到目标值。这可以用作针对以哈希表(例如DNS和某些Web服务)为基础实现的程序进行 DDOS - -Next section: [处理哈希碰撞](../04-collisions) -[目录](/.translations/cn/README.md#contents) +下一节: [处理哈希冲突](../04-collisions) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/04-collisions/README.md b/.translations/cn/04-collisions/README.md index 650eb8e..ef13f1b 100644 --- a/.translations/cn/04-collisions/README.md +++ b/.translations/cn/04-collisions/README.md @@ -39,4 +39,4 @@ static int ht_get_hash( ``` 下一节: [哈希表的函数](../05-methods) -[目录](/.translations/cn/README.md#contents) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/05-methods/README.md b/.translations/cn/05-methods/README.md index 94f30bd..39b487a 100644 --- a/.translations/cn/05-methods/README.md +++ b/.translations/cn/05-methods/README.md @@ -153,4 +153,4 @@ void ht_insert(ht_hash_table* ht, const char* key, const char* value) { ``` 下一节: [调整哈希表大小](../06-resizing) -[目录](/.translations/cn/README.md#contents) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/06-resizing/README.md b/.translations/cn/06-resizing/README.md index ed52636..81e5d4a 100644 --- a/.translations/cn/06-resizing/README.md +++ b/.translations/cn/06-resizing/README.md @@ -172,4 +172,4 @@ void ht_delete(ht_hash_table* ht, const char* key) { ``` 下一节: [附录](../07-appendix) -[目录](/.translations/cn/README.md#contents) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/README.md b/.translations/cn/README.md index e27c0cf..c4e6c67 100644 --- a/.translations/cn/README.md +++ b/.translations/cn/README.md @@ -2,30 +2,30 @@ # 哈希表的实现(C) -[哈希表](https://en.wikipedia.org/wiki/Hash_table) 是一种最常用的数据结构。由于哈希表具有的快速存储以及可扩展性的特性,使得其在计算机领域有着广泛的应用 +[哈希表](https://en.wikipedia.org/wiki/Hash_table) 是一种常用的数据结构。由于哈希表具有的快速存储以及可扩展性的特性,使得其在计算机领域有着广泛的应用 -在本篇教程中,我们将用 C 实现基于[开放寻址](https://en.wikipedia.org/wiki/Open_addressing)以及[再哈希](https://en.wikipedia.org/wiki/Double_hashing)的哈希表。 -通过这篇教程,你可以知道: +在本篇教程中,我们将用 C 实现[开放寻址](https://en.wikipedia.org/wiki/Open_addressing)以及[再哈希](https://en.wikipedia.org/wiki/Double_hashing)的哈希表。 +通过本篇教程,你可以知道: - 基础数据结构是如何在底层工作的 -- 深入了解到什么时候使用哈希表,而什么时候应避免使用哈希表以及为什么不能用 +- 了解到什么时候使用哈希表,而什么时候应避免使用哈希表以及为什么不能用 - 对 C 或许会有新的认知 -C 非常适合用于编写哈希表,主要是因为: +C 很适合用于编写哈希表,主要是因为: -- C 不需要引入任何东西就可以直接运行 -- 属于底层语言,所以可以深入的了解到在机器层面上的程序是如何运作的 +- C 并不需要引入其它额外的文件 +- C 属于底层语言,所以可以深入的了解到在机器层面上的程序是如何运作的 -本教程是假设你已熟悉 C 的语法。由于代码本身相对简单,大多数问题都可以通过搜索引擎来解决。 -如果你遇到其它问题,请打开 Github 的 [Issue](https://github.com/yigger/write-a-hash-table/issues) 进行提问 +本教程是假设你已熟悉 C 的语法。由于本教程的代码相对简单,大多数问题都可以通过搜索引擎来解决。 +如果你遇到其它问题,请打开 Github 的 [Issue](https://github.com/jamesroutley/write-a-hash-table/issues) 进行提问 -整个过程大概需要编写 200 行左右的代码,这大概会耗费你 1 ~ 2 个小时 +整个过程大概需要编写 200 行左右的代码,这大概需要耗费 1 ~ 2 个小时 ## 目录 1. [介绍](./01-introduction) 2. [哈希表数据结构](./02-hash-table) -3. [哈希函数](./03-hashing) +3. [哈希值函数](./03-hashing) 4. [冲突处理](./04-collisions) 5. [哈希表的方法](./05-methods) 6. [调整哈希表大小](./06-resizing) @@ -33,4 +33,4 @@ C 非常适合用于编写哈希表,主要是因为: ## Credits -本教程是由 [James Routley](https://twitter.com/james_routley) 编写,博客地址:[routley.io](https://routley.io)。 +本教程是由 [James Routley](https://twitter.com/james_routley) 编写,博客地址:[routley.io](https://routley.io) From 528e531c6c3706b405d91ba90311a2dd2514c9b0 Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Sat, 8 Sep 2018 11:37:18 +0800 Subject: [PATCH 08/10] fix translation wrong description --- .translations/cn/04-collisions/README.md | 19 ++++------- .translations/cn/05-methods/README.md | 37 +++++++++------------ .translations/cn/06-resizing/README.md | 42 ++++++++++-------------- .translations/cn/07-appendix/README.md | 8 ++--- .translations/cn/README.md | 4 +-- 5 files changed, 46 insertions(+), 64 deletions(-) diff --git a/.translations/cn/04-collisions/README.md b/.translations/cn/04-collisions/README.md index ef13f1b..ef7542c 100644 --- a/.translations/cn/04-collisions/README.md +++ b/.translations/cn/04-collisions/README.md @@ -1,25 +1,20 @@ -## 哈希碰撞 +## 哈希冲突 -上节提到的散列函数若通过输入无限的数据映射到有限数量的输出,必然会产生同样的索引值,所以也就导致了 -存储桶的索引碰撞,所以我们需要有好的方案去处理这些冲突。 +上节提到的哈希值函数存在`无限的输入数据映射到有限数量的输出数据`,则哈希值函数必然会生成同样的哈希值,所以也就导致了存储位置的索引冲突,所以需要一个方案来处理冲突后的元素 -显然,已经有很多人遇到了这个问题,所以也有很多方案可以处理这些情况,对于其它处理碰撞的方法,就不在这里赘述,请查看[附录](../07-appendix) - -在本文中我们将使用一种称为开放寻址和再哈希的技术来处理冲突。也就是说,在发生碰撞后,再哈希会使用两个哈希函数来计算 `i` 出现碰撞后的索引值 +在本文中我们将使用一种称为开放寻址和再哈希的技术来处理这些冲突。换句话说,在发生冲突后,再哈希会通过 `i` 变量使用两次哈希值函数来计算发现冲突后的索引值。当然,处理的方案不止一种,对于其它的方案,就不在这里赘述,请查看[附录](../07-appendix) ## 再哈希 -在 `i` 发生碰撞后的索引值可由以下公式算出: +在发生冲突后,哈希值可根据 `i` 由以下公式算出: ``` index = hash_a(string) + i * hash_b(string) % num_buckets ``` -通过以上式子我们可以发现,如果没有发生任何碰撞,即 `i = 0`,所以哈希的索引值仅仅只是 `hash_a` 的结果。 -如果碰撞发生了,即 i != 0,结果很明显会根据 `hash_b` 的返回结果进行变化 +通过以上公式我们可以知道,如果没有发生任何冲突,即 `i = 0`,所以哈希值仅仅只是 `hash_a` 的返回值。如果冲突发生了,即 `i != 0`,结果很明显会根据 `hash_b` 的结果进行变化 -很显然,如果 `hash_b` 的返回结果为 0 将导致第二项的结果也为 0,哈希表会一遍又一遍的尝试插入到同一个位置, -那么我们的这项处理就毫无意义了,所以,我们可以通过在 `hash_b` 的结果后面加 1 来避免这样的情况。 +很显然,如果 `hash_b` 的返回结果为 `0`,公式第二项的结果也为 0,哈希表会一遍又一遍的将元素尝试插入到同一位置,那么我们的这项处理就毫无意义了,所以,需要在 `hash_b` 的返回结果后面 `+1` 来避免这样的情况出现 ``` index = (hash_a(string) + i * (hash_b(string) + 1)) % num_buckets @@ -38,5 +33,5 @@ static int ht_get_hash( } ``` -下一节: [哈希表的函数](../05-methods) +下一节: [哈希表函数的实现](../05-methods) [目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/05-methods/README.md b/.translations/cn/05-methods/README.md index 39b487a..8c5202f 100644 --- a/.translations/cn/05-methods/README.md +++ b/.translations/cn/05-methods/README.md @@ -1,6 +1,6 @@ -# 哈希表API +# 哈希表 Api -哈希表需要实现以下 API 方法: +哈希表需要实现以下函数: ```c // hash_table.h @@ -11,8 +11,7 @@ void ht_delete(ht_hash_table* h, const char* key); ## 插入 -为了让一个键值对能插入到哈希表中,我们需要调用哈希函数来生成索引值,但该索引值的对应位置可能已经被使用, -也就是说发生了碰撞,则通过递增 `i` 的值来重新生成索引,直至找到合适的位置存储新增的键值对。 +为了让一个键值对能插入到哈希表中,我们需要调用哈希值函数来生成索引值,但索引值的对应位置可能已经被使用,也就是说发生了冲突,则通过递增 `i` 的值来重新生成索引,直至找到合适的位置存储新元素。 在插入了新元素后,我们同样需要将哈希表的 `count` 属性加一,这是为了便于[调整哈希表大小](../06-resizing) ```c @@ -34,10 +33,8 @@ void ht_insert(ht_hash_table* ht, const char* key, const char* value) { ## 查找 -查找过程跟插入过程差不多,也是通过 `key` 来生成索引值,然后通过该索引值寻找哈希表对应位置的元素, -如果找到的元素的 `key` 与传入的 `key` 一致,则当前元素便是我们需要要查找的结果。 -如果两个 `key` 不相等,则说明 `key` 在插入过程中可能发生了碰撞,需要继续通过 `ht_get_hash` 来生成索引, -然后重复上个过程,直至找到与参数 `key` 一致的元素。 +查找过程与插入过程类似,也是通过 `key` 来生成索引值,利用索引值寻找哈希表对应位置的元素,然后通过比较元素的 `key` 与参数 `key` 来确定当前元素是否我们需要要查找的元素。 +如果两个 `key` 不相等,则说明 `key` 在插入过程中可能发生了冲突,需要继续通过 `ht_get_hash` 来生成索引值,接着重复上个过程,直至找到与参数 `key` 一致的元素。 如果在查询过程中,`index` 索引返回了 `NULL`,则代表当前索引值为空,即元素不存在,结果返回 `NULL` ```c @@ -60,13 +57,12 @@ char* ht_search(ht_hash_table* ht, const char* key) { ## 删除 -删除元素的操作比插入或查找都要复杂。因为我们需要删除的元素的对应索引位置可能是发生了碰撞后的位置, -如果直接删除的话,就会破坏了我们处理碰撞的处理操作,从而无法查询到所有发生碰撞后的元素。 +删除元素的操作比插入或查找都更为复杂。在查找删除元素的过程中可能会出现冲突,如果直接删除查询结果的话,就会破坏了查询、插入等操作,从而无法定位到发生冲突后的元素。 所以,为了解决这个问题,我们不能删除这个元素,取而代之的是把这个元素做个标记,即软删除。 -(举个例子: `hello` 的索引是 `4`, `world` 的索引也是 `4`,通过碰撞处理函数,`world` 重新分配到了索引为 10 的位置。 +(举个例子: `hello` 的索引是 `4`, `world` 的索引也是 `4`,通过冲突处理函数,`world` 重新分配到了索引为 10 的位置。 如果直接删除索引为 `4` 的位置,在查询 `world` 的过程中就会因为 `4` 的位置返回 `NULL`,从而结果也就为 `NULL`) -我们需要定义一个标记位,即 `ht_item HT_DELETED_ITEM = {NULL, NULL}`,然后,把需要删除的元素的指向替换成指向 `HT_DELETED_ITEM` 元素 +我们当然需要定义一个标记值,即 `ht_item HT_DELETED_ITEM = {NULL, NULL}`,然后,把需要删除的元素的指向替换成指向 `HT_DELETED_ITEM` 元素即可 ```c // hash_table.c @@ -92,13 +88,12 @@ void ht_delete(ht_hash_table* ht, const char* key) { } ``` -在删除元素后,哈希表的 `count` 属性同样的需要减一 +在删除元素后,哈希表的 `count` 属性也需要减一 -由于做了这个操作,必然会影响到之前写的插入与查找操作,我们还需要稍微修改一下 +以上的操作会影响到本章开头编写的插入与查找函数,我们还需要稍微修改一下这两个函数 -在查找的时候,我们将查询到的元素与删除的标记位进行比较,以此来判断该元素是否已经被删除,如果已经删除了, -则跳过该元素,继续寻找下一个。 -在插入过程中,如果新增元素的索引位置恰巧命中了已删除元素的位置,则直接把新元素替换到该位置即可 +在查找的时候,我们将查询到的元素与 `HT_DELETED_ITEM` 进行比较,以此来判断该元素是否已经被删除,如果已经删除了,则跳过该元素,继续寻找下一个。 +在插入过程中,如果新增元素的索引位置恰巧命中了已删除元素的位置,则直接把新元素覆盖为旧元素 ```c @@ -128,11 +123,9 @@ char* ht_search(ht_hash_table* ht, const char* key) { ## 更新 -我们的哈希表目前不支持更新操作。因为当前结构不允许存在两个同样的键,假如存在同样的键, -那么在插入过程中肯定会发生碰撞,从而后一个键会插入到后一个位置。 -但是在查找过程中,由于键相同,所以生成的索引值也将是一样的,也就是说,第二个键将永远命中第一个键对应的位置。 +当前实现的哈希表并不支持更新操作。因为当前结构不能存在两个同样的键,假如存在同样的键,那么在插入过程中必然会产生冲突,从而导致相同的键由于冲突存储在不同的索引值位置。而在查找函数过程中,通过键生成的索引值肯定是一样的,这样就导致了无法查找到后面插入进来的键值元素,将永远只命中第一个符合键的元素 -为了解决这个问题,我们可以直接把待修改的元素替换成新的元素 +为了解决这个问题,我们可以通过修改插入函数,把新的元素覆盖旧元素 ```c // hash_table.c @@ -152,5 +145,5 @@ void ht_insert(ht_hash_table* ht, const char* key, const char* value) { } ``` -下一节: [调整哈希表大小](../06-resizing) +下一节: [调整哈希表长度](../06-resizing) [目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/06-resizing/README.md b/.translations/cn/06-resizing/README.md index 81e5d4a..11ab095 100644 --- a/.translations/cn/06-resizing/README.md +++ b/.translations/cn/06-resizing/README.md @@ -1,30 +1,24 @@ -# 调整哈希表的大小 +# 调整哈希表长度 -到目前为止,我们的哈希表的大小都是固定在最开始的 `53` 个位置,但随着越来越多的元素被添加进来, -哈希表在有限的空间内将会被填充满,这可能会导致两个问题: +到目前为止,我们的哈希表长度都是固定为 `53` ,但随着越来越多的元素被添加进来,哈希表在有限的空间内将会被塞满,这可能会导致两个问题: -1. 哈希表的各项性能会随着高冲突率而下降 -2. 哈希表只能存储固定大小的元素,当想要添加更多的元素到哈希表的时候,将会失败 +1. 哈希表的性能会随着高冲突率而下降 +2. 哈希表只能存储固定长度的元素,无法添加更多的元素 -如何解决以上问题?很简单,当哈希表即将要满的时候,扩大哈希表的大小就可以了。 -但需要制定策略,什么时候应该扩大,什么时候应该减小。 -这时候,在上一节我们的 `count` 属性就发挥了重要的作用,我们可以通过该属性与 `size` 属性来判断 -当前哈希表的存储状态,将它保存在 `load` 变量中,当满足以下条件时,需要进行调整大小: +如何解决以上问题?很简单,当哈希表即将要满的时候,增加哈希表的长度就可以了。 +但需要制定策略:什么时候应该扩大?什么时候应该减小? +在上一节的 `count` 属性的统计就发挥了重要的作用,我们可以通过该属性与 `size` 属性来计算当前哈希表的存储容量百分比,并保存在 `load` 变量中,当满足以下条件时,则进行调整: -- 增大哈希表的长度, if load > 0.7 -- 减小哈希表的长度, if load < 0.1 +- 当 load > 0.7,增加哈希表的长度 +- 当 load < 0.1,减小哈希表的长度 -为了调整大小,我们可以通过创建一个新的哈希表,其大小为现在的一半或者是两倍,并且将当前哈希表未被删除的数据迁移过去 +调整方案,我们可以通过创建一个新的哈希表,其长度为当前哈希表的一半或两倍,并将当前哈希表未被删除的数据迁移过去 -待调整的哈希表为当前大小的一半或者两倍的素数,但找到新的长度并不是一件简单的事情,为此, -需要设置一个初始值大小,增加哈希表长度的时候,只需在初始值的基础上找到下一个素数即可,当需要减小的时候, -将当前哈希表的长度减半,然后基于这个数值找到下一个素数 +新的哈希表为当前哈希表长度的一半或者两倍的素数,但找到新的长度并不是一件简单的事情,为此,需要设置一个初始值,当需要增加哈希表长度的时候,只需在初始值的基础上找到下一个素数即可。而当需要减小长度的时候,将当前哈希表的长度减半,然后基于这个数值找到下一个素数 -哈希表的初始大小设置为 50 - -我们使用比较暴力的方法去查找下一个素数,但事实上,通过此方法查找素数并不会耗费过多的时间与性能, -因为我们需要检查的数量是非常少的,所需的时间比遍历哈希表每个元素都要少 +哈希表的初始长度设置为 50 +我们将用比较粗暴的方法去查找下一个素数,但事实上,此方法并不会耗费过多的时间与性能,因为我们需要检查的数量其实是非常少的,所需的时间当然比遍历哈希表每个元素都要少 首先,我们把素数的相关方法保存在文件 `prime.h` 和 `prime.c` @@ -74,8 +68,8 @@ int next_prime(int x) { } ``` -下一步,需要更新 `ht_new` 方法以便支持可以根据传入的参数来生成指定大小的哈希表, -所以,需要新增一个方法 `ht_new_sized`,然后把我们之前的 `ht_new` 方法重命名为 `ht_new_sized` 并加上参数 +下一步,需要更新 `ht_new` 函数以便可以根据传入的参数来生成指定长度的哈希表, +所以,需要新增一个函数 `ht_new_sized`,然后把我们之前的 `ht_new` 函数重命名为 `ht_new_sized` 并加上参数 ```c // hash_table.c @@ -96,9 +90,9 @@ ht_hash_table* ht_new() { } ``` -到目前为止,我们已经完成了可以根据不同的参数来创建不同大小的哈希表的方法。 -下面,我们在这个方法的基础上编写调整哈希表大小的方法。 -首先,需要保证的待调整的哈希表大小不能低于预设的最小值,然后根据这个值,创建一个新的哈希表。 +到目前为止,我们已经完成了可以根据不同的参数创建不同长度的哈希表的函数。 +下面,我们在这个函数的基础上编写调整哈希表长度的函数。 +首先,需要保证的待调整的哈希表长度不能低于预设的最小值,然后根据这个值,创建一个新的哈希表。 ,接着,遍历旧的哈希表,把所有结果不为 `NULL` 以及没有被删除的元素都迁移到新的哈希表上, 当一切工作已经完成后,要记得释放旧的哈希表的内存,以防内存泄漏 diff --git a/.translations/cn/07-appendix/README.md b/.translations/cn/07-appendix/README.md index 0e0e86d..5d32a6c 100644 --- a/.translations/cn/07-appendix/README.md +++ b/.translations/cn/07-appendix/README.md @@ -23,11 +23,11 @@ #### 线性探测 -线性探测,当发生冲突的时候,顺序查看哈表表中的下一个位置,直至找到一个空的位置或遍历全表,哈希方法如下: +线性探测,当发生冲突的时候,顺序查看哈表表中的下一个位置,直至找到一个空的位置或遍历全表,哈希函数如下: - 插入:找到对应 key 的索引值,如果命中的位置为空,则把元素添加到这个位置。否则,查看冲突索引值的下个位置,直到找到一个空的位置去插入目标元素 - 查询:通过索引值定位到相应的位置,并通过比较两个 key 的值来确定待查找的元素,如果命中,则返回,否则,递增索引,重复上个过程 -- 删除:同样的通过查询方法定位到目标位置,然后删除目标元素,但是仔细想想,直接删除该位置的元素将会导致在此位置之后的元素无法被查找,因此,需要将哈希表中被删除键的右侧的所有元素重新插入到散列表 +- 删除:同样的通过查询函数定位到目标位置,然后删除目标元素,但是仔细想想,直接删除该位置的元素将会导致在此位置之后的元素无法被查找,因此,需要将哈希表中被删除键的右侧的所有元素重新插入到散列表 线性探测提供了良好的[局部性原理](https://en.wikipedia.org/wiki/Locality_of_reference),但是会遇到键簇(一组连续的条目)的问题, 使得探测的时间成本大大增加,因为每次发生冲突后可能需要遍历很长的一段距离才能找到可用的位置 @@ -35,9 +35,9 @@ #### 二次探查 此方法与线性探测类似,不同的一点在于发生冲突后,不是直接查找下一个索引位置,而是通过具有以下规律的序列来定位下个位置的索引值:`i, i + 1, i + 4, i + 9, i + 16, ...`, -其中 `i` 为发生冲突后的索引值。哈希方法如下: +其中 `i` 为发生冲突后的索引值。哈希函数如下: -- 插入:通过索引值以及序列方法查询到可用的位置,并将待添加的元素添加到此处 +- 插入:通过索引值以及序列查询到可用的位置,并将待添加的元素添加到此处 - 查询:通过索引值定位到相应的位置,并通过比较两个 key 的值来确定待查找的元素,如果命中,则返回,否则,通过序列函数计算下一个索引值,重复以上过程 - 删除:我们无法判断我们要删除的元素是否是冲突集的一部分,因此我们无法直接删除该元素而是使用标记的方法来实现删除操作 diff --git a/.translations/cn/README.md b/.translations/cn/README.md index c4e6c67..c529a54 100644 --- a/.translations/cn/README.md +++ b/.translations/cn/README.md @@ -27,8 +27,8 @@ C 很适合用于编写哈希表,主要是因为: 2. [哈希表数据结构](./02-hash-table) 3. [哈希值函数](./03-hashing) 4. [冲突处理](./04-collisions) -5. [哈希表的方法](./05-methods) -6. [调整哈希表大小](./06-resizing) +5. [哈希表函数的实现](./05-methods) +6. [调整哈希表长度](./06-resizing) 7. [附录: 处理哈希碰撞的方法与描述](./07-appendix) ## Credits From 0d3bc958290fbccda134a0391dbde067f83fa807 Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Sat, 8 Sep 2018 12:05:31 +0800 Subject: [PATCH 09/10] fix translate description --- .translations/cn/04-collisions/README.md | 4 ++-- .translations/cn/05-methods/README.md | 24 +++++++++++++++--------- .translations/cn/06-resizing/README.md | 13 ++++++++----- .translations/cn/07-appendix/README.md | 20 ++++++++++---------- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/.translations/cn/04-collisions/README.md b/.translations/cn/04-collisions/README.md index ef7542c..a093aaf 100644 --- a/.translations/cn/04-collisions/README.md +++ b/.translations/cn/04-collisions/README.md @@ -2,7 +2,7 @@ 上节提到的哈希值函数存在`无限的输入数据映射到有限数量的输出数据`,则哈希值函数必然会生成同样的哈希值,所以也就导致了存储位置的索引冲突,所以需要一个方案来处理冲突后的元素 -在本文中我们将使用一种称为开放寻址和再哈希的技术来处理这些冲突。换句话说,在发生冲突后,再哈希会通过 `i` 变量使用两次哈希值函数来计算发现冲突后的索引值。当然,处理的方案不止一种,对于其它的方案,就不在这里赘述,请查看[附录](../07-appendix) +在本文中我们将使用一种称为开放寻址和再哈希的技术来处理这些冲突。换句话说,在发生冲突后,再哈希会通过 `i` 变量使用两次哈希值函数来计算冲突后的哈希值。当然,处理的方案不止一种,对于其它的方案,就不在这里赘述,请查看[附录](../07-appendix) ## 再哈希 @@ -14,7 +14,7 @@ index = hash_a(string) + i * hash_b(string) % num_buckets 通过以上公式我们可以知道,如果没有发生任何冲突,即 `i = 0`,所以哈希值仅仅只是 `hash_a` 的返回值。如果冲突发生了,即 `i != 0`,结果很明显会根据 `hash_b` 的结果进行变化 -很显然,如果 `hash_b` 的返回结果为 `0`,公式第二项的结果也为 0,哈希表会一遍又一遍的将元素尝试插入到同一位置,那么我们的这项处理就毫无意义了,所以,需要在 `hash_b` 的返回结果后面 `+1` 来避免这样的情况出现 +显然,如果 `hash_b` 的返回结果为 `0`,公式第二项的结果也为 0,哈希表会一遍又一遍的将元素尝试插入到同一位置,那么我们的这项处理就毫无意义了,所以,需要在 `hash_b` 的返回结果后面 `+1` 来缓解这种的情况 ``` index = (hash_a(string) + i * (hash_b(string) + 1)) % num_buckets diff --git a/.translations/cn/05-methods/README.md b/.translations/cn/05-methods/README.md index 8c5202f..e5c9d17 100644 --- a/.translations/cn/05-methods/README.md +++ b/.translations/cn/05-methods/README.md @@ -11,8 +11,8 @@ void ht_delete(ht_hash_table* h, const char* key); ## 插入 -为了让一个键值对能插入到哈希表中,我们需要调用哈希值函数来生成索引值,但索引值的对应位置可能已经被使用,也就是说发生了冲突,则通过递增 `i` 的值来重新生成索引,直至找到合适的位置存储新元素。 -在插入了新元素后,我们同样需要将哈希表的 `count` 属性加一,这是为了便于[调整哈希表大小](../06-resizing) +为了让一个键值对能插入到哈希表中,我们需要调用哈希值函数来生成索引值,但索引值的对应位置可能已经被使用,也就是说发生了冲突,则可通过递增 `i` 的值来重新生成索引,直至找到合适的位置存储新元素。 +在插入了新元素后,需要将哈希表的 `count` 属性加一,这是为了便于后面[调整哈希表长度](../06-resizing) ```c // hash_table.c @@ -34,7 +34,9 @@ void ht_insert(ht_hash_table* ht, const char* key, const char* value) { ## 查找 查找过程与插入过程类似,也是通过 `key` 来生成索引值,利用索引值寻找哈希表对应位置的元素,然后通过比较元素的 `key` 与参数 `key` 来确定当前元素是否我们需要要查找的元素。 + 如果两个 `key` 不相等,则说明 `key` 在插入过程中可能发生了冲突,需要继续通过 `ht_get_hash` 来生成索引值,接着重复上个过程,直至找到与参数 `key` 一致的元素。 + 如果在查询过程中,`index` 索引返回了 `NULL`,则代表当前索引值为空,即元素不存在,结果返回 `NULL` ```c @@ -57,12 +59,14 @@ char* ht_search(ht_hash_table* ht, const char* key) { ## 删除 -删除元素的操作比插入或查找都更为复杂。在查找删除元素的过程中可能会出现冲突,如果直接删除查询结果的话,就会破坏了查询、插入等操作,从而无法定位到发生冲突后的元素。 +删除元素的操作比插入或查找都更为复杂。因为在查找删除元素的过程中可能会出现冲突,如果直接删除该查询结果的话,就会破坏了查询、插入等操作,从而无法定位到发生冲突后的元素。 + 所以,为了解决这个问题,我们不能删除这个元素,取而代之的是把这个元素做个标记,即软删除。 -(举个例子: `hello` 的索引是 `4`, `world` 的索引也是 `4`,通过冲突处理函数,`world` 重新分配到了索引为 10 的位置。 -如果直接删除索引为 `4` 的位置,在查询 `world` 的过程中就会因为 `4` 的位置返回 `NULL`,从而结果也就为 `NULL`) -我们当然需要定义一个标记值,即 `ht_item HT_DELETED_ITEM = {NULL, NULL}`,然后,把需要删除的元素的指向替换成指向 `HT_DELETED_ITEM` 元素即可 +举个例子: `hello` 的索引是 `4`, `world` 的索引也是 `4`,通过冲突处理函数,`world` 重新分配到了索引为 10 的位置。 +如果直接删除索引为 `4` 的位置,在查询 `world` 的过程中就会因为 `4` 的位置返回 `NULL`,从而结果也就为 `NULL` + +我们当然需要定义一个标记值,即 `ht_item HT_DELETED_ITEM = {NULL, NULL}`,然后,把所有待删除的元素都指向 `HT_DELETED_ITEM` 元素 ```c // hash_table.c @@ -90,9 +94,9 @@ void ht_delete(ht_hash_table* ht, const char* key) { 在删除元素后,哈希表的 `count` 属性也需要减一 -以上的操作会影响到本章开头编写的插入与查找函数,我们还需要稍微修改一下这两个函数 +由于以上的操作会影响到本章开头编写的插入与查找函数,我们还需要稍微修改一下这两个函数 -在查找的时候,我们将查询到的元素与 `HT_DELETED_ITEM` 进行比较,以此来判断该元素是否已经被删除,如果已经删除了,则跳过该元素,继续寻找下一个。 +在查询过程中,我们将查询到的元素与 `HT_DELETED_ITEM` 进行比较,以此来判断该元素是否已经被删除,如果已经删除了,则跳过该元素,继续寻找下一个。 在插入过程中,如果新增元素的索引位置恰巧命中了已删除元素的位置,则直接把新元素覆盖为旧元素 @@ -123,7 +127,9 @@ char* ht_search(ht_hash_table* ht, const char* key) { ## 更新 -当前实现的哈希表并不支持更新操作。因为当前结构不能存在两个同样的键,假如存在同样的键,那么在插入过程中必然会产生冲突,从而导致相同的键由于冲突存储在不同的索引值位置。而在查找函数过程中,通过键生成的索引值肯定是一样的,这样就导致了无法查找到后面插入进来的键值元素,将永远只命中第一个符合键的元素 +当前实现的哈希表并不支持更新操作。因为当前结构不能存在两个相同的键,假如存在相同的键,那么在插入过程中必然会产生冲突,从而导致相同的键由于冲突而存储在不同的位置。 + +在插入重复键以后,查找函数在查询过程中,由于键相同,则生成的哈希值肯定是一样的,这就导致了将永远无法查找到后面相同键的元素,且只能命中第一个先查找到的元素 为了解决这个问题,我们可以通过修改插入函数,把新的元素覆盖旧元素 diff --git a/.translations/cn/06-resizing/README.md b/.translations/cn/06-resizing/README.md index 11ab095..dbbb6f9 100644 --- a/.translations/cn/06-resizing/README.md +++ b/.translations/cn/06-resizing/README.md @@ -7,14 +7,14 @@ 如何解决以上问题?很简单,当哈希表即将要满的时候,增加哈希表的长度就可以了。 但需要制定策略:什么时候应该扩大?什么时候应该减小? -在上一节的 `count` 属性的统计就发挥了重要的作用,我们可以通过该属性与 `size` 属性来计算当前哈希表的存储容量百分比,并保存在 `load` 变量中,当满足以下条件时,则进行调整: +在上一节的 `count` 属性的统计就发挥了重要的作用,我们可以通过该属性与 `size` 属性来计算当前哈希表的存储容量百分比,并保存在 `load` 变量中,当满足以下条件时进行调整: - 当 load > 0.7,增加哈希表的长度 - 当 load < 0.1,减小哈希表的长度 -调整方案,我们可以通过创建一个新的哈希表,其长度为当前哈希表的一半或两倍,并将当前哈希表未被删除的数据迁移过去 +我们可以通过创建一个新的哈希表,其长度为当前哈希表的一半或两倍,并将当前哈希表未被删除的数据迁移到新的哈希表 -新的哈希表为当前哈希表长度的一半或者两倍的素数,但找到新的长度并不是一件简单的事情,为此,需要设置一个初始值,当需要增加哈希表长度的时候,只需在初始值的基础上找到下一个素数即可。而当需要减小长度的时候,将当前哈希表的长度减半,然后基于这个数值找到下一个素数 +新的哈希表长度为旧哈希表长度的一半或者两倍的素数,但找到新的长度并不是一件简单的事情,为此,需要设置一个初始值,当需要增加哈希表长度的时候,只需在初始值的基础上找到下一个素数即可。而当需要减小长度的时候,将旧哈希表的长度减半,然后基于这个数值找到下一个素数 哈希表的初始长度设置为 50 @@ -91,9 +91,12 @@ ht_hash_table* ht_new() { ``` 到目前为止,我们已经完成了可以根据不同的参数创建不同长度的哈希表的函数。 + 下面,我们在这个函数的基础上编写调整哈希表长度的函数。 -首先,需要保证的待调整的哈希表长度不能低于预设的最小值,然后根据这个值,创建一个新的哈希表。 -,接着,遍历旧的哈希表,把所有结果不为 `NULL` 以及没有被删除的元素都迁移到新的哈希表上, + +首先,需要保证哈希表长度不能低于预设的最小值,然后根据这个值,创建一个新的哈希表。 + +接着,遍历旧的哈希表,把所有结果不为 `NULL` 以及没有被删除的元素都迁移到新的哈希表上, 当一切工作已经完成后,要记得释放旧的哈希表的内存,以防内存泄漏 ```c diff --git a/.translations/cn/07-appendix/README.md b/.translations/cn/07-appendix/README.md index 5d32a6c..405cb08 100644 --- a/.translations/cn/07-appendix/README.md +++ b/.translations/cn/07-appendix/README.md @@ -7,19 +7,18 @@ ### 拉链法 -拉链法,每个哈希元素都指向一个链表,当发生碰撞时候,把冲突的元素直接新增到链表中,方法如下: +拉链法,每个哈希元素都指向一个链表,当发生碰撞时候,将冲突元素直接追加到链表,方法如下: - 插入:先通过哈希函数返回存储位置的索引值。如果当前位置还没被占用,则直接把元素添加到此位置。如果当前位置已经被占用了,则添加到链表的末端 -- 查询:通过索引值定位到待查找元素的位置,然后遍历当前位置的链表,如果找到则返回,否则,返回 `NULL` -- 删除:和查找也是同样的道理,先找到目标元素,如果命中,则把当前元素从链表中删除,如果当前链表只有一个元素,则设置索引位置为 `NULL` 即可 +- 查询:通过索引值定位到待查找元素的位置,然后遍历当前位置的链表,如果找到则直接返回结果,否则,返回 `NULL` +- 删除:和查找也是同样的道理,先找到目标元素,如果命中,则把当前元素从链表中删除,如果当前链表只有一个元素,则将当前元素标记为 `NULL` -拉链法的实现虽然很简单,但是空间效率太低了。因为哈希表的每个位置都需要存储一个指向链表的指针, -然而指针是会消耗内存的,实际上可以利用这些内存来存储更多的元素 +拉链法的实现虽然很简单,但是空间的利用率不高。因为哈希表的每个位置都需要存储一个指向链表的指针,而指针是会消耗内存的,实际上可以利用这些内存来存储更多的元素 ### 开放地址 开放地址可以解决拉链法所带来的空间浪费问题。当冲突发生后,冲突的元素应该被放置在哈希表其它空余的位置上。 -但是这个位置当然不是随意选取,如果随意选取的话,在查找的时候就无法查找到目标元素了。所以需要制定一套策略,供插入,查询使用 +但是这个位置当然不是随意选取,如果随意选取的话,在查找的时候就无法查找到目标元素了 #### 线性探测 @@ -29,12 +28,12 @@ - 查询:通过索引值定位到相应的位置,并通过比较两个 key 的值来确定待查找的元素,如果命中,则返回,否则,递增索引,重复上个过程 - 删除:同样的通过查询函数定位到目标位置,然后删除目标元素,但是仔细想想,直接删除该位置的元素将会导致在此位置之后的元素无法被查找,因此,需要将哈希表中被删除键的右侧的所有元素重新插入到散列表 -线性探测提供了良好的[局部性原理](https://en.wikipedia.org/wiki/Locality_of_reference),但是会遇到键簇(一组连续的条目)的问题, +线性探测提供了良好的[缓存性能](https://en.wikipedia.org/wiki/Locality_of_reference),但是会遇到键簇(一组连续的条目)的问题, 使得探测的时间成本大大增加,因为每次发生冲突后可能需要遍历很长的一段距离才能找到可用的位置 #### 二次探查 -此方法与线性探测类似,不同的一点在于发生冲突后,不是直接查找下一个索引位置,而是通过具有以下规律的序列来定位下个位置的索引值:`i, i + 1, i + 4, i + 9, i + 16, ...`, +此方法与线性探测类似,不同点在于发生冲突后,不是直接查找下一个索引位置,而是通过具有以下序列来定位下个位置的索引值:`i, i + 1, i + 4, i + 9, i + 16, ...`, 其中 `i` 为发生冲突后的索引值。哈希函数如下: - 插入:通过索引值以及序列查询到可用的位置,并将待添加的元素添加到此处 @@ -46,7 +45,8 @@ #### 再哈希 二次探查仍然可能存在长键簇带来的问题,而再哈希法正是为了解决此问题。为此, -当发生冲突的时候,我们将使用第二个哈希函数作为新索引值,且索引值应该是均匀分布的。 -在大多数的哈希表的实现上都使用再哈希法来解决冲突问题,同时,也是我们本教程所使用的方法 +当发生冲突的时候,使用二次哈希值的结果作为新的哈希值,且哈希值很大程度上是均匀分布的。 + +大多数哈希表的实现都使用再哈希法来解决冲突的问题,同时,也是我们本教程所使用的方法 From 0c20d27cc9d2afe8bb40bf1fd49a61b663ff15aa Mon Sep 17 00:00:00 2001 From: young <21738331@qq.com> Date: Wed, 12 Sep 2018 08:09:26 +0800 Subject: [PATCH 10/10] add cn link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 550a3c8..7e61d4d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[](/README.md) [](/.translations/fr/README.md) +[](/README.md) [](/.translations/fr/README.md) [](/.translations/cn/README.md) # Write a hash table in C