
LeetCode23|合并 K 个升序链表(分治递归)
一、题目大意
给一个链表数组lists,数组里面每一条链表都已经是升序。把全部链表合并成一条升序链表 ,返回合并后的链表。 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:1→1→2→3→4→4→5→6 边界:lists = []、lists=[[]],输出空链表。
二、核心思路(分治归并,和 148 思想一致)
- 分 :对链表数组的下标区间对半拆分,递归处理左区间、右区间,直到区间只剩 1 条链表直接返回;区间无元素返回
null。 - 合 :左右区间各自递归完成后,得到两条已经有序的链表,复用LeetCode21 合并两个有序链表,将两条链表合并。
时间复杂度:(O(knlog k)),k 链表条数,n 单链表平均节点数 空间复杂度:(O(log k)),递归栈开销
对比朴素循环两两合并:逐条往结果上合并,时间(O(k^2n)),大数据会超时,不推荐面试写。
三、完整 Java 代码
ini
/**
* Definition for singly‑linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length == 0) {
return null;
}
return merge(lists, 0 , lists.length - 1);
}
//递归:合并lists数组 [l, r]下标区间内所有链表
private ListNode merge(ListNode[] lists, int l, int r) {
if(l > r) {
return null;
}
if(l == r) {
return lists[l];
}
int mid = (l + r) / 2;
ListNode leftPart = merge(lists, l, mid);
ListNode rightPart = merge(lists, mid + 1, r);
return mergeTwoLists(leftPart, rightPart);
}
//LeetCode21:合并两个升序链表
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
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;
}
//拼接剩余部分
if(l1 != null) {
tail.next = l1;
} else {
tail.next = l2;
}
return dummy.next;
}
}
四、实例演示(表格)
输入:lists = [1→4→5 ,1→3→4 ,2→6],下标 0、1、2,调用merge(lists,0,2)
| 递归调用 | l | r | mid | 操作说明 | 返回值 |
|---|---|---|---|---|---|
| merge(0,2) | 0 | 2 | 1 | 拆 0,1、2,2 | mergeTwoLists (左结果,右结果) |
| ├─merge(0,1) | 0 | 1 | 0 | 拆 0,0、1,1 | mergeTwoLists (左结果,右结果) |
| │ ├─merge(0,0) | 0 | 0 | - | l==r直接返回 lists 0 |
1→4→5 |
| │ └─merge(1,1) | 1 | 1 | - | l==r直接返回 lists 1 |
1→3→4 |
| │ 合并两条链表 | - | - | - | mergeTwoLists | 1→1→3→4→4→5 |
| └─merge(2,2) | 2 | 2 | - | l==r直接返回 lists 2 |
2→6 |
| 顶层合并 | - | - | - | mergeTwoLists | 1→1→2→3→4→4→5→6 |
递归树
scss
merge(0‑2)
↙ ↘
merge(0‑1) merge(2‑2)
↙ ↘ ↓
merge(0‑0) merge(1‑1) 2→6
↓ ↓
1→4→5 1→3→4
↘ ↙
mergeTwoLists合并
↓
1→1→3→4→4→5
↘ ↙
mergeTwoLists合并
↓
1→1→2→3→4→4→5→6
五、语法坑与边界条件
-
分清两个函数:
merge(lists,l,r):接收数组 + 下标,递归分治;mergeTwoLists(l1,l2):只接收两个链表节点,做两两合并。不要传数组进 mergeTwoLists,会编译报错。
-
if(l > r) return null:拆分后区间不存在,没有链表,返回空;mergeTwoLists可以正常接收null。 -
if(l == r) return lists[l]:区间只剩 1 条链表,直接返回,不再拆分。 -
mergeTwoLists 内部,
tail = tail.next不能漏写。 -
边界处理:数组为空
lists.length==0直接返回 null;数组里面存放空链表null也可以被 mergeTwoLists 正常处理。
六、个人感悟
- 本题是21 题的升级版,分治思想和 148 排序链表自顶向下归并一模一样,旧代码复用。
- 朴素循环两两合并写法简单,但是时间效率差,面试优先写分治版本。
- 递归终止条件两个:
l>r(无元素)、l==r(单个元素),两个缺一不可。 - 还有小根堆(优先队列)解法,也是(O(knlog k)),适合了解思路。