数组,可以说是我们最常用的数据结构。
今天带大家来学习一下关于数组相关的知识点。
文章大纲:
- 线性表和非线性表?
- 数组的插入和删除为什么这么low?
- 数组和链表有什么区别?
- 数组查询为什么比链表快?
- 数组如何实现下标随机访问元素的?
- 数组为什么从0开始编号?
- 面试题:对角线遍历
公众号:Kuls
关注可领取Java学习资料
线性表和非线性表
想要了解数组,必然先要搞清楚它的结构是怎样的。
在数据结构中,我们常分为线性表和非线性结构
我们首先来看看,相应的数据结构有哪些?
线性表:
- 数组
- 链表
- 队列
- 栈
- …
非线性结构:
- 二叉树
- 堆
- 图
- …
上面列举了一些常见的数据结构,知道上面的数据结构,我相信大家已经知道线性表是什么好意思了,当然这里我也给大家简单介绍一下相关概念。
线性表: 由n个数据元素,组成的有限序列。如a[0]、a[1]、a[2]…a[n-1]。
公众号:Kuls
关注可领取Java学习资料
数组的插入和删除为什么这么low?
这个题应该是面试常问的题目了。
其实非常好理解,我们应该都知道数组在内存空间中是一段连续的空间,但是链表是通过指针来指向下一个内存区域。
对于插入
对于数组来说,如果我要在n个元素之间插入一个元素(假设这个位置是x),那么为了保持这个连续的空间,我需要把n-x个元素都往后移动一位。此时的时间复杂度为O(n)。
当然,也会存在一种情况,那就是我们将数据插入在数组的最后,那么也就不需要移动任何的数据,只需要保证数组的空间是足够的即可。此时的时间复杂度就为O(1)。
大家也可以通过看图来理解
对于删除
通过插入的案例,删除的思路其实也就出来了。
这里我也给大家画了一张图来理解。
公众号:Kuls
关注可领取Java学习资料
删除其实也有两种情况,一种删除数组中间的元素,时间复杂度为O(n);一种删除数组中最后一个元素,时间复杂度O(1)。
其实对于删除,我们可以进一步优化,对于要删除的元素,我们并不真正的删除它,而是给它一个标记,例如给它标记一个不会出现在数组中的值。
标记有什么用呢? 标记之后,我们不会真正对它进行删除,而是等数组的空间满了后,再对标记的元素进行删除,这样会提升一定的效率。
公众号:Kuls
关注可领取Java学习资料
数组和链表有什么区别?
- 物理存储分布上有差异,数组占用的是一段连续的内存空间。链表是分散的。
- 数组的大小是固定的,如果需要扩容,那必须得新建一个更大的数组,然后拷贝过去。链表的大小是不固定的。
- 内存空间占用。因为数组的大小是初始化的时候就定义的,所以它可能会存在一些没有存储元素的空间;链表因为要指向下一个结点,所以它需要花费一定的空间来存储前驱或者后继结点指针。
- 访问元素的时间复杂度不一样,数组因为在内存中是连续的,根据局部性原理,支持随机访问,时间复杂度为O(1)。但是对于链表来说,就不存在这种情况,只能通过指针一个一个遍历比较,时间复杂度为O(n)。
数组查询为什么比链表快?
其实在本文的前面就已经回答了这个题目的答案。
没错,一个最重要的原因就是局部性原理。对于数组是一串连续的内存空间存储,每次CPU缓存会读取一段连续的内存空间,恰好数组完全符合条件,但是链表就不行了,链表是通过指针指向的,是散列的。
缓存的速度显然比内存中的速度快的多。
除此之外,链表的寻址操作其实也会消耗一定量的时间,这也导致了它的速度比数组更慢。
公众号:Kuls
关注可领取Java学习资料
数组如何实现下标随机访问元素的?
首先,我们已经明确了数组是一串连续的内存空间,并且在内存空间中我们都是通过内存地址来指向一块区域的,下图是给大家画的一个简单的数组内存空间图:
通过下标进行随机访问,我们已知内存空间是连续的且有内存地址的,显然计算机可以通过一个计算公式得到相应要查询的下标的内存地址。
a[i]_add = base_add + i * data_size
显然,我们可以通过上述公式得到该数组的任意下标数据。
因此,我们通过数组下标随机访问数组的某元素时,时间复杂度为O(1)。
公众号:Kuls
关注可领取Java学习资料
数组为什么从0开始编号?
其实这个问题不是我提出的,我是在极客的一个专栏上看到的。
个人觉得挺有意思的,问题其实不难,你知道了上面的数组如何实现下标随机访问元素的?
的公式后,应该更简单了。
这是我们上面问题中的寻址公式。
a[i]_add = base_add + i * data_size
如果我们下标是从1开始,那么这个公式应该变成:
a[i]_add = base_add + (i-1) * data_size
公众号:Kuls
关注可领取Java学习资料
发现了什么吗?
你可能觉得就是多了个减一操作啊~
是的,多了这个减一操作,但是其实对于cpu来说这个影响确实不是很大,但是也是原因之一。
为什么多了减一操作也是原因之一呢?
在编程语言刚开始发展的时候,那时候的电脑可以说是内存极小,工程师想尽办法优化任何的cpu的浪费,如果多了这么一个减一会让CPU多算一次,那肯定不允许。
其实可以简单总结如下:
- 在计算资源缺乏的过去,0标号的写法可以节省编译时间
- 现代语言中0标号可以更优雅的表示数组字串
- 在支持指针的语言中,标号被视作是偏移量,因此从0开始更符合逻辑
当然如果你想要看其中的一些故事,可以在底部的链接中查阅。
公众号:Kuls
关注可领取Java学习资料