数据结构:SparseArray 和 ArrayMap原理

1,262 阅读3分钟

SparseArray、ArrayMap 是Android独有的,跟HashMap相比,是用时间换空间,能节省内存。内部有两个数组,分别存放key和value,key的类型为int,value支持泛型。其衍生出来的几个类:
SparseXXXArray: key类型为int,value类型为xxx(xxx为基础数据类型)
LongSparseArray: key类型为long,value支持泛型

性能对比:

www.jianshu.com/p/7b9a1b386…

SparseArray原理:

内部存放key和value的数组,结构如下:

private int[] mKeys;
private Object[] mValues;

WeChatb5d375f11e4d9e82d8d1d98df8c2e560.png

put 方法

  1. 首先使用了二分搜索法 O(log n)查找key数组中是否有该key,如果找到了就直接返回,是个正数;如果没找到,就取反。取反的作用其实是让查找的位置变成负数,这样根据是否大于0来判断是否找到,取反后再取反可以还原。取反效率比变成负数的效率高。

举例:
~x=-(x+1)
~10 = -(10+1) = -11
~(-11) = -(-11+1)=10
10的取反是-11,-11的取反是10

static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
    //细节,>>>为除2,效率更高
        final int mid = (lo + hi) >>> 1;
        final int midVal = array[mid];

        if (midVal < value) {
            lo = mid + 1;
        } else if (midVal > value) {
            hi = mid - 1;
        } else {
            return mid;  // value found
        }
    }
    return ~lo;  // value not present
}
public void put(int key, E value) {
    //二分搜索法
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i >= 0) {
    //大于0代表找到了,直接复制
        mValues[i] = value;
    } else {
        //取反,获取二分搜索法得到的最终位置
        i = ~i;
        //小于数组大小,并且该位置的数据被删除过,也是直接复制,key也需要赋值
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        //数量大于数组大小,并且删除过,调用gc进行垃圾回收、压缩
        if (mGarbage && mSize >= mKeys.length) {
            gc();
            // 压缩后,重新二分搜索,获取位置
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        //插入数据到到指定位置
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}

get

方法比较简单,二分查找到索引后返回value

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];
    }
}

delete

直接设置i位置的value为DELETED,并且设置标记为mGarbage

public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

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

gc

比较简单,赋值key和value数值,然后循环数组,如果值不为DELETED,数值往前移。

private void gc() {
    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;
}

ArrayMap原理:

首先内部有两个数组,mHashes存放key的hash值,mArray里面存放key和value。

int[] mHashes;
Object[] mArray;

WeChatd405d9164a4b2b9e22e9126c272d51ab.png

put

根据key,计算出hash,然后同样通过二分查找法计算索引位置,根据该索引位置,将key和value放入mArray中。插入值需要移动数组,通过System.arraycopy方法,在SparseArray通过GrowingArrayUtils.insert,内部也用了System.arraycopy来移动数组,效率更高。

public V put(K key, V value) {
    final int osize = mSize;
    ……
    //使用hash,再计算index
    hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
    index = indexOf(key, hash);
    //如果已存在该key,则直接替换value
    if (index >= 0) {
        index = (index<<1) + 1;
        final V old = (V)mArray[index];
        mArray[index] = value;
        return old;
    }

    index = ~index;
    //如果数组满,则需要扩容
    if (osize >= mHashes.length) {
        //如果大于8,则1.5倍扩展,如果4~8,则扩展到8,如果小于4,则扩展为4
        final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
        ……
        final int[] ohashes = mHashes;
        final Object[] oarray = mArray;
        //扩展数据
        allocArrays(n);
        ……
        if (mHashes.length > 0) {
            ……
            //将旧数组复制到新数组中
            System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
            System.arraycopy(oarray, 0, mArray, 0, oarray.length);
        }
        freeArrays(ohashes, oarray, osize);
    }
    //将index后的数据后移1位
    if (index < osize) {
        ……
        System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
        System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
    }
    ……
    //赋值
    mHashes[index] = hash;
    mArray[index<<1] = key;
    mArray[(index<<1)+1] = value;
    mSize++;
    return null;
}

get

方法特别简单,计算出索引值后,直接返回值

@Override
public V get(Object key) {
    final int index = indexOfKey(key);
    return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}

remove

计算出索引值后调用removeAt

public V removeAt(int index) {
    final Object old = mArray[(index << 1) + 1];
    final int osize = mSize;
    final int nsize;
    if (osize <= 1) {
        // 置空
        ……
    } else {
        nsize = osize - 1;
        //满足数组长度大于8且数据小于长度的三分之一,
        if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
            // Shrunk enough to reduce size of arrays.  We don't allow it to
            // shrink smaller than (BASE_SIZE*2) to avoid flapping between
            // that and BASE_SIZE.
            final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
            ……
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            //重新分配数组,此处的作用是压缩
            allocArrays(n);
            ……
            //复制到新的数组中
            if (index > 0) {
                ……
                System.arraycopy(ohashes, 0, mHashes, 0, index);
                System.arraycopy(oarray, 0, mArray, 0, index << 1);
            }
            //将index后的数据前移1位
            if (index < nsize) {
                ……
                System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            }
        } else {
            //将index后的数据前移1位
            if (index < nsize) {
                ……
                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;
        }
    }
    ……
    mSize = nsize;
    return (V)old;
}

ArrayMap的remove方法是每删除一个元素都会对数组进行移动,不像SparseArray一样,先赋空值,gc里才移数组。