數組是咱們比較熟悉的一種數據結構:固定大小,索引(下標)對應的槽位用以存儲數據:html
咱們要在數組中查找一個值,好比紅框圈中的 元素5 ,能夠經過遍歷或者排序後二分的方式達到目的。沒有更快捷的查找方式了嗎?顯然是有的,好比Map。
咱們對存 / 取動一動腦筋,仍是上圖的那些元素,假如咱們這樣存:java
此時,想獲取 元素5 很容易,直接array[5]就能夠,但問題也一樣突出,數組的length變得很大。這個例子中,最大的元素是79,還能夠接受,若是最大元素是98277呢?更大呢?面試
咱們以取餘數的方式做爲變通:對於元素集合{8,1,5,6,82,33}
,還將這些元素放入最開始的length爲6 的數組中。分別對元素除6取餘,計算結果以下:算法
8->2 1->1 5->5 6->0 82->4 33->3
把餘數做爲下標,存入數組。segmentfault
此時,咱們想在數組中查找是否存在元素5,只需對要查找的值——元素5,按數組的length取餘5%6=5
,直接array[5]便可。數組
這裏的按數組的length取餘,扮演的就是散列函數的角色!數據結構
什麼是散列函數?能夠理解爲,將元素儘量分散的打入到數組中的函數。函數
散列函數有兩個特徵:優化
5%6
老是等於5。同時也有兩個疑問,分別看下:spa
字符串的本質是字符數組,字符在ascii碼錶上就是數字。
對象是各類屬性構成的,這些屬性包括基本類型、字符串等等。
固然具體的算法要比取來的複雜,好比String的hashCode算法:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
沒錯,各類hashCode()方法,就是咱們一直在聊的散列函數!
Tips:
圍繞hashCode有幾道經典面試題,正值跳槽季,給你們安利一下: 1.Object的hashCode是否是內存地址? 2.什麼狀況下會覆寫hashCode()方法?你有沒有覆寫過這個方法? 3.若是對象A equals 對象B,則對象A和對象B的hashCode是否相等?反過來,對象A和對象B的hashCode值相等,equals是否返回true?
到目前爲止,一切很順利。length=6的數組完成了對集合{8,1,5,6,82,33}
全部元素的安置,但這是最簡單的狀況。若是再增長一個數字,就選西方人認爲不怎麼吉利的 13 好了,取餘計算13%6=1
,本來應該放在索引爲1的槽上,而咱們的數組如今已經滿員了。
這就是hash衝突的問題,怎麼解決?顯然直接覆蓋並不合理,那樣會丟掉原有的元素1。想一想HashMap,若是發生了hash衝突,就丟棄原有值,這種作法使用者確定沒法接收。
是時候讓另外一種數據結構登場了——鏈表。
數組佔用相鄰的整塊內存,且固定大小;鏈表則否則,因爲結構上存在指向下一個節點(內存地址)的指針,所以不要求內存地址連續,大小也不固定。
由於結構的緣故,鏈表在插入、刪除方面更有優點,修改指針指向便可;而數組在快速定位某槽位上更具優點,鏈表只能從頭遍歷。
加入鏈表後,散列表升級成這樣:
元素13放入時,計算hashCode爲1(姑且按取餘的方式進行理解)。若是索引爲1的槽位爲空,直接放入元素;若是索引爲1的槽位已經存在元素,將該槽位存儲結構變動爲鏈表。
根據Key值,計算hashCode。若是hashCode,也就是索引對應的槽位爲空或只有一個元素,直接返回該值;若是hashCode對應的槽位中的數據爲鏈表結構,對鏈表進行遍歷,直到找到與KEY equals的對象。
若是hash衝突比較多,會發生什麼狀況?
鏈表的無限擴張,會使得查詢變得緩慢,咱們最初不就是想用散列表解決快速查找的問題嗎?如上圖這種狀況,散列表幾乎失去了意義,又回到了遍歷查找的時代,這也是散列函數儘量將元素均勻分佈的緣由。怎麼解決?數組快要滿時,對其擴容!
HashMap也是這麼作的,初始值2^4=16的數組,默認0.75的擴容因子;當元素個數超過閾值,即16*0.75=12的時候,觸發resize方法進行擴容。數組大小翻倍,元素rehash後放入相應的槽位。
能夠看出,散列表就是HashMap的底層結構。固然了,JDK 1.8版本對其還有紅黑樹等優化,感興趣可查閱 Java 8系列之從新認識HashMap
ok,本篇文章到此就告一段落了,下一篇咱們探討下圖的經典問題——最短路徑,敬請關注!