文章目录
-
- [多链表合并:`mergeKLists` 函数详细解读与实现](#多链表合并:
mergeKLists
函数详细解读与实现) - 代码实现
- 代码分析
-
- [1. `Merge` 函数:合并两个有序链表](#1.
Merge
函数:合并两个有序链表) - [2. `mergeKLists` 函数:合并多个链表](#2.
mergeKLists
函数:合并多个链表) - [3. 辅助函数 `printList`](#3. 辅助函数
printList
)
- [1. `Merge` 函数:合并两个有序链表](#1.
- 时间复杂度与空间复杂度
- 总结
- 优先队列(堆)方法:合并K个有序链表
- 扩展思考
- 总结
- [多链表合并:`mergeKLists` 函数详细解读与实现](#多链表合并:
多链表合并:mergeKLists
函数详细解读与实现
问题描述
给定一个包含 k
个有序链表的数组 lists
,任务是将这些链表合并成一个有序链表。两两合并链表的方法可以解决这个问题,但我们需要优化代码,使得它更加高效。
输入与输出
输入:
lists
:一个包含k
个链表的数组,每个链表的节点值按升序排列。listsLen
:数组lists
的长度,即链表的数量。
输出:
- 返回一个合并后的有序链表。
示例
示例 1:
输入:
c
lists = [{1, 4, 5}, {1, 3, 4}, {2, 6}]
输出:
c
{1, 1, 2, 3, 4, 4, 5, 6}
示例 2:
输入:
c
lists = [{}]
输出:
c
{}
示例 3:
输入:
c
lists = [NULL]
输出:
c
NULL
思路
对于 k
个链表的合并,可以使用两种常见方法:
- 两两合并 :逐个将链表合并,最终合并成一个大链表。时间复杂度为
O(k * n)
,其中k
是链表的数量,n
是每个链表的平均长度。 - 优先队列(堆) :使用最小堆(优先队列)来合并链表。每次从堆中取出最小节点进行合并,最终合并为一个有序链表。该方法的时间复杂度为
O(n * log k)
,其中k
是链表的数量,n
是链表的总节点数。
在这个问题中,我们将采用 两两合并 方法,逐个合并链表,直到最终合并成一个大链表。
代码实现
c
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构体
struct ListNode {
int val;
struct ListNode *next;
};
/**
* 合并两个有序链表
*/
struct ListNode* Merge(struct ListNode* pHead1, struct ListNode* pHead2) {
if (pHead1 == NULL) return pHead2;
if (pHead2 == NULL) return pHead1;
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* cur = dummy;
while (pHead1 != NULL || pHead2 != NULL) {
if (pHead1 == NULL || (pHead2 != NULL && pHead1->val > pHead2->val)) {
cur->next = pHead2;
pHead2 = pHead2->next;
} else {
cur->next = pHead1;
pHead1 = pHead1->next;
}
cur = cur->next;
}
return dummy->next;
}
/**
* 合并k个链表
*/
struct ListNode* mergeKLists(struct ListNode** lists, int listsLen) {
if (listsLen == 0) return NULL;
struct ListNode* result = lists[0];
for (int i = 1; i < listsLen; i++) {
result = Merge(result, lists[i]);
}
return result;
}
// 辅助函数:打印链表
void printList(struct ListNode* head) {
while (head != NULL) {
printf("%d -> ", head->val);
head = head->next;
}
printf("NULL\n");
}
int main() {
struct ListNode* pHead1 = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead1->val = 1;
pHead1->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead1->next->val = 4;
pHead1->next->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead1->next->next->val = 5;
pHead1->next->next->next = NULL;
struct ListNode* pHead2 = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead2->val = 1;
pHead2->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead2->next->val = 3;
pHead2->next->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead2->next->next->val = 4;
pHead2->next->next->next = NULL;
struct ListNode* pHead3 = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead3->val = 2;
pHead3->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead3->next->val = 6;
pHead3->next->next = NULL;
struct ListNode* lists[] = { pHead1, pHead2, pHead3 };
int listsLen = 3;
struct ListNode* result = mergeKLists(lists, listsLen);
printList(result);
return 0;
}
代码分析
1. Merge
函数:合并两个有序链表
Merge
函数的核心思想是使用双指针遍历两个链表,逐个比较节点并将较小的节点添加到合并链表中。我们使用一个虚拟头节点dummy
,并通过cur
指针来跟踪当前合并链表的尾部。
2. mergeKLists
函数:合并多个链表
- 我们从第一个链表开始,然后依次将后续的链表与当前的合并结果进行合并。通过
Merge
函数逐步合并链表,直到合并完成。
3. 辅助函数 printList
printList
函数用于打印链表,帮助我们查看合并结果。
时间复杂度与空间复杂度
时间复杂度:
Merge
函数的时间复杂度为O(n)
,其中n
是两个链表的节点数。mergeKLists
函数的时间复杂度为O(k * n)
,其中k
是链表的数量,n
是每个链表的平均长度。
空间复杂度:
Merge
函数使用了常量级的额外空间,因此空间复杂度为O(1)
。mergeKLists
也只使用了常量级的额外空间,空间复杂度为O(1)
,除非你计入输入链表本身的空间。
总结
通过逐个合并链表的方法,我们成功将多个链表合并为一个有序链表。时间复杂度为 O(k * n)
,对于大多数实际问题而言,已经足够高效。如果链表的数量较大,也可以通过使用优先队列来优化算法。
优先队列(堆)方法:合并K个有序链表
优先队列(最小堆)方法比逐个合并的方法更高效,尤其是在链表数量 k
很大的时候。通过最小堆的特性,我们可以在每一步从 k
个链表的头节点中选出最小的节点,进行合并。
思路
- 最小堆 :每次从
k
个链表中取出最小的节点(堆顶元素),并将该节点的下一个节点加入堆中。 - 堆的作用 :堆是一种完全二叉树,支持插入和删除最小元素(堆顶元素)的操作。通过堆,我们能够在
O(log k)
的时间内获得当前最小的节点,这样合并所有链表的时间复杂度可以降为O(n log k)
,其中n
是链表的总节点数,k
是链表的数量。
代码实现
c
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构体
struct ListNode {
int val;
struct ListNode *next;
};
// 定义最小堆的节点结构体,用于优先队列
struct HeapNode {
struct ListNode* listNode;
int idx; // 用于标识链表的索引
};
// 最小堆的比较函数
int compare(struct HeapNode* a, struct HeapNode* b) {
return a->listNode->val - b->listNode->val; // 比较节点的值
}
// 最小堆插入操作
void heapInsert(struct HeapNode* heap, int* heapSize, struct HeapNode node) {
heap[*heapSize] = node;
int idx = *heapSize;
while (idx
> 0 && compare(&heap[idx], &heap[(idx - 1) / 2]) < 0) {
struct HeapNode tmp = heap[idx];
heap[idx] = heap[(idx - 1) / 2];
heap[(idx - 1) / 2] = tmp;
idx = (idx - 1) / 2;
}
(*heapSize)++;
}
// 最小堆删除堆顶操作
void heapPop(struct HeapNode* heap, int* heapSize) {
(*heapSize)--;
heap[0] = heap[*heapSize];
int idx = 0;
while (2 * idx + 1 < *heapSize) {
int left = 2 * idx + 1;
int right = 2 * idx + 2;
int smallest = idx;
if (left < *heapSize && compare(&heap[left], &heap[smallest]) < 0) {
smallest = left;
}
if (right < *heapSize && compare(&heap[right], &heap[smallest]) < 0) {
smallest = right;
}
if (smallest == idx) break;
struct HeapNode tmp = heap[idx];
heap[idx] = heap[smallest];
heap[smallest] = tmp;
idx = smallest;
}
}
// 合并K个链表
struct ListNode* mergeKLists(struct ListNode** lists, int listsLen) {
struct HeapNode heap[listsLen];
int heapSize = 0;
for (int i = 0; i < listsLen; i++) {
if (lists[i] != NULL) {
struct HeapNode node = {lists[i], i};
heapInsert(heap, &heapSize, node);
lists[i] = lists[i]->next;
}
}
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* cur = dummy;
while (heapSize > 0) {
struct HeapNode minNode = heap[0];
cur->next = minNode.listNode;
cur = cur->next;
if (lists[minNode.idx] != NULL) {
struct HeapNode newNode = {lists[minNode.idx], minNode.idx};
heap[0] = newNode;
lists[minNode.idx] = lists[minNode.idx]->next;
heapPop(heap, &heapSize);
}
}
return dummy->next;
}
// 辅助函数:打印链表
void printList(struct ListNode* head) {
while (head != NULL) {
printf("%d -> ", head->val);
head = head->next;
}
printf("NULL\n");
}
int main() {
struct ListNode* pHead1 = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead1->val = 1;
pHead1->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead1->next->val = 4;
pHead1->next->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead1->next->next->val = 5;
pHead1->next->next->next = NULL;
struct ListNode* pHead2 = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead2->val = 1;
pHead2->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead2->next->val = 3;
pHead2->next->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead2->next->next->val = 4;
pHead2->next->next->next = NULL;
struct ListNode* pHead3 = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead3->val = 2;
pHead3->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pHead3->next->val = 6;
pHead3->next->next = NULL;
struct ListNode* lists[] = { pHead1, pHead2, pHead3 };
int listsLen = 3;
struct ListNode* result = mergeKLists(lists, listsLen);
printList(result);
return 0;
}
代码解析
接下来,我们详细解析基于优先队列(最小堆)的方法来合并 k
个有序链表。
1. 最小堆实现:
为了在每次迭代中从 k
个链表中取出最小的节点,我们使用了一个最小堆(或优先队列)。堆是一种数据结构,可以有效地进行插入和删除操作。堆顶元素是堆中最小的元素,在本问题中即是当前 k
个链表头部最小的节点。
-
HeapNode
结构体:为了将链表节点和其来源的链表索引关联起来,我们定义了一个新的结构体
HeapNode
。每个HeapNode
包含链表节点listNode
和该节点所在链表的索引idx
。 -
堆操作:
heapInsert
: 将一个新的节点插入堆中。我们首先将新节点添加到堆的末尾,然后通过向上调整堆(即不断与父节点交换)来维持最小堆的性质。heapPop
: 移除堆顶元素,并通过向下调整堆(即不断与子节点交换)来维持最小堆的性质。
2. 合并K个链表的过程:
-
初始化堆:
我们首先遍历所有链表,将每个链表的头节点插入堆中。此时堆中存储的是每个链表的第一个元素。
-
合并过程:
我们反复从堆中弹出最小节点,并将其连接到结果链表的末尾。每当我们弹出一个节点后,如果该节点所属的链表还有后继节点,就将该后继节点插入堆中。这个过程一直持续,直到堆为空。
3. 边界条件:
- 空链表: 如果某个链表为空,我们不会将其插入堆中,避免无用的操作。
- 链表完全合并: 当堆为空时,所有链表中的节点都已经合并完毕。
4. 时间复杂度:
- 堆操作的复杂度: 每次堆操作(插入或删除)需要
O(log k)
时间,其中k
是链表的数量。 - 总操作次数: 假设所有链表总共有
n
个节点,那么每个节点都需要被插入堆一次,并且需要log k
的时间来进行堆操作。因此,总时间复杂度为O(n log k)
。
5. 空间复杂度:
- 堆的空间复杂度: 我们在合并过程中只需要存储
k
个链表的头节点,因此堆的空间复杂度为O(k)
。 - 结果链表的空间复杂度: 最终结果链表的空间复杂度为
O(n)
,因为它包含了所有节点。
总结
这种基于最小堆的方法通过高效的堆操作来实现合并 k
个有序链表,能够将时间复杂度降低到 O(n log k)
,在链表数量 k
很大时,性能优于逐一合并的方法。该方法尤其适用于链表数量较多且每个链表长度较短的场景。
扩展思考
-
多线程优化:
如果在多核机器上运行,可以考虑将合并操作并行化。例如,可以先将链表分组进行合并,合并完成后再合并这些子链表。这样可以有效利用多核处理器的优势。
-
内存优化:
在内存受限的环境中,可以考虑在合并时直接修改链表节点的指针而不是创建新的链表节点,减少额外的内存开销。
-
内存池:
为了避免频繁的内存分配和释放,可以考虑实现一个内存池,特别是在需要处理大量数据时,能够减少内存管理的开销。
总结
无论是两两合并法,还是基于最小堆的优化方法,都能够有效地解决合并 k
个有序链表的问题。在链表数量较大时,优先队列方法的优势更加明显,尤其在时间复杂度和空间复杂度方面的表现更为优秀。在实际应用中,可以根据链表的数量、节点的大小以及系统的资源限制来选择合适的方法。