合并k个已排序的链表

描述

合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。

数据范围:节点总数 0≤n≤50000≤n≤5000,每个节点的val满足 ∣val∣<=1000∣val∣<=1000

要求:时间复杂度 O(nlogn)O(nlogn)

示例1

输入:

[{1,2,3},{4,5,6,7}]

复制返回值:

{1,2,3,4,5,6,7}

解题思路:

整体高层思路

  • 如果只合并两个链表,用 Merge(list1, list2):用一个哑节点(dummy)和尾指针 cur,迭代比较两个链表当前节点,把较小的节点直接接到结果链表后,直到某个链表遍历完,把剩余整段接上即可(原地,不创建新节点)。
  • 如果合并 k 个链表(lists),用分治:把数组区间 [left, right] 对半分,分别递归合并左右两半得到两条有序链表,再把这两条按 Merge 合并;递归直到区间为空或只有一个链表。
  • 这种分治把合并过程变成类似归并排序的合并,能把多链表合并的复杂度降到较优。

下面逐段解释代码与关键点

  1. Merge 方法(合并两个有序链表,非递归、就地)
    签名:public ListNode Merge(ListNode list1, ListNode list2)
  • 边界处理:

    • if (list1 == null) return list2;
    • if (list2 == null) return list1;
      这两句处理其中一个输入为空的情况,直接返回另一个链表(可能为 null 或非空)。
  • 创建虚拟头结点和尾指针:

    • ListNode head = new ListNode(0);
    • ListNode cur = head;
      使用虚拟头可以避免单独处理第一个节点的连接逻辑,统一操作更简洁。
  • 主循环(同时遍历两个链表):

    • while (list1 != null && list2 != null) {
      if (list1.val <= list2.val) { cur.next = list1; list1 = list1.next; }
      else { cur.next = list2; list2 = list2.next; }
      cur = cur.next;
      }
      解释:
    • 每次比较 list1 和 list2 的当前节点值,把较小(或相等时取 list1)的节点直接接到 cur.next,
    • 然后把对应链表前进一位,并把 cur 指向新接上的节点。
    • 注意这里没有创建新节点,是把原节点“摘出来”接到结果链表上,所以是原地合并。
  • 处理剩余节点:

    • if (list1 == null) cur.next = list2; else cur.next = list1;
      解释:
    • 循环结束说明至少一个链表遍历完,另一个链表剩余的节点本身是有序的,把剩余整段直接接上即可。
  • 返回结果:

    • return head.next; // 跳过虚拟头,返回真实头

复杂度:

  • 时间 O(n + m),空间 O(1)(只用了常数个指针,原地操作)。
  1. divideMerge 方法(分治合并 lists[left..right])
    签名:ListNode divideMerge(ArrayList lists, int left, int right)
  • 基本情况:

    • if (left > right) return null; // 空区间
    • else if (left == right) return lists.get(left); // 区间只有一个链表,直接返回
  • 分治合并:

    • int mid = left + (right - left) / 2; // 防止溢出(常用写法)
    • return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
      解释:
    • 先递归合并左半段和右半段,得到两条有序链表,再把两条链表用 Merge 合并。
    • 这种做法等价于把 k 条链表两两合并(类似归并排序的合并阶段),递归深度约为 O(log k)。

时间复杂度直观分析(k 条链表,总结点数为 N):

  • 每一层分治把所有节点参与合并一次,总开销与节点数成线性关系。分治共有 O(log k) 层,因此总时间复杂度为 O(N log k)(这是多链表合并的经典下界并且是高效实现之一)。
  • 空间复杂度:递归栈占 O(log k)(分治递归深度),除此之外常数额外空间(Merge 是原地的 O(1))。
  1. mergeKLists(入口)
    签名:public ListNode mergeKLists (ArrayList lists)
  • 边界处理:
    • if (lists == null || lists.size() == 0) return null;
  • 调用分治:
    • return divideMerge(lists, 0, lists.size() - 1);

这样就能把 lists 中所有链表合并成一个。

示例帮助理解

  • 假设有 4 条链表 L0, L1, L2, L3:
    • divideMerge([0,3]) 会把区间分为 [0,1] 与 [2,3] 两部分,
    • 递归分别合并 [0,1] -> M01,合并 [2,3] -> M23,
    • 最后 Merge(M01, M23) 得到最终结果。
  • 每次 Merge 的成本与参与合并的节点数成线性关系,分层合并使总复杂度为 O(N log k)。

一句话总结

  • 用一个 O(1) 空间的原地 Merge 两条有序链表作为基本操作,配合分治把 k 条链表递归两两合并,最终以 O(N log k) 时间、O(log k) 额外栈空间把所有链表合并为一条有序链表。
import java.util.*;

/*
 * 假设已有的节点类(平台通常已定义),保留示例以便参考:
 * public class ListNode {
 *   int val;
 *   ListNode next = null;
 *   public ListNode(int val) {
 *     this.val = val;
 *   }
 * }
 */

public class Solution {

    /**
     * 合并两个有序链表为一个有序链表(非递归、就地合并)
     *
     * @param list1 有序链表1 的头结点
     * @param list2 有序链表2 的头结点
     * @return 合并后的有序链表头结点
     */
    public ListNode Merge(ListNode list1, ListNode list2) {
        // 如果其中一个链表为空,直接返回另一个(边界情况)
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }

        // 构造一个虚拟头节点,方便统一处理头节点连接逻辑
        ListNode head = new ListNode(0);
        // cur 指向结果链表的当前尾部
        ListNode cur = head;

        // 同时遍历两个链表,每次把较小的节点接到结果链表后面
        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                // list1 当前节点值更小或相等,把它接到 cur 后面
                cur.next = list1;
                // list1 前进
                list1 = list1.next;
            } else {
                // list2 当前节点值更小,把它接到 cur 后面
                cur.next = list2;
                // list2 前进
                list2 = list2.next;
            }
            // cur 前进到新接上的节点
            cur = cur.next;
        }

        // 退出循环表示至少有一个链表已经遍历完
        // 把剩余的非空链表直接接到结果链表后面(注意不要接 null)
        if (list1 == null) {
            cur.next = list2; // list2 可能为非空或 null(如果都为空则接 null 也没问题)
        } else {
            cur.next = list1;
        }

        // 返回虚拟头节点的下一个节点,即真正的合并后链表头
        return head.next;
    }

    /**
     * 使用分治(递归)合并 lists 数组中索引区间 [left, right] 的链表
     *
     * 思路:将区间一分为二,分别合并左右两边,然后再把两个结果合并(两两合并)
     *
     * @param lists 存放链表头结点的 ArrayList
     * @param left  区间左端点(包含)
     * @param right 区间右端点(包含)
     * @return 合并后的链表头结点,若 left > right 返回 null(空区间)
     */
    ListNode divideMerge(ArrayList<ListNode> lists, int left, int right) {
        // 空区间,返回 null
        if (left > right) {
            return null;
        }
        // 区间只有一个链表,直接返回该链表
        else if (left == right) {
            return lists.get(left);
        }

        // 取中点,使用更稳健的写法防止极端整型溢出(虽然索引通常不会那么大)
        int mid = left + (right - left) / 2;

        // 递归合并左半区间和右半区间,然后把两个结果合并并返回
        return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
    }

    /**
     * 合并 k 个有序链表(入口函数)
     *
     * @param lists 存放链表头结点的 ArrayList(可能为 null 或 空)
     * @return 合并后的有序链表头结点;若输入为空返回 null
     */
    public ListNode mergeKLists (ArrayList<ListNode> lists) {
        // 边界处理:lists 为 null 或者没有任何链表,直接返回 null
        if (lists == null || lists.size() == 0) return null;

        // 利用分治合并所有链表,区间为 [0, lists.size() - 1]
        return divideMerge(lists, 0, lists.size() - 1);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值