1. 程式人生 > >Android 從零學資料結構與演算法(3)——HashMap和LinkedHashMap

Android 從零學資料結構與演算法(3)——HashMap和LinkedHashMap

    本部落格的原創文章,都是本人平時學習所做的筆記,不做商業用途,如有侵犯您的智慧財產權和版權問題,請通知本人,本人會即時做出處理刪除文章。

  • HashMap

    基於雜湊表(散列表)的Map介面的實現,允許使用null鍵和null值,HashMap是非執行緒安全的,資料元素存取迭代是無序,順便提一下HashTable,HashTable是執行緒安全的,除了執行緒安全和null鍵值的區別,HashMap和HashTable大致相同。


    上圖是雜湊表的結構圖,這種表結構查詢效率高,如果我們把0-15的順序線表的每個地址看成一個"桶",而下標是"桶"的編號,我們通過計算key的hash值“與”上線性表的長度得到得到0-15的值,然後將value放入對應的"桶"內,這樣我們查詢的時候不用遍歷所有的元素,直接去對應的桶裡面查詢就可以了。

    下面我們一起看HashMap的原始碼,我是用Android studio檢視的api25的原始碼,不同api版本原始碼會有不同,首先來看看成員變數。

    //最少容量
    static final int DEFAULT_INITIAL_CAPACITY = 4;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //擴容因子(當容量到75%的時候就要開始擴容了)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空表
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
    //鍵值對的陣列
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
    //非空元素的長度
    transient int size;
    //容量
    int threshold;
    //擴容因子相關
    final float loadFactor = DEFAULT_LOAD_FACTOR
    //操作計數器
    transient int modCount;

    我們再來看看put方法:

public V put(K key, V value) {
	//儲存key為null的值,如果之前有值就覆蓋
        if (key == null)
            return putForNullKey(value);
	//獲取key的hash值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
	//通過hash&(length-1)得到元素長度內的值,也就是"桶"的下標,(&運算不熟悉的自行百度)
        int i = indexFor(hash, table.length);
	//通過桶的下標,遍歷"桶"裡面的元素(需要看一下HashMapEntry的實現),如果hash值相同,key相等,那就覆蓋value,返回舊的value
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
	//操作計數
        modCount++;
	//如果沒有相同的key,就新增新元素
        addEntry(hash, key, value, i);
        return null;
    }
	void addEntry(int hash, K key, V value, int bucketIndex) {
	//如果元素個數大於等於擴容因子容量(目前容量的75%),那就擴容原來的一倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
	//新增資料元素(內容簡單,程式碼就不貼了)
        createEntry(hash, key, value, bucketIndex);
    }
	void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
	//如果達到最大了就不擴容了
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
	//通過遍歷,把舊陣列中的元素新增到新陣列中
        transfer(newTable);
        table = newTable;
	//計算擴容因子容量,loadFactor=0.75f
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
  void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
	//雙層迴圈遍歷把元素新增到新數組裡面,因為indexFor()方法中的leng變了,
	//"桶"的下標就有可能變,所以需要遍歷全部元素,通過key的hash值重新計算桶的下標
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

    上面是put流程的部分原始碼,還需要自己對照原始碼看一看。如果能把put看明白,get就很簡單了。下面我們看一下remove方法原始碼:

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.getValue());
    } 
	final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
	//通過key的hash值獲取"桶"的下標
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        HashMapEntry<K,V> prev = table[i];
        HashMapEntry<K,V> e = prev;
	//遍歷這個"桶"內的元素
        while (e != null) {
            HashMapEntry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
		//只有第一個元素就是要刪除元素的時候prev才會等於e
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
    一些邏輯還需要自己認真屢一下。HashMap核心內容也就這些。下面我們一起看看LinkedHashMap。
  • LinkedHashMap

    LinkedHashMap是HashMap的子類,具有HashMap所有特性,只是額外維護一個雙向迴圈連結串列來保持迭代順序,當然,是犧牲了一定的效能。LinkedHashMap支援LRU演算法,LruCache就是基於LinkedHashMap來實現的。


下面一起看看原始碼,看一下僅有的兩個成員變數。

    //頭結點
    private transient LinkedHashMapEntry<K,V> header;
    //如果是true通過訪問排序,如果是false通過插入排序,預設為false
    private final boolean accessOrder;

    如果你去看一下Lrucache的構造方法,你會發現它傳入的就是true。LinkedHashMap裡面並沒有put的方法,只是重寫了addEntry()和createEntry()方法,HashMap的put方法最後會呼叫addEntry()和createEntry()方法。

 void addEntry(int hash, K key, V value, int bucketIndex) {
	//雙向迴圈連結串列頭節點(header後的節點)如果是按插入排序就是最先插入的元素,
	//如果是按訪問排序就是訪問最少的元素,把訪問最少或最老的元素賦值給eldest,
        LinkedHashMapEntry<K,V> eldest = header.after;
        if (eldest != header) {
            boolean removeEldest;
            size++;
            try {
		//removeEldestEntry()方法交給子類實現,通過判斷返回值來刪除eldest元素
		//預設返回false,所以不刪除
                removeEldest = removeEldestEntry(eldest);
            } finally {
                size--;
            }
		//刪除最少或最老的元素
            if (removeEldest) {
                removeEntryForKey(eldest.key);
            }
        }

        super.addEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
	//取出陣列中存的節點
        HashMapEntry<K,V> old = table[bucketIndex];
	//建立新節點
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
	//將新節點放在陣列中
        table[bucketIndex] = e;
	//將新元素節點新增到雙向迴圈連結串列,為什麼傳header呢,因為要把元素插入在header前面
	//header前面是尾節點,header後面是頭節點,新新增的元素需要新增到尾節點
        e.addBefore(header);
        size++;
    }
    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
	    //將新元素新增到header前面的尾節點,也就是header和之前的尾節點之間插入新元素節點
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

    上面就是LinkedHashMap新增新元素的核心程式碼。配合結構圖能更好的理解。每次新新增的元素都新增到header的前面,也就是尾節點,而header的後面的節點就是頭結點,所以在Lru演算法中,無論是訪問排序還是插入排序,需要刪除元素時,都是刪除頭結點。

  public V get(Object key) {
	//呼叫父類的獲取元素方法
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
	//記錄訪問
        e.recordAccess(this);
        return e.value;
    }
	void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
		//如果是訪問排序,就把訪問的資料元素放在尾節點
            if (lm.accessOrder) {
		//記錄操作
                lm.modCount++;
		//刪除自己
                remove();
		//把自己新增到header前面的尾節點
                addBefore(lm.header);
            }
        }

    上面試get()的核心程式碼,如果是插入排序,只需要遍歷查詢就可以了,如果是訪問排序,就需要把訪問的元素放在尾節點。

   public Map.Entry<K, V> eldest() {
        Entry<K, V> eldest = header.after;
        return eldest != header ? eldest : null;
    }

    eldest()方法是返回頭節點。以上就是LinkedHashMap比較核心的原始碼了。

  • 下篇預告

    下節我們一起學習樹。

------------------------------------------------       

    想要繼續跟我一起學習一起成長,請關注我的公眾號:程式設計師持續發展方案