数组在任何一种编程语言中,都是一种基础的数据结构。大家提到数组,都不会感到陌生,甚至会拍拍胸脯自信地说:这很简单啊。
大家有没有想过:数组为什么从 0 开始编号呢,为什么不从 1 开始呢?希望你带着这个疑问去看接下去的文章。
文中会涉及到时间复杂度的分析,不懂的朋友可以先看看这篇。
复杂度分析
什么是数组?
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同数据类型的数据。
这句话里有几个关键词,下面我就来给你解读一下。
1. 线性表(Linear List)。顾名思义,线性表就是数据排成一条线一样的结构。链表、队列、栈等数据结构也是线性表结构,我会在后文中一一讲解,敬请关注哦。而堆、树、图等则是非线性的,即数据间不是简单的前后关系。
2. 连续的内存空间,相同的数据类型。正是由于数组的这个杀手锏特性,数组可以实现随机访问,但是任何一种数据结构都是有其适用场合的,有利有弊的。数据的插入和删除操作,为了保证连续性,需要做大量的工作。
而数组的随机访问由寻址方式实现:
a[i]_address = base_address + i * data_type_size
data_type_size 为数组中每个元素的大小,如果数组中存储 int 类型数据, data_type_size 为 4 个字节。
举个例子:现在我们创建一个长度为 5 的 int 类型的数组,int[] a = new int[5]; 计算机会在内存中给数组 a[5] 分配一块连续的内存,我们设内存块的首地址为 base_address 为 1000,即
int a[5] | 内存地址 |
---|---|
a[0] | 1000~1003 |
a[1] | 1004~1007 |
a[2] | 1008~1011 |
a[3] | 1012~1015 |
a[4] | 1016~1019 |
当计算机需要访问元素 a[1] 时,它会根据上面的寻址公式,即:
a[i]_address = base_address + i * data_type_size
来计算出 a[1] 的内存地址,从而实现随机访问,根据下标访问时间复杂度为 O(1)。
另外在面试中面试官经常会问数组和链表的区别,我们可以这样回答:
链表适合插入、删除,时间复杂度为 O(1);数组适合查找,但是查找的时间复杂度并不为 O(1),即使是排好序的数组,你用二分查找,时间复杂度也是 O(logn)。
数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
数组的插入和删除操作
我们先来看插入操作。
如果我们在数组的末尾插入数据,我们不需要移动数据,时间复杂度为 O(1)。我们在数组的开头插入数据,需要移动 n 个数据,时间复杂度为 O(n)。假设我们在数组的任何位置插入数据的概率是相同的,则平均时间复杂度为 (1+2+3+···+n) / n = O(n)。
我们再来看删除操作,和插入类似,删除数组最后一个数据,时间复杂度为 O(1)。删除开头第一个数据,需要移动 n 个数据,时间复杂度为 O(n)。平均时间复杂度为 (1+2+3+···+n) / n = O(n)。
我们看到数组的插入和删除操作都是比较低效的,而这都是数组的连续性带来的。实际上,在某些特殊场景里,我们不一定会去追求数组中数据的连续性,如果我们可以将多次的删除集中在一起执行呢?如果这样做的话,执行效率是不是高了很多呢?
我们继续延用上面的例子给你解释:
假设数组 a[5] 存储了 5 个元素:1, 2, 3, 4, 5。现在,我们要依次删除 1, 2, 3 三个元素。我们可以先记录下已经删除的数据,每次的删除操作并不是真正地搬移数据,只是删除记录数据。当数组没有更多空间存储数据时,我们触发一次真正的删除操作,这样执行效率大大增加。
假如你了解 JVM 的垃圾回收算法,你会发现这就是它的核心思想。
数据结构与算法的最大魅力莫过于此,我们可以在很多地方发现他的影子,是程序的灵魂,也会有很多我们意想不到的「骚操作」。
容器能否代替数组
我们知道很多语言都提供了容器类,由于笔者比较熟悉 Java,这里我拿 Java 举例,java 中的 ArrayList 作为 Java 工程师几乎天天都在使用。我们知道 ArrayList 封装了很多数组操作的细节,改用方法代替,还有就是支持动态扩容,我们完全不用担心底层逻辑,当我们的空间不够用时,ArrayList 会自动帮我们将空间扩容成 1.5 倍。
但是 ArrayList 是无法存储基本类型的,如 int、long,需要封装成 Integer、Long 类,另外对于数据大小我们事先已知,数据操作较为简单,我们可以优先考虑数组,毕竟我们不需要用到 ArrayList 提供的大多数方法。
最后,我来回答大家这个问题:数组为什么从 0 开始编号呢,为什么不从 1 开始呢?
答曰:历史原因,因为 C 语言的设计者设计数组时是从 0 开始的,后来 Java 等高级语言都效仿 C 语言,再说,也让 C 语言学习者转入其他高级语言的学习减少了成本。怪不得,学校老师总是要求大家好好学 C 语言呢?
今天数组的学习就到这里了,我留个小问题,请你思考一下,二维数组的寻址是怎样的呢?
欢迎留言与我分享!
System.out.println("点个喜欢!欢迎关注我!");