散列表(字典)
文章目錄
- 問題
- 散列函數
- 應用案例
- 將散列表用于查找
- 防止重復
- 將散列表用作緩存
- 沖突
- 性能
- 裝填因子
- 良好的散列函數
- 小結
問題
你在一家雜貨店上班。有顧客來買東西時,你得在一個本子中查找價格。
如果本子的內容不是按字母順序排列的,你可能為查找蘋果(apple)的價格而瀏覽每一行,這需要很長的時間。使用的是簡單查找,需要瀏覽每一行。時間復雜度為O(n)O(n)O(n)。
如果本子的內容是按字母順序排列的,可使用二分查找來找出蘋果的價格,這需要的時間更短,為O(logn)O(log n)O(logn)。
前面介紹了兩種用于查找的數據結構:數組和鏈表,為了針對上面的問題,有個更快的查找方式,引入散列表。查找時使用散列函數。
散列函數
散列函數是這樣的函數,即無論你給它什么數據,它都還你一個數字。用專業術語來表達的話,我們會說,散列函數“將輸入映射到數字”。
需要滿足的要求:
- 單一映射,對于同樣的輸入,總能得到相同的輸出。
- 應將不同的輸入映射到不同的數字。(可逆函數)
對于上面的問題,使用散列表來解決,首先創建一個空數組。在這個數組中存儲商品價格。如蘋果價格,為此,將apple作為輸入交給散列函數,散列函數輸出為數組索引,根據索引我們就能找到對應位置蘋果的價格。
散列函數準確地指出了價格的存儲位置,你根本不用查找!之所以能做到,原因有:
- 散列函數總是將同樣的輸入映射到相同的索引。每次你輸入avocado,得到的都是同一個數字。因此,你可首先使用它來確定將鱷梨的價格存儲在什么地方,并在以后使用它來確定鱷梨的價格存儲在什么地方。
- 散列函數將不同的輸入映射到不同的索引。avocado映射到索引4,milk映射到索引0。每種商品都映射到數組的不同位置,讓你能夠將其價格存儲到這里。
- 散列函數知道數組有多大,只返回有效的索引。如果數組包含5個元素,散列函數就不會返回無效索引100
不同于數組和鏈表都被直接映射到內存,散列表更復雜,它使用散列函數來確定元素的存儲位置。
散列表也被稱為散列映射、映射、字典和關聯數組。
應用案例
將散列表用于查找
創建一個話簿,每個姓名都有對應的電話號碼,需要提供如下功能:
- 添加聯系人及其電話號碼。
- 通過輸入聯系人來獲悉其電話號碼。
這非常適合使用散列表來實現!在下述情況下,使用散列表是很不錯的選擇。
- 創建映射。
- 查找。
創建電話簿非常容易。首先,新建一個散列表。
phone_book = dict()Python還提供了一種創建散列表的快捷方式——使用一對大括號。
phonr_book = { } #與phone_book = dict()等效下面在電話簿中添加一些聯系人的電話號碼:
phone_book["jenny"] = 8675309 phone_book["emergency"] = 120現在,假設你要查找Jenny的電話號碼,為此只需向散列表傳入相應的鍵.
>>> print phone_book["jenny"] 8675309散列表被用于大海撈針式的查找。例如,你在訪問像http://adit.io這樣的網站時,計算機必須將adit.io轉換為IP地址。這個過程被稱為DNS解析(DNS resolution),這不是將網址映射到IP地址嗎?散列表是提供這種功能的方式之一.
防止重復
假設你負責管理一個投票站。顯然,每人只能投一票,但如何避免重復投票呢?有人來投票時,你詢問他的全名,并將其與已投票者名單進行比對。
為此,首先創建一個散列表,用于記錄已投票的人。
voted = { }有人來投票時,檢查他是否在散列表中。
value = voted.get("tom")如果“tom”在散列表中,函數get將返回它;否則返回None。你可使用這個函數檢查來投票的人是否投過票!
代碼如下:
voted = {}def check_voter(name):if voted.get(name):print "kick them out!"else:voted[name] = Trueprint "let them vote!"使用散列表來檢查是否重復,速度非常快。
將散列表用作緩存
假設你訪問網站facebook.com:
facebook的服務器處理需要時間,為了少做工作,提高facebook網站的訪問速度,服務器使用緩存,對于經常使用的主頁等,不讓服務器生成它,而是將主頁存儲起來,在需要時直接發送給用戶。緩存具有如下優點:
- 用戶能夠更快地看到網頁。
- Facebook需要做的工作更少。
緩存是一種常用的加速方式,所有大型網站都使用緩存,而緩存的數據則存儲在散列表中!
Facebook不僅緩存主頁,還緩存About頁面、Contact頁面、Terms and Conditions頁面等眾多其他的頁面。因此,它需要將頁面URL映射到頁面數據。
當你訪問Facebook的頁面時,它首先檢查散列表中是否存儲了該頁面。
代碼如下:
僅當URL不在緩存中時,你才讓服務器做些處理,并將處理生成的數據存儲到緩存中,再返回它。這樣,當下次有人請求該URL時,你就可以直接發送緩存中的數據,而不用再讓服務器進行處理了。
沖突
前面講到,散列函數最好是可逆函數,其總是將不同的鍵映射到數組的不同位置。實際上,幾乎不可能編寫出這樣的散列函數。
當給兩個鍵分配的位置相同時,就會產生沖突(collision),不同的輸入得到相同的映射值。沖突很糟糕,必須要避免。處理沖突的方式很多,最簡單的辦法如下:如果兩個鍵映射到了同一個位置,就在這個位置存儲一個鏈表:
- 散列函數很重要。最理想的情況是,散列函數將鍵均勻地映射到散列表的不同位置。
- 如果散列表存儲的鏈表很長,散列表的速度將急劇下降。然而,如果使用的散列函數很好,這些鏈表就不會很長!
如何選擇好的散列函數呢?
性能
在平均情況下,散列表的查找(獲取給定索引處的值)速度與數組一樣快,而插入和刪除速度與鏈表一樣快,因此它兼具兩者的優點!但在最糟情況下,散列表的各種操作的速度都很慢。因此,在使用散列表時,避開最糟情況至關重要。為此,需要避免沖突。而要避免沖突,需要有:
- 較低的填裝因子;
- 良好的散列函數。
裝填因子
裝填因子=散列表中包含的元素數/位置總數裝填因子=散列表中包含的元素數/位置總數裝填因子=散列表中包含的元素數/位置總數
一旦填裝因子開始增大,你就需要在散列表中添加位置,這被稱為調整長度(resizing):
- 首先創建一個更長的新數組:通常將數組增長一倍。
- 使用函數hash將所有的元素都插入到這個新的散列表中。
填裝因子越低,發生沖突的可能性越小,散列表的性能越高。一個不錯的經驗規則是:一旦填裝因子大于0.7,就調整散列表的長度。調整長度的開銷很大,因此你不會希望頻繁地這樣做。
良好的散列函數
良好的散列函數讓數組中的值呈均勻分布,糟糕的散列函數讓值扎堆,導致大量的沖突。
小結
散列表是一種功能強大的數據結構,其操作速度快,還能讓你以不同的方式建立數據模型。
- 你可以結合散列函數和數組來創建散列表。
- 沖突很糟糕,你應使用可以最大限度減少沖突的散列函數。
- 散列表的查找、插入和刪除速度都非常快。
- 散列表適合用于模擬映射關系。
- 一旦填裝因子超過0.7,就該調整散列表的長度。
- 散列表可用于緩存數據(例如,在Web服務器上)。
- 散列表非常適合用于防止重復。
總結
- 上一篇: 图像分割I
- 下一篇: PyTorch框架学习十八——Layer