PHP 中的數組是一種強大且靈活的數據類型。在講解它的底層實現以前,讓咱們先來看看它在實際使用中都有哪些重要的特性:php
// 能夠使用數字下標的形式定義數組
$arr= ['Mike', 2 => 'JoJo'];
echo $arr[0], $arr[2];
// 也能夠使用字符串下標定義數組
$arr = ['name' => 'Mike', 'age' => 22];
// 能夠順序讀取數組中的數據
foreach ($arr as $key => $value) {
// Do Something
}
echo current($arr);
echo next($arr);
// 也能夠隨機讀取數組中的數據
$arr = ['name' => 'Mike', 'age' => 22];
echo $arr['name'];
// 數組的長度是可變的
$arr = [1, 2, 3];
$arr[] = 4;
array_push($arr, 5);
複製代碼
基於這些特性,咱們能夠很輕易的使用 PHP 中的數組實現集合、棧、列表、字典等多種數據結構。那麼這些特性在底層是如何實現的呢?且聽我細細道來。html
PHP 中的數組其實是一個有序映射。映射是一種把 values 關聯到 keys 的類型。—— PHP手冊算法
在 PHP 中,這種映射關係是使用散列表(HashTable)
實現的,在 C 語言中,只能經過數字下標訪問數組元素,而經過 HashTable,咱們能夠使用 String Key 做爲下標來訪問數組元素。簡單地說,HashTable 經過映射函數
將一個 Strring Key 轉化爲一個普通的數字下標,並將對應的 Value 值儲存到下標對應的數組元素中。數組
PHP 中的 HashTable 由 zend_array
定義,它的數據結構以下:數據結構
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar reserve)
} v;
uint32_t flags; /* 經過 32 個可用標識,設置散列表的屬性 */
} u;
uint32_t nTableMask; /* 值爲 nTableSize 的負數 */
Bucket *arData; /* 用來儲存數據 */
uint32_t nNumUsed; /* arData 中的已用空間大小 */
uint32_t nNumOfElements; /* 數組中的元素個數 */
uint32_t nTableSize; /* 數組大小,老是 2 冪次方 */
uint32_t nInternalPointer; /* 下一個數據元素的指針,用於迭代(foreach) */
zend_long nNextFreeElement; /* 下一個可用的數值索引 */
dtor_func_t pDestructor; /* 數據析構函數(句柄) */
};
複製代碼
該結構中的 Bucket
即儲存元素的數組,arData
指向數組的起始位置,使用映射函數
對 key 值進行映射後能夠獲得偏移值,經過內存起始位置 + 偏移值便可在散列表中進行尋址操做。Bucket 的數據結構以下:函數
typedef struct _Bucket {
zval val; /* 值 */
zend_ulong h; /* 使用 time 33 算法對 key 進行計算後獲得的哈希值(或爲數字索引) */
zend_string *key; /* 當 key 值爲字符串時,指向該字符串對應的 zend_string(使用數字索引時該值爲 NULL) */
} Bucket;
複製代碼
散列表主要由儲存元素的數組(Bucket)和散列函數兩部分構成。性能
當指定一個 Key-Value
映射關係時,若是 Key 爲 String 類型,則先經過 Time 33
算法將其轉換爲一個 Int 類型的整數,而後再先經過 PHP 中某種特定的散列算法將該 Int 映射爲 Bucket 數組中的一個下標,最終將 Value 儲存到該下標對應的元素中。 經過 Key 訪問數組時,只須要使用相同的算法計算出對應下標,而後取出對應元素中的 Value 值,便可實現隨機讀取。ui
由上面所講可知,儲存在 HashTable 中的元素是無序的,而 PHP 中的數組是有序的,PHP 是如何解決這個問題的呢?spa
爲了實現 HashTable 的有序性,PHP 爲其增長了一張中間映射表,該表是一個大小與 Bucket 相同的數組,數組中儲存整形數據,用於保存元素實際儲存的 Value 在 Bucekt 中的下標。注意,加入了中間映射表後,Bucekt 中的數據是有序的,而中間映射表中的數據是無序的。這樣順序讀取時只須要訪問 Bucket 中的數據便可。.net
zend_array 中並無單獨定義中間映射表,而是將其與 arData 放在一塊兒,數組初始化時並不僅分配 Bucket 大小的內存,同時還會分配相同大小空間的數據來做爲中間映射表,其實現方式如圖:
由上一節可知,散列函數其實是先將 hash code
映射到中間映射表中,再由中間映射表指向實際存儲 Value 的元素。
PHP 中採用以下方式對 hash code 進行散列:
nIndex = key->h | nTableMask;
複製代碼
由於散列表的大小恆爲 2 的冪次方,因此散列後的值會位於 [nTableMask, -1] 之間,即中間映射表之中。
任何散列函數都會出現哈希衝突的問題,常見的解決哈希衝突的方法有如下幾種:
PHP 採用的是其中的鏈地址法
,將衝突的 Bucket 串成鏈表,這樣中間映射表映射出的就不是某一個元素,而是一個 Bucket 鏈表,經過散列函數定位到對應的 Bucket 鏈表時,須要遍歷鏈表,逐個對比 Key 值,繼而找到目標元素。
新元素 Hash 衝突時的插入分爲如下兩步:
next
中能夠看出,PHP 在 Bucket 原有的數組結構上,實現了靜態鏈表
,從而解決了哈希衝突的問題。
HashTable 中的查找過程其實已經在上面說完了:
time 33
算法對 key 值計算獲得 hash code
nIndex
,即元素在中間映射表的下標idx
靜態鏈表
的頭結點在 C 語言中,數組的長度是定長的,那麼若是空間已滿還需繼續插入的時候怎麼辦呢?PHP 的數組在底層實現了自動擴容機制,當插入一個元素且沒有空閒空間時,就會觸發自動擴容機制,擴容後再執行插入。
須要提出的一點是,當刪除某一個數組元素時,會先使用標誌位對該元素進行邏輯刪除,而不會當即刪除該元素所在的 Bucket,由於後者在每次刪除時進行一次排列操做,從而形成沒必要要的性能開銷。
擴容的過程爲:
閾值
,PHP 則會申請一個大小是原數組兩倍的新數組,並將舊數組中的數據複製到新數組中,由於數組長度發生了改變,因此 key-value 的映射關係須要從新計算,這個步驟爲重建索引。注:由於在重建索引時須要從新計算映射關係,因此將舊數組複製到新數組中時,中間映射表的數據是無需複製的。