python學習10.18:深刻底層瞭解Python字典和集合,一眼看穿他們的本質!

字典和集合是進行過性能高度優化的 數據結構,特別是對於查找、添加和刪除操做。本節將結合實例介紹它們在具體場景下的性能表現,以及與列表等其餘數據結構的對比。

例如,有一個存儲產品信息(產品 ID、名稱和價格)的列表,如今的需求是,藉助某件產品的ID找出其價格。則實現代碼以下:html

def find_product_price(products, product_id):
    for id, price in products:
        if id == product_id:
            return price
    return None

products = [
    (111, 100),
    (222, 30),
    (333, 150)
]

print('The price of product 222 is {}'.format(find_product_price(products, 222)))

運行結果爲:python

The price of product 222 is 30

在上面程序的基礎上,若是列表有 n 個元素,由於查找的過程須要遍歷列表,那麼最壞狀況下的時間複雜度就爲 O(n)。即便先對列表進行排序,再使用二分查找算法,也須要 O(logn) 的時間複雜度,更況且列表的排序還須要 O(nlogn) 的時間。

但若是用字典來存儲這些數據,那麼查找就會很是便捷高效,只需 O(1) 的時間複雜度就能夠完成,由於能夠直接經過鍵的哈希值,找到其對應的值,而不須要對字典作遍歷操做,實現代碼以下:算法

products = {
111: 100,
222: 30,
333: 150
}

print('The price of product 222 is {}'.format(products[222]))

運行結果爲:服務器

The price of product 222 is 30

有些讀者可能對時間複雜度並無直觀的認識,不要緊,再給你們列舉一個實例。下面的代碼中,初始化了含有 100,000 個元素的產品,並分別計算出了使用列表和集合來統計產品價格數量的運行時間:數據結構

# 統計時間須要用到 time 模塊中的函數,瞭解便可
import time

def find_unique_price_using_list(products):
    unique_price_list = []
    for _, price in products: # A
        if price not in unique_price_list: #B
            unique_price_list.append(price)
    return len(unique_price_list)

id = [x for x in range(0, 100000)]
price = [x for x in range(200000, 300000)]
products = list(zip(id, price))

# 計算列表版本的時間
start_using_list = time.perf_counter()
find_unique_price_using_list(products)
end_using_list = time.perf_counter()
print("time elapse using list: {}".format(end_using_list - start_using_list))

# 使用集合完成一樣的工做
def find_unique_price_using_set(products):
    unique_price_set = set()
    for _, price in products:
        unique_price_set.add(price)
    return len(unique_price_set)

# 計算集合版本的時間
start_using_set = time.perf_counter()
find_unique_price_using_set(products)
end_using_set = time.perf_counter()
print("time elapse using set: {}".format(end_using_set - start_using_set))

運行結果爲:app

time elapse using list: 68.78650900000001
time elapse using set: 0.010747099999989018
能夠看到,僅僅十萬的數據量,二者的速度差別就如此之大。而每每企業的後臺數據都有上億乃至十億數量級,所以若是使用了不合適的數據結構,很容易形成服務器的崩潰,不但影響用戶體驗,而且會給公司帶來巨大的財產損失。

那麼,字典和集合爲何能如此高效,特別是查找、插入和刪除操做呢?

字典和集合的工做原理

字典和集合能如此高效,和它們內部的數據結構密不可分。不一樣於其餘數據結構,字典和集合的內部結構都是一張哈希表:
  • 對於字典而言,這張表存儲了哈希值(hash)、鍵和值這 3 個元素。
  • 而對集合來講,哈希表內只存儲單一的元素。

對於以前版本的 Python 來講,它的哈希表結構以下所示:
| 哈希值 (hash)  鍵 (key)  值 (value)
. |           ...
0 |    hash0      key0    value0
. |           ...
1 |    hash1      key1    value1
. |           ...
2 |    hash2      key2    value2
. |           ...
這種結構的弊端是,隨着哈希表的擴張,它會變得愈來愈稀疏。好比,有這樣一個字典:
{'name': 'mike', 'dob': '1999-01-01', 'gender': 'male'}
那麼它會存儲爲相似下面的形式:
entries = [
['--', '--', '--']
[-230273521, 'dob', '1999-01-01'],
['--', '--', '--'],
['--', '--', '--'],
[1231236123, 'name', 'mike'],
['--', '--', '--'],
[9371539127, 'gender', 'male']
]
顯然,這樣很是浪費存儲空間。爲了提升存儲空間的利用率,如今的哈希表除了字典自己的結構,會把索引和哈希值、鍵、值單獨分開,也就是採用以下這種結構:
Indices
----------------------------------------------------
None | index | None | None | index | None | index ...
----------------------------------------------------

Entries
--------------------
hash0   key0  value0
---------------------
hash1   key1  value1
---------------------
hash2   key2  value2
---------------------
        ...
---------------------

在此基礎上,上面的字典在新哈希表結構下的存儲形式爲:
indices = [None, 1, None, None, 0, None, 2]
entries = [
[1231236123, 'name', 'mike'],
[-230273521, 'dob', '1999-01-01'],
[9371539127, 'gender', 'male']
]
經過對比能夠發現,空間利用率獲得很大的提升。

清楚了具體的設計結構,接下來再分析一下如何使用哈希表完成對數據的插入、查找和刪除操做。

哈希表插入數據

當向字典中插入數據時,Python 會首先根據鍵(key)計算出對應的哈希值(經過 hash(key) 函數),而向集合中插入數據時,Python會根據該元素自己計算對應的哈希值(經過 hash(valuse) 函數)。

例如:函數

dic = {"name":1}
print(hash("name"))
setDemo = {1}
print(hash(1))

運行結果爲:性能

8230115042008314683
1
獲得哈希值(例如爲 hash)以後,再結合字典或集合要存儲數據的個數(例如 n),就能夠獲得該元素應該插入到哈希表中的位置(好比,能夠用 hash%n 的方式)。

若是哈希表中此位置是空的,那麼此元素就能夠直接插入其中;反之,若是此位置已被其餘元素佔用,那麼 Python 會比較這兩個元素的哈希值和鍵是否相等:
  • 若是相等,則代表該元素已經存在,再比較他們的值,不相等就進行更新;
  • 若是不相等,這種狀況稱爲哈希衝突(即兩個元素的鍵不一樣,但求得的哈希值相同)。這種狀況下,Python 會使用開放定址法、再哈希法等繼續尋找哈希表中空餘的位置,直到找到位置。

具體遇到哈希衝突時,各解決方法的具體含義可閱讀《哈希表詳解》一節作詳細瞭解。優化

哈希表查找數據

在哈希表中查找數據,和插入操做相似,Python 會根據哈希值,找到該元素應該存儲到哈希表中的位置,而後和該位置的元素比較其哈希值和鍵(集合直接比較元素值):
  • 若是相等,則證實找到;
  • 反之,則證實當初存儲該元素時,遇到了哈希衝突,須要繼續使用當初解決哈希衝突的方法進行查找,直到找到該元素或者找到空位爲止。

這裏的找到空位,表示哈希表中沒有存儲目標元素。spa

哈希表刪除元素

對於刪除操做,Python 會暫時對這個位置的元素賦於一個特殊的值,等到從新調整哈希表的大小時,再將其刪除。

須要注意的是,哈希衝突的發生每每會下降字典和集合操做的速度。所以,爲了保證其高效性,字典和集合內的哈希表,一般會保證其至少留有 1/3 的剩餘空間。隨着元素的不停插入,當剩餘空間小於 1/3 時,Python 會從新獲取更大的內存空間,擴充哈希表,與此同時,表內全部的元素位置都會被從新排放。

雖然哈希衝突和哈希表大小的調整,都會致使速度減緩,可是這種狀況發生的次數極少。因此,平均狀況下,仍能保證插入、查找和刪除的時間複雜度爲 O(1)
相關文章
相關標籤/搜索