数组和链表到底应该如何选择?

前言

对于数组和链表这两种最基本的数据结构你应该并不会陌生,尽管他们看起来都非常的简单,但是无论是我在与别人交流或者面试中问到一些相关的问题时,我发现大多数人并没有完全的理解他们两者之间的区别,所以决定写这样一篇文章来对两者进行分析与比对。

数组

先来说说数组,我们都知道数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。

线性表,顾名思义就是一种线性的结构只有前后两个方向,链表实际上也是线性表结构,还有队列、栈这些都是。

随机访问

我们都知道数组可以做到O(1)的随机访问,那原因就在于连续的内存空间和相同的数据类型。假设我们定义一个数组大小为5,并且存储的是int类型的数据,那么在内存中就会为其分配一块连续的空间,假设地址为:10~19(按照int类型占4个字节来计算),那么现在要访问数组中随便一个数据就可以通过这个公式来实现:arr[i] = 10 + i * 4

由此公式便可以说数组可以实现O(1)时间复杂度的随机访问,但要注意的是,这里说的仅仅是随机访问,并不是查找,两者不能混为一谈。

插入

正是因为内存连续性的要求,也导致了数组在插入和删除时相对低效,假设我们要在数组第一个位置插入一个元素,那么就会造成从第一个位置开始,之后的所有位置都需要往后挪一位,所以其时间复杂度为O(n),但如果只是往最后一个位置插入一个元素,那其时间复杂度还是O(1),或者插入元素后不必保证原始数组中的顺序不变,那我们也可以做赋值操作,先把待插入位置上的元素查找出来,并添加到数组最后,然后再用待插入的元素覆盖原位置上的元素即可,这样操作下来其时间复杂度也是O(1)的。

所以从插入来看,如果业务上没有特殊的要求,也是完全可以做到和链表一样的时间复杂度的。

删除

删除和插入一样的问题,如果删除数组末尾的数组,时间复杂度也是O(1)的,但如果删除的是数组中第一个位置的元素,那也需要把之后的每一个元素都往前挪一位,但实际上在某些场景中,删除也可以稍加改动,比如每次删除时并不真正的删除,只有先做一个标记位,待到真正空间不足时,在一次性删除,这样就减少了每次删除都需要移动数据的问题。

链表

说完了数组,我们再来看看链表,前面已经提到过了,链表也是一种线性的数据结构,与数组不同的是,链表并不需要连续的内存空间,链表是通过指针的方式把每一个数据串联起来,对于单向链表来说,就是会额外记录一个后继指针next,而对于双向链表来说,除了后继指针next之外,还有一个前继指针pre。

在链表中我们习惯叫第一个结点为头结点,最后一个结点为尾结点,单向链表只能从头结点开始遍历,而双向链表还可以从尾结点开始遍历。

随机访问

链表中要想随机访问一个元素,就不能像数组一样做到O(1)的时间复杂度了,链表并不要求内存连续,所以没办法通过计算直接找到数据在内存中的位置,对于链表来说只能从头结点或者尾结点开始挨个遍历,因此其时间复杂度是O(n)的。

插入、删除

这里有个误区,每当问到链表和数组的区别时,就有人会说链表的插入、删除快,数组的插入、删除慢,这种说法是不严谨的,链表只有在头结点或者尾节点插入、删除时可以实现O(1)的时间复杂度,如果是在某个指定的位置插入、删除时,就不一样了,链表必须从头或者尾开始挨个遍历,直到找到目标值,而这个查找的过程时间复杂度是O(n)的。

数组和链表对比

通过前面的介绍你已经知道数组和链表对于访问、插入、删除各自的问题了,现在我们可以进行综合比较了

在这里插入图片描述
当然除了正常的时间复杂度对比之外,在实际的业务场景中,可能还需要考虑一些其他的问题。

1、数组虽然简单,但对于内存有连续性的要求,如果一次性申请太大数组空间,可能由于连续性空间不足,导致需要进行额外的内存碎片处理(甚至因为始终找不到一块连续的空间,而提示内存不足),而链表则没有这个问题。
2、数组是一种固定大小的存储结构,一旦申请大小就固定了,如果大小不够,就需要重新申请更大的空间,然后自己把原来的数据拷贝过去,缩小也是同样的道理,但对于链表来说则没有这个问题,链表天然的支持动态扩容与缩容。
3、链表中每个结点都需要额外的记录指向下一个结点的指针(双向链表还需要记录指向前一个结点的指针),所以会额外消耗这部分内存空间,而数组则没有这个方面消耗。
4、由于链表每个结点在内存中没有连续性的要求,所以如果频繁的对链表进行插入、删除则有可能造成内存碎片严重。
5、数组内存连续性的特性,可以充分利用pagecache的预读性,实现更高效的访问。

Guess you like

Origin blog.csdn.net/CSDN_WYL2016/article/details/120298026