哈希表 HashTable(又名散列表)
简介
其实通过标题上哈希表的英文名HashTable,我们就可以看出这是一个组合的数据结构Hash+Table。
Hash是什么?它是一个函数,作用可以通过一个公式来表示: index = HashFunction(key),通过hash函数计算出一个固定的值,这个值就是哈希表中的索引。
Table是什么?它可以看作是一个数组array,作用是存储Hash函数计算出来的值。
当然除了这2个结构外,还有key和value值需要存储,这2个值可以用一个链表来存储。
为什么哈希表使用这么广泛
哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况下和链表的性能一样为O(n)。 不过通常并不会这么坏,合理设计的哈希算法能有效的避免这类情况,通常哈希表的这些操作时间复杂度为O(1)。
所以PHP中使用了HashTable存储各种数据,JAVA中也有HashMap,HashTable 等数据结构。
基本概念
哈希表是一种通过哈希函数,将特定的键映射到特定值的一种数据结果,它维护键和值之间的一一对应关系。
- 键(key):用于操作数据标识。
- 槽(slot/bucket):哈希表中用于保存数据的单元,数据真正存放的容器。
- 哈希函数(hash function):将key映射到应该存放的slot所在位置的函数。
哈希表和数组区别
哈希表可以理解为数组的扩展,数组一般是使用索引下标来寻址。
如果关键字key的索引范围较小且是数字,我们可以使用数组来存放。
如果关键字key的范围比较大,用数组的话,申请的内存空间就比较大了。这样内存空间利用率就比较低效。
所以人们开始想办法,能不能有一种方法,把它映射到特定的区域,这个“方法”就是哈希函数。
index = HashFunction(key)
hash冲突
我们用hash函数映射数据的时候,可能会出现不同key通过hash函数映射到了同一个索引上的情况,及是说不同的key通过hash函数计算得出了相同的值,这就是hash冲突。怎么解决呢? 一般由2种方法:链接法和开放寻址法
所以hash函数的算法显得很重要。
链接法
链接法是通过一个链表来保存冲突的值,也就是不同的key映射到一个槽中的时候,用链表来保存这些值。
开放寻址法
使用开放寻址法是槽本身直接存放数据,在插入数据时如果key所映射到的索引已经有数据了,这说明发生了冲突,这是会寻找下一个槽,如果该槽也被占用了则继续寻找下一个槽,直到寻找到没有被占用的槽,在查找时也使用同样的策略来进行。
由于开放寻址法处理冲突的时候占用的是其他槽位的空间,这可能会导致后续的key在插入的时候更加容易出现哈希冲突,所以采用开放寻址法的哈希表的装载因子不能太高,否则容易出现性能下降。
装载因子:是哈希表保存的元素数量和哈希表容量的比,通常采用链接法解决冲突的哈希表的装载 因子最好不要大于1,而采用开放寻址法的哈希表最好不要大于0.5。
哈希表的实现
在了解到哈希表的原理之后要实现一个哈希表也很容易,主要需要完成的工作只有三点:
- 实现哈希函数
- 冲突的解决
- 操作接口的实现
- 扩容
数据结构
首先我们需要一个容器来保存我们的哈希表,哈希表需要保存的内容主要是保存进来的的数据,同时为了方便的得知哈希表中存储的元素个数,需要保存一个大小字段,第二个需要的就是保存数据的容器了。
作为实例,下面将实现一个简易的哈希表。基本的数据结构主要有两个,一个用于保存哈希表本身,另外一个就是用于实际保存数据的单链表了,定义如下:
// 存储键值
typedef struct _Bucket
{
char *key;
void *value;
struct _Bucket *next;
} Bucket;
typedef struct _HashTable
{
int size;
int elem_num;
Bucket** buckets;
} HashTable;
为了简化,key的数据类型为字符串,而存储的数据类型可以为任意类型。
Bucket结构体是一个单链表,这是为了解决多个key哈希冲突的问题,也就是前面所提到的的链接法。当多个key映射到同一个index的时候将冲突的元素链接起来。
哈希函数实现
哈希函数需要尽可能的将不同的key映射到不同的槽(slot或者bucket)中,首先我们采用一种最为简单的哈希算法实现:将key字符串的所有字符加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了。
static int hash_str(char *key)
{
int hash = 0;
char *cur = key;
while(*(cur++) != '\0') {
hash += *cur;
}
return hash;
}
// 使用这个宏来求得key在哈希表中的索引
#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)
这里实现的是一个简单的hash函数,其实开源的有很多优秀的hash算法
操作接口实现
为了操作哈希表,实现了如下几个操作接口函数:
int hash_init(HashTable *ht); // 初始化哈希表
int hash_lookup(HashTable *ht, char *key, void **result); // 根据key查找内容
int hash_insert(HashTable *ht, char *key, void *value); // 将内容插入到哈希表中
int hash_remove(HashTable *ht, char *key); // 删除key所指向的内容
int hash_destroy(HashTable *ht);
下面以初始化、插入和获取操作函数为例:
int hash_init(HashTable *ht)
{
ht->size = HASH_TABLE_INIT_SIZE;
ht->elem_num = 0;
ht->buckets = (Bucket **)calloc(ht->size, sizeof(Bucket *));
if(ht->buckets == NULL) return FAILED;
LOG_MSG("[init]\tsize: %i\n", ht->size);
return SUCCESS;
}
初始化的主要工作是为哈希表申请存储空间,函数中使用calloc函数的目的是确保数据存储的槽为都初始化为0,以便后续在插入和查找时确认该槽为是否被占用。
int hash_insert(HashTable *ht, char *key, void *value)
{
// check if we need to resize the hashtable
resize_hash_table_if_needed(ht);
int index = HASH_INDEX(ht, key);
Bucket *org_bucket = ht->buckets[index];
Bucket *tmp_bucket = org_bucket;
// check if the key exits already
while(tmp_bucket)
{
if(strcmp(key, tmp_bucket->key) == 0)
{
LOG_MSG("[update]\tkey: %s\n", key);
tmp_bucket->value = value;
return SUCCESS;
}
tmp_bucket = tmp_bucket->next;
}
Bucket *bucket = (Bucket *)malloc(sizeof(Bucket));
bucket->key = key;
bucket->value = value;
bucket->next = NULL;
ht->elem_num += 1;
if(org_bucket != NULL)
{
LOG_MSG("[collision]\tindex:%d key:%s\n", index, key);
bucket->next = org_bucket;
}
ht->buckets[index]= bucket;
LOG_MSG("[insert]\tindex:%d key:%s\tht(num:%d)\n",
index, key, ht->elem_num);
return SUCCESS;
}
上面这个哈希表的插入操作比较简单,简单的以key做哈希,找到元素应该存储的位置,并检查该位置是否已经有了内容,如果发生碰撞则将新元素链接到原有元素链表头部。
查找实现:
在查找时也使用插入同样的策略,找到元素所在的位置,如果存在元素,则将该链表的所有元素的key和要查找的key依次对比, 直到找到一致的元素,否则说明该值没有匹配的内容。
int hash_lookup(HashTable *ht, char *key, void **result)
{
int index = HASH_INDEX(ht, key);
Bucket *bucket = ht->buckets[index];
if(bucket == NULL) goto failed;
while(bucket)
{
if(strcmp(bucket->key, key) == 0)
{
LOG_MSG("[lookup]\t found %s\tindex:%i value: %p\n",
key, index, bucket->value);
*result = bucket->value;
return SUCCESS;
}
bucket = bucket->next;
}
failed:
LOG_MSG("[lookup]\t key:%s\tfailed\t\n", key);
return FAILED;
}
扩容
由于在插入过程中可能会导致哈希表的元素个数比较多,如果超过了哈希表的容量,则说明肯定会出现碰撞,出现碰撞则会导致哈希表的性能下降,为此如果出现元素容量达到容量则需要进行扩容。由于所有的key都进行了哈希,扩容后哈希表不能简单的扩容,而需要重新将原有已插入的预算插入到新的容器中。
static void resize_hash_table_if_needed(HashTable *ht)
{
if(ht->size - ht->elem_num < 1)
{
hash_resize(ht);
}
}
static int hash_resize(HashTable *ht)
{
// double the size
int org_size = ht->size;
ht->size = ht->size * 2;
ht->elem_num = 0;
LOG_MSG("[resize]\torg size: %i\tnew size: %i\n", org_size, ht->size);
Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket *));
Bucket **org_buckets = ht->buckets;
ht->buckets = buckets;
int i = 0;
for(i=0; i < org_size; ++i)
{
Bucket *cur = org_buckets[i];
Bucket *tmp;
while(cur)
{
// rehash: insert again
hash_insert(ht, cur->key, cur->value);
// free the org bucket, but not the element
tmp = cur;
cur = cur->next;
free(tmp);
}
}
free(org_buckets);
LOG_MSG("[resize] done\n");
return SUCCESS;
}
哈希表的扩容首先申请一块新的内存,大小为原来的2倍,然后重新将元素插入到哈希表中,读者会发现扩容的操作的代价为O(n),不过这个问题不大,因为只有在到达哈希表容量的时候才会进行。
这篇文章是对下面这个链接的学习和理解:
http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable 我觉得这篇文章很容易让人明白哈希表,非常感谢作者!
https://github.com/reeze/tipi/tree/master/book/sample/chapt03/03-01-01-hashtable 代码示例
哈希表 HashTable(又名散列表)的更多相关文章
- 哈希表查找(散列表查找) c++实现HashMap
算法思想: 哈希表 什么是哈希表 在前面讨论的各种结构(线性表.树等)中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较.这一类 ...
- 哈希表(hashtable)的javascript简单实现
javascript中没有像c#,java那样的哈希表(hashtable)的实现.在js中,object属性的实现就是hash表,因此只要在object上封装点方法,简单的使用obejct管理属性的 ...
- 哈希表(Hashtable)简述
一,哈希表(Hashtable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对,其中 ...
- c/c++ 哈希表 hashtable
c/c++ 哈希表 hashtable 概念:用key去查找value 实现hash函数有很多方法,本文用除留余数法. 除留余数法的概念: 取一个固定的基数的余数,注意不能用偶数,用偶数的话,分布会不 ...
- C#中哈希表(HashTable)的用法详解以及和Dictionary比较
1. 哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对, ...
- Java中哈希表(Hashtable)是如何实现的
Java中哈希表(Hashtable)是如何实现的 Hashtable中有一个内部类Entry,用来保存单元数据,我们用来构建哈希表的每一个数据是Entry的一个实例.假设我们保存下面一组数据,第一列 ...
- 转 C#中哈希表(HashTable)的用法详解
看了一遍有关哈希表的文字,作者总结的真是不错 .收藏起来 1. 哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提 ...
- Hash表(hash table ,又名散列表)
直接进去主题好了. 什么是哈希表? 哈希表(Hash table,也叫散列表),是根据key而直接进行访问的数据结构.也就是说,它通过把key映射到表中一个位置来访问记录,以加快查找的速度.这个映射函 ...
- Hash表 hash table 又名散列表
直接进去主题好了. 什么是哈希表? 哈希表(Hash table,也叫散列表),是根据key而直接进行访问的数据结构.也就是说,它通过把key映射到表中一个位置来访问记录,以加快查找的速度.这个映射函 ...
随机推荐
- java8学习之自定义收集器实现
在上次花了几个篇幅对Collector收集器的javadoc进行了详细的解读,其涉及到的文章有: http://www.cnblogs.com/webor2006/p/8311074.html htt ...
- java8学习之比较器深入
继续接着上次[http://www.cnblogs.com/webor2006/p/8331498.html]的比较器进行探究,上次排序采用是的利用Collections.sort()进行的,下面采用 ...
- IC SPEC相关数据
---恢复内容开始--- 静态电流:静态电流是指没有信号输入时的电流,也就是器件本身在不受外部因素影响下的本身消耗电流. 纹波电压的害处: 1.容易在用设备中产生不期望的谐波,而谐波会产生较多的危害: ...
- Liunx centos 系统 修改hostname
1 centos6下修改hostname [root@centos6 ~]$ hostname # 查看当前的hostnmae centos6.magedu.com [root@centos6 ~]$ ...
- DveOps路线指南
学习DevOps所需的技能 1. 编程语言 python java javascrit 2. 学习不同的操作系统概念 进程管理,线程和兵法,套接字,I/O管理,虚拟化,内存储存储和文件系统. 3. ...
- 【Winform-右下角弹窗】实现右下角弹窗,提示信息
网页是否经常在电脑右下角弹窗显示消息?其实Winform也是可以实现的.下面介绍两种方法. 第一步:设计窗体 第二步:实现代码 第一种方法 引用user32 声明常量 窗体Load事件 窗体FormC ...
- JAVA学长
https://www.cnblogs.com/chenmingjun/p/9697371.html
- Python黑科技神奇去除马赛克
图片修复程序-可用于水印去除 在现实的生活中,我们可能会遇到一些美好的或是珍贵的图片被噪声干扰,比如旧照片的折痕,比如镜头上的灰尘或污渍,更或者是某些我们想为我所用但有讨厌水印,那么有没有一种办法可以 ...
- Spring Boot教程(三十二)多数据源配置与使用(2)
Spring-data-jpa支持 对于数据源的配置可以沿用上例中DataSourceConfig的实现. 新增对第一数据源的JPA配置,注意两处注释的地方,用于指定数据源对应的Entity实体和Re ...
- HTML DOM的学习
请看下面的 HTML 片段: <html> <head> <title>DOM 教程</title> </head> <body> ...