在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中實現的一個精簡版。