zoukankan      html  css  js  c++  java
  • 哈希表 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++) != '') {
            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]	size: %i
    ", 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]	key: %s
    ", 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]	index:%d key:%s
    ", index, key);
            bucket->next = org_bucket;
        }
     
        ht->buckets[index]= bucket;
     
        LOG_MSG("[insert]	index:%d key:%s	ht(num:%d)
    ",
            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]	 found %s	index:%i value: %p
    ",
                    key, index, bucket->value);
                *result = bucket->value;
     
                return SUCCESS;
            } 
     
            bucket = bucket->next;
        }
     
    failed:
        LOG_MSG("[lookup]	 key:%s	failed	
    ", 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]	org size: %i	new size: %i
    ", 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
    ");
     
        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  代码示例

  • 相关阅读:
    HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别(转)
    JSP语法
    Web开发基础(读书笔记)
    eclispe新导入的文件有个小红叉号(x)的问题
    Vue处理数据,数组更新,但视图无法得到及时更新
    VUE 利用tab切换+同路由跳转传参(check)+vant上拉加载制作订单列表(终)
    适配方案一之:利用rem和less计算制作移动端适配页面
    Git常用命令务忘
    git提交代码步骤笔记
    Vue仿淘宝订单状态的tab切换效果——(但现实中不会用此种方式进行存储数据)
  • 原文地址:https://www.cnblogs.com/jiujuan/p/11109509.html
Copyright © 2011-2022 走看看