1 题目
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 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.length0 <= k <= 10^40 <= lists[i].length <= 500-10^4 <= lists[i][j] <= 10^4lists[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 的边界条件。
核心思路:分治 + 合并两个升序链表
- 分治 :将 K 个链表不断拆分为左右两部分,直到每部分只剩 0 个或 1 个链表(递归终止条件);
- 合并:从最小的子问题开始,依次合并两个升序链表,最终得到整体有序的合并链表。
完整 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. 合并两个升序链表的逻辑
这是本题的基础子问题,也是链表的经典操作,步骤如下:
- 初始化虚拟头节点和遍历指针
cur; - 循环比较两个链表的当前节点值,将较小的节点 接在
cur后,同时移动对应链表的指针; - 循环结束后,将剩余非空的链表直接接在结果末尾(剩余节点本身已有序);
- 返回虚拟头节点的下一个节点,即合并后链表的头节点。
复杂度分析
- 时间复杂度 :O(NlogK)
- 分治过程会将 K 个链表拆分为 logK 层;
- 每一层的合并操作总共有 N 个节点参与(所有链表节点总数),每层时间复杂度为 O(N);
- 总时间 = 层数 × 每层时间 = O(NlogK)。
- 空间复杂度 :O(logK)
- 递归调用栈的深度为分治的层数,即 logK;
- 若使用迭代实现分治,空间复杂度可优化为 O(1)。
边界条件处理
代码已覆盖题目所有边界情况:
- 输入链表数组为空(
lists = []):分治时left > right,直接返回nullptr; - 输入链表数组包含空链表(
lists = [[]]):合并单个空链表,直接返回nullptr; - 链表数组中部分链表为空:合并两个链表时,空链表会被直接跳过,不影响结果。
其他解法说明(堆排序)
除了归并排序,也可以用 ** 优先队列(小根堆)** 实现,核心思路是:
- 将所有链表的头节点加入小根堆(按节点值升序);
- 取出堆顶节点(当前最小值)加入结果,若该节点有下一个节点,则将下一个节点加入堆;
- 重复步骤 2,直到堆为空。
该方法时间复杂度同样为 O(NlogK),空间复杂度为 O(K)(堆的大小),适合不习惯递归的场景,但归并排序的空间复杂度更优,且代码更易扩展。
总结
- 合并 K 个升序链表的最优解是归并排序(分治),时间 O(NlogK),空间 O(logK);
- 核心是拆分问题 (分治)和解决基础子问题(合并两个升序链表);
- 虚拟头节点是链表操作的常用技巧,能大幅简化边界条件处理;
- 代码已覆盖所有题目边界,可直接运行测试。
3 小结
分而治之是递归吗?
分治思想就是把一个问题分解成若干个子问题,然后分别解决这些子问题,最后合并子问题的解得到原问题的解,这种思想广泛存在于递归算法中。
本题具体细节没太明白,还是回头看看这个算法笔记,今天就这样写一下。