《算法(第四版)》1.3 节在介绍背包、队列和栈时,用 Java 介绍了链表。现总结其相关内容如下。
定义:链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该结点含有一个泛型的元素和一个指向另一条链表的引用。
要构造链表,我们首先用一个嵌套类来定义结点(Node):
private class Node { Item item; Node next; }
一个 Node 对象含有两个实例变量,类型分别为 Item(类型参数)和 Node。
现在,根据递归定义,我们只需要一个 Node 类型的变量就能表示一条链表,只要保证它的值是 null 或者指向另一个 Node 对象且该对象的 next 域指向了另一条链表即可。
例如,下面构造一条含有元素 to、be 和 or 的链表:
public class Test<Item> { private class Node { Item item; Node next; } public static void main(String[] args) { Node first = new Node(); Node second = new Node(); Node third = new Node(); first.item = "to"; second.item = "be"; third.item = "or"; first.next = second; second.next = third; } }
上述代码的结果是,third 是一条链表(它是一个结点的引用,该结点指向 null,即一个空链表), second 也是一条链表(它是一个结点的引用,且该结点含有一个指向 third 的引用,而 third 是一条链表), first 也是一条链表(它是一个结点的引用,且该结点含有一个指向 second 的引用,而 second 是一条链表)。
链表表示的是一列元素。我们也可以用一个数组来表示一列元素。不同之处在于,在链表中向序列插入元素或是从序列中删除元素都更方便。
要在首结点为 first 的给定链表开头插入字符串 not,我们先将 first 保存在 oldfirst 中,然后将一个新结点赋予 first,并将它的 item 域设为 not,next 域设为 oldfirst。以上过程如图 1 所示。
删除一条链表的首结点,只需将 first 指向 first. next。此过程如图 2 所示。
如何才能在链表的尾部添加一个新结点?要完成这个任务,我们需要一个指向链表最后一个结点的链接,因为该结点的链接必须被修改并指向一个含有新元素的新结点。
链接表示对结点的引用。
我们不能在链表代码中草率地决定维护一个额外的链接,因为每个修改链表的操作都需要添加检查是否要修改该变量(以及作出相应修改)的代码。例如,我们刚刚讨论过的删除链表首结点的代码就可能改变指向链表的尾结点的引用,因为当链表中只有一个结点时,它既是首结点又是尾结点!另外,这段代码也无法处理链表为空的情况(它会使用空链接)。类似这些情况的细节使链表代码特别难以调试。在链表结尾插入新结点的过程如图 1.3.8 所示。
现在,我们已经展示了在链表中如何通过若干指令实现以下操作,其中我们可以通过 first 链接访问链表的首结点并通过 last 链接访问链表的尾结点:
❏在表头插入结点;
❏从表头删除结点;
❏在表尾插入结点。
其他操作,例如以下几种,就不那么容易实现了:
❏删除指定的结点;
❏在指定结点前插入一个新结点。
例如,我们怎样才能删除链表的尾结点呢?last 链接帮不上忙,因为我们需要将链表尾结点的前一个结点中的链接(它指向的正是 last)值改为 null。在缺少其他信息的情况下,唯一的解决办法就是遍历整条链表并找出指向 last 的结点。这种解决方案并不是我们想要的,因为它所需的时间和链表的长度成正比。
实现任意插入和删除操作的标准解决方案是使用双向链表,其中每个结点都含有两个链接,分别指向不同的方向。
可以用以下循环处理链表的每个结点,其中 first 指向链表的首结点:
for (Node x = first; x ! = null; x = x.next) { // 处理 x.item }