数据结构与算法(2)数组
数组
一、什么是数组?
- 数组是一种线性表数据结构。他用一组连续的内存空间,来存储一组具有相同类型的数据。
线性表
- 线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。而与它对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,非线性表中,数据之间并不是简单的前后关系。
连续的内存空间和相同类型的数据
- 数组正是因为这两个限制,它才有了一个堪称“杀手锏”的特性:“随机访问”。但有利就有弊,这两个限制也让数组的很多操作变得非常低效,比如想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。
寻址公式
-
a[i]_address = base_address + i * data_type_size
- base_address为内存块的首地址
- i为数组中元素所在
- data_type_size表示数组中每个元素的大小
低效的“插入”和“删除”
-
插入操作
如果在数组末尾插入元素就不需要移动数据,这时的时间复制度为O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次向后移动一位,所以最坏的时间复制度为O(n)。因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复制度为(1+2+...n)/n=O(n)。
如果数组是有序的,如果要将某个数据插入到第k个位置,为了避免大规模的数据搬移,我们可以直接将第k位的数据搬移到数组元素的最后,把新的元素直接放入第k个位置。
-
删除操作
跟插入数据类似,如果我们要删除第k个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。
算法的时间复制度也和插入操作一样。
-
警惕数据的访问越界问题
数组越界在C语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。
这种情况下,一般都会出现莫名其妙的逻辑错误,debug的难度非常的大。而且很多计算机病毒也正是利用了代码中数组越界可以访问非法地址的漏洞,来攻击系统,所以写代码的时候一定要警惕数组越界。但并非所有语言都像C一样,把数组越界检查工作丢该程序员来做,像Java本身就会做越界检查。
-
容器能够完全替代数组
对于业务开发,直接使用容器就够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这时候数组就会优于容器,成为首选。
数组为什么是从0开始
-
从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。如果用a来表示数组的首地址,a[0]就是偏移为0的位置,也就是首地址,a[k]就表示偏移k个type_size的位置,所以计算a[k]的内存地址只需要用这个公式
a[i]_address = base_address + k * data_type_size
但如果,数组从1开始计数,那我们计算数组元素a[k]的内存地址就会变为:
a[i]_address = base_address + (k - 1) * data_type_size
对比两个公式,我们不难发现,从1开始编码,每次随机访问数组元素都多了一次减法运算,对于CPU来说,就是多了一次减法指令。
数组为了减少一次减法操作,数组选择了从0开始编号,而不是从1开始。