哈希表 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映射到表中一个位置来访问记录,以加快查找的速度.这个映射函 ...
随机推荐
- Django学习系列11:在服务器中处理POST请求
之前的代码还没有为表单指定action=属性,因此提交表单默认返回之前渲染的页面,即“/”,这个由视图函数home_page处理.下面修改这个视图函数,让它能处理POST请求. 这意味着要为视图函数h ...
- Python核心技术与实战——二一|巧用上下文管理器和with语句精简代码
我们在Python中对于with的语句应该是不陌生的,特别是在文件的输入输出操作中,那在具体的使用过程中,是有什么引伸的含义呢?与之密切相关的上下文管理器(context manager)又是什么呢? ...
- 前端面试题-display:none和visibility:hidden的区别
一.display:none和visibility:hidden的区别 1.1 空间占据 1.2 回流和渲染 1.3 株连性 二.空间占据 display:none 隐藏后的元素不占据任何空间,而 v ...
- Javascript实现图片点击弹出
一直想给安装一个缩略图点击弹出的插件,但是找了找几乎都是用的php来做的,插件的使用和安装极其繁琐,于是上网查了些demo,自己实现了一个纯js的图片弹出插件. 实现的思路是通过编写hook图片的on ...
- C#写入文件内容时提示:文件正被另一个人或程序使用
创建文件后未将文件关闭 string sTransLogFile = sTransLogPath + "\\" + DateTime.Now.ToString("yyyy ...
- C# 遍历控件名称
List<string> list = new List<string>(); list.Add("textBox2"); list.Add("t ...
- Sql Service中的分页
创建存储过程如下: CREATE PROCEDURE [dbo].[sp_GetPageList] ), --表名 ) = '*', --字段名(全部字段为*) ), --排序字段(必须!支持多字段) ...
- 51 Nod 1068 Bash游戏v3
1068 Bash游戏 V3 题目来源: Ural 1180 基准时间限制:1 秒 空间限制:131072 KB 分值: 20 难度:3级算法题 收藏 关注 有一堆石子共有N个.A B两个人轮流 ...
- sh_05_列表遍历
sh_05_列表遍历 name_list = ["张三", "李四", "王五", "王小二"] # 使用迭代遍历列表 ...
- HDU 6155 Subsequence Count (DP、线性代数、线段树)
题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=6155 题解 DP+线代好题.(考场上过多时间刚前两题,没怎么想这题--) 首先列出一个DP式: 设\( ...