笔记:数据结构与算法之美 05 | 数组:为什么很多编程语言中数组都从0开始编号?

数组

  • 一种线性表数据结构
  • 一组连续的内存空间
  • 存储一组具有相同类型的数据

线性表(Linear List)

数据排成一条线一样的结构

  • 数据最多只有前和后两个方向

tips:除了数组,链表、队列、栈等也是线性表结构

笔记:数据结构与算法之美 05 | 数组:为什么很多编程语言中数组都从0开始编号?_第1张图片

非线性表

数据之间并不是简单的前后关系

tips:比如二叉树、堆、图等

笔记:数据结构与算法之美 05 | 数组:为什么很多编程语言中数组都从0开始编号?_第2张图片

连续的内存空间和相同类型的数据

正因如此,才有了“随机访问”的特性

数组如何实现根据下标随机访问数组元素
笔记:数据结构与算法之美 05 | 数组:为什么很多编程语言中数组都从0开始编号?_第3张图片

通过如下寻址公式,计算出该元素存储的内存地址

a[i]_address = base_address + i * data_type_size

纠正一个常见的错误:
问:数组和链表的区别
答:数组适合查找,查找时间复杂度为O(1) ,链表适合插入删除,时间复杂度为O(1)
补充:数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。即便是排好序的数组,用二分查找,时间复杂度也是O(logn)

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

插入操作
问:假设数组长度为n,将数据插入到数组的第k个位置
答:

  • 有序数组:需要把k~n这部分元素顺序往后挪一位
    如果在数组末尾插入元素,那么就不需要移动数据,时间复杂度为O(1)
    如果在数组开头插入元素,那么所有数据往后移动一位,最坏时间复杂度是O(n)
    因为在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为(1+2+…n)/n=O(n)

  • 无序数组:将第k位的数据搬移到数组元素的最后,把新的数据直接放入到第k个位置
    此时时间复杂度就会降为O(1)。

此思想在快排中也会用到

删除操作
和插入类似,如果要删除第k个位置的数据,为了内存连续性,也需要搬移数据
删除末尾时间复杂度O(1)
删除开头时间复杂度O(n)
平均情况时间复杂度O(n)
实际情况中

  • 数组空间够用时:将要删除的元素进行标记,进行逻辑删除
  • 数组空间不够时:统一进行删除,减少数据搬移次数

JVM标记清除垃圾回收算法的核心思想就是如此,mysql的b+树上删除也是这么做的

数组的访问越界问题

由于访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。
java:会抛出ArrayOutOfIndexException
c:需要自己检查处理

容器能否完全替代数组

数组:定义时需要预先指定大小,因为需要分配连续的内存空间
如果已经申请了大小为10的数组,当需要存储第11个数据时,需要重新分配一块更大的空间,将原先的数据复制过去,然后再将新的数据插入
ArrayList:无需关心底层扩容逻辑,每次存储空间不够,将空间自动扩容为1.5倍大小(空间不足则新增一半)

由于扩容操作涉及内存申请和数据搬移,比较耗时,所以如果事情能确定数据大小,最好在创建时事先指定数据大小

数组更合适的场景

  • ArrayList无法存储基本类型,比如int、long,需要封装为Integer、Long类,如果特别关注性能,或者希望使用基本类型,就可以使用数组
  • 事先大小已知可以考虑
  • 多维数组
  • 非常底层的开发,比如网络框架

解答开篇

如果从0开始,计算a[k]的内存地址公式为

a[k]_address = base_address + k * type_size

如果从1开始,计算a[k]的内存地址公式为

a[k]_address = base_address + (k-1)*type_size

从1开始,每次随机访问数组元素都多了一次减法运算!

你可能感兴趣的:(数据结构,算法,链表)