导读:本文主要介绍为什么需要链表、链表的定义、链表用 Java 实现、链表和数组的对比、链表最常见的面试题

我们之前学习数组时讲过,数组和链表是众多高级数据结构实现的基石,哈希表、二叉树等数据结构都是基于数组和链表实现的。之前我们已经学习过了最基础的数据结构数组(数组:最基础的数据结构),今天我们来学习数据结构中的另一块基石:链表。

之前我们学习数组时说过,数组是用一组连续的内存空间存储数据。现在假设内存空间只有零散的 200 KB,我们用数组向操作系统的内存管理器申请 200 KB 的存储空间,虽然还剩余有 200 KB 的内存空间,但是不是连续的,所以内存管理器将不能完成这个指令。

从这里我们可以看到数组的缺点,就是要求内存空间是连续的,不然不能分配空间。数组一开始初始化就要求指定大小,不管用不用那么多空间,内存管理器都会为数组分配指定的大小,数据存储不足,容易造成空间浪费。

链表的出现,正是为了弥补数组的缺点。

链表:不保存在连续存储空间中,每一个元素都保存了到下一个元素地址的数据结构。链表上每一个元素称为节点(Node),第一个元素叫头节点(Head Node),最后一个元素叫尾节点(Tail Node)。

我们将上面的链表的定义,翻译成 Java 代码。

// 链表中的节点

上面的这个是单链表,链表除了这个单链表还包括双链表和循环链表两种。

通过单链表的定义的我们知道,每个节点只能获取到它的下一个节点,如果我们知道了某个节点,需要获取它的上一个节点,我们只能重新从头节点进行遍历。这样的检索效率太过于低下,为了解决这个问题,我们引入了双链表。双链表在单链表的基础上,每个节点保存了上一个节点的地址,我们将双链表的定义翻译成 Java 代码。

public

知道了单链表和双链表后,这里循环列表也可以分成单向循环链表和双向循环链表。单向循环链表是尾节点保存头节点地址,双向循环链表是头节点的前指针保存尾节点的地址,尾节点的后指针保存头节点的地址。

了解完链表后,这里我们用之前学习的数组和链表做一个对比,主要比较数组和链表的空间利用率和二者的时间复杂度。

  • 利用使用率
    数组:数组初始化时便要指定大小,创建后数组大小无法改变。添加元素数组大小不足时,需要重新创建数组,将之前数组的元素复制迁移到新数组中,所以数组的空间利用率等于元素需要的空间大小除以创建出来的数组大小
    链表:链表中的节点都是需要时才会创建出来,但是我们真正需要的其实是每个节点中存储的数据。所以链表的空间利用率等于节点中存储的数据除以整个节点
  • 时间复杂度
    查询:之前介绍数组我们分析过,数组通过索引查询的时间复杂度是 O(1)。而链表是顺序访问,不论从头节点还是尾节点开始访问,都需要一个一个节点的遍历,所以用大 O 表示法(不知道什么是大 O 表示法可以参考我之前的文章代码复杂度分析)链表的查询时间复杂度是 O(n)。
    添加、删除:不同位置进行添加、删除,数组和链表的时间复杂度是不一样的,在之前介绍 Java 容器那篇文章中,已经分析过了为什么不同位置数组和链表添加、删除元素,时间复杂度不一样(Java容器框架学习整理)。这里只说下结论,(假设数组不需要扩容)数组在末尾添加、删除元素的时间复杂度是 O(1),在开头和中间添加、删除元素时间复杂度是 O(n)。双向链表在头尾添加、删除元素,时间复杂度是 O(1),在中间添加、删除元素时间复杂度是 O(n)。

最后我们以链表中一道常见的面试题,反转链表来作为本文的结束。

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 5->4->3->2->1->NULL
输出: 1->2->3->4->5->NULL
限制:0 <= 节点个数 <= 5000

这个题我们用单链表来解决,就使用我们前面自己定义的单链表解决。这题可以使用递归解决,但是不推荐,所以这个题我们使用迭代法解决。我们解决这个面试题的核心思想是,新建一个链表作为反转链表,从头开始遍历需要反转的链表中每个节点,每个节点遍历出来的时候都是反转链表的头节点,然后这个头节点指向之前遍历出来的节点。

public