1、由問題引出哈希表
爲了介紹哈希表,咱們先來看leetcode上一個簡單的問題。算法
解決思路:
先建立一個映射,而後掃描一遍傳入的字符串,將每一個字符對應出現的頻率存入到映射中,以後在掃描一遍傳入的字符串,返回第一個頻率爲1的字符,若是不存在則返回-1.緩存
另外,題目中告訴咱們假定該字符串只包含小寫字母,那麼也就是說咱們這個映射所映射的字符只多是a~z這26種。數據結構
在這種狀況下,咱們可使用一種簡單思惟就是咱們不使用像二叉樹這樣的數據結構來實現這個映射,咱們直接製做一個包含有26個元素的數組,數組的中存儲的數據就是對應索引在字母表中對應的字母出現的頻率函數
例如:索引爲0的位置存儲的值表明的是字母a出現的頻率,索引爲1表明字母b的頻率,索引2表明字母c的頻率,以此類推。學習
這樣作不只僅是理解起來簡單,更重要的是當咱們進行讀寫操做時,時間複雜度爲O(1) url
解決代碼:
public int firstUniqChar(String s) { int[] freq = new int[26]; for(int i = 0 ; i < s.length() ; i ++) freq[s.charAt(i) - 'a'] ++; for(int i = 0 ; i < s.length() ; i ++) if(freq[s.charAt(i) - 'a'] == 1) return i; return -1; }
上述問題理解起來仍是比較容易的,介紹這個問題的緣由是由於這個問題背後隱藏着哈希表這種數據結構。那麼到底什麼是哈希表呢?spa
2、什麼是哈希表?
在解決上面說的問題時,咱們開闢了一個有26個空間的int數組frequency,其實就是一個哈希表。.net
具體來講,咱們想要作的其實就是讓每個字符和一個數字之間進行一個映射關係,那麼這個數字呢表示的是字符在整個字符串中出現的頻率。設計
這裏有一個關鍵點,咱們能使用這樣一個數組就能解決問題,即咱們能直接使用freq[0]獲取到字符a的頻率,是由於咱們將每個字符都和一個索引進行了對應,使得字符和索引之間有個對應關係,以後就能夠直接使用這個索引在數組中尋找到相應的字符的映射內容。這裏的關係就是這個索引的值等於這個字符的ASCII碼減去字符a的ASCII碼。
哈希表的本質就是把咱們真正關心的那個內容在咱們這個問題中是字符對應的這個內容(鍵)轉化成一個索引,而後直接用一個數組來存儲相應的頻率(值),那麼因爲數組自己是支持隨機訪問的,因此咱們可使用O(1)的時間複雜度來完成各項的操做,這就是哈希表的一個巨大優點。
3、使用哈希表須要處理的兩件任務
在對哈希表有必定的瞭解後,咱們能夠看出來,對於哈希表有兩個核心問題。
一、設計合理的哈希函數
對於咱們所關注的這個內容,拿上述問題來講,咱們關注的是字符它所對應的頻率,那麼對於每個字符,咱們必須首先把它轉化成一個索引。在通常狀況裏,一個哈希表中是能夠存儲各類相應的數據類型的,對於每種數據類型,咱們都須要一個方法把它轉化成一個索引,相應的咱們關心的這個類型轉換成索引的這個函數,就稱之爲是哈希函數。
若是更嚴謹的來講,在上述問題中,咱們的哈希函數其實就能夠寫成這個樣子
F(ch) = ch – ‘a’
F(ch)就是對於給定的一個字符,咱們經過這個函數f就把它轉化成一個索引,這個轉化的方法就是很是簡單的,用ch對應的ascii值減去a對應的ascii的值就行了。
那麼咱們有了這個哈希函數,可以把咱們真正關心的這個數據類型轉換成索引,以後咱們只須要在哈希表上進行操做就行了,在這個問題中這個操做很是的簡單,直接在這個數組對應的索引上去查找或者進行加操做就行了,很是的容易。
不過並非全部的時候哈希表都是這麼容易的,咱們在這個問題中把鍵轉化爲索引的轉化方式正好是一一對應的這樣的一種轉化,因此咱們能夠很是容易地直接用一個數組來存儲全部的內容,但在大多數狀況下咱們處理的數據會更加複雜。
好比說咱們對居民的信息感興趣,那麼在這裏咱們識別每個居民的這個鍵多是他對應的身份證號,可是在我國身份證號是很是複雜的,一共有18位數,很顯然這個18位數咱們總體把它看做一個整數來講就太大了,此時咱們就不能直接用這個數字來當作數組的索引,由於這樣作,實際上也浪費了不少的空間,固然有更多的數據類型,它自己就跟索引可能8竿子打不着,區別巨大。
最典型的狀況就是字符串,好比說咱們關注學生的信息,可是咱們是用學生的名字來做爲鍵查看每一個學生的具體信息,那麼這會這個鍵就是一個字符串,咱們如何設計一個哈希函數,將字符串轉化爲索引?這就是哈希表中咱們須要考慮的問題。
除此以外還有各類數據結構牽扯到這種問題,包括好比說咱們關係的這個鍵多是一個浮點型或者是一個複合的類型,好比說是一個日期,每個日期包含可能有年月日相關的信息,甚至更多一些例如時分秒這樣的信息,那麼這樣一種複合的數據類型,咱們在使用哈希表這種數據結構的時候都須要將它們首先轉化爲一個索引纔可使用,那麼相應的,咱們就須要設計一個合理的哈希函數,那麼這就是咱們在學習哈希表的時候要處理的第1個任務。
二、避免哈希衝突
不少時候,咱們就不得不處理一個在哈希表中很是關鍵的問題就是兩個不一樣的鍵,經過咱們的哈希函數以後,它們對應了一樣一個索引,那麼這種狀況呢,一般稱之爲叫作產生了哈希衝突。
那麼咱們在哈希表上的操做其實最複雜的部分也就是在解決這種哈希衝突上。
若是咱們設計的哈希函數很是好,是一一對應的,那就像咱們在以前所完成的這個問題同樣,咱們對哈希表的操做也將很是簡單,不過對於更通常的狀況,咱們在哈希表上的操做主要就要考慮如何解決哈希衝突,那麼這就是咱們學習哈希表要主要解決的第2個關鍵問題。
另外還有一種極端狀況,就是假設咱們只有一個空間,全部的元素都存儲在其中,這樣其實就變成了一個鏈表,時間複雜度變爲O(n)。不過這種狀況基本不可能出現。
4、總結
對於哈希表這種數據結構,它充分的體現了咱們在學習算法設計領域的時候,一個很是重要的很是經典的思想,也就是用空間換時間。仔細的思考一下本身處理的不少算法問題,包括學習的不少經典算法,其實本質上都是用空間換時間,不少時候咱們多存儲一些東西、預處理一些東西、緩存一些東西,那麼在實際執行咱們的算法任務的時候,咱們完成這個任務獲得這個結果就會快不少,哈希表很是完美的體現了這一點。
回到我麼上面說的身份證號的例子,那麼假如咱們能夠開闢無限大的數組或者更準確的說的話,咱們能夠開闢這個18個9這麼多的空間,這樣的一個數組的話,那麼咱們徹底能夠用在這一小節所使用的這種最爲簡單的一個數組的方式,來解決咱們所須要的這種數據存儲的功能,或者是這種映射的功能,咱們在這樣巨大的一個數組中可使用O(1)的時間完成對任意一個身份證號,相應的這個居民的信息增刪改查相應的內容是很是容易的,不過實際上很難能開闢一個這麼大的空間,有興趣能夠實際的計算一下,開闢這麼大的一個空間,就算每個空間只存儲一個32位的整型的話,那麼總體這是多麼大的一個空間。
另外的一個極端就是咱們對應的這個數組只有一個位置,只有1的空間,此時其實就至關於咱們要存儲的全部的內容都會產生哈希衝突,咱們把全部的內容都堆着。在這惟一的這個數組空間中,假設咱們以鏈表的方式來組織咱們總體的這個數據的話,那相應的各項操做所完成的時間複雜度就變成了o(n)這個級別,這就是設計哈希表的兩個極端狀況。
哈希表總體就是在這兩者之間產生一個平衡。