数据结构与算法 - 数组

一、数组

1. 概述

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

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

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

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

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

空间占用

Java中数组结构为

  • 8字节markword,记录对象的锁状态、对象实例的哈希值、年龄信息、GC状态
  • 4字节class指针(压缩class指针的情况)
  • 4字节数组大小(决定了数组最大容量是2^32
  • 数组元素 + 对齐字节(Java中所有对象大小都是8字节的整数倍,不足的要用对齐字节补足)

例如

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]时,就相当于

  • 先找到第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),一般是64bytes,一次读的数据少了不划算,因此最少读64bytes填满一个缓存行,因此读入某个数据时,也会读取其临近的数据,这就是空间局部性。

对效率的影响

比较下面 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, 0] ... [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(); 
}

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

6. 习题

6.1 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。

示例 3:

输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

提示:

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -10^9 <= nums1[i], nums2[j] <= 10^9

进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?

解法一:使用双指针法从后向前填充 nums1,避免了覆盖尚未比较的元素

class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int i = m + n - 1;m--;n--;while(n >= 0) {if(m >= 0 && nums1[m] > nums2[n]) {nums1[i--] = nums1[m--];} else {nums1[i--] = nums2[n--];}}}
}

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

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

相关文章

手把手教你用家用电脑完成图片和视频AI去水印功能

一.效果展示 二.video-subtitle-remover源码地址 soda151314/video-subtitle-remover: 基于AI的图片/视频硬字幕去除、文本水印去除&#xff0c;无损分辨率生成去字幕、去水印后的图片/视频文件。无需申请第三方API&#xff0c;本地实现。AI-based tool for removing hard-cod…

随堂测小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;试题信息管理&#xff0c;标签类型管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;考试成绩&#xff0c;试题信息&#xff0…

SOMEIPSRV_RPC_11: 字段的设定器和有效载荷

测试目的&#xff1a; 验证字段的setter方法是否按照规范要求&#xff0c;通过请求/响应调用实现&#xff0c;其中请求消息的负载包含期望的字段值&#xff0c;响应消息的负载包含已设置到字段的值。 描述 本测试用例旨在验证DUT&#xff08;Device Under Test&#xff0c;被…

【区块链+绿色低碳】碳低链 | FISCO BCOS应用案例

在碳中和、碳达峰国家战略的号召下&#xff0c;碳中和数字化、协同低碳的发展如火如荼。但是在金融业的实际场景应用中&#xff0c; 存在数据收集效率低、数据核查困难、服务单一等问题&#xff0c;痛点集中为两个&#xff1a;一是数据冗杂&#xff0c;可能会存在数据篡改&…

MySQL存储引擎和

MySQL存储引擎 在数据库中保存的是一张张有着千丝万缕关系的表&#xff0c;所以表设计的好坏&#xff0c;将直接影响着整个数据库。而在设计表的时候&#xff0c;最关注的一个问题是使用什么存储引擎。MySQL中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种…

LeetCode 144.二叉树的前序遍历 C写法

LeetCode 144.二叉树的前序遍历 思路&#x1f9d0;&#xff1a; 遍历很简单&#xff0c;但是我们需要开空间进行值的存储&#xff0c;结点个数也可以用递归进行统计&#xff0c;开好空间就可以用数组进行值的存储&#xff0c;注意下标要么用全局&#xff0c;要么指针解引用&…

Astro 实现TodoList网页应用案例

Astro 是一个现代化的静态站点生成器和前端框架&#xff0c;它具有独特的设计理念&#xff1a;岛屿架构。它允许开发人员使用组件化的方式构建内容优先的网站&#xff0c;将各种技术栈&#xff08;如React、Vue、Svelte等&#xff09;的组件无缝集成到同一个项目中。 1、创建项…

使用注意力机制的seq2seq

一、背景 1、机器翻译中&#xff0c;每个生成的词可能相关于源句子中不同的词&#xff0c;但是之前用的是最后一个RNN层出来的context。 2、加入注意力 &#xff08;1&#xff09;假设输入序列中有&#x1d447;个词元&#xff0c; 解码时间步&#x1d461;′的上下文变量是…

​LLM大模型从入门到精通(7)--企业大模型开发流程​

一、大模型项目开发的两种方式 2023年以来&#xff0c;随着ChatGPT的火爆&#xff0c;使得LLM成为研究和应用的热点&#xff0c;但是市面上大部分LLM都存在一个共同的问题&#xff1a;模型都是基于过去的经验数据进行训练完成&#xff0c;无法获取最新的知识&#xff0c;以及各…

最新Python编程、机器学习与深度学习应用

近年来&#xff0c;人工智能领域的飞速发展极大地改变了各个行业的面貌。当前最新的技术动态&#xff0c;如大型语言模型和深度学习技术的发展&#xff0c;展示了深度学习和机器学习技术的强大潜力&#xff0c;成为推动创新和提升竞争力的关键。特别是PyTorch&#xff0c;凭借其…

昇思25天学习打卡营第26天|munger85

ShuffleNet图像分类 和mobilenet一样&#xff0c;也是在资源有限的设备上进行神经网络来做ai图像分类的小模型&#xff0c;在保持精度的同时大大降低了模型的计算量。 是基本块 就是真正的网络&#xff0c;如果模型size是2&#xff0c;就是输出的时候多一些&#xff0c;精细一…

jdk版本管理利器-sdkman

1.什么是sdkman&#xff1f; sdkman是一个轻量级、支持多平台的开源开发工具管理器&#xff0c;可以通过它安装任意主流发行版本&#xff08;例如OpenJDK、Kona、GraalVM等等&#xff09;的任意版本的JDK。通过下面的命令可以轻易安装sdkman: 2.安装 curl -s "https://…

安装 moleculeSTM 踩坑日记

“学习 LLM &#xff0c;在大模型时代为自己存张船票”。 相信很多人都有这样的想法。那么&#xff0c;在 AI for science 领域&#xff0c;哪些 LLM 模型值得一试呢&#xff1f; 笔者认为&#xff1a; LLM 直接预测 SMILES 性质 or 直接生成 SMILES 的技术路线是行不通的。因…

系统移植(十)Linux内核源码解析(未整理)

1、分析make <board_name>_defconfig执行过程详解 分析Makefile文件&#xff0c;分析Makefile文件的规则中目标为"<board_name>_defconfig", 打开linux内核源码目录下的Makefile,搜索“%config”字符串&#xff0c;得到以下结果 %config: scripts_basic …

进程间通信--套接字socket

前面提到的管道、消息队列、共享内存、信号和信号量都是在同一台主机上进行进程间通信&#xff0c;那要想跨网络与不同主机上的进程之间通信&#xff0c;就需要Socket通信了。 实际上&#xff0c;Socket通信不仅可以跨网络与不同主机的进程间通信&#xff0c;还可以在同主机上…

安防监控视频平台LntonAIServer视频监控管理平台裸土检测算法

LntonAIServer裸土检测算法代表了一种先进的土地监测技术&#xff0c;它利用人工智能的强劲能力&#xff0c;实现了对裸土区域的自动识别和实时监测。该算法的推出&#xff0c;为环境保护、农业管理以及城市规划等多个领域提供了创新的解决方案&#xff0c;其应用前景广阔&…

maven插件1(timer-plugin)

概述 timer plugin, 提供4个goal: currentTimecurrentDatecurrentMonthcurrentYear 打包 命令 maven clean install 常见错误 goalPrefix MISSING 错误信息 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-plugin-plugin:3.13.1:helpmojo (help-goal)…

速通JS模块化规范

目录 1模块化概述 1.1什么是模块化&#xff1f; 1.2为什么需要模块化&#xff1f; 2有哪些模块化规范&#xff1f; 3导入与导出的概念 4CommonJS 规范 4.1初步体验 4.2导出数据 4.3导入数据 4.4扩展理解 4.5浏览器端运行 5ES6 模块化规范 5.1初步体验 5.2Node 中运…

创建自己的 Omnigraph (python篇)

Omnigraph 是 Nvidia Omniverse 中一个强大的视觉化脚本工具&#xff0c;它让开发者能够以直观和灵活的方式创建复杂的行为和交互性。通过结合 Action Graphs 和 Push Graphs&#xff0c;以及利用丰富的节点库&#xff0c;用户可以在 Omniverse 平台上构建出令人惊叹的虚拟世界…

【书生大模型实战】L1-LangGPT结构化提示词编写实践

一、关卡任务 背景问题&#xff1a;近期相关研究发现&#xff0c;LLM在对比浮点数字时表现不佳&#xff0c;经验证&#xff0c;internlm2-chat-1.8b (internlm2-chat-7b)也存在这一问题&#xff0c;例如认为13.8<13.11。 任务要求&#xff1a;利用LangGPT优化提示词&#x…