在php的底层实现中,hash表是最多见的一种使用。那hash表具体是怎样实现的呢?php
在了解到哈希表的原理以后要实现一个哈希表也很容易,主要须要完成的工做只有三点:html
首先咱们须要一个容器来保存咱们的哈希表,哈希表须要保存的内容主要是保存进来的的数据, 同时为了方便的得知哈希表中存储的元素个数,须要保存一个大小字段, 第二个须要的就是保存数据的容器了。做为实例,下面将实现一个简易的哈希表。基本的数据结构主要有两个, 一个用于保存哈希表自己,另一个就是用于实际保存数据的单链表了,定义以下:算法
typedef struct _Bucket { char *key; void *value; struct _Bucket *next; } Bucket; typedef struct _HashTable { int size; int elem_num; Bucket** buckets; } HashTable;
上面的定义和PHP中的实现相似,为了便于理解裁剪了大部分无关的细节,在本节中为了简化, key的数据类型为字符串,而存储的数据类型能够为任意类型。sql
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; ++cur; } return hash; } // 使用这个宏来求得key在哈希表中的索引 #define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)
这个哈希算法比较简单,它的效果并很差,在实际场景下不会使用这种哈希算法, 例如PHP中使用的是称为DJBX33A算法, 这里列举了Mysql,OpenSSL等开源软件使用的哈希算法, 有兴趣的读者能够前往参考。函数
有兴趣的读者能够运行本小节实现的哈希表实现,在输出日志中将看到不少的哈希冲突, 这是本例中使用的哈希算法过于简单形成的.性能
为了操做哈希表,实现了以下几个操做接口函数:ui
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);
下面以初始化、插入和获取操做函数为例:es5
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都进行了哈希,扩容后哈希表不能简单的扩容,而须要从新将原有已插入的预算插入到新的容器中。
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),不过这个问题不大,由于只有在到达哈希表容量的时候才会进行。
在查找时也使用插入一样的策略,找到元素所在的位置,若是存在元素, 则将该链表的全部元素的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; }
PHP中数组是基于哈希表实现的,依次给数组添加元素时,元素之间是有前后顺序的, 而这里的哈希表在物理位置上显然是接近平均分布的,这样是没法根据插入的前后顺序获取到这些元素的, 在PHP的实现中Bucket结构体还维护了另外一个指针字段来维护元素之间的关系。 具体内容在后一小节PHP中的HashTable中进行详细说明。上面的例子就是PHP中实现的一个精简版。