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

版权声明:ahoj https://blog.csdn.net/Hanoi_ahoj/article/details/82931630

原文地址

目录

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

  • 1.1 效率原因
  • 1.2 历史原因

2 数组的特点

  • 2.1 随机访问
  • 2.2 低效的“插入”和“删除”
    • 2.2.1 低效原因
    • 2.2.2 改进方法

3 数组越界问题

4 容器和数组用哪个更好


正文

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

1.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 开始编号。

1.2 历史原因

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

还有一些语言并不是从0开始计数的,如:Matlab。

据我所知:python还支持负数下标。

2 数组的特点

2.1 随机访问

数组是一种线性数据结构,占用一段连续的内存,存储相同类型的数据。

例如:这样一段代码

int arr[10] = { 0 };
for (int i=0; i<10; i++)
{
        arr[i] = i;
}

查看数组信息:(addr=0x7ffeefbff530, size=40, variable expression=‘arr’).

我们再来打断点看看内存情况:

arr[9]

看内存情况我们得到:

arr共使用40字节内存,首地址为0x7ffeefbff530

arr[0]地址为:0x7ffeefbff530

arr[9]地址为:0x7ffeefbff554

每个int有4个字节,故arr[9]结尾为0x7ffeefbff558

0x7ffeefbff558-0x7ffeefbff530 = 24 16进制

(28)16->(40)10

数组随机访问时:通过计算元素存储位置的内存地址来访问相应的元素

a[i]_address = base_address + i * data_type_size

数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。

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

2.2.1 低效原因

数组为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效。

插入:

假设数组长度为n,要将一个数据插入到第 k 个位置,为了把第 k 个位置腾出来,我们需要将k~n这部分元素顺序的向后移动一位。

插入的时间复杂度:

最好情况:在数组的末尾插入元素,不需要移动数据了,时间复杂度为O(1)。

最坏情况:在开头插入元素,那就需要把所有的数据向后移动一位,时间复杂度为O(n)。

平均时间复杂度:(1+2+…+n)/n = O(n)。

删除:

删除操作和插入操作类似,删除了某一元素后,需要搬移数据。

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

2.2.2 改进方法

插入

如果数组中的元素没有任何规律,数组只是被当作一个数据集合,在这种情况下,如果要将某个数据插入到第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三个元素,需要搬移三次后面的数据,为了避免这个重复的搬移工作,可以先记录下来已经删除的数据,每次的删除操作并不是真正的搬移数据,只是记录数据已经被删除,当数组中没有更多的空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了搬移工作,这也是标记清除垃圾回收算法的核心思想。

3 数组越界问题

有如下代码:

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;
}

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

为啥呢?

因为在C语言中,除了受限制的内存,其他所有内存空间都是可以自由访问的。

那为什么会无限打印呢?

根据我所学和百度的知识解释下:函数体内的局部变量存在栈区,在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的地址就不紧跟着数组后面了。另外这个还和编译环境有关,对于不同的编译器,在内存分配时,会按照内存地址递增或递减的方式进行分配。如果是内存地址递减的方式,就会造成无限循环。

4 容器和数组用哪个更好

对于数组类型,很多语言提供了容器类,例如我正在学的C++中的Vector。

那什么时候用数组,什么适合用容器呢?

容器的优势就是可以将很多数组操作的细节封装起来,有的还支持动态扩容。

王争老师:如果特别关注性能,或者数据大小已知,且对数据操作非常简单,用不到容器提供的大部分方法,可以是用数组。


原文地址

猜你喜欢

转载自blog.csdn.net/Hanoi_ahoj/article/details/82931630