链表高频题目和必备技巧
1. 链表类题目注意点
1,如果笔试中空间要求不严格,直接使用容器来解决链表问题
2,如果笔试中空间要求严格、或者在面试中面试官强调空间的优化,需要使用额外空间复杂度**O(1)**的方法
3,最常用的技巧-快慢指针
4,链表类题目往往都是很简单的算法问题,核心考察点也并不是算法设计,是coding能力
5,这一类问题除了多写多练没有别的应对方法
注意: 链表类问题既然练的就是coding,那么不要采取空间上讨巧的方式来练习
注意: 链表相关的比较难的问题是约瑟夫环问题,会在之后补充
2. 相关题目
注意:
这些题目往往难度标为“简单”,是因为用容器解决真的很简单
但是不用容器、实现额外空间复杂度O(1)的方法并不轻松,包括很多提交的答案也都没有符合要求
-
题目1 : 返回两个无环链表相交的第一个节点
-
测试链接 : https://leetcode.cn/problems/intersection-of-two-linked-lists/
-
思路
- 先判断是否不相交
- 若有一个为空则不相交
- 长链表提前走差值个, 随后两链表一同走, 走到尾巴, 若不相同, 则不相交
- 两链表从头走, 直至寻找到第一个公共节点
- 先判断是否不相交
-
代码
-
public static ListNode getIntersectionNode(ListNode h1, ListNode h2) { // 1. 先判断是否不相交 // 边界: 只要有一个为空就不相交 if (h1 == null || h2 == null) { return null; } ListNode a = h1, b = h2; // 计算两个链表长度之差 int diff = 0; while (a.next != null) { a = a.next; diff++; }// a来到最后一个结点 while (b.next != null) { b = b.next; diff--; }// b来到最后一个结点 // 边界: 如果两个链表的尾结点不相等, 则一定不相交 if (a != b) { return null; } // 2. 寻找第一个公共结点 // 如果长度不等,把长链表给a,短链表给b if (diff >= 0) { a = h1; b = h2; } else { a = h2; b = h1; } // 取差值的绝对值 diff = Math.abs(diff); // 长链表先走差值步 while (diff-- != 0) { a = a.next; } // 长链表和短链表一起走 while (a != b) { a = a.next; b = b.next; }// 当再次相交的时候停止 return a; }
-
-
-
题目2 : 每k个节点一组翻转链表
-
测试链接:https://leetcode.cn/problems/reverse-nodes-in-k-group/
-
思路
-
由于要返回总的头结点, 单独讨论第一组
- 第一组够不够k个, 不够直接返回head
- 第一组反转(反转的过程中, lastTeamEnd.next指向下一组的头), 并记录反转后的头,用于返回
- 反转后记录lastTeamEnd为原来的head(start),
-
讨论接下来其他组
-
判断接下来的组够不够k个, 不够直接返回
-
够了,翻转,调整将上一组的尾连到这一组的头, lastTeamEnd来到这一组的尾
-
一致循环到没有下一组
-
-
-
代码
-
public static ListNode reverseKGroup(ListNode head, int k) { // 1. 先讨论第一组 ListNode start = head; // 看第一组够不够k个 ListNode end = teamEnd(start, k); if (end == null) { return head; } // 第一组 很特殊因为牵扯到换头的问题 head = end;// 反转后头变成尾 reverse(start, end);// 在反转的过程中会连上下一组 // 翻转之后start变成了上一组的结尾节点 ListNode lastTeamEnd = start;// 前一组的头变成了尾 // 2. 接下来的其他组 while (lastTeamEnd.next != null) {// 下一组还有没有 start = lastTeamEnd.next;// 下一组的初头 end = teamEnd(start, k);// 下一组的初尾 if (end == null) {// 不够 return head;// 直到下一组不够k个 } reverse(start, end);// 够了,反转 lastTeamEnd.next = end;// 上一组的尾巴要连到这一组的end(反转后变成头) lastTeamEnd = start;// 该组变为要调整的下一组的上一组 } return head;// 直到没有下一组 } // 当前组的开始节点是s,往下数k个找到当前组的结束节点返回 public static ListNode teamEnd(ListNode s, int k) { while (--k != 0 && s != null) {// 当前计数 s = s.next;// 走向下一个 } return s; } // s -> a -> b -> c -> e -> 下一组的开始节点 // 上面的链表通过如下的reverse方法调整成 : e -> c -> b -> a -> s -> 下一组的开始节点 public static void reverse(ListNode s, ListNode e) { e = e.next;// 先保存下一组的第一个节点 ListNode pre = null, cur = s, next = null; while (cur != e) {// 当前节点不是下一组的结点, 反转 next = cur.next; cur.next = pre; pre = cur; cur = next; } s.next = e;// 将下一组的结点连在反转后的尾巴上 }
-
-
-
题目3 : 复制带随机指针的链表
-
测试链接 : https://leetcode.cn/problems/copy-list-with-random-pointer/
-
思路
- 将原来的结点每一个都赋值一份放在原节点的后面, 将原节点和现节点串在一起
- 利用新老节点的关系, 设置每一个新节点的random指针
- 老链表分离 : 老链表重新连在一起,新链表重新连在一起
- 返回新链表的头节点
-
代码
-
public static Node copyRandomList(Node head) { // 特殊处理 if (head == null) { return null; } // 克隆 // 1 -> 2 -> 3 -> ... // 变成 : 1 -> 1' -> 2 -> 2' -> 3 -> 3' -> ... Node cur = head; Node next = null; while (cur != null) { next = cur.next;// 记录下一个 cur.next = new Node(cur.val);// 克隆 cur.next.next = next;// 原来的下一个接在克隆后的节点上 cur = next;// 当前节点走到原来的下一个 } // 利用上面新老节点的结构关系,设置每一个新节点的random指针 cur = head; Node copy = null; while (cur != null) { next = cur.next.next;// 记录下一个值 copy = cur.next;// 记录当前节点的克隆节点 copy.random = cur.random != null ? cur.random.next : null; cur = next; } // 新老链表分离 : 老链表重新连在一起,新链表重新连在一起 Node ans = head.next;// 记录新链表的头节点 cur = head; while (cur != null) { next = cur.next.next; copy = cur.next; cur.next = next;// 原链表恢复原状 copy.next = next != null ? next.next : null; cur = next; } // 返回新链表的头节点 return ans; }
-
-
-
题目4 : 判断链表是否是回文结构。这个题的流程设计甚至是考研常用。快慢指针找中点。
-
测试链接 : https://leetcode.cn/problems/palindrome-linked-list/
-
思路
-
利用快慢指针找中点
-
找到后中点就是slow,从中点开始往后的节点逆序
-
开始头尾对照,设置临时变量进行处理,以保留原来的结点用于还原链表
-
把链表调整回原来的样子再返回判断结果
-
-
代码
-
public static boolean isPalindrome(ListNode head) { // 特例判断 if (head == null || head.next == null) {// 没有/只有一个结点 return true; } // 1. 快慢指针找中点 ListNode slow = head, fast = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; }// fast跳不了 // 2. 现在中点就是slow,从中点开始往后的节点逆序 // head -> ... -> slow -> ... -> ... ListNode pre = slow; ListNode cur = pre.next; ListNode next = null; pre.next = null;// ! 原来slow.next要先保留,赋值给cur, 随后中点的next要悬空(用于后续翻转时作为判断值) while (cur != null) { next = cur.next;// 以防cur->next改变后后续指针丢失 cur.next = pre; pre = cur; cur = next; } // 3. 开始对照,设置临时变量进行处理,以保留原来的结点用于还原链表 boolean ans = true; ListNode left = head; ListNode right = pre; // head -> ... -> slow <- ... <- pre // left往右、right往左,每一步比对值是否一样,如果某一步不一样答案就是false while (left != null && right != null) {// 循环至中点的next == null,说明为回文结构 if (left.val != right.val) { ans = false; break;// 先不返回, 把链表复原 } left = left.next; right = right.next; } // 4. 把链表调整回原来的样子再返回判断结果 // head -> ... -> slow <- ... <- pre // pre -> ... -> slow cur = pre.next; pre.next = null; next = null; while (cur != null) {// 中点的next为null next = cur.next; cur.next = pre; pre = cur; cur = next; } return ans; }
-
-
某考研题要求收尾挨个相接也用的同样的方法
-

-
题目5 : 返回链表的第一个入环节点。快慢指针找中点。
-
测试链接 : https://leetcode.cn/problems/linked-list-cycle-ii/
-
思路
- 特殊处理
- 设置双指针, 若f在跳的过程中先走到null, 说明无环, 返回
- 当fs相遇, f回到头结点, s不变
- f每次跳一步, s每次跳一步
- 最后相遇时, 一定是在入环的第一个节点
-
代码
-
public static ListNode detectCycle(ListNode head) { // 特殊处理: 为null, 只有一个结点, 只有两个结点且能到头 if (head == null || head.next == null || head.next.next == null) { return null; } // 1. f跳两步 s跳一步 ListNode slow = head.next;// 已经排除过特殊情况,直接往下跳 ListNode fast = head.next.next; while (slow != fast) { // !!如果f先走到头, 说明没有环 if (fast.next == null || fast.next.next == null) { return null; } slow = slow.next; fast = fast.next.next; } // 2. 相遇时, f回到头结点, s保持不变 fast = head; // 3. f跳一步 s跳一步 while (slow != fast) { slow = slow.next; fast = fast.next; } // 4. 最后相遇时一定是在入环的第一个节点 return slow; }
-
-
-
题目6 : 在链表上排序。要求时间复杂度O(n * log n),额外空间复杂度O(1),还要求排序有稳定性。
-
测试链接 : https://leetcode.cn/problems/sort-list/
-
思路
- 计算链表的长度, 用于限制步长
- 开始按照步长进行排序
- 先处理第一组,因为排序后很可能要换头
- 继续处理其他组
-
代码
-
public static ListNode sortList(ListNode head) { // 计算链表的长度, 用于限制步长 int n = 0; ListNode cur = head; while (cur != null) { n++; cur = cur.next; } // l1...r1 每组的左部分 // l2...r2 每组的右部分 // next 下一组的开头 // lastTeamEnd 上一组的结尾 ListNode l1, r1, l2, r2, next, lastTeamEnd; for (int step = 1; step < n; step <<= 1) {// 进来了,说明step<n,即n>=2 // 第一组很特殊,因为要决定整个链表的头,所以单独处理 // 找第一组的头尾, 第二组的头尾, 保存剩余链表的头部并将12组分离 l1 = head; r1 = findEnd(l1, step); l2 = r1.next; r2 = findEnd(l2, step); next = r2.next; r1.next = null; r2.next = null; merge(l1, r1, l2, r2); head = start;// 临时保存 lastTeamEnd = end; // 接下来的其他组 while (next != null) { l1 = next; r1 = findEnd(l1, step); l2 = r1.next; if (l2 == null) {// 若l2不存在, 则无需进行合并, 直接停止 lastTeamEnd.next = l1;// 尾接头,break不是return,因为要继续外层的循环 break; } r2 = findEnd(l2, step); next = r2.next; r1.next = null;// !!!!都是断开r!!!!!!!!!!!!! r2.next = null; merge(l1, r1, l2, r2); lastTeamEnd.next = start;// 将上一组和调整好的这一组链接在一起 lastTeamEnd = end;// 这一组的尾变成lastTeamEnd } } return head; } // 包括s在内,往下数k个节点返回 // 如果不够,返回最后一个数到的非空节点 public static ListNode findEnd(ListNode s, int k) { while (s.next != null && --k != 0) { s = s.next; } return s; } public static ListNode start; public static ListNode end; // l1...r1 -> null : 有序的左部分 // l2...r2 -> null : 有序的右部分 // 整体merge在一起,保证有序 // 并且把全局变量start设置为整体的头,全局变量end设置为整体的尾 public static void merge(ListNode l1, ListNode r1, ListNode l2, ListNode r2) { ListNode pre; // 找头 if (l1.val <= l2.val) { start = l1; pre = l1; l1 = l1.next; } else { start = l2; pre = l2; l2 = l2.next; } // 合并 while (l1 != null && l2 != null) { if (l1.val <= l2.val) { pre.next = l1; pre = l1; l1 = l1.next; } else { pre.next = l2; pre = l2; l2 = l2.next; } } // 找尾 if (l1 != null) { pre.next = l1; end = r1; } else { pre.next = l2; end = r2; } }
-
-