数据结构与算法笔记:基础篇 - 数组:为什么数组都是从0开始编号

概述

提到数组,大家应该都不陌生。每一种编程语言基本都会有数组这种数据类型。不过,它不仅仅是一种编程语言中的数据类型,还是一种基础的数据结构。尽管数组看起来非常简单,但是我估计很多人并没有理解这个数据结构的精髓。

在大部分编程语言中,数组都是从 0 开始编号的,你是否想过,为什么数组要从 0 开始编号,而不是从 1 开始呢?


如何实现随机访问?

数组就是一种线性表数据结构。它用一组连续内存空间,存储一组具有相同类型的数据

这个定义中有几个关键词,需要注意一下。

第一个关键词是线性表。线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实,除了数组,链表、队列、栈等也是线性结构。
在这里插入图片描述
与线性表对立的就是非线性表,比如二叉树、图、堆等。之所以叫非线性,是因为,在非线性表中,数据之间不是简单的前后关系。
在这里插入图片描述

第二个关键词是连续的内存空间和相同的数据类型。正是因为这两个限制,数组才有了堪称杀手锏的 “特性”:随机访问。但是有利就有弊,这两个限制也让数组的很多操作变得非常低效,比如删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。

说到数据的访问,你知道数组是如何实现根据下标随机访问数组元素的吗?

我们拿 int[] a = new int[10] 来举例。计算机给数组 a 分盘了一块连续的存储空间 1000~1039,其中,内存块的首地址为 base_address = 1000
在这里插入图片描述
我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:

a[i]_address = base_address + i * data_type_size

其中 data_type_size 为数组中元素的大小。例子中,数组存储的是 int 类型数据,所以,data_type_size 为 4 个字节。

注意,在这里我要纠正一个 “错误”。在面试时,经常会问数组和链表的区别,很多人都回答说,“链表适合插入和删除,时间复杂度为 O ( 1 ) O(1) O(1);数组适合查找,查找时间复杂度为 O ( 1 ) O(1) O(1)”。

这种表述是不正确的。数组是适合查找,但查找的时间复杂度并不是 O ( 1 ) O(1) O(1)。即便是排序好的数组,利用二分查找,时间复杂度也是 O ( l o g n ) O(logn) O(logn)。所以,正确的表述是,数组支持随机访问,根据下标随机访问的时间复杂度为 O ( 1 ) O(1) O(1)

低效的 “插入” 和 “删除”

首先看插入操作

假设数组的长度为 n,现在,如果我们需要将一个数据插入到数组中第 k 个位置。为了把第 k 个位置腾出来,给新来的数据,我们需要将第 k~n 这部分的元素都顺序地往后挪一位。那插入操作的时间复杂度是多少呢?

如果在数组末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O ( 1 ) O(1) O(1)。但如果在数组的第 开头插入数据,那所有的数据都要依次往后移动一位,最坏时间复杂度为 O ( n ) O(n) O(n)。因为我们在数组每个位置插入数据的概率是一样的,所以平均时间复杂度为 1 + 2 + . . . + n n \frac {1+2+...+n} {n} n1+2+...+n=O(n)。

如果数组中的数据都是有序的,我们在某个位置插入一个新的元素时,就必须按照刚才的方法搬移 k 之后的元素。但是,如果数组中的数据并没有任何规律,数组只是一个存储数据的集合。在这种情况下,如果要将某个数据插入到第 k 个位置,为了避免大规模的数据搬移,我们还有一个简单的办法,就是直接将第 k 位的数据搬移到数组的最后,把新元素放入第 k 个位置。

假设数组 a[10] 中存储了 5 个元素: a,b,c,d,e

现在需要将元素 x 插入到第 3 个位置,我们只需要将 c 放入 a[5],将 a[2] 赋值为 x 即可。最后数组的元素如下:a,b,x,d,e,c
在这里插入图片描述
利用这种处理技巧,在特定场景下,在第 k 个位置插入一个元素的时间复杂度就会降为 O ( 1 ) O(1) O(1)。这个思想在快速排序中也会用到,后面的章节会进行详细的讲解。

再来看看删除操作

跟插入数据类似,若要删除第 k 个元素,为了内存的连续性,也需要搬移数据,不然就会出现空洞,内存就不连续了。

如果删除最末尾的数据,则最好情况时间复杂度为 O ( 1 ) O(1) O(1);如果删除开头元素,则最坏情况时间复杂度为 O ( n ) O(n) O(n)

实际上,在某些特定场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起,删除的效率是不是会提高很多呢?

假设数组 a[10] 中存储了 8 个元素:a,b,c,d,e,f,g,h。现在要依次删除 a,b,c

为了避免 d,e,f,g,h 这几个数据被搬移三次,我们可以先记录下已经删除的数据。每次删除操作并不是真正地搬移数据,只是记录数据已被删除。当数组没有更多空间存储数据时,我们再出发一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。

如果你了解 JVM,你会发现,这不就是 JVM 标记清楚垃圾回收算法的核心思想吗?没错,数据结构和算法的魅力就在于此,很多时候我们并不是要去死记硬背某个数据结构或者算法,而是要学习它背后的思想和处理技巧,这些东西才是最有价值的

警惕数组的访问越界问题

首先开看一段 C 语言代码的运行结果:

int main(int argc, char* argv[]) {int i = 0;int arr[3] = {0};for (; i<=3; i++) {arr[i] = 0;printf("hello");}return 0;
}

不知道你有没有发现,这段代码的运行结果并非是打印三行 “hello”,而是会无限打印 “hello”,这是为什么呢?

因为,数组大小为 3,而因为代码书写错误,导致 for 循环的结束条件错误的写成了 i<=3,而非 i<3 ,所以当 i=3 时,数组 a[3] 访问越界了。

在 C 语音中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。根据前面讲的数组寻址公式,a[3] 也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 arr[3] = 0 就相当于 i=0,所以就会导致代码死循环。

数组越界在 C 语音中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就不会报错。

这种情况下,一般都会出现莫名其妙的逻辑错误,就像上面的例子,debug 的难度非常大。而且,很多计算机病毒也正是利用了代码中的数组越界可以访问非法地址的漏洞,来攻击系统,所以,写代码时一定要警惕数组越界。

好在,并非所有的语言都像 C ,把数组越界的检查工作交给程序员来做,像 Java 本身就会做越界检查,比如下面的代码,就会抛出 java.lang.ArrayIndexOutOfBoundsException

int[] a = new int[3];
a[3] = 10;

容器能否完全替代数组?

针对数组类型,很多语音都提供了容器类,比如 Java 中的 ArrayList、C++ STL 中的 vector。在项目开发中,什么时候适合用数组?什么时候适合用容器呢?

比如 Java 中的 ArrayList 与数组相比,到底有哪些优势呢?

个人觉得,ArrayList 最大的优势就是可以将很多操作数组的细节封装起来。比如前面提到的数组插入、删除数据时需要搬移其他数据。另外,它还有一个优势就是支持动态扩容

数组本身要预先指定大小,因为需要分配连续的内存空间。如果我们申请了大小为 10 的数组,当第 11 个数据需要存储到数组中时,我们就需要重新分配一块更大的空间,将原来的数据复制过去,然后再插入新数据。

如果使用 ArrayList,就完全不需要关心底层的扩容逻辑,ArrayList 已经帮我们实现好了。每层存储空间不足时,它都会将空间自动扩容为 1.5 倍大小。

不过需要注意的是,因为扩容涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候就实现指定数据大小

比如,我们从数据库中取出 10000 条数据放入 ArrayList。我们看下面这几行代码,你会发现,相比之下,事先指定数据大小可以省掉很多次内存申请和数据迁移的时间。

ArrayList<User> users = new ArrayList(10000);
for (int i=0; i < 10000; i++) {users.add(xxx);
}

那在高级语言中,数组是不是就无用武之地了?当然不是,有些时候,用数组会更适合写,下面是我总结的几条经验。

  1. Java ArrayList 无法存储基本数据类型,比如 int、long,需要封装为 IntegerLong 类,而装箱和拆箱则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,可以选用数组。
  2. 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
  3. 还有一个我个人的喜好,当要表示多为数组时,用数组往往更加直观。比如 Object[][] array;而用容器的话则需要这样定义 ArrayList<ArrayList<Object>> array

对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但是如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

数组为什么从 0 开始编号?

从数组存储的内存模型上来看,“下标” 最切确的定义应该是 “偏移(offset)”。前面也讲过,如果 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 表示偏移 ktype_size 位置,所以计算 a[k] 的内存地址只需要用这个公式:

a[k]_address = base_address + k * type_size

如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存就会变为:

a[k]_address = base_address + (k - 1) * type_size

对比两个公式,不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算。

数组作为非常底层的数据结构,通过下标随机访问数组元素又是非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减去操作,数组选择从 0 开始编号,而不是从 1开始编号。

不过,个人认为,数组从 0 开始编号的主要原因是历史原因。

C 语音设计者用 0 开始计数数组下标,之后的编程语音为了在一定程度上减少 C 语音程序员的学习成本,因此沿用了从 0 开始编号的习惯。

总结

本章学习了数组,它可以说是最基础、最简单的数据结构。数组用一块连续的内存空间,来存储相同类型的一组数据,最大的特点是支持随机访问,但插入、删除也因此变得比较低效,平均时间复杂度为 O ( n ) O(n) O(n)。在平时的业务开发中,可以直接使用编程语音提供的容器类,但是,如果特别底层的开发,直接使用数组可能会更适合。

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

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

相关文章

AB测试实战

AB测试实战 1、AB测试介绍&#x1f43e; 很多网站/APP的首页都会挂一张头图(Banner)&#xff0c;用来展示重要信息&#xff0c;头图是否吸引人会对公司的营收带来重大影响&#xff0c;一家寿险公司Humana设计了如下三张头图&#xff0c;现在需要决定使用哪一张放到首页&#x…

FastDFS分布式文件系统

一、概述 FastDFS是一款由国人余庆开发的轻量级开源分布式文件系统&#xff0c;它对文件进行管理&#xff0c;功能包括&#xff1a;文件存储、文件同步、文件访问&#xff08;文件上传、文件下载&#xff09;等&#xff0c;主要解决大容量文件存储和高并发访问问题&#xff0c…

jenkins应用2-freestyle-job

1.jenkins应用 1.jenkins构建的流程 1.使用git参数化构建&#xff0c;用标签区分版本 2.git 拉取gitlab远程仓库代码 3.maven打包项目 4.sonarqube经行代码质量检测 5.自定义制作镜像发送到远程仓库harbor 6.在远程服务器上拉取代码启动容器 这个是构建的整个过程和步骤…

保姆级教程:Redis 主从复制原理及集群搭建

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。 &#x1f60a; 座右铭&#xff1a;不…

线程池的工作原理

文章目录 一、应用场景二、工作原理三、主要函数 一、应用场景 传统并发变成的缺陷&#xff1a; 1.创建和销毁线程上花费的时间和消耗的系统资源&#xff0c;甚至可能要比花在处理实际的用户请求的时间和资源要多得多 2. 活动的线程需要消耗系统资源&#xff0c;如果启动太多&…

26、matlab多项式曲线拟合:polyfit ()函数

1、polyfit 多项式曲线拟合 语法 语法&#xff1a;p polyfit(x,y,n) 返回次数为 n 的多项式 p(x) 的系数&#xff0c;该阶数是 y 中数据的最佳拟合&#xff08;基于最小二乘指标&#xff09;。 语法&#xff1a;[p,S] polyfit(x,y,n) 还返回一个结构体 S 语法&#xff1a;[…

优化 mac 储存空间的方法 只需一招为你的苹果电脑提速

在职场中&#xff0c;许多人都对苹果电脑情有独钟。苹果电脑以其简洁美观的设计、流畅稳定的性能以及出色的用户体验&#xff0c;成为了众多职场人士的得力助手。无论是处理文档、制作演示文稿&#xff0c;还是进行创意设计等工作&#xff0c;苹果电脑都能展现出其独特的优势&a…

微信小程序公众号二合一分销商城源码系统 基于PHP+MySQL组合开发的 可多商户商家入驻 带完整的安装代码包以及搭建教程

系统概述 微信小程序公众号二合一分销商城源码系统&#xff0c;是基于PHPMySQL组合开发的一款高效、稳定的电子商务平台解决方案。该系统创新性地将微信公众号与小程序的功能进行了深度整合&#xff0c;为商家提供了一个功能齐全、易于管理的分销商城系统。通过此系统&#xf…

Vue3+vant 带你实现常见的历史记录的业务功能

前言 大部分小伙伴不管是开发PC端还是H5移动端&#xff0c;都会遇到历史搜索的功能。对用户的历史记录进行增删查可以是接口&#xff0c;也可以是前端用缓存实现&#xff0c;一般用浏览器缓存实现的比较多&#xff0c;这篇文章就来教你如何用LocalStorage对历史记录数据的存储、…

视创云展元宇宙虚拟展厅,带来沉浸式的逛展体验!

近年来&#xff0c;随着科技的飞速发展和市场需求的不断演变&#xff0c;众多企业纷纷将目光转向线上虚拟展厅的建设。视创云展元宇宙虚拟展厅凭借其创新性和实用性&#xff0c;为众多企业带来了前所未有的宣传体验&#xff0c;成为了商企展示自我、推广产品的全新舞台。 与传统…

Java 生成SSL证书

第一步&#xff1a; cmd打开指令窗口 第二步&#xff1a; 运行指令&#xff1a; keytool -keystore hk.jks -storepass 123456 -deststoretype jks -genkeypair -keyalg RSA -validity 365 -alias contrastserver -dname "CN111.230.63.59, OUHK, OHK, LZH, STGD, CCN&…

XSKY对象存储深度结合Alluxio分布式缓存系统,GPU利用率提高至90%以上

近日&#xff0c;Alluxio分布式缓存系统完成了与XSKY星辰天合的 XEOS V6.4 对象存储的兼容性测试&#xff0c;旨在解决数据管理和加速方面的挑战。双方进行了深度的产品对接和联合开发&#xff0c;将 Alluxio 分布式缓存系统与 XEOS 对象存储的众多应用特性进行结合&#xff0c…

38. 【Java教程】日期和时间处理

本小节我们将学习 Java 中的日期和时间&#xff0c;日期和时间在我们的实际开发中非常常用&#xff0c;例如用户的注册、数据的增删改、对敏感信息的操作等等都需要记录下日期和时间。通过本小节的学习&#xff0c;你将了解到什么是日期、什么是时间、什么是时区&#xff0c;Ja…

3072. 将元素分配到两个数组中 II

题目 给你一个下标从 1 开始、长度为 n 的整数数组 nums 。 现定义函数 greaterCount &#xff0c;使得 greaterCount(arr, val) 返回数组 arr 中 严格大于 val 的元素数量。 你需要使用 n 次操作&#xff0c;将 nums 的所有元素分配到两个数组 arr1 和 arr2 中。在第一次操…

WebGL画粗线

目录 前言 基本思路 求左右端点 实现 组织数据 顶点着色器计算端点坐标 效果 前言 WebGL绘制模式有点、线、面三种&#xff1b;通过点的绘制可以实现粒子系统等&#xff0c;通过线可以绘制一些连线关系&#xff1b;面就强大了&#xff0c;通过面&#xff0c;我们可以…

视频监控汇聚平台LntonCVS国标GB28181协议实现语音对讲功能

在当今这个智能技术飞速发展的时代&#xff0c;人工智能已经成为了电子产品领域的一股不可忽视的热门趋势。随着科技的不断进步&#xff0c;越来越多的电子产品开始融入人工智能技术&#xff0c;从而为其开拓了全新的发展路径。在这个大背景下&#xff0c;安防摄像头无插件直播…

基于springboot实现智慧校园之家长子系统项目【项目源码】计算机毕业设计

基于springboot实现智慧校园之家长子系统演示 SpringBoot框架介绍 本课题程序开发使用到的框架技术&#xff0c;英文名称缩写是SpringBoot&#xff0c;在JavaWeb开发中使用的流行框架有SSH、SpringBoot、SpringMVC等&#xff0c;作为一个课题程序采用SSH框架也可以&#xff0c…

【学习】测试用例设计与执行的黄金法则

在软件测试领域&#xff0c;测试用例的设计与执行是确保产品质量的关键环节。一个优秀的测试用例能够揭示软件中的缺陷&#xff0c;而高效的执行则能保障测试覆盖的全面性。如同璀璨的星辰指引航船前行&#xff0c;以下黄金法则将引领测试用例设计与执行的过程&#xff0c;确保…

Go 群发邮件Redis 实现邮件群发

一、安装 go get github.com/go-redis/redis/v8 go get gopkg.in/gomail.v2 二、使用"gopkg.in/gomail.v2"群发 package mainimport (gomail "gopkg.in/gomail.v2" )func main() {// 邮件内容m : gomail.NewMessage()m.SetHeader("From", &qu…

【杂谈】AIGC之Stable Diffusion:AI绘画的魔法

Stable Diffusion&#xff1a;AI绘画的魔法 引言 在AI的世界里&#xff0c;Stable Diffusion就像一位魔法师&#xff0c;它能够将我们脑海中的幻想&#xff0c;用画笔一一描绘出来。今天&#xff0c;就让我们一探这位魔法师的奥秘&#xff0c;看看它是如何从无到有&#xff0…