zoukankan      html  css  js  c++  java
  • 哈希表的实现

    在php的底层实现中,hash表是最常见的一种使用。那hash表具体是怎样实现的呢?

    哈希表的实现

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

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

    数据结构

    首先我们需要一个容器来保存我们的哈希表,哈希表需要保存的内容主要是保存进来的的数据, 同时为了方便的得知哈希表中存储的元素个数,需要保存一个大小字段, 第二个需要的就是保存数据的容器了。作为实例,下面将实现一个简易的哈希表。基本的数据结构主要有两个, 一个用于保存哈希表本身,另外一个就是用于实际保存数据的单链表了,定义如下:

    typedef struct _Bucket
    {
        char *key;
        void *value;
        struct _Bucket *next;
    } Bucket;
     
    typedef struct _HashTable
    {
        int size;
        int elem_num;
        Bucket** buckets;
    } HashTable;

    上面的定义和PHP中的实现类似,为了便于理解裁剪了大部分无关的细节,在本节中为了简化, 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;
            ++cur;
        }
     
        return hash;
    }
     
    // 使用这个宏来求得key在哈希表中的索引
    #define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)

    这个哈希算法比较简单,它的效果并不好,在实际场景下不会使用这种哈希算法, 例如PHP中使用的是称为DJBX33A算法, 这里列举了Mysql,OpenSSL等开源软件使用的哈希算法, 有兴趣的读者可以前往参考。

    有兴趣的读者可以运行本小节实现的哈希表实现,在输出日志中将看到很多的哈希冲突, 这是本例中使用的哈希算法过于简单造成的.

    操作接口的实现

    为了操作哈希表,实现了如下几个操作接口函数:

    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都进行了哈希,扩容后哈希表不能简单的扩容,而需要重新将原有已插入的预算插入到新的容器中。

    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),不过这个问题不大,因为只有在到达哈希表容量的时候才会进行。

    在查找时也使用插入同样的策略,找到元素所在的位置,如果存在元素, 则将该链表的所有元素的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;
    }

    PHP中数组是基于哈希表实现的,依次给数组添加元素时,元素之间是有先后顺序的, 而这里的哈希表在物理位置上显然是接近平均分布的,这样是无法根据插入的先后顺序获取到这些元素的, 在PHP的实现中Bucket结构体还维护了另一个指针字段来维护元素之间的关系。 具体内容在后一小节PHP中的HashTable中进行详细说明。上面的例子就是PHP中实现的一个精简版。

  • 相关阅读:
    「疫期集训day11」沙漠
    「树形DP」洛谷P2607 [ZJOI2008]骑士
    「疫期集训day10」玫瑰
    「疫期集训day9」七月
    核心容器(概念)
    初识Spring
    IOC(控制反转思想)原型理论推导
    图片在上,文字在下并且等间距的三个菜单按钮
    编写登陆接口
    001使用gltf创建3d模型
  • 原文地址:https://www.cnblogs.com/tangchuanyang/p/6186196.html
Copyright © 2011-2022 走看看