Android特别的数据结构(一) SparseArray源码解析

news2025/2/28 20:03:32

1.数据结构

class SparseArray<E> implements Cloneable

  • 由两个数组构成,一个数组mKeys类型为int[],存放Key,一个数组mValues类型为 E[],存放Value。
  • Key数组升序排列。
  • 默认初始容量:10
  • 扩容:
    • 如果当前长度<=4,则扩容到8
    • 否则,扩容为当前长度的 2 倍
  • 没有哈希冲突问题!因为key就是int类型数据。

请添加图片描述

相关源码:

public class SparseArray<E> implements Cloneable {
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
    private int[] mKeys;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
    private Object[] mValues;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
    private int mSize;
}

2. 插入/更新

  • put(int key, E value)
  • set(int key, E value)

put() 插入语法如下

SparseArray sparseArray = new SparseArray();
sparseArray.put(4,obj2);

主要流程为:

  1. 二分查找key所在的位置,如果找到,则替换原有value
  2. 如果没有找到,则将后续元素平移一格,将自己插入

图示如下:

请添加图片描述

看到源码:

//添加元素
public void set(int key, E value){put(key,value);}
public void put(int key, E value) {
    //在现有的范围mSize中,二分查找key在mKeys中的下标
    //解释(1)
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    //由于升序二分,如果连续插入升序key,那么时间复杂度就是O(logn)
    //如果逆序插入key,那么时间复杂就是O(n)因为要平移
    if (i >= 0) {
        //如果找到了,就覆盖,因为key只为integer,所以不存在hash冲突
        mValues[i] = value;
    } else {
        //如果没找到
        //包括符号位按位取反,拿到的是二分搜索最后的lo值
        //解释(1)
        i = ~i;
        //如果lo值对应的value是DELETED,就直接填入
        //解释(2)
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        //如果lo值位置已经被占用了,在平移数组并插入元素之前,先清理废弃的下标
        //解释(3)
        if (mGarbage && mSize >= mKeys.length) {
            //先清理一下数组
            gc();
            //然后重新计算新的lo位置
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        //维护Key的升序!!!key数组没有内部碎片!!!(数组平移->整理)
        //安全插入,可能出现扩容和数组元素平移
        //解释(4)
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        //添加完成
        mSize++;
    }
}

解释(1)

过程中,通过ContainerHelpers进行二分查找,通过GrowingArrayUtils进行元素插入。我们先来看到ContainerHelpers,它的工作原理就是我们熟知的二分查找:

  1. 如果mid的值就是要找的,返回mid
  2. 否则更新low、high两个指针的值,继续二分查找
  3. 如果最后都找不到,返回一个负数
//ContainerHelpers
static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
        // >>>1相当于 除以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
}

与平时写的二分查找几乎一样,但是返回的是~lo,这是对lo的值按位取反。由于lo逻辑上保证了值为正数,所以~lo一定是负数,满足了没有查找到的返回负数的约定。但由于我们还需要在原先找的最后一个位置进行插入,所以需要复用lo的值,后续只需要再进行一次 ~ 按位取反运算即可,即 lo = ~~lo

解释(2)

如果找到的下标存的value是DELETED,说明这个位置可以直接替换为新的值,而不需要先清理空下标,再平移元素,再插入新值。

public class SparseArray<E> implements Cloneable {
    private static final Object DELETED = new Object();
    public void delete(int key){
        //让key对应的value置为 DELETED
        //...
        mValues[i] = DELETED;
        //提示下次插入的时候调整数组结构
        mGarbage = true;
    }
}

解释(3)

如果需要插入的位置被占用,就只能先平移数组,然后再把自己插入。在此之前,数组中可能存在上述DELETED节点(如果mGarbage为真),所以要先通过gc()清理,然后重新计算插入位置i,再进行数组平移和元素插入。

public class SparseArray<E> implements Cloneable {
    //目的是用来清理那些被删掉的value的节点 (标记-整理 算法)
    //思路类似移动零,把后续有用的值往前整理
    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];
			//找到value为DELETED的位置
            if (val != DELETED) {
                if (i != o) {
                    //整理
                    keys[o] = keys[i];
                    values[o] = val;
                    //将用不到的置空,去掉强引用
                    values[i] = null;
                }
                o++;
            }
        }
        //整理完成
        mGarbage = false;
        //最后o所到达的下标为所有可用数据的最大下标,此后的key都是无用的,且value全都被置空
        mSize = o;
    }
}

上述代码可以参考 leetcode-移动零 的算法设计,用o来表示:后续可用的元素要整理到的下标。

解释(4)

真正要插入元素了,需要先将数组进行移动,这也是SparseArray时间复杂度O(N)的问题来源,二分查找复杂度为O(N),但移动元素的平均时间复杂度就是数组插入元素的时间复杂度O(N)。对于mKeys和mValues的处理是一样的,主要流程为:

  1. 如果移动后不会越界,就移动,然后插入新的值
  2. 如果会越界,需要先进行扩容,然后拷贝元素到新数组,再插入新的值。
//GrowingArrayUtils
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    //如果在原来的数组中还能放得下
    assert currentSize <= array.length;
    //看看能否将lo+1位置以后的内容全都右移一个单元
    if (currentSize + 1 <= array.length) {
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        //然后将当前的值填入
        array[index] = element;
        return array;
    }
    //如果已经放不下了,只能申请一块扩容后的一块空间
    //将原来的内容拷贝到新的,同时将element插入

    //暂且认为是让VM虚拟机创建了一个新的数组
    @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;
}

//扩容:
public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
}

3. 插入/更新元素

  • append(int key, E value)

假设当前SparseArray内容如下:

请添加图片描述

如果我要插入的key为40(大于已存在的最大key),直接加在最后就好了,时间复杂度为O(1),但如果按照 put() 的设计,要先二分查找,找到新的mKeys中的位置为index=4,时间复杂度为O(logN)。

大多数情况,我们的Key总是几乎升序的,在这种情况下,总使用put()效率自然底下,所以通过append(),加快升序Key数据的添加。

特别的,如果Key为升序Integer类型,SparseArray的性能比HashMap要好,不论是运行时间还是内存占用。

//SparseArray
//用这个方法来插入key升序数据,时间复杂度为O(1)
//用put()插入key升序的数据,时间复杂度为O(logn)
public void append(int key, E value) {
    //如果原先数组有数据了,而且 key 并不是最大的,就正常put
    if (mSize != 0 && key <= mKeys[mSize - 1]) {
        put(key, value);
        return;
    }
    //如果原先数组是空的,或者key超过了已有key的最大值,
    // 那么就可以在mSize的位置直接拼接,没必要再去二分一趟

    //如果mSize没更新,gc整理一下数组
    if (mGarbage && mSize >= mKeys.length) {
        gc();
    }
    //直接将数据填在数组的最后
    mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
    mValues = GrowingArrayUtils.append(mValues, mSize, value);
    //更新mSize
    mSize++;
}

其中,由GrowingArrayUtils来执行真正的append()逻辑,可能需要扩容:

//GrowingArrayUtils
public static <T> T[] append(T[] array, int currentSize, T element) {
    if (currentSize + 1 > array.length) {
        @SuppressWarnings("unchecked")
        //扩容
        T[] newArray = ArrayUtils.newUnpaddedArray(
            (Class<T>) array.getClass().getComponentType(), growSize(currentSize));
        System.arraycopy(array, 0, newArray, 0, currentSize);
        array = newArray;
    }
    //如果要扩容先扩容
    //然后直接在最后位置添加新元素
    array[currentSize] = element;
    return array;
}

4. 查找元素

  • get(int key)
  • get(int key, E valueIfKeyNotFound)

这个就简单了,就是二分查找,时间复杂度为O(logN),如果找到的value不是DELETED,就返回,否则返回null,或者默认值:

//SparseArray<E>
public E get(int key) {
    return get(key, null);
}
//如果没找到,可以给一个默认值,类似getOrDefault()的设计。
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];
    }
}

5. 查找排序第index位的Key值

  • int keyAt(int index)

我们知道SparseArray维护的mKeys是升序数组,除了为本身逻辑提供二分查找,也为用户提供了key升序的特性,可以直接拿到第i小,第k大的元素。

//key是升序的,拿到排在第index的key
public int keyAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {

        throw new ArrayIndexOutOfBoundsException(index);
    }
    //除了删除操作,都要判断是否需要整理数组
    if (mGarbage) {
        gc();
    }

    return mKeys[index];
}

6. 查找对应Value所在的位置

  • int indexOfValueByValue(E value)

就是O(N)的遍历查找,查看目标Value所在的下标,如果没找到就返回-1

//SparseArray
public int indexOfValueByValue(E value) {
    //除了删除,都需要尝试整理数组
    if (mGarbage) {
        gc();
    }
	//遍历
    for (int i = 0; i < mSize; i++) {
        //允许value为null,所以要先做null的判断
        if (value == null) {
            if (mValues[i] == null) {
                return i;
            }
        } else {
            //如果value不为null,再去用equals()来判断
            if (value.equals(mValues[i])) {
                return i;
            }
        }
    }
    return -1;
}

SparseArray允许value为null,所以要先对null做判断,如果不为null,才尝试equals()比较,这和HashMap对null元素的判断设计一致。

7. 删除元素

  • remove(int key)
  • delete(int key)

这个我们在前面将 DELETED value的时候也提到了delete()的主要任务,我们来看一下源码:

//SparseArray
private static final Object DELETED = new Object();
private boolean mGarbage = false;

public void remove(int key) {
    delete(key);
}
public void delete(int key) {
    //一样也是先二分查找,找到下标位置
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        //并不是直接置空,也不调整结构,只有下次要用的时候才去调整,提高效率
        if (mValues[i] != DELETED) {
            //标记为DELETED
            mValues[i] = DELETED;
            //提示下次插入的时候调整数组结构
            mGarbage = true;
        }
    }
}

为了兼容不同程序员对“删除”操作命名的习惯,设计为remove()和delete()的作用一样。

8. 删除元素并返回原值

  • E removeReturnOld(int key)

和删除的逻辑一致,不过在将mValue[i]=DELETED之前,将原来的值记录下来,用于返回。

//SparseArray.java
public E removeReturnOld(int key) {
    //不仅delete,还把原有的值返回出去,如果存在的话
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
	//如果找到了要删除元素的下标
    if (i >= 0) {
        if (mValues[i] != DELETED) {
            //将原有的值记录
            final E old = (E) mValues[i];
            //将value置为DELETED这个对象
            mValues[i] = DELETED;
            //通知下次插入、查找操作的时候先进行整理
            mGarbage = true;
            //返回原有的值
            return old;
        }
    }
    //如果要删除的元素不存在,直接return null
    return null;
}

9. 删除排序第index位的元素

  • removeAt(int index)

我们知道,在SparseArray的mKeys中,所有已插入的key是按升序排列的,这设计初衷是为了配合二分查找,但也为我们的业务提供了方便,类似TreeMap,它提供了按Key排序的插入!

//SparseArray
//删除按key排序为index位置上的元素
public void removeAt(int index) {
    //越界检查
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    //SparseArray中的删除就是把value置为DELETED,并将mGarbage标志置为true
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

10. 批量删除

  • removeAtRange(int index, int size)

可以删除排序第index位以及接连着的size个数的元素

//SparseArray
public void removeAtRange(int index, int size) {
    //为了不越界,最后一个index为mSize和 index+size的最小值
    final int end = Math.min(mSize, index + size);
    for (int i = index; i < end; i++) {
        removeAt(i);
    }
}

总结

SparseArray与HashMap/TreeMap对比,表中说明只针对SparseArray

任务SparseArray说明HashMapTreeMap
删除O(logN)二分查找,置DELETEDO(1)O(logN)
顺序插入O(1) or O(N)O(1)原因:append()直接放在最后
可能扩容
O(N)原因:可能需要整理数组gc()
O(1)O(logN)
随机插入O(logN) or O(N)O(logN):二分查找插入位置
O(N);可能需要整理数组gc()
O(N):插入前可能需要移动数组元素
O(1)O(logN)
倒序插入O(N)O(logN):二分查找插入位置
O(N):一定需要移动数组元素
O(1)O(logN)
查找O(logN)二分查找O(1)O(logN)
查找排序为index的KeyO(1)+O(N)O(1)直接index索引
O(N):可能需要整理数组gc()
结合API自行实现结合API自行实现

同样KeyValue类型时,几乎只有一半的内存占用:SparseArray = 1/2 HashMap

这个也挺好理解的,假设有N个元素,大概来测算一下:

//TreeMap
Entry<K,V> root;// 21 * N Byte
class Entry<K,V>{//至少21 Byte
    K key;//4 Byte
    V value;//4 Byte
    Entry<K,V> left;//4 Byte
    Entry<K,V> right;//4 Byte
    Entry<K,V> parent;//4 Byte
    boolean color = BLACK;//1 Byte
}
//HashMap
Node<K,V>[] table;//16 * N Byte
class Node<K,V>{//至少16 Byte
    final int hash;//4 Byte
    final K key;//4 Byte
    V value;//4 Byte
    Node<K,V> next;//4 Byte
}
//SparseArray
int[] mKeys;//4 * N Byte
Object[] mValues;//4 * N Byte

可以发现,由于不同的数据结构设计,导致了其一个元素占用的内存空间大小不同。

  • TreeMap一个元素至少占用 21 Byte内存空间
  • HashMap一个元素至少占用 16 Byte内存空间
  • SparseArray一个元素至少占用8 Byte内存空间

对于Android来说,一个APP的内存十分吃紧,使用数据结构时,不希望带来额外的内存压力。如果使用Integer作为key,建议使用SparseArray。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/396974.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Hbuilder 下载与安装教程

文章目录Hbuilder下载与安装教程Hbuilder简介一&#xff0c;下载Hbuilder二&#xff0c;安装Hbuilder三&#xff0c;简单使用四&#xff0c;Hbuilderx 调试Hbuilder下载与安装教程 Hbuilder简介 Builder是DCloud&#xff08;数字天堂&#xff09;推出的一款支持HTML5的Web开发…

你还在手撸SQL?ChatGPT笑晕在厕所

文章目录你还在手撸SQL&#xff1f;ChatGPT笑晕在厕所一、背景二、面向Chat编程1. 数据库设计2. 建表语句3. 加中文注释4. 数据模拟5. 查询成绩6. 修改课程任课老师7. 删除课程8. 删除一个有关联数据的课程总结你还在手撸SQL&#xff1f;ChatGPT笑晕在厕所 一、背景 经典3表设…

【项目精选】基于SSH的医院在线挂号系统(视频+论文+源码)

点击下载源码 医院挂号系统主要用于实现医院的挂号&#xff0c;前台基本功能包括&#xff1a;用户注册、用户登录、医院查询、挂号、取消挂号、修改个人信息、退出等。 后台基本功能包括&#xff1a;系统管理员登录、医院管理、科室管理、公告管理、退出系统等。 本系统结构如…

图文讲解MongoDB该怎么安装

一、安装前必读 我这里是Centos7 Linux 内核 注意&#xff1a;本文的命令使用的是 root 用户登录执行&#xff0c;不是 root 的话所有命令前面要加 sudo 二、环境配置 2.1 停止防火墙 systemctl status firewalld #查看firewall systemctl stop firewalld …

Vector - CAPL - 测试报告函数介绍

测试报告是我们开发脚本中必备的一个模块,今天我们介绍一下测试报告中的常用函数,让我们开发出更加清晰、美观的报告,让我们的测试工作更加轻松。 TestCaseComment

备战蓝桥python——完全平方数

完全平方数 链接: 完全平方数 暴力解法&#xff1a; n int(input()) for i in range(1, n1):if(((i*n)**0.5)%10.0):print(i)break运用数论相关知识求解 任意一个正整数都可以被分解成若干个质数乘积的形式&#xff0c;例如 :2022∗5120 \ 2^{2}*5^{1}\,20 22∗51 由此…

JVM的了解与学习

一:jvm是什么 jvm是java虚拟机java Virtual Machine的缩写 jdk包含jre和java DevelopmentTools 二:什么是java虚拟机 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。java虚拟机有自己完善的硬体结构,如处理器、堆栈、寄存器等,还有…

Hive映射Hbase

依赖条件 已有Hadoop、Hive、Zookeeper、HBase 环境。 为什么Hive要映射Hbase HBase 只提供了简单的基于 Key 值的快速查询能力&#xff0c;没法进行大量的条件查询&#xff0c;对于数据分析来说&#xff0c;不太友好。 hive 映射 hbase 为用户提供一种 sqlOnHbase 的方法。…

zookeeper 集群配置

文章目录zookeeper 集群配置1、集群安装zookeeper 集群配置 1、集群安装 1) 集群安装 在 hadoop102、hadoop103 和 hadoop104 三个节点上都部署 Zookeeper。 2) 解压安装 在 hadoop102 解压 Zookeeper 安装包到/opt目录下 输入命令&#xff1a;tar -zxvf apache-zookeeper-3.…

C++——IO流

目录 C语言的输入与输出 流是什么 CIO流 C标准IO流 C文件IO流 二进制读写 文本读写 stringstream的简单介绍 C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键 盘)读取数据&#xff0c;并将值存放在变量中。…

自学大数据第5天~hadoop集群搭建(二)

配置集群/分布式环境 1,修改文件workers 需要把所有节点数据节点的主机名写入该文件,每行一个,默认localhost(即把本机(namenode也作为数据节点),所以我们在伪分布式是没有配置该文件; 在进行分布式时需要删掉localhost(又可能文件中没有该配置,没有那就不用删了,配置一下数据…

147597-66-8,p-SCN-Bn-NOTA,NOTA-P-苯-NCS新型双功能螯合剂

p-SCN-Bn-NOTA | NOTA-P-苯-NCS | CAS&#xff1a;147597-66-8 | 纯度&#xff1a;95%1.p-SCN-Bn-NOTA试剂信息&#xff1a;CAS号&#xff1a;147597-66-8外观&#xff1a;白色固体分子量&#xff1a;C20H26N4O6S分子式&#xff1a;448.4928溶解性&#xff1a;溶于有机溶剂&…

Java语法中的方法引用::是个什么鬼?

1.函数式接口 函数式接口&#xff08;Functional Interface&#xff09;就是一个有且仅有一个抽象方法&#xff08;通俗来说就是只有一个方法要去被实现&#xff0c;因此我们也能通过这个去动态推断参数类型&#xff09;&#xff0c;但是可以拥有多个非抽象方法的接口。函数式接…

大数据架构设计与数据计算流程

大数据架构设计Hadoop有3个核心组件&#xff1a;分布式文件系统HDFS&#xff1b;分布式运算编程框架MapReduce&#xff1b;分布式资源调度平台YARN。HBase&#xff0c;Hadoop dataBase&#xff0c;基于HDFS的NoSQL数据库&#xff0c;面向列式的内存存储&#xff0c;定期将内存数…

基于MyBatis依次、批量、分页增删改查

我们知道处理数据有三种思路&#xff1a;依次、批量、分页&#xff0c;对应方法如下 依次处理&#xff1a;在 Java 里面写 for 循环&#xff0c;依次使用 SQL 语句&#xff0c;频繁连接断开数据库批量处理&#xff1a;在 MyBatis 里面用 <foreach> 拼接成一条长 SQL 语句…

详细讲解零拷贝机制的进化过程

一、传统拷贝方式&#xff08;一&#xff09;操作系统经过4次拷贝CPU 负责将数据从磁盘搬运到内核空间的 Page Cache 中&#xff1b;CPU 负责将数据从内核空间的 Page Cache 搬运到用户空间的缓冲区&#xff1b;CPU 负责将数据从用户空间的缓冲区搬运到内核空间的 Socket 缓冲区…

Caddy2学习笔记——Caddy2反向代理docker版本的headscale

一、个人环境概述 本人拥有一个国内云服务商的云主机和一个备案好的域名&#xff0c;通过caddy2来作为web服务器。我的云主机系统是Ubuntu。 我的云主机是公网ip&#xff0c;地址为&#xff1a;43.126.100.78&#xff1b;我备案好的域名是&#xff1a;hotgirl.com。后面的文章…

CNStack 助推龙源电力扛起“双碳”大旗

作者&#xff1a;CNStack 容器平台、龙源电力&#xff1a;张悦超 、党旗 龙源电力容器云项目背景 龙源电力集团是世界第一大风电运营商&#xff0c; 随着国家西部大开发战略推进&#xff0c;龙源电力已经把风力发电场铺设到全国各地&#xff0c;甚至是交通极不便利的偏远地区&…

[2.1.6]进程管理——线程的实现方式和多线程模型

文章目录第二章 进程管理线程的实现方式和多线程模型一、线程的实现方式&#xff08;一&#xff09;用户级线程&#xff08;二&#xff09;内核级线程二、多线程模型&#xff08;一&#xff09;一对一模型&#xff08;二&#xff09;多对一模型&#xff08;三&#xff09;多对多…

STM32MP157-Linux输入设备应用编程-多点触摸屏编程

文章目录前言多点触摸屏tslib库简介tslib库移植tslib库函数使用打开触摸屏设备配置触摸屏设备打开并配置触摸屏设备读取触摸屏设备多点触摸屏程序编写触点数据结构体定义事件定义计算触点数量判断单击、双击判断长按、移动判断放大、缩小外部调用代码流程图&#xff08;草图&am…