数组与链表

138 阅读7分钟

简介

各种数据结构,不管是队列,栈等线性数据结构还是树,图的等非线性数据结构,从根本上底层都是数组和链表。不管你用的是数组还是链表,用的都是计算机内存,物理内存是一个个大小相同的内存单元构成的,如图:

image.png (图 1. 物理内存)

而数组和链表虽然用的都是物理内存,都是两者在对物理的使用上是非常不一样的,如图:

(图 2. 数组和链表的物理存储图)

不难看出,数组和链表只是使用物理内存的两种方式。

数组是连续的内存空间,通常每一个单位的大小也是固定的,因此可以按下标随机访问。而链表则不一定连续,因此其查找只能依靠别的方式,一般我们是通过一个叫 next 指针来遍历查找。链表其实就是一个结构体。 比如一个可能的单链表的定义可以是:

interface ListNode<T> {
  data: T;
  next: ListNode;
}

data 是数据域,存放数据,next 是一个指向下一个节点的指针。

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

从上面的物理结构图可以看出数组是一块连续的空间,数组的每一项都是紧密相连的,因此如果要执行插入和删除操作就很麻烦。对数组头部的插入和删除时间复杂度都是[公式],而平均复杂度也是[公式],只有对尾部的插入和删除才是[公式]。简单来说”数组对查询特别友好,对删除和添加不友好“。为了解决这个问题,就有了链表这种数据结构。链表适合在数据需要有一定顺序,但是又需要进行频繁增删除的场景,具体内容参考后面的《链表的基本操作》小节。

(图 3. 一个典型的链表逻辑表示图)

❝ 后面所有的图都是基于逻辑结构,而不是物理结构

链表只有一个后驱节点 next,如果是双向链表还会有一个前驱节点 pre。

❝ 有没有想过为啥只有二叉树,而没有一叉树。实际上链表就是特殊的树,即一叉树。

链表

链表是一种怎么样的结构呢?链表就是一种可以把数据串联起来的结构,每个元素会有指向下一个元素的指针(末尾的没有普通链表),就像现实世界中的火车一样一节一节的串联起来;链表根据自身的指针指向又可以分为:单向链表、双向链表、循环链表;

特点

  • 在内存中可以存在任何地方,不要求连续。 
  • 每一个数据都保存了下一个数据的内存地址,通过这个地址找到下一个数据。 第一个人知道第二个人的座位号,第二个人知道第三个人的座位号……
  • 增加数据和删除数据很容易。 再来个人可以随便坐,比如来了个人要做到第三个位置,那他只需要把自己的位置告诉第二个人,然后问第二个人拿到原来第三个人的位置就行了。其他人都不用动。
  • 查找数据时效率低,因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。 要找到第三个人,必须从第一个人开始问起。
  • 不指定大小,扩展方便。链表大小不用定义,数据随意增删。

基本操作

  1. 插入

插入只需要考虑要插入位置前驱节点和后继节点(双向链表的情况下需要更新后继节点)即可,其他节点不受影响,因此在给定指针的情况下插入的操作时间复杂度为O(1)。这里给定指针中的指针指的是插入位置的前驱节点。

伪代码:

temp = 待插入位置的前驱节点.next
待插入位置的前驱节点.next = 待插入指针
待插入指针.next = temp

如果没有给定指针,我们需要先遍历找到节点,因此最坏情况下时间复杂度为 O(N)

❝ 提示 1: 考虑头尾指针的情况。

❝ 提示 2: 新手推荐先画图,再写代码。等熟练之后,自然就不需要画图了。

  1. 删除

只需要将需要删除的节点的前驱指针的 next 指针修正为其下下个节点即可,注意考虑**「边界条件」**。

伪代码:

待删除位置的前驱节点.next = 待删除位置的前驱节点.next.next

❝ 提示 1: 考虑头尾指针的情况。

❝ 提示 2: 新手推荐先画图,再写代码。等熟练之后,自然就不需要画图了。

  1. 遍历

遍历比较简单,直接上伪代码。

迭代伪代码:

当前指针 =  头指针
while 当前节点不为空 {
   print(当前节点)
   当前指针 = 当前指针.next
}

一个前序遍历的递归的伪代码:

dfs(cur) {
    if 当前节点为空 return
    print(cur.val)
    return dfs(cur.next)
}

优缺点

  1. 数组的优点
  • 插入删除速度快
  • 内存利用率高,不会浪费内存
  • 大小没有固定,拓展很灵活。
  1. 数组的缺点
  • 不能随机查找,必须从第一个开始遍历,查找效率低

数组

特点

  • 在内存中,数组是一块连续的区域。 
  • 数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。 
  • 插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。
  • 随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。
  • 并且不利于扩展,数组定义的空间不够时要重新定义数组。

优缺点

  1. 数组的优点
  • 随机访问性强
  • 查找速度快
  1. 数组的缺点
  • 插入和删除效率低
  • 可能浪费内存
  • 内存空间要求高,必须有足够的连续内存空间。
  • 数组大小固定,不能动态拓展

链表和数组主要区别

  1. 数组的元素个数是固定的,而组成链表的结点个数可按需要增减;
  2. 数组元素的存诸单元在数组定义时分配,链表结点的存储单元在程序执行时动态向系统申请:
  3. 数组中的元素顺序关系由元素在数组中的位置(即下标)确定,链表中的结点顺序关系由结点所包含的指针来体现。
  4. 对于不是固定长度的列表,用可能最大长度的数组来描述,会浪费许多内存空间。
  5. 对于元素的插人、删除操作非常频繁的列表处理场合,用数组表示列表也是不适宜的。若用链表实现,会使程序结构清晰,处理的方法也较为简便。

例如在一个列表中间要插入一个新元素,如用数组表示列表,为完成插入工作,插入处之后的全部元素必须向后移动一个位置空出的位置用于存储新元素。

对于在一个列表中删除一个元素情况,为保持数组中元素相对位置连续递增,删除处之后的元素都得向前移一个位置。如用链表实现列表.链表结点的插人或删除操作不再需要移动结点,只需改变相关的结点中的后继结点指针的值即可,与结点的实际存储位置无关。