数组

  • 什么是数组
    • 如何实现随机访问
    • 低效的插入和删除
    • 警惕数组越界
  • 关于容器和数组

什么是数组

什么是数组?它的定义就是线性表+连续的内存空间+相同数据类型的数据。

什么是线性表?线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。而与它相对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。

什么是连续的内存空间?当我们开辟一个数组的时候,会在内存中连续位置开辟一段空间。这样的优点是,我们可以随机访问数组中的元素, 缺点也是很明显的,我们要时时刻刻保证它是连续的,也就是说,当我们删除和插入数据时,要进行大规模的数据移动,以保证数据连续。

如何实现随机访问

数组是如何实现根据下标随机访问数组元素?

我们拿一个长度为 10 的 int 类型的数组
int[] a = new int[10]来举例。在这个图中,计算机给数组 a[10],分配了一块连续内存空间 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(logn)。所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。

现在我们来回答为什么很多编程语言数组从零开始。

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

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:a[k]_address = base_address + (k-1)*type_size

对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。
数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。

低效的插入和删除

因为我们为了保证连续,所以假如我们将第k个元素删除,那么我们需要将k后面的元素全部移动到前面。
这样子最好的情况时删除最后一个元素,因为这样不用移动元素。最差的情况就是删除第一个元素了,你知道为什么吗?

如果我们要多次删除数组中的元素,就需要我们多次移动数组上的元素。这很明显不是一种很好的操作。于是我们想到一种解决方案,就是将数组中所有删除记录下来,等到空间不够用了,我们再一次性删除。这样,我们只需要移动一次数组中的元素。

举个例子:
我们继续来看例子。数组 a[10]中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 三个元素。

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

如果你了解 JVM,你会发现,这不就是 JVM 标记清除垃圾回收算法的核心思想吗?没错,数据结构和算法的魅力就在于此,很多时候我们并不是要去死记硬背某个数据结构或者算法,而是要学习它背后的思想和处理技巧,这些东西才是最有价值的。如果你细心留意,不管是在软件开发还是架构设计中,总能找到某些算法和数据结构的影子。

插入也是同理。

我们不能够同时插入一些元素,但是我们将第k个元素直接移动到最后,然后将元素放到第k个位置。

警惕数组越界


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

我们分析上述例子,第一感觉是会打印3次的“hello,world”,但是事实并非如此。这段代码的运行结果是打印无数次的"hello,world",直到内存耗尽。为什么会这样呢?

我们分析代码可以看见,当i=3时,arr[3]是会造成数组越界的。
但是,在 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。
根据我们前面讲的数组寻址公式,a[3]也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 a[3]=0 就相当于 i=0,所以就会导致代码无限循环。
数组越界在 C 语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。这种情况下,一般都会出现莫名其妙的逻辑错误,就像我们刚刚举的那个例子.但并非所有的语言都像 C 一样,把数组越界检查的工作丢给程序员来做,像 Java 本身就会做越界检查,比如下面这几行 Java 代码,就会抛java.lang.ArrayIndexOutOfBoundsException

关于容器和数组

针对数组类型,很多语言都有容器类。比如java中的ArrayList。那什么时候使用容器,什么时候使用数组呢。
容器的优势:

  • 将很多数组操作细节封装起来。
  • 支持动态扩容。

不过,这里需要注意一点,因为扩容操作涉及内存申请和数据搬移,是比较耗时的。扩容就是当申请的内存不够时,我们可以将再申请1.5倍的内存,然后将数据移动过去。所以,如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小。


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

什么时候使用数组:

  • Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
  • 对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。

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

为什么很多编程语言数组从0开始相关推荐

  1. 为什么很多编程语言中数组都是从 0 开始编号

    正文 1 为什么很多编程语言中数组都是从 0 开始编号 1.1 效率原因 从内存模型来看,"下标"也称为"偏移". 我们知道在C语言中数组名代表首地址(第一个元 ...

  2. C语言变长数组data[0]【总结】

    C语言变长数组data[0][总结] 1.前言 今天在看代码中遇到一个结构中包含char data[0],第一次见到时感觉很奇怪,数组的长度怎么可以为零呢?于是上网搜索一下这样的用法的目的,发现在li ...

  3. numpy 数组 填充 0、1和各种值

    numpy 数组 填充 0.1和各种值 文章目录 numpy 数组 填充 0.1和各种值 一维数组 相同值 边缘值 递减值 最大值 平均值 中位数 最小值 边缘对称 边缘外的空气对称 原数组 二维数组 ...

  4. 剑指offer:给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,..,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]

    给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]. 不能使用除 ...

  5. 【2011年全国试题3】已知循环队列存储在一维数组A[0…n-1],且队列非空时,front和rear分别指向队头元素和队尾元素。若初始时队列为空,且

    [2011年全国试题3]已知循环队列存储在一维数组A[0-n-1],且队列非空时,front和rear分别指向队头元素和队尾元素.若初始时队列为空,且要求第一个进入队列的元素存储在A[0]处,则初始时 ...

  6. 在MapInfo平台上开发用户定制的应用程序的编程语言 MapBasic v6.0 1CD

    在MapInfo平台上开发用户定制的应用程序的编程语言 MapBasic v6.0 1CD IDRISI.Andes.v15.00-ISO 1CD(地理信息系统(GIS)及图象处理软件) IDRISI ...

  7. c语言从文件中读取数据存入数组_在c语言中数组 a[i++] 和 a[++i]的 区别? 数组a[0]++又是什么意思?...

    在c语言中,数组 a[i++] 和数组 a[++i] 有区别吗? 首先我们先看下面的内容: b = a++; //先计算表达式的值,即先把a赋值给了b:然后a再自加1. b = ++a: //先a自加 ...

  8. 用滚动数组求解0/1背包问题

    用滚动数组求解0/1背包问题(此处仅求装入背包的最大价值) // 由于第i个阶段(考虑物品i)的解dp[i][ * ]只与第i-1个阶段(考虑物品i-1)的解dp[i-1][ * ]有关,这种情况下保 ...

  9. 如何将var str = “[[[0,32],[3,2],[2,1]]]“;转为数组arr=[[[0,32],[3,2],[2,1]]]?

    问题1:如何将var str = "[[[0,32],[3,2],[2,1]]]";转为数组arr=[[[0,32],[3,2],[2,1]]]: 问题1.1:为什么可以JSON. ...

最新文章

  1. 深度CTR预估模型的演化之路2019最新进展
  2. apue.h头文件(UNIX环境高级编程)
  3. 【maven install报错】Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
  4. next数组_【阿里面试热身题】数组去重(动画展示)
  5. HTML 部分常用属性、组成属性|...超链接、路径、锚点、列表、滚动、URL编码、表格、表单、GET和POST
  6. 深度学习框架 TensorFlow.NET 0.1.0,完善变量更新操作
  7. 【Java】身份证的验证
  8. Linux网卡配置出错无法联网-联网报错解决方法
  9. 在UI设计中用什么样的字体?
  10. 小学计算机优秀说课稿ppt,【说课备考】各学科说课稿示范第7天 — 小学信息技术...
  11. Ubuntu使用ZTE MF832S上网卡拨号上网
  12. MATLAB与高等数学--dsolve命令
  13. 还原文件打开方式为未知应用程序
  14. 很不错的SQLite工具 SQLiteSpy
  15. 军品研制过程评审活动-(一)论证阶段
  16. openlayers 地图上加图标_openlayers地图添加标志物
  17. 新司机的黑裙战斗机 篇二:入门—新司机的黑群晖指北——软件篇(上)
  18. CCIE第一天---QoS
  19. 求解圆圈中最后剩下的数字
  20. 【思维与逻辑】有1000瓶药水,但其中有一瓶毒药水,需要多少只小白鼠?

热门文章

  1. C# 生成Word文件(图片,文字)
  2. 广西教师招聘需要计算机考试证,想参加广西教师编制考试?得先达到这4点要求!...
  3. 为什么计算机系老师不去当程序员拿高薪,反而来当老师?
  4. java处理全角半角字符问题
  5. 【Linux环境】Linux系统下如何关闭Java进程
  6. 手机如何传输高清视频
  7. 从“边界信任”到“零信任”,安全访问的“决胜局”正提前上演
  8. Android按下录音录音动画效果 ,自定义录音、播放动画View
  9. 华为freebuds 5无线充电充不上电怎么办?
  10. 羊皮卷的故事-第十六章-羊皮卷之九