源代碼選用 最多見的 cpythonpython
首先來看看構建dict的基礎設施:算法
typedef struct {
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;函數
這個結構體爲dict中key-value,其中的me_hash爲me_key的hash值,[空間換時間]。除此以外,咱們發現me_key與me_value都是PyObject指針類型,這也說明了爲何dict中的key與value能夠爲python中的任何類型數據。spa
struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill;
Py_ssize_t ma_used;
Py_ssize_t ma_mask;
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};設計
這個結構體即是dict了。按照咱們一般的理解,dict應該是可變長對象啊!爲何這裏還有PyObject_HEAD,而不是PyObject_VAR_HEAD。仔細一看,dict的可變長與 string,list,tuple 仍有不一樣以外,後者能夠經過PyObject_VAR_HEAD中的ob_size來指明其內部有效元素的個數。但dict不能這樣作,因此dict乾脆繞開PyObject_VAR_HEAD,並且除了有ma_used這個字段來交代出其有效元素的個數,還須要ma_fill來交代清楚曾經有效元素的個數(用來計算加載率)。指針
ma_mask,則牽扯到hash中的散列函數;
ma_smalltable,python一貫的有限空間換時間,一個小池子來應付大多數的小dict(不超過PyDict_MINSIZE);
ma_lookup,則是一次探測與二次探測函數的實現。對象
在展開dict實現細節前,先把dict使用的解決衝突的開放定址法介紹一下。咱們知道哈希,就是將一個無限集合映射到一個有限集,若是選擇理想的hash函數,可以將預期處理到的元素均勻分佈到有限集中便可在O(1)時間內完成元素查找。但理想的hash函數是不存在的,且因爲映射的本質(無限到有限)必然出出現一個位置有多個元素要‘佔據’,這就須要解決衝突。現有的解決衝突的方法:內存
其中建域法基本思想爲假設哈希函數的值域爲[0,m-1],則設向量HashTable[0..m-1]爲基本表,另外設立存儲空間向量OverTable[0..v]用以存儲發生衝突的記錄。源碼
其中前兩種方法實現最爲簡單高效,下面回顧下開放定址與鏈地址法。string
開放定址法:造成hash表時,某元素在第一次探測其應該佔有的位置時,若是發現此處(記爲A)已經被別人佔了,那就在從A開始,再次探測(固然此次探測使用的hash函數與第一次已經不同了),若是發現仍是被別人佔了,那麼繼續探測,至到找到一個可用位置(也有可能在當下條件下永遠找不到)。開放地址法有一個相當重要的問題須要解決,那就是在一個元素離開hash表時,如何處理離開後的位置狀態。若是設置爲原始空狀態,那麼後續的有效元素就沒法識別了,由於在查找時一樣是依據上面的探測規則進行查找,因此必須告訴探測函數某個位置雖然無有效元素了,但後續的探測可能會出現有效元素。咱們能夠發現,開放定址法很容易發生衝突(主要是一次探測以上成功的元素佔取其它元素應該在第一次探測成功的位置),因此就須要加大hash有效空間。
鏈地址法:鏈地址法的思想很簡單,你不是可能會出現多個元素對應同一個位置,那麼我就在這個位置拉出一個鏈表來存放因此hash到這個位置的元素。很簡單吧,還節約內存呢!很遺憾,python的設計者沒有選它。
那爲何python發明者選擇了開放定址而不是鏈地址法,在看python源碼時看到這麼一段話:
Open addressing is preferred over chaining since the link overhead(開銷) for chaining would be substantial(大量) (100% with typical malloc overhead).
因爲鏈地址法須要動態的生成鏈表結點(malloc),因此時間效率不如開放定址法(但開放定址法的裝載率不能高於2/3,相對於鏈地址法的空間開銷也是毋庸置疑的),由此能夠看出python的設計時代已經不是那個內存只有512k可供使用的時代了,對內存的苛刻已經讓步於效率。固然這須要考慮到python因爲實現動態而必須靠自身的設計將損失的時間效率儘量地補回來。
好了,交待完開放定址法與爲何python設計者選擇它後,咱們來看看dict如何實現這個算法的。前面已經看到每一個key-value由一個Entry結構體實現,python就是利用entry自身的信息來指明每一個位置的狀態:原始空狀態、有效元素離去狀態、有效元素佔據狀態。
其中dict的hash方法與衝突解決方法的思路以下:
lookdict(k,v)
- index <- hash1(k),freeslot<-Null,根據me_key與me_value選擇二、三、4一個執行;
- 查看index處的值處於’有效元素佔據‘狀態,判斷data[index]與v是否一致(地址或內容),一致,則返回查找成功;不然轉5
- index所指向的位置處於’原始空‘狀態,查找失敗,若freeslot==Null返回index;不然返回freeslot;轉5
- index所指向的位置處於’有效元素離去‘狀態,freeslot<-index, 轉5
- index <- hash2(index),,轉2
dict的lookdict方法實現充分體現了python對內存的利用率與空間換時間提升效率上,表現爲以下方面: