概念
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
- 线性表(Linear List)。顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。
- 非线性表。比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。
- 连续的内存空间和相同类型的数据。正是因为这两个限制,它才有了一个堪称“杀手锏”的特性:“随机访问”。
数组如何实现根据下标随机访问数组元素
我们拿一个长度为 10 的 int 类型的数组 int[] a = new int[10]来举例。计算机给数组分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。
计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:
a[i]_address = base_address + i * data_type_size
data_type_size 表示数组中每个元素的大小。我们举的这个例子里,数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。
数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)
低效的“插入”和“删除”
假设数组的长度为 n,现在,如果我们将一个数据插入元素到第 k 个位置,需要将第 k~n 这部分的元素都顺序地往后挪一位;如果我们将第 k 个位置的元素删除,需要将第 k-1~n 这部分的元素都顺序地往前挪一位。很容易可以看出时间复杂度是O(n)。有种特殊情况,就是在数组尾部进行增删时间复杂度是O(1)。
删除可以利用 JVM 标记清除垃圾回收算法的核心思想。执行删除操作时只记录数据已经被删除,当数组没有更多空间存储数据时再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。
警惕数组的访问越界问题
代码因为书写错误,导致 for 循环的结束条件错写为了 i<=3 而非 i<3,所以当 i=3 时,数组 a[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;
}
在 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。根据我们前面讲的数组寻址公式,a[3]也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 a[3]=0 就相当于 i=0,所以就会导致代码无限循环。
容器不能完全替代数组
- Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
- 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
- 表示多维数组时,用数组往往会更加直观。比如 Object[][] array;而用容器的话则需要这样定义:ArrayList<ArrayList > array。
此文章为 2 月Day8学习笔记,内容来源于 极客时间《数据结构与算法之美》。
另外,最近重温操作系统时发现了一个免费精品好课,闪客的《Linux0.11源码趣读》,这个课给我感觉像在用看小说的心态学操作系统源码,写的确实挺牛的,通俗易懂,直指本源,我自己也跟着收获了很多。
这个课在极客时间上是免费的,口碑很不错,看评论下很多人在催更和重温,强烈推荐!