雜湊表(hash table)也叫雜湊表,是一種非常重要的資料結構,應用場景及其豐富,許多快取技術(比如 memcached)的核心其實就是在記憶體中維護一張大的雜湊表。
在討論雜湊表之前,我們先大概了解下其他資料結構在新增,查詢等基礎操作執行效能
**陣列:**採用一段連續的儲存單元來儲存資料。對於制定下標的查詢,時間複雜度為o(1);通過給定值進行查詢,需要遍歷陣列,逐一比對給定關鍵字和陣列元素,時間複雜度為o(n),當然,對於有序陣列,則可採用二分查詢,插值查詢,斐波那契查詢等方式,可將查詢複雜度提高為o(logn);對於一般的插入刪除操作,涉及到陣列元素的移動,其平均複雜度也為o(n)
**線性鍊錶:**對於鍊錶的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度為o(1),而查詢操作需要遍歷鍊錶逐一進行比對,複雜度為o(n)。
**二叉樹:**對一棵相對平衡的有序二叉樹,對其進行插入,查詢,刪除等操作,平均複雜度為o(logn)。
**雜湊表:**相比上述幾種資料結構,在雜湊表中進行新增,刪除,查詢等湊在哦,效能十分之搞,不考慮雜湊衝突的情況下,僅需一次定位即可完成,時間複雜度為o(1)
資料結構的物理儲存結構只有兩種:順序儲存結構和鏈式儲存結構(棧、佇列、樹、圖等是從邏輯結構中去抽象,對映到記憶體中),雜湊表的主幹就是陣列。
比如我們要新增或查詢某個元素,我們通過把當前元素的關鍵字 通過某個函式對映到陣列中的某個位置,通過陣列下標一次定位就可完成操作。這個函式可以簡單描述為:儲存位置 = f(關鍵字),這個函式f一般稱為雜湊函式,這個函式的設計好壞會直接影響到雜湊表的優劣。
雜湊衝突
然而萬事無完美,如果兩個不同的元素,通過雜湊函式得出的實際儲存位址相同怎麼辦?也就是說,當我們對某個元素進行雜湊運算,得到乙個儲存位址,然後要進行插入的時候,發現已經被其他元素占用了,其實這就是所謂的雜湊衝突,也叫雜湊碰撞。前面我們提到過,雜湊函式的設計至關重要,好的雜湊函式會盡可能地保證 計算簡單和雜湊位址分布均勻,但是,我們需要清楚的是,陣列是一塊連續的固定長度的記憶體空間,再好的雜湊函式也不能保證得到的儲存位址絕對不發生衝突。那麼雜湊衝突如何解決呢?雜湊衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被占用的儲存位址),再雜湊函式法,鏈位址法,而hashmap即是採用了鏈位址法,也就是陣列 鍊錶的方式。
hashmap的主幹是乙個entry陣列。entry是hashmap的基本組成單元,每乙個entry包含乙個key-value鍵值對。(其實所謂map其實就是儲存了兩個物件之間的對映關係的一種集合)
entry是hashmap中的乙個靜態內部類,以下是對應的**,主要用於儲存資料節點。
/**
* basic hash bin node, used for most entries. (see below for
* treenode subclass, and in linkedhashmap for its entry subclass.)
*/static
class
node
implements
map.entry
public
final k getkey()
public
final v getvalue()
public
final string tostring()
public
final
inthashcode()
public
final v setvalue
(v newvalue)
public
final
boolean
equals
(object o)
return
false;}
}
簡單來說,hashmap由陣列 鍊錶組成,陣列是hashmap的主體,鍊錶則是主要為了解決雜湊衝突而存在的,如果定位到的陣列位置不含鍊錶(當前entry的next指向null),那麼查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含鍊錶,對於新增操作,其時間複雜度為o(n),首先遍歷鍊錶,存在即覆蓋,否則新增;對於查詢操作來講,仍需遍歷鍊錶,然後通過key物件的equals方法逐一比對查詢。所以,效能考慮,**hashmap中的鍊錶出現越少,效能才會越好。**jdk8之後,將鍊錶更改為紅黑樹(當目前位置儲存的結點大於8的時候會轉換)。
其他引數以及固定變數
// 預設的初始化容量
static
final
int default_initial_capacity =
1<<4;
// aka 16
// 最大容量
static
final
int maximum_capacity =
1<<30;
// 預設的負載因子
static
final
float default_load_factor =
0.75f
;// hash衝突之後,鍊錶儲存的結點大於這個值之後轉變為紅黑樹儲存
static
final
int treeify_threshold =8;
// 在進行擴容的時候小於此值的時候,將紅黑樹轉換為鍊錶儲存
static
final
int untreeify_threshold =6;
// 可以對容器進行treeified的最小表容量。(否則,如果乙個bin中有太多節點,就會重新調整表的大小。)至少4 * treeify_threshold,以避免調整大小和treeification閥值之間的衝突。
static
final
int min_treeify_capacity =64;
// 雜湊桶,在第一次使用的時候進行初始化,容量總是2的倍數
transient node
table;
// 包含的真實 key-value 數目
transient
int size;
// hashmap被改變的次數,由於hashmap非執行緒安全,在對hashmap進行迭代時,如果期間其他執行緒的參與導致hashmap的結構發生變化了(比如put,remove等操作),需要丟擲異常concurrentmodificationexception
transient
int modcount;
// 閾值,當table == {}時,該值為初始容量(初始容量預設為16);當table被填充了,也就是為table分配記憶體空間後,threshold一般為 capacity*loadfactory。hashmap在進行擴容時需要參考threshold。
int threshold;
// hash表的負載因子
final
float loadfactor;
hashmap的四個建構函式:
// 使用給定的值進行容量和負載因子的初始化
public
hashmap
(int initialcapacity,
float loadfactor)
// 使用給定的值進行容量初始化,預設負載因子
public
hashmap
(int initialcapacity)
// 空方法,使用預設的負載因子進行構造,
public
hashmap()
public
hashmap
(map<
?extendsk,
?extends
v> m)
注意:hashmap在進行物件構造的時候,還沒有進行空間分配,只有在第一次進行put()的時候才進行的空間分配,也就是在resize()中進行的。
// hashmap的put方法
public v put
(k key, v value)
// 實現map.put和其相關的方法,
final v putval
(int hash, k key, v value,
boolean onlyifabsent,
boolean evict)
if(e.hash == hash &&
((k = e.key)
== key ||
(key != null && key.
equals
(k))))
break
; p = e;}}
// 如果e不為空,則說明key重複,直接進行value覆蓋即可
if(e != null)
} modcount;
// 判斷是否進行擴容
HashMap原始碼分析
public hashmap int initialcapacity,float loadfactor 2 接下來是重要的put方法,put方法用於將鍵值對儲存到map中,讓我們來具體分析一下。public v put k key,v value if key null 若key為null,則將va...
HashMap 原始碼分析
1 getentry object key 方法 final entrygetentry object key return null 根據key的hash值計算出索引,得到table中的位置,然後遍歷table處的鍊錶 for entrye table indexfor hash,table.le...
HashMap原始碼分析
public v put k key,v value if key null return putfornullkey value int hash hash key int i indexfor hash,table.length for entrye table i e null e e.nex...