LeetCode 23.合并 K 个升序链表(分治递归)

32 阅读3分钟

image.png

LeetCode23|合并 K 个升序链表(分治递归)

一、题目大意

给一个链表数组lists,数组里面每一条链表都已经是升序。把全部链表合并成一条升序链表,返回合并后的链表。 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:1→1→2→3→4→4→5→6 边界:lists = []、lists=[[]],输出空链表。

二、核心思路(分治归并,和 148 思想一致)

  1. 分:对链表数组的下标区间对半拆分,递归处理左区间、右区间,直到区间只剩 1 条链表直接返回;区间无元素返回null。
  2. 合:左右区间各自递归完成后,得到两条已经有序的链表,复用LeetCode21 合并两个有序链表,将两条链表合并。

时间复杂度:(O(knlog k)),k 链表条数,n 单链表平均节点数 空间复杂度:(O(log k)),递归栈开销

对比朴素循环两两合并:逐条往结果上合并,时间(O(k^2n)),大数据会超时,不推荐面试写。

三、完整 Java 代码

/**
 * 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)

递归调用lrmid操作说明返回值
merge(0,2)021拆 [0,1]、[2,2]mergeTwoLists (左结果,右结果)
├─merge(0,1)010拆 [0,0]、[1,1]mergeTwoLists (左结果,右结果)
│ ├─merge(0,0)00-l==r直接返回 lists [0]1→4→5
│ └─merge(1,1)11-l==r直接返回 lists [1]1→3→4
│ 合并两条链表---mergeTwoLists1→1→3→4→4→5
└─merge(2,2)22-l==r直接返回 lists [2]2→6
顶层合并---mergeTwoLists1→1→2→3→4→4→5→6

递归树

                 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

五、语法坑与边界条件

  1. 分清两个函数:

    • merge(lists,l,r):接收数组 + 下标,递归分治;
    • mergeTwoLists(l1,l2):只接收两个链表节点,做两两合并。不要传数组进 mergeTwoLists,会编译报错。
  2. if(l > r) return null:拆分后区间不存在,没有链表,返回空;mergeTwoLists可以正常接收null。

  3. if(l == r) return lists[l]:区间只剩 1 条链表,直接返回,不再拆分。

  4. mergeTwoLists 内部,tail = tail.next不能漏写。

  5. 边界处理:数组为空lists.length==0直接返回 null;数组里面存放空链表null也可以被 mergeTwoLists 正常处理。

六、个人感悟

  1. 本题是21 题的升级版,分治思想和 148 排序链表自顶向下归并一模一样,旧代码复用。
  2. 朴素循环两两合并写法简单,但是时间效率差,面试优先写分治版本。
  3. 递归终止条件两个:l>r(无元素)、l==r(单个元素),两个缺一不可。
  4. 还有小根堆(优先队列)解法,也是(O(knlog k)),适合了解思路。