小白的算法初識課堂(part5)--散列表

學習筆記
學習書目:《算法圖解》- Aditya Bhargava




散列函數


散列函數是這樣的一個函數,即無論你給它什麼樣的數據,它都還你一個數字。如果用專業術語來表達的話,我們可以說,散列函數“將輸入映射到數字”。

在這裏插入圖片描述

我們可以認爲散列函數輸出的數字沒啥規律,但是散列函數還是有一些必須要滿足的要求的:

  • 它必須是一致的。例如,假設你輸入apple時得到的是4,那麼每次輸入apple時,得到的都必須爲4。
  • 它應將不同的輸入映射到不同的數字。

散列函數將輸入映射成數字,有啥用呢?

爲了回答這個問題,我們構建一個空數組:

在這裏插入圖片描述


假如我有一個小商店,我要將商品的價格存儲在這個數組中。

現在,我想先把apple的價格存入數組,則我將apple作爲輸入交給散列函數,並得到其輸出爲3,那麼我們就把apple的價格存儲到數組的索引3處;緊接着,我想將milk的價格存入數組,按照同樣的步驟得到散列函數的輸出0,則我們把milk的價格存儲到數組的索引0處.不斷重複這個步驟,直至數組填滿:

在這裏插入圖片描述

如果,我現在想知道pen的價格,我不用在數組中查找,只需要將pen作爲輸入交給散列函數,散列函數告訴我pen的價格存儲在數組的索引4處,那麼我們就得到了pen的價格20.5


散列函數會準確的幫助我們找到商品價格的存儲位置,我們不用自己去查找。之所以散列函數可以這樣做,是因爲以下幾點:

  • 散列函數總是將同樣的輸入映射到相同的索引。
  • 散列函數將不同的輸入映射到不同的索引。
  • 散列函數知道數組有多大,只返回有效的索引。

我們可以結合散列函數和數組創建一種被稱爲散列表的數據結構。散列表是一種包含額外邏輯的數據結構。數組和鏈表都被直接映射到內存,但散列表更復雜,它使用散列函數來確定元素的存儲位置。在我們之後將學習的複雜數據結構中,散列表可能是最有用的,也被稱爲散列映射、映射、字典和關聯數組。


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

shop = dict()

創建散列表shop之後,我們在其中添加一些商品及其價格:

shop['apple'] = 3.5
shop['milk'] = 15.0
shop['pen'] = 20.5

我們看一下剛剛創建的字典:

In [67]: shop
Out[67]: {'apple': 3.5, 'milk': 15.0, 'pen': 20.5}

再利用字典查看一下pen的價格:

In [68]: shop['pen']
Out[68]: 20.5

散列表由鍵和值組成。在散列表shop中,鍵爲商品名,值爲商品價格。散列表將鍵映射到值。


防止重複


剛纔我已經開了一個小商店,我每天要登記新進的商品,並把新商品的名稱及其價格登記在我的散列表中。爲了防止登記重複,我會在登記新商品之前,先檢查一下散列表中是否已經登記過該商品,如果我發現已經登記了該商品(函數get返回該商品價格),那我就不登記它,如果沒有登記過(函數get返回None),那我就登記它:

In [70]: print(shop.get('orange'))
None

In [71]: print(shop.get('pen'))
20.5

現在我們構造一個函數,來判斷是否登記過某商品:

def check_pro(name, price):
    if shop.get(name) is None:
        shop[name] = price
        print('Register now')
    else:
        print('Item already exists')

控制檯調用:

In [78]: check_pro('book', 30)
Register now

In [79]: check_pro('book', 30)
Item already exists

衝突


在解釋衝突之前,我想先舉個例子方便理解。

假設我有一個數組,它有26個位置。我的散列函數規則很簡單,它按照商品首字母的順序分配商品價格在數組中的存儲位置。這時,我們可能會提出疑問:如果我有兩個商品book和bunny,它們的首字母都相同,那麼我該怎麼存儲呢?如果我先存儲了book的價格,它在數組的索引1處,那麼當我存儲bunny的價格時,該咋辦呢?這種情況就叫做衝突:給兩個鍵分配相同的位置。

處理衝突的方法有很多,最簡單的就是下面這種:

如果兩個鍵映射到了同一個位置,就在這個位置存儲一個鏈表。

在這裏插入圖片描述

我們看到,book和bunny映射到了同一個位置,因此在這個位置存儲一個鏈表。在查詢apple的價格時,速度依然很快,但在查詢bunny的價格時,速度要慢些:你必須在相應的鏈表中找到bunny.

我們可能會覺得這個鏈表很短,沒什麼大不了的。但是,如果我的商店進的大部分商品的商品名稱都是以b開頭的,那這個鏈表將會相當的長,我們用這個散列表的速度將會很慢,這是一個非常糟糕的狀況。


這裏總結了兩點經驗:

  • 散列函數很重要。前面的散列函數將所有的鍵都映射到一個位置,而最理想的情況是,散列函數將鍵均勻地映射到散列表的不同位置。
  • 如果散列表存儲的鏈表很長,散列表的速度將急劇下降。然而,如果使用的散列函數很好,這些鏈表就不會很長。

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


性能


在平均情況下,散列表執行各種操作(查找、插入、刪除)的時間都爲O(1)O(1). O(1)O(1)被稱爲常量時間,它並不意味着馬上,而是說不管散列表多大,所需的時間都相同。

下面我們來比較線性時間、對數時間和常量時間:

在這裏插入圖片描述

我們看到上面第三幅圖中,表示運行時間的曲線是水平的。這意味着平均情況下,無論散列表包含1個元素還是10億個元素,從其中獲取數據所需的時間都相同。

但在糟糕的情況下,散列表所有操作的運行時間都爲O(n)O(n),即線性時間,這真的是很慢了。因此,爲了避免衝突,需要有:

  • 較低的填裝因子
  • 良好的散列函數

填裝因子


散列表的填裝因子很容易計算,即:散列表包含的元素數/位置總數。由此可知,填裝因子度量的是散列表中有多少位置是空的。

散列表使用數組來存儲數據,因此我們需要計算數組中被佔用的位置數。例如下面的散列表的填裝因子爲2/5:

在這裏插入圖片描述

如果此時,我們的散列表只有50個位置,但是我要存儲100件商品的價格,那麼填裝因子將爲2.填裝因子大於1則意味着,商品數量超過了數組的位置數。一旦填裝因子開始增大,我們就需要在散列表中添加位置,這被稱爲調整長度。

例如,假設我的散列表有4個位置,我已經存儲了3個商品價格,那麼填裝因子爲3/4.此時,爲了調整長度,我會先創建一個更長的新數組(通常將原數組增長1倍),這個新數組有8個位置,接下來,我需要使用函數hash將所有的元素都插入到這個新的散列表中,那麼這個新散列表的填裝因子爲3/8,比原來低得多。填裝因子越小,發送衝突可能性就越小。一個不錯的經驗規則是:一旦填裝因子大於0.7,就調整散列表的長度。

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