学习自极客时间的《数据结构与算法之美》 作者:王争
数组
一. 什么是数组
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
- 什么是线性表(Linear List)。顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。
而非线性表的代表则有树,图,堆等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。 - 连续的内存空间和相同类型的数据,使得数组支持随机访问,但是带来的麻烦就是删除和插入操作时变得不高效。
数组是如何实现根据下标随机访问数组元素的?
当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址
a[i]_address = base_address + i * data_type_size
其中 data_type_size 表示数组中每个元素的大小。
数组和链表的区别:
链表适合插入、删除,时间复杂度 O(1);数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
二. 低效的“插入”和“删除”
** (1) 插入**
如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。 因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+…n)/n=O(n)。
如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数据插入到第 k 个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放入第 k 个位置。快排中就是利用了这个思想。
(2)删除
和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。
实际上,在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,删除的效率就可以提高很多。这就是JVM 标记清除垃圾回收算法的核心思想。
JVM标记清除算法:
大多数主流虚拟机采用可达性分析算法来判断对象是否存活,在标记阶段,会遍历所有 GC ROOTS,将所有 GC ROOTS 可达的对象标记为存活。只有当标记工作完成后,清理工作才会开始。
不足:1.效率问题。标记和清理效率都不高,但是当知道只有少量垃圾产生时会很高效。2.空间问题。会产生不连续的内存空间碎片。
三. 数组的访问越界问题
1 | int main(int argc, char* argv[]){ |
这段代码的运行结果并非是打印三行“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. 寻找旋转排序数组中的最小值
我虽然刷的题不多,哈哈,但是一般数组这个数据结构的解题算法和双指针,排序,二分查找等结合的比较多,个人总结哈!