简介

其实通过标题上哈希表的英文名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。

哈希表的实现

在了解到哈希表的原理之后要实现一个哈希表也很容易,主要需要完成的工作只有三点:

  1. 实现哈希函数
  2. 冲突的解决
  3. 操作接口的实现
  4. 扩容

数据结构

首先我们需要一个容器来保存我们的哈希表,哈希表需要保存的内容主要是保存进来的的数据,同时为了方便的得知哈希表中存储的元素个数,需要保存一个大小字段,第二个需要的就是保存数据的容器了。

作为实例,下面将实现一个简易的哈希表。基本的数据结构主要有两个,一个用于保存哈希表本身,另外一个就是用于实际保存数据的单链表了,定义如下:

// 存储键值
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(又名散列表)的更多相关文章

  1. 哈希表查找(散列表查找) c++实现HashMap

    算法思想: 哈希表 什么是哈希表 在前面讨论的各种结构(线性表.树等)中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较.这一类 ...

  2. 哈希表(hashtable)的javascript简单实现

    javascript中没有像c#,java那样的哈希表(hashtable)的实现.在js中,object属性的实现就是hash表,因此只要在object上封装点方法,简单的使用obejct管理属性的 ...

  3. 哈希表(Hashtable)简述

    一,哈希表(Hashtable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对,其中 ...

  4. c/c++ 哈希表 hashtable

    c/c++ 哈希表 hashtable 概念:用key去查找value 实现hash函数有很多方法,本文用除留余数法. 除留余数法的概念: 取一个固定的基数的余数,注意不能用偶数,用偶数的话,分布会不 ...

  5. C#中哈希表(HashTable)的用法详解以及和Dictionary比较

    1.  哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对, ...

  6. Java中哈希表(Hashtable)是如何实现的

    Java中哈希表(Hashtable)是如何实现的 Hashtable中有一个内部类Entry,用来保存单元数据,我们用来构建哈希表的每一个数据是Entry的一个实例.假设我们保存下面一组数据,第一列 ...

  7. 转 C#中哈希表(HashTable)的用法详解

    看了一遍有关哈希表的文字,作者总结的真是不错 .收藏起来 1.  哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提 ...

  8. Hash表(hash table ,又名散列表)

    直接进去主题好了. 什么是哈希表? 哈希表(Hash table,也叫散列表),是根据key而直接进行访问的数据结构.也就是说,它通过把key映射到表中一个位置来访问记录,以加快查找的速度.这个映射函 ...

  9. Hash表 hash table 又名散列表

    直接进去主题好了. 什么是哈希表? 哈希表(Hash table,也叫散列表),是根据key而直接进行访问的数据结构.也就是说,它通过把key映射到表中一个位置来访问记录,以加快查找的速度.这个映射函 ...

随机推荐

  1. IDA Pro - 使用IDA Pro逆向C++程序

    原文地址:Reversing C++ programs with IDA pro and Hex-rays 简介 在假期期间,我花了很多时间学习和逆向用C++写的程序.这是我第一次学习C++逆向,并且 ...

  2. leetcode上题目的分类

    leetcode链表部分题目 https://zhuanlan.zhihu.com/p/29800285 <[Leetcode][链表]相关题目汇总/分析/总结> leetcode堆部分题 ...

  3. Codeforces Round #395 Div.1 C pacifist【JZOJ5449】Pacifist

    题目 papyrus 喜欢谜题... 来解一道如何? 在你面前有一个被加密了的数组,其原数组是一个等差序列,你面前的则是将原数组中的所有数字都对m 取模再打乱后而得到的新数组 papyrus 给你出的 ...

  4. NOIP2017提高A组模拟10.6】Biology

    题目 trie 暴力就是对于每个询问的T个字符串 第i个和第i+1个直接个从后暴力枚举每位是否相同, 但这个方法TLE 我们考虑是否可以用更快的方法来求出两个字符串的最长公共后缀. 我们把所有的字符串 ...

  5. 通过喝水清晰简单了解I/O五大模型

    一般单次I/O请求会分为两个阶段,每个阶段对于I/O的处理方式是不同的 I/O会经历一个等待资源的阶段 阻塞,指的是在数据不可用时,I/O请求会一直阻塞,直到数据返回 数据不可用时,立即返回,直到被通 ...

  6. Angular CLI 创建你的第一个 Angular 示例程序

    第一步:安装 Angular CLI 你要使用 Angular CLI 来创建项目.创建应用和库代码,并执行多种开发任务,比如测试.打包和发布. 全局安装 Angular CLI. 要想使用 npm  ...

  7. source和resource的区别

    idea中,有时新导入的工程会出现 类的标识为红色的J,此时为无效,并且该类不能被编译,这是因为该类所在的文件夹java没有被标记为Sources Root,而放置配置文件的resources文件夹没 ...

  8. <meta>标签 的一些用法

    网上找到的一些资料:自己留着! 链接 :http://www.wzsky.net/html/Website/htmlcss/116165.html meta是html语言head区的一个辅助性标签.也 ...

  9. sqli-libs(2)

    Lesson2:数字型注入我们先来测试一下 http://127.0.0.1/sql1/Less-2/?id=1%20and%201=1 发现未报错 这里知道我们的数字型注入是不需要 进行单引号闭合的 ...

  10. eclipse中设置tab为4个空格

    1.insert space for tabs前打勾 2.General settings中选择Spaces only 3.搞定