《算法圖解》學習筆記習題和代碼(第五章 散列表)Python3

目錄

第五章 散列表

 5.1 散列函數 

Python的散列表實現--字典 (代碼)

練習1

5.2 應用案例 

5.2.1 將散列表用於查找 

電話簿查找(代碼)

 5.2.2 防止重複 

防止重複投票(代碼)

5.2.3 將散列表用作緩存

散列表用作緩存(僞代碼)

 5.2.4 小結 

5.3 衝突 

5.4 性能 

5.4.1 填裝因子 

5.4.2 良好的散列函數 

 練習2

5.5 小結


第五章 散列表

  • 一種基本的數據結構
  • 散列表的內部機制:實現、衝突和散列函數。

假設你在雜貨店上班,需要查找某個商品的價格,比如蘋果(apple)。如果採用簡單查找的方式,就要瀏覽價格表每一行,需要的時間爲O(n),如果價格表是按字母排序的話,可以使用二分查找,需要的時間是O(logn)。

O(n)和O(logn)速度相差很大,假如你每秒鐘可以瀏覽十行價格單。

那麼如果我想,說出一個商品,店員能馬上報出價格(需要的時間是O(1)),比二分查找還要快,應該怎麼辦?這是散列函數的用武之地。  

 5.1 散列函數 

  • Python提供的散列表實現爲字典,你可使用函數dict來創建散列表。

散列函數:你給它什麼數據,它都還你一個數字。散列函數“將輸入映射到數字”。

我們爲商品價格創建一個散列表。將商品名稱作爲輸入交給散列函數。

先創建一個空數組。

比如將apple作爲輸入交給散列函數。  

散列函數的輸出爲3,因此我們將蘋果的價格存儲到數組的索引3處 。

接着對牛奶,鱷梨等商品進行以上操作。 不斷地重複這個過程,最終整個數組將填滿價格。

當你想知道鱷梨價錢的時候,只要輸入鱷梨,它將告訴你鱷梨的價格存儲在索引4處,你就知道了它的價格。

散列函數準確地指出了價格的存儲位置,你根本不用查找!原因如下:

  • 散列函數總是將同樣的輸入映射到相同的索引
  • 散列函數將不同的輸入映射到不同的索引。(不同索引位置上的值可能是相同的)
  • 散列函數知道數組有多大,只返回有效的索引。如果數組包含5個元素,散列函數就不會返回無效索引100。 

這樣就通過數組和散列函數創建了一種被稱爲散列表(hash table哈希表)的數據結構。散列表也被稱爲散列映射、映射、字典和
關聯數組。

所有優秀的語言都提供了散列表的實現,Python提供的散列表實現爲字典,你可使用函數dict來創建散列表。 

Python的散列表實現--字典 (代碼)

#Python的散列表數據結構就是字典
book = dict()
book['apple'] = 0.67
book['milk'] = 1.49
book['avocado'] = 1.49

print(book)

OUT:  {'apple': 0.67, 'milk': 1.49, 'avocado': 1.49}

散列表由鍵(key)和值(value)組成。在前面的散列表book中,鍵爲商品名,值爲商品價格。散列表將鍵映射到值。 

練習1

對於同樣的輸入,散列表必須返回同樣的輸出,這一點很重要。如果不是這樣的,就無法找到你在散列表中添加的元素! 

請問下面哪些散列函數是一致的? 
5.1 f(x) = 1            <---------無論輸入是什麼,都返回1

答:一致。
5.2 f(x) = rand()     <-----------每次都返回一個隨機數

答:不一致。
5.3 f(x) = next_empty_slot()      <--------------返回散列表中下一個空位置的索引

答:不一致。
5.4 f(x) = len(x)               <-------------將字符串的長度用作索引

答:一致。

5.2 應用案例 

散列表應用廣泛。

5.2.1 將散列表用於查找 

電話簿,通過姓名查找手機號。

電話簿的功能:添加聯繫人及其電話號碼。通過輸入聯繫人來獲悉其電話號碼。 

適合通過散列表實現:

創建映射。
查找。

電話簿查找(代碼)

#1.散列表用於查找  eg.電話簿
phone_book = {}  #與dict()函數等效
phone_book['jenny'] = 8675309
phone_book['emergency'] = 911

print(phone_book['jenny'])

 OUT: 8675309

訪問網頁也是通過網址映射到ip地址。這個過程被稱爲DNS解析(DNS resolution),散列表是提供這種功能的方式之一。 

 5.2.2 防止重複 

假設你管理一個投票站,你要防止人投票重複。

每次有人來投票時,你都得瀏覽這個長長的名單,以確定他是否投過票。更好的辦法,就是使用散列表!  

#散列表用於防止重複
voted = {} 
value = voted.get('tom')
print(value)

OUT: None

'tom'在列表裏,get函數就返回它,不在就返回None。

防止重複投票(代碼)

voted = {}
def check_voter(name):
    if voted.get(name):
        print ("Kick them out")
    else:
        voted[name] = True     #將新的姓名存在散列表(字典)裏
        print("Let them vote")

 驗證一下

check_voter('Tom')
>>>Let them vote
check_voter('Mike')
>>>Let them vote
check_voter('Mike')          #Mike前面已經投過了,出現過了
>>>Kick them out

5.2.3 將散列表用作緩存

訪問網頁的原理:假設你訪問網站facebook.com。 

(1) 你向Facebook的服務器發出請求。 
(2) 服務器做些處理,生成一個網頁並將其發送給你。 
(3) 你獲得一個網頁。

 每次搜索內容,可能需要幾秒鐘,你覺得緩慢。但是實際上Facebook的服務器在努力地爲數以百萬的用戶提供服務。有沒有辦法讓Facebook的服務器少做些工作,從而提高Facebook網站的訪問速度呢? 就可以用到緩存了。

假如你的孩子問你月球離地球多遠,你要google,再告訴她答案,需要耗時幾分鐘。但她如果總問你,你就記住了這個答案:月球離地球238 900英里。你不用再google,一秒鐘就告訴她答案。

緩存的工作原理就是:網站將數據記住,而不再重新計算。

Facebook被反覆要求做同樣的事情:“當我註銷時,請向我顯示主頁。”有鑑於此,它不讓服務器去生成主頁,而是將主頁存儲起來,並在需要時將其直接發送給用戶。 

緩存有兩個優點:

  • 用戶能夠更快地看到網頁
  • Facebook需要做的工作更少。

緩存是一種常用的加速方式,所有大型網站都使用緩存,而緩存的數據則存儲在散列表中。

Facebook不僅緩存主頁,還緩存About頁面、Contact頁面等,因此,它需要將頁面URL映射到頁面數據。 

散列表用作緩存(僞代碼)

僞代碼如下:

#3.散列表用作緩存
cache = {}         #一般緩存是要有內容
def get_page(url):
    if cache.get(url):                 #如果鏈接在緩存散列表裏
        return cache[url]                  #返回相應緩存的內容(網頁)
    else:
        data = get_data_from_server(url)    #如果鏈接不在緩存散列表裏,就從服務器獲取數據
        cache[url] = data                     #再將數據保存到緩存中去,便於下次訪問使用
        return data

 5.2.4 小結 

總結一下,散列表適合用於: 
模擬映射關係;
防止重複;
緩存/記住數據,以免服務器再通過處理來生成它們。

5.3 衝突 

我們創建一個數組,假設散列函數是按商品首字母映射存儲,共26個位置。

比如,將蘋果價格(apple)存儲到散列表中,給它分配的就是第一個位置,接着存香蕉,第二個位置。

 接下來存鱷梨(avagado),但用於存“a”開頭的第一個位置已經存放了蘋果,這時就產生了衝突。處理衝突最常見的方式是:如果兩個鍵映射到了同一個位置,就在這個位置存儲一個鏈表。

 此時查詢蘋果、鱷梨價格時就要花費更多時間搜尋鏈表。如果第一個位置存儲的鏈表過長(最糟情況,比如你的商店只賣a開頭的商品),散列表的速度就會變得很慢。

散列函數很重要,好的散列函數很少導致衝突。 

5.4 性能 

在平均情況下,散列表執行各種操作的時間都爲O(1)。O(1)被稱爲常量時間。下圖是簡單回顧:

不管數組多大,在平均情況下,散列表的速度確實很快。 

在最糟情況下(數組位置存儲鏈表),散列表所有操作的運行時間都爲O(n)——線性時間 。

散列表同數組和鏈表的比較:

散列表的查找(獲取給定索引處的值)速度與數組一樣快,而插入和刪除速度與鏈表一樣快,因此它兼具兩者的優點。

在最糟情況下,散列表的各種操作的速度都很慢。所以需要避免衝突。

避免衝突,需要有:
較低的填裝因子;
良好的散列函數。

底下兩小節是非必讀部分,因爲大多編程語言都內置非常良好的散列表實現,不用自己實現。

5.4.1 填裝因子 

填裝因子計算:

 散列表總元素數/位置總數

舉例說明填充因子:

 散列表各位置都填充一個元素,填充因子爲1。

若有100個元素,只有50個位置,填充因子爲2。就需要在散列表中添加位置,這被稱爲調整長度(resizing),創建更長的數組。

調整長度的開銷很大,一般不希望頻繁地這樣操作。但平均而言,即便考慮到調整長度所需的時間,散列表操作所需的時間也爲O(1)。 

5.4.2 良好的散列函數 

良好的散列函數讓數組中的值呈均勻分佈。 

糟糕的散列函數讓值扎堆,導致大量的衝突。

 什麼是好的散列函數,如果好奇可以研究本書最後一章SHA函數。

 練習2

散列函數的結果必須是均勻分佈的,這很重要。它們的映射範圍必須儘可能大。最糟糕的散列函數莫過於將所有輸入都映射到散列表的同一個位置。 假設你有四個處理字符串的散列函數。 
A. 不管輸入是什麼,都返回1。 
B. 將字符串的長度用作索引。 
C. 將字符串的第一個字符用作索引。即將所有以a打頭的字符串都映射到散列表的同一個位置,以此類推。 
D. 將每個字符都映射到一個素數:a = 2,b = 3,c = 5,d = 7,e = 11,等等。對於給定的字符串,這個散列函數將其中每個字符對應的素數相加,再計算結果除以散列表長度的餘數。例如,如果散列表的長度爲10,字符串爲bag,則索引爲(3 + 2 + 17) % 10 = 22 % 10 = 2。

在下面的每個示例中,上述哪個散列函數可實現均勻分佈?假設散列表的長度爲10。 
5.5 將姓名和電話號碼分別作爲鍵和值的電話簿,其中聯繫人姓名爲Esther、Ben、Bob和Dan。 
5.6 電池尺寸到功率的映射,其中電池尺寸爲A、AA、AAA和AAAA。 
5.7 書名到作者的映射,其中書名分別爲Maus、Fun Home和Watchmen。 

答:

                      5.5 C,D均可實現均勻分佈

                      5.6 B,D

                      5.7 B,C,D

(來自書後參考答案,應該是均可以實現比較均勻的分佈。)

5.5 小結

不用自己實現散列表,比如Python提供的散列表(字典),能夠獲得平均情況下的性能:O(1) 常量時間。

散列表是一種功能強大的數據結構,其操作速度快,還能讓你以不同的方式建立數據模型。

你可以結合散列函數和數組來創建散列表。
衝突很糟糕,你應使用可以最大限度減少衝突的散列函數。
散列表的查找、插入和刪除速度都非常快。
散列表適合用於模擬映射關係。
一旦填裝因子超過0.7,就該調整散列表的長度。
散列表可用於緩存數據(例如,在Web服務器上)。
散列表非常適合用於防止重複。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章