php 數組的結構和定義

數組是PHP中很是強大、靈活的一種數據類型,它的底層實現爲散列表(HashTable,也稱做:哈希表)node

散列表是根據關鍵碼值(Key value)而直接進行訪問的數據結構,它的key - value之間存在一個映射函數,能夠根據key經過映射函數直接索引到對應的value值,它不以關鍵字的比較爲基本操做,採用直接尋址技術(就是說,它是直接經過key映射到內存地址上去的),從而加快查找速度,在理想狀況下,無須任何比較就能夠找到待查關鍵字,查找的指望時間爲O(1)。算法

存放記錄的數組稱作散列表,這個數組用來存儲value,而value具體在數組中的存儲位置由映射函數根據key計算肯定,映射函數能夠採用取模的方式,key能夠經過一些譬如「times 33」的算法獲得一個整形值,而後與數組總大小取模獲得在散列表中的存儲位置。這是一個普通散列表的實現,PHP散列表的實現總體也是這個思路,只是有幾個特殊的地方,下面就是PHP中HashTable的數據結構:數組

 1 struct _zend_array {
 2     zend_refcounted_h gc; //引用計數
 3     union {
 4         struct {
 5             ZEND_ENDIAN_LOHI_4(
 6                 zend_uchar    flags,
 7                 zend_uchar    nApplyCount,
 8                 zend_uchar    nIteratorsCount,
 9                 zend_uchar    consistency)
10         } v;
11         uint32_t flags;
12     } u;
13     uint32_t          nTableMask; //哈希值計算掩碼,等於nTableSize的負值(nTableMask = -nTableSize)
14     Bucket           *arData;     //存儲元素數組,指向第一個Bucket
15     uint32_t          nNumUsed;   //已用Bucket數
16     uint32_t          nNumOfElements; //哈希表有效元素數
17     uint32_t          nTableSize;     //哈希表總大小,爲2的n次方
18     uint32_t          nInternalPointer;
19     zend_long         nNextFreeElement; //下一個可用的數值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3;  則nNextFreeElement = 2;
20     dtor_func_t       pDestructor;
21 };

HashTable中有兩個很是相近的值:nNumUsednNumOfElementsnNumOfElements表示哈希表已有元素數,那這個值不跟nNumUsed同樣嗎?爲何要定義兩個呢?實際上它們有不一樣的含義,當將一個元素從哈希表刪除時並不會將對應的Bucket移除,而是將Bucket存儲的zval修改成IS_UNDEF,只有擴容時發現nNumOfElements與nNumUsed相差達到必定數量(這個數量是:ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5))時纔會將已刪除的元素所有移除,從新構建哈希表。因此nNumUsed>=nNumOfElements數據結構

HashTable中另一個很是重要的值arData,這個值指向存儲元素數組的第一個Bucket,插入元素時按順序 依次插入 數組,好比第一個元素在arData[0]、第二個在arData[1]...arData[nNumUsed]。PHP數組的有序性正是經過arData保證的,這是第一個與普通散列表實現不一樣的地方。函數

既然arData並非按key映射的散列表,那麼映射函數是如何將key與arData中的value創建映射關係的呢?ui

實際上這個散列表也在arData中,比較特別的是散列表在ht->arData內存以前,分配內存時這個散列表與Bucket數組一塊兒分配,arData向後移動到了Bucket數組的起始位置,並非申請內存的起始位置,這樣散列表能夠由arData指針向前移動訪問到,即arData[-1]、arData[-2]、arData[-3]......散列表的結構是uint32_t,它保存的是value在Bucket數組中的位置。spa

因此,總體來看HashTable主要依賴arData實現元素的存儲、索引。插入一個元素時先將元素按前後順序插入Bucket數組,位置是idx,再根據key的哈希值映射到散列表中的某個位置nIndex,將idx存入這個位置;查找時先在散列表中映射到nIndex,獲得value在Bucket數組的位置idx,再從Bucket數組中取出元素。指針

 

映射函數(即:散列函數)是散列表的關鍵部分,它將key與value創建映射關係,通常映射函數能夠根據key的哈希值與Bucket數組大小取模獲得,即key->h % ht->nTableSize,可是PHP卻不是這麼作的:code

nIndex = key->h | ht->nTableMask;

顯然位運算要比取模更快。blog

nTableMasknTableSize的負數,即:nTableMask = -nTableSize,由於nTableSize等於2^n,因此nTableMask二進制位右側所有爲0,也就保證了nIndex落在數組索引的範圍以內(|nIndex| <= nTableSize):

 

哈希碰撞是指不一樣的key可能計算獲得相同的哈希值(數值索引的哈希值直接就是數值自己),可是這些值又須要插入同一個散列表。通常解決方法是將Bucket串成鏈表,查找時遍歷鏈表比較key。

PHP的實現也是如此,只是將鏈表的指針指向轉化爲了數值指向,即:指向衝突元素的指針並無直接存在Bucket中,而是保存到了value的zval中:

 1 struct _zval_struct {
 2     zend_value        value;            /* value */
 3     ...
 4     union {
 5         uint32_t     var_flags;
 6         uint32_t     next;                 /* hash collision chain(哈希碰撞鏈) */
 7         uint32_t     cache_slot;           /* literal cache slot */
 8         uint32_t     lineno;               /* line number (for ast nodes) */
 9         uint32_t     num_args;             /* arguments number for EX(This) */
10         uint32_t     fe_pos;               /* foreach position */
11         uint32_t     fe_iter_idx;          /* foreach iterator index */
12     } u2;
13 };

當出現衝突時將原value的位置保存到新value的zval.u2.next中,而後將新插入的value的位置更新到散列表,也就是後面衝突的value始終插入header

數組中存儲元素的結構

1 typedef struct _Bucket {
2     zval              val; //存儲的具體value,這裏嵌入了一個zval,而不是一個指針
3     zend_ulong        h;   //key根據times 33計算獲得的哈希值,或者是數值索引編號
4     zend_string      *key; //存儲元素的key
5 } Bucket;
相關文章
相關標籤/搜索