数据结构 -- 数组

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

这里的关键字是“线性表”,“连续的内存空间和相同的数据类型

 

线性表:就是数据排成一条线一样的数据结构。除了数组,还有链表,队列,栈等。对于线性表和非线性表我前面的博客中有介绍。这里就不详述了

 

连续的存储空间和相同的数据类型:正因为这个原因,数组才有了一个堪称杀手锏的特性“随机访问”。

也就是根据索引或者说下标去获取数组中的数据。这个有利那同时也存在弊端,

弊端一:这两个限制让数组的很多操作变得低效,例如新增、删除操作,为了保证存储空间得连续性,需要做大量的数据搬移工作

弊端二:如果我们需要申请100MB的内存空间,而内存剩余的空间超过了100MB,但内存中却没有超过100MB的连续内存空间,那么仍然会申请失败。

弊端二让我联想到了JVM中的“标记-清理”算法。当使用标记清理算法清除了两个1KB的不连续的内存空间后,我们去创建了一个2KB的对象,那么这个两个1KB的内存地址就不能够使用,需要申请新的内存地址。就会造成大量的不连续的内存碎片。

 

问题:数组是如何通过下标随机访问数组元素的呢?

内存寻址法:a[i]_address  = base_address + i * data_type_size  (一维数组)

其中:"i"表示数组下标, base_address表示内存块的首地址,data_type_size表示数组中每个数据元素的大小

对于二维数组a[n][m]中找到a[i][j]的内存地址

内存寻址方法:a[i][j]_address = base_address +  (i*n+j) * data_type_size

以此类推:三位数组:a[n][m][q]

三维数组:a[i][j][k]_address = base_address +  (i*n*m+j*m+k) * data_type_size

…………

 

 

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

插入:如果往第K个位置插入一个数据元素,那么从数组尾部,到第K个位置的数据元素都往后移动一位,再把数据插入第K个位置。

时间复杂度分析:最好的情况O(1),最坏的情况O(n),平均情况(1+2+3+…+n)/n=O(n)

优化:把要插入的值先插入数组的末尾,再与第K个位置的数据元素交换位置,这也是快速排序的处理思想。

删除:跟插入操作类似,删除第K个位置的数据元素后,还需要把从第K个位置开始,到数组尾部的数据元素往前移动一位。

时间复杂度分析: 最好的情况O(1),最坏的情况O(n),平均情况O(n)

在一些实际的场景中,需要对多个数据元素进行删除,会把多个数据元素放到一起删除,而不是一个一个的进行删除。实际应用的场景,就是JVM的“标记-整理”算法,通过可达性分析法,先标记需要删除的垃圾数据。当内存满的时候,触发依次垃圾回收,把标记了的垃圾数据一次性清理掉,在对内存进行整理操作。

 

容器

容器ArrayList相对于数组有两大优势

优势一:将很多数组操作细节进行了封装,比如前面提到了,插入删除操作,需要对数据进行搬移

优势二:动态扩容,当存储空间不够的时候,会将空间自动扩容为1.5倍大小

所以在实际的开发过程中一般使用容器ArrayList

那什么时候,使用数组呢?

1、特别关注性能或者希望使用基本数据类型的时候,这样的场景一般是底层开发

2、事先知道数组大小,且操作简单,用不到ArrayList大部分方法,可以使用数组

3、表示多维数组的时候,数组更为直观,比如:Object[][] array,如果用集合的话ArrayList<ArrayList<Object>>

 

 

问题:数组的下标,为什么是从0开始 ,而不是从1开始?

1、性能原因

“下标”最确切的的定义应该是“偏移(offset)”。如果用a来表示数组的首地址,a[0]的偏移为0的位置,也就是首地址,而a[k]表示偏移k个type_size的位置,所以计算a[K]的内存地址公式为:

a[k]_address  = base_address + k * data_type_size

如果下标为1的话,计算a[k]的内存地址的公式为:

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

对于CPU来说,就会多一次减法指令

2、历史原因

c语言的数组下标是0,java沿用C语言这一特性

以前在学习多线程CAS的时候,老师总是说到偏移量,之前还不明白,现在懂了

 

数组访问越界问题?

这个问题为什么最后才总结呢,在高手的世界中,只能默默的当一个吃瓜群众。

java语言的话,java本身会做越界检查,如果越界,会抛出越界异常:java.lang.ArrayIndexOutOfBoundsException。

对于C语言,下面代码会陷入无限循环中

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

导致无限循环的原因:摘一些高手的评论,本人暂时就只能默默吃瓜了,看不懂

高手1:函数体内的局部变量存在栈上,且是连续压栈。在Linux进程的内存布局中,栈区在高地址空间,从高向低增长。变量i和arr在相邻地址,且i比arr的地址大,所以arr越界正好访问到i。当然,前提是i和arr元素同类型,否则那段代码仍是未决行为。

高手2:例子中死循环的问题跟编译器分配内存和字节对齐有关 数组3个元素 加上一个变量a 。4个整数刚好能满足8字节对齐 所以i的地址恰好跟着a2后面 导致死循环。。如果数组本身有4个元素 则这里不会出现死循环。。因为编译器64位操作系统下 默认会进行8字节对齐 变量i的地址就不紧跟着数组后面了。

高手3:关于数组越界访问导致死循环的问题,我也动手实践了一下,发现结果和编译器的实现有关,gcc有一个编译选项(-fno-stack-protector)用于关闭堆栈保护功能。默认情况下启动了堆栈保护,不管i声明在前还是在后,i都会在数组之后压栈,只会循环4次;如果关闭堆栈保护功能,则会出现死循环。请参考:https://www.ibm.com/developerworks/cn/linux/l-cn-gccstack/index.htm

 

参考:数据结构之美  --王争 课程

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值