SparseArray:解析與實現

介紹

Android提供了SparseArray,這也是一種KV形式的數據結構,提供了相似於Map的功能。可是實現方法卻和HashMap不同。它與Map相比,能夠說是各有千秋。java

優勢android

  • 佔用內存空間小,沒有額外的Entry對象
  • 沒有Auto-Boxing

缺點git

  • 不支持任意類型的Key,只支持數字類型(int,long)
  • 數據條數特別多的時候,效率會低於HashMap,由於它是基於二分查找去找數據的

相關參考 SparseArray vs HashMapgithub

總的來講,SparseArray適用於數據量不是很大,同時Key又是數字類型的場景。
好比,存儲某月中天天的某種數據,最多也只有31個,同時它的key也是數字(可使用1-31,也可使用時間戳)。
再好比,你想要存儲userid與用戶數據的映射,就可使用這個來存儲。數組

接下來,我將講解它的特性與實現細節。數據結構

直觀的認知

它使用的是兩個數組來存儲數據,一個數組存儲key,另外一個數組來存儲value。隨着咱們不斷的增長刪除數據,它在內存中是怎麼樣的呢?咱們須要有一個直觀的認識,能幫助咱們更好的理解和體會它。函數

初始化的狀態

內部有兩個數組變量來存儲對應的數據,mKeys用來存儲key,mValues用來存儲泛型數據,注意,這裏使用了Object[]來存儲泛型數據,而不是T[]。爲何呢?這個後面在講。
初始化的狀態工具

插入數據

以下圖所示,插入數據,老是「緊貼」數組的左側,換句話說,老是從最左邊的一個空位開始使用。我一開始沒詳細探究的時候,都覺得它是相似HashMap那樣稀疏的存儲。學習

另外一個值得注意的事情是,key老是有序的,無論通過多少次插入,key數組中,key老是從小到大排列。google

插入數據

擴容

當一直插入數據,快滿的時候,就會自動的擴容,建立一個更大的數組出來,將現有的數據所有複製過去,再插入新的數據。這是基於數組實現的數據結構共同的特性。

刪除

刪除是使用標記刪除的方法,直接將目標位置的有效元素設置爲一個DELETED標記對象。
刪除數據

查詢數據

怎麼查數據呢?
好比咱們查5這個數據get(5),那麼它是在mKeys中去查找是否存在5,若是存在,返回index,而後用這個index在對應的mValues取出對應的值就行了。

實現

接下來咱們按照本身的理解,來實現這樣的一個數據結構,從而學習它的一些細節和思想,加深對它的理解,有利於在生產中,能更有效的,正確的使用它。

肯定接口(API)

首先,肯定一下,咱們須要暴露什麼樣的功能給別人使用。固然了,答案是顯而易見的,固然是插入,查詢,刪除等功能了。

public class SparseArray<E> {

    public SparseArray() {
    }

    public SparseArray(int initCap) {
    }

    public void put(int key, E value) {  
    }

    public E get(int key) {  
    }

    public void delete(int key) {
    }

    public int size() {
    }
  
}

上面列舉了咱們須要的功能,無參構造函數,有參數構造函數(指望能主動設置初始容量),put數據,get數據,刪除數據,以及獲取當前數據有多少。

實現put方法

put數據是最核心的方法,通常咱們開發一個東西,也是先開發建立數據的功能,這樣才能接着開發展現數據的功能。因此咱們先來實現put方法。

按照以前的理解,咱們須要一些成員變量來存儲數據。

private int[] mKeys;
private Object[] mValues;
private int mSize = 0;

須要先找到put到什麼位置
這裏會有兩種狀況:

  • 我要put的key不存在,應該put到什麼地方?
  • 我要put的key已經存在,直接覆蓋

所以第一步,須要先找一下,當前key,是否存在。咱們使用二分查找來處理。

public void put(int key, E value) {
    int i = BinarySearch.search(mKeys, mSize, key);
    if (i >= 0) {
        // 找到了有兩種狀況
        // 1.是對應的mValues有一個有效的數據對象,直接覆蓋
        // 2.對應的mValues裏面是一個DELETED對象,一樣的,直接覆蓋
        mValues[i] = value;
    } else {

    }
}

若是在數組中找到了,那麼操做就很簡單,直接覆蓋就完事了。

若是沒找到呢,咱們須要將數據插入到正確的位置上,這個所謂正確的位置,指的是,插入以後,依然保證數組有序的狀況。打個比方:1, 4, 5, 8,請問3應該插入哪裏,固然是放到index=1的地方,結果就是1, 3, 4, 5, 8了。

那若是key不存在,怎麼知道應該放到哪裏呢?

咱們來看一下這個二分查找,它幫咱們解決了這個小問題。

public static int search(int[] arr, int size, int target) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
        final int mid = (lo + hi) >>> 1;
        final int value = arr[mid];

        if (value == target) {
            return mid;
        } else if (value > target) {
            hi = mid - 1;
        } else {
            lo = mid + 1;
        }
    }

    return ~lo;
}

按照傳統的思想,查找類的API,若是找不到,通常都會返回-1,可是這個二分查找,返回了lo的取反。這會達到什麼效果呢。

狀況1:數組是空的,那麼查找任何東西,都找不到,那會怎麼樣?根據代碼能夠知道,循環都進不去,那麼直接返回了~0,也就是最大的負數。咱們只須要知道它是一個負數。

狀況2:數組不是空的,好比1, 3, 5,咱們找2,這裏簡單的單步執行一下:

lo = 0, size = 3, hi = 2, 好,進入循環
mid = (0 + 2) / 2 = 1, value = 3
value > 2, 因此 hi = 1 - 1 = 0, 再次循環
mid = (0 + 0)  / 2 = 0, value = 1
value < 2, so, lo = 0 + 1; 退出循環
返回~1

若是你在嘗試去驗算其餘狀況,你會發現,返回值恰好是它應該放置的位置的取反。換句話說,返回值再取反後,就能夠獲得,這個key應該插入的位置。

這應該是二分查找的一個小技巧。很是的實用!

接下來,想想,0取反是負數,任何正數取反,也都是負數,也就是說,只要是負數,就表明沒找到,再將這個數取反,就獲得了,應該put的位置!

因此,代碼繼續實現爲:

public void put(int key, E value) {
    int i = BinarySearch.search(mKeys, mSize, key);
    if (i >= 0) {
        // 找到了有兩種狀況
        // 1.是對應的mValues有一個有效的數據對象,直接覆蓋
        // 2.對應的mValues裏面是一個DELETED對象,一樣的,直接覆蓋
        mValues[i] = value;
    } else {
        i = ~i;
        mKeys = GrowingArrayUtil.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtil.insert(mValues, mSize, i, value);
        mSize++;
    }
}

實現get方法

接下來,咱們實現get方法。
get方法實現就比較簡單了,只須要經過二分查找找到對應的index,再從value數組中取出對象便可。

public E get(int key) {
    // 首先查找這個key存不存在
    int i = BinarySearch.search(mKeys, mSize, key);

    if (i < 0) {
        return null;
    } else {
        return (E)mValues[i];
    }
}

實現delete方法

delete方法,就是刪除某個key,對應的細節是,找到這個key是否存在,若是存在的話,將value數組中對應位置的數據設置爲一個常量DELETED。這樣作的好處就是比較快捷,而不須要真正的去刪除元素。固然因爲這個DELETED對象存在value數組中,對put和get以及size方法都會帶來一些影響。

下面的代碼,定義一個靜態的final變量DELETED用來做爲標記已經刪除的變量。
另外一個成員變量標記,當前value數組中是否有刪除元素這個狀態信息。

private static final Object DELETED = new Object();

/**
 * 標記是否有DELETED元素標記
 * */
private boolean mHasDELETED = false;

public void delete(int key) {
    // 刪除的時候爲標記刪除,先要找到是否有這個key,若是沒有,就不必刪除了;
    // 找到了key看一下對應的value是否已是DELETED,若是是的話,也不必再刪除了
    int i = BinarySearch.search(mKeys, mSize, key);
    if (i >= 0 && mValues[i] != DELETED) {
        mValues[i] = DELETED;
        mHasDELETED = true;
    }
}

實現size方法

size方法返回在這個容器中,數據對象有多少個。因爲DELETED對象的存在,key數組和value數組,以及成員變量mSize都無法靠譜得直接獲得有效數據的count。

所以這裏須要一個內部的工具方法gc(),它的做用就是,若是有DELETED對象存在,那麼就從新整理一下數組,將DELETED對象都移除,數組中只保留有效數據便可。

先來看gc的實現

private void gc() {

    int placeHere = 0;

    for (int i = 0; i < mSize; i++) {
        Object obj = mValues[i];
        if (obj != DELETED) {

            if (i != placeHere) {
                mKeys[placeHere] = mKeys[i];
                mValues[placeHere] = obj;
                mValues[i] = null;
            }
            placeHere++;
        }

    }

    mHasDELETED = false;
    mSize = placeHere;
}

它的內部邏輯很簡單,就是從頭至尾遍歷value數組,把每個不是DELETED的對象都從新放置一遍,覆蓋掉前面的DELETED對象。

而後,咱們再看一下size的實現

public int size() {
    if (mHasDELETED) {
        gc();
    }
    return mSize;
}

完善get方法

假設有這樣的一個場景,put(1, a), put(2, b), delete(2), get(2)。按照如今的get實現,就會返回DELETED對象出去,因此,因爲DELETED的存在,咱們須要完善一下get方法的邏輯。

public E get(int key) {
    // 首先查找這個key存不存在
    int i = BinarySearch.search(mKeys, mSize, key);

    // 這裏有兩種狀況
    // 若是key小於0,說明在mKeys中,沒有目標key,沒找到
    // 若是key大於0,還要看一下,對應的mValues中,是否那個元素是DELETED,由於刪除的時候是標記刪除的
    // 以上兩種狀況都是沒有找到
    if (i < 0 || mValues[i] == DELETED) {
        return null;
    } else {
        return (E)mValues[i];
    }
}

完善put方法

補充的代碼上面我都寫了註釋,講解了這兩坨額外的代碼是用來處理什麼狀況的。

public void put(int key, E value) {
    int i = BinarySearch.search(mKeys, mSize, key);
    if (i >= 0) {

        // 找到了有兩種狀況
        // 1.是對應的mValues有一個有效的數據對象,直接覆蓋
        // 2.對應的mValues裏面是一個DELETED對象,一樣的,直接覆蓋

        mValues[i] = value;
    } else {
        i = ~i;

        // 這一段代碼是處理這一的場景的
        // 1 2 3 5, delete 5, put 4
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        // 另外一種狀況
        // 若是有刪除的元素,而且數組裝滿了,這個時候須要先GC,再從新搜一下key的位置
        if (mHasDELETED && mSize >= mKeys.length) {
            gc();
            i = ~BinarySearch.search(mKeys, mSize, key);
        }

        mKeys = GrowingArrayUtil.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtil.insert(mValues, mSize, i, value);
        mSize++;
    }
}

最後,GrowingArrayUtil.insert是作了什麼?

其實提及來很簡單,用一個過程來歸納一下通常狀況。

[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
insert(index=2, value=99)
1.複製index=2之前的元素 [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
2.複製index=2之後的元素,日後挪一位 [1, 2, 3, 3, 4, 5, 0, 0, 0, 0]
3.將index=2的位置,放入99 [1, 2, 99, 3, 4, 5, 0, 0, 0, 0]

固然,這裏要處理,若是恰好數據滿了,插入新數據,就須要建立一個新的,更大的數組來複制之前的數據了。

/**
 * @param rawArr 原始數組
 * @param size 有效數據的長度,與數組長度不同,若是數組長度大於有效數據的長度,那麼往裏面插入數據是OK的
 *             若是有效數據的長度等於數組的長度,那麼要插入數據,就要建立更大的數組
 * @param insertIndex 插入index
 * @param insertValue 插入到index的數值
 * */
public static int[] insert(int[] rawArr, int size, int insertIndex, int insertValue) {
    if (size < rawArr.length) {
        System.arraycopy(rawArr, insertIndex, rawArr, insertIndex + 1, size - insertIndex);
        rawArr[insertIndex] = insertValue;
        return rawArr;
    }

    int[] newArr = new int[rawArr.length * 2];
    System.arraycopy(rawArr, 0, newArr, 0, insertIndex);
    newArr[insertIndex] = insertValue;
    System.arraycopy(rawArr, insertIndex, newArr, insertIndex + 1, size - insertIndex);
    return newArr;
}

public static <T> Object[] insert(Object[] rawArr, int size, int insertIndex, T insertValue) {
    if (size < rawArr.length) {
        System.arraycopy(rawArr, insertIndex, rawArr, insertIndex + 1, size - insertIndex);
        rawArr[insertIndex] = insertValue;
        return rawArr;
    }

    Object[] newArr = new Object[rawArr.length * 2];
    System.arraycopy(rawArr, 0, newArr, 0, insertIndex);
    newArr[insertIndex] = insertValue;
    System.arraycopy(rawArr, insertIndex, newArr, insertIndex + 1, size - insertIndex);
    return newArr;
}

好了,關於SparseArray的講解就到這裏結束了。完整的源碼能夠查看我寫的,也能夠查看官方的

以前提到的一些疑問點

  • 爲何用Object[],而不是T[]

個人理解是,若是使用泛型數組T[],你就必須構造出一個泛型數組,那麼構造泛型數組,你須要能建立泛型對象,也就是說,必須調用T的構造函數才能建立泛型對象,可是因爲是泛型,構造函數是不肯定的,只能經過反射的形式來調用,這樣顯然就效率和穩定性上有一些問題。所以大多數泛型的實現,都是經過Object對象來存儲泛型數據。

若是你以爲這篇內容對你有幫助的話,不妨打賞一下,哪怕是小小的一份支持與鼓勵,也會給我帶來巨大的動力,謝謝:)
你的鼓勵是我創做的動力!

相關文章
相關標籤/搜索