小白的算法初识课堂(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')) NoneIn [71]: print(shop.get('pen')) 20.5現在我們構造一個函數,來判斷是否登記過某商品:
def check_pro(name, price):if shop.get(name) is None:shop[name] = priceprint('Register now')else:print('Item already exists')控制臺調用:
In [78]: check_pro('book', 30) Register nowIn [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)O(1)O(1)被稱為常量時間,它并不意味著馬上,而是說不管散列表多大,所需的時間都相同。
下面我們來比較線性時間、對數時間和常量時間:
我們看到上面第三幅圖中,表示運行時間的曲線是水平的。這意味著平均情況下,無論散列表包含1個元素還是10億個元素,從其中獲取數據所需的時間都相同。
但在糟糕的情況下,散列表所有操作的運行時間都為O(n)O(n)O(n),即線性時間,這真的是很慢了。因此,為了避免沖突,需要有:
- 較低的填裝因子
- 良好的散列函數
填裝因子
散列表的填裝因子很容易計算,即:散列表包含的元素數/位置總數。由此可知,填裝因子度量的是散列表中有多少位置是空的。
散列表使用數組來存儲數據,因此我們需要計算數組中被占用的位置數。例如下面的散列表的填裝因子為2/5:
如果此時,我們的散列表只有50個位置,但是我要存儲100件商品的價格,那么填裝因子將為2.填裝因子大于1則意味著,商品數量超過了數組的位置數。一旦填裝因子開始增大,我們就需要在散列表中添加位置,這被稱為調整長度。
例如,假設我的散列表有4個位置,我已經存儲了3個商品價格,那么填裝因子為3/4.此時,為了調整長度,我會先創建一個更長的新數組(通常將原數組增長1倍),這個新數組有8個位置,接下來,我需要使用函數hash將所有的元素都插入到這個新的散列表中,那么這個新散列表的填裝因子為3/8,比原來低得多。填裝因子越小,發送沖突可能性就越小。一個不錯的經驗規則是:一旦填裝因子大于0.7,就調整散列表的長度。
總結
以上是生活随笔為你收集整理的小白的算法初识课堂(part5)--散列表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 总数达 145 颗,天文学家发现土星还存
- 下一篇: 小白的算法初识课堂(part6)--广度