【月光宝盒get√】用时间置换空间,聊聊稀疏数组的那些事儿
背景
数据结构是指带有结构特性的数据元素的集合。在数据结构中,数据之间通过一定的组织结构关联在一起,便于计算机存储和使用。从大类划分,数据结构可以分为线性结构和非线性结构,适用于不同的应用场景。
- 线性结构:
线性结构作为最常用的数据结构,它的特点是单个数据之间存在一对一的线性关系。包含两种不同的存储结构:顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。

(线性结构)
链式存储的线性表被叫做链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息
线性结构常见的有:数组、队列、链表和栈。
- 非线性结构:
除了线性结构,其他的数据结构均为非线性结构,特点是单个数据之间存在多个对应关系,常见的有:二维数组,多维数组,广义表,树结构,图结构

(常见的非线性结构)
稀疏数组(Sparse Array)
在各种各样的数据结构中,最基础、最常用的是数组。数组可以非常直观的表示数据在一维或多维空间中的关系,与现实中的情形更接近,所以被大多数程序员当做"首选"的数据结构,然而,在部分应用场景中使用数组存储数据时会出现各种各样的情况,这是就需要在数组的基础上,对数据结构进行优化,衍生出稀疏数组等新的数据结构。
以五子棋局为例,我们应该如何存储棋盘上的落子情况呢?

(使用二位数组存储五子棋盘)
如果使用一个二维数组对棋盘落子进行存储,当我们拿到一个棋盘类数据内容时,大部分内容都是没有意义的0,有意义数据并不相邻,很多空间被浪费。对于五子棋来说,这个问题可能不是很明显,但如果"棋盘"足够大,被浪费的空间就会影响到软件的功能实现,此时引入稀疏数组(SparseArray)就具有了重要的意义。
稀疏数组将数组中的内容进行压缩,存储在一个更为精练的二维数组中,稀疏数组的本质其实就是用时间置换空间。
具体的处理的方法是:
- 该数组之中一共有几行几列进行记录
- 把相同元素内容忽略后,只记录具有不同内容单元的位置
稀疏数组的实现
节约存储空间显然是稀疏数组的一个优势,但是读取性能是否可以会比二维数组差很多?
为了讲清这个问题,我们可以先看一下Android中SparseArray的实现逻辑。SparseArray内部是通过两个数组来进行数据存储的。一个存储key,另外一个存储value。我们从源代码中能够看到key和value各自是用数组表示:
private int[] mKeys;
private Object[] mValues;
同时,SparseArray在存储和读取数据时候,使用的是二分查找法:
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
...
}
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
...
}
在put添加数据的时候,会使用二分查找法和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好。所以,SparseArray存储的元素都是按元素的key值从小到大排列好的。 而在获取数据的时候,也是使用二分查找法判断元素的位置,这样可以使数据的获取变得更加高效。所以,在key的数据量(可以理解为棋盘上去掉空白后的棋子数量)不大时,稀疏数组读取性能是有保障的。
典型应用场景
做开发的都知道,想让系统变快有个最简单的办法就是加内存。对于程序可以做大量的缓存来加速,即所谓"空间换时间"。但是在特定环境,程序可使用的内存是有限的。
在移动设备上,内存是个稀缺资源,例如iPhone 7的内存为2G,而最新款的iPhone 13也仅为4G。所以,稀疏数组这种"时间换空间"的技术最早被广泛应用在移动开发领域。
除了移动端,另一个内存紧缺的运行环境是浏览器。虽然没有明文规定,但在业界的共同认知里,浏览器会对单一线程进行内存限制,例如64位的chrome,每个tab页的内存消耗不允许超过4G。这个限制,在单页面应用还不成熟的十几年前,不会成为问题。因为,那时大家所关注的,还是如何提升后端的处理性能,前端只是一种静态的网页表达方式。
随着前端工程化的高速发展,各种前端工程脚手架日渐成熟,WebComponent标准被提上日程,企业开始由C/S向B/S应用转型。这就要求前端开发者,需要面对单页面处理复杂业务数据的挑战。前端程序从最开始设计以及整个开发过程中都需要考虑内存的使用情况,尽可能的降低内存占用,防止网页崩溃。以前端电子表格为例,我们通常需要为用户提供上百万个单元格(100列 x 1万行),但其中有数据的单元格可能只有几百个。为了减少数据模型占用的内存,我们最终的解决方式是将表格的数据存储方式由常规数组改成稀疏数组,内存占用可以降低到几十分之一,以确保浏览器内存不会被撑爆。

(稀疏矩阵存储策略)
不只是“时间换空间”;
相较于传统的链式存储或是数组存储,稀疏矩阵存储构建了基于索引Key的数据字典。在松散布局的表格数据中,稀疏矩阵只会对非空数据进行存储,而不需要对空数据开辟额外的内存空间。
使用这种特殊的存储策略,除了可以降低内存占用,还使得数据片段化变得容易,可以随时框取整个数据层中的一片数据,进行序列化或反序列化,而无需处理同一数据结构内的其他数据。
借用这样的特性,我们可以随时替换或恢复整个存储结构中的任何一个级别的节点,以改变引用的方式高效解决了表格数据回滚和恢复,而这一点也是电子表格支持在线协同的技术基础。

总结
本节为大家介绍了稀疏数组的基础知识,技术实现和应用场景,以前端电子表格为例,展示了这个技术在节约内存空间,实现回滚恢复等领域的优势。
在后续我们还会继续为大家介绍更多严肃和有趣的内容~

觉得不错点个赞再走吧~
【月光宝盒get√】用时间置换空间,聊聊稀疏数组的那些事儿的更多相关文章
- 剑指offer-第五章优化时间和空间效率(数组中的逆序对的总数)
题目:在数组中如果两个数字的前面的数比后面的数大,则称为一对逆序对.输入一个数组求出数组中逆序对的总数. 以空间换时间:思路:借助一个辅助数组,将原来的数组复制到该数组中.然后将该数组分成子数组,然后 ...
- 剑指offer-第五章优化时间和空间效率(数组中出现次数超过一半的数字)
题目:输入一个数组,找出一个数字,它在数组中出现的次数超过数组的一半. 题目规定如果可以改变数组中元素的位置. 思路1:如果数组是排序的,那么中间元素的位置不就是次数超过数组一半的元素吗?是的,因此我 ...
- java性能时间与空间消耗
Java性能时间与空间消耗 一.减少时间消耗 标准代码优化 (1) 将循环不变量的计算移出循环 例如:for (int i=0; i<size()*2; i++) { ... } ------& ...
- C/C++用匿名数据结构实现时间和空间名利双收
程序的时间和空间,往往是一对矛盾,比如计算CRC32的时候会用到余式表 DWORD *crcTable; // DWORD[256]; 余式表可以用某种规则计算生成,为缩短文章长度就不写出来了,总之要 ...
- Linux学习之CentOS(十六)-----内存置换空间(swap)之建置(转)
内存置换空间(swap)之建置 安装时一定需要的两个 partition 啰! 一个是根目录,另外一个就是 swap(内存置换空间), swap 的功能就是在应付物理内存不足的情况下所造成的内存延伸记 ...
- 测试Protobuffer的定义格式对其时间和空间的影响
测试Protobuffer格式的A命令转换为std::string的的字节个数,分别测试了工程中用到的几种命令: a)AddLayer:108 b)AddSource:209 c)MoveLayer: ...
- protobuf中会严重影响时间和空间损耗的地方
http://blog.chinaunix.net/uid-26922071-id-3723751.html 当前项目中普遍用到GOOGLE 的一个开源大作PROTOBUF,把它作为网络应用层面的传输 ...
- 88、展示Tensorflow计算图上每个节点的基本信息以及运行时消耗的时间和空间
''' Created on May 24, 2017 @author: p0079482 ''' #使用程序输出日志 import tensorflow as tf with tf.Session( ...
- HashSet扩容机制在时间和空间上的浪费,远大于你的想象
一:背景 1. 讲故事 自从这个纯内存项目进了大客户之后,搞得我现在对内存和CPU特别敏感,跑一点数据内存几个G的上下,特别没有安全感,总想用windbg抓几个dump看看到底是哪一块导致的,是我的代 ...
随机推荐
- 深层剖析鸿蒙轻内核M核的动态内存如何支持多段非连续性内存
摘要:鸿蒙轻内核M核新增支持了多段非连续性内存区域,把多个非连续性内存逻辑上合一,用户不感知底层的不同内存块. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列九 动态内存Dynamic Mem ...
- 数学log的基本知识
在数学中,对数是对求幂的逆运算,正如除法是乘法的倒数,反之亦然.这意味着一个数字的对数是必须产生另一个固定数字(基数)的指数, 在简单的情况下,乘数中的对数计数因子.如果a的x次方等于N(a>0 ...
- 多线程编程<二>
wait()与notify(): 1 public class ThreadComDemo { 2 public static void main(String[] args) { 3 try { 4 ...
- 工作多年后再来聊聊IO
IO模型 IO是Input/Output的缩写.Linix网络编程中有五种IO模型: blocking IO(阻塞IO) nonblocking IO(非阻塞IO) IO multiplexing(多 ...
- ES6扩展——正则扩展(u、y修饰符)
//下面三行代码效果一样 //全局匹配开头为a的 const regexp1 = /^a/g; const regexp2 = new RegExp('a','g'); const regexp3 = ...
- linux上安装Docker (非常简单的安装方法) 2019
Docker的三大核心概念:镜像.容器.仓库 镜像:类似虚拟机的镜像.用俗话说就是安装文件. 容器:类似一个轻量级的沙箱,容器是从镜像创建应用运行实例, 可以将其启动.开始.停止.删除.而这些容器都是 ...
- Ubuntu 配置、使用samba共享文件夹
安装库 sudo apt install smbclient samba samba-common 启动samba sudo /etc/init.d/samba start 备份配置文件 sudo c ...
- 对抗生成网络GAN
该方法常用于: 图像生成 图像修复,训练用了MSE+Global+Local数据,其中Global+Local判别式用于全局+局部一致性. 图像超分辨率重构 GAN的基本原理,主要包含两个网络 ...
- 从源码角度分析 MyBatis 工作原理
一.MyBatis 完整示例 这里,我将以一个入门级的示例来演示 MyBatis 是如何工作的. 注:本文后面章节中的原理.源码部分也将基于这个示例来进行讲解.完整示例源码地址 1.1. 数据库准备 ...
- ECMAScript版本知识点汇总
ECMAScript版本知识点汇总 ES5 btoa.atob 对参数进行base64格式编码.解码 /** * btoa() * base64编码 * @param {string} str * @ ...