前言
数组是一种数据结构,用于存储相同类型的多个元素。它是一种线性数据结构,其中的元素按照顺序排列,并且可以通过索引访问和操作。每个元素在数组中都有一个唯一的索引,用于标识其位置。
那大家有想过数组根据索引随机访问为什么很快呢?
数组存储结构
数组之所以能够快速进行随机访问,是因为数组内的元素是连续存储的。在创建数组时,内存会按照顺序进行分配,并且每个元素都会相邻地存储在内存中。
这种布局使得通过索引来进行快速访问成为可能,因为可以利用元素的索引来计算其内存地址。由于元素存储在相邻的内存位置上,处理器可以根据索引快速计算出任何元素的内存地址。
ini
int[] array = {1,2,3,4,5}
知道了数组的数据 起始地址 BaseAddress
,就可以由公式 BaseAddress + i * size
计算出索引 i 元素的地址
- i 即索引,在 Java、C 等语言都是从 0 开始
size
是每个元素占用字节,例如int
占 4,double
占 8
随机访问性能
即根据索引查找元素,时间复杂度是O(1)。
那么二维数组呢?
二维数组是一种特殊的数组,可以理解为一维数组中的每个元素存放的是一个数组,比如下面的二维数组:
ini
int[][] array = {
{11, 12, 13, 14, 15},
{21, 22, 23, 24, 25},
{31, 32, 33, 34, 35},
};
内存结构图如下:
- 二维数组占 32 个字节,其中 array[0],array[1],array[2] 三个元素分别保存了指向三个一维数组的引用
- 三个一维数组各占 40 个字节
- 它们在内层布局上是连续的
二维数组哪种遍历顺序快呢?
针对二维数组的遍历方式,下面哪种比较快呢?
- ij方法: 先遍历行
ini
public static void ij(int[][] a, int rows, int columns) {
long sum = 0L;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
sum += a[i][j];
}
}
System.out.println(sum);
}
- ji方法: 先遍历列
ini
public static void ji(int[][] a, int rows, int columns) {
long sum = 0L;
for (int j = 0; j < columns; j++) {
for (int i = 0; i < rows; i++) {
sum += a[i][j];
}
}
System.out.println(sum);
}
测试一下
ini
int rows = 1000000;
int columns = 14;
int[][] a = new int[rows][columns];
StopWatch sw = new StopWatch();
sw.start("ij");
ij(a, rows, columns);
sw.stop();
sw.start("ji");
ji(a, rows, columns);
sw.stop();
System.out.println(sw.prettyPrint());
执行结果
markdown
0
0
StopWatch '': running time = 96283300 ns
---------------------------------------------
ns % Task name
---------------------------------------------
016196200 017% ij
080087100 083% ji
可以看到 ij
的效率比 ji
快很多,为什么呢?
这里不得不说计算机组成原理中的一个基础原理,局部性原理。
- cpu 读取内存(速度慢)数据后,会将其放入高速缓存(速度快)当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了。
- 缓存的最小存储单位是缓存行(
cache line
),一般是 64 bytes,一次读的数据少了不划算啊,因此最少读64 bytes
填满一个缓存行,因此读入某个数据时也会读取其临近的数据 ,这就是所谓空间局部性。
这下原因显而易见了,因为ij
的方式是按照数组存储的顺序读取,会命中高速缓存,而ji
方式并不会。
总结
最后我们根据数组的存储特性总结下数组这一数据结构的特点如下 :
-
存储相同类型的元素:数组中只能存储相同类型的元素,例如整数、浮点数、字符等。这种限制使得数组在处理同一类型数据时非常有效。
-
连续的内存空间:数组的元素在内存中是连续存储的,每个元素占用相同的内存大小。这种连续存储的结构使得数组能够进行快速的随机访问。
-
固定大小:数组在创建时需要指定大小,一旦创建后,大小通常不能改变。这意味着数组的长度是固定的,无法动态增加或减少。
-
使用索引访问元素:数组中的每个元素都有一个唯一的索引,通过索引可以直接访问和修改数组中的元素。索引从0开始,依次递增。
-
随机访问效率高:由于数组的元素在内存中连续存储,并且可以通过索引直接访问,因此数组具有快速的随机访问能力。通过索引,可以在常数时间复杂度O(1)内访问特定位置的元素。
-
内存空间的浪费:由于数组需要在创建时指定固定大小,可能会导致内存空间的浪费。如果数组的大小远大于实际存储的元素数量,会浪费一部分内存空间。
-
插入和删除元素不高效:由于数组的大小固定,插入和删除元素时需要进行元素的移动操作,这可能导致效率较低。特别是在数组的开头或中间插入/删除元素时,需要移动较多的元素。
总的来说,数组是一种简单而有效的数据结构,适用于需要快速随机访问元素的场景。然而,由于其固定大小和插入/删除的低效性,对于频繁的插入和删除操作,可能需要考虑链表等数据结构的使用。