原文連接:http://www.orlion.ga/241/php
1、哈希表(HashTable)算法
大部分動態語言的實現中都使用了哈希表,哈希表是一種經過哈希函數,將特定的鍵映射到特定值得一種數據數組
結構,它維護鍵和值之間一一對應關係。數據結構
鍵(key):用於操做數據的標示,例如PHP數組中的索引或者字符串鍵等等。函數
槽(slot/bucket):哈希表中用於保存數據的一個單元,也就是數組真正存放的容器。優化
哈希函數(hash function):將key映射(map)到數據應該存放的slot所在位置的函數。ui
哈希衝突(hash collision):哈希函數將兩個不一樣的key映射到同一個索引的狀況。指針
目前解決hash衝突的方法有兩種:連接法和開放尋址法。排序
一、衝突解決遞歸
(1)連接法
連接法經過使用一個鏈表來保存slot值的方式來解決衝突,也就是當不一樣的key映射到一個槽中的時候使用鏈表
來保存這些值。(PHP中正是使用了這種方式);
(2)開放尋址法
使用開放尋址法是槽自己直接存放數據,在插入數據時若是key所映射到的索引已經有數據了,這說明有衝突,
這時會尋找下一個槽,若是該槽也被佔用了則繼續尋找下一個槽,直到找到沒有被佔用的槽,在查找時也是這樣
二、哈希表的實現
哈希表的實現主要完成的工做只有三點:
* 實現哈希函數
* 衝突的解決
* 操做接口的實現
(1)數據結構
首先須要一個容器來曹村咱們的哈希表,哈希表須要保存的內容主要是保存進來的數據,同時爲了方便的得知哈希表中存儲的元素個數,須要保存一個大小字段,第二個須要的就是保存數據的容器。下面將實現一個簡易的哈希表,基本的數據結構主要有兩個,一個用於保存哈希表自己,另一個就是用於實際保存數據的單鏈表了,定義以下:
typedef struct _Bucket { char *key; void *value; struct _Bucket *next; } Bucket; typedef struct _HashTable { int size; Bucket* buckets; } HashTable;
上邊的定義與PHP中的實現類似,爲了簡化key的數據類型爲字符串,而存儲的結構能夠爲任意類型。
Bucket結構體是一個單鏈表,這是爲了解決哈希衝突。當多個key映射到同一個index的時候將衝突的元素連接起來
(2)哈希函數實現
咱們採用一種最簡單的哈希算法實現:將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)
PHP使用的哈希算法稱爲DJBX33A。爲了操做哈希表定義了以下幾個操做函數:
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_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); // 找到key所映射到的索引 Bucket *org_bucket = ht->buckets[index]; Bucket *bucket = (Bucket *)malloc(sizeof(Bucket)); // 爲新元素申請空間 bucket->key = strdup(key); // 將值內容保存起來,這裏只是簡單的將指針指向要存儲的內容,而沒有將內容複製 bucket->value = value; LOG_MSG("Insert data p: %p\n", value); ht->elem_num += 1; // 記錄一下如今哈希表中的元素個數 if(org_bucket != NULL) { // 發生了碰撞,將新元素放置在鏈表的頭部 LOG_MSG("Index collision found with org hashtable: %p\n", org_bucket); bucket->next = org_bucket; } ht->buckets[index]= bucket; LOG_MSG("Element inserted at index %i, now we have: %i elements\n", index, ht->elem_num); return SUCCESS; }
在查找時首先找到元素所在的位置,若是存在元素,則將鏈表中的全部元素的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) return FAILED; // 查找這個鏈表以便找到正確的元素,一般這個鏈表應該是隻有一個元素的,也就不一樣屢次循環 // 要保證這一點須要有一個合適的哈希算法。 while(bucket) { if(strcmp(bucket->key, key) == 0) { LOG_MSG("HashTable found key in index: %i with key: %s value: %p\n", index, key, bucket->value); *result = bucket->value; return SUCCESS; } bucket = bucket->next; } LOG_MSG("HashTable lookup missed the key: %s\n", key); return FAILED; }
PHP中的數組是基於哈希表實現的,依次給數組添加元素時,元素之間是有順序的,而這裏的哈希表在物理上顯然是接近平均分佈的,這樣是沒法根據插入的前後順序獲取到這些元素的,在PHP的實現中Bucket結構體還維護了另外一個指針字段來維護元素之間的關係。
2、PHP的哈希表實現
一、PHP的哈希實現
PHP中的哈希表是十分重要的一個數據接口,基本上大部分的語言特徵都是基於哈希表的,例如:變量的做用域和變量的存儲,類的實現以及Zend引擎內部的數據有不少都是保存在哈希表中的。
(1)數據結構及說明
Zend爲了保存數據之間的關係使用了雙向鏈表來保存數據
(2)哈希表結構
PHP中的哈希表實如今Zend/zend_hash.c中,PHP使用以下兩個數據結構來實現哈希表,HashTable結構體用於保存整個哈希表須要的基本信息,而Bucket結構體用於保存具體的數據內容,以下:
typedef struct _hashtable { uint nTableSize; // hash Bucket的大小,最小爲8,以2x增加 uint nTableMask; // nTableSize-1,索引取值的優化 uint nNumOfElements; // hash Bucket中當前存在的元素個數,count()函數會直接返回此值 ulong nNextFreeElement; // 下一個數字索引的位置 Bucket *pInternalPointer; // 當前遍歷的指針(foreach 比for快的緣由之一) Bucket *pListHead; // 存儲數頭元素指針 Bucket *pListTail; // 存儲數組尾元素指針 Bucket **arBuckets; // 存儲hash數組 dtor_func_t pDestructor; zend_bool persistent; unsigned char nApplyCount; // 標記當前hash Bucket被遞歸訪問的次數(防止屢次遞歸) zend_bool bApplyProtection;// 標記當前hash桶容許不容許屢次訪問,不容許時,最多隻能遞歸3此 #if ZEND_DEBUG int inconsistent; #endif } HashTable;
nTableSize字段用於標示哈希表的容量,哈希表的初始化容量最小爲8.首先看看哈希表的初始化函數:
ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, hash_func_t pHashFunction, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC) { uint i = 3; //... if (nSize >= 0x80000000) { /* prevent overflow */ ht->nTableSize = 0x80000000; } else { while ((1U << i) < nSize) { i++; } ht->nTableSize = 1 << i; } // ... ht->nTableMask = ht->nTableSize - 1; /* Uses ecalloc() so that Bucket* == NULL */ if (persistent) { tmp = (Bucket **) calloc(ht->nTableSize, sizeof(Bucket *)); if (!tmp) { return FAILURE; } ht->arBuckets = tmp; } else { tmp = (Bucket **) ecalloc_rel(ht->nTableSize, sizeof(Bucket *)); if (tmp) { ht->arBuckets = tmp; } } return SUCCESS; }
例如若是設置初始大小爲10,則上面的算法將會將大小調整爲16.也就是始終將大小調整爲接近初始大小的2的整數次方
爲何這麼調整呢?先看看HashTable將哈希值映射到槽位的方法:
h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask;
從上邊的_zend_hash_init()函數中可知,ht->nTableMask的大小爲ht->nTableSize – 1。這裏使用&操做而不是使用取模,這是由於相對來講取模的操做的消耗和按位與的操做大不少。
設置好了哈希表的大小後就須要爲哈希表申請存儲空間了,如上邊初始化的代碼,根據是否須要持久保存而調用了不一樣的內存申請方法,是須要持久體現的是在前面PHP生命週期裏介紹的:持久內容能在多個請求之間可訪問,而若是是非持久存儲則會在在請求結束時釋放佔用的空間。具體內容將在內存管理中詳解
HashTable中的nNumOfElements字段很好理解,每插入一個元素或者unset刪掉元素時會更新這個字段,這樣在進行count()函數統計數組元素個數時就能快速的返回。
nNextFreeElement字段很是有用,先看一段PHP代碼:
<?php $a = array(10 => 'Hello'); $a[] = 'TIPI'; var_dump($a); // ouput array(2) { [10]=> string(5) "Hello" [11]=> string(5) "TIPI" }
PHP中能夠不指定索引值向數組中添加元素,這時將默認使用數字做爲索引,和C語言中的枚舉相似,而這個元素的索引究竟是多個就由nNextFreeElement字段決定了。若是數組中存在了數字key,則會默認使用最新使用的key+1,如上例中已經存在了10做爲key的元素,這樣新插入的默認索引就爲11了。
下面看看保存哈希表數據的槽位數據結構體:
typedef struct bucket { ulong h; // 對char *key進行hash後的值,或者是用戶指定的數字索引值 uint nKeyLength; // hash關鍵字的長度,若是數組索引爲數字,此值爲0 void *pData; // 指向value,通常是用戶數據的副本,若是是指針數據,則指向pDataPtr void *pDataPtr; // 若是是指針數組,此值會指向真正的value,同時上面pData會指向此值 struct bucket *pListNext; // 整個hash表的下一個元素 struct bucket *pListLast; // 整個hash表的上一個元素 struct bucket *pNext; // 存放在同一個hash Bucket內的下一個元素 struct bucket *pLast; // 存放在同一個hash Bucket內的上一個元素 char arKey[1]; /* 存儲字符索引,此項必須放在最末尾,由於此處只定義了1個字節,存儲的其實是指向char *key的值, 這就意味着能夠省去再賦值一次的消耗,並且,有時此值並不須要,因此同時還節省了空間。 */ } Bucket;
如上面各字段的註釋。h字段保存哈希表key哈希後的值。在PHP中可使用字符串或者數字做爲數組的索引。由於數字的索引是惟一的。若是再進行一次哈希將會極大的浪費。h字段後面的nKeyLength字段是做爲key長度的標示,若是索引是數字的話,則nKeyLength爲0.在PHP中定義數組時若是字符串能夠被轉換成數字也會進行轉換。因此在PHP中例如'10','11'這類的字符索引和數字索引10,11沒有區別
Bucket結構體維護了兩個雙向鏈表,pNext和pLast指針分別指向本槽位所在的鏈表的關係
而pListNext和pListLast指針指向的則是整個哈希表全部的數據之間的連接關係。HashTable結構體中的pListHead和pListTail則維護整個哈希表的頭元素指針和最後一個元素的指針
哈希表的操做接口:
PHP提供了以下幾類操做接口:
初始化操做,例如zend_hash_init()函數,用於初始化哈希表接口,分配空間等。
查找,插入,刪除和更新操做接口,這是比較常規的操做。
迭代和循環,這類的接口用於循環對哈希表進行操做。
複製,排序,倒置和銷燬等操做。