Skip to content

translate to chinese #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .translations/cn/01-introduction/README.md
Original file line number Diff line number Diff line change
@@ -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#目录)
95 changes: 95 additions & 0 deletions .translations/cn/02-hash-table/README.md
Original file line number Diff line number Diff line change
@@ -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 <stdlib.h>
#include <string.h>

#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#目录)
75 changes: 75 additions & 0 deletions .translations/cn/03-hashing/README.md
Original file line number Diff line number Diff line change
@@ -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#目录)
37 changes: 37 additions & 0 deletions .translations/cn/04-collisions/README.md
Original file line number Diff line number Diff line change
@@ -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#目录)
Loading