例如,有一個儲存產品資訊(產品 ID、名稱和價格)的列表,現在的需求是,藉助某件產品的ID找出其價格。則實現程式碼如下:
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)))
執行結果為:
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]))
有些讀者可能對時間複雜度並沒有直觀的認識,沒關係,再給大家列舉一個例項。下面的程式碼中,初始化了含有 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))
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]
透過對比可以發現,空間利用率得到很大的提高。
雜湊表插入資料
當向字典中插入資料時,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 會根據雜湊值,找到該元素應該儲存到雜湊表中的位置,然後和該位置的元素比較其雜湊值和鍵(集合直接比較元素值):
如果相等,則證明找到;
反之,則證明當初儲存該元素時,遇到了雜湊衝突,需要繼續使用當初解決雜湊衝突的方法進行查詢,直到找到該元素或者找到空位為止。
這裡的找到空位,表示雜湊表中沒有儲存目標元素。
需要注意的是,雜湊衝突的發生往往會降低字典和集合操作的速度。因此,為了保證其高效性,字典和集合內的雜湊表,通常會保證其至少留有 1/3 的剩餘空間。隨著元素的不停插入,當剩餘空間小於 1/3 時,Python 會重新獲取更大的記憶體空間,擴充雜湊表,與此同時,表內所有的元素位置都會被重新排放。
例如,有一個儲存產品資訊(產品 ID、名稱和價格)的列表,現在的需求是,藉助某件產品的ID找出其價格。則實現程式碼如下:
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)))
執行結果為:
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))
執行結果為:
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 會根據雜湊值,找到該元素應該儲存到雜湊表中的位置,然後和該位置的元素比較其雜湊值和鍵(集合直接比較元素值):
如果相等,則證明找到;
反之,則證明當初儲存該元素時,遇到了雜湊衝突,需要繼續使用當初解決雜湊衝突的方法進行查詢,直到找到該元素或者找到空位為止。
這裡的找到空位,表示雜湊表中沒有儲存目標元素。
需要注意的是,雜湊衝突的發生往往會降低字典和集合操作的速度。因此,為了保證其高效性,字典和集合內的雜湊表,通常會保證其至少留有 1/3 的剩餘空間。隨著元素的不停插入,當剩餘空間小於 1/3 時,Python 會重新獲取更大的記憶體空間,擴充雜湊表,與此同時,表內所有的元素位置都會被重新排放。