Leetcode 113 合并 K 个升序链表

1 题目

23. 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

复制代码
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

复制代码
输入:lists = []
输出:[]

示例 3:

复制代码
输入:lists = [[]]
输出:[]

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i]升序 排列
  • lists[i].length 的总和不超过 10^4

2 代码实现

c++

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge (lists , 0 , lists.size() - 1 );
    }
private:
    ListNode* merge (vector<ListNode*>& lists , int left , int right ){
        if (left > right ) return nullptr ;
        if (left == right ) return lists [left];
        int mid = left + (right - left ) / 2 ;
        ListNode* leftList = merge(lists,left ,mid );
        ListNode* rightList = merge (lists,mid + 1 , right );
        return mergeTwoLists (leftList , rightList);
    }
    ListNode* mergeTwoLists(ListNode* l1 ,ListNode* l2 ){
        ListNode dummy ;
        ListNode* cur = &dummy ;
        while (l1 != nullptr && l2 != nullptr){
            if (l1 -> val < l2 -> val ){
                cur -> next = l1 ;
                l1 = l1 -> next ;
            }else {
                cur -> next = l2 ;
                l2 = l2 -> next ;
            }

            cur = cur -> next ;
        }
        cur -> next = l1 != nullptr ? l1 : l2 ;
        return dummy.next ;
    }
};

思考

一看是hard题,吓尿了,理论上可以堆排序,但是实际上我数据结构的堆排序代码根本不会写啊...

直接看题解好了,有点难哦。

题解

合并 K 个升序链表

合并 K 个升序链表的最优解法是归并排序(分治思想),时间复杂度为 O(NlogK)(N 是所有链表的节点总数,K 是链表个数),空间复杂度为 O(logK)(递归调用栈深度),能高效处理题目中 K≤104 的边界条件。

核心思路:分治 + 合并两个升序链表

  1. 分治 :将 K 个链表不断拆分为左右两部分,直到每部分只剩 0 个或 1 个链表(递归终止条件);
  2. 合并:从最小的子问题开始,依次合并两个升序链表,最终得到整体有序的合并链表。

完整 C++ 代码实现

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 定义链表节点结构
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    // 主函数:合并K个升序链表
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge(lists, 0, lists.size() - 1);
    }

private:
    // 分治函数:合并[left, right]区间内的链表
    ListNode* merge(vector<ListNode*>& lists, int left, int right) {
        // 递归终止条件1:区间无效(left > right),返回空
        if (left > right) return nullptr;
        // 递归终止条件2:区间只有1个链表,直接返回该链表
        if (left == right) return lists[left];
        // 分治:拆分为左右两个子区间,递归合并
        int mid = left + (right - left) / 2; // 避免整数溢出
        ListNode* leftList = merge(lists, left, mid);
        ListNode* rightList = merge(lists, mid + 1, right);
        // 合并两个升序链表,返回合并结果
        return mergeTwoLists(leftList, rightList);
    }

    // 辅助函数:合并两个升序链表(基础经典题)
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        // 虚拟头节点:简化链表头节点的处理(无需单独判断空链表)
        ListNode dummy;
        ListNode* cur = &dummy; // 遍历指针,指向虚拟头节点
        // 当两个链表都非空时,比较节点值,取较小的节点加入结果
        while (l1 != nullptr && l2 != nullptr) {
            if (l1->val < l2->val) {
                cur->next = l1;
                l1 = l1->next;
            } else {
                cur->next = l2;
                l2 = l2->next;
            }
            cur = cur->next; // 遍历指针后移
        }
        // 处理剩余非空的链表(直接接在结果末尾)
        cur->next = l1 != nullptr ? l1 : l2;
        // 返回合并后链表的真实头节点(虚拟头节点的下一个节点)
        return dummy.next;
    }
};

// 辅助函数:创建链表(根据数组)
ListNode* createList(const vector<int>& nums) {
    ListNode dummy;
    ListNode* cur = &dummy;
    for (int num : nums) {
        cur->next = new ListNode(num);
        cur = cur->next;
    }
    return dummy.next;
}

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head != nullptr) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

// 测试主函数
int main() {
    // 示例1:输入[[1,4,5],[1,3,4],[2,6]]
    vector<vector<int>> numLists = {{1,4,5}, {1,3,4}, {2,6}};
    vector<ListNode*> lists;
    for (auto& nums : numLists) {
        lists.push_back(createList(nums));
    }
    Solution sol;
    ListNode* res = sol.mergeKLists(lists);
    printList(res); // 输出:1 1 2 3 4 4 5 6

    // 示例2:输入[]
    vector<ListNode*> lists2;
    ListNode* res2 = sol.mergeKLists(lists2);
    printList(res2); // 输出:(空)

    // 示例3:输入[[]]
    vector<ListNode*> lists3 = {createList({})};
    ListNode* res3 = sol.mergeKLists(lists3);
    printList(res3); // 输出:(空)

    return 0;
}

核心知识点解析

1. 分治思想的核心逻辑

将大问题拆分为规模更小、解决方式相同的子问题,解决子问题后合并结果,最终解决原问题:

  • 拆分:将 K 个链表拆分为「左 K/2 个」和「右 K/2 个」,直到子问题只剩 0 或 1 个链表;
  • 合并:合并两个有序链表是基础子问题,该操作时间复杂度为 O(N)(N 是两个链表的节点总数)。
2. 虚拟头节点(Dummy Node)的作用

合并两个链表时,使用虚拟头节点(一个无实际值的临时节点),可以 ** 避免单独处理「结果链表头节点为空」** 的边界情况,让代码更简洁:

  • cur 指针遍历虚拟头节点,最终结果的真实头节点是 dummy.next
  • 无论原始链表是否为空,该逻辑都能正常工作。
3. 合并两个升序链表的逻辑

这是本题的基础子问题,也是链表的经典操作,步骤如下:

  1. 初始化虚拟头节点和遍历指针 cur
  2. 循环比较两个链表的当前节点值,将较小的节点 接在 cur 后,同时移动对应链表的指针;
  3. 循环结束后,将剩余非空的链表直接接在结果末尾(剩余节点本身已有序);
  4. 返回虚拟头节点的下一个节点,即合并后链表的头节点。

复杂度分析

  • 时间复杂度 :O(NlogK)
    • 分治过程会将 K 个链表拆分为 logK 层;
    • 每一层的合并操作总共有 N 个节点参与(所有链表节点总数),每层时间复杂度为 O(N);
    • 总时间 = 层数 × 每层时间 = O(NlogK)。
  • 空间复杂度 :O(logK)
    • 递归调用栈的深度为分治的层数,即 logK;
    • 若使用迭代实现分治,空间复杂度可优化为 O(1)。

边界条件处理

代码已覆盖题目所有边界情况:

  1. 输入链表数组为空(lists = []):分治时 left > right,直接返回 nullptr
  2. 输入链表数组包含空链表(lists = [[]]):合并单个空链表,直接返回 nullptr
  3. 链表数组中部分链表为空:合并两个链表时,空链表会被直接跳过,不影响结果。

其他解法说明(堆排序)

除了归并排序,也可以用 ** 优先队列(小根堆)** 实现,核心思路是:

  1. 将所有链表的头节点加入小根堆(按节点值升序);
  2. 取出堆顶节点(当前最小值)加入结果,若该节点有下一个节点,则将下一个节点加入堆;
  3. 重复步骤 2,直到堆为空。

该方法时间复杂度同样为 O(NlogK),空间复杂度为 O(K)(堆的大小),适合不习惯递归的场景,但归并排序的空间复杂度更优,且代码更易扩展。

总结

  1. 合并 K 个升序链表的最优解是归并排序(分治),时间 O(NlogK),空间 O(logK);
  2. 核心是拆分问题 (分治)和解决基础子问题(合并两个升序链表);
  3. 虚拟头节点是链表操作的常用技巧,能大幅简化边界条件处理;
  4. 代码已覆盖所有题目边界,可直接运行测试。

3 小结

分治算法解题套路框架

分而治之是递归吗?

分治思想就是把一个问题分解成若干个子问题,然后分别解决这些子问题,最后合并子问题的解得到原问题的解,这种思想广泛存在于递归算法中。

本题具体细节没太明白,还是回头看看这个算法笔记,今天就这样写一下。

相关推荐
rainbow7242442 小时前
系统学习AI的标准化路径,分阶段学习更高效
大数据·人工智能·学习
高洁012 小时前
基于物理交互的具身智能决策框架设计
算法·机器学习·数据挖掘·transformer·知识图谱
REDcker2 小时前
TCP 拥塞控制算法详解:CUBIC、BBR 及传统算法
tcp/ip·算法·php
£漫步 云端彡2 小时前
Golang学习历程【第十三篇 并发入门:goroutine + channel 基础】
开发语言·学习·golang
偷吃的耗子2 小时前
[CNN算法理解]:二、卷积层(从生活实例到技术细节)
算法·cnn·生活
2301_790300962 小时前
C++与Docker集成开发
开发语言·c++·算法
TracyCoder1232 小时前
LeetCode Hot100(22/100)——141. 环形链表
算法·leetcode·链表
AutumnorLiuu2 小时前
C++并发编程学习(二)—— 线程所有权和管控
java·c++·学习
一起养小猫2 小时前
Flutter for OpenHarmony 进阶:递归算法与数学证明深度解析
算法·flutter