1. 程式人生 > >演算法快學筆記(五):散列表

演算法快學筆記(五):散列表

1. 介紹

當需要根據給定的值需要快速得到想要值的時候,散列表是一個非常有用的資料結構,假設你在一家雜貨店上班。有顧客來買東西時,你得在一個本子中查
找價格,如果本子的內容不是按字母順序排列的,你可以使用簡單查詢法,從頭到尾以一個一個的找,時間複雜度為O(n),如果本子的內容是按字母順序排列的,可使用二分查詢來找出蘋果的價格,這需要的時間會短一些,為O(log n)。

二分查詢的速度非常快。但作為收銀員,在本子中查找價格是件很痛苦的事情,哪怕本子的內容是有序的。要是能夠記住所有商品價格,就馬上知道答案而且不用查找了。

雜湊函式是這樣的函式,簡單的說,即無論你給它什麼資料,它都會給你一個代表位置的數字。

但雜湊函式必須滿足一些要求。

  • 它必須是一致的。例如,假設你輸入apple時得到的是4,那麼每次輸入apple時,得到的都必須為4。如果不是這樣,散列表將毫無用處。
  • 它應將不同的輸入對映到不同的數字。 例如,如果一個雜湊函式不管輸入是什麼都返回1,
    它就不是好的雜湊函式。最理想的情況是,將不同的輸入對映到不同的數字。

2. 原理介紹

雜湊函式返回的數字到底有什麼用了?下面我們來看下這個數字是如何幫助我們快速查詢的。

首先建立一個空陣列:

在這裡插入圖片描述

你將在這個陣列中儲存商品的價格。下面來將商品的價格加入到這個陣列中,放到陣列中的位置由商品名稱經過雜湊函式處理的結果決定,例如

  • Apple 的雜湊值為3
  • Milk 的雜湊值為0
  • 計算其他商品的雜湊值

放入了Apple以及Milk後陣列,變成了下面的樣子:

在這裡插入圖片描述

現在假設需要知道Apple的價格。你無需在陣列中查詢,只需將Apple作為輸入
交給雜湊函式, 他會告訴你蘋果的價格儲存在索引3處,於是你就得到了蘋果的價格。

雜湊函式準確地指出了價格的儲存位置,你根本不用查詢!之所以能夠這樣,具體原因如下。

  • 雜湊函式總是將同樣的輸入對映到相同的索引。每次你輸入Apple,得到的都是同一個
    數字。因此,你可首先使用它來確定將鱷梨的價格儲存在什麼地方,並在以後使用它來
    確定鱷梨的價格儲存在什麼地方。
  • 雜湊函式將不同的輸入儘量對映到不同的索引。
  • 雜湊函式知道陣列有多大,只返回有效的索引。如果陣列包含5個元素,雜湊函式就不會返回無效索引100

3. 重要特性

3.1 衝突

還是以查詢商品價格為例,假設你有一個長度為26的陣列,而你使用的雜湊函式也很簡單,就是按照字母表順序來分配商品在陣列中的位置。

對於該種情況,Apple(蘋果)與Avocados(鱷梨)分配的位置相同,該種情況被稱為衝突。處理衝突的方式很多,最簡單的辦法如下:如果兩個鍵對映到了同一個位置,就在這個位置儲存一個連結串列,結構如下:
在這裡插入圖片描述

在這個例子中, apple和avocado對映到了同一個位置,因此在這個位置儲存一個連結串列。在需要查詢香蕉的價格時,速度依然很快。但在需要查詢蘋果的價格時,速度要慢些:你必須在相應
的連結串列中找到apple。如果這個連結串列很短也沒什麼,但假設你工作的雜貨店只銷售名稱以字母A打頭的商品的話,會出現下面的情況:

  • 第一個位置包含了所有的商品,此時的查詢變成了連結串列查詢
  • 除了第一個位置外,其他的位置都是空的,資源分配嚴重不均!

通過前面的描述,可以說明雜湊函式的好壞很重要,最理想的情況是,雜湊函式將鍵均勻地對映到散列表的不同位置。

3.2 效能

在平均情況下,散列表的查詢(獲取給定索引處的值)速度與陣列一樣快,而插入和刪除速度與連結串列一樣快,因此它兼具兩者的優點!但在最糟情況下,散列表的各種操作的速度都很慢。
因此,在使用散列表時,避開最糟情況至關重要。為此,需要避免衝突。而要避免衝突,需要有:

  • 較低的填裝因子;
  • 較好的雜湊函式

3.2.1 填裝因子

散列表的填裝因子=散列表包含的元素數/位置總數,如果一個元素需要放入包含3個位置的散列表中,則填裝因子就為1/3.

填裝因子大於1意味著商品數量超過了陣列的位置數。一旦填裝因子開始增大,你就需要在散列表中新增位置,這被稱為調整長度(resizing) 。
例如,假設有一個像下面這樣相當滿的散列表(陣列長度為4,已經包含3個元素),你就需要調整它的長度。為此,

  • 你首先建立一個更長的新陣列:通常將陣列增長一倍,長度現在變成了8
  • 你需要使用函式hash將所有的元素都插入到這個新的散列表中

這個新散列表的填裝因子為3/8,比原來低多了!填裝因子越低,發生衝突的可能性越小,散列表的效能越高,一個不錯的經驗規則是:一旦填裝因子大於0.7,就調整散列表的長度

由於重新調整大小涉及到重新分配記憶體以及重新插入操作,因此調整長度是開銷很大的。

3.2.2 良好的雜湊結構

良好的雜湊函式讓陣列中的值呈均勻分佈,糕的雜湊函式讓值扎堆,導致大量的衝突。

4. 總結

  • 散列表適合用於模擬對映關係
  • 平均情況下,散列表所有操作的執行時間都為O(1)
  • 最糟情況下,散列表所有操作的執行時間都為O(n)
  • 你可能不需要自己去實現散列表,每種語言基本都提供了散列表實現
  • 一個不錯的經驗規則是:一旦填裝因子大於0.7,就調整散列表的長度