链表是一种线性数据结构,其中每一个节点元素都是一个单独的对象,链表中的结点通过每个元素的引用字段连接起来。
在求解链表类型题目时,我们可以使用 dummy 虚拟头结点连接到链表头部,更方便操作。
在操作链表时,也可以尝试使用前驱结点、后继结点来进行链表的连接、断开、插入等操作。
通过以下 16 道算法题,理解数据结构链表吧。
206. 反转链表 - 简单
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
题解:
需要注意链表指针的操作,可以使用迭代法或者递归法求解。
递归法:
递归反转传入的链表头结点
head以及前驱结点prev,初始时,prev为空如果
head为空,那么直接返回前驱结点保存头结点的下一个结点
next的指针令
head连接到且前驱结点prev上,实现反转递归,此时链表头结点为
next,前驱结点为head迭代法:
- 使用一个虚拟头结点
dummy作为反转后的链表的头部- 遍历链表
- 声明一个临时结点
s指向当前遍历的结点p- 当前遍历结点
p移动到下一个结点s称为p结点的前驱结点,令s的下一个结点指向虚拟头结点的下一个结点- 令虚拟头结点的下一个结点指向
s,实现反转
代码:
// 递归法
public ListNode reverseList(ListNode head){
return reverseList(head, null);
}
private ListNode reverseList(ListNode head, ListNode prev) {
if (head == null) {
return prev;
}
ListNode next = head.next;
head.next = prev;
return reverseList(next, head);
}
// 迭代法
public ListNode reverseList(ListNode head){
ListNode dummy = new ListNode(), p = head;
while (p != null) {
ListNode s = p;
p = p.next;
s.next = dummy.next;
dummy.next = s;
}
return dummy.next;
}
92. 反转链表 II - 中等
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
题解:
拿到链表之后,我们先遍历链表,直到遍历到待翻转部分链表的前一个结点,即前驱结点
pre随后使用一个指针
start指向待翻转部分链表的头部此后继续遍历链表,直到遍历到待翻转部分链表的最后一个结点,即
end再使用一个指针指向待翻转部分链表的后一个结点,即后继结点
succ随后翻转链表,原先的头结点
start变成尾结点,尾结点end变成头结点。拼接链表,让前驱结点拼接到end,start拼接上succ
代码:
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0, head);
ListNode pre = dummy;
// 让 pre 指向待翻转部分链表的前驱结点
for (int i = 1; i < left; i++) {
pre = pre.next;
}
// start 指向待翻转部分链表的头部,end 指向待翻转部分链表的尾部
ListNode start = pre.next;
ListNode end = start;
for (int i = 1; i < right-left+1; i++) {
end = end.next;
}
// 让 succ 执行待翻转部分链表的后继结点
ListNode succ = end.next;
// 切断链表
end.next = null;
// 翻转链表,随后拼接链表
pre.next = reverse(start);
start.next = succ;
return dummy.next;
}
private ListNode reverse(ListNode head) {
ListNode dummy = new ListNode();
while (head != null) {
ListNode next = head;
head = head.next;
next.next = dummy.next;
dummy.next = next;
}
return dummy.next;
}
}
21. 合并两个有序链表 - 简单
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
题解:
声明一个虚拟头结点
dummy指向合并后的指针的头部,并维护一个指针p总是指向合并链表的尾部。比较两个链表的当前所指向结点的值,令指针
p指向值较小的结点,并且值较小的节点的指针移动到下一个结点。
代码:
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(), p = dummy;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
p.next = list1;
list1 = list1.next;
}else {
p.next = list2;
list2 = list2.next;
}
p = p.next;
}
p.next = list1 == null ? list2 : list1;
return dummy.next;
}
23. 合并K个升序链表 - 困难
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下: [1->4->5,1->3->4,2->6] 将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
题解:
使用分治法合并链表数组中的链表。
将数组分割成两个子集,然后各自合并子集中的链表,最后只会得到两个链表,合并这两个链表。
- 对半分割数组,得到两个新的数组,继续分割这两个数组
- 分割到最后,子数组只会有一个元素,即只有单个链表,合并相邻的两个子数组,即合并两个链表,得到一个合并后的链表
- 其余各个子数组也合并完成,两两合并这些子链表,最终得到一个升序的链表
代码:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int l, int r) {
// 只有单个元素,返回这个链表
if (l == r) {
return lists[l];
}
if (l > r) {
return null;
}
// 分割子集
int mid = (r - l) / 2 + l;
// 合并两个链表,递归分割子集,最终分割到只有一个元素,再结束递归,逐渐合并成单条链表
return mergeTowList(merge(lists, l, mid), merge(lists, mid + 1, r));
}
// 合并两个升序链表
private ListNode mergeTowList(ListNode a, ListNode b) {
ListNode dummy = new ListNode();
ListNode p = dummy;
while (a != null && b != null) {
if (a.val <= b.val) {
p.next = a;
a = a.next;
} else {
p.next = b;
b = b.next;
}
p = p.next;
}
p.next = a == null ? b : a;
return dummy.next;
}
}
24. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
题解:
创建一个虚拟头结点
dummy,连接到链表头部。再创建一个临时结点
temp,初始指向dummy。我们交换链表中的相邻两个结点的条件是,
temp当前指向的结点后面要有两个结点,即temp.next != null && temp.next.next != null如果后面有两个结点,那么就交换接下来的两个结点
n1和n2:temp.next = n2 n1.next = n2.next; n2.next = n1;此后令 temp 指向
n1,继续执行以上步骤
代码:
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(-1, head);
ListNode temp = dummy;
while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummy.next;
}
25. K 个一组翻转链表 - 困难
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
题解:
把链表分为三部分:
已翻转 -> 待翻转 -> 未翻转
- 声明一个虚拟头结点
dummy,连接链表head- 声明两个辅助指针,待翻转部分的前驱结点
pre,待翻转部分的末尾end,初始时pre和end都指向dummy- 循环 k 次,确定待翻转部分的尾部节点,即
end- 如果
end指向空,那么即待翻转部分不够 k 个,直接让已翻转部分拼接待翻转部分即可- 否则,声明一个辅助结点
next,指向未翻转部分的头部- 声明一个辅助结点
start,指向待翻转部分的头部- 翻转待翻转部分链表,让前驱结点的
next指向翻转后的链表的头部;此前未翻转时的头部结点已经成为尾部,可以作为未翻转部分的前驱结点
代码:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy, end = dummy;
while (end.next != null) {
for (int i = 0; i < k && end != null; i++) {
end = end.next;
}
if (end == null) {
break;
}
ListNode start = pre.next;
ListNode next = end.next;
end.next = null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = start;
}
return dummy.next;
}
private ListNode reverse(ListNode head) {
ListNode dummy = new ListNode(-1, head);
while (head != null) {
ListNode s = head;
head = head.next;
s.next = dummy.next;
dummy.next = s;
}
return dummy.next;
}
}
160. 相交链表 - 简单
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;
在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。
换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
题解:
对于如下链表
绿色部分长度为 a,红色部分长度为 b,蓝色相交部分长度为 c
维护两个指针 n1, n2 分别指向链表 A 和 B,遍历链表,当其中一个指针为 null 时,将其指向另外一条链表,继续遍历,如果指针遍历到的结点相同,即为交点。若同时指向 null,说明没有交点。
证明,n1 第一遍历到 null 时,走过的路程为 a+c; 同理,n2 走过的路程为 b+c
重新指向另外一条链表后,有交点的情况下,直到两个结点指向相同时,n1 走过的路程为 b; n2 走过的路程为 a。总共走过的路程为 a+c+b = b+c+a,因此指向的点可以证明为交点。
在没有交点的情况下,两者走过的总路程为 a+b = b+a,最后都同时指向了 null
代码:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode n1 = headA, n2 = headB;
while (n1 != n2) {
n1 = n1 == null ? headB : n1.next;
n2 = n2 == null ? headA : n2.next;
}
return n1;
}
}
234. 回文链表 - 简单
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例:
输入:head = [1,2,2,1]
输出:true
题解:
利用快慢指针确定链表的中点,然后从中点处反转链表,得到链表
reverse。随后逐个比较
head链表和reverse链表的值,如果发现不相等,那么不是回文链表,返回false;如果能够遍历完,那么是回文链表,返回true
代码:
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
fast = reverse(slow);
slow = head;
while (fast != null) {
if (slow.val != fast.val) {
return false;
}
slow = slow.next;
fast = fast.next;
}
return true;
}
private ListNode reverse(ListNode head) {
ListNode dummy = new ListNode(-1);
while (head != null) {
ListNode s = head;
head = head.next;
s.next = dummy.next;
dummy.next = s;
}
return dummy.next;
}
}
83. 删除排序链表中的重复元素 - 简单
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例:
输入:head = [1,1,2]
输出:[1,2]
题解:
- 使用一个指针
cur维护当前遍历的链表结点,初始指向head- 循环遍历链表,遍历条件为
cur != null && cur.next != null- 如果当前结点
cur与其下一个结点的值相等,那么令cur指向其下一个的下一个结点- 如果值不相等,那么令
cur指向下一个结点
代码:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return null;
}
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
}
82. 删除排序链表中的重复元素 II - 中等
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
题解:
- 有可能头结点就是重复的元素,为了方便删除节点,声明一个虚拟头结点
dummy连接头结点- 声明一个指针
cur表示当前遍历结点- 如果当前遍历结点的下两个元素相等,即
cur.next.val == cur.next.next.val,那么不断的删除这些重复的结点:
- 使用一个变量
x保存重复结点的值,即x = cur.next.val- 如果当前遍历结点
cur的下一个结点的值等于x,即cur.next.val == x,那么删除掉这个节点,即令cur.next = cur.next.next- 不断重复过程
3.2,需要注意可能cur.next可能为空- 如果当前遍历结点的下两个元素不相等,那么让
cur指向下一个结点
代码:
public ListNode deleteDuplicates(ListNode head) {
// 虚节点连接 head
ListNode dummy = new ListNode(-1, head);
ListNode cur = dummy;
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
int x = cur.next.val;
while (cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
}
328. 奇偶链表 - 中等
给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。
第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。
请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。
你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。
示例:
输入: head = [1,2,3,4,5]
输出: [1,3,5,2,4]
题解:
声明三个指针:
odd负责遍历链表中的奇数结点even负责遍历链表中的偶数结点evenHead指向第一个偶数结点,方便后续“奇数链表”连接“偶数链表”开始连接“奇数链表”和“偶数链表”
- 循环遍历链表,遍历条件是,偶数结点不为空并且其下一个结点也非空,这样才有必要让奇数结点连接下一个奇数结点。
- 将奇数结点连接下一个奇数结点,即
odd.next = even.next- 令奇数结点指向下一个结点,即
odd = odd.next- 令偶数结点连接下一个偶数结点,即
even.next = odd.next- 令偶数结点指向下一个结点,即
even = even.next- 循环结束后,即“奇数链表”和“偶数链表”生成完毕,连接这两个链表
- 返回链表
代码:
public ListNode oddEvenList(ListNode head) {
if (head == null) {
return null;
}
ListNode odd = head, evenHead = head.next, even = evenHead;
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
725. 分隔链表 - 中等
给你一个头结点为 head 的单链表和一个整数 k ,请你设计一个算法将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 1 。这可能会导致有些部分为 null 。
这 k 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在后面的长度。
返回一个由上述 k 部分组成的数组。
示例:
输入:head = [1,2,3], k = 5
输出:[[1],[2],[3],[],[]]
解释: 第一个元素 output[0] 为 output[0].val = 1 ,output[0].next = null 。 最后一个元素 output[4] 为 null ,但它作为 ListNode 的字符串表示是 [] 。
题解:
- 首先确定链表的长度 n
- 使用一个
ListNode[] res数组存放结果- 随后确定分割后的每一部分的长度,每一部分的长度至少为 n/k,前面的 n%k 部分长度加一,即 n/k+1
- 开始遍历链表,分割链表,分割 k 次,使用指针
cur遍历链表- 当前分割部分的链表长度
partSize为n/k,如果是前n%k部分,那么其长度加一res对应位置存放当前分割部分链表的头部- 遍历链表到
partSize处,断开与后面结点的连接,同时令cur指向下一个结点,继续分割
代码:
public ListNode[] splitListToParts(ListNode head, int k) {
// 结果集
ListNode[] res = new ListNode[k];
// 确定链表长度
int n = 0;
ListNode cur = head;
while (cur != null) {
n++;
cur = cur.next;
}
// 确定每个分割链表的长度
int quotient = n / k, remainder = n % k;
int idx = 0;
cur = head;
for (int i = 0; i < k && cur != null; i++) {
int partSize = quotient + (idx < remainder ? 1 : 0);
res[idx++] = cur;
for (int j = 1; j < partSize; j++) {
cur = cur.next;
}
// 切断链表
ListNode next = cur.next;
cur.next = null;
cur = next;
}
return res;
}
61. 旋转链表 - 中等
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例:
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
题解:
- 将链表转换为循环链表,同时确定原链表的长度
len- 旋转链表,每个结点向右移动 k 个位置,因此旋转后,新的头结点
head为原链表的倒数第len-(k%len)个结点- 确定了新的头结点后,断开循环链表,已知链表的长度,从头结点移动
len-1次,到达链表尾部,断开循环链表
代码:
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) {
return head;
}
int len = 0;
ListNode cur = head;
// 变成循环链表
while (cur.next != null) {
len++;
cur = cur.next;
}
len++;
cur.next = head;
cur = head;
// 头结点变成倒数第 K 个
for (int i = 0; i < len - (k % len); i++) {
cur = cur.next;
}
head = cur;
// 移动长度 len,断开链表
for (int i = 1; i < len; i++) {
cur = cur.next;
}
cur.next = null;
return head;
}
19. 删除链表的倒数第 N 个结点 - 中等
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
题解:
使用快慢指针,我们定义两个指针
first、second。first总是领先secondn 个结点,因此当first遍历到链表末尾null时,second刚好遍历到链表倒数第 n 个结点。我们使用一个虚拟头结点
dummy连接到链表上,让second从dummy处开始遍历,那么当first遍历到末尾时,second刚好遍历到倒数第n-1个结点,更方便修改链表。
代码:
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1, head);
ListNode first = head, second = dummy;
for (int i = 0; i < n; i++) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
return dummy.next;
}
148. 排序链表 - 中等
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表
示例:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
题解:
归并排序。
使用快慢指针确定链表的中点,随后以中点为界,继续分割前半部分和后半部分,如此往复,直到分割到只剩下一个结点,将相邻两个结点合并,层层往上,最后合并为一个有序链表。
具体思路为:
- 我们拿到一个链表,确定它的头部
head和尾部tail,初始时tail 为 null,是一个完整的链表,此后分割的过程中,tail变成链表中的某个结点- 如果
head是一个空节点,直接返回,不用排序- 如果
head的下一个结点是tail,那么断开他们的连接,并返回head。此时链表已经分割到只剩下单个节点,可以开始合并- 不是上面 2, 3 的情况,那么使用快慢指针确定链表的中点,以中点为界,继续分割视频
fast总是移动两次,slow总是移动一次,直到fast指向tail,此时slow就是待分割链表的中点,继续分割链表- 分割链表的前半部分
l1和分割链表的后半部分l2升序排序,第一次合并时,两个链表都是单个节点,必定升序,此后每次合并,都是两个升序链表,可以很方便的合并- 将合并后的链表返回给上一层,继续合并
代码:
class Solution {
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
private ListNode sortList(ListNode head, ListNode tail) {
if (head == null) {
return head;
}
if (head.next == tail) {
head.next = null;
return head;
}
// 确定中点
ListNode fast = head, slow = head;
while (fast != tail && fast.next != tail) {
slow = slow.next;
fast = fast.next.next;
}
// 继续分割列表
ListNode l1 = sortList(head, slow);
ListNode l2 = sortList(slow, tail);
// 合并链表,层层往上
return merge(l1, l2);
}
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode();
ListNode cur = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 == null ? l2 : l1;
return dummy.next;
}
}
147. 对链表进行插入排序 - 中等
给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
插入排序 算法的步骤:
插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。 重复直到所有输入数据插入完为止。 下面是插入排序算法的一个图形示例。部分排序的列表(黑色)最初只包含列表中的第一个元素。每次迭代时,从输入数据中删除一个元素(红色),并就地插入已排序的列表中。
对链表进行插入排序。
示例:
输入: head = [4,2,1,3]
输出: [1,2,3,4]
题解:
插入排序的基本思路为,将输入数据中的第一个元素划分到有序列表中并删除,此后不断从输入数据中取值并插入到有序列表中的合适位置。
使用一个虚拟头结点
dummy连接 head。使用一个
last指针,指向有序部分中的最后一个元素,使用cur指针指向无序列表中的第一个元素
- 如果
last的值小于cur的值,那么直接令last连接的cur上- 否则,寻找合适的位置插入
cur由于是单向链表,我们不能像数组一样从有序列表的最后一个元素往回查找,因此声明一个前驱结点prev从头开始遍历,直到prev的下一个结点的值比cur的大,插入cur到prev之后。同时,有序列表的最后一个元素连接到无序列表的第一个元素。last.next = cur.next; cur.next = prev.next; prev.next = cur;
- 插入成功后,
cur指向下一个待插入元素,即cur = last.next
代码:
public ListNode insertionSortList(ListNode head) {
ListNode dummy = new ListNode(-1, head);
ListNode last = head, cur = head.next;
while (cur != null) {
if (last.val <= cur.val) {
last = last.next;
} else {
ListNode prev = dummy;
while (prev.next.val <= cur.val) {
prev = prev.next;
}
last.next = cur.next;
cur.next = prev.next;
prev.next = cur;
}
cur = last.next;
}
return dummy.next;
}