数据结构(1)-SparseArray

331 阅读4分钟

1 SparseArray 简介

SparseArray是Android特有的map类型集合容器,位于android.util包中,通过双数组实现,存储key的数组保持递增,因此可以通过二分查找来进行插入、遍历和删除等操作,如上图所示,与HashMap相比有以不同:

  • 空间复杂度低于HashMap,主要有以下原因,1⃣️是key是基本数据类型避免了auto-boxing,2⃣️是不需要封装成数据结构Entry来存储
  • 用数组实现,key递增插入数组,通过二分查找实现遍历,不同于HashMap的数组+链表/红黑树,因此时间复杂度低于HashMap
  • 为了提升效率,SparseArra在删除元素的时候并未立即删除和挪动后面元素,而是用DELETED进行标示,只有计算size、添加元素需要扩容或者通过index操作时进行gc
  • 部分接口未进行index效验,使用时防止出现IndexOutOfBoundsException,比如setValueAt(index, value)、removeAt(index),

2 SparseArray 源码分析

2.1 接口分析

 /** 通过获取元素 */
 public E get(int key)
 public E get(int key, E valueIfKeyNotFound)
 /** 删除指定index/key的元素 */
 public void delete(int key)
 public E removeReturnOld(int key)
 public void remove(int key)
 public void removeAt(int index)
 public void removeAtRange(int index, int size)
 /** 添加key-value */
 public void put(int key, E value)
 /** 替换index位置元素value值 */
 public void setValueAt(int index, E value)
 /** 存储元素数量 */
 public int size()
 /** 清空集合 */
 public void clear()
 /** 在已有元素尾部添加元素key-value,效率高 */
 public void append(int key, E value)
 /** 获取指定index位置key */
 public int keyAt(int index)
 /** 获取指定index位置value */
 public E valueAt(int index)
 /** 获取key的index位置 */
 public int indexOfKey(int key)
 /** 获取value的index位置 */
 public int indexOfValue(E value)

2.2 源码分析

a. 成员变量

public class SparseArray<E> implements Cloneable {
    // 标示,标示该位置可以被使用或者需要gc
    private static final Object DELETED = new Object();
    // 可以gc标示
    private boolean mGarbage = false;
    // 存储key的数组
    private int[] mKeys;
    // 存储value的数组
    private Object[] mValues;
    // 有效大小
    private int mSize;
    }

b. 构造方法

分配数组大小的ArrayUtils.newUnpaddedObjectArray大小根据文档是不小于size,具体可以参考https://cs.android.com/android/platform/superproject/+/master:art/runtime/mirror/array-alloc-inl.h

public SparseArray() {
        // 默认大小为10
        this(10);
    }
 
 public SparseArray(int initialCapacity) {
        // 初始化大小为0时mKeys和mVaules数组初始化为null
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            // 分配数组大小
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        // 当前大小为0
        mSize = 0;
    }

c. get方法

    public E get(int key) {
        return get(key, null);
    }
    public E get(int key, E valueIfKeyNotFound) {
        // 根据key二分查找,返回结果是正数表示找到了,负数表示未找到,但是-i是该key的插入位置
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // i < 0 未找到, DELETED表示该元素存在但被删除了返回默认值
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

d. remove方法

   public void delete(int key) {
        // 二分查找获取index位置 
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        if (i >= 0) {
            // 找到了index位置并未被删除设置DELETED标示,同时设置gc使能标示
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
    public void removeAt(int index) {
        // 删除直接index位置元素,可能出现IndexOutOfBoundsException异常
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }

e. put方法

  public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // key-value已存在,替换value
        if (i >= 0) {
            mValues[i] = value;
        } else {
            // ~i就是插入位置
            i = ~i;
            // 插入位置元素已无效,直接替换key和value
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            // gc后需要从新计算插入位置
            if (mGarbage && mSize >= mKeys.length) {
                gc();

                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            // 自增产在指定index位置插入元素,具体实现见下面
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
 public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
        assert currentSize <= array.length;
        // 当前数组可以存放size+1
        if (currentSize + 1 <= array.length) {
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            array[index] = element;
            return array;
        }
        // 从新创建数组,growSize()自增长二倍,最小值为8
        @SuppressWarnings("unchecked")
        T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
                growSize(currentSize));
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }

e. append方法

    // 效率高,不需要通过二分查找计算插入位置
    public void append(int key, E value) {
        // 先判断是否可以插入队尾
        if (mSize != 0 && key <= mKeys[mSize - 1]) {
            put(key, value);
            return;
        }
        if (mGarbage && mSize >= mKeys.length) {
            gc();
        }
        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        mSize++;
    }

f. gc方法

什么时候gc?

  • 操作方法依赖index位置时,gc前的index位置与传入的index存在不同,需要gc,比如keyAt(int index)
  • 计算大小,比如size(),通过gc来获取真实size大小
    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];
            // 真实数据进行copy
            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);
    }

3 SparseArray 总结

  1. DELETED标示位配合gc可以提升效率
  2. System.arraycopy、ArrayUtils.newUnpaddedArray等JNI方法可以多多使用
  3. 根据不同的数据结构提供特有的方法来提升效率,比如append,当确定要拆入的key是递增的,插入效率就很高
  4. SparseBooleanArray,SparseLongArray等实现与SparseArray基本类似,优点是不仅仅key避免了auto-boxing,value也避免了auto-boxing,推荐使用,缺点是没有gc和DELETED标识位,造轮子的机会来了