1. 程式人生 > >從STL容器的使用對比,研究hashmap…

從STL容器的使用對比,研究hashmap…

java和C++裡面都有hashmap。

C++ 中容器的一些對比

1、你的百萬級的資料放到vector不大合適。因為vector需要連續的記憶體空間,顯然在初始化這個容器的時候會花費很大的容量。 (vector就相當於下面說的陣列,定址很容易)

2、如果你需要在資料中間進行插入,list 是最好的選擇,vector   的插入效率會讓你痛苦得想死。

Llist型別的使鏈式關係(list就相當於下面說的連結串列),插入非常快,但是查詢卻費時,需要遍歷。

3、涉及到查詢的話用map比較好,因為map的內部資料結構用rb-tree實現,而用vector你只能用線性查詢,效率很低。

C++裡面map使用紅黑樹實現。其查詢操作是O(logN)的,

hash_map的查詢,內部是通過一個從key到value的運算函式來實現的,這個函式“只接受key作為引數”,也就是說,hash_map的查詢 演算法與資料量無關,所以認為它是O(1)級的。

不過網上貌似對C++的hashmap 的具體實現描述的不是太多,java的多,不過實現原理是基本一樣的,所以研究一下。

下面是java裡的hashmap實現過程。

http://zhangshixi.iteye.com/blog/672697 http://blog.csdn.net/vking_wang/article/details/14166593

1. HashMap的資料結構

資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。

      陣列

陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難

連結串列

連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易。

雜湊表

那麼我們能不能綜合兩者的特性,做出一種定址容易,插入刪除也容易的資料結構?答案是肯定的,這就是我們要提起的雜湊表。雜湊表((Hash table既滿足了資料的查詢方便,同時不佔用太多的內容空間,使用也十分方便。

  雜湊表有多種不同的實現方法,我接下來解釋的是最常用的一種方法—— 拉鍊法,我們可以理解為“連結串列的陣列” ,如圖:

從STL容器的使用對比,研究hashmap的實現

從上圖我們可以發現雜湊表是由陣列+連結串列組成的,一個長度為16的陣列中,每個元素儲存的是一個連結串列的頭結點。那麼這些元素是按照什麼樣的規則儲存到陣列中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的雜湊值對陣列長度取模得到。比如上述雜湊表中,12=12,28=12,108=12,140=12。所以12、28、108以及140都儲存在陣列下標為12的位置。

  HashMap其實也是一個線性的陣列實現的,所以可以理解為其儲存資料的容器就是一個線性陣列。這可能讓我們很不解,一個線性的陣列怎麼實現按鍵值對來存取資料呢?這裡HashMap有做一些處理。

  首先HashMap裡面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性陣列,這個陣列就是Entry[],Map裡面的內容都儲存在Entry[]裡面。

    transient Entry[] table;

2. HashMap的存取實現

     既然是線性陣列,為什麼能隨機存取?這裡HashMap用了一個小演算法,大致是這樣實現:

// 儲存時:
int hash = key.hashCode(); // 這個hashCode方法這裡不詳述,只要理解每個key的hash是一個固定的int值
int index = hash % Entry[].length;
Entry[index] = value;

// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];

1)put

疑問:如果兩個key通過hash%Entry[].length得到的index相同,會不會有覆蓋的危險?

  這裡HashMap裡面用到鏈式資料結構的一個概念。上面我們提到過Entry類裡面有一個next屬性,作用是指向下一個Entry。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會後又進來一個鍵值對B,通過計算其index也等於0,現在怎麼辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起。所以疑問不用擔心。也就是說陣列中儲存的是最後插入的元素。到這裡為止,HashMap的大致實現,我們應該已經清楚了。

從上圖中可以看出,HashMap底層就是一個數組結構,陣列中的每一項又是一個連結串列。當新建一個HashMap的時候,就會初始化一個數組。

   原始碼如下:

Java程式碼  收藏程式碼
  1. transient Entry[] table;  
  2. static class Entry implements Map.Entry {  
  3.     final K key;  
  4.     V value;  
  5.     Entry next;  
  6.     final int hash;  
  7.     ……  
  8. }  

   可以看出,Entry就是陣列中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了連結串列。

3.    HashMap的存取實現:

   1) 儲存:

Java程式碼  收藏程式碼
  1. public V put(K key, V value) {  
  2.     // HashMap允許存放null鍵和null值。  
  3.     // 當key為null時,呼叫putForNullKey方法,將value放置在陣列第一個位置。  
  4.     if (key == null)  
  5.         return putForNullKey(value);  
  6.     // 根據key的keyCode重新計算hash值。  
  7.     int hash = hash(key.hashCode());  
  8.     // 搜尋指定hash值在對應table中的索引。  
  9.     int i = indexFor(hash, table.length);  
  10.     // 如果 i 索引處的 Entry 不為 null,通過迴圈不斷遍歷 e 元素的下一個元素。  
  11.     for (Entry e = table[i]; e != null; e = e.next) {  
  12.         Object k;  
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.             V oldValue = e.value;  
  15.             e.value = value;  
  16.             e.recordAccess(this);  
  17.             return oldValue;  
  18.         }  
  19.     }  
  20.     // 如果i索引處的Entry為null,表明此處還沒有Entry。  
  21.     modCount++;  
  22.     // 將key、value新增到i索引處。  
  23.     addEntry(hash, key, value, i);  
  24.     return null;  
  25. }  

   從上面的原始碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在陣列中的位置(即下標),如果陣列該位置上已經存放有其他元素了,那麼在這個位置上的元素將以連結串列的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置上。

2)get

 public V get(Object key) {         if (key == null)             return getForNullKey();         int hash = hash(key.hashCode());         //先定位到陣列元素,再遍歷該元素處的連結串列         for (Entry e = table[indexFor(hash, table.length)];              e != null;              e = e.next) {             Object k;             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                 return e.value;         }         return null; }

   有了上面儲存時的hash演算法作為基礎,理解起來這段程式碼就很容易了。從上面的原始碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到陣列中對應位置的某一元素,然後通過key的equals方法在對應位置的連結串列中找到需要的元素。

歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 物件。HashMap 底層採用一個 Entry[] 陣列來儲存所有的 key-value 對,當需要儲存一個 Entry 物件時,會根據hash演算法來決定其在陣列中的儲存位置,在根據equals方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Entry時,也會根據hash演算法找到其在陣列中的儲存位置,再根據equals方法從該位置上的連結串列中取出該Entry。

3. 解決hash衝突的辦法

  1. 開放定址法(線性探測再雜湊,二次探測再雜湊,偽隨機探測再雜湊)
  2. 再雜湊法
  3. 鏈地址法
  4. 建立一個公共溢位區

Java中hashmap的解決辦法就是採用的鏈地址法。

4. 再雜湊rehash過程

當雜湊表的容量超過預設容量時,必須調整table的大小。當容量已經達到最大可能值時,那麼該方法就將容量調整到Integer.MAX_VALUE返回,這時,需要建立一張新表,將原表的對映到新表中。