数据结构与算法之美 为什么数组从0开始(自我提升第十天)

有了刚才的小插曲(致读者),现在话不多说,直接进入正题,冲冲冲!!!

文章目录

      • 数据结构与算法之美
        • 数组的定义:
        • 好了,了解完了这些,接下来就是了解数组插入、删除的时候了

数据结构与算法之美

基础篇一知识点:

菜鸟一开始看到这个数组为什么从0开始,感觉又是一个值得写一写的东西,结果听了极客时间的课,顿时感觉,果然永远不要把希望交给马扁子,真的是讲数组,结果大部分是讲java里面的ArrayList,瞬间吐血,菜鸟还好学过一点java(菜鸟没有用过ArrayList),但是看我博客的读者可不一定看过java,更别说什么ArrayList了,所以菜鸟这里不会提及ArrayList,只希望带给大家最好的感觉,主要还是最基础的c语言(如果c语言也没学,那就打扰了)。

数组的定义:

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

这里有几个关键词:
1、线性表

顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。(后面这些都是数据结构中的重点,现在知道就好,今后肯定会慢慢讲的)
数据结构与算法之美 为什么数组从0开始(自我提升第十天)_第1张图片

有了线性表,自然就有非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。(这些都是数据结构中的重点,现在知道就好,这里定义自然也不是全面的,今后肯定会慢慢讲的)
数据结构与算法之美 为什么数组从0开始(自我提升第十天)_第2张图片
2、连续的内存空间和相同类型的数据
正是因为这两个限制,它才有了一个堪称“杀手锏”的特性:“ 随机访问 ”(通过下标访问元素,复杂度O(1))。但有利就有弊,这两个限制也让数组的很多操作变得非常低效,比如要想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。

在讲数组删除、插入搬移之前,我们应该先来了解,数组是怎么通过下标随机访问的?

在这个图中,计算机给int数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。
数据结构与算法之美 为什么数组从0开始(自我提升第十天)_第3张图片
我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。(例如:a[0]的地址就是1000,大小为4,因为int型占4个字节)

而通过下标的寻址公式自然就可以写成这样:

a[i]_address(第i个元素所在地址) = base_address(起始地址) + i * data_type_size(每一个内存单元的大小)

从这个式子不难看出,从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。(即相对于第一个数组元素的偏移量)

如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:

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

这里相较于上面,对于计算机执行通过下标寻找数组元素时,无疑就多了一个减法操作,虽然看上去影响不大,但是当你追求很快的速度,且数组很大的时候,这时多的操作的利弊自然就会流露出来了。

这里附上极客时间老师的另一个说法:

不过我认为,上面解释得再多其实都算不上压倒性的证明,说数组起始编号非 0 开始不可。所以我觉得最主要的原因可能是历史原因。
C 语言设计者用 0 开始计数数组下标,之后的 Java、JavaScript 等高级语言都效仿了 C 语言,或者说,为了在一定程度上减少 C 语言程序员学习 Java 的学习成本,因此继续沿用了从 0 开始计数的习惯。实际上,很多语言中数组也并不是从 0 开始计数的,比如
Matlab。甚至还有一些语言支持负数下标,比如 Python。

这里给出一个极客时间老师面试别人时发现的问题,相信对你有所帮助:

这里我要特别纠正一个“ 错误 ”。我在面试的时候,常常会问数组和链表的区别,很多人都回答说,“链表适合插入、删除,时间复杂度 O(1);数组适合查找,查找时间复杂度为O(1)”。
实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不为O(1)。即便是排好序的数组,你用二分查找,时间复杂度也是 O(logn)。所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。

好了,了解完了这些,接下来就是了解数组插入、删除的时候了

1、插入
假设数组的长度为 n,现在,如果我们需要将一个数据插入到数组中的第 k 个位置。为了把第 k 个位置腾出来,给新来的数据,我们需要将第 k~n 这部分的元素都顺序地往后挪一位。那插入操作的时间复杂度是多少呢?你可以自己先试着分析一下。

菜鸟分析:
最好情况时间复杂度:要插入的第k个位置是数组中的最后一个(这里假设数组足够大,且具有一定的顺序要求,不会溢出,因为极客时间老师是用的java的ArrayList,有自动扩大数组功能),那么,可以直接插入不用移动,时间复杂度自然就是O(1)

最坏情况时间复杂度:要插入的第k个位置是数组的第一个,难么数组的数必须全部向后移动一位,所以时间复杂度就是O(n)

平均时间复杂度:因为插入每一个位置的概率相同(1/n,也就是n种情况),所以可以直接变成这样:1 * 1/n+ 2 * 1/n+…n * 1/n=(1+2+3+…+n) / n=O(n),这个不难理解,其中1,2,3…是插入不同位置需要移动的数组元素个数。

这里为读者扩充一下,如果越界会怎么样!!!

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;
}

大家应该看出来,这里的i<=3,但是数组只有2,没有3,所以产生越界,那么打印的就不是三个了,而是很多,具体多少,菜鸟也不知道,可能得占完你的整个电脑。

这里原因就给出极客时间老师的解释吧(我以前搜索过,但是发现好像没收藏博客,反正大致意思就是你越界之后,c语言不会报错,而是随便在内存中寻找地址作为超出的访问地址,然后就会一直去别的地方继续执行了) [有懂的读者,希望能积极交流]

我们知道,在 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。根据我们前面讲的数组寻址公式,a[3] 也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 a[3]=0 就相当于 i=0,所以就会导致代码无限循环。
数组越界在 C 语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。
这种情况下,一般都会出现莫名其妙的逻辑错误,就像我们刚刚举的那个例子,debug 的难度非常的大。而且,很多计算机病毒也正是利用到了代码中的数组越界可以访问非法地址的漏洞,来攻击系统,所以写代码的时候一定要警惕数组越界。

2、删除
这里菜鸟就不分析时间复杂度了,希望读者能自己分析,欢迎下方留言。

跟插入数据类似,如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。

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

好了,这一节算是没有涉及java,也算让菜鸟问心无愧了!!!关于Java的,菜鸟得自己去搞懂咯,再见

你可能感兴趣的:(极客,经验分享,数组的底层,数据结构与算法之美,了解数组插入删除)