在c语言中 数组的下标都是从0开始的,为什么很多编程语言中数组都是从 0 开始编号?...

1、什么是数组?

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

概念解析:

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

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

4c4e736d3811db4203f0483a3bf9bb91.png

c45554731bd9c3f5c17266c5109aef45.png

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

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

d401e7d48359a7cef779c5c357d3af3c.png

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

1 a[i]_address = base_address + i * data_type_size2

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");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 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还支持负数下标。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值