樹的結構說得差很少了,如今咱們來講說一種數據結構叫作哈希表(hash table),哈希表有是幹什麼用的呢?咱們知道樹的操做的時間複雜度一般爲O(logN),那有沒有更快的數據結構?固然有,那就是哈希表;java
1.哈希表簡介算法
哈希表(hash table)是一種數據結構,提供很快速的插入和查找操做(有的時候甚至刪除操做也是),時間複雜度爲O(1),對比時間複雜度就能夠知道哈希表比樹的效率快得多,而且哈希表的實現也相對容易,然而沒有任何一種數據結構是完美的,哈希表也是;哈希表最大的缺陷就是基於數組,由於數組初始化的時候大小是肯定的,數組建立後擴展起來比較困難;數組
當哈希表裝滿了以後,就要把數據轉移到一個更大的哈希表中,這會很費時間,並且哈希表不支持有順序的遍歷,由於從哈希表中遍歷數據是隨機的;因此咱們使用哈希表的前提是:不須要有序的遍歷數據,能夠大概知道數據量的多少;知足這兩點就能夠用哈希表;數據結構
那有人就要問了,說得這麼厲害,哈希表究竟是什麼樣子的啊?下面就隨便說兩個吧。。。函數
很經典的例子就是英語字典,咱們查字典的時候能夠根據這個單詞就能夠找到第xxx頁,在這裏該單詞和頁數就對應起來了,這能夠說是一個哈希表;spa
再舉個現實中的例子,在上學的時候每一個人在學校裏都會有一個學號,你這我的在學校中就對應這個學號,假如校長手上有一個記錄全校學生的表,而後根據學號找一個學生時,就能很快鎖定這個學生的姓名,性別,班級等信息;有沒有想過假如沒有學號的話,校長想找一個學生就只能根據姓名去找,但是同名同姓的人這麼多,想找到目標學生不是一件容易的事。。。。。線程
ok,在這裏哈希表能夠看做是校長手上的那個表(其實就是一個數組),咱們根據咱們要存的信息生成一個表中的位置的號碼(在這裏這個號碼就是數組的下標),根據這個號碼咱們就知道該數據存在數組的哪一個位置,而後將數據保存進去就能夠了;假若有個大小爲20的數組,我要存「aaa」,咱們能夠想個很厲害的辦法將這個字符串變成一個比較小的數字,好比是10,那麼就把這個字符串存到數組的第10個位置,這樣作的好處就是下次若是要從哈希表中查詢(或刪除)「aaa」這個字符串時,只須要將「aaa」字符串算出那個號碼10,而後直接去數組中第10個位置找一個看有沒有這個字符串,是否是很簡單啊!blog
因此如今咱們須要解決的就是想個很厲害的辦法能夠將字符串變成一個比較小的數字(這個過程叫作哈希化),還要保證這個數字不能超過數組的最大邊界!字符串
2 哈希化hash
哈希化就是想辦法將咱們要保存的數據對應一個數組下標,在數組的該位置下保存數據;咱們能夠把這個過程專業一點的說一下:把要保存的數據,經過哈希函數轉化爲對應的數組下標;如今咱們的目標就是怎麼編寫一個哈希函數可使得字符串變成數組下標;
這裏咱們能夠假設一個字符串t數組的大小爲30,String[] str = new String[30]; 要存「cats」這個單詞,最容易想到的辦法就是用ASCII碼,可是因爲ASCII碼太多了很差記,因而咱們能夠本身設置一套規則,我就假設a到z分別對應1到26,外加空格對應0,如今一套最簡陋的規則就出來了,我那麼「cats」這個單詞:c = 3,a = 1,t = 20,s = 19,如今「cats」有兩種辦法變成數組的下標;
額外補充一下:假如咱們要保存的字符串有50個,那麼咱們new的數組大小必定要是它的兩倍大,即 new String[100];,後面會說到這個緣由
2.1哈希函數實現一
怎麼實現比較好呢?別想那麼多,直接相加就好,3+1+20+19 = 43,這個時候就有個小問題,咱們的數組的大小爲30,也就是說數組下標最大值是29,而這裏咱們的數字爲43,怎麼將43變成29之內的數(包括29)呢?由於任何數除以30的餘數只都在0-29之間,因而咱們用43除以30拿到餘數13,那麼咱們就把」cats「放到數組下標爲13的位置,str[13] = "cats";
這種哈希函數的實現很容易,可是每每越容易的東西缺點就越大,最大的缺陷就是有不少單詞變成數字是相同的,好比was,tin,give等100多個單詞變成數字後都是43,而後咱們恰巧添加單詞的時候就是這些單詞,如今問題來了,多個單詞最後算出來的數組下標很大機率上是同樣的,也就是數組一個位置要放多個數據,怎麼解決這個問題呢?咱們能夠換一種哈希函數的實現來下降這個機率
2.2 哈希函數實現二
由2.1能夠知道太多的單詞變成數字的結果是同樣的,那麼咱們就要想辦法爲每個單詞都對應一個獨一無二的整數,而後用這個整數除以數組的大小取餘數,就能夠知道該單詞在數組中的存放位置;
因而啊,咱們能夠利用冪的連乘來獲得這個獨一無二的整數,好比「cats」用這種計算方法:3*273+1*272+20*271+19*270,有點相似二進制變成十進制,經過這個算法,能夠獲得一個獨一無二的整數,其餘的任何單詞經過這種方法算出來的結果幾乎是不可能相等的,有興趣的能夠試試;而後將這個計算結果除以30取餘數,就能夠獲得一個數組的位置,而後將該字符串丟到裏面便可;
不知道你們有沒有發現這種方法的一個問題,由於數組的大小是必定的,並且咱們是經過取餘數來獲得數組的位置,那麼問題來了,即便是兩個不相同的整數分別除以30,最後的餘數是相等的;
就好比有兩個字符串經過冪的連乘最後獲得32和62(固然咱們這裏確定不會獲得這兩個整數,爲了好理解隨便拿兩個數),雖然這兩個數是獨一無二的,可是除以30餘數都爲2,那麼兩個數據要保存到哈希表中確定會有衝突,下後面咱們來解決一下這個衝突;
有個簡單的哈希函數實現看一下(雖然還能夠進行修改一下,可是這個已經差很少了);
3.衝突
衝突的緣由就是兩個獨一無二的整數除以數組的大小,取餘數是相等的,而數組中一個位置只能存一個數據,這就致使了衝突,解決衝突的辦法有兩種;
3.1 解決方法一(開放地址法)
還記得前面說過數組的大小要爲實際數量的兩倍嗎?就是爲了這個時候用的,假如一個單詞已經放在了數組的第15個位置那裏,另一個單詞原本也要放在第15的位置,因爲這個位置已經被別人佔了。那就放在數組的另一個位置上,反正還有不少數組比較大,這種方式叫作------開放地址法
3.2 解決方法二(鏈地址法 )
既然有兩個數據都要放在數組的一個位置上,那就想辦法把第二個數據連在第一個數據後面,經過第一個數據能夠找到第二個數據,而數組中只保存第一個數據的地址;其實就是一句話,數組中每一個位置放一個鏈表;
這種方法的好處很明顯,完美解決上述衝突,不須要用什麼花裏胡哨的操做;缺陷就是當鏈表太長了,咱們要查詢這個鏈表的最後面的數據,只能慢慢遍歷這個鏈表,而咱們知道,鏈表的優點是插入和刪除,而對於查詢這種操做是比較坑爹的,而咱們前面用了紅黑樹這樣的結構來完美解決鏈表的缺點;最後,咱們就差很少想到了一個比較實用的方法:數組的每一個位置都存放一個鏈表,當鏈表的節點不多的時候,那就用鏈表吧!可是當鏈表慢慢的變長,當節點數目到達一個界限的時候,咱們就把這個鏈表變成一個紅黑樹,比較完美的方案,這也叫作------鏈地址法
順便一提,jdk7的HashMap就是數組中放鏈表,即便鏈表很長也不會變紅黑樹;jdk8中的HashMap才增長了變紅黑樹這個操做
4.開放地址法
所謂的開放地址法就是:根據咱們要保存的數據計算出來的數組下標的那個位置已經存放了數據,這個時候咱們就要再找一個空位置,而後將要保存的數據丟進去便可,那麼怎麼找比較好呢?這裏提供三種方式,線性探測,二次探測和再哈希法,下面就看看這三種方式究竟是怎麼工做的;
4.1 線程探測
看名字線性就知道是從前日後尋找空的位置,舉個很簡單的例子,當一個字符串通過運算對應於數組下標爲52,然而此時52這個位置上已經有了數據,那麼就嘗試放到53的位置,假如53的位置也已經放了數據,那就放到54位置,就這樣一直日後慢慢找,直到找到一個空的位置就把數據放進去;而此時找的次數越多,假如已經找到56的位置,那麼從53到56這麼多位置叫作填充序列,當填充序列很長的時候,咱們就稱爲原始彙集,下圖所示:
這裏填充序列的中有5個填充單元,咱們也能夠說步數爲1,每次探測都是前進一步;咱們能夠知道當探測的次數越多的時候,說明彙集越嚴重,下一次再想添加到這個位置的數據的效率就越低;
還有就是當哈希表填充得越滿,效率也就越低,因此當哈希錶快滿了以後就要擴展,而java中數組是不能直接進行擴展的,須要再新建一個數組,而後想辦法將這個哈希表中的數據複製到新的數組中,注意,這裏不能直接複製,由於新的數組的容量和原來的數組不同,那麼原來哈希表中全部的數據必需要從新哈希化,而後放入到新的數組中,很是耗時....
4.2 二次探測
根據前面咱們的線性探測能夠知道,假如通過哈希函數計算出來的原始數組下標爲x,那麼線性探測的位置是x+1,x+2,x+3,x+4.....,;那麼 進行二次探測找的位置就是x+12,x+22,x+32,x+42.....其實就是按照步數的平方進行探測看裏面有沒有數據,沒有的話才放進去新的數據,二次探測能夠防止彙集太長所致使的效率降低問題;
對於二次探測來講,若是當前計算出來的位置爲x,首先會探測x後面一個位置,若是這個位置有數據,那就多日後4個位置看有沒有數據,假如仍是有數據,那麼二次探測可能會以爲你這個彙集特別長,因而此次跳得更遠的位置,當前位置後面的16的位置等等,直到最後跳過整個數組, 這樣能夠避免一個一個的位置慢慢探測的底下效率,二次探測下圖所示:
二次探測也有點問題,會致使二次彙集,那什麼又是二次彙集呢?其實跟原始彙集差很少吧!好比184,302,420,544這幾個整數都要放到哈希表中,並且這幾個數通過哈希算法算出來的數組下標都爲7,302須要以1步長進行探測,而420要先以1爲步長,而後以4步長進行探測,而544要先以1爲步長,而後以4爲步長,最後以16步長進行探測,假如後面還有數據對應的數組下標爲7,那麼仍是要重複這個步驟,並且是愈來愈長....這也是一種彙集,我的感受從某種意義來講和原始彙集性質差很少吧!
二次探測不經常使用,由於有更好的辦法解決,就是再哈希法;
4.3 再哈希法
用再哈希法能夠消除原始彙集和二次彙集,那麼什麼是再哈希法呢?咱們能夠知道產生原始彙集和二次彙集的緣由其實差很少,都是因爲多個數據添加到哈希表中的同一個位置,而後根據步長一個一個位置的探測,直到找到一個空的位置,若是須要找的位置特別多,那麼這就是彙集,添加的效率的就會大幅度下降;
那麼咱們就要想一種方法即便多個數據要放在哈希表的同一個位置,可是不須要從頭開始一個一個位置的探測,若是每一個數據均可以產生一個獨一無二的步長那不就行了麼!而後直接根據這個步長探測該位置將數據丟進去就ok了;
因而咱們準備了兩個哈希函數,一個哈希函數就是咱們上面說到的能夠產生對應的數組下標,另一個哈希函數能夠產生步長,其實就是多個數據放在同一個位置產發生衝突,就用這個哈希函數再次哈希化產生一個步長,根據這個步長進行探測就能夠了,而不用每次都從第一個步長開始;好比下面就有一個產生步長的哈希函數,咱們能夠知道步長的範圍是1-constant,注意步長不能爲0,不然就原地踏步了。。。
上圖中,假如咱們往哈希表中添加的數據是數字,那就直接將數據和數組大小取餘獲得數組下標,這裏的key就是咱們的數據,constant只要是小於數組容量的一個質數,隨便什麼均可以
順便一提:再哈希法使用的前提必須保證數組的容量爲一個質數,由於這樣才能使得全部位置都被探測到;能夠試試假如數組容量爲15,步長爲5,一個數據通過計算獲得額數組下標爲0,那麼探測的位置應該爲:(0+5)%15 = 5,、(5+5)%15 = 10,(10+5)%15 = 0,只會探測0、五、10這三個位置;可是若是數組容量爲質數13,步長爲5,第一個數據下標仍是0,那麼探測位置爲:(0+5)%13 = 5,、(5+5)%13 = 10,(10+5)%13 = 二、(2+5)%13 = 7,(7+5)%13 = 12,(12+5)%13 = 4,(4+5)%13 = 9等等,能夠看到每次探測的位置都不同,能夠探測到數組中全部位置只要有空的就把數據當進去便可;
假如使用的是開放地址法,那麼探測序列就用這個再哈希法生成,其實很容易!
5.鏈地址法
能夠看到上面的開放地址法有點麻煩,須要找到探測序列真的是日了狗了,麻煩的我都不想看了,若是能夠不用這麼麻煩那該多好呀,ok,那就用鏈地址法吧!就相似下面這樣的結構,原始的數組中不直接保存數據,每一個位置只是保存第一個數據的引用,經過該位置第一個引用就能夠取到後面全部的數據!若是鏈表太長遍歷起來就比較費勁,能夠轉爲紅黑樹效率就高了不少;
這裏其實沒什麼好說的,由於數組和鏈表的使用很熟悉了,沒什麼特別難的東西,基本邏輯:只須要新建一個MyHashTable的類,這個類中有幾個屬性:一個數組,一個int類型的屬性標識數組真實容量的大小;最好有個節點類爲靜態內部類,這個靜態內部類中實現了對鏈表的增刪改查的操做;而後在MyHashTable類中寫一個哈希函數的方法,根據這個哈希函數得出來的數組下標,最後對數組的增刪改查了!
6.總結
哈希表其實還能夠用在外部存儲中,也就是硬盤中,有興趣的能夠看看,不過我感受到這裏就差很少了!其實哈希表的內容沒多少吧,最主要的就是哈希函數的選取,選擇一個好的哈希函數可使得咱們的哈希表的效率更高!而後就是數組中存數據的方式,能夠直接在數組中存數據,也能夠在數組中存節點的引用,其實吧,知不知道二維數組?在咱們這個數組中每一個位置存的是另一個數組的引用,這樣其實也行,因爲擴展起來很困難,使用鏈表比使用二維數組好。。。