varch/doc/varch:dict容器(哈希表).md
2024-04-22 00:09:51 +08:00

370 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 介绍
dict字典是逻辑上离散的容器与set容器很相似set容器是以`index - data`形式存在而dict容器是以`key - value`形式存在set的index是整型数dict的key为字符串这里和python的dict不太一样python的key除了可以字符串还可以整型或者元组等varch类似python的dict叫做map映射set的data和dict的value本质是同一个东西。
varch的dict容器在底层实现上采用了**哈希表**查找迅速而且也支持随机访问占用空间相对较小。可以通过迭代器遍历dict。
## 接口
### 创建和删除dict对象
```c
dict_t dict_create(int dsize);
void dict_delete(dict_t dict);
#define dict(type) // 为了更简便的使用对dict_create套一层宏定义
#define _dict(dict) // 对dict_delete套一层宏定义并在dict删除后置为空
```
其中**dict_t**为dict的结构体创建方法则会返回一个空的dict对象创建失败则返回NULL其中`dsize`传入数据的大小。删除方法则是删除传入的dict对象。创建方法和删除应该成对使用创建出来在结束使用应该删除掉。
```c
void test(void)
{
dict_t dict = dict(int); // 定义并创建一个int型的dict
_dict(dict); // 成对使用,用完即删除
}
```
### dict的插入和移除
```c
void* dict_insert(dict_t dict, const char *key, void *value);
int dict_erase(dict_t dict, const char *key);
```
dict通过hash值来定位的,可以很快的通过hash值定位到数据存储的位置
插入的方法是添加指定key并将数据复制到这个键(在其中,value传入NULL时则只是开辟空间,不进行赋值),在插入key的过程中会进行查重,保证key的唯一性,插入成功后返回插入后的数据的地址,插入失败则是返回NULL。而移除则是移除指定键的数据,成功返回1,失败返回0
### dict数据的读写
```c
void* dict_value(dict_t dict, const char *key);
void* dict_error(dict_t dict);
#define dict_at(dict, type, key)
```
`dict_value`方法就是根据键来获取数据的地址,返回的则是指定的数据的地址,`dict_error()`则是失败。而`dict_at`则是在`dict_value`的基础上加多类型,`dict_value`具备读写保护机制,因为返回的是`dict_error()`而不是NULL所以在使用`dict_at`方法`i`写错了就会修改`dict_error()`指向的内容,而不会导致奔溃。
dict的随机访问是通过计算出键的哈希值根据哈希值定位在哈希表中的数据。
```c
void test(void)
{
dict_t dict = dict(int);
int value;
value = 100; dict_insert(dict, "hello", &value);
value = 1; dict_insert(dict, "ZhangSan", &value);
value = 2; dict_insert(dict, "LiSi", &value);
value = 3; dict_insert(dict, "WangWu", &value);
value = 4; dict_insert(dict, "SunLiu", &value);
value = 5; dict_insert(dict, "QianQi", &value);
printf("dict[hello] = %d\r\n", dict_at(dict, int, "hello"));
printf("dict[SunLiu] = %d\r\n", dict_at(dict, int, "SunLiu"));
_dict(dict);
}
```
结果:
```
dict[hello] = 100
dict[SunLiu] = 4
```
### dict的大小和和数据大小
```c
int dict_size(dict_t dict);
int dict_dsize(dict_t dict);
```
dict的`size`很好理解,也就是像数组那样的大小,`dsize`也就是创建时候传入的数据的大小。
```c
void test(void)
{
dict_t dict = dict(int);
int value;
value = 100; dict_insert(dict, "hello", &value);
value = 1; dict_insert(dict, "ZhangSan", &value);
value = 2; dict_insert(dict, "LiSi", &value);
value = 3; dict_insert(dict, "WangWu", &value);
value = 4; dict_insert(dict, "SunLiu", &value);
value = 5; dict_insert(dict, "QianQi", &value);
printf("size = %d, value size = %d\r\n", dict_size(dict), dict_dsize(dict));
_dict(dict);
}
```
结果:
```
size = 6, value size = 4
```
### dict查找
```c
int dict_find(dict_t dict, const char *key);
```
这个方法其实套`dict_value`实现只是find成功返回1失败返回0。
### dict迭代器
```c
void dict_it_init(dict_t dict);
void* dict_it_get(dict_t dict, char **key);
```
dict也支持内置的迭代器但主要dict的迭代器用于遍历。因为数组要遍历的时候是知道键从0开始逐一递增遍历的。但是dict是离散型的key无法通过这种逐一递增的方式进行遍历所以这里给定了两个迭代器函数用于遍历dict。
`dict_it_init`初始化迭代器。`dict_it_get`获取迭代,更新迭代位置,`*key`为输出的key当前所在的key也可以传入NULL不接收返回迭代位置的数据。
通过`dict_size`来把控迭代次数。
```c
void test(void)
{
dict_t dict = dict(int);
int value;
char *key;
void *data;
int i;
value = 100; dict_insert(dict, "hello", &value);
value = 1; dict_insert(dict, "ZhangSan", &value);
value = 2; dict_insert(dict, "LiSi", &value);
value = 3; dict_insert(dict, "WangWu", &value);
value = 4; dict_insert(dict, "SunLiu", &value);
value = 5; dict_insert(dict, "QianQi", &value);
dict_it_init(dict, DICT_HEAD);
i = dict_size(dict);
while (i--)
{
data = dict_it_get(dict, &key);
printf("dict[%s] = %d\r\n", key, *(int *)data);
}
_dict(dict);
}
```
结果:
```
dict[LiSi] = 2
dict[QianQi] = 5
dict[SunLiu] = 4
dict[WangWu] = 3
dict[ZhangSan] = 1
dict[hello] = 100
```
## 源码解析
### dict结构体
dict容器的所有结构体都是隐式的也就是不能直接访问到结构体成员的这样子的方式保证了模块的独立与安全防止外部调用修改结构体的成员导致dict存储结构的破坏。所以dict解析器只留了唯一一个dict的声明在头文件然后结构体的定义都在源文件。只能使用dict容器提供的方法对dict对象进行操作。
dict类型声明
```c
typedef struct DICT *dict_t;
```
使用时候,只是用`dict_t`即可。
```c
/* dict type define */
typedef struct DICT
{
groove_t *base; /* base address for groove data */
void *error; /* error space */
int vsize; /* size of value */
unsigned int size; /* size of dict */
unsigned int capacity; /* capacity of dict */
unsigned int it; /* iterator index */
} DICT;
```
`DICT`结构体中包含了6个成员`base`(哈希表的基地址),`error`dict的错误区当随机访问到不存在的key时候就会返回这个错误的地址如此在使用`at`方法时候不会操作到无效的内存地址),`size`dict的大小也就是dict的长度`vsize`(每个数据的大小),`capacity`(哈希表的容量),`it`(迭代器遍历的时候,记录当前访问的哈希表索引)。
dict容器最主要的问题就是解决哈希冲突以及哈希表容量调整的问题。
```c
/* dict node type define */
typedef struct
{
unsigned int hash; /* hash value */
char *key; /* key */
void *value; /* value */
} GROOVE, *groove_t;
```
在dict里面数据通过数组进存储每个数组项只是存储每个槽groove的地址每个槽里面就存储3部分的内容当前这个槽在当前的hash表中的hash值为了更快的查找hash冲突的键值对以及实际存储的键值对。
```
+------+------------------------+------------------------+
| hash | key | value |
+------+------------------------+------------------------+
| ... | ... | ... |
+------+------------------------+------------------------+
| ... | ... | ... |
+------+------------------------+------------------------+
```
### dict创建及删除
```c
dict_t dict_create(int vsize)
{
dict_t dict;
if (vsize <= 0) return NULL;
dict = (dict_t)malloc(sizeof(DICT));
if (!dict) return NULL;
dict->error = malloc(vsize);
if (!dict->error) { free(dict); return NULL; }
dict->base = NULL;
dict->vsize = vsize;
dict->size = 0;
dict->capacity = 0;
dict->it = 0;
return dict;
}
```
dict的创建只是创建了一个空的dict对基本参数进行了初始化。而删除方法就是释放在对dict操作的过程中分配的内存空间。
### dict的插入
```c
void* dict_insert(dict_t dict, const char *key, void *value)
{
groove_t groove = NULL;
unsigned int hash = 0, index;
int len = 0;
/*
对传入的形参有效性的检查
*/
if (!dict) return NULL;
if (!key) return NULL;
/*
检查大小有没有超过哈希表容量的 3/4 了,超过 3/4 就对哈希表进行扩容
因为超过 3/4 说明哈希表已经很满了,查找的效率会大大降低,所以就要保持哈希表一些空闲的空间
这里扩容是呈2的指数依次变化的也就是 4、8、16、32 ... 这样子增长
*/
/* the current capacity affects the search rate and needs to be expanded */
if (dict->size >= ((dict->capacity >> 2) + (dict->capacity >> 1))) /* size exceeds 3/4 of capacity */
{
/*
在扩容的过程中,先是重新分配一块新容量的空间
然后把旧的哈希表里面的成员,一个个重新哈希插回到新的哈希表中
*/
/* allocate new hash table space */
if (!dict_resize(dict, dict->capacity < MIN_CAPACITY ? MIN_CAPACITY : dict->capacity << 1)) return NULL;
}
/*
计算插进来的key的哈希值默认使用了bkdr哈希算法让hash值对哈希表容量进行取模
然后在检查这个hash值有没有冲突了冲突了的话通过开放定址法线性+1向后寻找空闲空间
在向后寻找的过程中检查相应槽中的hash值hash值为-1表示被erase方法移除了而实际槽没有被删除的标识也是视为这个槽可以用
*/
/* find a free groove */
len = strlen(key);
hash = hash_bkdr((void *)key, len) % dict->capacity;
index = hash;
while (dict->base[index] && dict->base[index]->hash != -1)
{
index = (index + 1) % dict->capacity;
if (index == hash) return NULL;
}
/*
分配槽的空间
*/
/* space allocation */
groove = dict->base[index];
if (!groove) groove = (groove_t)malloc(sizeof(GROOVE));
if (!groove) return NULL;
groove->key = (char *)malloc(len + 1);
if (!groove->key) { if (!dict->base[index]) free(groove); return NULL; }
groove->value = malloc(dict->vsize);
if (!groove->value) { free(groove->key), groove->key = NULL; if (!dict->base[index]) free(groove); return NULL; }
/* assign */
groove->hash = hash; // 记录当前槽的哈希值
strcpy(groove->key, key);
if (value) memcpy(groove->value, value, dict->vsize);
/* insert */
dict->base[index] = groove;
dict->size++;
return groove->value;
}
```
实际存储例子:
```c
value = 100; dict_insert(dict, "hello", &value);
value = 1; dict_insert(dict, "ZhangSan", &value);
value = 2; dict_insert(dict, "LiSi", &value);
value = 3; dict_insert(dict, "WangWu", &value);
value = 4; dict_insert(dict, "SunLiu", &value);
value = 5; dict_insert(dict, "QianQi", &value);
value = 8; dict_insert(dict, "WangBa", &value);
value = 9; dict_insert(dict, "LiuJiu", &value);
```
哈希表
```
+------+------------------------+------------------------+
| hash | key | value |
+------+------------------------+------------------------+
| 0 | ZhangSan | 1 |
+------+------------------------+------------------------+
| 1 | QianQi | 5 |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| 4 | SunLiu | 4 |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| 6 | WangBa | 8 |
+------+------------------------+------------------------+
| 7 | LiSi | 2 |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| 9 | WangWu | 3 |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| | | |
+------+------------------------+------------------------+
| 14 | hello | 100 |
+------+------------------------+------------------------+
| 14 | LiuJiu | 9 |
+------+------------------------+------------------------+
```
### dict的移除
```c
int dict_erase(dict_t dict, const char *key)
{
groove_t groove;
unsigned int index, next;
if (!dict) return 0;
if (!key) return 0;
/*
要移除就得先找到这个key所在哈希表的位置
find_index就是根据类似前面的插入的方法同逻辑没找到就返回-1
*/
index = find_index(dict, key);
if (index == -1) return 0;
groove = dict->base[index];
if (groove->key) { free(groove->key); groove->key = NULL; } // 把key释放并置为NULL
if (groove->value) { free(groove->value); groove->value = NULL; } // 把value释放并置为NULL
groove->hash = -1; // 槽就不真正释放而是把hash值标志为-1-1哈希值在哈希表中用不上
dict->size--;
/*
如同插入方法,插入要适配大小容量,移除也是
当大小小于容量 1/4 时候,就调整容量,缩小到一般,这样就占用 1/2 了
*/
if (dict->capacity > MIN_CAPACITY && dict->size <= (dict->capacity >> 2)) /* size less than 1/4 of capacity */
{
dict_resize(dict, dict->capacity >> 1);
}
return 1;
}
```