链表(Linked list) 是一种 动态 的线性数据结构,但并不是按线性的顺序存储数据。链表通常由一连串结点(Node)组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的引用(prev或next)。
链表的优点是不需要处理固定容量的问题,但是链表失去了数组随机读取的优点。
1. 链表
链表是一种天然带有递归性质的线性结构,在链表中数据存储在结点中,结点中同时存储了指向下一个结点的引用(这里只介绍单向链表)。
链表类中的操作:
方法 | 描述 |
---|---|
int getSize() | 获取链表中元素个数 |
boolean isEmpty() | 返回链表是否为空 |
void add(int index, E e) | 在链表的 index 位置添加新元素 e |
void addFirst(E e) | 在链表头部添加新元素 e |
addLast(E e) | 在链表末尾添加新元素 e |
E get(int index) | 获得链表的第 index 个位置的元素 |
E getFirst() | 获得链表的第一个元素 |
E getLast() | 获得链表的最后一个元素 |
E set(int index, E e) | 修改链表的第 index 个位置的元素为 e,返回该位置之前的元素 |
boolean contains(E e) | 查找链表中是否存在元素 e |
E remove(int index) | 从链表中删除第 index 个位置的元素,返回被删除的元素 |
E removeFirst() | 从链表中删除第一个元素,返回被删除的元素 |
E removeLast() | 从链表中删除最后一个元素,返回被删除的元素 |
void removeElement(E e) | 从链表中删除元素e |
1 | // 结点类 |

1 | public class LinkedList<E> { |
2. 向链表中添加元素
方法 | 描述 |
---|---|
int getSize() | 获取链表中元素个数 |
boolean isEmpty() | 返回链表是否为空 |
void add(int index, E e) | 在链表的 index 位置添加新元素 e |
void addFirst(E e) | 在链表头部添加新元素 e |
addLast(E e) | 在链表末尾添加新元素 e |
先将链表类的几个基本属性和方法声明出来。
1 | public class LinkedList<E> { |

在链表头部添加元素,只需将之前整个链表的 head
赋给新插入结点的 next
,同时维护 head
和 size
。
1 | // 在链表头添加新的元素 e |

根据图示可以看出,在某个位置添加元素最重要的是要找到前一个结点,设置一个结点引用 prev
,通过移动 prev
找到待插入的位置的前一个位置,之后进行 node.next = prev.next
和 prev.next = node
两步操作就可以完成在某个位置添加元素的操作,最后维护 size
。注意此时 prev
移动的次数,如果放在 3 这个位置,那么 prev
移动 2 次,即当 prev == 2
时就停止,所以循环条件是 i < index - 1
。
但是,如果 index == 0
,即在链表头添加元素,由于 head
没有前一个结点,所以这种情况下需要单独处理。
切记两个操作不开颠倒顺序,否则,当执行完 prev.next = node
时,prev.next
已经指向了 node
,再执行 node.next = prev.next
就出现了逻辑上的错误。
需要说明的是,在链表中一个位置添加元素是不常用的操作,其实在链表中没有索引这个概念,这里只是借助这个概念。
1 | // 在链表的 index 位置添加新元素 e |
在链表末尾添加元素就可以直接复用 add(int index, E e)
方法,将 index
设置为 size
即可。
1 | // 在链表末尾添加新的元素e |
3. 虚拟头结点
之前在向链表中添加元素时,如果是在头部添加元素,由于头结点没有前结点,因此需要单独考虑。为了将对链表头的操作与其他操作统一起来,设立一个虚拟头结点(dummyHead),虚拟头结点不存储元素,只有一个指向 head
的引用。

有了虚拟头结点,便可以将添加操作统一起来。新增 dummyHead
属性,初始化时将其建立起来,但元素值和 next 引用均为空。
1 | public class LinkedList<E> { |
有了虚拟头结点后,从链表头部添加元素就不需要再单独考虑,因为 dummyHead
中 next
就指向链表第一个元素。注意,增加了虚拟头结点后,只需遍历 index 次,所以循环继续进行的条件是 i < index
。
1 | // 在链表的 index 位置添加新元素 e |
此时向链表头部添加元素就可以复用 add
方法。
1 | // 在链表头部添加新元素 e |
4. 链表的遍历、查询和修改
方法 | 描述 |
---|---|
E get(int index) | 获得链表的第 index 个位置的元素 |
E getFirst() | 获得链表的第一个元素 |
E getLast() | 获得链表的最后一个元素 |
E set(int index, E e) | 修改链表的第 index 个位置的元素为 e,返回该位置之前的元素 |
boolean contains(E e) | 查找链表中是否存在元素 e |
在链表中是不常使用索引的,因此 get
方法也不经常使用,更多是用来熟悉链表的遍历操作。
查询操作是需要找到 index 位置的结点,而添加操作是要找到 index 的前一个结点,因此对于查询操作,从链表真正的第一个结点开始遍历,由于设置了虚拟头结点,因此,遍历操作是从 dummyHead.next
进行的。
1 | // 获得链表的第 index 个位置的元素 |
获取链表中第一个元素和最后一个元素就可以直接复用 get
方法。
1 | // 获得链表的第一个元素 |
1 | // 获得链表的最后一个元素 |
同样,修改元素也和查询元素一样需要遍历,只不过是把 index 位置的结点中数据域进行修改,最后维护 size
。
1 | // 修改链表的第 index 个位置的元素为 e,返回该位置之前的元素 |
设置一个查找的方法 contains(E e)
,查询链表中是否包含元素 e。由于此时不知道具体索引位置,因此需要遍历整个链表,需要不断进行,直到到达链表的最后。
1 | public boolean contains(E e) { |
为了方便输出,重写 toString()
方法。
1 |
|
5. 从链表中删除元素
方法 | 描述 |
---|---|
E remove(int index) | 从链表中删除第 index 个位置的元素,返回被删除的元素 |
E removeFirst() | 从链表中删除第一个元素,返回被删除的元素 |
E removeLast() | 从链表中删除最后一个元素,返回被删除的元素 |
void removeElement(E e) | 从链表中删除元素e |
想要实现在链表 index 位置删除元素,和添加元素一样,先找到待删除元素的前一个结点 prev
,然后将待删除结点用临时结点 retNode
暂存起来,然后跳过待删除结点,直接将待删除结点的下一个结点赋给 prev
的 next
引用。为了避免对象游离,将待删除结点 retNode
的 next
引用设置为空。最后将 retNode
结点的数据返回并维护 size
。

1 | // 从链表中删除第 index 个位置的元素,返回被删除的元素 |
有了 remove
方法,从链表中删除第一个元素和最后一个元素就可以直接复用 remove
方法。
1 | // 从链表中删除第一个元素,返回被删除的元素 |
1 | // 从链表中删除最后一个元素,返回被删除的元素 |
要想从链表中删除元素,最重要的是要先找到它,找到之后,就和删除某个位置的元素一样操作就可以了。寻找的过程还是链表遍历的过程,删除元素是要找到待删除元素之前的结点 prev
,因此遍历链表时从 dummyHead
开始,同时,遍历结束的条件是 prev.next == null
。
1 | // 从链表中删除元素 e |
6. 链表操作的时间复杂度
添加操作
方法 | 时间复杂度 |
---|---|
add(int index, E e) | O(n/2) = O(n) |
addFirst(E e) | O(1) |
addLast(E e) | O(n) |
由于在末尾添加元素需要遍历整个链表,所以时间复杂度为 O(n)
;而在链表头部添加元素,是 O(1)
的时间复杂度;以上两个操作的时间复杂度是和数组相反的。当在链表任一位置插入元素是,平均来看,是往链表的中间添加元素,时间复杂度为 O(n/2)
,总体也是 O(n)
级别。
删除操作
方法 | 时间复杂度 |
---|---|
remove(int index) | O(n/2) = O(n) |
removeFirst() | O(1) |
removeLast() | O(n) |
removeElement(E e) | O(n/2) = O(n) |
如果删除最后一个元素,需要从头遍历整个链表,时间复杂度为 O(n)
;删除第一个元素,只需 O(1)
的时间复杂度;这两个操作的时间复杂度和数组也是相反的。平均来看,remove(int index)
和 removeElement(E e)
的时间复杂度为 O(n)
。
修改、查找操作
方法 | 时间复杂度 |
---|---|
set(int index, E e) | O(n) |
get(int index) | O(n) |
getFirst() | O(1) |
getLast() | O(n) |
contains(E e) | O(n) |
链表的修改和查找操作时间复杂度也是 O(n)
。
总的来说,如果只对链表的头部进行操作,或者只对链表头部进行查找,时间复杂度是 O(1)
,
链表整体实现: LikedList.java
7. 使用链表实现栈
栈的接口:
1 | public interface Stack<E> { |
由于在链表头操作的时间复杂度为 O(1)
,因此栈的 pop
和 push
都应在链表头进行,也就是说,链表的头部是栈顶,尾部是栈底。
1 | // 用链表实现栈 |
8. 使用链表实现队列
对于队列,要在一端添加元素,另一端删除元素,但是对于链表来说势必会有一个 O(n)
的操作,其实之前用数组实现队列的时候也有同样的问题,不过用循环队列改进后,性能就提升了。因此用链表实现队列,也进行改进。
由于有 head
变量,所以之前对于链表头部的操作都是简单的,因此,可以考虑同时在链表尾部设置 tail
变量,指向最后一个结点,在对链表操作的时候,同时维护 tail
。
添加了 tail
之后,想要在尾部添加元素就变得很方便,相当于在 tail
后面添加一个结点,这是,从链表的头部和尾部添加元素都是容易的
添加了 tail
之后,要想在尾部删除元素,就是说要找到 tail
前一个结点,但找到前一个结点又要进行遍历,所以从尾部删除元素是不容易的。
总的来说,添加 tail
变量之后,在链表尾部添加元素变得方便,而在头部添加或删除都是方便的,因此,选择在链表头部删除元素,在链表尾部添加元素,即:将链表头作为队首,链表尾部作为队尾。

先将简单的操作进行实现。
1 | public class LinkedListQueue<E> implements Queue<E> { |
进行入队操作,如果队列不为空,则直接将元素添加到最后即可,同时维护 tail
和 size
。当队列为空时,head
和 tail
均指向 null
,此时向空队中添加一个元素时,直接将 tail
指向入队的那个结点,同时维护 head
和 size
。
1 |
|
正常出队的时候,只需按照链表中删除第一个元素,维护 size
即可。但是要注意,当队列中只有一个元素,head.next
为空,经过 head = head.next
操作后,队列已经为空,此时 head == null
,因此还需要额外维护下 tail
。
1 |
|
查看队首元素只需将 head
数据域中的元素返回即可。
1 |
|
最后重写 toString()
方法。
1 |
|
用链表实现栈整体实现:LinkedListQueue.java