深入理解PHP核心(六)雜湊表以及PHP的雜湊表實現,深入理解
原文連結:http://www.orlion.ga/241/
一、雜湊表(HashTable)
大部分動態語言的實現中都使用了雜湊表,雜湊表是一種通過雜湊函數,將特定的鍵映射到特定值得一種資料
結構,它維護鍵和值之間一一對應關係。
鍵(key):用於操作資料的標示,例如PHP數組中的索引或者字串鍵等等。
槽(slot/bucket):雜湊表中用於儲存資料的一個單元,也就是數組真正存放的容器。
雜湊函數(hash function):將key映射(map)到資料應該存放的slot所在位置的函數。
雜湊衝突(hash collision):雜湊函數將兩個不同的key映射到同一個索引的情況。
目前解決hash衝突的方法有兩種:連結法和開放定址法。
1、衝突解決
(1)連結法
連結法通過使用一個鏈表來儲存slot值的方式來解決衝突,也就是當不同的key映射到一個槽中的時候使用鏈表
來儲存這些值。(PHP中正是使用了這種方式);
(2)開放定址法
使用開放定址法是槽本身直接存放資料,在插入資料時如果key所映射到的索引已經有資料了,這說明有衝突,
這時會尋找下一個槽,如果該槽也被佔用了則繼續尋找下一個槽,直到找到沒有被佔用的槽,在尋找時也是這樣
2、雜湊表的實現
雜湊表的實現主要完成的工作只有三點:
* 實現雜湊函數
* 衝突的解決
* 操作介面的實現
(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結構體還維護了另一個指標欄位來維護元素之間的關係。
二、PHP的雜湊表實現
1、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代碼:
'Hello');$a[] = 'TIPI';var_dump($a); // ouputarray(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沒有區別
雜湊表的操作介面:
PHP提供了如下幾類操作介面:
http://www.bkjia.com/PHPjc/1115246.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/1115246.htmlTechArticle深入理解PHP核心(六)雜湊表以及PHP的雜湊表實現,深入理解 原文連結:http://www.orlion.ga/241/ 一、雜湊表(HashTable) 大部分動態語言的實現中都使...