数据结构与算法之美学习笔记:第五讲
一、数组的基本概念
数组是一种线性数据结构,它用连续的内存空间,来存储一组具有相同类型的数据
1、线性表
第一线性表:顾名思义,线性表就是数据排成像一条线一样的结构,每个线性上的数据最多只有前后两个方向,其实除了数组、链表、队列、栈等也是线性结构
而与它相对立的概念是非线性表,比如二叉树、堆、图等,之所以叫非线性,是因为,在非线性表中、数据之间并不是简单的前后关系
2、连续的内存空间和相同类型的数据
第二个是连续的内存空间和相同类型的数据,正是因为这两个限制,它才有了一个堪称“杀手锏”的特性“随机访问”。但有利有弊,这两个限制
也让数组的很多操作变得非常低效,比如要在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。
二、如何实现随机访问
1、如何随机访问数组中的元素
说到数据的访问,那你知道数组是如何实现根据下标随机访问数组元素的吗?
我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内训中的数据,但计算机需要随机访问数组中的某个元素时
它会首先通过下面的寻址公式,计算出该元素存储的内存地址:
a[i]_address = base_address + i * data_type_size
data_type_size表示数组中每个元素的大小,我们举的这个例子里,数组中存储的是int类型数据,所以data_type_size就为4个字节。
2、数组和链表的区别
很多人回答说:链表适合插入、删除、时间复杂度O(1);数组适合查找,查找时间复杂度为 O(1)”
实际上这种表述是不准确的,数组是适合查找操作,但是查找的时间复杂度并不为O(1),即便是排好序的数组,你用二分查找,时间复杂度也是O(logn)。
所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
三、低效的插入和删除
1、究竟为什么会导致低效?
插入:最好O(1) 最坏O(n) 平均O(n)
删除:最好O(1) 最坏O(n) 平均O(n)
2、又有哪些改进方法呢?
数组若无序,插入新的元素时,可以将第K个位置元素移动到数组末尾,把新的元素,插入到第k个位置,此处复杂度为O(1)
实际上,在某些场景下,我们并不一定非得追求数组中数据的连续性,如果我们将多次删除操作集中在一起执行,删除的效率是不是是提高很多的呢?
多次删除集中在一起,提高删除效率
记录下已经被删除的数据,每次的删除操作并不是搬移数据,只是记录数据已经被删除,当数组没有更多的存储空间时,
再触发一次真正的删除操作。即JVM标记清除垃圾回收算法。
四. 警惕数组的访问越界问题
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语言循环越界访问的例子说明访问越界的bug。此例在《C陷阱与缺陷》出现过,很惭愧,看过但是现在也只有一丢丢印象。翻了下书,替作者加上一句话:
如果用来编译这段程序的编译器按照内存地址递减的方式给变量分配内存,那么内存中的i将会被置为0,则为死循环永远出不去。
五. 容器能否完全替代数组
1、ArrayList相对于数组有什么优势
相比于数字,java中的ArrayList封装了数组的很多操作,并支持动态扩容。一旦超过存储容量,扩容时比较耗内存,因为涉及到内存申请和数据搬移。
所以、如果事先能确定需要存储的 数据大小,最好在创建ArrayList 的时候事先指定数据大小
比如我们要从数据中去吃醋10000条数据放入ArrayList。我们看下面几行代码,你会发现,相比之下,
实现指定数据大小可以省掉很多次内存申请和数据搬移操作
ArrayList<User> users = new ArrayList(10000); for (int i = 0; i < 10000; ++i) { users.add(xxx); }
2、数组适合的场景:
1) Java ArrayList 的使用涉及装箱拆箱,有一定的性能损耗,如果特别管柱性能,可以考虑数组
2) 若数据大小事先已知,并且涉及的数据操作非常简单,可以使用数组
3) 表示多维数组时,数组往往更加直观。
4) 业务开发容器即可,底层开发,如网络框架,性能优化。选择数组。
六. 解答开篇问题
1、从性能方面
1) 从偏移角度理解a[0] 0为偏移量,如果从1计数,会多出K-1。增加cpu负担。为什么循环要写成
for(int i = 0;i<3;i++)
而不是
for(int i = 0 ;i<=2;i++)
第一个直接就可以算出3-0 = 3 有三个数据,而后者 2-0+1个数据,多出1个加法运算,很恼火。
2) 也有一定的历史原因
1、C语言设计者用哪个0开始计数数组下表,之后java、JavaScript 等高级语言都效仿了 C 语言
2、或者说,为了在一定程度上减少 C 语言程序员学习 Java 的学习成本,因此继续沿用了0开始计数的 习惯
3、实际上,很多语言中数组并不是从0开始计数的,比如 Matlab。甚至还有一些语言支持负数下标,比如Python
七、课后思考题
前面我基于数组的原理引出 JVM 的标记清除垃圾回收算法的核心理念我不知道你是否使用 Java 语言,理解JVM
如果你熟悉,可以在评论区回顾下你理解的标记清除垃圾回收算法。
前面我们讲到一维数组的内存寻址公式,那你可以思考一下类比一下,二维数组的内存寻址公式是怎样的呢?