diff --git a/.translations/cn/01-introduction/README.md b/.translations/cn/01-introduction/README.md new file mode 100644 index 0000000..aceebd3 --- /dev/null +++ b/.translations/cn/01-introduction/README.md @@ -0,0 +1,55 @@ +# 介绍 + +哈希表是基于关联数组 [API](#API) 实现的一种快速存储与检索数据的数据结构, +为了让大家更好的阅读本文,我在文章底部的[摘要](#摘要)添加了一些相关术语的解析 + +哈希表是由一系列的"桶"组成,每个桶都存储一组键值对。为了确定存储键值对的桶放置的位置, +我们通过将键作为参数传递给哈希函数,然后哈希函数生成一个整型索引值作为数组的索引。 +当需要检索数据时,也是同样的通过此哈希函数生成相应的索引来找到对应的元素 + +由于数组的存取复杂度均为 `O(1)`,而哈希表是借助数组实现的,所以哈希表具有快速存储和检索数据的特性 + +本文将要实现的哈希表仅支持字符类型的键值对,原则上应当支持任意类型的键值对, +但由于 UNICODE 的编码并不容易实现且超出了本篇文章的范围,所以此处只支持 ASCII 的编码类型 + +## API + +关联数组是一组无序键值对的集合,故插入重复的键是无效的,且具有如下方法: + +- `search(a, k)`: 关联数组 `a` 通过参数键 `k` 返回 `v`,如果查询失败,则返回 NULL +- `insert(a, k, v)`: 在关联数组 `a` 插入键值对 `k:v` +- `delete(a, k)`: 通过键 `k` 删除关联数组 `a` 中对应的键值对,若键不存在,则不做任何操作 + +## 起始 + +由于本文是由 C 编写的,如果你还不清楚 C,可以参考 [Daniel Holden's](https://github.com/orangeduck) 编写的 +《[Build Your Own Lisp](http://www.buildyourownlisp.com/chapter2_installation)》,这是一本非常不错的书籍,如果可以的话,我建议你读一读 + +## 代码结构 + +本教程的代码结构如下: + +``` +. +├── build +└── src + ├── hash_table.c + ├── hash_table.h + ├── prime.c + └── prime.h +``` + +`src` 文件夹放置我们将要编写的代码,`build` 文件夹放置编译后的二进制文件 + +## 摘要 + +很多技术名词都有各种不同的叫法,为了便于大家的理解,在本文中,我们统一用以下命名: + +- 关联数组: 通过 [API](#api) 实现的一种抽象的数据结构。也称为映射,符号表,字典 + +- 哈希表: 通过散列函数实现的快速存储与检索的关联数组。也称为哈希映射,映射,哈希或字典。 + +关联数组可以用许多不同的底层数据结构实现,一种比较低性能且较为简单的方法是把元素直接存储在数组中,在检索的时候逐个遍历对比。因为哈希表通常都是由关联数组来实现的,所以人们经常把关联数组和哈希表的称呼混在一起 + +下一节: [哈希表结构](../02-hash-table) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/02-hash-table/README.md b/.translations/cn/02-hash-table/README.md new file mode 100644 index 0000000..2fe6f4c --- /dev/null +++ b/.translations/cn/02-hash-table/README.md @@ -0,0 +1,95 @@ +# 哈希表结构 + +哈希表的元素由以下结构 `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` 的结构体,然后将键值对参数存储到结构体对应的属性,最后,将该方法标记为 `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 的长度,在后面的章节中,我们将会通过 [调整哈希表大小](../06-resizing) 来动态调整哈希表的存储大小。 + +使用 `calloc` 初始化 `items` 属性,这里用 `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/cn/README.md#目录) diff --git a/.translations/cn/03-hashing/README.md b/.translations/cn/03-hashing/README.md new file mode 100644 index 0000000..839a859 --- /dev/null +++ b/.translations/cn/03-hashing/README.md @@ -0,0 +1,75 @@ +# 哈希值函数 + +在这一节中,将编写哈希值函数来计算哈希索引值 + +哈希值函数应当满足以下要求: + +- 输入一个字符串,返回一个 `0` - `m` 的整型数字, `m` 不大于哈希的长度,即上一节中 `ht_hash_table` 的 `size` + +- 索引值结果必须均匀分布,如果计算的索引值分布不均匀,会导致大多数键都落在同一索引,且在哈希表插入过程中将会产生大量[冲突](#不符合预期的问题数据),冲突将会在很大程度上降低哈希表的性能 + +## 算法 + +我们将使用字符串来计算哈希值,下面是该算法的伪代码 + +``` +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 +``` + +从上面的伪代码可以看出,要生成一个哈希值,我们需要做以下两步操作: + +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; +} +``` + +## 不符合预期的问题数据 + +理想的哈希值函数无论输入的参数是什么,其结果都是均匀分布的。然而,无论怎么优化哈希值函数,都很难生成唯一的哈希值,肯定会出现 `不符合预期` 的键,这些键生成的哈希值往往会与其它键所生成哈希值一致 + +这类问题数据集的存在,意味着没有完美的哈希值函数可以生成唯一的哈希值。所以我们所能做的就是想办法处理出现问题的数据 + +问题数据同时也存在安全性问题。如果某些恶意用户通过不断提供冲突的键给程序,那么当查询的时候就会比预期时间 `O(1)` 花费更多的时间 `O(n)` 才能找到目标值。这可以用作针对以哈希表(例如DNS和某些Web服务)为基础实现的程序进行 DDOS + +下一节: [处理哈希冲突](../04-collisions) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/04-collisions/README.md b/.translations/cn/04-collisions/README.md new file mode 100644 index 0000000..a093aaf --- /dev/null +++ b/.translations/cn/04-collisions/README.md @@ -0,0 +1,37 @@ +## 哈希冲突 + +上节提到的哈希值函数存在`无限的输入数据映射到有限数量的输出数据`,则哈希值函数必然会生成同样的哈希值,所以也就导致了存储位置的索引冲突,所以需要一个方案来处理冲突后的元素 + +在本文中我们将使用一种称为开放寻址和再哈希的技术来处理这些冲突。换句话说,在发生冲突后,再哈希会通过 `i` 变量使用两次哈希值函数来计算冲突后的哈希值。当然,处理的方案不止一种,对于其它的方案,就不在这里赘述,请查看[附录](../07-appendix) + +## 再哈希 + +在发生冲突后,哈希值可根据 `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) +[目录](/.translations/cn/README.md#目录) diff --git a/.translations/cn/05-methods/README.md b/.translations/cn/05-methods/README.md new file mode 100644 index 0000000..e5c9d17 --- /dev/null +++ b/.translations/cn/05-methods/README.md @@ -0,0 +1,155 @@ +# 哈希表 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` 属性也需要减一 + +由于以上的操作会影响到本章开头编写的插入与查找函数,我们还需要稍微修改一下这两个函数 + +在查询过程中,我们将查询到的元素与 `HT_DELETED_ITEM` 进行比较,以此来判断该元素是否已经被删除,如果已经删除了,则跳过该元素,继续寻找下一个。 +在插入过程中,如果新增元素的索引位置恰巧命中了已删除元素的位置,则直接把新元素覆盖为旧元素 + + +```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#目录) diff --git a/.translations/cn/06-resizing/README.md b/.translations/cn/06-resizing/README.md new file mode 100644 index 0000000..dbbb6f9 --- /dev/null +++ b/.translations/cn/06-resizing/README.md @@ -0,0 +1,172 @@ +# 调整哈希表长度 + +到目前为止,我们的哈希表长度都是固定为 `53` ,但随着越来越多的元素被添加进来,哈希表在有限的空间内将会被塞满,这可能会导致两个问题: + +1. 哈希表的性能会随着高冲突率而下降 +2. 哈希表只能存储固定长度的元素,无法添加更多的元素 + +如何解决以上问题?很简单,当哈希表即将要满的时候,增加哈希表的长度就可以了。 +但需要制定策略:什么时候应该扩大?什么时候应该减小? +在上一节的 `count` 属性的统计就发挥了重要的作用,我们可以通过该属性与 `size` 属性来计算当前哈希表的存储容量百分比,并保存在 `load` 变量中,当满足以下条件时进行调整: + +- 当 load > 0.7,增加哈希表的长度 +- 当 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#目录) diff --git a/.translations/cn/07-appendix/README.md b/.translations/cn/07-appendix/README.md new file mode 100644 index 0000000..405cb08 --- /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 的值来确定待查找的元素,如果命中,则返回,否则,通过序列函数计算下一个索引值,重复以上过程 +- 删除:我们无法判断我们要删除的元素是否是冲突集的一部分,因此我们无法直接删除该元素而是使用标记的方法来实现删除操作 + +通过以上策略,二次探查比较好的缓解了线性探测的长键簇问题 + +#### 再哈希 + +二次探查仍然可能存在长键簇带来的问题,而再哈希法正是为了解决此问题。为此, +当发生冲突的时候,使用二次哈希值的结果作为新的哈希值,且哈希值很大程度上是均匀分布的。 + +大多数哈希表的实现都使用再哈希法来解决冲突的问题,同时,也是我们本教程所使用的方法 + + diff --git a/.translations/cn/README.md b/.translations/cn/README.md new file mode 100644 index 0000000..c529a54 --- /dev/null +++ b/.translations/cn/README.md @@ -0,0 +1,36 @@ +[](/README.md) [](/.translations/fr/README.md) [](/.translations/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 属于底层语言,所以可以深入的了解到在机器层面上的程序是如何运作的 + +本教程是假设你已熟悉 C 的语法。由于本教程的代码相对简单,大多数问题都可以通过搜索引擎来解决。 +如果你遇到其它问题,请打开 Github 的 [Issue](https://github.com/jamesroutley/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 0000000..ccc90e6 Binary files /dev/null and b/.translations/flags/cn.png differ 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