PHP是一門入門容易,使用范圍廣泛的語言,以其靈活性以及web后端開發被很多人熟知,也被很多人戲稱“PHP是世界上最好的語言”。本人是一名“忠實”的PHPer,相信用過PHP的程序員都會體會到PHP數組的靈活性,相對傳統的C語言,使用起來很是方便,擁有關聯數組(key值可以是字符串),不需要預定義數組空間大小,關聯數組,不需要指定key的快速索引賦值等等便利方法,這段時間研究了一下PHP數組的底層結構,並總結分析,里面含有一些我自己的猜想,如有錯誤請指出。
1.PHP的數組底層結構
哈希結構是一種非常重要的數據結構,他是一種通過key映射到value的結構,由於其特性,可以在大部分的情況下讓查找和插入的效率達到O(1),在很多語言或者系統里面都有顯性得體現出來,具體的實現思路有很多種。詳細的介紹可以看我的博客數據結構之哈希結構
typedef struct Bucket{ ulong h;//哈希值 uint nKeyLength; //key的長度,如果key是整形,則此項不需要賦值 Bucket* pNext; //該桶后面的桶,沖突處理的桶 Bucket* pLast; //該桶前面的桶,沖突處理的桶 Bucket* pListNext; //用以記錄數組的順序,該元素前一個元素。 Bucket* pListLast; //用以記錄數組的順序,該元素后一個元素。 const char * pData; //模擬記錄PHP數據,原來是void *pData和 void *pDataPtr char arKey[1] //記錄key,之所以是[1]是因為這是柔性成員,具體可以百度C99柔性成員 }Bucket;
下面是PHP HashTable結構,HashTable是用以存儲Bucket數組和Bucket信息的哈希表結構,采用雙向鏈表的拉鏈法結構。
typedef struct HashTable{ uint nTableSize; //哈希表的大小 uint nTableMask; //哈希表掩碼,用以矯正過長的哈希值 ulong nNumOfElements; //記錄當前哈希表存儲了多少個元素,用count($arr)其實就是取出hash表的這個數據 ulong NextFreeELement; //記錄下一個空閑位置的索引位置,$arr[]=$value里的$value就會放到該空間。 Bucket* pListHead; //記錄PHP數組的第一個元素 Bucket* pLstTail; //記錄PHP數組的最后一個元素 Bucket* pInternalPointer; //記錄當前哈希表指向的Bucket,在foreach,current,next,prev等等會用到, Bucket** arBuckets; //指向存儲實際Hash數組的指針的指針。 }
可能首次去看數據結構可能會覺得有點難受,密密麻麻的一堆東西,下面我會一個個分析數據字段。
2.Bucket結構體
1.h(哈希值)
通過key映射的哈希值(未經過糾正)h,為了讓不同key值均勻分配到哈希表的各個位置,必須要有一個好的哈希函數,而PHP選用的是time33算法,也就是下面的算法(簡化版)。
ulong hash(const char* key){ ulong hash; for(int i=0;key[i];i++){ hash=hash*33+key[i]; } return hash; }
當然啦在PHP的具體實現細節又會有點不同,但是原理是差不多的。
2.nKeyLength(字符的個數)
如果使用的是關聯索引,那么此處nKeyLength就是字符的個數,比如說$arr['key']='value' ,那么這個值就為3,如果是索引數組,此字段就不會用上。
3.pNext pLast (記錄該桶的前后桶)
繼續引用百度的圖,類似於下面的哈希表,拿元素337來說,他的pNext指向353的位置,pLast指向1的位置,只不過下面是單向鏈表,沒有看到當前元素指向前一個元素。
4.pListNext(記錄該桶在數據上的后元素)
這個字段從命名意思就可以看出,是鏈表的指向后繼元素的指針。比如說作如下賦值。guangdong的pListNext指向beijing,beijing的pListNext指向shanghai.....
$arr[2]="guangdong"; $arr[1]="beijing"; $arr[3]="shanghai"; $arr[4]="zhejiang";
所以你如果用foreach去遍歷數組,會發現一個很有趣的現象。輸出的結果如下,居然不是按照數組下標1,2,3,4順序去輸出,其實只要你理解了PHP的存儲數組的數據結構你就很明白了。
2 => "guangdong" 1 => "beijing" 3 => "shanghai" 4 => "zhejiang"
他的數據結構如下圖顯示,第一個元素是guangdong,然后來個元素beijing,於是guangdong的pListLast指向beijing,后面的元素同理。而foreach遍歷會從第一個元素(也就是pListHead指向的Bucket,詳看下文HashTable的介紹)去輸出,然后再指向下一個元素,因此輸出的順序不是按照下標來的,而是按照賦值順序來的,這也是為什么foreach遍歷數組要比for遍歷要快的原因,因為for每次查找元素都要去做一次哈希映射查找對應下標的Bucket,而foreach只需要遍歷Bucket鏈表就好了。pListLast與pListNext同理,只是指向前一個數組元素。
5.arKey(用以存儲key值)
這是一個c99柔性成員,如果需要深究可以百度查查c的柔性成員,如果這一個關聯數組,這個arKey就是存儲對應的key值。$arr['abc']='value';那么arKey存儲的就是abc。
3.HashTable結構體
1.nTableSize(哈希表的大小)
這是哈希表的分配Bucket空間的大小,默認會分配8個Bucket空間,當存儲元素個數大於8個就會存儲16個,如此下去,存儲的個數為2x大小,即8,16,32,64...
2.nTableMask(糾正掩碼)
用以糾正過長的哈希值,值為nTableSize-1,比如說一個我有一個字符經過哈希函數得出值為9,但是nTableSize為8,那該怎么辦呢,存放到第1個位置吧,計算方法就是9 mod 8,但是在計算機里面下標是從0開始,因此我們會使用&運算得出結果,9&7=1。
3.nNumOfElements(數組元素個數)
用以統計數組元素的個數,PHP的count()元素其實就是獲取這個值。
4.NextFreeElement(下一個空閑的元素)
用以存儲下一個空閑的元素的值。當你的數組是索引數組,用到$arr[]=value賦值就會用到,如果你上次賦值的元素下標是100,那么NextFreeELement就為101了。無關你的元素個數。
5.pListHead(鏈表的頭部元素)
這個Bucket指針從名字就可以看出來,用以指向鏈表的頭部元素,例如你給一個數組第一次附上一個值$arr[]=value1,那么這個指針就是指向value1。
6.pListTail(鏈表的尾部元素)
原理同上,只是指向尾部元素,每次來一個新的數組元素,pListTail就會指向它。
7.nInternalPointer(用以指向內部指向的元素)
如果我們用foreach遍歷數組,這個指針就會指向當前遍歷的元素,用以保存當前指向記錄。用到此項的還有current(),next(),prev()函數。
8.arBuckets(用以存儲Bucket在C的內部數組)
此項為指針的指針,可以用於操作Bucket數組。
下面列出一副圖來說明PHP的數組結構(為版面清晰忽略了兩種指向前一個Bucket的指針:pListLast,pLast)。
最后我大概猜想一下foreach函數的執行過程,首先是將nInternalPointer指向HashTable的第一個Buckets,也就是pListHead,如果不為空則輸出該元素,然后nInternaPointer指向該Bucket的下一個元素,也就是pListNext,如此循環下去。
void foreach_print(HashTable *ht){ // 指向數組的頭元素 ht->pInternalPointer=ht->pListHead; // 如果不空則循環遍歷下去 while(ht->pInternalPointer){ printf("[%s][%s]\n", ht->pInternalPointer->arKey,ht->pInternalPointer->pData); // 然后指向下一個元素 ht->pInternalPointer = ht->pInternalPointer->pListNext; } }
最后附上自己的關聯數組實現方法,各位有興趣的可以下載來看看。