- 理想狀況下,無需任何比較就能找到待查關鍵字,查找的期望時間複雜度爲
O(1)
PHP7散列表基本結構:
// zend_array和HashTable的含義是相同的,沒有任何區別
typedef struct _zend_array zend_array;
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
// 此處的union可以先忽略
union {
...
} u;
// 用於散列函數映射存儲在arData數組中的下標
uint32_int nTableMask;
// 存儲元素數組,每個元素的結構統一爲Bucket,arData指向第一個Bucket
Bucket *arData;
// 已用Bucket數
uint32_t nNumUsed;
// 數組實際存儲的元素數
uint32_t nNumOfElements;
// 數組的總容量
uint32_t nTableSize;
uint32_t nInternalPointer;
// 下一個可用的數值索引,如arr[] = 1;arr["a"] = 2;arr[] = 3;則nNextFreeElement = 2;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
}
散列表的結構中有很多成員,以下是比較重要的幾個成員:
arData
:散列表中保存 存儲元素(即Bucket
)的數組,其內存是連續的,arData
指向數組的起始位置nTableSize
:數組的總容量,即可以容納的元素數量。arData
的內存大小是根據這個值確定的,他的大小是2的冪次方,最小爲8。也就是說散列表的大小依次按照8
、16
、32
、64
…進行遞增。nTableMask
:這個值在散列函數根據key
的hash code
映射元素的位置時用到。他的值實際就是nTableSize
的負數,即nTableMask = -nTableSize
,用位運算來表示的話則爲:nTableMask = ~nTableSize + 1
(此處需要撰寫一篇筆記專門說明,參考本文件夾內《4.2、原碼、反碼和補碼》)。nNumUsed
、nNumOfElements
:nNumUsed
是指當前使用掉的Bucket
數量,而nNumOfElements
則是數組中的有效元素的數量,注意兩者是有區別的。在數組中,使用掉的Bucket
中並非一定是有效元素,當我們從數組中刪除元素的時候並不會把元素馬上從數組中移除,而是將元素的類型標記爲IS_UNDEF
。只有在數組的容量超限,需要進行擴容時纔會刪除。這一機制使得nNumUsed >= nNumOfElements
。如果數組沒有擴容,那麼nNumUsed
將一直是遞增的,無論是否刪除元素。nNextFreeElement
:這個值是給自動確認數值索引使用的,從0
開始。比如$a[] = 1
,這個時候就將這值增加爲1
,下次再有$a[]
的操作時就使用剛剛得到的1
作爲新元素的索引值。pDestructor
:當刪除或覆蓋數組中的某個元素時,如果提供了這個函數句柄,則在刪除或覆蓋後調用此函數,對舊元素進行清理。u
:這個結構主要是一些輔助作用,比如flags
用來設置散列表的一些屬性:是否持久化、是否已經初始化等。
Bucket
Bucket
的結構較爲簡單,此結構主要用於保存元素的key
和value
。除了這兩個還有一個整型的h
,它的含義是hash code
:如果元素是數值索引,那麼它的元素就是數值索引的值;如果是字符串索引,那麼就根據字符串key
通過Time33
算法計算得到散列值,h
的值用來映射元素的存儲位置。另外,存儲的value
也直接嵌入到Bucket
結構中:
typedef struct _Bucket {
zval val;// 存儲的具體value,這裏嵌入一個zval,而不是一個指針
zend_ulong h;// key根據Time33計算得到的哈希值,或者是數值索引
zend_string *key;// 存儲元素的key
} Bucket;
arData
arData
在數組初始化的時候並不分配內存,而是在首次插入數據的時候觸發內存分配。由zend_hash_real_init_ex()
進行內存的分配,分配的大小包括中間表和元素數組,共計:nTableSize * (sizeof(Bucket) + sizeof(uint32_t))
。分配完成後會把HashTable->u.flags
打上HASH_FLAG_INITIALIZED
掩碼,這樣下次再插入的時候發現已經分配了內存就不會再觸發內存分配了。分配完成後,HashTable->arData
會指向第一個Bucket
的位置。
完成內存的分配之後就可以進行插入操作了。插入時首先將元素按照順序插入
arData
,然後將其在arData
中的位置存儲到中間表,它在中間表中的位置計算方式是:根據key
的hash code
(即key->h
)與nTableMask
計算後得到的中間表的位置(nIndex = h | ht->nTableMask
)。
哈希衝突
散列表中不同元素的
key
有時候經過hash計算
會得到相同的結果(即想存入相同的映射表的位置),而映射表中的一個位置只能存儲一個元素,這時候就發生了哈希衝突。
常用的解決方法是把衝突的
Bucket
串成鏈表,這樣一來映射表映射的就不是一個Bucket元素
,而是一個Bucket鏈表
。在查找的時候需要根據key
遍歷鏈表來查找相應的元素。PHP
就是採用的這種方式解決哈希衝突。
HashTable
中的Bucket
會記錄與他衝突的元素的位置(當前是隻能標記相鄰的)。在設置映射時,如果發現對應的位置已經有元素了,就會把已經存在的值(後加:的位置)放到新申請的Bucket
中再把映射表指向新申請的Bucket
。《PHP7內核剖析》書中有句話叫做“即每次都會把衝突的元素插到開頭”,這裏有兩點不明:一是衝突的元素是指新元素還是舊元素(後續查看,是新元素),二是鏈表的開頭是指新申請的Bucket
還是之前的舊Bucket
(後續查看,是新申請的Bucket
)。這裏的描述與例子是衝突的,例子中並沒有把舊的值放入新申請的Bucket
,例子截圖如下:
關於上述衝突的說明:“就會把已經存在的值(後加:的位置)放到新申請的
Bucket
中再把映射表指向新申請的Bucket
。”這句話坑死爹爹了。。之前一直感覺很奇怪,纔有了下面的疑惑。這邊解讀一下:這裏是指把已經存在的值的位置放到新申請的Bucket
中的next
中,並非是把整個值拷過去。。
個人覺得明明只要把新
Bucket
連接到舊的Bucket
後面就可以了,爲何需要修改映射表的指向呢?很繁瑣啊,把值拷來拷去的。。
寫在最後:將衝突流程走了一遍之後發現,現在的做法確實是最優的:衝突時只需要修改映射表的值,再將新元素的
next
指向之前的元素。如果按照我的想法,保持映射表位置不變,那麼就需要寫入新元素,再修改末端元素的指向。假如鏈表中衝突元素已經較多,定位到末端元素也是個費勁的事,還不如現在這種方案,確保每次隻影響兩個地方即可。
查找
查找過程比較簡單,流程如下:首先根據
key
計算hash code
,即zend_string->h
與nTableMask
計算得到的nIndex
。然後根據nIndex
從映射表查找到鏈表頭元素位置idx
。從idx
取出第一個Bucket
,開始與key
進行對比,判斷是否是需要查找的元素,如果是就終止遍歷,否就繼續根據zval.u2.next
往下查找。。