建議收藏!細說HashMap實現,Hash衝突模擬思路講解。
思路記錄:
1.HashMap主體是一個Entry陣列
,當多個key
值經過hash計算得到的索引
一致,則發生Hash衝突
,該索引上的Entry物件就會成為連結串列
的頭結點,後續同索引的Entry將會插入該連結串列。
2.當進行刪除時,應考慮如果目標物件在連結串列中且還有子節點
,就需要按照連結串列的刪除法進行,防止後續物件丟失。假設father為目標物件的前一個結節。則father.next = father.next.next
3.擴容對於新舊兩個陣列以及索引
需要理清思路,並且在put
,get
,remove
的程式碼裡分清值傳遞與引用傳遞
,防止自以為物件追加進連結串列其實沒有。
4.程式碼每個關鍵處都有註釋,閱讀前需對HashMap的基本特點心中有大概的理解。
程式碼實現:
介面類
對於每個操作的hashConfictIndex
引數作用,因為能引起hash衝突
的情況比較難找到,所以追加了這個引數,傳入-1
就按正常情況呼叫hash
方法對陣列規模取餘計算索引
,如果我們想給索引5的位置追加物件來模仿雜湊衝突
,就需要傳入5
來模擬。這種模擬的物件在get
和remove
時也需要我們手動傳入同index,因為正常的hash計算根據我們給的key並不一定
能求得我們插入時設定的索引。
package com.lx.myhashmap; public interface MyHashMapInter { public String get(String key,int hashConfictIndex); public void put(String key,String value,int hashConfictIndex); public void remove(String key,int hashConfictIndex); interface MyEntryInter{ public String get(); public void set(); } }
實現類
在每個關鍵處都有註釋以及控制檯輸出,如果看著不清楚可以複製程式碼執行跟斷點走,記得修改包名com.xx.xxx
package com.lx.myhashmap; import java.util.Map; public class MyHashMap implements MyHashMapInter { //初始容量 private int initCapacity; //已放入元素 public int counts; //Entry陣列 MyEntry[] table; //擴容係數 private double loadFactory = 0.75; public MyHashMap(int initCapacity, double loadFactory) { this.initCapacity = initCapacity; this.loadFactory = loadFactory; table = new MyEntry[initCapacity]; } @Override public String get(String key, int hashConflictIndex) { int index; if (-1 == hashConflictIndex) { index = hash(key) % initCapacity; } else { index = hashConflictIndex; } //陣列該索引處空 if (table[index] == null) { return null; } //key相同 if (table[index].key.equals(key)) { return table[index].value; } //遍歷連結串列 MyEntry temp = table[index]; while (temp.next != null) { temp = temp.next; if (temp.key.equals(key)) { return temp.value; } } return null; } @Override public void put(String key, String value, int hashConflictIndex) { int index; //計算該key索引 if (-1 == hashConflictIndex) { index = hash(key) % initCapacity; } else { index = hashConflictIndex; } System.out.println("計算得到索引為 " + index); if (null == table[index]) {//如果該索引上無物件 table[index] = new MyEntry(key, value, null); counts++; reLoad(); System.out.println("put: 在陣列中新增"); } else {//如果已有物件 //如果key相同 if (table[index].key.equals(key)) { table[index].value = value; System.out.println("put: 覆蓋陣列物件"); } else {//遍歷連結串列 System.out.println("put: 進入連結串列"); MyEntry temp = table[index]; MyEntry father = temp; while (temp.next != null) { father = temp; temp = temp.next; if (temp.key.equals(key)) { temp.value = value; System.out.println("put: 連結串列物件覆蓋"); return; } } temp.next = new MyEntry(key, value, null); counts++; reLoad(); System.out.println("put: 連結串列尾追加"); } } } @Override public void remove(String key, int hashConflictIndex) { int index; if (-1 == hashConflictIndex) { index = hash(key) % initCapacity; } else { index = hashConflictIndex; } if (table[index] == null) { System.out.println("remove:沒有該值"); } if (table[index].key.equals(key)) { if (table[index].next == null) { table[index] = null; System.out.println("remove: 陣列中找到了 刪除"); return; } else { table[index] = table[index].next; System.out.println("remove: 找到了 刪除 該鏈下一元素補位"); return; } } MyEntry temp = table[index]; while (temp.next != null) { MyEntry father = temp; temp = temp.next; if (temp.key.equals(key)) { if (temp.next == null) { father.next = null; System.out.println("remove: 連結串列中已經找到,刪除"); } else { father.next = father.next.next; System.out.println("remove: 連結串列中已經找到,刪除並連結兩端"); } return; } } System.out.println("remove: 已遍歷完連結串列,沒有該值"); } /* 雜湊方法 */ private int hash(String key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } private void reLoad() { double top = initCapacity * loadFactory; if (counts >= top) { System.out.println("reload: 達到閾值,擴容 "); initCapacity = 2 * initCapacity; MyEntry[] oldTable = table; table = new MyEntry[initCapacity]; counts = 0; for(int i = 0; i < oldTable.length; i++) { MyEntry entry = oldTable[i]; while (entry != null) { put(entry.key, entry.value, -1); entry = entry.next; } } } } class MyEntry implements MyEntryInter { public String key; public String value; public MyEntry next; public MyEntry(String key, String value, MyEntry next) { this.key = key; this.value = value; this.next = next; } @Override public String get() { return value; } @Override public void set() { this.value = value; } } }
測試案例
1.陣列覆蓋
程式碼:
package com.lx.myhashmap;
import java.util.HashMap;
public class Demo {
public static void main(String[] args) {
MyHashMap map = new MyHashMap(8,0.75);
map.put("1","hello",-1);
System.out.println("------------------------------");
System.out.println("值為:"+map.get("1",-1)+"\n----------------");
map.put("1","world",-1);
System.out.println("------------------------------");
System.out.println("值為:"+map.get("1",-1)+"\n----------------");
}
}
輸出:
計算得到索引為 1
put: 在陣列中新增
------------------------------
值為:hello
----------------
計算得到索引為 1
put: 覆蓋陣列物件
------------------------------
值為:world
----------------
2.連結串列追加
這裡我們不傳-1
,而是傳入索引1來模擬雜湊衝突(假設也計算出了1)
程式碼:
package com.lx.myhashmap;
import java.util.HashMap;
public class Demo {
public static void main(String[] args) {
MyHashMap map = new MyHashMap(8,0.75);
map.put("1","hello",-1);
System.out.println("------------------------------");
System.out.println("值為:"+map.get("1",-1)+"\n----------------");
map.put("3","world",1);
System.out.println("------------------------------");
System.out.println("值為:"+map.get("3",1)+"\n----------------");
}
}
輸出:
計算得到索引為 1
put: 在陣列中新增
------------------------------
值為:hello
----------------
計算得到索引為 1
put: 進入連結串列
put: 連結串列尾追加
------------------------------
值為:world
----------------
3.擴容
程式碼:
package com.lx.myhashmap;
import java.util.HashMap;
public class Demo {
public static void main(String[] args) {
MyHashMap map = new MyHashMap(8,0.75);
map.put("1","hello",-1);
map.put("2","hello",-1);
map.put("3","hello",1);
map.put("4","hello",1);
map.put("5","hello",-1);
map.put("6","hello",-1);
map.put("7","hello",-1);
}
}
輸出:
計算得到索引為 1
put: 在陣列中新增
計算得到索引為 2
put: 在陣列中新增
計算得到索引為 1
put: 進入連結串列
put: 連結串列尾追加
計算得到索引為 1
put: 進入連結串列
put: 連結串列尾追加
計算得到索引為 5
put: 在陣列中新增
計算得到索引為 6
reload: 達到閾值,擴容
計算得到索引為 1
put: 在陣列中新增
計算得到索引為 3
put: 在陣列中新增
計算得到索引為 4
put: 在陣列中新增
計算得到索引為 2
put: 在陣列中新增
計算得到索引為 5
put: 在陣列中新增
計算得到索引為 6
put: 在陣列中新增
put: 在陣列中新增
計算得到索引為 7
put: 在陣列中新增
4.元素在連結串列中間的刪除
這是最特殊的地方,刪除不能簡單的設空,需要考慮鏈會不會斷掉。
程式碼:
package com.lx.myhashmap;
import java.util.HashMap;
public class Demo {
public static void main(String[] args) {
MyHashMap map = new MyHashMap(8,0.75);
map.put("1","hello",-1);
map.put("3","hello",1);
map.put("4","hello",1);
System.out.println(map.get("3",1));
map.remove("3",1);
System.out.println(map.get("3",1));
}
}
輸出:
計算得到索引為 1
put: 在陣列中新增
計算得到索引為 1
put: 進入連結串列
put: 連結串列尾追加
計算得到索引為 1
put: 進入連結串列
put: 連結串列尾追加
hello
remove: 連結串列中已經找到,刪除並連結兩端
null
總結:
主要還是學習思路,目前剩餘連結串列size大於8轉紅黑樹的功能待實現,因為樹的體系比較複雜才學到二叉查詢樹,直接跳過去學紅黑樹不現實,後面等可以寫出紅黑樹了再回頭把這裡完善。