Java - How to implement basic HashMap by hashing, chaining

簡單說,當我們利用Java提供的HashMap可以實作出key => value對應的關係,而且它的

效率很高,號稱O(1)耶!!,簡直就像一個array,若字串"Ben"在array[500]的address,假設

你知道array[500] => "Ben" (鬼才會去記它在哪XD),也是O(1)。但若你事先不知道,就一定要

利用迴圈走訪了,此時O(n) (array length),才有可能得到"Ben"的值,關鍵在於沒有關鍵的key

阿! 由於array只能存integer,因此我們可以透過hash function幫我們計算出hash table存放Entry

(組成key and value)的位置,如此一來以後只要將key轉換成table的index,就可以快速查詢到

value囉!

請見下圖(來源: wiki)

圖一


不過,世事難料,有些key在經由hash function的轉換,會得到相同的index,此時若是key也相

等,很簡單就覆蓋掉舊的值,但若是不同的該怎麼辦?總不能不管他吧?因此可以透過

chaining的方式,就大家很熟悉的link list來做個串接,如下圖(來源: wiki)

圖二

如此一來,就算是index相同,也可以保留第二筆資料,不過,以後您若是想得到一個

index內的某筆資料,可不是table當下所存的object address就是你要的,還必須去檢查這

個object的key是否與你所要搜尋的key是相等的,意思就是還是要走訪這個object,直到它指

向null為止!

如圖二:若我要搜尋Sandra Dee所對應的value,經由hash function計算得到index = 152,但此

時buckets 152 address存放的entry為John Smith,所以key不相等,就必須走訪至他所串接的下

一筆資料(由於它的下一筆不指向null),這時才會得到我們要的!

因此,當我們在使用Java提供的HashMap,其實它的內部也做了這些基本的功能,包括

hash function去找index、及chaining Entry等,但其實還包括transfer (若table在使用上超過了原本

預設的size,程式還可以自動擴充table!)。這裡只會舉很簡單的put(K k, V v)、get(Object o)

、remove(Object Key)等基本的method!

接下來的部分,會從HashMap source code獨立出來的幾個method做說明,主要的概念與做

法主要源自於source code,非本人所自創orz...


1. define MyMapEntry class

在MyHashMap class內我會定義這個內部類別,這個類別的意義就是建立一個Entry,表示

key與value所組成的基本原件!,如下:

  1. class MyMapEntry<K, V> implements Map.Entry<K, V>{
  2. int hash;
  3. K key;
  4. V value;
  5. MyMapEntry<K, V> next;
  6. public MyMapEntry(int h, K k, V v, MyMapEntry<K, V> n){
  7. hash = h;
  8. key = k;
  9. value = v;
  10. next = n;
  11. }
  12. @Override
  13. public K getKey() {
  14. // TODO Auto-generated method stub
  15. return key;
  16. }
  17. @Override
  18. public V getValue() {
  19. // TODO Auto-generated method stub
  20. return value;
  21. }
  22. @Override
  23. public V setValue(V newValue) {
  24. // TODO Auto-generated method stub
  25. V oldValue = value;
  26. value = newValue;
  27. return oldValue;
  28. }
  29. public String toString() {
  30. return getKey() + " => "+getValue();
  31. }
  32. }

基本上,這個類別所產生的物件實體會assign到table對應的位置,並且也定義了成員變數next

,來作為當有多個Entry皆存在於同一個table的位置時,可做一個Entry object串接另一個Entry

 object的紀錄!(若只有一個Entry,則next指到null)

2. define MyHashMap class

這個類別為外部類別使用的窗口,如同HashMap class,如下:

  1. class MyHashMap<K, V>{
  2. private MyMapEntry<K, V> tables[];
  3. private final int DEFAULT_OPACITY = 16;
  4. @SuppressWarnings("unchecked")
  5. public MyHashMap(){
  6. tables = new MyMapEntry[DEFAULT_OPACITY];
  7. }
  8. ...
  9. }

當你new MyHashMap後,此時會建立一個MyHashMap型態的tables陣列,預設長度為16!

3. add an Entry

假設在某個外部類別,您要使用MyHashMap,你得先定義您的key, value型態

  1. String str1 = "123";
  2. String str2 = "321";
  3. MyHashMap<String, Integer> map = new MyHashMap<String, Integer>();
  4. map.put(str1, 10);
  5. map.put(str2, 100);

當您呼叫put method,傳進key, value值,此時put method內會先呼叫indexFor及createEntry,

如下:

  1. private int indexFor(int hash, int len){
  2. return hash & (len - 1);
  3. }
  4. public V put(K k, V v){
  5. int hash = k.hashCode();
  6. int i = indexFor(hash, tables.length);
  7. System.out.println("put key="+k+", value="+v);
  8. if(tables[i] != null){
  9. System.out.println("table["+i+"] != null, Filter old key and new key are same?");
  10. for(MyMapEntry<K, V> e = tables[i]; e != null ; e = e.next){
  11. if(e.hash == hash && (k.equals(e.key))){
  12. System.out.println("Old key and new key are same! Override old key's value");
  13. V oldValue = e.value;
  14. e.setValue(v);
  15. return oldValue;
  16. }
  17. }
  18. System.out.println("Old key and new key are different!");
  19. }
  20. System.out.println("Create new Entry, and assign to table["+i+"]");
  21. createEntry(hash, k, v, i);
  22. return null;
  23. }
  24. private void createEntry(int h, K k, V v, int index){
  25. MyMapEntry<K, V> entry = tables[index];
  26. tables[index] = new MyMapEntry<K, V>(h, k, v, entry);
  27. System.out.println("Prepend new Entry("+k+") to current Entry("+entry+")");
  28. }

首先,它會先將key轉換成hashCode,hashCode如何計算?

假設目前傳進去的key="123",套用Java hashCode method的計算公式為

h = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] //s[] 字元陣列, n 陣列長度

字元'1' =>  ascii code為49,以此類推2 => 50 ...

那麼換算後"123"的 h = 48690, 二進位為1011 1110 0011 00102

此時呼叫indexFor method,將hash value與table length-1 (15)做AND運算,如下

   1011 1110 0011 0010
& 0000 0000 0000 1111
----------------------------------
    0000 0000 0000 0010 => 十進位為2

得到的tables index = 2,表示這個key, value組成的Entry將assign to tables[2]

再來檢查此時的tables[2]是否指向null (default initialize value)

由於是第一筆(肯定為null),接下來就再去呼叫createEntry method

首先宣告的entry變數會先取得現階段tables[2] 指向的位址,以當作new MyMapEntry所指向

的目標,如此一來,假設有第二個Entry剛好也是在tables[2],它將會是在前頭,第一個

Entry會在它的後頭,表示越慢進來的在越前面!

再來又put第二筆,key = "321"

換算後得到 h = 50610, 二進位為1100 0101 1011 00102

此時呼叫indexFor method進行運算

   1100 0101 1011 0010
& 0000 0000 0000 1111
----------------------------------
    0000 0000 0000 0010 => 十進位為2

再來的第二筆data,index也是2,此時tables[2]已不是指向null了,這時就會發生碰撞的問題

,總不能將已有的Entry給取代掉,因此會進入if 內,進行tables[2]下所擁有的Entry走訪

for(MyMapEntry<K, V> e = tables[i]; e != null ; e = e.next){ ... }

假設取得的key與現有的key不僅hash相等,同時內容本身也相等,那麼新的key對應的value

會做覆蓋舊value的動作! 否則, 就像前面一樣去呼叫createEntry method!

一樣地,先取得現有的Entry (非null),帶進new MyHashEntry內,此時新的Entry's next變數會

指向舊的Entry reference address。

此時key - "321"如同圖二的"John Smith",而key - "123"如同圖二的"Sandra Dee"

4. get mapping value by key

這邊要說明的是如何取得某個key對應的值。

  1. public V get(Object key){
  2. int hash = key.hashCode();
  3. int i = indexFor(hash, tables.length);
  4. V value = null;
  5. if(tables[i] != null){
  6. for(MyMapEntry<K, V> e = tables[i]; e != null ; e = e.next){
  7. if(e.hash == hash && (key.equals(e.key))){
  8. value = e.getValue();
  9. }
  10. }
  11. }
  12. return value;
  13. }

一樣在找到對應的tables index時,檢查是否指到null,因此當你在使用map.get(key)時,若是

不存在的key會得到null的回傳值。但是,當確定這個key是有put進去的時候,實際上,可不

一定是tables[index]就是你要搜尋的Entry,還必須進一步的走訪檢查,hash值與key本身是否

相符。

5. remove Entry by key

這個method將會去remove掉key所組成的Entry。這個method內的code主要由本人撰寫,若有誤

請不吝指正orz

  1. public V remove(Object key){
  2. int hash = key.hashCode();
  3. int i = indexFor(hash, tables.length);
  4. if(tables[i] != null){
  5. MyMapEntry<K, V> e = tables[i];
  6. MyMapEntry<K, V> prev = e;
  7. while(e != null){
  8. if(e.hash == hash && e.key.equals(key)){
  9. if(prev == e){
  10. tables[i] = e.next;
  11. }else{
  12. prev.next = e.next;
  13. }
  14. return e.value;
  15. }
  16. prev = e;
  17. e = e.next;
  18. }
  19. }
  20. return null;
  21. }
當你指定要刪除某個key時,一樣地要去走訪,此時可不是把找到的key刪掉就沒你的事囉!

還必須將當下刪掉的Entry,將它的前一個Entry指向它的下一個Entry!

例如:

剛剛的例子我有新增兩筆Entry,剛好都在tables[2]的address內。如下:

tables[2] => key -"321"本身

key -"321"的 next => key "123"本身

key "123"的next => null

假設我要刪除key "123",那麼我會宣告兩個變數e, prev

一開始e => tables[2] , prev => e

此時e.hash不等於要刪除key的hash,且"321" not equals "123" => 不成立

因此,prev = e,即prev繼續指向"321"本身,而e = e.next,即e指向"123"本身

再來e.hash等於要刪除key的hash,且"123" equals "123" => 成立

如此執行prev.next = e.next,表示"321"的next => "123"的next ,為null,會變成

tables[2] => key -"321"本身

key -"321"的 next => null

key "123"的next => null,這時就沒有任何一個object指向"123"(好孤單XD)


結論

1. 上面探討的概念為參考Java HashMap實作的方法,只舉put , get , remove等基本的method

2. 裡面所列舉method,有簡化部分內容,如實際的hashMap在put時還會檢查tables size是否

已超過定義的size (預設為16),若超過HashMap還會自動擴充!!,並做資料移轉, 這邊就沒

提到orz...

3. 以上的解說,純為個人的見解,若有誤請不吝指正XD

留言