HASH 的感性認識算法
先看數組存儲數據是怎麼樣的。數組
如今有一個數組,它裏面每一個單元存儲的是數據的地址數據結構
這叫指針數組吧,假設它有100個單元函數
咱們稱他爲p[100]學習
如今我想把一百個數據(地址)放到裏面優化
咱們想把某個數據放到p的第幾個單元徹底是由設計
咱們決定的,能夠說想怎麼放就怎麼放3d
是一種亂放,既然是亂放,那麼查找起來就比較耗時。指針
哈希表是怎麼存儲數據的呢?code
哈希表一樣是一個指針數組。
一樣須要存儲100個數據,須要的就不是100個單元了,由於哈希表要把某個數據存放在某個單元不是隨機的一個過程,而是算出來的,這個算法叫哈希函數
好比要存儲一個數據對
張三 1882356
李四 23456789
王五 58856456
張三通過哈希函數算出來的值是138,那麼哈希表最少須要138個單元,由於張三對應的數據1882356要存儲在指針數組的p[138]的位置上。
李四通過哈希函數算出來的值是500,那麼2356789這個數據就要存放在p[500]這個位置上。
因此你明白了,"數據的地址放在指針數組的哪一個單元格是算出來的,是有跡可尋的,並非想放在哪裏就放在哪裏"
那查找的時候就好查找了,你要查找張三對應的數據,
直接把張三用哈希函數算一下,獲得138,哦 張三的數據就在p[138]這個位置上,因此一下就找到了。
哈希表是一個key- value的數據對,p是一個指針數組,用來存放value的地址,那麼key和value的關係是下面這樣的。
p[f(key)]=&value
能夠看出哈希表有時候會浪費很大空間的,好比上面張三 李四 王五那個例子 若是按照哈希表存儲要定義一個500個大小的指針數組。怎麼解決這個問題呢?我再看看書。
2018/09/03 : 優化hash函數能夠解決浪費很大空間解決衝突這個問題。
HASH的理性認識
8/03 爲何會有哈希表,人們存儲數據時,想找到一種查找,插入和刪除、更新複雜度都不是很大的方法,因而哈希表應運而生。
- 數組存儲空間是連續的,佔用內存嚴重,佔用空間很大,但數組的二分查找時間複雜度小。
- 尋址容易,插入和刪除困難。
- 鏈表存儲區間離散,佔用內存小,空間複雜度小,可是時間複雜度很大。
- 尋址困難,插入和刪除容易。
- 哈希表
- 尋址容易 插入和刪除也容易的數據結構
hash table的實現,最經常使用的是拉鍊法,能夠理解爲「鏈表的數組
從上圖咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。通常狀況是經過hash(key)%len得到,也就是元素的key的哈希值對數組長度取模獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存儲在數組下標爲12的位置。
HashMap的存取
HashMap的功能是經過「鍵(key)」可以快速的找到「值」。下面咱們分析下HashMap存數據的基本流程:
一、 當調用put(key,value)時,首先獲取key的hashcode,int hash = key.hashCode();
二、 再把hash經過一下運算獲得一個int h.
hash ^= (hash >>> 20) ^ (hash >>> 12); // >>>是無符號的右移運算, 高位直接補零,低位移除。>>表示帶符號的右移運算, 高位若是符號位爲正補零,符號位負補一,低位直接移除
hash = hash ^ (hash >>> 20) ^ (hash >>> 12); ^表明異或運算
int h = hash ^ (hash >>> 7) ^ (hash >>> 4);
爲何要通過這樣的運算呢?這就是HashMap的高明之處。先看個例子,一個十進制數32768(二進制1000 0000 0000 0000),通過上述公式運算以後的結果是35080(二進制1000 1001 0000 1000)。看出來了嗎?或許這樣還看不出什麼,再舉個數字61440(二進制1111 0000 0000 0000),運算結果是65263(二進制1111 1110 1110 1111),如今應該很明顯了,它的目的是讓「1」變的均勻一點,散列的本意就是要儘可能均勻分佈。那這樣有什麼意義呢?看第3步。
三、 獲得h以後,把h與HashMap的承載量(HashMap的默認承載量length是16,能夠自動變長。在構造HashMap的時候也能夠指定一個長 度。這個承載量就是上圖所描述的數組的長度。)進行邏輯與運算,即 h & (length-1),這樣獲得的結果就是一個比length小的正數,咱們把這個值叫作index。其實這個index就是索引將要插入的值在數組中的 位置。第2步那個算法的意義就是但願可以得出均勻的index,這是HashTable的改進,HashTable中的算法只是把key的 hashcode與length相除取餘,即hash % length,這樣有可能會形成index分佈不均勻。還有一點須要說明,HashMap的鍵能夠爲null,它的值是放在數組的第一個位置。
四、 咱們用table[index]表示已經找到的元素須要存儲的位置。先判斷該位置上有沒有元素(這個元素是HashMap內部定義的一個類Entity, 基本結構它包含三個類,key,value和指向下一個Entity的next),沒有的話就建立一個Entity<K,V>對象,在 table[index]位置上插入,這樣插入結束;若是有的話,經過鏈表的遍歷方式去逐個遍歷,看看有沒有已經存在的key,有的話用新的value替 換老的value;若是沒有,則在table[index]插入該Entity,把原來在table[index]位置上的Entity賦值給新的 Entity的next,這樣插入結束。
大學學過記憶最深入的是除留餘數發
假設哈希表長爲m,p爲小於等於m的最大素數,則哈希函數爲
h(k)=k % p ,其中%爲模p取餘運算。
例如,已知待散列元素爲(18,75,60,43,54,90,46),表長m=10,p=7,則有
h(18)=18 % 7=4 h(75)=75 % 7=5 h(60)=60 % 7=4
h(43)=43 % 7=1 h(54)=54 % 7=5 h(90)=90 % 7=6
h(46)=46 % 7=4
此時衝突較多。爲減小衝突,可取較大的m值和p值,如m=p=13,結果以下:
h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8
h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12
h(46)=46 % 13=7
這就是hash表會浪費空間的緣由,若是hash算法比較優秀,能夠節省空間。請參考如上的改進後的hash table。
改進後的hash table以下如所示
-----------------------------------------2019年4月17日21:23:30-------------------------------------------------
-----------------------------------------鏈地址法(即拉鍊法)解決衝突-------------------------------------------------
咱們先複習數據結構裏的一個知識點:在一個長度爲n(假設是10000)的線性表(假設是ArrayList)裏,存放着無序的數字;若是咱們要找一個指定的數字,就不得不經過從頭至尾依次遍從來查找,這樣的平均查找次數是n除以2(這裏是5000)。
咱們再來觀察Hash表(這裏的Hash表純粹是數據結構上的概念,和Java無關)。它的平均查找次數接近於1,代價至關小,關鍵是在Hash表裏,存放在其中的數據和它的存儲位置是用Hash函數關聯的。
咱們假設一個Hash函數是x*x%5,( * & %的優先級是從左至右)固然實際狀況裏不可能用這麼簡單的Hash函數,咱們這裏純粹爲了說明方便,而Hash表是一個長度是11的線性表。若是咱們要把6放入其中,那麼咱們首先會對6用Hash函數計算一下,結果是1,因此咱們就把6放入到索引號是1這個位置。一樣若是咱們要放數字7,通過Hash函數計算,7的結果是4,那麼它將被放入索引是4的這個位置。這個效果以下圖所示。
這樣作的好處很是明顯。好比咱們要從中找6這個元素,咱們能夠先經過Hash函數計算6的索引位置,而後直接從1號索引裏找到它了。
不過咱們會遇到「Hash值衝突」這個問題。好比通過Hash函數計算後,7和8會有相同的Hash值,對此Java的HashMap對象採用的是」鏈地址法「的解決方案。效果以下圖所示。
具體的作法是,爲全部Hash值是i的對象創建一個同義詞鏈表。假設咱們在放入8的時候,發現4號位置已經被佔,那麼就會新建一個鏈表結點放入8。一樣,若是咱們要找8,那麼發現4號索引裏不是8,那會沿着鏈表依次查找。
雖然咱們仍是沒法完全避免Hash值衝突的問題,可是Hash函數設計合理,仍能保證同義詞鏈表的長度被控制在一個合理的範圍裏。
在學習gp的hashjoin時,有一個值得關注的問題就是,若是hash table過大,內存中都放不下這個映射表時怎麼辦?