目录
一、链表反转
1.1 问题描述
将单链表的所有节点指针方向反转,使链表从 1→2→3→null 变为 3→2→1→null。
1.2 核心思想
把每个节点的 next 指针从"指向后"改成"指向前"
原链表: 1 → 2 → 3 → 4 → null
目标: null ← 1 ← 2 ← 3 ← 4 (即 4 → 3 → 2 → 1 → null)
1.3 三指针分工
| 指针 | 作用 | 说明 |
|---|---|---|
prev |
指向已反转部分的头部 | "前一个",已处理完毕 |
cur |
指向正在处理的节点 | "当前的",处理中 |
next |
暂存下一个待处理节点 | 保险绳,防止断链 |
1.4 代码实现
public ListNode reverseList(ListNode head) {
ListNode prev = null; // 已反转部分的头部(初始为空)
ListNode cur = head; // 当前处理的节点
while (cur != null) {
ListNode next = cur.next; // 1. 暂存下一个(保险绳)
cur.next = prev; // 2. 反转指针方向
prev = cur; // 3. prev前进
cur = next; // 4. cur前进
}
return prev; // cur为null时,prev指向新头节点
}
1.5 图解全过程
初始: null 1 → 2 → 3 → null
↑ ↑
prev cur/next
第1轮后: null ← 1 2 → 3 → null
↑ ↑
prev cur/next
第2轮后: null ← 1 ← 2 3 → null
↑ ↑
prev cur/next
第3轮后: null ← 1 ← 2 ← 3 null
↑ ↑
prev cur(结束)
返回 prev = 3 (新头节点)
1.6 四步口诀
1. 买保险:
next = cur.next(暂存后路)
2. 反方向:cur.next = prev(调转箭头)
3. 往前走:prev = cur(后卫跟进)
4. 继续走:cur = next(前锋前进)
1.7 复杂度分析
| 类型 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(n) | 遍历一次链表 |
| 空间 | O(1) | 只使用三个指针 |
二、445. 两数相加 II
2.1 问题描述
两个非空 链表表示两个非负整数,最高位在前,每位存一位数字。返回相加后的链表(同样最高位在前)。
与题2的区别:
| 题号 | 存储方式 | 示例 |
|---|---|---|
| 2 | 逆序(个位在前) | 2→4→3 表示 342 |
| 445 | 正序(高位在前) | 7→2→4→3 表示 7243 |
2.2 核心思想
反转 → 相加(复用题2)→ 再反转
l1: 7 → 2 → 4 → 3 (表示 7243)
l2: 5 → 6 → 4 (表示 564)
步骤1: 反转两个链表
l1: 3 → 4 → 2 → 7
l2: 4 → 6 → 5
步骤2: 相加(逆序相加,同题2)
3+4=7, 4+6=10(写0进1), 2+5+1=8, 7+0=7
结果: 7 → 0 → 8 → 7
步骤3: 反转结果
最终: 7 → 8 → 0 → 7 (表示 7807)
验证: 7243 + 564 = 7807 ✓
2.3 代码实现
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 1. 反转两个链表(变成逆序,方便从低位加)
ListNode n1 = reverse(l1);
ListNode n2 = reverse(l2);
// 2. 相加(此时和题2完全一样)
ListNode dummy = new ListNode(), tail = dummy;
int carry = 0;
while (n1 != null || n2 != null || carry != 0) {
int v1 = (n1 != null) ? n1.val : 0;
int v2 = (n2 != null) ? n2.val : 0;
int sum = v1 + v2 + carry; // 先算总和
int digit = sum % 10; // 当前位(个位)
carry = sum / 10; // 新进位
tail.next = new ListNode(digit); // 接个位
tail = tail.next;
if (n1 != null) n1 = n1.next;
if (n2 != null) n2 = n2.next;
}
// 3. 反转结果(变回正序)
return reverse(dummy.next);
}
// 链表反转(复用上一节代码)
private ListNode reverse(ListNode head) {
ListNode prev = null, cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
}
2.4 关键技巧
| 技巧 | 代码 | 作用 |
|---|---|---|
| 哑节点 | ListNode dummy = new ListNode() |
简化头节点处理 |
| 0补位 | (n1 != null) ? n1.val : 0 |
处理链表长度不一 |
| 进位条件 | `while (... | |
| 三次反转 | 输入两链表反转,结果再反转 | 将正序问题转为逆序问题 |
2.5 复杂度分析
| 类型 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(max(m, n)) | 三次遍历,常数倍 |
| 空间 | O(max(m, n)) | 结果链表空间 |
2.6 常见错误
// ❌ 错误:进位和结果搞反
in = (v1 + v2 + carry) % 10; // 把个位存进carry变量!
int result = (v1 + v2 + in) / 10; // 错误!in已被修改
// ✅ 正确:先存sum,再拆分
int sum = v1 + v2 + carry;
int digit = sum % 10; // 当前位
carry = sum / 10; // 新进位
三、23. 合并K个升序链表(分治版)
3.1 问题描述
给定链表数组 lists,每个链表已按升序排列,合并为一个升序链表返回。
输入: lists = [[1,4,5], [1,3,4], [2,6]]
输出: 1→1→2→3→4→4→5→6
3.2 核心思想:分治归并
将k个链表两两配对,逐层合并,类似归并排序
lists: [L1, L2, L3, L4, L5, L6, L7, L8]
第1轮: (L1+L2), (L3+L4), (L5+L6), (L7+L8) → 4个结果
第2轮: (L12+L34), (L56+L78) → 2个结果
第3轮: (L1234+L5678) → 1个结果
3.3 递归三要素
| 要素 | 内容 |
|---|---|
| 函数定义 | mergeRange(lists, left, right) 合并 lists[left..right] |
| 终止条件 | left == right,只有一个链表,直接返回 |
| 递归逻辑 | 分两半分别合并,再合并两个结果 |
3.4 代码实现
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
return mergeRange(lists, 0, lists.length - 1);
}
// 分治:合并 lists[left..right]
private ListNode mergeRange(ListNode[] lists, int left, int right) {
// 【终止条件】只有一个,直接返回
if (left == right) {
return lists[left];
}
// 【分】找中点,分成两半
int mid = left + (right - left) / 2;
// 【治】递归合并左半部分
ListNode l1 = mergeRange(lists, left, mid);
// 【治】递归合并右半部分
ListNode l2 = mergeRange(lists, mid + 1, right);
// 【合】合并两个有序链表
return mergeTwoLists(l1, l2);
}
// 合并两个有序链表(复用题21代码)
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(), tail = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = (l1 != null) ? l1 : l2; // 接剩余部分
return dummy.next;
}
}
3.5 递归展开图解
以 lists = [L1, L2, L3, L4] 为例:
调用: mergeRange(0, 3) // 合并 L1,L2,L3,L4
│
├── mid = 1
│
├── 左: mergeRange(0, 1) // 合并 L1,L2
│ │
│ ├── mid = 0
│ ├── 左: mergeRange(0, 0) → L1 (base case)
│ └── 右: mergeRange(1, 1) → L2 (base case)
│ └── 合并 L1+L2 → L12
│
└── 右: mergeRange(2, 3) // 合并 L3,L4
│
├── mid = 2
├── 左: mergeRange(2, 2) → L3 (base case)
└── 右: mergeRange(3, 3) → L4 (base case)
└── 合并 L3+L4 → L34
│
└── 合并 L12+L34 → L1234
3.6 为什么比暴力合并快?
| 方式 | 每个链表被合并次数 | 总比较次数 |
|---|---|---|
| 暴力(逐个合并) | 第i个合并i次 | 1+2+...+(k-1) = O(k²) |
| 分治(两两配对) | 每层合并1次,共log k层 | k × log k = O(k log k) |
暴力的问题: 最后一个链表要和前面所有结果比较,次数不均衡
分治的优势: 每个链表只在每一层参与一次合并,完全均衡!
3.7 复杂度分析
| 类型 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(kn log k) | k个链表,每个长度n,log k层 |
| 空间 | O(log k) | 递归栈深度 |
3.8 记忆口诀
分:找中点劈两半
治:递归合并各自半
合:两个结果再合并
终:只剩一个就返回
四、三题对比总结
| 题目 | 核心技巧 | 关键操作 | 复杂度 |
|---|---|---|---|
| 链表反转 | 三指针 | next暂存、cur.next = prev、双指针前移 |
时间O(n) 空间O(1) |
| 两数相加II | 三次反转 | 反转→相加→反转,将正序转逆序处理 | 时间O(n) 空间O(n) |
| 合并K个链表 | 分治归并 | 递归分半、合并两个、逐层归并 | 时间O(kn log k) 空间O(log k) |
五、通用模板速查
5.1 哑节点 + 尾指针模板
ListNode dummy = new ListNode(), tail = dummy;
// 处理逻辑...
tail.next = new ListNode(val); // 或 tail.next = 已有节点
tail = tail.next;
return dummy.next;
5.2 链表反转模板
ListNode prev = null, cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
5.3 合并两个链表模板
ListNode dummy = new ListNode(), tail = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
tail.next = l1; l1 = l1.next;
} else {
tail.next = l2; l2 = l2.next;
}
tail = tail.next;
}
tail.next = (l1 != null) ? l1 : l2;
return dummy.next;
5.4 分治递归模板
private ListNode mergeRange(ListNode[] lists, int left, int right) {
if (left == right) return lists[left];
int mid = left + (right - left) / 2;
ListNode l1 = mergeRange(lists, left, mid);
ListNode l2 = mergeRange(lists, mid + 1, right);
return merge(l1, l2);
}
六、一句话总结
反转用三指针,正序加法先反转,K个链表分治并。