题目详情可见 合并两个有序链表
x// 递归:明确 「当前节点」「该做什么」「什么时候做」「注意返回值」// - 当前节点:两个链表的头节点// - 该做什么:选择值小的节点,递归下一个节点// - 什么时候做:先序 || 后序 都 🉑️// - 返回值:子问题的头节点,接在一起即可 「当前节点.next = 子问题返回值」public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 处理当前节点 // 如果两个节点都为 null,说明已经遍历完了两个两边,返回 null if (list1 == null && list2 == null) return null; // 如果 list1 为 null,说明 list2 没有处理完,直接返回 list2 if (list1 == null) return list2; // 如果 list2 为 null,说明 list1 没有处理完,直接返回 list1 if (list2 == null) return list1; // 两个 list 都不为空,选择值小的,递归下一个节点 if (list1.val < list2.val) { // 当前节点.next = 子问题返回值 list1.next = mergeTwoLists(list1.next, list2); return list1; } else { list2.next = mergeTwoLists(list1, list2.next); return list2; }}
// 非递归public ListNode mergeTwoLists(ListNode list1, ListNode list2) { // 虚拟一个假头节点,方便后续处理 ListNode dummy = new ListNode(-1); ListNode p = dummy;
ListNode p1 = list1; // list1 的指针节点 ListNode p2 = list2; // list2 的指针节点
// 当 p1 和 p2 均非空时 while (p1 != null && p2 != null) { if (p1.val <= p2.val) { p.next = p1; p1 = p1.next; } else { p.next = p2; p2 = p2.next; } p = head.next; } // 当 p1 或 p2 为空时 p.next = p1 == null ? p2 : p1;
return dummy.next;}题目详情可见 合并K个升序链表
利用 优先队列 来处理,即 最小堆
每次选择堆顶元素(最小值),并加入 「堆顶元素.next」
其实与合并两个链表思路一致,只不过合并两个链表是选择两个元素中的最小值,合并K个链表是选择K个元素中的最小值而已
xxxxxxxxxx// 如果可以不用递归,就不用递归;非递归的耗时更短public ListNode mergeKLists(ListNode[] lists) { if (lists == null) return null; Queue<ListNode> pq = new PriorityQueue<>((o1, o2) -> (o1.val - o2.val)); for (ListNode list : lists) { if (list != null) pq.offer(list); } return mergeKListsHelper(pq);}
private ListNode mergeKListsHelper(Queue<ListNode> pq) { ListNode dummy = new ListNode(-1); ListNode p = dummy; while (!pq.isEmpty()) { ListNode node = pq.poll(); if (node.next != null) pq.offer(node.next); p.next = node; p = p.next; } return dummy.next;}题目详情可见 环形链表
采用快慢指针的方法
slow 每次移动一步;fast 每次移动两步
如果存在环,slow 和 fast 必会相遇
xxxxxxxxxxpublic boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; // 此处不需要判断 slow,如果 fast 不为 null,slow 经过的路径肯定不为 null while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false;}题目详情可见 环形链表 II
和上面题目的区别:需要返回链表开始入环的第一个节点
k = x + y
2k = x + y + z + y
所以有:x = z
相遇后,令其中一个回到起点,然后以相同的速度移动,再次相遇时即是环起点
xxxxxxxxxxpublic ListNode detectCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; // 核心代码 if (slow == fast) { slow = head; while (slow != fast) { slow = slow.next; fast = fast.next; } return slow; } } return null;}题目详情可见 链表的中间结点
利用快慢指针,当 fast 到达终点时,slow 刚好到达中点
当节点数量为奇数时,中间节点正好为 1 个
当节点数量为偶数时,中间节点有 2 个,正好返回第 2 个中间节点
xxxxxxxxxxpublic ListNode middleNode(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow;}题目详情可见 相交链表
思路:当 A 移动完后,接着从 B 开始移动;同理,当 B 移动完后,接着从 A 开始移动
- 如果有交点,则会在交点处相遇
- 如果没有交点,则会移动到 null 处停止 (这个处理太妙了)
xxxxxxxxxxpublic ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode pA = headA; ListNode pB = headB; while (pA != pB) { if (pA == null) pA = headB; else pA = pA.next; if (pB == null) pB = headA; else pB = pB.next; } return pA; }题目详情可见 删除链表的倒数第N个结点
先行节点:提前移动N步
后行节点:在先行节点移动完N步后再移动
当先行节点到达表尾时,后行节点的位置即为倒数第N个节点
xxxxxxxxxxpublic ListNode removeNthFromEnd(ListNode head, int n) { // 虚拟一个假头节点,方便后续处理 「head = [1], n = 1」 ListNode dummy = new ListNode(-1); dummy.next = head; // prev 为了删除倒数第N个节点 ListNode prev = dummy; ListNode slow = head; ListNode fast = head; for (int i = 0; i < n; i++) fast = fast.next;
while (fast != null) { prev = slow; slow = slow.next; fast = fast.next; } prev.next = slow.next; return dummy.next;}技巧总结
虚拟头节点,方便处理一些特殊 null 指针情况
快、慢指针套路
- 求中间节点 / 判断环(两倍速度)
- 倒数第 N 个节点(fast 先移动 N 步)
- 确定入环节点(先两倍速度,后一倍速度)
合并链表
- 两个链表合并(依次比较头节点)
- K 个链表合并(利用优先队列,快速得到最小值元素)
链表相交
- 先走完自己的链表,然后转而走对方链表,相等处即为交点