哈希表 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。

哈希表的实现

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

  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  代码示例

posted @ 2017-06-03 13:35  九卷  阅读(738)  评论(0编辑  收藏  举报