实现hash表
一、如何理解hash?
-
C语言中,如果数据被存放到数组中 。可以使用 arr[3] 这种方法直接访问arr数组的第四个元素。
-
这是一种非常快的访问方法。使用下标访问。
-
把数组理解成hash表,而这个3 理解成key值。那么可以得到:
int hash_arr_get_value(int *arr,int key) { return arr[key]; }
-
那如果这个key值不是一个数字呢?比如说是一个字符串。我需要通过一个字符串,得到对应的值。比如,我需要通过名字得到手机号码:
char *hash_arr_get_number(char **arr,char *str_key) { /*需要先把number转成一个key值。*/ unsigned int key = hash(str_key); /*然后访问真正的数据*/ return arr[key]; }
-
4中的 arr,就是一个hash表。而把key从别的数据转成一个整数下标的函数,就是hash函数。
-
使用这种hash表存储,当要查询数据的时候,不用每次遍历查找,只需要通过固定计算,使用下标查找到想要的结果。算法时间复杂度为O(1).
二、使用C语言实现一个hash
-
具体代码参考这里:hash表实现;
-
简单的讲解:
-
我写的这个hash依赖了 value 的模块。这是为了方便管理hash表的key和值。如果写死了key是string,value也是string,后续的扩展就变得十分困难。
/* 将value抽象一下,以后不管是字符串,还是函数指针,还是别的什么值,都可以通过修改抽象的成员解决。 */ typedef int (*on_destory_cb)(void *); typedef struct value_t { void *p; int size; on_destory_cb on_destroy; }value_t;
-
value_create 和 value_destroy是对应的。如果创建了,就要销毁。因为C语言需要考虑分配空间和释放空间。
-
hash表操作函数。hash表也是一种存储方式。也是需要增删改查的操作。
/** * 创建一个hash表。输入 表的大小和hash函数。对于不同的场景,使用不同的hash函数。 */ fv_hash_t *hash_create(int size,hash_calculate_cb hash_fn,hash_cmp_cb hash_cmp); /** * 向hash表中添加元素。如果key值已经存在,会修改原来的元素。 */ int hash_update(fv_hash_t *hash,value_t *key,value_t *data); /** * 从hash表中删除key值对应的元素。如果元素不存在,返回-1.删除失败返回-1. */ int hash_remove(fv_hash_t * hash,value_t *key); /** * 获取hash表中key值对应的data数据的指针。 */ value_t *hash_get(fv_hash_t *hash,value_t *key); /** * 遍历hash表中的元素。key值和data的值。还有参数。 */ int hash_foreach(fv_hash_t *hash,hash_visit_cb on_visit,void *param); /** * 计算hash表中有多少元素 */ int hash_get_size(fv_hash_t *hash); /** * 销毁整个hash表。 */ int hash_destory(fv_hash_t *hash);
-
三、如何解决hash冲突
-
使用外部拉链法解决hash冲突。也就是,如果新插入一个数据到A槽,A槽已经被占用了。怎么办?就弄个链表,把新的数据插入到原来数据所在的来链表中。
-
简单看看hash_update函数
/** * 向hash表中添加元素。如果key值已经存在,会修改原来的元素。 * 如果key值已经存在,就是用头插法,插到最前面。这样用户检查的时候永远得到的是最前面的数据。 * 这里的实现需要完善。因为如果用户一直插一样的值,会浪费内存。我不想加删除的代码,是因为担心一直循环查找会不会降低性能。 */ int hash_update(fv_hash_t *hash,value_t *key,value_t *data) { return_value_if_fail(hash != NULL && key != NULL && data != NULL,-1); fv_hash_elem_t *hash_elem = NULL; /*hash函数非空*/ if(hash->hash_fn) { /*计算key值*/ unsigned int key_index = hash->hash_fn(key->p)%hash->capacity; /*创建 fv_hash_elem_t 对象。*/ hash_elem = hash_elem_create(key,data); return_value_if_fail(hash_elem,-1); /*如果当前hash槽已经被占用*/ if(hash->table[key_index] != NULL) { hash_elem->next = hash->table[key_index]; hash->table[key_index] = hash_elem; } else { hash->table[key_index] = hash_elem; } hash->size++; } return 0; }
四、如何保证hash的性能
-
创建大小合理的散列表。
-
负载 因子:是当前插入到表的元素个数和可用槽的总数的比,得到的结果。
a = n/table_size
-
如果负载因子变大,性能会下降。如果小于0.2,再让它变小,对于性能的影响特别小。
-
我觉得可能0.5-1会好些吧。
-
-
确保散列表的槽数是一个素数。
-
需要使用代表性数据测试。最好只使用一个除法运算,就是最后n%table_size的模运算。
-
考虑冲突,在可能的地方使用外部拉链法。
五、使用
- 参考 示例代码。
- 使用场景:
- 频繁的查找,而且对性能有要求的地方。需要使用hash表。