1.哈希表介紹
前面我們已經介紹了許多類型的數據結構。在想要查詢容器內特定元素時,有序向量使得我們能使用二分查找法進行精確的查詢((O(logN)對數復雜度,很高效)。
可人類總是不知滿足,依然在尋求一種更高效的特定元素查詢的數據結構,哈希表/散列表(hash table)就應運而生啦。哈希表在特定元素的插入,刪除和查詢時都能夠達到O(1)常數的時間復雜度,十分高效。
1.1 哈希算法
哈希算法的定義:把任意長度的輸入通過哈希算法轉換映射為固定長度的輸出,所得到的輸出被稱為哈希值(hashCode = hash(input))。哈希映射是一種多對一的關系,即多個不同的輸入有可能對應着一個相同的哈希值輸出;也意味着,哈希映射是不可逆,無法還原的。
舉個例子:我們有一個好朋友叫熊大,大家都叫他老熊。可以理解為是一個hash算法:對於一個人名,我們一般稱呼為"老" + 姓氏(單姓) (hash(熊大) = 老熊)。同時,我們還有一個好朋友叫熊二,我們也叫他老熊(hash(熊二) = 老熊)。當熊大和熊二兩個好朋友同時和我們聚會時,都稱呼他們為老熊就不太合適啦,因為這時出現了hash沖突。老熊這個稱呼同時對應了多個人,多個不同的輸入對應了相同的哈希值輸出。
java在Object這一最高層對象中實現了hashCode方法,並允許子類重寫更適應自身,沖突概率更低的hashCode方法。
1.2 哈希表實現的基本思路
哈希表存儲的是key-value鍵值對結構的數據,其基礎是一個數組。
由於采用hash算法會出現hash沖突,一個數組下標對應了多個元素。常見的解決hash沖突的方法有:開放地址法、重新哈希法、拉鏈法等等,我們的哈希表實現采用的是拉鏈法解決hash沖突。
采用拉鏈法的哈希表將內部數組的每一個元素視為一個插槽(slot)或者桶(bucket),並將數據存放在鍵值對節點(EntryNode)中。EntryNode除了存放key和value,還維護着一個next節點的引用。為了解決hash沖突,單個插槽內的多個EntryNode構成一個簡單的單向鏈表,插槽指向鏈表的頭部節點,新的數據將會插入當前鏈表的尾部。
key值不同但映射的hash值相同的元素在哈希表的同一個插槽中以鏈表的形式共存。
1.3 哈希表的負載因子(loadFactor):
哈希表在查詢數據時通過直接計算數據hash值對應的插槽,迅速獲取到key值對應的數據,進行非常高效的數據查詢。
但依然存在一個問題:雖然設計良好的hash函數可以盡可能的降低hash沖突的概率,但hash沖突還是不可避免的。當發生頻繁的哈希沖突時,對應的插槽內可能會存放較多的元素,導致插槽內的鏈表數據過多。而鏈表的查詢效率是非常低的,在極端情況下,甚至會出現所有元素都映射存放在同一個插槽內,此時的哈希表退化成了一個鏈表,查詢效率急劇降低。
一般的,哈希表存儲的數據量一定時,內部數組的大小和數組插槽指向的鏈表長度成反比。換句話說,總數據量一定,內部數組的容量越大(插槽越多),平均下來桶鏈表的長度也就越小,查詢效率越高。
同等數據量下,哈希表內部數組容量越大,查詢效率越高,但同時空間占用也越高,這本質上是一個空間換時間的取舍。
哈希表允許用戶在初始化時指定負載因子(loadFactor):負載因子代表着存儲的總數據量和內部數組大小的比值。插入新數據時,判斷哈希表當前的存儲量和內部數組的比值是否超過了負載因子。當比值超過了負載因子時,哈希表認為內部過於擁擠,查詢效率太低,會觸發一次擴容的rehash操作。rehash會對內部數組擴容,將存儲的元素重新進行hash映射,使得哈希表始終保持一個合適的查詢效率。
通過指定自定義的負載因子,用戶可以控制哈希表在空間和時間上取舍的程度,使哈希表能更有效地適應用戶的使用場景。
指定的負載因子越大,哈希表越擁擠(負載高,緊湊),查詢效率越低,空間效率越高。
指定的負載因子越小,哈希表越稀疏(負載小,松散),查詢效率越高,空間效率越低。
2.哈希表ADT接口
和之前介紹的鏈表不同,我們在哈希表的ADT接口中暴露出了哈希表內部實現的EntryNode鍵值對節點。通過暴露出去的public方法,用戶在使用哈希表時,可以獲得內部的鍵值對節點,靈活的訪問其中的key、value數據(但沒有暴露setKey方法,不允許用戶自己設置key值)。
public interface Map <K,V>{ /** * 存入鍵值對 * @param key key值 * @param value value * @return 被覆蓋的的value值 */ V put(K key,V value); /** * 移除鍵值對 * @param key key值 * @return 被刪除的value的值 */ V remove(K key); /** * 獲取key對應的value值 * @param key key值 * @return 對應的value值 */ V get(K key); /** * 是否包含當前key值 * @param key key值 * @return true:包含 false:不包含 */ boolean containsKey(K key); /** * 是否包含當前value值 * @param value value值 * @return true:包含 false:不包含 */ boolean containsValue(V value); /** * 獲得當前map存儲的鍵值對數量 * @return 鍵值對數量 * */ int size(); /** * 當前map是否為空 * @return true:為空 false:不為空 */ boolean isEmpty(); /** * 清空當前map */ void clear(); /** * 獲得迭代器 * @return 迭代器對象 */ Iterator<EntryNode<K,V>> iterator(); /** * 鍵值對節點 內部類 * */ class EntryNode<K,V>{ final K key; V value; EntryNode<K,V> next; EntryNode(K key, V value) { this.key = key; this.value = value; } boolean keyIsEquals(K key){ if(this.key == key){ return true; } if(key == null){ //:::如果走到這步,this.key不等於null,不匹配 return false; }else{ return key.equals(this.key); } } EntryNode<K, V> getNext() { return next; } void setNext(EntryNode<K, V> next) { this.next = next; } public K getKey() { return key; } public V getValue() { return value; } public void setValue(V value) { this.value = value; } @Override public String toString() { return key + "=" + value; } } }
3.哈希表實現細節
3.1 哈希表基本屬性:
public class HashMap<K,V> implements Map<K,V>{ /** * 內部數組 * */ private EntryNode<K,V>[] elements; /** * 當前哈希表的大小 * */ private int size; /** * 負載因子 * */ private float loadFactor; /** * 默認的哈希表容量 * */ private final static int DEFAULT_CAPACITY = 16; /** * 擴容翻倍的基數 * */ private final static int REHASH_BASE = 2; /** * 默認的負載因子 * */ private final static float DEFAULT_LOAD_FACTOR = 0.75f; //========================================構造方法=================================================== /** * 默認構造方法 * */ @SuppressWarnings("unchecked") public HashMap() { this.size = 0; this.loadFactor = DEFAULT_LOAD_FACTOR; elements = new EntryNode[DEFAULT_CAPACITY]; } /** * 指定初始容量的構造方法 * @param capacity 指定的初始容量 * */ @SuppressWarnings("unchecked") public HashMap(int capacity) { this.size = 0; this.loadFactor = DEFAULT_LOAD_FACTOR; elements = new EntryNode[capacity]; } /** * 指定初始容量和負載因子的構造方法 * @param capacity 指定的初始容量 * @param loadFactor 指定的負載因子 * */ @SuppressWarnings("unchecked") public HashMap(int capacity,int loadFactor) { this.size = 0; this.loadFactor = loadFactor; elements = new EntryNode[capacity]; } }
3.2 通過hash值獲取對應插槽下標:
獲取hash的方法僅和數據自身有關,不受到哈希表存儲數據量的影響。
因此getIndex方法的時間復雜度為O(1)。
/** * 通過key的hashCode獲得對應的內部數組下標 * @param key 傳入的鍵值key * @return 對應的內部數組下標 * */ private int getIndex(K key){ return getIndex(key,this.elements); } /** * 通過key的hashCode獲得對應的內部數組插槽slot下標 * @param key 傳入的鍵值key * @param elements 內部數組 * @return 對應的內部數組下標 * */ private int getIndex(K key,EntryNode<K,V>[] elements){ if(key == null){ //::: null 默認存儲在第0個桶內 return 0; }else{ int hashCode = key.hashCode(); //:::通過 高位和低位的異或運算,獲得最終的hash映射,減少碰撞的幾率 int finalHashCode = hashCode ^ (hashCode >>> 16); return (elements.length-1) & finalHashCode; } }
3.3 鏈表查詢方法:
當出現hash沖突時,會在對應插槽處生成一個單鏈表。我們需要提供一個方便的單鏈表查詢方法,將增刪改查接口的部分公用邏輯抽象出來,簡化代碼的復雜度。
值得注意的是:在判斷Key值是否相等時使用的是EntryNode.keyIsEquals方法,內部最終是通過equals方法進行比較的。也就是說,判斷key值是否相等和其它數據結構一樣,依然是由equals方法決定的。hashCode方法的作用僅僅是使我們能夠更快的定位到所映射的插槽處,加快查詢效率。
思考一下,為什么要求在重寫equals方法的同時,也應該重寫hashCode方法?
/** * 獲得目標節點的前一個節點 * @param currentNode 當前桶鏈表節點 * @param key 對應的key * @return 返回當前桶鏈表中"匹配key的目標節點"的"前一個節點" * 注意:當桶鏈表中不存在匹配節點時,返回桶鏈表的最后一個節點 * */ private EntryNode<K,V> getTargetPreviousEntryNode(EntryNode<K,V> currentNode,K key){ //:::不匹配 EntryNode<K,V> nextNode = currentNode.next; //:::遍歷當前桶后面的所有節點 while(nextNode != null){ //:::如果下一個節點的key匹配 if(nextNode.keyIsEquals(key)){ return currentNode; }else{ //:::不斷指向下一個節點 currentNode = nextNode; nextNode = nextNode.next; } } //:::到達了桶鏈表的末尾,返回最后一個節點 return currentNode; }
3.4 增刪改查接口:
哈希表的增刪改查接口都是通過hash值直接計算出對應的插槽下標(getIndex方法),然后遍歷插槽內的桶鏈表進行進一步的精確查詢(getTargetPreviousEntryNode方法)。在負載因子位於正常范圍內時(一般小於1),桶鏈表的平均長度非常短,可以認為單個桶鏈表的遍歷查詢時間復雜度為(O(1))。
因此哈希表的增刪改查接口的時間復雜度都是O(1)。
@Override public V put(K key, V value) { if(needReHash()){ reHash(); } //:::獲得對應的內部數組下標 int index = getIndex(key); //:::獲得對應桶內的第一個節點 EntryNode<K,V> firstEntryNode = this.elements[index]; //:::如果當前桶內不存在任何節點 if(firstEntryNode == null){ //:::創建一個新的節點 this.elements[index] = new EntryNode<>(key,value); //:::創建了新節點,size加1 this.size++; return null; } if(firstEntryNode.keyIsEquals(key)){ //:::當前第一個節點的key與之匹配 V oldValue = firstEntryNode.value; firstEntryNode.value = value; return oldValue; }else{ //:::不匹配 //:::獲得匹配的目標節點的前一個節點 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); //:::獲得匹配的目標節點 EntryNode<K,V> targetNode = targetPreviousNode.next; if(targetNode != null){ //:::更新value的值 V oldValue = targetNode.value; targetNode.value = value; return oldValue; }else{ //:::在桶鏈表的末尾 新增一個節點 targetPreviousNode.next = new EntryNode<>(key,value); //:::創建了新節點,size加1 this.size++; return null; } } } @Override public V remove(K key) { //:::獲得對應的內部數組下標 int index = getIndex(key); //:::獲得對應桶內的第一個節點 EntryNode<K,V> firstEntryNode = this.elements[index]; //:::如果當前桶內不存在任何節點 if(firstEntryNode == null){ return null; } if(firstEntryNode.keyIsEquals(key)){ //:::當前第一個節點的key與之匹配 //:::將桶鏈表的第一個節點指向后一個節點(兼容next為null的情況) this.elements[index] = firstEntryNode.next; //:::移除了一個節點 size減一 this.size--; //:::返回之前的value值 return firstEntryNode.value; }else{ //:::不匹配 //:::獲得匹配的目標節點的前一個節點 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); //:::獲得匹配的目標節點 EntryNode<K,V> targetNode = targetPreviousNode.next; if(targetNode != null){ //:::將"前一個節點的next" 指向 "目標節點的next" ---> 相當於將目標節點從桶鏈表移除 targetPreviousNode.next = targetNode.next; //:::移除了一個節點 size減一 this.size--; return targetNode.value; }else{ //:::如果目標節點為空,說明key並不存在於哈希表中 return null; } } } @Override public V get(K key) { //:::獲得對應的內部數組下標 int index = getIndex(key); //:::獲得對應桶內的第一個節點 EntryNode<K,V> firstEntryNode = this.elements[index]; //:::如果當前桶內不存在任何節點 if(firstEntryNode == null){ return null; } if(firstEntryNode.keyIsEquals(key)){ //:::當前第一個節點的key與之匹配 return firstEntryNode.value; }else{ //:::獲得匹配的目標節點的前一個節點 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); //:::獲得匹配的目標節點 EntryNode<K,V> targetNode = targetPreviousNode.next; if(targetNode != null){ return targetNode.value; }else{ //:::如果目標節點為空,說明key並不存在於哈希表中 return null; } } }
3.5 擴容rehash操作:
前面提到,當插入數據時發現哈希表過於擁擠,超過了負載因子指定的值時,會觸發一次rehash擴容操作。
擴容時,我們的內部數組擴容了2倍,所以對於每一個插槽內的元素在rehash時存在兩種可能:
1.依然映射到當前下標插槽處
2.映射到高位下標處(當前下標 + 擴容前內部數組長度大小)
注意觀察0,4,8三個元素節點,在擴容前(對4取模)都位於下標0插槽;擴容后,數組容量翻倍(對8取模),存在兩種情況,0,8兩個元素哈希值依然映射在下標0插槽(低位插槽),而元素4則被映射到了下標4插槽(高位插槽)(當前下標(0) + 擴容前內部數組長度大小(4))。
通過遍歷每個插槽,將內部元素按順序進行rehash,得到擴容兩倍后的哈希表(數據保留了之前的順序,即先插入的節點依然位於桶鏈表靠前的位置)。
和向量擴容一樣,雖然rehash操作的時間復雜度為O(n)。但是由於只在插入時偶爾的被觸發,總體上看,rehash操作的時間復雜度為O(1)。
哈希表擴容前:
哈希表擴容后:
/** * 哈希表擴容 * */ @SuppressWarnings("unchecked") private void reHash(){ //:::擴容兩倍 EntryNode<K,V>[] newElements = new EntryNode[this.elements.length * REHASH_BASE]; //:::遍歷所有的插槽 for (int i=0; i<this.elements.length; i++) { //:::為單個插槽內的元素 rehash reHashSlot(i,newElements); } //:::內部數組 ---> 擴容之后的新數組 this.elements = newElements; } /** * 單個插槽內的數據進行rehash * */ private void reHashSlot(int index,EntryNode<K, V>[] newElements){ //:::獲得當前插槽第一個元素 EntryNode<K, V> currentEntryNode = this.elements[index]; if(currentEntryNode == null){ //:::當前插槽為空,直接返回 return; } //:::低位桶鏈表 頭部節點、尾部節點 EntryNode<K, V> lowListHead = null; EntryNode<K, V> lowListTail = null; //:::高位桶鏈表 頭部節點、尾部節點 EntryNode<K, V> highListHead = null; EntryNode<K, V> highListTail = null; while(currentEntryNode != null){ //:::獲得當前節點 在新數組中映射的插槽下標 int entryNodeIndex = getIndex(currentEntryNode.key,newElements); //:::是否和當前插槽下標相等 if(entryNodeIndex == index){ //:::和當前插槽下標相等 if(lowListHead == null){ //:::初始化低位鏈表 lowListHead = currentEntryNode; lowListTail = currentEntryNode; }else{ //:::在低位鏈表尾部拓展新的節點 lowListTail.next = currentEntryNode; lowListTail = lowListTail.next; } }else{ //:::和當前插槽下標不相等 if(highListHead == null){ //:::初始化高位鏈表 highListHead = currentEntryNode; highListTail = currentEntryNode; }else{ //:::在高位鏈表尾部拓展新的節點 highListTail.next = currentEntryNode; highListTail = highListTail.next; } } //:::指向當前插槽的下一個節點 currentEntryNode = currentEntryNode.next; } //:::新擴容elements(index)插槽 存放lowList newElements[index] = lowListHead; //:::lowList末尾截斷 if(lowListTail != null){ lowListTail.next = null; } //:::新擴容elements(index + this.elements.length)插槽 存放highList newElements[index + this.elements.length] = highListHead; //:::highList末尾截斷 if(highListTail != null){ highListTail.next = null; } } /** * 判斷是否需要 擴容 * */ private boolean needReHash(){ return ((this.size / this.elements.length) > this.loadFactor); }
3.6 其它接口實現:
@Override public boolean containsKey(K key) { V value = get(key); return (value != null); } @Override public boolean containsValue(V value) { //:::遍歷全部桶鏈表 for (EntryNode<K, V> element : this.elements) { //:::獲得當前桶鏈表第一個節點 EntryNode<K, V> entryNode = element; //:::遍歷當前桶鏈表 while (entryNode != null) { //:::如果value匹配 if (entryNode.value.equals(value)) { //:::返回true return true; } else { //:::不匹配,指向下一個節點 entryNode = entryNode.next; } } } //:::所有的節點都遍歷了,沒有匹配的value return false; } @Override public int size() { return this.size; } @Override public boolean isEmpty() { return (this.size == 0); } @Override public void clear() { //:::遍歷內部數組,將所有桶鏈表全部清空 for(int i=0; i<this.elements.length; i++){ this.elements[i] = null; } //:::size設置為0 this.size = 0; } @Override public Iterator<EntryNode<K,V>> iterator() { return new Itr(); } @Override public String toString() { Iterator<EntryNode<K,V>> iterator = this.iterator(); //:::空容器 if(!iterator.hasNext()){ return "[]"; } //:::容器起始使用"[" StringBuilder s = new StringBuilder("["); //:::反復迭代 while(true){ //:::獲得迭代的當前元素 EntryNode<K,V> data = iterator.next(); //:::判斷當前元素是否是最后一個元素 if(!iterator.hasNext()){ //:::是最后一個元素,用"]"收尾 s.append(data).append("]"); //:::返回 拼接完畢的字符串 return s.toString(); }else{ //:::不是最后一個元素 //:::使用", "分割,拼接到后面 s.append(data).append(", "); } } }
4.哈希表迭代器
1. 由於哈希表中數據分布不是連續的,所以在迭代器的初始化過程中必須先跳轉到第一個非空數據節點,以避免無效的迭代。
2. 當迭代器的下標到達當前插槽鏈表的末尾時,迭代器下標需要跳轉到靠后插槽的第一個非空數據節點。
/** * 哈希表 迭代器實現 */ private class Itr implements Iterator<EntryNode<K,V>> { /** * 迭代器 當前節點 * */ private EntryNode<K,V> currentNode; /** * 迭代器 下一個節點 * */ private EntryNode<K,V> nextNode; /** * 迭代器 當前內部數組的下標 * */ private int currentIndex; /** * 默認構造方法 * */ private Itr(){ //:::如果當前哈希表為空,直接返回 if(HashMap.this.isEmpty()){ return; } //:::在構造方法中,將迭代器下標移動到第一個有效的節點上 //:::遍歷內部數組,找到第一個不為空的數組插槽slot for(int i=0; i<HashMap.this.elements.length; i++){ //:::設置當前index this.currentIndex = i; EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; //:::找到了第一個不為空的插槽slot if(firstEntryNode != null){ //:::nextNode = 當前插槽第一個節點 this.nextNode = firstEntryNode; //:::構造方法立即結束 return; } } } @Override public boolean hasNext() { return (this.nextNode != null); } @Override public EntryNode<K,V> next() { this.currentNode = this.nextNode; //:::暫存需要返回的節點 EntryNode<K,V> needReturn = this.nextNode; //:::nextNode指向自己的next this.nextNode = this.nextNode.next; //:::判斷當前nextNode是否為null if(this.nextNode == null){ //:::說明當前所在的桶鏈表已經遍歷完畢 //:::尋找下一個非空的插槽 for(int i=this.currentIndex+1; i<HashMap.this.elements.length; i++){ //:::設置當前index this.currentIndex = i; EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; //:::找到了后續不為空的插槽slot if(firstEntryNode != null){ //:::nextNode = 當前插槽第一個節點 this.nextNode = firstEntryNode; //:::跳出循環 break; } } } return needReturn; } @Override public void remove() { if(this.currentNode == null){ throw new IteratorStateErrorException("迭代器狀態異常: 可能在一次迭代中進行了多次remove操作"); } //:::獲得需要被移除的節點的key K currentKey = this.currentNode.key; //:::將其從哈希表中移除 HashMap.this.remove(currentKey); //:::currentNode設置為null,防止反復調用remove方法 this.currentNode = null; } }
5.哈希表性能
5.1 空間效率:
哈希表的空間效率很大程度上取決於負載因子。通常,為了保證哈希表查詢的高效性,負載因子都設置的比較小(小於1),因而可能會出現許多空的插槽,浪費空間。
總體而言,哈希表的空間效率低於向量和鏈表。
5.2 時間效率:
一般的,哈希表增刪改查接口的時間復雜度都是O(1)。但是出現較多的hash沖突時,沖突范圍內的key的增刪改查效率較低,時間效率會有一定的波動。
總體而言,哈希表的時間效率高於向量和鏈表。
哈希表的時間效率很高,可天下沒有免費的午餐,據統計,哈希表的空間利用率通常情況下還不到50%。
哈希表是一個使用空間來換取時間的數據結構,對查詢性能有較高要求的場合,可以考慮使用哈希表。
6.哈希表總結
6.1 當前版本缺陷
至此,我們已經實現了一個基礎的哈希表,但還存在許多明顯缺陷:
1.當hash沖突比較頻繁時,查詢效率急劇降低。
jdk在1.8版本的哈希表實現(java.util.HashMap)中,對這一場景進行了優化。當內部桶鏈表的節點個數超過一定數量(默認為8)時,會將插槽中的桶鏈表轉換成一個紅黑樹(查詢效率為O(logN))。
2.不支持多線程
在多線程的環境,並發的訪問一個哈希表會導致諸如:擴容時內部節點死循環、丟失插入數據等異常情況。
6.2 查詢特定元素的方法
我們目前查詢特定元素有幾種不同的方法:
1.順序查找
在無序向量或者鏈表中,查找一個特定元素是通過從頭到尾遍歷容器內元素的方式實現的,執行速度正比於數據量的大小,順序查找的時間復雜度為O(n),效率較低。
2.二分查找
在有序向量以及后面要介紹的二叉搜索樹中,由於容器內部的元素是有序的,因此可以通過二分查找比較的方式查詢特定的元素,二分查找的時間復雜度為O(logN),效率較高。
3.哈希查找
在哈希表中,通過直接計算出數據hash值對應的插槽(slot)(時間復雜度O(1)),查找出對應的數據,哈希查找的時間復雜度為O(1),效率極高。
特定元素的查找方式和排序算法的關系
1.順序查找對應冒泡排序、選擇排序等,效率較低,時間復雜度(O(n²))。
2.二分查找對應快速排序、歸並排序等,效率較高,時間復雜度(O(nLogn))。
3.哈希查找對應基排序,效率極高,時間復雜度(O(n))。
在大牛劉未鵬的博客中有更為詳細的說明,http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick。
6.3 完整代碼
哈希表ADT接口:

1 public interface Map <K,V>{ 2 /** 3 * 存入鍵值對 4 * @param key key值 5 * @param value value 6 * @return 被覆蓋的的value值 7 */ 8 V put(K key,V value); 9 10 /** 11 * 移除鍵值對 12 * @param key key值 13 * @return 被刪除的value的值 14 */ 15 V remove(K key); 16 17 /** 18 * 獲取key對應的value值 19 * @param key key值 20 * @return 對應的value值 21 */ 22 V get(K key); 23 24 /** 25 * 是否包含當前key值 26 * @param key key值 27 * @return true:包含 false:不包含 28 */ 29 boolean containsKey(K key); 30 31 /** 32 * 是否包含當前value值 33 * @param value value值 34 * @return true:包含 false:不包含 35 */ 36 boolean containsValue(V value); 37 38 /** 39 * 獲得當前map存儲的鍵值對數量 40 * @return 鍵值對數量 41 * */ 42 int size(); 43 44 /** 45 * 當前map是否為空 46 * @return true:為空 false:不為空 47 */ 48 boolean isEmpty(); 49 50 /** 51 * 清空當前map 52 */ 53 void clear(); 54 55 /** 56 * 獲得迭代器 57 * @return 迭代器對象 58 */ 59 Iterator<EntryNode<K,V>> iterator(); 60 61 /** 62 * 鍵值對節點 內部類 63 * */ 64 class EntryNode<K,V>{ 65 final K key; 66 V value; 67 EntryNode<K,V> next; 68 69 EntryNode(K key, V value) { 70 this.key = key; 71 this.value = value; 72 } 73 74 boolean keyIsEquals(K key){ 75 if(this.key == key){ 76 return true; 77 } 78 79 if(key == null){ 80 //:::如果走到這步,this.key不等於null,不匹配 81 return false; 82 }else{ 83 return key.equals(this.key); 84 } 85 } 86 87 EntryNode<K, V> getNext() { 88 return next; 89 } 90 91 void setNext(EntryNode<K, V> next) { 92 this.next = next; 93 } 94 95 public K getKey() { 96 return key; 97 } 98 99 public V getValue() { 100 return value; 101 } 102 103 public void setValue(V value) { 104 this.value = value; 105 } 106 107 @Override 108 public String toString() { 109 return key + "=" + value; 110 } 111 } 112 }
哈希表實現:

1 public class HashMap<K,V> implements Map<K,V>{ 2 3 //===========================================成員屬性================================================ 4 /** 5 * 內部數組 6 * */ 7 private EntryNode<K,V>[] elements; 8 9 /** 10 * 當前哈希表的大小 11 * */ 12 private int size; 13 14 /** 15 * 負載因子 16 * */ 17 private float loadFactor; 18 19 /** 20 * 默認的哈希表容量 21 * */ 22 private final static int DEFAULT_CAPACITY = 16; 23 24 /** 25 * 擴容翻倍的基數 兩倍 26 * */ 27 private final static int REHASH_BASE = 2; 28 29 /** 30 * 默認的負載因子 31 * */ 32 private final static float DEFAULT_LOAD_FACTOR = 0.75f; 33 34 //========================================構造方法=================================================== 35 /** 36 * 默認構造方法 37 * */ 38 @SuppressWarnings("unchecked") 39 public HashMap() { 40 this.size = 0; 41 this.loadFactor = DEFAULT_LOAD_FACTOR; 42 elements = new EntryNode[DEFAULT_CAPACITY]; 43 } 44 45 /** 46 * 指定初始容量的構造方法 47 * @param capacity 指定的初始容量 48 * */ 49 @SuppressWarnings("unchecked") 50 public HashMap(int capacity) { 51 this.size = 0; 52 this.loadFactor = DEFAULT_LOAD_FACTOR; 53 elements = new EntryNode[capacity]; 54 } 55 56 /** 57 * 指定初始容量和負載因子的構造方法 58 * @param capacity 指定的初始容量 59 * @param loadFactor 指定的負載因子 60 * */ 61 @SuppressWarnings("unchecked") 62 public HashMap(int capacity,int loadFactor) { 63 this.size = 0; 64 this.loadFactor = loadFactor; 65 elements = new EntryNode[capacity]; 66 } 67 68 //==========================================內部輔助方法============================================= 69 /** 70 * 通過key的hashCode獲得對應的內部數組下標 71 * @param key 傳入的鍵值key 72 * @return 對應的內部數組下標 73 * */ 74 private int getIndex(K key){ 75 return getIndex(key,this.elements); 76 } 77 78 /** 79 * 通過key的hashCode獲得對應的內部數組插槽slot下標 80 * @param key 傳入的鍵值key 81 * @param elements 內部數組 82 * @return 對應的內部數組下標 83 * */ 84 private int getIndex(K key,EntryNode<K,V>[] elements){ 85 if(key == null){ 86 //::: null 默認存儲在第0個桶內 87 return 0; 88 }else{ 89 int hashCode = key.hashCode(); 90 91 //:::通過 高位和低位的異或運算,獲得最終的hash映射,減少碰撞的幾率 92 int finalHashCode = hashCode ^ (hashCode >>> 16); 93 return (elements.length-1) & finalHashCode; 94 } 95 } 96 97 /** 98 * 獲得目標節點的前一個節點 99 * @param currentNode 當前桶鏈表節點 100 * @param key 對應的key 101 * @return 返回當前桶鏈表中"匹配key的目標節點"的"前一個節點" 102 * 注意:當桶鏈表中不存在匹配節點時,返回桶鏈表的最后一個節點 103 * */ 104 private EntryNode<K,V> getTargetPreviousEntryNode(EntryNode<K,V> currentNode,K key){ 105 //:::不匹配 106 EntryNode<K,V> nextNode = currentNode.next; 107 //:::遍歷當前桶后面的所有節點 108 while(nextNode != null){ 109 //:::如果下一個節點的key匹配 110 if(nextNode.keyIsEquals(key)){ 111 return currentNode; 112 }else{ 113 //:::不斷指向下一個節點 114 currentNode = nextNode; 115 nextNode = nextNode.next; 116 } 117 } 118 119 //:::到達了桶鏈表的末尾,返回最后一個節點 120 return currentNode; 121 } 122 123 /** 124 * 哈希表擴容 125 * */ 126 @SuppressWarnings("unchecked") 127 private void reHash(){ 128 //:::擴容兩倍 129 EntryNode<K,V>[] newElements = new EntryNode[this.elements.length * REHASH_BASE]; 130 131 //:::遍歷所有的插槽 132 for (int i=0; i<this.elements.length; i++) { 133 //:::為單個插槽內的元素 rehash 134 reHashSlot(i,newElements); 135 } 136 137 //:::內部數組 ---> 擴容之后的新數組 138 this.elements = newElements; 139 } 140 141 /** 142 * 單個插槽內的數據進行rehash 143 * */ 144 private void reHashSlot(int index,EntryNode<K, V>[] newElements){ 145 //:::獲得當前插槽第一個元素 146 EntryNode<K, V> currentEntryNode = this.elements[index]; 147 if(currentEntryNode == null){ 148 //:::當前插槽為空,直接返回 149 return; 150 } 151 152 //:::低位桶鏈表 頭部節點、尾部節點 153 EntryNode<K, V> lowListHead = null; 154 EntryNode<K, V> lowListTail = null; 155 //:::高位桶鏈表 頭部節點、尾部節點 156 EntryNode<K, V> highListHead = null; 157 EntryNode<K, V> highListTail = null; 158 159 while(currentEntryNode != null){ 160 //:::獲得當前節點 在新數組中映射的插槽下標 161 int entryNodeIndex = getIndex(currentEntryNode.key,newElements); 162 //:::是否和當前插槽下標相等 163 if(entryNodeIndex == index){ 164 //:::和當前插槽下標相等 165 if(lowListHead == null){ 166 //:::初始化低位鏈表 167 lowListHead = currentEntryNode; 168 lowListTail = currentEntryNode; 169 }else{ 170 //:::在低位鏈表尾部拓展新的節點 171 lowListTail.next = currentEntryNode; 172 lowListTail = lowListTail.next; 173 } 174 }else{ 175 //:::和當前插槽下標不相等 176 if(highListHead == null){ 177 //:::初始化高位鏈表 178 highListHead = currentEntryNode; 179 highListTail = currentEntryNode; 180 }else{ 181 //:::在高位鏈表尾部拓展新的節點 182 highListTail.next = currentEntryNode; 183 highListTail = highListTail.next; 184 } 185 } 186 //:::指向當前插槽的下一個節點 187 currentEntryNode = currentEntryNode.next; 188 } 189 190 //:::新擴容elements(index)插槽 存放lowList 191 newElements[index] = lowListHead; 192 //:::lowList末尾截斷 193 if(lowListTail != null){ 194 lowListTail.next = null; 195 } 196 197 //:::新擴容elements(index + this.elements.length)插槽 存放highList 198 newElements[index + this.elements.length] = highListHead; 199 //:::highList末尾截斷 200 if(highListTail != null){ 201 highListTail.next = null; 202 } 203 } 204 205 /** 206 * 判斷是否需要 擴容 207 * */ 208 private boolean needReHash(){ 209 return ((this.size / this.elements.length) > this.loadFactor); 210 } 211 212 //============================================外部接口================================================ 213 214 @Override 215 public V put(K key, V value) { 216 if(needReHash()){ 217 reHash(); 218 } 219 220 //:::獲得對應的內部數組下標 221 int index = getIndex(key); 222 //:::獲得對應桶內的第一個節點 223 EntryNode<K,V> firstEntryNode = this.elements[index]; 224 225 //:::如果當前桶內不存在任何節點 226 if(firstEntryNode == null){ 227 //:::創建一個新的節點 228 this.elements[index] = new EntryNode<>(key,value); 229 //:::創建了新節點,size加1 230 this.size++; 231 return null; 232 } 233 234 if(firstEntryNode.keyIsEquals(key)){ 235 //:::當前第一個節點的key與之匹配 236 V oldValue = firstEntryNode.value; 237 firstEntryNode.value = value; 238 return oldValue; 239 }else{ 240 //:::不匹配 241 242 //:::獲得匹配的目標節點的前一個節點 243 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); 244 //:::獲得匹配的目標節點 245 EntryNode<K,V> targetNode = targetPreviousNode.next; 246 if(targetNode != null){ 247 //:::更新value的值 248 V oldValue = targetNode.value; 249 targetNode.value = value; 250 return oldValue; 251 }else{ 252 //:::在桶鏈表的末尾 新增一個節點 253 targetPreviousNode.next = new EntryNode<>(key,value); 254 //:::創建了新節點,size加1 255 this.size++; 256 return null; 257 } 258 } 259 } 260 261 @Override 262 public V remove(K key) { 263 //:::獲得對應的內部數組下標 264 int index = getIndex(key); 265 //:::獲得對應桶內的第一個節點 266 EntryNode<K,V> firstEntryNode = this.elements[index]; 267 268 //:::如果當前桶內不存在任何節點 269 if(firstEntryNode == null){ 270 return null; 271 } 272 273 if(firstEntryNode.keyIsEquals(key)){ 274 //:::當前第一個節點的key與之匹配 275 276 //:::將桶鏈表的第一個節點指向后一個節點(兼容next為null的情況) 277 this.elements[index] = firstEntryNode.next; 278 //:::移除了一個節點 size減一 279 this.size--; 280 //:::返回之前的value值 281 return firstEntryNode.value; 282 }else{ 283 //:::不匹配 284 285 //:::獲得匹配的目標節點的前一個節點 286 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); 287 //:::獲得匹配的目標節點 288 EntryNode<K,V> targetNode = targetPreviousNode.next; 289 290 if(targetNode != null){ 291 //:::將"前一個節點的next" 指向 "目標節點的next" ---> 相當於將目標節點從桶鏈表移除 292 targetPreviousNode.next = targetNode.next; 293 //:::移除了一個節點 size減一 294 this.size--; 295 return targetNode.value; 296 }else{ 297 //:::如果目標節點為空,說明key並不存在於哈希表中 298 return null; 299 } 300 } 301 } 302 303 @Override 304 public V get(K key) { 305 //:::獲得對應的內部數組下標 306 int index = getIndex(key); 307 //:::獲得對應桶內的第一個節點 308 EntryNode<K,V> firstEntryNode = this.elements[index]; 309 310 //:::如果當前桶內不存在任何節點 311 if(firstEntryNode == null){ 312 return null; 313 } 314 315 if(firstEntryNode.keyIsEquals(key)){ 316 //:::當前第一個節點的key與之匹配 317 return firstEntryNode.value; 318 }else{ 319 //:::獲得匹配的目標節點的前一個節點 320 EntryNode<K,V> targetPreviousNode = getTargetPreviousEntryNode(firstEntryNode,key); 321 //:::獲得匹配的目標節點 322 EntryNode<K,V> targetNode = targetPreviousNode.next; 323 324 if(targetNode != null){ 325 return targetNode.value; 326 }else{ 327 //:::如果目標節點為空,說明key並不存在於哈希表中 328 return null; 329 } 330 } 331 } 332 333 @Override 334 public boolean containsKey(K key) { 335 V value = get(key); 336 return (value != null); 337 } 338 339 @Override 340 public boolean containsValue(V value) { 341 //:::遍歷全部桶鏈表 342 for (EntryNode<K, V> element : this.elements) { 343 //:::獲得當前桶鏈表第一個節點 344 EntryNode<K, V> entryNode = element; 345 346 //:::遍歷當前桶鏈表 347 while (entryNode != null) { 348 //:::如果value匹配 349 if (entryNode.value.equals(value)) { 350 //:::返回true 351 return true; 352 } else { 353 //:::不匹配,指向下一個節點 354 entryNode = entryNode.next; 355 } 356 } 357 } 358 359 //:::所有的節點都遍歷了,沒有匹配的value 360 return false; 361 } 362 363 @Override 364 public int size() { 365 return this.size; 366 } 367 368 @Override 369 public boolean isEmpty() { 370 return (this.size == 0); 371 } 372 373 @Override 374 public void clear() { 375 //:::遍歷內部數組,將所有桶鏈表全部清空 376 for(int i=0; i<this.elements.length; i++){ 377 this.elements[i] = null; 378 } 379 380 //:::size設置為0 381 this.size = 0; 382 } 383 384 @Override 385 public Iterator<EntryNode<K,V>> iterator() { 386 return new Itr(); 387 } 388 389 @Override 390 public String toString() { 391 Iterator<EntryNode<K,V>> iterator = this.iterator(); 392 393 //:::空容器 394 if(!iterator.hasNext()){ 395 return "[]"; 396 } 397 398 //:::容器起始使用"[" 399 StringBuilder s = new StringBuilder("["); 400 401 //:::反復迭代 402 while(true){ 403 //:::獲得迭代的當前元素 404 EntryNode<K,V> data = iterator.next(); 405 406 //:::判斷當前元素是否是最后一個元素 407 if(!iterator.hasNext()){ 408 //:::是最后一個元素,用"]"收尾 409 s.append(data).append("]"); 410 //:::返回 拼接完畢的字符串 411 return s.toString(); 412 }else{ 413 //:::不是最后一個元素 414 //:::使用", "分割,拼接到后面 415 s.append(data).append(", "); 416 } 417 } 418 } 419 420 /** 421 * 哈希表 迭代器實現 422 */ 423 private class Itr implements Iterator<EntryNode<K,V>> { 424 /** 425 * 迭代器 當前節點 426 * */ 427 private EntryNode<K,V> currentNode; 428 429 /** 430 * 迭代器 下一個節點 431 * */ 432 private EntryNode<K,V> nextNode; 433 434 /** 435 * 迭代器 當前內部數組的下標 436 * */ 437 private int currentIndex; 438 439 /** 440 * 默認構造方法 441 * */ 442 private Itr(){ 443 //:::如果當前哈希表為空,直接返回 444 if(HashMap.this.isEmpty()){ 445 return; 446 } 447 //:::在構造方法中,將迭代器下標移動到第一個有效的節點上 448 449 //:::遍歷內部數組,找到第一個不為空的數組插槽slot 450 for(int i=0; i<HashMap.this.elements.length; i++){ 451 //:::設置當前index 452 this.currentIndex = i; 453 454 EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; 455 //:::找到了第一個不為空的插槽slot 456 if(firstEntryNode != null){ 457 //:::nextNode = 當前插槽第一個節點 458 this.nextNode = firstEntryNode; 459 460 //:::構造方法立即結束 461 return; 462 } 463 } 464 } 465 466 @Override 467 public boolean hasNext() { 468 return (this.nextNode != null); 469 } 470 471 @Override 472 public EntryNode<K,V> next() { 473 this.currentNode = this.nextNode; 474 //:::暫存需要返回的節點 475 EntryNode<K,V> needReturn = this.nextNode; 476 477 //:::nextNode指向自己的next 478 this.nextNode = this.nextNode.next; 479 //:::判斷當前nextNode是否為null 480 if(this.nextNode == null){ 481 //:::說明當前所在的桶鏈表已經遍歷完畢 482 483 //:::尋找下一個非空的插槽 484 for(int i=this.currentIndex+1; i<HashMap.this.elements.length; i++){ 485 //:::設置當前index 486 this.currentIndex = i; 487 488 EntryNode<K,V> firstEntryNode = HashMap.this.elements[i]; 489 //:::找到了后續不為空的插槽slot 490 if(firstEntryNode != null){ 491 //:::nextNode = 當前插槽第一個節點 492 this.nextNode = firstEntryNode; 493 //:::跳出循環 494 break; 495 } 496 } 497 } 498 return needReturn; 499 } 500 501 @Override 502 public void remove() { 503 if(this.currentNode == null){ 504 throw new IteratorStateErrorException("迭代器狀態異常: 可能在一次迭代中進行了多次remove操作"); 505 } 506 507 //:::獲得需要被移除的節點的key 508 K currentKey = this.currentNode.key; 509 //:::將其從哈希表中移除 510 HashMap.this.remove(currentKey); 511 512 //:::currentNode設置為null,防止反復調用remove方法 513 this.currentNode = null; 514 } 515 } 516 }
哈希表簡單的測試代碼:

1 public class MapTest { 2 public static void main(String[] args){ 3 testJDKHashMap(); 4 5 System.out.println("================================================="); 6 7 testMyHashMap(); 8 } 9 10 private static void testJDKHashMap(){ 11 java.util.Map<Integer,String> map1 = new java.util.HashMap<>(1,2); 12 System.out.println(map1.put(1,"aaa")); 13 System.out.println(map1.put(2,"bbb")); 14 System.out.println(map1.put(3,"ccc")); 15 System.out.println(map1.put(1,"aaa")); 16 System.out.println(map1.put(2,"bbb")); 17 System.out.println(map1.put(3,"ccc")); 18 System.out.println(map1.put(1,"111")); 19 System.out.println(map1.put(3,"aaa")); 20 System.out.println(map1.put(4,"ddd")); 21 System.out.println(map1.put(5,"eee")); 22 System.out.println(map1.put(6,"fff")); 23 System.out.println(map1.put(8,"ggg")); 24 System.out.println(map1.put(11,"bbb")); 25 System.out.println(map1.put(22,"ccc")); 26 System.out.println(map1.put(33,"111")); 27 System.out.println(map1.put(9,"111")); 28 System.out.println(map1.put(10,"111")); 29 System.out.println(map1.put(12,"111")); 30 System.out.println(map1.put(13,"111")); 31 System.out.println(map1.put(14,"111")); 32 33 System.out.println(map1.toString()); 34 System.out.println(map1.containsKey(1)); 35 System.out.println(map1.containsKey(11)); 36 System.out.println(map1.containsValue("bbb")); 37 System.out.println(map1.containsValue("aaa")); 38 System.out.println(map1.size()); 39 System.out.println(map1.get(1)); 40 System.out.println(map1.get(2)); 41 System.out.println(map1.get(3)); 42 System.out.println(map1.remove(1)); 43 System.out.println(map1.remove(2)); 44 System.out.println(map1.size()); 45 46 } 47 48 private static void testMyHashMap(){ 49 com.xiongyx.datastructures.map.Map<Integer,String> map2 = new com.xiongyx.datastructures.map.HashMap<>(1,2); 50 System.out.println(map2.put(1,"aaa")); 51 System.out.println(map2.put(2,"bbb")); 52 System.out.println(map2.put(3,"ccc")); 53 System.out.println(map2.put(1,"aaa")); 54 System.out.println(map2.put(2,"bbb")); 55 System.out.println(map2.put(3,"ccc")); 56 System.out.println(map2.put(1,"111")); 57 System.out.println(map2.put(3,"aaa")); 58 System.out.println(map2.put(4,"ddd")); 59 System.out.println(map2.put(5,"eee")); 60 System.out.println(map2.put(6,"fff")); 61 System.out.println(map2.put(8,"ggg")); 62 System.out.println(map2.put(11,"bbb")); 63 System.out.println(map2.put(22,"ccc")); 64 System.out.println(map2.put(33,"111")); 65 System.out.println(map2.put(9,"111")); 66 System.out.println(map2.put(10,"111")); 67 System.out.println(map2.put(12,"111")); 68 System.out.println(map2.put(13,"111")); 69 System.out.println(map2.put(14,"111")); 70 71 System.out.println(map2.toString()); 72 System.out.println(map2.containsKey(1)); 73 System.out.println(map2.containsKey(11)); 74 System.out.println(map2.containsValue("bbb")); 75 System.out.println(map2.containsValue("aaa")); 76 System.out.println(map2.size()); 77 System.out.println(map2.get(1)); 78 System.out.println(map2.get(2)); 79 System.out.println(map2.get(3)); 80 System.out.println(map2.remove(1)); 81 System.out.println(map2.remove(2)); 82 System.out.println(map2.size()); 83 } 84 }
我們的哈希表實現是demo級別的,功能簡單,也比較好理解,希望這能夠成為大家理解更加復雜的產品級哈希表實現的一個跳板。在理解了demo級別代碼的基礎之上,去閱讀更加復雜的產品級實現代碼,更好的理解哈希表,更好的理解自己所使用的數據結構,寫出更高效,易維護的程序。
本系列博客的代碼在我的 github上:https://github.com/1399852153/DataStructures ,存在許多不足之處,請多多指教。