【LeetCode 热题 100】23. 合并 K 个升序链表——(解法四)迭代式归并

Problem: 23. 合并 K 个升序链表
题目:给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。

整体思路

这段代码旨在解决 “合并 K 个排序链表” 的问题,它采用了一种非常高效且空间优化的 自底向上(或称迭代式)归并排序 策略。与之前的递归分治法相比,这种方法避免了递归调用,从而消除了对调用栈的依赖,实现了真正的 O(1) 额外空间复杂度。

算法的核心思想是从最小的单位(单个链表)开始,以轮次(pass)的形式,逐步将它们两两合并成更大的有序链表,直到所有链表都合并成一个。

其核心逻辑步骤如下:

  1. 处理边界情况

    • 检查输入的链表数组 lists 是否为空,如果是,则直接返回 null
  2. 迭代合并轮次

    • 算法的核心是一个外层 for 循环,由 step 变量驱动。step 代表了当前轮次中,被合并的“子单元”的大小。它从 1 开始,每次翻倍(1, 2, 4, 8, ...)。
    • step = 1:算法将 lists[0]lists[1] 合并,lists[2]lists[3] 合并,以此类推,将所有链表两两配对进行合并。合并的结果存回配对的第一个位置(例如,merge(lists[0], lists[1]) 的结果存回 lists[0])。
    • step = 2:现在,lists[0] 是一个包含两个原始链表的有序链表,lists[2] 也是。算法将 lists[0]lists[2] 合并,lists[4]lists[6] 合并…
    • 持续进行:这个过程不断重复,每次合并的链表“块”都越来越大,直到 step 的大小超过了链表数组的长度,此时所有链表都已被合并到 lists[0] 中。
  3. 配对合并

    • 内层的 for 循环负责在每一轮中找到要合并的配对。
    • i += step * 2 这个跳跃方式确保了我们每次都能找到下一对独立的“块”来进行合并。例如,当 step=1 时,我们处理 (0, 1),然后跳到 (2, 3)。当 step=2 时,我们处理 (0, 2),然后跳到 (4, 6)
    • lists[i] = mergeTwo(lists[i], lists[i + step]); 调用标准的“合并两个有序链表”的辅助函数来完成实际的合并工作。
  4. 返回结果

    • 所有轮次结束后,lists[0] 中就包含了最终的、完全合并好的链表,将其返回。

完整代码

/**
 * Definition for singly-linked list.
 */
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 {
    /**
     * 合并 K 个排序链表(自底向上归并排序法)。
     * @param lists 一个包含 K 个排序链表的数组
     * @return 合并后的单一排序链表
     */
    public ListNode mergeKLists(ListNode[] lists) {
        int n = lists.length;
        // 边界情况:没有链表需要合并
        if (n == 0) {
            return null;
        }
        
        // 外层循环控制合并的轮次。step 代表当前合并的“块”的大小。
        // step 从 1 开始,每次翻倍,直到能覆盖所有链表。
        for (int step = 1; step < n; step *= 2) {
            // 内层循环负责在当前轮次中,找到所有需要合并的配对。
            // 每次跳跃 2*step,以找到下一对独立的块。
            for (int i = 0; i < n - step; i += step * 2) {
                // 将 lists[i] 和 lists[i + step] 这两个块进行合并,
                // 结果存回 lists[i]。
                lists[i] = mergeTwo(lists[i], lists[i + step]);
            }
        }
        
        // 所有轮次结束后,最终的合并结果存储在 lists[0] 中。
        return lists[0];
    }

    /**
     * 辅助函数:合并两个有序链表。
     * @param l1 第一个有序链表
     * @param l2 第二个有序链表
     * @return 合并后的有序链表
     */
    private ListNode mergeTwo(ListNode l1, ListNode l2) {
        // 使用哨兵节点简化合并逻辑
        ListNode dummy = new ListNode();
        ListNode cur = dummy;
        
        // 迭代比较,将较小的节点链接到结果链表
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        // 将剩余的链表部分直接追加到末尾
        cur.next = l1 == null ? l2 : l1;
        
        return dummy.next;
    }
}

时空复杂度

时间复杂度:O(N log K)

  1. 外层循环step 变量从 1 开始,每次乘以 2,直到 step >= KK 是链表数量 n)。这个循环的执行次数是 O(log K)
  2. 内层循环与 mergeTwo:这是分析的关键。对于外层循环的每一轮(即一个固定的 step 值),内层循环和它调用的 mergeTwo 函数合起来,会完整地遍历所有 N 个节点一次。例如,当 step=1 时,merge(0,1), merge(2,3)… 这些操作的总和是遍历所有节点。当 step=2 时,merge(0,2), merge(4,6)… 这些操作的总和还是遍历所有节点。因此,每一轮的代价是 O(N)

综合分析
总的时间复杂度 = (轮次) * (每轮的代价) = O(log K) * O(N)。因此,最终时间复杂度为 O(N log K),其中 N 是总节点数,K 是链表数。

空间复杂度:O(1)

  1. 主要存储开销:该算法是迭代的,没有使用递归,因此没有递归栈的开销。
  2. 辅助数据结构:算法没有使用优先队列或任何其他与输入规模成比例的数据结构。mergeTwo 函数只使用了 dummycur 等常数个指针。
  3. 原地操作:算法在输入的 lists 数组上原地进行修改。

综合分析
算法所需的额外辅助空间是常数级别的。因此,其空间复杂度为 O(1)。这是该解法相比于递归分治法和优先队列法的最大优势,也是此问题的最优空间复杂度解法。

参考灵神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值