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

1、什么是数组?

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

概念解析:

      线性表:线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。

连续的内存空间和相同类型的数据:所以数组根据下标具有随机访问特性,这两个限制也让数组的很多操作变得非常低效,比如要想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。

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

举例:长度为 10 的 int 类型的数组 int[] a = new int[10]来举例。在我画的这个图中,计算机给数组分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。

计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,首先通过下面的寻址公式,计算出该元素存储的内存地址:

1 a[i]_address = base_address + i * data_type_size
2
3 data_type_size 表示数组中每个元素的大小。我们举的这个例子里,数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。

面试常见问题:数组和链表的区别?

“链表适合插入、删除,时间复杂度 O(1);数组的查找操作时间复杂度并不是O(1)。即便是排好的数组,用二分查找,时间复杂度也是O(logn)。所以应该这么说:数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。

3、数组低效的“插入”和“删除”

低效原因:

数组为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效,因为需要搬移数据。
插入:
假设数组长度为n,要将一个数据插入到第 k 个位置,为了把第 k 个位置腾出来,我们需要将k~n这部分元素顺序的向后移动一位。
插入的时间复杂度:
最好情况:在数组的末尾插入元素,不需要移动数据了,时间复杂度为O(1)。
最坏情况:在开头插入元素,那就需要把所有的数据向后移动一位,时间复杂度为O(n)。
平均时间复杂度:(1+2+…+n)/n = O(n)。

 删除:和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为O(1),如果要删除开头数据,则最坏情况时间复杂度为O(n),平均时间复杂度为O(n)。

改进方法:

插入:

如果数组中的元素没有任何规律,数组只是被当作一个数据集合,在这种情况下,如果要将某个数据插入到第k个位置,为了避免大规模的数据迁移,一个简单的办法就是直接将现在第k个元素放到最后,把新元素放进来。
例如:arr[10] = {a,b,c,d,e}要将x插入第三个位置arr[10]={a,b,x,d,e,c}
这样插入的时间复杂度为O(1),这种方法在快速排序中也会用到。

删除:
在某些特殊情况下,我们并不一定非得追求数组中数据的连续性,如果我们将多次删除操作放在一起执行,效率会高很多。
举个例子:a[10]={a,b,c,d,e,f,g,h} 如果我们要依次删除abc三个元素,需要搬移三次后面的数据,为了避免这个重复的搬移工作,可以先记录下来已经删除的数据,每次的删除操作并不是真正的搬移数据,只是记录数据已经被删除,当数组中没有更多的空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了搬移工作,这也是标记清除垃圾回收算法的核心思想。

4、 数组越界问题

首先看一段C语言代码:

1 int main(int argc, char* argv[]){
2     int i = 0;
3     int arr[3] = {0};
4     for(; i<=3; i++){
5         arr[i] = 0;
6         printf("hello world\n");
7     }
8     return 0;
9 }

这段代码的结果并不是打印三行hello world,而是无限打印hello world。为啥呢?

因为在C语言中,除了受限制的内存,其他所有内存空间都是可以自由访问的。a[3]也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 a[3]=0 就相当于 i=0,所以就会导致代码无限循环。

那为什么会无限打印呢?

根据我所学和百度的知识解释下:函数体内的局部变量存在栈区,在Linux内存布局中,栈区在高地址空间,从高到低增长,先int i = 0;再int arr[3]={0};变量i和arr地址相邻,并且i地址比arr地址大,首先压栈的i,a[2],a[1],a[0],循环中arr访问越界正好到i,而此时i变量的地址是数组当前进程的,所以进行修改的时候,操作系统并不会终止进程。当然这只是32位操作系统下,64位操作系统下 默认会进行8字节对齐 变量i的地址就不紧跟着数组后面了。另外这个还和编译环境有关,对于不同的编译器,在内存分配时,会按照内存地址递增或递减的方式进行分配。如果是内存地址递减的方式,就会造成无限循环。

5、容器和数组

相比于数组,Java中的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 的使用涉及装箱拆箱,有一定的性能损耗,如果特别关注性能,可以考虑数组;
  2) 若数据大小事先已知,并且涉及的数据操作非常简单,可以使用数组;
  3) 表示多维数组时,数组往往更加直观;
  4) 业务开发使用容器即可。若涉及底层开发,如网络框架,性能优化等,则最好选择数组。

6、解答开篇问题

1、 效率原因:

从内存模型来看,“下标”也称为“偏移”。我们知道在C语言中数组名代表首地址(第一个元素的地址),a[0]就是偏移为 0 的位置。a[k]就表示偏移 k 个元素类型大小的位置。得出计算公式:

a[k]_address = base_address + k * type_size
但是钥匙从 1 开始计数,那这个公式就会变为:
a[k]_address = base_address + (k-1) * type_size

对比两个公式,如果从 1 开始编号,每次随机访问数组元素就多了一次减法运算,对于CPU来说就是多了一次减法指令。

数组作为非常基础的数据结构,通过下标访问数组元素又是数组上的基础操作,效率优化应做的很好,所以为了减少一次减法操作,数组选择了从 0 开始编号。

2、历史原因:

C语言的设计者用 0 开始计数下标,之后的Java、C++等高级语言都效仿C语言,沿用了从0开始计数的习惯。

还有一些语言并不是从0开始计数的,如:Matlab。 甚至Python还支持负数下标。

原文地址:https://www.cnblogs.com/gshao/p/12349522.html

时间: 2024-11-09 05:07:55

为什么很多编程语言中数组都是从 0 开始编号?的相关文章

Chapter 5 数组:为什么很多编程语言种数组都是从0开始编号?

如何实现随机访问? 线性表:数组,队列,链表,栈 非线性表:树,图 总结:数组用一块连续的内存空间,来存储相同类型的一组数据,最大的特点就是支持随机访问,但插入,删除操作也因此变得比较低效,平均情况时间复杂度未O(n).在平时的业务开发种,我们可以直接使用变成语言提供的容器类,但是,如果是特别底层的开发,直接使用数组会更合适. 原文地址:https://www.cnblogs.com/zhaohu/p/9974996.html

《数据结构与算法之美》 &lt;03&gt;数组:为什么很多编程语言中数组都从0开始编号?

提到数组,我想你肯定不陌生,甚至还会自信地说,它很简单啊. 是的,在每一种编程语言中,基本都会有数组这种数据类型.不过,它不仅仅是一种编程语言中的数据类型,还是一种最基础的数据结构.尽管数组看起来非常基础.简单,但是我估计很多人都并没有理解这个基础数据结构的精髓. 在大部分编程语言中,数组都是从 0 开始编号的,但你是否下意识地想过,为什么数组要从 0 开始编号,而不是从 1 开始呢? 从 1 开始不是更符合人类的思维习惯吗? 你可以带着这个问题来学习接下来的内容. 如何实现随机访问? 什么是数

05| 数组:为什么很多编程语言中数组都从 0 开始编号?

提到数组,我想你肯定不陌生,甚至还会自信地说,它很简单啊. 是的,在每一种编程语言中,基本都会有数组这种数据类型.不过,它不仅仅是一种编程语言中的数据类型,还是一种最基础的数据结构.尽管数组看起来非常基础.简单,但是我估计很多人都并没有理解这个基础数据结构的精髓. 在大部分编程语言中,数组都是从0开始编号的,但你是否下意识地想过,为什么数组要从0开始编号,而不是从1开始呢? 从1开始不是更符合人类的思维习惯吗? 你可以带着这个问题来学习接下来的内容. 如何实现随机访问? 什么是数组?我估计你心中

5、数组:为什么很多编程语言中数组都从0开始编号?

从数组的内存模型上来看,"下标"最确切的定义应该是"偏移(offset)".那么a[0]就是偏移为0 的位置,即首地址,a[k]就表示偏移k个type_size的位置,所以计算a[k]的内存地址: a[k]_address = base_adress+k*typ_size 但是,如果从1开始计数,那么a[k]=base_adress+(k-1)*typ_size.不难发现,从1开始,对CPU而言,多了一次减法指令,那么效率的优化就尽可能做到极致. 原文地址:http

编程语言中到处都能见到的$符号

0 前言 最近开发过程中使用了多种脚本语言,在这些语言中发现很多的$符号,而每种脚本语言的$符号的用法和意义均不相同,所以借博文总结总结.在linux应用开发中经常使用makefile脚本和shell脚本,有趣的是,这两种脚本使用$符号时存在明显差异,虽然在这两种脚本中$均和变量有关,但是makefile中变量使用括号包裹,而shell脚本缺并不需要括号. 1 shell脚本 定义变量 VAR=<value> 使用变量,变量名不需要使用括号包裹 $VAR 例子 URL="http:/

为什么在操作系统以及很多编程语言中把1970年作为时间的最小值

我们知道在Java中System.currentTimeMillis()方法来获取系统当前时间与1970年1月1日0点之间的毫秒差距.而在.NET中也有System.Environment.TickCount()方法来获取与1970年1月1日0点之间的毫秒差距,这个1970.01.01 00:00:00就是系统的最小时间,那么为什么系统的最小时间是这个,而不是0001.01.01 00:00:00呢? 这个还是要从计算机的构造说起,可以追溯到Unix操作系统.UNIX系统认为1970年1月1日0

为什么数组角标从0开始

背景 我们知道大部分编程语言中的数组都是从0开始编号的,即array[0]是数组的第一个元素.这个和我们平时生活中从1开始编号的习惯相比显得很反人类.那么究竟是什么样的原因让大部分编程语言数组都遵从了这个神奇的习惯呢?本文最初是受stackoverflow上的一个问题的启发,通过搜集和阅读了一些资料在这里做个总结.当然,本文摘录较多的过程结论,如果你想把这篇文章当做快餐享用的话,可以直接跳到文章末尾看结论. 最早的原因 在回答大部分我们无法解释的诡异问题时,我们最常用的辩词通常是历史原因.那么,

Java中数组操作 java.util.Arrays 类常用方法的使用

任何一门编程语言,数组都是最重要和常用的数据结构之一,但不同的语言对数组的构造与处理是不尽相同的. Java中提供了java.util.Arrays 类能方便地操作数组,并且它提供的所有方法都是静态的.下面介绍一下Arrays类最常用的几个方法. 1.  数组排序 Arrays工具类提供了一个sort方法,只需要一行代码即可完成排序功能. 2.  数组转换为字符串 Arrays提供了一个toString方法,可以直接把一个数组转换为字符串,这样可以方便观察数组里的元素. //来源:公众号[时光与

面试题3:在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

package siweifasan_6_5; /** * @Description:在一个长度为n的数组里的所有数字都在0到n-1的范围内. * 数组中某些数字是重复的,但不知道有几个数字是重复的.也不知道每个数字重复几次. * 请找出数组中任意一个重复的数字. * 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2. * @Parameters: // Parameters: // numbers: an array of integers //