“選擇正確的數據結構並堅持使用它!雖然對於某個特定操作來說也許還存在更高效的數據結構,但是在這些數據結構之間進行轉換的代價可能會抵消效率上的增益。” 摘自《Python高性能編程》
Python中的列表本質是動態的數組
,它與數組的區別在於:
1)數組定義好之后就無法擴容了,而列表在定義好之后可以擴容;
2)數組只能同時存儲一種類型的數據,而列表可以同時存儲不同類型的數據。列表為什么沒有數組這樣的限制呢?
我們知道數組底層的存儲結構是順序存儲結構,這樣的結構有這樣一些優點:
邏輯上相鄰的節點在物理位置上也是相鄰的,可以節省空間,並且可以實現隨機存取(也稱直接訪問)。創建一個數組時,會在內存中開辟一塊固定長度的區域用於直接存儲元素,擴容要考慮這塊區域的后面是否有存儲其他對象,所以數組在定義好之后就無法擴容了。而且在查詢時,是根據索引和元素存儲大小去計算地址偏移量的,如果元素類型不一致,所占內存空間不相同,就不能實現隨機存儲,所以數組不能同時存儲不同類型的數據;而列表就不同了,它存儲的是每個元素在內存中的地址(即引用),當列表中空白占位低於1/3時,會在內存中開辟一塊更大的空間,並將舊列表中存儲的地址復制到新列表中,舊列表則被銷毀,這樣就實現了擴容。因為列表存儲的是元素的引用這個特性,而引用所占的內存空間是相同的,這樣便可以同時存放不同類型的數據了。
Python中的字典底層是通過散列表(哈希表)來實現的, “哈希表是根據
關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做
散列函數,存放記錄的數組叫做散列表。”
字典本質也是一個數組,但其索引是鍵經過散列函數處理后得到的散列值,散列函數的目的是使鍵均勻地分布在散列表中,並且可以在內存中以O(1)的時間復雜度進行尋址,從而實現快速查找和修改。散列表其實是一個稀疏數組(總是有空白元素的數組稱為稀疏數組),散列表里的單元通常叫作表元。在字典的散列表當中,每個鍵值對都占用一個表元,每個表元都有兩個部分,一個是對鍵的引用,另一個是對值的引用。散列表中散列函數的設計困難在於將數據均勻分布在散列表中,從而盡量減少散列碰撞和沖突(散列沖突指的是在查詢過程中通過索引定位到有值的表元時,發現該位置的key和查詢的key不相等。高級的散列函數能夠使沖突數目最小化。散列沖突在這里不過多描述)。
字典數據的添加和查詢過程如下:
1)添加:把key通過散列函數轉換成一個整型數字(即散列值),把這個值最低的幾位數字當作數組存儲value的下標,最后再存儲value到數組中;
2)查詢:使用散列函數將key轉換為數組的下標,並定位到數組對應位置獲取value。
字典為什么是無序的?字典的無序是由兩方面構成的:
1)上面有說過Python中列表會設法保證大概還有1/3的表元是空的,所以快要達到這個閥值時,原有的散列表會被復制到一個更大的空間中去。在這個過程中會重新對字典的鍵進行散列化(散列表的大小增加了,那散列值所占位數和用作索引的位數都會隨之增加,這樣做的目的是為了減少發生散列沖突的概率),生成新的散列值,此時由於散列值的不同,則可能導致鍵的次序不同;
2)另一方面就是就是散列沖突,當添加新鍵時發生散列沖突(即新鍵通過散列函數處理得到的散列值和字典中其他鍵的散列值相同,則需要在散列值中另外再取幾位,然后用特殊的方法處理一下,把新得到的數字當作索引來尋找空的表元),新鍵可能會被安排到另一個位置,於是新添加的元素可能就跑到前面去了。
最后再說一點,因為字典的存儲使用了散列表,為了減少散列沖突發生概率,散列表必須是稀疏的,所以字典在內存上的開銷是很大的。
Python中列表和字典在時間復雜度的比較
|
list
|
dict
|
查詢
|
查找某個元素的索引:O(1)
搜索某個元素:O(n)
|
獲取某個元素: O(1)
根據鍵搜索:O(1)
|
添加
|
尾部添加:O(1)
任意位置添加:O(n)
|
鍵添加:O(1)
|
刪除
|
尾部刪除:O(1)
|
根據鍵來刪除:O(1)
|
修改
|
索引賦值:O(1)
|
鍵賦值:O(1)
|