1、什么是链表?(摘自LeetCode-小灰的算法之旅)

地下党都是一些什么样的人物呢? 在影视作品中,我们可能都见到过地下工作者的经典话语: “上级的姓名、住址,我知道,下级的姓名、住址,我也知道,但是这些都是我们党的秘密,不能告诉你们!” 地下党借助这种单线联络的方式,灵活隐秘地传递着各种重要信息。 在计算机科学领域里,有一种数据结构也恰恰具备这样的特征,这种数据结构就是链表。 链表是什么样子的?为什么说它像地下党呢? 让我们来看一看单向链表的结构。

链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。 单向链表的每一个节点又包含两部分,一部分是存放数据的变量 data,另一部分是指向下一个节点的指针 next。
private static class Node {int data;Node next;}
链表的第 1 个节点被称为头节点,最后 1 个节点被称为尾节点,尾节点的 next 指针指向空。 与数组按照下标来随机寻找元素不同,对于链表的其中一个节点 A,我们只能根据节点 A 的 next 指针来找到该节点的下一个节点 B,再根据节点 B 的 next 指针找到下一个节点 C…… 这正如地下党的联络方式,一级一级,单线传递。

什么是双向链表? 双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有 data 和 next 指针,还拥有指向前置节点的 prev 指针。

接下来我们看一看链表的存储方式。 如果说数组在内存中的存储方式是顺序存储,那么链表在内存中的存储方式则是 随机存储。 什么叫随机存储呢? 上一节我们讲解了数组的内存分配方式,数组在内存中占用了连续完整的存储空间。而链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠 next 指针关联起来。这样可以灵活有效地利用零散的碎片空间。 让我们看一看下面两张图,对比一下数组和链表在内存中分配方式的不同。

图中的箭头代表链表节点的 next 指针。

2、链表的基本操作(摘自LeetCode-小灰的算法之旅)
2.1、查找节点
在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。 例如给出一个链表,需要查找从头节点开始的第 3 个节点。

第 1 步,将查找的指针定位到头节点。
第 2 步,根据头节点的 next 指针,定位到第 2 个节点。
第 3 步,根据第 2 个节点的 next 指针,定位到第 3 个节点,查找完毕。

2.2、更新节点
如果不考虑查找节点的过程,链表的更新过程会像数组那样简单,直接把旧数据替换成新数据即可。
2.3、插入节点
与数组类似,链表插入节点时,同样分为 3 种情况。
- 尾部插入
- 中间插入
- 头部插入
尾部插入,是最简单的情况,把最后一个节点的 next 指针指向新插入的节点即可。
头部插入,可以分成两个步骤。
第 1 步,把新节点的 next 指针指向原先的头节点。
第 2 步,把新节点变为链表的头节点。
中间插入,同样分为两个步骤。
第 1 步,新节点的 next 指针,指向插入位置的节点。
第 2 步,插入位置前置节点的 next 指针,指向新节点。
只要内存空间允许,能够插入链表的元素是无穷无尽的,不需要像数组那样考虑扩容的问题。
2.4、删除元素
链表的删除操作同样分为 3 种情况。
- 尾部删除
- 头部删除
- 中间删除
尾部删除,是最简单的情况,把倒数第 2 个节点的 next 指针指向空即可。
头部删除,也很简单,把链表的头节点设为原先头节点的 next 指针即可。
中间删除,同样很简单,把要删除节点的前置节点的 next 指针,指向要删除元素的下一个节点即可。
这里需要注意的是,许多高级语言,如 Java,拥有自动化的垃圾回收机制,所以我们不用刻意去释放被删除的节点,只要没有外部引用指向它们,被删除的节点会被自动回收。

//头节点指针private Node head;//尾节点指针private Node last;//链表实际长度private int size;/*** 链表插入元素* @param data 插入元素* @param index 插入位置*/public void insert(int data, int index) throws Exception {if (index<0 || index>size) {throw new IndexOutOfBoundsException("超出链表节点范围!");}Node insertedNode = new Node(data);if(size == 0){//空链表head = insertedNode;last = insertedNode;} else if(index == 0){//插入头部insertedNode.next = head;head = insertedNode;}else if(size == index){//插入尾部last.next = insertedNode;last = insertedNode;}else {//插入中间Node prevNode = get(index-1);insertedNode.next = prevNode.next;prevNode.next = insertedNode;}size++;}/*** 链表删除元素* @param index 删除的位置*/public Node remove(int index) throws Exception {if (index<0 || index>=size) {throw new IndexOutOfBoundsException("超出链表节点范围!");}Node removedNode = null;if(index == 0){//删除头节点removedNode = head;head = head.next;}else if(index == size-1){//删除尾节点Node prevNode = get(index-1);removedNode = prevNode.next;prevNode.next = null;last = prevNode;}else {//删除中间节点Node prevNode = get(index-1);Node nextNode = prevNode.next.next;removedNode = prevNode.next;prevNode.next = nextNode;}size--;return removedNode;}/*** 链表查找元素* @param index 查找的位置*/public Node get(int index) throws Exception {if (index<0 || index>=size) {throw new IndexOutOfBoundsException("超出链表节点范围!");}Node temp = head;for(int i=0; i<index; i++){temp = temp.next;}return temp;}/*** 输出链表*/public void output(){Node temp = head;while (temp!=null) {System.out.println(temp.data);temp = temp.next;}}/*** 链表节点*/private static class Node {int data;Node next;Node(int data) {this.data = data;}}public static void main(String[] args) throws Exception {MyLinkedList myLinkedList = new MyLinkedList();myLinkedList.insert(3,0);myLinkedList.insert(7,1);myLinkedList.insert(9,2);myLinkedList.insert(5,3);myLinkedList.insert(6,1);myLinkedList.remove(0);myLinkedList.output();}
以上是对单链表相关操作的代码实现。为了尾部插入的方便,代码中额外增加了指向链表尾节点的指针 last。
3、数组vs链表

