基础数据结构之数组

news/2024/9/23 7:47:28/文章来源:https://www.cnblogs.com/potterCoding/p/18425142

数组

1) 概述

定义

在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识

In computer science, an array is a data structure consisting of a collection of elements (values or variables), each identified by at least one array index or key

因为数组内的元素是连续存储的,所以数组中元素的地址,可以通过其索引计算出来,例如:

int[] array = {1,2,3,4,5}

知道了数组的数据起始地址 \(BaseAddress\),就可以由公式 \(BaseAddress + i * size\) 计算出索引 \(i\) 元素的地址

  • \(i\) 即索引,在 Java、C 等语言都是从 0 开始
  • \(size\) 是每个元素占用字节,例如 \(int\)\(4\)\(double\)\(8\)

小测试

byte[] array = {1,2,3,4,5}

已知 array 的数据的起始地址是 0x7138f94c8,那么元素 3 的地址是什么?

答:0x7138f94c8 + 2 * 1 = 0x7138f94ca

空间占用

Java 中数组结构为

  • 8 字节 markword
  • 4 字节 class 指针(压缩 class 指针的情况)
  • 4 字节 数组大小(决定了数组最大容量是 \(2^{32}\)
  • 数组元素 + 对齐字节(java 中所有对象大小都是 8 字节的整数倍[^12],不足的要用对齐字节补足)

例如

int[] array = {1, 2, 3, 4, 5};

的大小为 40 个字节,组成如下

8 + 4 + 4 + 5*4 + 4(alignment)

随机访问性能

即根据索引查找元素,时间复杂度是 \(O(1)\)

2) 动态数组

java 版本

public class DynamicArray implements Iterable<Integer> {private int size = 0; // 逻辑大小private int capacity = 8; // 容量private int[] array = {};/*** 向最后位置 [size] 添加元素** @param element 待添加元素*/public void addLast(int element) {add(size, element);}/*** 向 [0 .. size] 位置添加元素** @param index   索引位置* @param element 待添加元素*/public void add(int index, int element) {checkAndGrow();// 添加逻辑if (index >= 0 && index < size) {// 向后挪动, 空出待插入位置System.arraycopy(array, index,array, index + 1, size - index);}array[index] = element;size++;}private void checkAndGrow() {// 容量检查if (size == 0) {array = new int[capacity];} else if (size == capacity) {// 进行扩容, 1.5 1.618 2capacity += capacity >> 1;int[] newArray = new int[capacity];System.arraycopy(array, 0,newArray, 0, size);array = newArray;}}/*** 从 [0 .. size) 范围删除元素** @param index 索引位置* @return 被删除元素*/public int remove(int index) { // [0..size)int removed = array[index];if (index < size - 1) {// 向前挪动System.arraycopy(array, index + 1,array, index, size - index - 1);}size--;return removed;}/*** 查询元素** @param index 索引位置, 在 [0..size) 区间内* @return 该索引位置的元素*/public int get(int index) {return array[index];}/*** 遍历方法1** @param consumer 遍历要执行的操作, 入参: 每个元素*/public void foreach(Consumer<Integer> consumer) {for (int i = 0; i < size; i++) {// 提供 array[i]// 返回 voidconsumer.accept(array[i]);}}/*** 遍历方法2 - 迭代器遍历*/@Overridepublic Iterator<Integer> iterator() {return new Iterator<Integer>() {int i = 0;@Overridepublic boolean hasNext() { // 有没有下一个元素return i < size;}@Overridepublic Integer next() { // 返回当前元素,并移动到下一个元素return array[i++];}};}/*** 遍历方法3 - stream 遍历** @return stream 流*/public IntStream stream() {return IntStream.of(Arrays.copyOfRange(array, 0, size));}
}
  • 这些方法实现,都简化了 index 的有效性判断,假设输入的 index 都是合法的

插入或删除性能

头部位置,时间复杂度是 \(O(n)\)

中间位置,时间复杂度是 \(O(n)\)

尾部位置,时间复杂度是 \(O(1)\)(均摊来说)

3) 二维数组

int[][] array = {{11, 12, 13, 14, 15},{21, 22, 23, 24, 25},{31, 32, 33, 34, 35},
};

内存图如下

  • 二维数组占 32 个字节,其中 array[0],array[1],array[2] 三个元素分别保存了指向三个一维数组的引用

  • 三个一维数组各占 40 个字节

  • 它们在内层布局上是连续

更一般的,对一个二维数组 \(Array[m][n]\)

  • \(m\) 是外层数组的长度,可以看作 row 行
  • \(n\) 是内层数组的长度,可以看作 column 列
  • 当访问 \(Array[i][j]\)\(0\leq i \lt m, 0\leq j \lt n\)时,就相当于
    • 先找到第 \(i\) 个内层数组(行)
    • 再找到此内层数组中第 \(j\) 个元素(列)

小测试

Java 环境下(不考虑类指针和引用压缩,此为默认情况),有下面的二维数组

byte[][] array = {{11, 12, 13, 14, 15},{21, 22, 23, 24, 25},{31, 32, 33, 34, 35},
};

已知 array 对象起始地址是 0x1000,那么 23 这个元素的地址是什么?

答:

  • 起始地址 0x1000
  • 外层数组大小:16字节对象头 + 3元素 * 每个引用4字节 + 4 对齐字节 = 32 = 0x20
  • 第一个内层数组大小:16字节对象头 + 5元素 * 每个byte1字节 + 3 对齐字节 = 24 = 0x18
  • 第二个内层数组,16字节对象头 = 0x10,待查找元素索引为 2
  • 最后结果 = 0x1000 + 0x20 + 0x18 + 0x10 + 2*1 = 0x104a

4) 局部性原理

这里只讨论空间局部性

  • cpu 读取内存(速度慢)数据后,会将其放入高速缓存(速度快)当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了
  • 缓存的最小存储单位是缓存行(cache line),一般是 64 bytes,一次读的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其临近的数据,这就是所谓空间局部性

对效率的影响

比较下面 ij 和 ji 两个方法的执行效率

int rows = 1000000;
int columns = 14;
int[][] a = new int[rows][columns];StopWatch sw = new StopWatch();
sw.start("ij");
ij(a, rows, columns);
sw.stop();
sw.start("ji");
ji(a, rows, columns);
sw.stop();
System.out.println(sw.prettyPrint());

ij 方法

public static void ij(int[][] a, int rows, int columns) {long sum = 0L;for (int i = 0; i < rows; i++) {for (int j = 0; j < columns; j++) {sum += a[i][j];}}System.out.println(sum);
}

ji 方法

public static void ji(int[][] a, int rows, int columns) {long sum = 0L;for (int j = 0; j < columns; j++) {for (int i = 0; i < rows; i++) {sum += a[i][j];}}System.out.println(sum);
}

执行结果

0
0
StopWatch '': running time = 96283300 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
016196200  017%  ij
080087100  083%  ji

可以看到 ij 的效率比 ji 快很多,为什么呢?

  • 缓存是有限的,当新数据来了后,一些旧的缓存行数据就会被覆盖
  • 如果不能充分利用缓存的数据,就会造成效率低下

以 ji 执行为例,第一次内循环要读入 \([0,0]\) 这条数据,由于局部性原理,读入 \([0,0]\) 的同时也读入了 \([0,1] ... [0,13]\),如图所示

但很遗憾,第二次内循环要的是 \([1,0]\) 这条数据,缓存中没有,于是再读入了下图的数据

这显然是一种浪费,因为 \([0,1] ... [0,13]\) 包括 \([1,1] ... [1,13]\) 这些数据虽然读入了缓存,却没有及时用上,而缓存的大小是有限的,等执行到第九次内循环时

缓存的第一行数据已经被新的数据 \([8,0] ... [8,13]\) 覆盖掉了,以后如果再想读,比如 \([0,1]\),又得到内存去读了

同理可以分析 ij 函数则能充分利用局部性原理加载到的缓存数据

举一反三

  1. I/O 读写时同样可以体现局部性原理

  2. 数组可以充分利用局部性原理,那么链表呢?

    答:链表不行,因为链表的元素并非相邻存储

5) 越界检查

java 中对数组元素的读写都有越界检查,类似于下面的代码

bool is_within_bounds(int index) const        
{ return 0 <= index && index < length(); 
}
  • 代码位置:openjdk\src\hotspot\share\oops\arrayOop.hpp

只不过此检查代码,不需要由程序员自己来调用,JVM 会帮我们调用

习题

E01. 合并有序数组 - 对应 Leetcode 88

将数组内两个区间内的有序元素合并

[1, 5, 6, 2, 4, 10, 11]

可以视作两个有序区间

[1, 5, 6] 和 [2, 4, 10, 11]

合并后,结果仍存储于原有空间

[1, 2, 4, 5, 6, 10, 11]

方法1

递归

  • 每次递归把更小的元素复制到结果数组
merge(left=[1,5,6],right=[2,4,10,11],a2=[]){merge(left=[5,6],right=[2,4,10,11],a2=[1]){merge(left=[5,6],right=[4,10,11],a2=[1,2]){merge(left=[5,6],right=[10,11],a2=[1,2,4]){merge(left=[6],right=[10,11],a2=[1,2,4,5]){merge(left=[],right=[10,11],a2=[1,2,4,5,6]){// 拷贝10,11}}}}}
}

代码

public static void merge(int[] a1, int i, int iEnd, int j, int jEnd,int[] a2, int k) {if (i > iEnd) {System.arraycopy(a1, j, a2, k, jEnd - j + 1);return;}if (j > jEnd) {System.arraycopy(a1, i, a2, k, iEnd - i + 1);return;}if (a1[i] < a1[j]) {a2[k] = a1[i];merge(a1, i + 1, iEnd, j, jEnd, a2, k + 1);} else {a2[k] = a1[j];merge(a1, i, iEnd, j + 1, jEnd, a2, k + 1);}
}

测试

int[] a1 = {1, 5, 6, 2, 4, 10, 11};
int[] a2 = new int[a1.length];
merge(a1, 0, 2, 3, 6, a2, 0);

方法2

代码

public static void merge(int[] a1, int i, int iEnd,int j, int jEnd,int[] a2) {int k = i;while (i <= iEnd && j <= jEnd) {if (a1[i] < a1[j]) {a2[k] = a1[i];i++;} else {a2[k] = a1[j];j++;}k++;}if (i > iEnd) {System.arraycopy(a1, j, a2, k, jEnd - j + 1);}if (j > jEnd) {System.arraycopy(a1, i, a2, k, iEnd - i + 1);}
}

测试

int[] a1 = {1, 5, 6, 2, 4, 10, 11};
int[] a2 = new int[a3.length];
merge(a1, 0, 2, 3, 6, a2);

本文,已收录于,我的技术网站 pottercoding.cn,有大厂完整面经,工作技术,架构师成长之路,等经验分享!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网站搭建

第一步,服务器申请选择一:免费云服务器,免费虚拟主机 如:阿贝云阿贝云提供了免费的云服务器和免费的云虚拟主机,可根据自己的实际应用情况选择。 首先注册一个账户,然后需要支付0.3元做一个实名认证,如果实名认证成功了大概率会开通成功。如果失败了可能是服务器资源池不…

读构建可扩展分布式系统:方法与实践12分布式数据库案例

分布式数据库案例1. Redis 1.1. 2009年首次发布1.1.1. 更注重原始性能和简单性,而不是数据安全性和一致性1.2. 主要吸引力在于它能够同时充当分布式缓存和数据存储 1.3. 维护一个内存中的数据存储,也称为数据结构存储(data structure store) 1.4. 配置Redis将每个命令记录到一…

Java代码审计篇 - ofcms系统审计思路讲解 - 篇2 - SQL注入漏洞审计

Java代码审计篇 | ofcms系统审计思路讲解 - 篇2 | SQL注入漏洞审计 1. 前言 我发现很多文章包括教程,大概套路是:只说有漏洞的点,将有漏洞的点指出,然后分析代码;或者黑盒测试出漏洞之后,然后分析代码。 我认为这是在分析漏洞代码,而非代码审计。代码审计文章或教程应该…

Java代码审计篇 - ofcms系统审计思路讲解 - 篇3 - 文件上传漏洞审计

Java代码审计篇 | ofcms系统审计思路讲解 - 篇3 | 文件上传漏洞审计 1 文件上传代码审计【有1处】 文件上传漏洞我们需要着重关注的是文件在被java代码解析到保存下来之间有无验证过滤,因此什么样的上传方式,什么样的保存方式都不重要,大家着重关注代码对文件的验证过滤手段…

Java代码审计篇 - ofcms系统审计思路讲解 - 篇4 - XXE漏洞审计

Java代码审计篇 | ofcms系统审计思路讲解 - 篇4 | XXE漏洞审计 1. XXE代码审计【有1处】 XXE代码审计常搜索的关键字如下: XMLReader SAXBuilder SAXReader SAXParserFactory Digester DocumentBuilderFactory ...还有一个特殊的,用于加载.jrxml 文件,这是 JasperReports 特…

Java反序列化利用链篇 | CC6链分析(通用版CC链)

CC6 CC6和CC1之间的区别 在CC1的LazyMap链中,调用链如下: AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() LazyMap.get() ChainedTransformer.transform() InvokerTransformer.transform() Runtime.exec()而在CC1链中,对CommonsCollections和jdk版本是有限…

Java反序列化利用链篇 | CC3链分析、TemplatesImpl类中的调用链、TrAXFilter、InstantiateTransformer类的transform()【本系列文章的分析重点】

CC3链分析 1. CC3链背景 前面介绍了CC1和CC6,这两条链子虽然前面的入口类不同CC1入口类是AnnotationInvocationHandler CC6入口类是HashMap但是其触发恶意代码的方式是相同的,都是InvokerTransformer.transform()触发Runtime.getRuntime().exec()实现命令执行。而在很多情况下…

VUE学习day one

学习来源:【前端最新Vue2+Vue3基础入门到实战项目全套教程,自学前端vue就选黑马程序员,一套全通关!】https://www.bilibili.com/video/BV1HV4y1a7n4?vd_source=6dac49feb8d7fd76b147c8cf8c0c2b5a Vue是什么?Vue是一个用于构建用户界面(基于数据动态渲染出用户看到的页面)…

全网最适合入门的面向对象编程教程:51 Python 函数方法与接口-使用 Zope 实现接口

在 Python 中,Zope 提供了一种机制来定义和实现接口。Zope 的接口模块通常用于创建可重用的组件,并确保组件遵循特定的接口规范。全网最适合入门的面向对象编程教程:51 Python 函数方法与接口-使用 Zope 实现接口摘要: 在 Python 中,Zope 提供了一种机制来定义和实现接口。…

Java反序列化利用链篇 | CC1链的第二种方式-LazyMap版调用链【本系列文章的分析重点】

CC1链的第二种方式-LazyMap版调用链 目录LazyMap构造payloadCC1的调用链参考链接LazyMap 在之前的CC1链中分析,其实是其中一种方式(国内版本),还有另外一种方式,也是ysoserial中的CC1链的方式(国外版本)。 区别在于调用transform的类是不同的。 在寻找transform调用的时…

瑞云科技AIGC云平台:重塑电商设计流程!

在快节奏的电商市场中,商品更新换代的速度越来越快,而电商设计团队传统的设计流程和工作模式却难以满足当前行业对快速响应、高效发展和降低成本的实际需求.对此,瑞云科技针对电商设计行业的痛点,提供了全新的AIGC创作云平台.从2022年ChatGPT的发布到,AI正以惊人的速度席卷全球…