1. 程式人生 > >Java資料結構和演算法:HashMap的實現原理

Java資料結構和演算法:HashMap的實現原理

1. HashMap概述

HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。

2. HashMap的資料結構

在java程式語言中,最基本的結構就是兩種,一個是陣列,另外一個是模擬指標(引用),所有的資料結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列的結合體。

hashmap

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

3. ArrayMap對比HashMap

在Java裡面用Collection裡面的HashMap作為容器我們使用的頻率很高,而ArrayMap是Android api提供的一種用來提升特定場和記憶體使用率的特殊資料結構。今天我就寫一篇部落格記錄一下

4. HashMap

hashmap

Java庫裡的HashMap其實是一個連續的連結串列陣列,通過讓key計算hash值後插入對應的index裡。當hash值發生碰撞時,可以採用線性探測,二次hash,或者後面直接變成連結串列的結構來避免碰撞。因為hash的值不是連續的,所以hashmap實際需要佔用的大小會比它實際能裝的item的容量要大。我們可以看一下HashMap的原始碼:

 public HashMap(int initialCapacity, float loadFactor)   
 {   
     // 初始容量不能為負數  
     if (initialCapacity < 0)   
         throw new IllegalArgumentException(   
        "Illegal initial capacity: " +   
             initialCapacity);   
     // 如果初始容量大於最大容量,讓出示容量  
     if (initialCapacity > MAXIMUM_CAPACITY)   
         initialCapacity = MAXIMUM_CAPACITY;   
     // 負載因子必須大於 0 的數值  
if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException( loadFactor); //.... // 設定容量極限等於容量 * 負載因子 threshold = (int)(capacity * loadFactor); // 初始化 HashMap用於儲存的陣列 table = new Entry[capacity]; // ① init(); }

你會發現它又一個變數叫loadfactor,還有threshold。threshold就是臨界值的意思,代表當前HashMap的儲存機構能容納的最大容量,它等於loadfactor * 容量。當HashMap記錄存入的item size大於threshold後,HashMap就會進行擴容(resize)。當我們第一次新建一個HashMap物件的時候,預設的容量是16,若你只打算在HashMap裡放入3個元素那將浪費至少13個空間。

6. ArrayMap

ArrayMap是怎麼實現節省記憶體的呢?先放資料結構圖:

hashmap

他用兩個陣列來模擬Map,第一個陣列存放存放item的hash值,第二陣列是把key,value連續的存放在數組裡,通過先算hash在第一個數組裡找到它的hash index,根據這個index在去第二個數組裡找到這個key-value。

在這裡,在第一個數組裡查詢hash index的方法當然是用二分查詢啦(binary search)。

hashmap

這個資料結構的設計就做到了,有多個item我就分配多少記憶體,做到了memory的節約。並且因為資料結構是通過陣列組織的,所以遍歷的時候可以用index直接遍歷也是很方便的有沒有!但是缺點也很明顯,查詢達不到HashMap O(1)的查詢時間。

當要儲存的物件較少的時候(1000以下的時候)可以考慮用ArrayMap來減少記憶體的佔用。

7. hashmap和hashtable的區別

繼承和實現區別

Hashtable是基於陳舊的Dictionary類,完成了Map介面;HashMap是Java 1.2引進的Map介面的一個實現(HashMap繼承於AbstractMap,AbstractMap完成了Map介面)。

執行緒安全不同

HashTable的方法是同步的,HashMap是未同步,所以在多執行緒場合要手動同步HashMap。

對null的處理不同

HashTable不允許null值(key和value都不可以),HashMap允許null值(key和value都可以)。即 HashTable不允許null值其實在編譯期不會有任何的不一樣,會照樣執行,只是在執行期的時候Hashtable中設定的話回出現空指標異常。 HashMap允許null值是指可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示 HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。

方法不同

HashTable有一個contains(Object value),功能和containsValue(Object value)功能一樣。

5、HashTable使用Enumeration,HashMap使用Iterator。

6、HashTable中hash陣列預設大小是11,增加的方式是 old*2+1。HashMap中hash陣列的預設大小是16,而且一定是2的指數。

7、雜湊值的使用不同,HashTable直接使用物件的hashCode,程式碼是這樣的:

 int hash = key.hashCode();
 int index = (hash & 0x7FFFFFFF) % tab.length;

而HashMap重新計算hash值,而且用與代替求模:

int hash = hash(k);

int i = indexFor(hash, table.length);

static int hash(Object x) {

    int h = x.hashCode();

    h += ~(h << 9);

    h ^= (h >>> 14);

    h += (h << 4);

    h ^= (h >>> 10);

    return h;

}

static int indexFor(int h, int length) {

    return h & (length-1);

}

Hashtable的實現原理

Hashtable類似HashMap,使用hash表來儲存鍵值對。hash表定義:根據設定的hash函式和處理衝突的方式(開放定址、公共溢位區、鏈地址、重雜湊…)將一組關鍵字對映到一個有限的連續的地址集上(即bucket陣列或桶陣列),並以關鍵字在地址集中的“像”作為記錄在表中的儲存位置,這種表稱為hash表。

hash衝突發生時,通過“連結串列法”或叫”拉鍊法”來處理衝突,即通過一個連結串列儲存鍵值對(Map.Entry)。每個Entry物件都有next指標用於指向下一個具有相同hashcode值的Entry。

HashMap的實現原理

public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {

    private static final int MINIMUM_CAPACITY = 4;//最小容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
    static final float DEFAULT_LOAD_FACTOR = .75F;//裝載因子
    transient int size;

    private static final Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >>> 1];
    transient HashMapEntry<K, V>[] table;



    static class HashMapEntry<K, V> implements Entry<K, V> {
        final K key;
        V value;
        final int hash;
        HashMapEntry<K, V> next;

    }

}
class HashMapEntry{
    K key;
    V value;
    int hash;
    HashMapEntry<K, V> next;
}

hashmap

二次雜湊

@Override public V put(K key, V value) {
   ...
   int hash = Collections.secondaryHash(key);
   HashMapEntry<K, V>[] tab = table;
   int index = hash & (tab.length - 1);
   ...
}

//Collections.secondaryHash

public static int secondaryHash(Object key) {
    return secondaryHash(key.hashCode());
}

private static int secondaryHash(int h) {
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

table 是一個大小為 2 n 的一維陣列,其中存放的是一個個的 HashMapEntry,而 HashMapEntry 是包含了 hash、key 與 value 值及一個指向 HashMapEntry 的 next 指標

key.hashCode()
雜湊碼就是將物件的資訊經過一些轉變形成一個獨一無二的int值,這個值儲存在一個array中

int hash = secondaryHash(key.hashCode()) 二次 hash,減少碰撞
求出key的hash值,根據hash值得出在table中的索引,而後遍歷對應的單鏈表,碰撞

int index = hash & (tab.length - 1)根據雜湊值計算出對應的key在雜湊陣列中的索引,若在存放的過程中,index 值相同,則會連結當前 entry 的 next 指標上。

如果兩個鍵的hashcode相同,你如何獲取值物件?

當我們呼叫get()方法,HashMap會使用鍵物件的hashcode找到bucket位置,找到bucket位置之後,會呼叫keys.equals()方法去找到連結串列中正確的節點。

負載因子(百分比):HashMap的大小 = 初始容量*負載因子,擴容集合,負載因子和初始容量會影響HashMap的效能,初始容量預設是16,負載因子預設是0.75

先通過key.hashCode()計算出key的雜湊值,如果雜湊值相等,則通過equals()方法比較內容是否相同

JDK8:位桶+連結串列/紅黑樹

concurrentHashMap:執行緒安全,分段鎖
Collections.synchronizedMap()

底層資料結構使用的是雜湊表(雜湊陣列),陣列的每個元素都是一個單鏈表的頭節點,連結串列是用來解決衝突的,如果不同的key對映到了陣列的同一位置處,就將其放入單鏈表中

LinkedHashMap:雙向連結串列,LruCache底層使用
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
引數1:初始容量,引數2:負載因子,引數3:是否開啟按訪問順序排序

LinkedHashMap

雙向迴圈連結串列,LinkedHashMap可以用來實現LRU演算法,accessOrder為true,表示按訪問順序排序
當accessOrder為true時,才會開啟按訪問順序排序的模式,才能用來實現LRU演算法。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成為最近訪問的Entry,因此便把該Entry加入到了雙向連結串列的末尾(get方法通過呼叫recordAccess方法來實現,put方法在覆蓋已有key的情況下,也是通過呼叫recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向連結串列的後面,多次操作後,雙向連結串列前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head後面的那個Entry)便是最近最少使用的Entry。

HashSet的實現原理

HashSet是通過HashMap實現的,只是使用了HashMap的鍵,沒有使用HashMap的值
hashCode(),雜湊值,HashSet的元素會根據雜湊值儲存,雜湊值一樣的元素會儲存在同一個區域,也叫桶原理(bucket),這也查詢起來效率會高很多

但是在元素被新增進HashSet集合後,修改元素中參與計算雜湊值的屬性,再呼叫remove()方法時不起作用,會導致記憶體洩露

HashMap與HashTable的主要區別

  • HashTable執行緒更加安全,代價就是因為它粗暴的添加了同步鎖,所以會有效能損失。其實有更好的concurrentHashMap可以替代HashTable
  • HashTable:hash值對length取模,HashMap中則通過h&(length-1)的方法來代替取模
  • Hashtable不允許key或者value使用null值,而HashMap可以。
  • Hashtable擴容時,將容量變為原來的2倍加1,而HashMap擴容時,將容量變為原來的2倍。
  • Hashtable計算hash值,直接用key的hashCode(),而HashMap重新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算,且這裡一般先用hash&0x7FFFFFFF後,再對length取模,&0x7FFFFFFF的目的是為了將負的hash值轉化為正值,因為hash值有可能為負數,而&0x7FFFFFFF後,只有符號外改變,而後面的位都不變。

ArrayList與HashSet的區別



Android 5.0之後對HashMap的修改

之前發現在Android 5.0的機子上放在HashMap裡面的資料取出後跟Android 5.0之下的機子不一樣,導致專案裡面一個接口出了問題(介面做了快取,request引數順序變化的話就會導致一些資料拿不到),然後去查看了一下Android 5.0和Android 4.4 關於HashMap的原始碼,使用meld檢視差異能夠看到果然google對HashMap的實現做了修改.

下圖左邊為Android 5.0的原始碼,右邊為Android 4.4的原始碼

hashmap

hashmap

從原始碼中可以看到,Android 5.0 在計算key的HashCode使用的是下面的演算法.

private static int secondaryHash(int h) {  
    // Spread bits to regularize both segment and index locations,  
    // using variant of single-word Wang/Jenkins hash.  
    h += (h <<  15) ^ 0xffffcd7d;  
    h ^= (h >>> 10);  
    h += (h <<   3);  
    h ^= (h >>>  6);  
    h += (h <<   2) + (h << 14);  
    return h ^ (h >>> 16);  
}  

而Android 4.4中計算Key的HashCode的演算法明顯跟Android 5.0中不同,所以這也導致了在get之後,在兩個系統上同樣的資料不同的順序。如果對儲存的資料有順序需求的話改為使用紅黑樹構建的TreeMap就OK了.

static int secondaryHash(Object key) {  
    int hash = key.hashCode();  
    hash ^= (hash >>> 20) ^ (hash >>> 12);  
    hash ^= (hash >>> 7) ^ (hash >>> 4);  
    return hash;  
}  

資料結構與演算法

相關推薦

Java資料結構演算法HashMap實現原理

1. HashMap概述 HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。 2. HashMap的資料結構 在java程式語

Java資料結構演算法HashMap,雜湊表,雜湊函式

1. HashMap概述 HashMap是基於雜湊表的Map介面的非同步實現(Hashtable跟HashMap很像,唯一的區別是Hashtalbe中的方法是執行緒安全的,也就是同步的)。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保

Java資料結構演算法哈夫曼樹

本章介紹哈夫曼樹。和以往一樣,本文會先對哈夫曼樹的理論知識進行簡單介紹,然後給出C語言的實現。後續再分別給出C++和Java版本的實現;實現的語言雖不同,但是原理如出一轍,選擇其中之一進行了解即可。若文章有錯誤或不足的地方,請幫忙指出! 哈夫曼樹的介紹

JAVA資料結構演算法第三章(棧佇列)

棧 棧是限制僅在一個位置上進行插入和刪除的線性表。允許插入和刪除的一端為末端,稱為棧頂。另一端稱為棧底。不含任何資料元素的棧稱為空棧。棧又成為後進先出(LIFO)表,後進入的元素最先出來。 首先,棧是一個線性表,元素之間具有線性關係,即前驅後繼關係,其次,

Java資料結構演算法(一)簡介

  本系列部落格我們將學習資料結構和演算法,為什麼要學習資料結構和演算法,這裡我舉個簡單的例子。   程式設計好比是一輛汽車,而資料結構和演算法是汽車內部的變速箱。一個開車的人不懂變速箱的原理也是能開車的,同理一個不懂資料結構和演算法的人也能程式設計。但是如果一個開車的人懂變速箱的原理,比如降低速

Java資料結構演算法)棧-----棧本質原理實現+ArrayDeque類實現

自定義棧 class MyStack{ private int[] a; private int size; private int top; private final int ERROR

【scala 資料結構演算法】Scala實現氣泡排序

主要內容: 1、氣泡排序演算法原理 2、scala實現 3、python實現 4、goland實現 氣泡排序演算法原理: 1、比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。 2、對

java資料結構演算法

java資料結構與演算法 寫給讀者的話: 本人是一個剛剛畢業的程式設計師,大學期間資料結構學的比較紮實,來工作後發現雖然概念都知道,但是應用不是很熟練,所以打算重新擼幾遍資料結構,正好在寫java,這裡就用java描述資料結構了;然後有幾個要點: 1)實踐永遠是檢驗真理的唯一

資料結構演算法第八章 圖論演算法

9.1 若干定義 圖的定義:一個圖(Graph) G=(V,E)是由頂點的集合V和邊Edge的集合E組成的。每一條邊就是一個頂點對(v,w),其中(v,w) ∈E。有時候也把邊叫做弧。如果頂點對是有序的,那麼圖就是有向的。有的圖也叫做有向圖。頂點w和頂點v鄰接當且僅當(v,w)

java資料結構演算法程式設計作業系列篇-陣列

/** * 程式設計作業 2.1 向highArray.java程式(清單2.3)的HighArray類新增一個名為getMax()的方法,它返回 陣列中最大關鍵字的值,當陣列為空時返回-1。向main()中新增一些程式碼來使用這個方法。 可以假設所有關鍵字都是正數。 2.2 修改程式設計作業

Java資料結構演算法)最短路徑---Dijkstra+Floyd

參考博文 Floyd演算法和Dijkstra演算法都不能針對帶有負權邊的圖,否則一直走負權邊,沒有最小,只有更小!! Floyd演算法 import java.util.Scanner; //Floyd演算法 class Graph{ public int[][] ad

Java資料結構演算法)拓撲排序

參考博文 拓撲排序 public class Main { public static void main(String[] args){ System.out.println("請輸入一個圖的鄰接矩陣(8X8):"); int[][] map = new int[

Java資料結構演算法)最小生成樹---Kruskal演算法(並查集)

該文章利用prime演算法求得連通圖的最小生成樹對應的邊權最小和,prime演算法是從頂點的角度思考和解決問題。本文介紹的Kruskal演算法將從邊的角度考慮並解決問題,利用了並查集方便地解決了最小生成樹的問題。 本文參考博文 //並查集 class UnionSameSet{

Java資料結構演算法)最小生成樹---prime演算法

參考博文 public class Main { public static void main(String[] args){ int inf = 1000000;//無窮大 //圖,可以這樣認為:圖的任意兩個頂點之間都有邊,兩頂點無法到達的,可以認為他們之間的邊權是

Java資料結構演算法)圖的DFSBFS

DFS+BFS import java.util.*; //以無向圖為例,實現圖的深度優先搜尋和廣度優先搜尋 class Graph{ public int[][] adjacencyMatrix;//鄰接矩陣,1代表有邊,0代表沒有邊 public int arcNumb

Java資料結構演算法)堆---優先佇列、堆排序

堆主要用於實現優先佇列。 利用有序陣列可以實現優先佇列(從小到大或從大到小的陣列),刪除的時間複雜度是O(1),但是插入的時間複雜度是O(N)。用堆實現優先佇列,插入和刪除的時間複雜度都是O(logN)。 簡介 堆是一種完全二叉樹,且每個節點的值都大於等於子節點值(大根堆)。

Java資料結構演算法 - 堆

堆的介紹 Q: 什麼是堆? A: 這裡的“堆”是指一種特殊的二叉樹,不要和Java、C/C++等程式語言裡的“堆”混淆,後者指的是程式設計師用new能得到的計算機記憶體的可用部分 A: 堆是有如下特點的二叉樹: 1) 是一棵完全二叉樹 2) 通常由陣列實現。前面介

Java資料結構演算法(七)——連結串列

目錄   前面部落格我們在講解陣列中,知道陣列作為資料儲存結構有一定的缺陷。在無序陣列中,搜尋效能差,在有序陣列中,插入效率又很低,而且這兩種陣列的刪除效率都很低,並且陣列在建立後,其大小是固定了,設定的過大會造成記憶體的浪費,過小又不能滿足資料量的儲存。  

Java資料結構演算法(一)樹

Java資料結構和演算法(一)樹 前面講到的連結串列、棧和佇列都是一對一的線性結構,這節講一對多的線性結構 - 樹。「一對多」就是指一個元素只能有一個前驅,但可以有多個後繼。 一、樹 度(Degree) :節點擁有的子樹數。樹的度是樹中各個節點度的最大值。 節點 :度為 0 的節點稱為葉節

Java資料結構演算法(二)樹的基本操作

Java資料結構和演算法(二)樹的基本操作 一、樹的遍歷 二叉樹遍歷分為:前序遍歷、中序遍歷、後序遍歷。即父結點的訪問順序 1.1 前序遍歷 基本思想:先訪問根結點,再先序遍歷左子樹,最後再先序遍歷右子樹即根—左—右。圖中前序遍歷結果是:1,2,4,5,7,8,3,6。 // 遞迴實現前序遍歷