数据结构-数组

数组作为线性表数据结构,凭借连续内存空间和相同类型数据实现随机访问,但也导致插入和删除操作低效。插入操作平均时间复杂度为O(n),删除操作同样涉及数据搬移。Java中的ArrayList等容器可以封装数组操作,动态扩容但可能增加性能消耗。数组从0开始编号简化寻址公式,提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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

线性表:顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前后两个方向。而且除了数组、链表、队列、栈等也是线性表结构。
image.png

而与它对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为在非线性表中,数据之间并不是简单的前后关系。
image.png

连续的内存空间和相同类型的数据:正是因为这两个条件的限制,才有了一个特别厉害的特性:随机访问。但是有利有弊,这两个限制也让数组的很多操作变得很低效,比如想要在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。

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

我们拿一个长度为10的int类型的数组举例

int[] a = new int[]

image.png
计算机给数组a分配了一块连续内存1000-1039,其中存块的首地址为base_addr = 1000。

我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据,当计算机需要访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:
a[i] 内存地址 = base_addr + i * 每个元素大小(因为数组中存的是int类型,所以元素大小为4个字节)

抵效的插入和删除

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

插入

假设数组的长度为n,现在我们需要将一个数组插入到数组的第k
个位置。为了给k腾出位置,我们就需要把k-n这部分的元素顺序的往后依次挪一位。
如果这个k代表是末尾,那么就不要移动数据,那么此时的时间复杂度为O(1),如果k是在数组的开头位置,那么所有的数据都要依次往后挪一位,那么最坏的时间复杂度为O(n)。因为我们在每一个位置插入元素的概率是一样的,所以平均情况时间复杂度为O(n)。

以上的情况是为了在数组有序的排序,那么如果我们在数组中没有任何规律,只是当做存储数据的集合,在这种情况下,我们如果要将一个数据插入到数组第k哥位置,为了防止大规模数据的搬移,我们还有一种简单方法就是,直接将第k的数据搬到数组元素的最后位置,把新元素放到第k个位置。见下图:
image.png
我现在想把x放到c的位置,那么我就可以把c放到数组最后,把x放到之前c的位置。这种方式在特定的场景下,时间复杂度就会降为O(1)。这种处理思想在快速排序中是被应用到的。

删除

跟插入操作类似,如果我想删除第k个位置的元素,为了保持内存的连续性,我们也需要搬运数据,不然就会导致内存不会连续。

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

实际上在某些特定的场景下,我们并不一定追求数组的连续性,如果我们将多次的删除操作集中一起执行,那么删除的效率是不是会提高很多?
举个例子:
image.png
一个数组中存储了8个元素,现在我们依次删除a,b,c三个元素,为了避免d,e,f,g,h这三个数据会被搬运三次,我们可以先记录已经删除的数据。每次删除操作并不是真的搬运数据,只是在基础上先打上一个删除的标记。如果当前数据没有更多的空间存储数据,我们再出发删除操作,将要删的一次性删除,这样就大大减少了删除操作导致的数据搬移。

容器能否完全替代数组

针对数组的类型,Java中提供了ArrayList等一些容器。

那我们什么时候适合数组,什么时候适合容器?
ArrayList最大的优势就是可以将很多数组操作的细节封装起来,比如前面提到的数组插入删除时需要搬移数据等。另外还有一个优势就是动态扩容。

数组本身定义的时候就要指定大小,因为需要分配连续的内存空间。如果我们申请了大小为10的数组,当第11个数据需要存储到数组中,我们就要重新分配一块更大的空间,将原来的数据复制过去,然后重新将新的数据插入。

如果使用了ArrayList,我们就完全不需要关系底层的扩容逻辑,ArrayList已经帮我们实现好了,每次存储空间不够的时候,都会扩容1.5倍。
不过这里需要注意一点,因为扩容操作设计内存的申请和数据搬移,是比较耗时的,所以如果先能确定需要存储的数据大小,最好在创建ArrayList的时候事先指定数据大小。

回到问题,那什么时候适合数组,什么时候适合容器?
1.ArrayList无法存储基本类型,比如int、long需要封装为Integer、Long类,而插箱装箱需要一定的性能消耗,如果特别关注性能,或者希望使用基本类型,就可以选择数组
2.如果数据大小事先固定好,并且对数据的操作很简单,用不到ArrayList提供的大部分方法,就可以直接使用数组。
3.如果要表示多维数组,用数组会更直观
4.对于业务开发,直接使用容器就足够了。毕竟损失一丢性能,完全不会影响到系统整体的性能。

数组为什么从0开始编号,而不是从1开始?

从内存模型来看,下标最好的定义应该是偏移。
如果计算a[k]的内存地址,需要用到下面的公式

a[k] 内存地址 = base_addr + k * 每个元素大小

如果数组开始从1开始计数,那么计算a[k]的内存地址就变为

a[k] 内存地址 = base_addr + (k-1* 每个元素大小

对比两个公式,如果从1开始编号,每次随机访问数组元素都多了一次减法运算,对于cpu来说,也多了一次减法指令。为了保持效率吧,选择了从0开始。但主要原因还是c语言开始就这么定义的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值