數組在 PHP 中很是強大、靈活的一種數據類型,和 Java、C 等靜態語言不一樣,咱們在初始化 PHP 數組的時候沒必要指定大小和存儲數據的類型,在賦值的時候能夠經過數字索引,也能夠經過字符串索引的方式:算法
基於 PHP 數組的強大特性,咱們能夠輕易實現更加複雜的數據結構,好比棧、隊列、列表、集合、字典等。PHP 數組功能之因此如此強大,得益於底層基於散列表實現。數組
PHP數組底層數據結構 數據結構
PHP 數組底層依賴的散列表數據結構定義以下(位於 Zend/zend_types.h):函數
這個散列表中有不少成員,咱們挑幾個比較重要的來說講:性能
Bucket 的結構比較簡單,主要用來保存元素的 key 和 value,以及一個整型的 h(散列值,或者叫哈希值):若是元素是數值索引,則其值就是數值索引的值;若是是字符串索引,那麼其值就是 key 經過 Time33 算法計算獲得的散列值,h 的值用來最終映射元素的存儲位置。Bucket 的數據結構以下:ui
PHP 數組的基本實現 spa
散列表主要由兩部分組成:存儲元素數組、散列函數。散列表的基本實現前面已經探討過,PHP 中的數組除了具有散列表的基本特色以外,還有一個特別的地方,那就是它是有序的(與Java中的HashMap的無序有所不一樣):數組中各元素的順序和插入順序一致。這個是怎麼實現的呢?code
爲了實現 PHP 數組的有序性,PHP 底層的散列表在散列函數與元素數組之間加了一層映射表,這個映射表也是一個數組,大小和存儲元素的數組相同,存儲元素的類型爲整型,用於保存元素在實際存儲的有序數組中的下標 —— 元素按照前後順序依次插入實際存儲數組,而後將其數組下標按照散列函數散列出來的位置存儲在新加的映射表中:blog
這樣,就能夠完成最終存儲數據的有序性了。索引
PHP 數組底層結構中並無顯式標識這個中間映射表,而是與 arData 放到了一塊兒,在數組初始化的時候並不只僅分配用於存儲 Bucket 的內存,還會分配相同數量的 uint32_t 大小的空間,這兩塊空間是一塊兒分配的,而後將 arData 偏移到存儲元素數組的位置,而這個中間映射表就能夠經過 arData 向前訪問到。
數組的初始化
數組的初始化主要是針對 HashTable 成員的設置,初始化時並不會當即分配 arData 的內存,插入第一個元素以後纔會分配 arData 的內存。初始化操做能夠經過 zend_hash_init 宏完成,最後由 _zend_hash_init_int 函數處理(該函數定義在 Zend/zend_hash.c 文件中):
此時的 HashTable 只是設置了散列表的大小及其餘成員的初始值,還沒法用來存儲元素。
插入數據
插入時會檢查數組是否已經分配存儲空間,由於初始化並無實際分配 arData 的內存,在第一次插入時纔會根據 nTableSize 的大小分配,分配之後會把 HashTable->u.flags 打上 HASH_FLAG_INITIALIZED 掩碼,這樣,下次插入時發現已經分配了就不會重複操做,這段檢查邏輯位於 _zend_hash_add_or_update_i 函數中:
if (UNEXPECTED(!(HT_FLAGS(ht) & HASH_FLAG_INITIALIZED))) { zend_hash_real_init_mixed(ht); if (!ZSTR_IS_INTERNED(key)) { zend_string_addref(key); HT_FLAGS(ht) &= ~HASH_FLAG_STATIC_KEYS; zend_string_hash_val(key); } goto add_to_hash; }
若是 arData 尚未分配,則最終由 zend_hash_real_init_mixed_ex 完成內存分配:
分配完 arData 的內存後就能夠進行插入操做了,插入時先將元素按照順序插入 arData,而後將其在 arData 數組中的位置存儲到根據 key 的散列值與 nTableMask 計算獲得的中間映射表中的對應位置:
上述只是最基本的插入處理,不涉及已存在數據的覆蓋和清理。
哈希衝突
PHP 數組底層的散列表採用鏈地址法解決哈希衝突,即將衝突的 Bucket 串成鏈表。
HashTable 中的 Bucket 會記錄與它衝突的元素在 arData 數組中的位置,這也是一個鏈表,衝突元素的保存位置不在 Bucket 結構中,而是保存在了存儲元素 zval 的 u2 結構中,即 Bucket.val.u2.next,因此插入時分爲如下兩步:
// 將映射表中原來的值保存到新 Bucket 中,哈希衝突時會用到(以鏈表方式解決哈希衝突) Z_NEXT(p->val) = HT_HASH_EX(arData, nIndex); // 再把新元素數組存儲位置更新到數據表中 // 保存idx:((unit32_t*))(ht->arData)[nIndex] = idx HT_HASH_EX(arData, nIndex) = HT_IDX_TO_HASH(idx);
數組查找
清楚了 HashTable 的實現和哈希衝突的解決方式以後,查找的過程就比較簡單了:首先根據 key 計算出的散列值與 nTableMask 計算獲得最終散列值 nIndex,而後根據散列值從中間映射表中獲得存儲元素在有序存儲數組中的位置 idx,接着根據 idx 從有序存儲數組(即 arData)中取出 Bucket,遍歷該 Bucket,判斷 Bucket 的 key 是不是要查找的 key,若是是則終止遍歷,不然繼續根據 zval.u2.next 遍歷比較。
對應的底層源碼以下:
刪除數據
關於數組數據刪除前面咱們在介紹散列表中的 nNumUsed 和 nNumOfElements 字段時已經說起過,從數組中刪除元素時,並無真正移除,並從新 rehash,而是當 arData 滿了以後,纔會移除無用的數據,從而提升性能。即數組在須要擴容的狀況下才會真正刪除元素:首先檢查數組中已刪除元素所佔比例,若是比例達到閾值則觸發從新構建索引的操做,這個過程會把已刪除的 Bucket 移除,而後把後面的 Bucket 往前移動補上空位,若是尚未達到閾值則會分配一個原數組大小 2 倍的新數組,而後把原數組的元素複製到新數組上,最後重建索引,重建索引會將已刪除的 Bucket 移除。
對應底層代碼以下:
除此以外,數組還有不少其餘操做,好比複製、合併、銷燬、重置等,這些操做對應的代碼都位於 zend_hash.c 中,感興趣的同窗能夠去看看。