ArrayMap跟HashMap區(qū)別

Hash碰撞的解決方式

  • 提起存儲鍵值對,首先想到的是Map集合,但是對于hash算法導(dǎo)致的hash碰撞,一般有兩種解決方式: 鏈表法跟開放地址法,對于Android應(yīng)用開發(fā)來說,正好對應(yīng)著HashMap跟ArrayMap的解決方式

ArrayMap: 開放地址法

  • 筆者在使用Retrofit請求網(wǎng)絡(luò)時(shí)由于需要將數(shù)據(jù)封裝成map集合發(fā)送給后端,但是一般都是提交數(shù)據(jù)量很小的內(nèi)容,因此使用ArrayMap替換HashMap已節(jié)約內(nèi)存空間.這里通過源碼分析兩者之間的差別
  1. 首先查看構(gòu)造函數(shù)
 public ArrayMap() { 
        this(0, false);
    }
 public ArrayMap(int capacity) {
        this(capacity, false);
    }
/**
* identityHashCode: 是否使用系統(tǒng)的System.identityHashCode(key),跟對象存儲位置地址值相關(guān) , 
* false時(shí)使用對象的hashCode,如果重寫就使用對象的hashCode方法(String 重寫了,所以map鍵常用的字符串是一致的),未重寫則使用object的hashcode
* 同系統(tǒng)的調(diào)用同一個本地方法,結(jié)果值一致
*/
public ArrayMap(int capacity, boolean identityHashCode) {
        mIdentityHashCode = identityHashCode;

        // 這是同HashMap不同的地方,如果當(dāng)前沒有設(shè)置大小默認(rèn)為0,hashMap默認(rèn)數(shù)組為16,節(jié)約空間了吧
        if (capacity < 0) {
            mHashes = EMPTY_IMMUTABLE_INTS;
            mArray = EmptyArray.OBJECT;
        } else if (capacity == 0) {
            mHashes = EmptyArray.INT;
            mArray = EmptyArray.OBJECT;
        } else {
            // 初始化值,根據(jù)提供的大小,但同HashMap會改變(注意并非指定多少即為多大空間數(shù)組)
            allocArrays(capacity); 
        }
        mSize = 0;
}
  1. 搞懂幾個數(shù)據(jù)含義
  • ArrayMap是由兩個數(shù)組構(gòu)成的 mHashes 和 mArray
  • mHashes : 由鍵的Hash值根據(jù)大小有序的數(shù)組 mHashes [key1.hash , key2.hash , ....],(key1.hash跟key2.hash完全可以相等,這個沒關(guān)系的)
  • mArray: 鍵值對依次排列的數(shù)組 mArray[key1, value1, key2, value2 ....]
  1. 查看 put()方法
    @Override
    public V put(K key, V value) {
        final int osize = mSize;
        final int hash;
        int index;
        if (key == null) { //key是空,則通過indexOfNull查找對應(yīng)的index,支持存儲null鍵
            hash = 0;
            index = indexOfNull();
        } else { //否則 通過key查找下標(biāo)index 
            hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode(); //構(gòu)造函數(shù)中指定使用哪種hash值
            index = indexOf(key, hash); //稍后解釋(根據(jù)hash值及key找到對應(yīng)的下標(biāo)index ,存在為正,不然為 ~位置 一個負(fù)數(shù)值)
        }
        if (index >= 0) { //找到了當(dāng)前存儲的key鍵在mArray中替換成新值value并返回舊值
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            mArray[index] = value;
            return old;
        }

        index = ~index; //將indexOf中找到的~index在次~即為(~~index) == index一個正值,即為數(shù)據(jù)將要插入的位置
        if (osize >= mHashes.length) { //判斷是否需要擴(kuò)容大小,擴(kuò)容為默認(rèn) 4 ->8 -> 12 -> 18(不足向上取,比如初始7個數(shù)據(jù)則數(shù)組大小為8),擴(kuò)容后賦值并回收原有數(shù)組
            final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
            //將原來的數(shù)組賦值保存后在使用allocArrays(n)對原來數(shù)組根據(jù)新大小n進(jìn)行擴(kuò)容
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n); //具體的擴(kuò)容函數(shù),創(chuàng)建一個新的數(shù)據(jù),可能存在緩存數(shù)組,稍后講解

            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { //對于兩個線程同時(shí)擴(kuò)容報(bào)錯,同HashMap一致是非線程安全
                throw new ConcurrentModificationException();
            }

            if (mHashes.length > 0) { 
                //將上方保存的原有數(shù)據(jù)拷貝到新數(shù)組中,注意下標(biāo)從0開始及大小原有數(shù)組長度
                if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            }
            //釋放原有數(shù)組,已經(jīng)在上方拷貝過了,這里涉及到緩存數(shù)據(jù)
            //查看是否數(shù)據(jù)還有利用價(jià)值,其后會講到,大致是對數(shù)據(jù)量為4或者8做一個緩存以便其后還會利用到
            //如果數(shù)據(jù)量超過8就就沒有必要占用內(nèi)存去緩存它了
            freeArrays(ohashes, oarray, osize);
        }

        if (index < osize) {  //index是指當(dāng)前數(shù)據(jù)key的hash值下標(biāo)即key應(yīng)該插入數(shù)組的位置不是最后一位則移動他將插入位置及其后的所有數(shù)據(jù)騰出位置給其插入使用
            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
                    + " to " + (index+1));
            //注意: 將mHashes的index 位置以后的向后移動一位,同時(shí)mArray也是同時(shí)向后移動2位,給key.hash,跟(key,value)插入讓出位置
            System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }

        if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
            if (osize != mSize || index >= mHashes.length) { //再次判斷是否線程安全
                throw new ConcurrentModificationException();
            }
        }
        mHashes[index] = hash; //在特定位置插入mHashes,及mArray數(shù)組上
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++; //數(shù)據(jù)值+1
        return null;
    }
  1. 初始化數(shù)組函數(shù) allocArrays(capacity)用于數(shù)組的初始化及緩存數(shù)據(jù)
  • 看這個緩存函數(shù)需要搞懂一個數(shù)組擴(kuò)容大小改變關(guān)系(在put函數(shù)中BASE_SIZE = 4)
    final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
    
  • 默認(rèn)值為 4 ,第一次擴(kuò)容為 4 * 2 = 8 , 以后每次擴(kuò)容為增加當(dāng)前值得 1/2 倍.即下次為 12 ->18 ...
    private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        //設(shè)置緩存用的,當(dāng)數(shù)據(jù)量由24 降為 8時(shí)不用再次重新創(chuàng)建加載,而是直接使用mTwiceBaseCache緩存的數(shù)組即可
        if (size == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) { //緩存 8數(shù)據(jù)存在 
                    final Object[] array = mTwiceBaseCache;
                    mArray = array; 
                    mTwiceBaseCache = (Object[])array[0]; 
                    mHashes = (int[])array[1]; 
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }
        }

        mHashes = new int[size];
        mArray = new Object[size<<1];
    }
  1. 對于查找indexOf(key,hash)

    int indexOf(Object key, int hash) {
        final int N = mSize;

        // Important fast case: if nothing is in here, nothing to look for.
        if (N == 0) {
            return ~0;
        }
        // 使用二分查找到當(dāng)前hash值在mHashes數(shù)組中的下標(biāo),如果不存在返回(~當(dāng)前需要插入的位置)后續(xù)在~即為正在需要插入的位置 (~~A == A)
        int index = binarySearchHashes(mHashes, N, hash);

        if (index < 0) { //如果沒有找到就返回一個負(fù)值
            return index;
        }

        //如果當(dāng)前index下標(biāo)的key同查找的一致就返回
        if (key.equals(mArray[index<<1])) {
            return index;
        }

        // 先向上查找相同hash值是否key一致,否則向下查找
        int end;
        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
            if (key.equals(mArray[end << 1])) return end;
        }
       
        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
            if (key.equals(mArray[i << 1])) return i;
        }

        // 如果都沒有找到,將返回需要插入位置的負(fù)值,注意這里是相同hash值的最后一個其后添加數(shù)據(jù)
        return ~end;
    }
  1. remove 方法:
  • 在某種條件下,會重新分配內(nèi)存,保證分配給ArrayMap的內(nèi)存在合理區(qū)間,減少對內(nèi)存的占用。但是如果每次remove都重新分配空間,會浪費(fèi)大量的時(shí)間。
  • 因此在此處,Android使用的是用空間換時(shí)間的方式,以避免效率低下。無論從任何角度,頻繁的分配回收內(nèi)存一定會耗費(fèi)時(shí)間的。
public V removeAt(int index) {
        final Object old = mArray[(index << 1) + 1]; 
        final int osize = mSize; //msize是當(dāng)前存儲實(shí)際數(shù)據(jù)大小,mHashes.length為當(dāng)前申請的數(shù)組大小
        final int nsize;
        if (osize <= 1) {
            // 當(dāng)實(shí)際數(shù)據(jù)為1時(shí),即移除以后恢復(fù)成默認(rèn)值null
            if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            mHashes = EmptyArray.INT;
            mArray = EmptyArray.OBJECT;
            freeArrays(ohashes, oarray, osize);
            nsize = 0;
        } else {
            nsize = osize - 1; 
            // 如果當(dāng)前數(shù)組大于 8并且實(shí)際數(shù)據(jù)量小于 數(shù)組長度的 1/3 則重新分配內(nèi)存空間
            if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
                //重新分配后實(shí)際數(shù)組的大小
                final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);

                if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
                //備份數(shù)據(jù)以便后期復(fù)制使用
                final int[] ohashes = mHashes;
                final Object[] oarray = mArray;
                allocArrays(n); //重新分配數(shù)據(jù),注意里面會用到已經(jīng)緩存的4和8的數(shù)據(jù)

                if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { //判斷是否線程安全
                    throw new ConcurrentModificationException();
                }
                //移除index上的數(shù)據(jù)分兩個部分移除,首先復(fù)制0--index大小的數(shù)據(jù)就是到 (index -1)處
              
                if (index > 0) {
                    if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
                    System.arraycopy(ohashes, 0, mHashes, 0, index);
                    System.arraycopy(oarray, 0, mArray, 0, index << 1);
                }
                //  再次從index + 1 處移動nsize - index的數(shù)據(jù)復(fù)制到新數(shù)組中
                if (index < nsize) {
                    if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
                            + " to " + index);
                    System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
                    System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                            (nsize - index) << 1);
                }
            } else { //不用重新分配內(nèi)存,直接可用的則直接移動其后的數(shù)據(jù),并將最后一位設(shè)置成默認(rèn)null
                if (index < nsize) {
                    if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
                            + " to " + index);
                    System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                    System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                            (nsize - index) << 1);
                }
                mArray[nsize << 1] = null;
                mArray[(nsize << 1) + 1] = null;
            }
        }
        if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
            throw new ConcurrentModificationException();
        }
        mSize = nsize; //減少一位并返回刪除的舊值
        return (V)old;
    }
  1. 重點(diǎn)關(guān)注緩存 freeArrays(final int[] hashes, final Object[] array, final int size) , 結(jié)合 allocArrays(int size)同看
    • 注意參數(shù)的內(nèi)容 : hashes 跟 array的長度關(guān)系是 1/ 2 , 切記切記啊
    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
        if (hashes.length == (BASE_SIZE*2)) { //如果當(dāng)前緩存的大小為8
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCacheSize < CACHE_SIZE) {
                    array[0] = mTwiceBaseCache; //第一次的array[0] = null, 但是其后的將會是一個鏈表結(jié)構(gòu)這一次緩存中的array[0] ->指向的是上一次緩存的整體數(shù)字,但是array的中長度依然是 16 ,最多緩存 10個
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mTwiceBaseCache = array;
                    mTwiceBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                            + " now have " + mTwiceBaseCacheSize + " entries");
                }
            }
        } else if (hashes.length == BASE_SIZE) { //同上所述不過緩存的數(shù)據(jù)為4,array的總長度為8,最多緩存10個
            synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mBaseCache = array;
                    mBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                            + " now have " + mBaseCacheSize + " entries");
                }
            }
        }
    }
  1. 再次查看 allocArrays

    private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        if (size == (BASE_SIZE*2)) { //8數(shù)據(jù)同下
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) {
                    final Object[] array = mTwiceBaseCache;
                    mArray = array;
                    mTwiceBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) { //如果當(dāng)前緩存的有數(shù)據(jù),則array是一個大小為8的鏈?zhǔn)浇Y(jié)構(gòu),加入緩存了多次,但是被復(fù)用以后其被賦值給mArray的時(shí)候依然是一個大小為8且在后續(xù)會被重新賦值的,所以不用擔(dān)心鏈?zhǔn)蕉鄬咏Y(jié)構(gòu)問題
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];  //將下一層緩存的數(shù)據(jù)重新賦值給4個緩存
                    mHashes = (int[])array[1]; //當(dāng)前為4的數(shù)組賦值給mHashes
                    array[0] = array[1] = null; 
                    mBaseCacheSize--; //緩存池中數(shù)量減1
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }
        }

        mHashes = new int[size];
        mArray = new Object[size<<1];
    }
  • 緩存數(shù)據(jù)圖如下


    image
  1. 特別解釋,為何最大緩存數(shù)據(jù)是10層:
  • 沒有注意幾個數(shù)據(jù)情況 :幾個緩存數(shù)據(jù)及他們的長度均是靜態(tài)成員變量,也就是他們是跟類一致而跟對象無關(guān),所以是所有的ArrayMap對象共用的,不管你創(chuàng)建多少ArrayMap都將共用這最多10個緩存池,相當(dāng)于線程池一樣兒的
    static Object[] mBaseCache; 
    static int mBaseCacheSize;
    static Object[] mTwiceBaseCache;
    static int mTwiceBaseCacheSize;
    

SparseArray

  • 同ArrayMap一樣是由兩個數(shù)組構(gòu)成的,不同的是他并不是map集合
//成員變量分析
public class SparseArray<E> implements Cloneable {
    private static final Object DELETED = new Object(); //數(shù)組中的數(shù)據(jù)如果被刪除,可能僅僅標(biāo)記value為DELETED并不會每次都重新移動數(shù)組,太消耗時(shí)間了,同時(shí)可以直接在Deleted位置上put數(shù)據(jù)覆蓋之
    private boolean mGarbage = false; //是否需要垃圾回收,即value中存在DELETED,但并不一定會被回收的,在delete(key)時(shí)被標(biāo)記為Deleted
    private int[] mKeys; //一個key對應(yīng)一個value,且key是int型不可重復(fù),多在從0-N有序列使用,比如RecycleView中的viewType就是有序且不可重復(fù)的,緩存即使用SparseArray
    private Object[] mValues;
    private int mSize;
 public SparseArray() { //默認(rèn)keys和values數(shù)組長度為10
        this(10);
    }
  • 存儲put
public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果找到i則i對應(yīng)的value數(shù)組位置修改值
        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i; //沒有找到,這個只是根據(jù)二分查找到第一個大于i的位置插入這里,其后值向后移動

            if (i < mSize && mValues[i] == DELETED) { //如果當(dāng)前位置被標(biāo)記為Deleted刪除標(biāo)記,表示該位置可用
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            //有Deleted則mGarbage = true,數(shù)組是可以靠移動而不是擴(kuò)容就能容納該put新值的
            if (mGarbage && mSize >= mKeys.length) {
                gc(); //新gc將deleted標(biāo)志都刪除掉
 
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            //如果位置沒有Deleted,且數(shù)組已經(jīng)滿了,則二倍的擴(kuò)容
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
  • put步驟:
    1. 如果當(dāng)前i位置根據(jù)二分查找找到了,則替換原來值為value新值
    2. 如果當(dāng)前i不存在,則找到第一個大于i的位置標(biāo)示插入位置,此時(shí)有兩種情況,1該位置設(shè)置為Deleted則直接插入即可,否則判斷是否已滿且有被標(biāo)記Deleted的位置,通過GC移動數(shù)組,將deleted位置處刪除則可正常的插入了
    3. 如果數(shù)組滿了,且沒有Deleted標(biāo)志,則需要2倍的擴(kuò)容數(shù)組后在插入啦!
  • 這里的gc比較有意思,可以看一下,使用雙指針移動刪除數(shù)組中被標(biāo)記為Deleted
private void gc() {
        // Log.e("SparseArray", "gc start with " + mSize);
        int n = mSize;
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            if (val != DELETED) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
        // Log.e("SparseArray", "gc end with " + mSize);
    }
  • get和delete函數(shù)都很簡單,這里只貼出來,標(biāo)記為Deleted即可,不用真正的去移動數(shù)組,提高效率
public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

HashMap

  • 對于大數(shù)據(jù)量的存儲還是推薦使用HashMap,因?yàn)锳rrayMap如果大量數(shù)據(jù)插入時(shí)需要時(shí)間復(fù)雜度O(n),而使用hashMap最壞時(shí)間復(fù)雜度O(logn,都在一個紅黑樹下),平均時(shí)間復(fù)雜度O(1),主要看數(shù)據(jù)量來換算是否已時(shí)間換取空間對于很多算法選擇無非就是時(shí)間跟空間兩者的平衡
  1. 數(shù)據(jù)結(jié)構(gòu)為:HashMap實(shí)際上是一個“鏈表散列”的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體;

    • 根據(jù)使用場景設(shè)置初始容量及負(fù)載因子
    • hashMap實(shí)現(xiàn)了Map接口,使用鍵值對進(jìn)行映射,map中不允許出現(xiàn)重復(fù)的鍵(key)
    • Map接口分為兩類:TreeMap跟HashMap
      • TreeMap保存了對象的排列次序,hashMap不能
      • HashMap可以有空的鍵值對(null-null),是非線程安全的,但是可以調(diào)用collections的靜態(tài)方法synchronizeMap()實(shí)現(xiàn)
    • HashMap中使用鍵對象來計(jì)算hashcode值
    • HashMap比較快,因?yàn)槭鞘褂梦ㄒ坏逆I來獲取對象
  2. 源碼解析:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            HashMap.Node<K, V>[] tab;
            HashMap.Node<K, V> p;
            int n, i;
            if ((tab = table) == null || (n = tab.length) == 0) //列表為空或者長度為0 初始化一個HashMap
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)  //當(dāng)前位置上沒有數(shù)據(jù)new一個鏈表
                tab[i] = newNode(hash, key, value, null);
            else { //當(dāng)前數(shù)組Hash位置上面已經(jīng)存在了值
                HashMap.Node<K, V> e;
                K k;
                if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k)))) //有相同的hash鍵,直接替換
                    e = p;
                else if (p instanceof HashMap.TreeNode) //如果當(dāng)前是紅黑樹使用紅黑樹的添加
                    e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                else { //當(dāng)前為鏈表結(jié)構(gòu)的添加
                    for (int binCount = 0; ; ++binCount) { //計(jì)算當(dāng)前鏈表的長度
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null); //放到鏈表尾部
                            /**
                             * 當(dāng)前鏈表從0開始遍歷一直到 7,所以長度大于等于 8轉(zhuǎn)化為紅黑樹存儲,如果remove時(shí)樹的數(shù)據(jù)量=<6 ,紅黑樹會轉(zhuǎn)化成鏈表結(jié)構(gòu)存儲
                             *
                             * 解析:因?yàn)榧t黑樹的平均查找長度是log(n),長度為8的時(shí)候,平均查找長度為3,如果繼續(xù)使用鏈表,平均查找長度為8/2=4,這才有轉(zhuǎn)換為樹的必要。
                             * 鏈表長度如果是小于等于6,6/2=3,雖然速度也很快的,但是轉(zhuǎn)化為樹結(jié)構(gòu)和生成樹的時(shí)間并不會太短。
                             * 選擇6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉(zhuǎn)換,而導(dǎo)致的不停轉(zhuǎn)化樹與鏈表導(dǎo)致效率低下的問題;
                             */
                            if (binCount >= TREEIFY_THRESHOLD - 1)
                                treeifyBin(tab, hash); //如果
                            break;
                        }
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount; //記錄變化次數(shù),防止在多線程中迭代器取值導(dǎo)致錯誤,fail-fill檢查機(jī)制
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
    1. 通過源碼我們可以得出:

      1. 當(dāng)我們往HashMap中put元素的時(shí)候,先根據(jù)key的hashCode重新計(jì)算hash值,根據(jù)hash值得到元素在數(shù)組中的下標(biāo),(hash值計(jì)算 = 32位hash的 hash值 ^ 高16位后再 & 上 length -1 )

        1. 如果數(shù)組該位置已經(jīng)存放有其他元素,則在這個位置上以鏈表的形式存放,新加入的放在鏈頭,最后加入的放在鏈尾,如果鏈表中有相應(yīng)的key則替換value值為最新的并返回舊值;
        2. 如果該位置上沒有其他元素,就直接將該位置放在此數(shù)組中的該位置上;
        3. 我們希望HashMap里面的元素位置盡量的分布均勻?qū)?使得每個位置上的元素只有一個,這樣當(dāng)使用hash算法得到這個位置的時(shí)候,馬上就可以知道對應(yīng)位置的元素就是我們要的,不用再遍歷鏈表,這樣就大大優(yōu)化了查詢效率;
        4. 計(jì)算hash值的方法hash(int h),理論上是對數(shù)組長度取模運(yùn)算 % ,但是消耗比較大,源代碼調(diào)用為:
         /**
              * Returns index for hash code h.
              */
         static int indexFor(int h, int length) {  
             return h & (length-1); //HashMap底層數(shù)組長度總是2的n次方,每次擴(kuò)容為2倍
         }
         
         //Map數(shù)組初始化取值:
         //當(dāng) length 總是 2 的 n 次方時(shí),h& (length-1)運(yùn)算等價(jià)于對 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。
         // Find a power of 2 >= initialCapacity
         int capacity = 1;
             while (capacity < initialCapacity)  
                 capacity <<= 1; //每次增加2倍,所以總長度一定為2的次方
        
        1. 為何hash碼初始化為2的次方數(shù)探討:

          image

          分析:

          1. 當(dāng)它們和 15-1(1110)“與”的時(shí)候,產(chǎn)生了相同的結(jié)果,也就是說它們會定位到數(shù)組中的同一個位置上去,這就產(chǎn)生了碰撞,8 和 9 會被放到數(shù)組中的同一個位置上形成鏈表,那么查詢的時(shí)候就需要遍歷這個鏈 表,得到8或者9,這樣就降低了查詢的效率。同時(shí),我們也可以發(fā)現(xiàn),當(dāng)數(shù)組長度為 15 的時(shí)候,hash 值會與 15-1(1110)進(jìn)行“與”,那么最后一位永遠(yuǎn)是 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠(yuǎn)都不能存放元素了,空間浪費(fèi)相當(dāng)大,更糟的是這種情況中,數(shù)組可以使用的位置比數(shù)組長度小了很多,這意味著進(jìn)一步增加了碰撞的幾率,減慢了查詢的效率!

          2. 而當(dāng)數(shù)組長度為16時(shí),即為2的n次方時(shí),2n-1 得到的二進(jìn)制數(shù)的每個位上的值都為 1,這使得在低位上&時(shí),得到的和原 hash 的低位相同,加之 hash(int h)方法對 key 的 hashCode 的進(jìn)一步優(yōu)化,加入了高位計(jì)算,就使得只有相同的 hash 值的兩個值才會被放到數(shù)組中的同一個位置上形成鏈表。

          3. 當(dāng)數(shù)組長度為 2 的 n 次冪的時(shí)候,不同的 key 算得得 index 相同的幾率較小,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時(shí)候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了

  • 總結(jié): 根據(jù)上面 put 方法的源代碼可以看出,當(dāng)程序試圖將一個key-value對放入HashMap中時(shí),程序首先根據(jù)該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位于 Entry 鏈的頭部——具體說明繼續(xù)看 addEntry() 方法的說明。

    1. HashMap的擴(kuò)容機(jī)制

      1. 什么時(shí)候擴(kuò)容: 達(dá)到閾值,數(shù)組大小 * 加載因子默認(rèn)16* 0.75 = 12個
      2. JDK 1.7沒有引入紅黑樹,使用數(shù)組加上鏈表,JDK1.8引入紅黑樹,使用數(shù)組+鏈表+紅黑樹存儲,當(dāng)鏈表值大于等于8轉(zhuǎn)化為紅黑樹,當(dāng)remove時(shí)紅黑樹大小小于等于6時(shí)會轉(zhuǎn)化為鏈表,設(shè)置一個過渡值7防止不停地put,remove導(dǎo)致不聽轉(zhuǎn)化而使得效率低下;
      if (loHead != null) {
                      if (lc <= UNTREEIFY_THRESHOLD) //6
                          tab[index] = loHead.untreeify(map); //將紅黑樹轉(zhuǎn)化成鏈表
                      else {
                          tab[index] = loHead;
                          if (hiHead != null) // (else is already treeified)
                              loHead.treeify(tab);
                      }
                  }
      

歸納HashMap

  1. 簡單地說,HashMap 在底層將 key-value 當(dāng)成一個整體進(jìn)行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數(shù)組來保存所有的 key-value 對,當(dāng)需要存儲一個 Entry 對象時(shí),會根據(jù) hash 算法來決定其在數(shù)組中的存儲位置,在根據(jù) equals 方法決定其在該數(shù)組位置上的鏈表中的存儲位置;當(dāng)需要取出一個Entry 時(shí),也會根據(jù) hash 算法找到其在數(shù)組中的存儲位置,再根據(jù) equals 方法從該位置上的鏈表中取出該Entry。

  2. 初始化HashMap默認(rèn)值為16, 初始化時(shí)可以指定initial capacity,若不是2的次方,HashMap將選取第一個大于initial capacity 的2n次方值作為其初始長度 ;

  3. 初始化負(fù)載因子為0.75,如果超過16 * 0.75 = 12個數(shù)據(jù)就會將數(shù)組擴(kuò)容為2倍 長度為32 , 并后重新計(jì)算每個元素在數(shù)組中的位置,而這是一個非常消耗性能的操作,如果我們已經(jīng)預(yù)知 HashMap 中元素的個數(shù),那么預(yù)設(shè)元素的個數(shù)能夠有效的提高 HashMap 的性能。 如果負(fù)載印在越大,對空間利用越充分,但是會降低查詢效率,如果負(fù)載因子越小,則散列表過于稀疏,對空間造成浪費(fèi)(時(shí)間<-> 空間轉(zhuǎn)換: 0.75最佳)

  4. 多線程中的檢測機(jī)制:Fail-Fast,通過標(biāo)記modCount(使用volatile修飾,保證線程間修改的可見性) 域 修改次數(shù),在迭代初始化時(shí)候賦值,以后每次next的時(shí)候都會判斷是否相等

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
        Entry[] t = table;
        while (index < t.length && (next = t[index++]) == null)  
            ;
        }
    }
    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    
  5. 建議使用concurrent HashMap在多線程中

  6. Map的遍歷,

    1. 使用entrySet獲取鍵值對的Entry集合,只需要遍歷一次

    2. 使用keySet先遍歷所有的鍵,在根據(jù)鍵調(diào)取get(key),需要遍歷兩次

    3. JDK1,8以上新增forEach()遍歷,先遍歷hash表,如果是鏈表結(jié)構(gòu)遍歷后再遍歷下一個hash值的鏈表

    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                   Node<K,V>[] tab;
                   if (action == null)
                       throw new NullPointerException();
                   if (size > 0 && (tab = table) != null) {
                       int mc = modCount;
                       // Android-changed: Detect changes to modCount early.
                       for (int i = 0; (i < tab.length && modCount == mc); ++i) {
                           for (Node<K,V> e = tab[i]; e != null; e = e.next)
                               action.accept(e);
                       }
                       if (modCount != mc)
                           throw new ConcurrentModificationException();
                   }
         }
          Map map = new HashMap();
    
   Iterator iter = map.entrySet().iterator();
   while (iter.hasNext()) {
   Map.Entry entry = (Map.Entry) iter.next();
   Object key = entry.getKey();
   Object val = entry.getValue();
   }
  ```

HashMap頭添加擴(kuò)容導(dǎo)致死循環(huán)分析

  • 在JDK1.7之前HashMap的擴(kuò)容是從頭部添加元素,導(dǎo)致在多線程環(huán)境下put操作使得Entry鏈表形成環(huán)形數(shù)據(jù)結(jié)構(gòu),則next節(jié)點(diǎn)永不為null,導(dǎo)致死循環(huán)獲取Entry
  • 主要是在put元素HashMap擴(kuò)容階段:JDK1.7的擴(kuò)容函數(shù)為:
void transfer(Entry[] newTable) {
    Entry[] src = table; //舊的hash表
    int newCapacity = newTable.length;
    //下面這段代碼的意思是:
    //  從OldTable里摘一個元素出來,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next; //注意添加順序,是由頭部開始添加,這個地方會導(dǎo)致循環(huán)的產(chǎn)生,加入線程A執(zhí)行完畢掛起了,此時(shí)e = 3 , next = 7 , 3.next = 7的
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
  • 分析,如果對于線程A , B分別對HashMap擴(kuò)容
    1. 對于上方的擴(kuò)容函數(shù)當(dāng)兩個線程A,B運(yùn)行到Entry<K,V> next = e.next, 時(shí)
    2. 對于 舊Hash表中的3 ->7線程A此時(shí) e =3, e.next = next = 7 , 3指向的是 7 運(yùn)行到這個時(shí)候A掛起了,
    3. 這個時(shí)候B開始正常運(yùn)行擴(kuò)容 這個時(shí)候還是 3->7 添加到新表時(shí)先添加3,在3的前面添加7 此時(shí)運(yùn)行完成以后是 7->3,即7.next = 3 , 3.next = null ,這個時(shí)候還是正常的操作
    4. 但是,還記得我們上面的線程A是3->7的時(shí)候掛起了,如果此時(shí)開始運(yùn)行下面代碼 e.next = newTable[i]; e = 3 ,而此時(shí)newTable[i]已經(jīng)被線程B修改成7了,因此3.next = 7這一步替換了3步中的3.next = null,happened before原則滿足 ,將3重新添加到鏈表頭結(jié)點(diǎn)處,e重新賦值為7 ,還記得3中的 7.next = 3,注意這里并沒有修改7.next的值哦,因此put進(jìn)入死循環(huán)中
    5. 為了防止這個JDK1.8優(yōu)化了從尾部添加元素,安全性無法保證多線程會被覆蓋!

Hashtable(默認(rèn)容量 11 ,加載因子0.75)

  • 概述

    和HashMap一樣兒,Hashtable也是一個散列表,它存儲的內(nèi)容是鍵值對,implements Map<>

  • 成員變量的含義:table, count, threshold, loadFactor, modCount

    • table是一個Entry[]數(shù)組類型,Entry就是一個單向鏈表,而key-value 鍵值對就是存儲在entry中
    • count是Hashtable的大小,是保存鍵值對的數(shù)量
    • threshold為判斷是否需要擴(kuò)容 = 容量 * 加載因子
    • loadFactor 加載因子
    • modCount用來實(shí)現(xiàn)fail-fast同步機(jī)制
  • put方法

    1. 判斷value 是否為null,為空拋出異常;
    2. 計(jì)算key的hash值獲得key在table數(shù)組的位置index,如果table[index]元素不為空,進(jìn)行迭代,如果遇到相同的key,則直接替換,并返回舊的value
    3. 否則將其插入到table[index]位置上
    public synchronized V put(K key, V value) {
            // Make sure the value is not null確保value不為null
            if (value == null) {
                throw new NullPointerException();
            }
    
            // Makes sure the key is not already in the hashtable.
            //確保key不在hashtable中
            //首先,通過hash方法計(jì)算key的哈希值,并計(jì)算得出index值,確定其在table[]中的位置
            //其次,迭代index索引位置的鏈表,如果該位置處的鏈表存在相同的key,則替換value,返回舊的value
            Entry tab[] = table;
            int hash = hash(key);
            int index = (hash & 0x7FFFFFFF) % tab.length;
            for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
                if ((e.hash == hash) && e.key.equals(key)) {
                    V old = e.value;
                    e.value = value;
                    return old;
                }
            }
    
            modCount++;
            if (count >= threshold) {
                // Rehash the table if the threshold is exceeded
                //如果超過閥值,就進(jìn)行rehash操作
                rehash();
    
                tab = table;
                hash = hash(key);
                index = (hash & 0x7FFFFFFF) % tab.length;
            }
    
            // Creates the new entry.
            //將值插入,返回的為null
            Entry<K,V> e = tab[index];
            // 創(chuàng)建新的Entry節(jié)點(diǎn),并將新的Entry插入Hashtable的index位置,并設(shè)置e為新的Entry的下一個元素
            tab[index] = new Entry<>(hash, key, value, e);
            count++;
            return null;
        }
    

Hashtable與HashMap的比較

  1. HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內(nèi)部的方法基本都經(jīng)過synchronized 修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧!);

    1. 因?yàn)榫€程安全的問題,HashMap 要比 HashTable 效率高一點(diǎn)。另外,HashTable 基本被淘汰,不要在代碼中使用它 ;
    2. 對Null key 和Null value的支持: HashMap 中,null 可以作為鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應(yīng)的值為 null。。但是在 HashTable 中 put 進(jìn)的鍵值只要有一個 null,直接拋出 NullPointerException。
    3. 初始容量大小和每次擴(kuò)充容量大小的不同 : ①創(chuàng)建時(shí)如果不指定容量初始值,Hashtable 默認(rèn)的初始大小為11,之后每次擴(kuò)充,容量變?yōu)樵瓉淼?n+1。HashMap 默認(rèn)的初始化大小為16。之后每次擴(kuò)充,容量變?yōu)樵瓉淼?倍。②創(chuàng)建時(shí)如果給定了容量初始值,那么 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴(kuò)充為2的冪次方大小。也就是說 HashMap 總是使用2的冪作為哈希表的大小。
    4. 底層數(shù)據(jù)結(jié)構(gòu): JDK1.8 以后的 HashMap 在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長度大于閾值(默認(rèn)為8)時(shí),將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時(shí)間。Hashtable 沒有這樣的機(jī)制。
HashMap jdk1.8紅黑樹
  • 為何在1.8以后使用紅黑樹替換鏈表優(yōu)化hash碰撞,首先鏈表容易維護(hù)但不利于查找O(n),紅黑樹是一種特殊的二叉樹,其利于查找(二分查找法O(logn)),但相對應(yīng)的維護(hù)比較復(fù)雜(需要左旋,右旋及更改節(jié)點(diǎn)顏色等)
  • 因此對于數(shù)量小于等于6的hash碰撞使用鏈表存儲,對于數(shù)量大于等于8的hash碰撞優(yōu)化成使用紅黑樹存儲,增刪改查比較快捷,空出數(shù)字7是為了頻繁的在6-8之間增加刪除而導(dǎo)致不停的變化數(shù)據(jù)起到一個緩沖作用
    • 選擇紅黑樹是為了解決二叉查找樹的缺陷,二叉查找樹在特殊情況下會變成一條線性結(jié)構(gòu)(這就跟原來使用鏈表結(jié)構(gòu)一樣了,造成很深的問題),遍歷查找會非常慢。而紅黑樹在插入新數(shù)據(jù)后可能需要通過左旋,右旋、變色這些操作來保持平衡,引入紅黑樹就是為了查找數(shù)據(jù)快,解決鏈表查詢深度的問題,我們知道紅黑樹屬于平衡二叉樹,但是為了保持“平衡”是需要付出代價(jià)的,但是該代價(jià)所損耗的資源要比遍歷線性鏈表要少,所以當(dāng)長度大于8的時(shí)候,會使用紅黑樹,鏈表長度低于6,就把紅黑樹轉(zhuǎn)回鏈表,因?yàn)楦静恍枰爰t黑樹,引入反而會慢。
紅黑樹
  • 在紅黑樹中的hash值一致,他是根據(jù)comporeTo()來比較數(shù)值的:比如對于常見的String鍵來說,相同的hash值是根據(jù)native compareTo()比較兩個字符串差值來計(jì)算出紅黑樹的大小,因此紅黑樹中不存在相同數(shù)值的數(shù)據(jù),一旦相同就表示是存儲的相同的鍵(equals方法相同)需要替換值了
  • 簡單說一下什么是紅黑樹:特點(diǎn)
    1. 根節(jié)點(diǎn)為黑色
    2. 子節(jié)點(diǎn)跟他的父節(jié)點(diǎn)不能同時(shí)是紅色:即紅色節(jié)點(diǎn)的子節(jié)點(diǎn)都是黑色節(jié)點(diǎn)
    3. 從根節(jié)點(diǎn)到任何一個葉子結(jié)點(diǎn)其經(jīng)過的黑色節(jié)點(diǎn)個數(shù)相同
    4. 所有空葉子結(jié)點(diǎn)都是黑色的,這里的葉子結(jié)點(diǎn)指的是空結(jié)點(diǎn),常用 NIL 表示,一般可以省略,但在用到的時(shí)候需要補(bǔ)全它
    • 注意:由于特性2,3可確保沒有一條路徑會比其他路徑長出倆倍,因此它是一個相對平衡的二叉樹,相對于普通二叉樹可能退化成鏈表更優(yōu)秀,同時(shí)相對于完全二叉樹(最大跟最小最多差1且最深的節(jié)點(diǎn)都位于左邊),平衡二叉樹(左子樹跟右子樹深度差的絕對值不能超過1,且左右子樹也是平衡二叉樹)需要轉(zhuǎn)化步驟更少,通過更少的更改數(shù)據(jù)既能達(dá)到相對平衡的效果,對于Hash沖突解決最優(yōu),無非還是空間換取時(shí)間操作
  • 左旋跟右旋的理解


    image
  • 分析:
    1. 對L進(jìn)行左旋就是將L的右節(jié)點(diǎn)設(shè)置成x的父節(jié)點(diǎn),即將L變成一個左節(jié)點(diǎn)==>左旋中的左是指將旋轉(zhuǎn)的節(jié)點(diǎn)變成一個左節(jié)點(diǎn)
    2. 對P進(jìn)行右旋就是將P的左節(jié)點(diǎn)設(shè)置成p的父節(jié)點(diǎn),即將P變成一個右節(jié)點(diǎn)==>右旋中的右是指將旋轉(zhuǎn)的節(jié)點(diǎn)變成一個右節(jié)點(diǎn)
查找 時(shí)間復(fù)雜度O(logN)
  • 對于紅黑樹的查找來說


    image
插入
  • 注意:
    1. 紅黑樹插入節(jié)點(diǎn)均是最底層的葉子節(jié)點(diǎn)處
    2. 插入節(jié)點(diǎn)顏色為紅色,不為黑色是因?yàn)楦鶕?jù)特性3,任何一個葉子節(jié)點(diǎn)經(jīng)過黑色節(jié)點(diǎn)個數(shù)相同,如果是黑色一定會更改其中個數(shù)導(dǎo)致不相同而需要重新左旋右旋更改顏色等多余操作,如果是紅色則有可能不需要做任何操作也達(dá)到平衡了
  • 假設(shè): 待插入節(jié)點(diǎn)為N,其父節(jié)點(diǎn)為P, 其祖父節(jié)點(diǎn)為G,U是N的叔叔節(jié)點(diǎn)(P的兄弟節(jié)點(diǎn)),則插入紅黑色有下面幾種情況:
    1. N是根節(jié)點(diǎn),即紅黑樹的第一個節(jié)點(diǎn)
    2. N的父節(jié)點(diǎn)P為黑色
    3. P是紅色的不是根節(jié)點(diǎn)(根節(jié)點(diǎn)一定是黑色),他的兄弟節(jié)點(diǎn)U也是紅色的
    4. P為紅色,而U為黑色
  • 對于以上1,2,3情況來說,他們都不涉及旋轉(zhuǎn),只涉及顏色翻轉(zhuǎn)而已
    1. 對于情況1 , 2 并不影響紅黑樹的性質(zhì)即不會破壞平衡,直接插入即可
    2. 對于3來說,P,U均為紅色,直接變色即可,P和U變成黑色,G變成紅色,如果G是根節(jié)點(diǎn),直接變黑,否則則將G作為插入節(jié)點(diǎn),遞歸向上檢查是否造成不平衡
  • 對于情況4來說,P為紅色,U為黑色,這種分成四種情況:
    1. P是G的左孩子,若N是P的左孩子,則將祖父節(jié)點(diǎn)G右旋即可
    2. P是G的左孩子,若N是P的右孩子,則將P先左旋轉(zhuǎn)(左轉(zhuǎn)后情況如1),然后再將祖父節(jié)點(diǎn)G右旋轉(zhuǎn)
    3. P是G的右孩子,若N是P的右孩子,則將祖父節(jié)點(diǎn)G左旋轉(zhuǎn)即可
    4. P是G的右孩子,若N是P的左孩子,則先將P右旋轉(zhuǎn)(右旋轉(zhuǎn)后情況如3),然后在將祖父節(jié)點(diǎn)G左旋轉(zhuǎn)
  • 由于添加只能是葉子節(jié)點(diǎn),所以只能是這四種情況,注意黑色U可能是Nul節(jié)點(diǎn)的
刪除
  • 對于刪除操作來說,只能是一下四種情況:
    1. 刪除節(jié)點(diǎn)的左,右子樹都不為Nul;
    2. 刪除節(jié)點(diǎn)的左子樹為Nul,右子樹非空;
    3. 刪除節(jié)點(diǎn)的右子樹為Nul,左子樹非空;
    4. 刪除節(jié)點(diǎn)的左,右子樹均為Nul;
      在TreeMap源碼中可以查看以上四種情況分析: 對于1來說,刪除節(jié)點(diǎn)最終還是刪除非空的一個葉子節(jié)點(diǎn)
        private void deleteEntry(TreeMapEntry<K,V> p) {
          modCount++;
          size--;
    
          // If strictly internal, copy successor's element to p and then make p
          // point to successor.
          if (p.left != null && p.right != null) {  //對應(yīng)第一種情況,左,右子樹均不為Nul
              TreeMapEntry<K,V> s = successor(p);
              p.key = s.key;
              p.value = s.value;
              p = s;
          } // 沒有返回,將第一種情況轉(zhuǎn)化成了 2,3,4 種情況
    
          // Start fixup at replacement node, if it exists.
          TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);
    
          if (replacement != null) { //對應(yīng)于2,3中情況,左,右子樹有一個為Nul
              // Link replacement to parent
              replacement.parent = p.parent;
              if (p.parent == null) 
                  root = replacement;
              else if (p == p.parent.left)
                  p.parent.left  = replacement;
              else
                  p.parent.right = replacement;
    
              // Null out links so they are OK to use by fixAfterDeletion.
              p.left = p.right = p.parent = null;
    
              // Fix replacement
              if (p.color == BLACK)
                  fixAfterDeletion(replacement);
          } else if (p.parent == null) { // return if we are the only node.
              root = null;
          } else { //  第四種情況,左右子樹均為Nul
              if (p.color == BLACK)
                  fixAfterDeletion(p);
    
              if (p.parent != null) {
                  if (p == p.parent.left)
                      p.parent.left = null;
                  else if (p == p.parent.right)
                      p.parent.right = null;
                  p.parent = null;
              }
          }
      }
    
  • 以上可以至源碼中查看算法實(shí)現(xiàn)細(xì)節(jié),如有不當(dāng)之處,歡迎給予評論指正!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容