LeetCode23|合并K个升序链表 分治递归极简解法

0 阅读2分钟

前言

合并多条有序链表,暴力逐个合并效率很低,分治是最优解之一,时间复杂度 O(nlogk),代码简洁好记,下面逐行拆解这段 JS 代码。

链表节点定义(题目自带)

function ListNode(val, next) {
  this.val = val === undefined ? 0 : val
  this.next = next === undefined ? null : next
}

完整代码

var mergeKLists = function(lists) {
  // 合并两个有序链表(LeetCode21逻辑)
  const mergeTwo = (a, b) => {
    const dummy = new ListNode(0);
    let cur = dummy;
    while (a && b) {
      if (a.val < b.val) {
        cur.next = a;
        a = a.next;
      } else {
        cur.next = b;
        b = b.next;
      }
      cur = cur.next;
    }
    // 拼接剩余未遍历完的链表
    cur.next = a || b;
    return dummy.next;
  }

  // 分治递归:合并区间 [l, r] 的所有链表
  const merge = (l, r) => {
    // 区间只有一条链表,直接返回
    if (l === r) return lists[l];
    // 左边界大于右边界,无链表返回null
    if (l > r) return null;
    // 二分取中点
    const mid = (l + r) >> 1;
    // 递归合并左半区、右半区
    const left = merge(l, mid);
    const right = merge(mid + 1, r);
    // 将左右合并后的两条链表合并
    return mergeTwo(left, right);
  }

  // 从整个数组区间 0 ~ lists.length-1 开始分治
  return merge(0, lists.length - 1);
};

代码逐段解析

1. mergeTwo(a, b):合并两条有序链表

核心是虚拟头节点 dummy,规避链表头节点判断逻辑:

  1. cur 指针遍历拼接;
  2. 每次把更小值的节点接到 cur.next
  3. 循环结束后,直接拼接剩下未走完的链表;
  4. 返回 dummy.next 即为合并后的链表头部。

2. merge(l, r):分治递归函数

采用二分拆分思想,类似归并排序:

  1. 终止条件1:l === r,当前区间只剩一条链表,直接返回;
  2. 终止条件2:l > r,区间不存在链表,返回空;
  3. (l + r) >> 1 等价 Math.floor((l+r)/2),快速取中间下标;
  4. 递归拆分左区间 [l,mid]、右区间 [mid+1,r]
  5. 左右区间各自合并完成后,调用 mergeTwo 合并两条结果链表。

3. 入口调用

merge(0, lists.length - 1) 代表对整个链表数组做分治合并。

思路总结

  1. 分治拆分:把 k 条链表不断二分,直到每组只剩 1 条链表;
  2. 逐层向上合并:每两层合并为一条有序链表;
  3. 底层复用「合并两有序链表」基础逻辑,代码复用性强。

复杂度

  • 时间:O(nlogk),n 所有节点总数,k 链表数量;
  • 空间:O(logk) 递归栈开销。

适用场景

面试首选解法,代码短、逻辑清晰,相比最小堆不需要手动实现堆结构,上手更快。