python 字典實現原理
引言
Python中dict物件是表明了其是一個原始的Python資料型別,按照鍵值對的方式儲存,其中文名字翻譯為字典,顧名思義其通過鍵名查詢對應的值會有很高的效率,時間複雜度在常數級別O(1).本文針對其實現的資料結構進行原理性說明和拓展,不涉及Python的原始碼剖析。
dict底層實現
在Python2中,dict的底層是依靠雜湊表(Hash Table)進行實現的,使用開放地址法解決衝突. 所以其查詢的時間複雜度會是O(1),下文會具體講解雜湊表的工作原理和解決衝突時的具體方法。
雜湊表
雜湊表是key-value型別的資料結構,通過關鍵碼值直接進行訪問。通過雜湊函式進行鍵和陣列的下標對映從而決定該鍵值應該放在哪個位置,**雜湊表可以理解為一個鍵值需要按一定規則存放的陣列,而雜湊函式就是這個規則。**此處提出幾個專業名詞後面會一一進行介紹。
雜湊函式
裝填因子
衝突
1.雜湊表產生的原因
假設我們存在一個簡單的鍵值對結構,鍵-員工號,值-是否在崗。現在需要這樣一個功能,輸入員工號,返回該員工是否在崗,理想的方法是建立一個長度為Max(員工號)的陣列,陣列下標就是員工號,陣列中的值用0和1對是否在崗進行區分,這樣只需要O(1)的時間複雜度就可以完成操作,但是擴充套件性不強,存在以下問題。 1.假設新進員工的員工號比Max(員工號)還要大,這就需要重新申請陣列進行遷移操作。 2.假設一種極端的情況,存在兩個員工,員工號分別是1和100000000001,這樣子的話按照先前的設計思路,是會浪費很大的儲存空間的。
上面兩點,第一點是因為陣列的固定申請大小的屬性所決定,而第二點就是引入雜湊表的原因,會不會存在一個方法,讓一個大員工號變小而而且沒有標記,雜湊函式便產生,假設此處的雜湊規則是除3取模,則員工1得到的雜湊值是1,員工100000000001得到的雜湊值是2,這樣的話按照設計思路,只需要一個大小為2的陣列便可以覆蓋了,這就是雜湊思想。 演算法中時間和空間是不能兼得的,雜湊表就是一種用合理的時間消耗去減少大量空間消耗的操作,這取決於具體的功能要求。
2.雜湊函式
上面的例子中雜湊函式的設計很隨意,但是從這個例子中我們也可以得到資訊:
雜湊函式就是一個對映,因此雜湊函式的設定很靈活,只要使得任何關鍵字由此所得的雜湊函式值都落在表長允許的範圍之內即可; 並不是所有的輸入都只對應唯一一個輸出,也就是雜湊函式不可能做成一個一對一的對映關係,其本質是一個多對一的對映,這也就引出了下面一個概念–衝突。
3.衝突
只要不是一對一的對映關係,衝突就必然會發生,還是上面的極端例子,這時新加了一個員工號為2的員工,先不考慮我們的陣列大小已經定為2了,按照之前的雜湊函式,工號為2的員工雜湊值也是2,這與100000000001的員工一樣了,這就是一個衝突,針對不同的解決思路,提出三個不同的解決方法。
4.衝突解決方法
4.1 開放地址
開放地址的意思是除了雜湊函式得出的地址可用,當出現衝突的時候其他的地址也一樣可用,常見的開放地址思想的方法有線性探測再雜湊,二次探測再雜湊,這些方法都是在第一選擇被佔用的情況下的解決方法。
4.2 再雜湊法
這個方法是按順序規定多個雜湊函式,每次查詢的時候按順序呼叫雜湊函式,呼叫到第一個為空的時候返回不存在,呼叫到此鍵的時候返回其值。
4.3 鏈地址法
將所有關鍵字雜湊值相同的記錄都存在同一線性連結串列中,這樣不需要佔用其他的雜湊地址,相同的雜湊值在一條連結串列上,按順序遍歷就可以找到。
4.4公共溢位區
其基本思想是:所有關鍵字和基本表中關鍵字為相同雜湊值的記錄,不管他們由雜湊函式得到的雜湊地址是什麼,一旦發生衝突,都填入溢位表。
5.裝填因子α
一般情況下,處理衝突方法相同的雜湊表,其平均查詢長度依賴於雜湊表的裝填因子。雜湊表的裝填因子定義為表中填入的記錄數和雜湊表長度的桌布,也就是標誌著雜湊表的裝滿程度。直觀看來,α越小,發生衝突的可能性就越小,反之越大。一般0.75比較合適,涉及數學推導。
實現細節
CPython使用偽隨機探測(pseudo-random probing)的散列表(hash table)作為字典的底層資料結構。由於這個實現細節,只有可雜湊的物件才能作為字典的鍵。
Python中所有不可變的內建型別都是可雜湊的。 可變型別(如列表,字典和集合)就是不可雜湊的,因此不能作為字典的鍵。
字典的三個基本操作(新增元素,獲取元素和刪除元素)的平均事件複雜度為O(1),但是他們的平攤最壞情況複雜度要高得多,為O(N).