Hash函數
非哈希表的特點:關鍵字在表中的位置和它之間不存在一個確定的關系,查找的過程為給定值一次和各個關鍵字進行比較,查找的效率取決於和給定值進行比較的次數。
哈希表的特點:關鍵字在表中位置和它之間存在一種確定的關系。
哈希函數:一般情況下,需要在關鍵字與它在表中的存儲位置之間建立一個函數關系,以f(key)作為關鍵字為key的記錄在表中的位置,通常稱這個函數f(key)為哈希函數。
hash : 翻譯為“散列”,就是把任意長度的輸入,通過散列算法,變成固定長度的輸出,該輸出就是散列值。
這種轉換是一種壓縮映射,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來唯一的確定輸入值。
簡單的說就是一種將任意長度的消息壓縮到莫伊固定長度的消息摘要的函數。
hash沖突:就是根據key即經過一個函數f(key)得到的結果的作為地址去存放當前的key value鍵值對(這個是hashmap的存值方式),但是卻發現算出來的地址上已經有人先來了。就是說這個地方被搶了啦。這就是所謂的hash沖突啦。
哈希函數處理沖突的方法
1.開放定址法:
其中 m 為表的長度
對增量di有三種取法:
線性探測再散列 di = 1 , 2 , 3 , ... , m-1
平方探測再散列 di = 1 2 , -2 , 4 , -4 , 8 , -8 , ... , k的平方 , -k平方
隨機探測再散列 di 是一組偽隨機數列
2.鏈地址法
這種方法的基本思想是將所有哈希地址為i的元素構成一個稱為同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於經常進行插入和刪除的情況。
3.再哈希
這種方法是同時構造多個不同的哈希函數:
Hi=RH1(key) i=1,2,…,k
當哈希地址Hi=RH1(key)發生沖突時,再計算Hi=RH2(key)……,直到沖突不再產生。這種方法不易產生聚集,但增加了計算時間。
4.建立公共溢出區
這種方法的基本思想是:將哈希表分為基本表和溢出表兩部分,凡是和基本表發生沖突的元素,一律填入溢出表
HashMap的Hash沖突處理辦法
hashmap出現了Hash沖突的時候采用第二種辦法:鏈地址法。
代碼示例:
有一個”國家”(Country)類,我們將要用Country對象作為key,它的首都的名字(String類型)作為value。下面的例子有助於我們理解key-value對在HashMap中是如何存儲的。
public class Country { String name; long population; public Country(String name, long population) { super(); this.name = name; this.population = population; } public String getName() { return name; } public void setName(String name) { this.name = name; } public long getPopulation() { return population; } public void setPopulation(long population) { this.population = population; } // If length of name in country object is even then return 31(any random // number) and if odd then return 95(any random number). // This is not a good practice to generate hashcode as below method but I am // doing so to give better and easy understanding of hashmap. @Override public int hashCode() { if (this.name.length() % 2 == 0) return 31; else return 95; } @Override public boolean equals(Object obj) { Country other = (Country) obj; if (name.equalsIgnoreCase((other.name))) return true; return false; } } public class HashMapStructure { public static void main(String[] args) { Country india = new Country("India", 1000); Country japan = new Country("Japan", 10000); Country france = new Country("France", 2000); Country russia = new Country("Russia", 20000); HashMap<Country, String> countryCapitalMap = new HashMap<Country, String>(); countryCapitalMap.put(india, "Delhi"); countryCapitalMap.put(japan, "Tokyo"); countryCapitalMap.put(france, "Paris"); countryCapitalMap.put(russia, "Moscow"); Iterator<Country> countryCapitalIter = countryCapitalMap.keySet().iterator();// put debug point at this line while (countryCapitalIter.hasNext()) { Country countryObj = countryCapitalIter.next(); String capital = countryCapitalMap.get(countryObj); System.out.println(countryObj.getName() + "----" + capital); } } }
在注釋處加入debug,可以通過watch查看countryCapitalMap的結構:
從上圖可以觀察到以下幾點:
有一個叫做table大小是16的Entry數組。
這個table數組存儲了Entry類的對象。HashMap類有一個叫做Entry的內部類。這個Entry類包含了key-value作為實例變量。我們來看下Entry類的結構。Entry類的結構:
static class Entry implements Map.Entry{
final K key;
V value;
Entry next;
final int hash;
...//More code goes here
}
1).每當往hashmap里面存放key-value對的時候,都會為它們實例化一個Entry對象,這個Entry對象就會存儲在前面提到的Entry數 組table中。現在你一定很想知道,上面創建的Entry對象將會存放在具體哪個位置(在table中的精確位置)。答案就是,根據key的 hashcode()方法計算出來的hash值(來決定)。hash值用來計算key在Entry數組的索引。
2).現在,如果你看下上圖中數組的索引15,它有一個叫做HashMap$Entry的Entry對象。
3).我們往hashmap放了4個key-value對,但是看上去好像只有1個元素!!!這是因為,如果兩個元素有相同的hashcode,它們會 被放在同一個索引上。問題出現了,該怎么放呢?原來它是以鏈表(LinkedList)的形式來存儲的(邏輯上)。因此他們都在hash值為15的位置 上存着了,然后把多個Entry,用next進行鏈接。
======================================================================
hash沖突的解決方法以及hashMap的底層實現
大家平時都用過hashMap,但是可能大家對hashMap的底層實現不太了解,同時對其中可能出現的hash沖突有些不了解,這幾天我翻了下資料,也稍微了解下,記錄下來,以免遺忘。
上圖就是一個散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。但是當關鍵字數量比較大的時候,難免就會造成一個問題,就是不一樣的關鍵字隱射到同一個地址上,這樣就造成了一個問題,就是hash沖突。那么如何解決呢?
一般比較常用的方法有開放地址法:
1. 開放尋址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)為散列函數,m為散列表長,di為增量序列,可有下列三種取法:
1.1. di=1,2,3,…,m-1,稱線性探測再散列;順序查看表的下一單元,直至找到某個空單元,或查遍全表。
1.2. di=1^2,-1^2,2^2,-2^2,⑶^2,…,±(k)^2,(k<=m/2)稱二次探測再散列;在表的左右進行跳躍式探測。
1.3. di=偽隨機數序列,稱偽隨機探測再散列。根據產生的隨機數進行探測。
2 再散列法:建立多個hash函數,若是當發生hash沖突的時候,使用下一個hash函數,直到找到可以存放元素的位置。
3 拉鏈法(鏈地址法):就是在沖突的位置上簡歷一個鏈表,然后將沖突的元素插入到鏈表尾端,
4 建立公共溢出區:將哈希表分為基本表和溢出表,將與基本表發生沖突的元素放入溢出表中。
底層的hashMap是由數組和鏈表來實現的,就是上面說的拉鏈法。首先當插入的時候,會根據key的hash值然后計算出相應的數組下標,計算方法是index = hashcode%table.length,(這個下標就是上面提到的bucket),當這個下標上面已經存在元素的時候那么就會形成鏈表,將后插入的元素放到尾端,若是下標上面沒有存在元素的話,那么將直接將元素放到這個位置上。
當進行查詢的時候,同樣會根據key的hash值先計算相應的下標,然后到相應的位置上進行查找,若是這個下標上面有很多元素的話,那么將在這個鏈表上一直查找直到找到對應的元素。