合并 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

思路

我们可以想到一种最朴素的方法:用一个变量\textit{ans}来维护以及合并的链表,第i次循环把第i个链表和\textit{ans}合并,答案保存到\textit{ans}中。

代码

class Solution {
public:
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
        if ((!a) || (!b)) return a ? a : b;
        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
        while (aPtr && bPtr) {
            if (aPtr->val < bPtr->val) {
                tail->next = aPtr; aPtr = aPtr->next;
            } else {
                tail->next = bPtr; bPtr = bPtr->next;
            }
            tail = tail->next;
        }
        tail->next = (aPtr ? aPtr : bPtr);
        return head.next;
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode *ans = nullptr;
        for (size_t i = 0; i < lists.size(); ++i) {
            ans = mergeTwoLists(ans, lists[i]);
        }
        return ans;
    }
};


复杂度分析

时间复杂度:假设每个链表的最长长度是n。在第一次合并后,\textit{ans}的长度为n;第二次合并后,\textit{ans}的长度为2\times n,第i次合并后,\textit{ans}的长度为i\times n。第i次合并的时间代价是O(n + (i - 1) \times n) = O(i \times n),那么总的时间代价为O(\sum_{i = 1}^{k} (i \times n)) = O(\frac{(1 + k)\cdot k}{2} \times n) = O(k^2 n),故渐进时间复杂度为O(k^2 n)
空间复杂度:没有用到与kn规模相关的辅助空间,故渐进空间复杂度为O(1)

### 合并 k 个升序链表算法实现 #### 方法一:暴力求解 一种简单的方法是将所有链表中的节点值提取出来存入一个数组中,对该数组进行排序后再将其转换回链表形式。这种方法的时间复杂度主要由排序决定,即 \(O(N \log N)\),其中 \(N\) 是所有链表中节点总数。 以下是基于 Python 的实现: ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def mergeKLists(lists): nodes = [] for l in lists: # 遍历每个链表 while l: # 提取当前链表的所有节点值 nodes.append(l.val) l = l.next if not nodes: # 如果没有任何节点,则返回 None return None # 对所有节点值进行排序 nodes.sort() # 构建新的有序链表 head = ListNode(nodes[0]) current = head for value in nodes[1:]: current.next = ListNode(value) current = current.next return head ``` 此方法虽然易于理解,但在性能上可能不如其他更优化的方式[^2]。 --- #### 方法二:优先队列(最小堆) 利用最小堆可以有效地解决该问题。每次从堆中取出最小的节点,并将其加入结果链表中。由于堆的操作时间复杂度为 \(O(\log K)\),而总共有 \(N\) 个节点需要处理,因此整体时间复杂度为 \(O(N \log K)\)。 下面是基于 Python 和 `heapq` 库的实现方式: ```python import heapq def mergeKLists(lists): min_heap = [] for i, node in enumerate(lists): # 初始化堆,放入每条链表的第一个节点 if node: heapq.heappush(min_heap, (node.val, i)) lists[i] = node.next # 移动指针到下一个节点 dummy = ListNode(0) # 创建虚拟头结点 curr = dummy while min_heap: val, idx = heapq.heappop(min_heap) # 取出堆中最小值及其对应的链表索引 curr.next = ListNode(val) # 添加到结果链表中 curr = curr.next if lists[idx]: # 若对应链表还有剩余节点,则继续加入堆 heapq.heappush(min_heap, (lists[idx].val, idx)) lists[idx] = lists[idx].next # 更新链表指针 return dummy.next ``` 通过使用最小堆,可以在每次操作时快速找到当前未合并部分的最小值[^2]。 --- #### 方法三:分治法 另一种高效的解决方案是采用分治策略。两两合并链表直到最终只剩下一个完整的链表为止。这种做法能够减少不必要的重复比较次数,总体时间复杂度同样为 \(O(N \log K)\)。 以下是 C++ 实现版本: ```cpp #include <vector> using namespace std; struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} }; ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { // 辅助函数:合并两个有序链表 ListNode dummy(0); ListNode* tail = &dummy; while (list1 && list2) { if (list1->val < list2->val) { tail->next = list1; list1 = list1->next; } else { tail->next = list2; list2 = list2->next; } tail = tail->next; } tail->next = list1 ? list1 : list2; return dummy.next; } ListNode* merge(vector<ListNode*>& lists, int left, int right) { // 分治核心逻辑 if (left == right) return lists[left]; if (left > right) return nullptr; int mid = (left + right) / 2; ListNode* l1 = merge(lists, left, mid); // 左半边递归合并 ListNode* l2 = merge(lists, mid + 1, right); // 右半边递归合并 return mergeTwoLists(l1, l2); // 合并左右子结果 } ListNode* mergeKLists(vector<ListNode*>& lists) { return merge(lists, 0, static_cast<int>(lists.size()) - 1); } ``` 分治法的核心在于逐步缩小问题规模并通过多次调用辅助函数完成整个过程[^1]。 --- ### 性能对比分析 | **方法** | **时间复杂度** | **空间复杂度** | |------------------|--------------------|-------------------| | 暴力求解 | \(O(N \log N)\) | \(O(N)\) | | 最小堆 | \(O(N \log K)\) | \(O(K)\) | | 分治法 | \(O(N \log K)\) | \(O(\log K)\) | 以上三种方法各有优劣,具体选择取决于实际应用场景以及对时间和内存资源的要求[^4]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Wanderer001

ROIAlign原理

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

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

打赏作者

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

抵扣说明:

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

余额充值