Jiang's blog

数据结构学习笔记---数组

Word count: 1.6kReading time: 5 min
2020/03/23 Share

学习自极客时间的《数据结构与算法之美》 作者:王争

数组

一. 什么是数组

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

  • 什么是线性表(Linear List)。顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。
    而非线性表的代表则有树,图,堆等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。
  • 连续的内存空间和相同类型的数据,使得数组支持随机访问,但是带来的麻烦就是删除和插入操作时变得不高效。

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

87GhSs.png

当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址

a[i]_address = base_address + i * data_type_size

其中 data_type_size 表示数组中每个元素的大小。

数组和链表的区别:
链表适合插入、删除,时间复杂度 O(1);数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。

二. 低效的“插入”和“删除”

** (1) 插入**

  1. 如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。 因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+…n)/n=O(n)。

  2. 如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数据插入到第 k 个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放入第 k 个位置。快排中就是利用了这个思想。

87YAET.png

(2)删除

  1. 和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。

  2. 实际上,在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,删除的效率就可以提高很多。这就是JVM 标记清除垃圾回收算法的核心思想。

JVM标记清除算法:

  • 大多数主流虚拟机采用可达性分析算法来判断对象是否存活,在标记阶段,会遍历所有 GC ROOTS,将所有 GC ROOTS 可达的对象标记为存活。只有当标记工作完成后,清理工作才会开始。

  • 不足:1.效率问题。标记和清理效率都不高,但是当知道只有少量垃圾产生时会很高效。2.空间问题。会产生不连续的内存空间碎片。

三. 数组的访问越界问题

1
2
3
4
5
6
7
8
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;
}

这段代码的运行结果并非是打印三行“hello word”,而是会无限打印“hello world。因为,数组大小为 3,a[0],a[1],a[2],而我们的代码因为书写错误,导致 for 循环的结束条件错写为了 i<=3 而非 i<3,所以当 i=3 时,数组 a[3]访问越界。
我们知道,在 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。根据我们前面讲的数组寻址公式,a[3]也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 a[3]=0 就相当于 i=0,所以就会导致代码无限循环。

对于数组访问越界造成无限循环,是编译器的问题,对于不同的编译器,在内存分配时,会按照内存地址递增或递减的方式进行分配。老师的程序,如果是内存地址递减的方式,就会造成无限循环。

四. 数组和容器

容器最大的优势就是可以将很多数组操作的细节封装起来。比如前面提到的数组插入、删除数据时需要搬移其他数据等。另外,它还有一个优势,就是支持动态扩容。我们就完全不需要关心底层的扩容逻辑。
所以,在平时的业务开发中,我们可以直接使用编程语言提供的容器类,但是,如果是特别底层的开发,直接使用数组可能会更合适。

五. 问题思考

二维数组的内存寻址公式是怎样的呢?

对于 m * n 的数组,a [ i ][ j ] (i < m,j < n)的地址为:

address = base_address + ( i * n + j) * type_size

关于数组和链表的几个必知必会的代码实现

  • 实现一个支持动态扩容的数组

  • 实现一个大小固定的有序数组,支持动态增删改操作

  • 实现两个有序数组合并为一个有序数组

力扣关于数组的题目

11. 盛最多水的容器
15. 三数之和
153. 寻找旋转排序数组中的最小值

我虽然刷的题不多,哈哈,但是一般数组这个数据结构的解题算法和双指针,排序,二分查找等结合的比较多,个人总结哈!

CATALOG
  1. 1. 数组
    1. 1.0.1. 一. 什么是数组
    2. 1.0.2. 二. 低效的“插入”和“删除”
    3. 1.0.3. 三. 数组的访问越界问题
    4. 1.0.4. 四. 数组和容器
    5. 1.0.5. 五. 问题思考
  2. 1.1. 关于数组和链表的几个必知必会的代码实现
  3. 1.2. 力扣关于数组的题目