使用最小堆合并 K 个排序链表

17 Mar 2025 | 6 分钟阅读

高效地合并K个已排序的链表是计算机科学和软件开发中的一个典型挑战。这个任务涉及将各种已经按升序排序的链表合并成一个单一的已排序链表。使用最小堆(Min Heap)数据结构是处理此问题的一种更有效的方法。在本文中,我们将探讨使用最小堆合并K个已排序链表的技术,以及它的实现和时间及空间复杂度。

合并K个已排序链表:简介

当处理K个已排序链表时,一种简单但耗时的解决方案是成对合并链表,直到它们全部合并成一个单一链表。然而,这种方法的时间复杂度为O(N^2),其中N表示所有链表中节点的总数。

  • 相反,最小堆方法提供了一个更高效的解决方案,将时间复杂度降低到O(N log K),其中N表示节点总数,K表示链表的数量。

理解最小堆

最小堆是一种完全二叉树,其中每个节点的值都小于或等于其子节点的值。它通常用数组表示,这使得快速插入和提取最小值成为可能。在这种情况下,K个链表的头节点将被存储在最小堆中。

算法:使用最小堆合并K个已排序链表

输入: K个已排序的链表。

输出: 一个包含所有K个输入链表元素的单一已排序链表。

步骤:

步骤1: 初始化一个空的最小堆。

  • 最小堆将存储由节点值和节点引用组成的元组。
  • 节点将根据它们的值进行比较。

步骤2: 遍历所有K个链表,并将它们的头节点插入最小堆。

  • 对于输入集中的每个链表
  • 如果头节点存在(即,它不是None),则将一个元组(node.val, node)插入最小堆。
  • 移动到链表中的下一个节点。

步骤3: 初始化一个空的已排序链表,用于存储合并的输出。

  • 它将用于创建最终的已排序链表。

步骤4: 当最小堆不为空时

  • 从最小堆中提取最小节点。
  • 将提取的节点附加到结果链表中。
  • 将提取节点的指针移动到其下一个节点,如果存在,则将其重新插入最小堆。

步骤5: 返回结果链表的头节点。

  • 这将是合并的已排序链表的头节点。

示例

我们来看三个已排序的链表

链表1 1 -> 4 -> 5

链表2 1 -> 3 -> 4

链表3 2 -> 6

我们将应用最小堆合并算法,将这些链表合并成一个单一的已排序列表。

步骤1: 初始化一个最小堆

我们首先创建一个空的最小堆。

步骤2: 填充最小堆

将三个链表的头节点插入最小堆。

步骤3: 合并过程

当最小堆不为空时

  1. 从最小堆中提取最小元素(来自链表1的1)。
  2. 将此最小元素(1)添加到结果链表中。
  3. 将链表1的下一个节点推入最小堆(即4)。

继续此过程

  1. 从链表2提取1。
  2. 将1添加到结果链表中。
  3. 将链表2的下一个节点推入最小堆(即3)。

  1. 从链表3提取2。
  2. 将2添加到结果链表中。
  3. 将链表3的下一个节点推入最小堆(即6)。

  1. 从链表2提取3。
  2. 将3添加到结果链表中。
  3. 将链表2的下一个节点推入最小堆(即4)。

  1. 从链表1提取4。
  2. 将4添加到结果链表中。
  3. 将链表1的下一个节点推入最小堆(即5)。

  1. 从链表2提取4。
  2. 将4添加到结果链表中。
  3. 将链表2的下一个节点推入最小堆(即None)。

  1. 从链表1提取5。
  2. 将5添加到结果链表中。
  3. 将链表1的下一个节点推入最小堆(即None)。

  1. 从链表3提取6。
  2. 将6添加到结果链表中。
  3. 将链表3的下一个节点推入最小堆(即None)。

堆:(空)

合并这三个已排序链表后的结果链表将是:1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6

实施

输出

Merge K Sorted Linked Lists using Min Heap

说明

ListNode类

  • ListNode类代表链表中的一个节点。
  • 它包含两个属性:val(节点的值)和next(指向链表中下一个节点的引用)。

自定义比较方法

  • 此版本中的关键 addition 是ListNode类中的__lt__方法。
  • __lt__(小于)是Python中的一个特殊方法,它定义了自定义对象的<运算符的行为。
  • 通过实现__lt__,它使得ListNode实例可以根据它们的val属性进行比较。这对于使用heapq模块进行正确比较至关重要。

mergeKLists函数

  • mergeKLists函数接受一个已排序链表列表作为输入,并将它们合并成一个单一的已排序链表。
  • 它利用最小堆来高效地合并链表。
  • pushNode函数在节点存在时将其添加到最小堆。

createLinkedList函数

  • 此函数从值列表中创建一个链表。

示例输入和输出

  • list变量包含三个样本排序链表。
  • 调用mergeKLists函数与这些链表进行合并。
  • 然后遍历合并后的已排序链表以收集其值并显示为输出。

时间复杂度

构建堆

将所有K个链表的头节点插入最小堆需要O(K log K)的时间。

合并

  • 每个节点将只会被添加和移除一次。在任何时候,堆中节点的最大数量将是K。
  • 合并所有节点所需的总操作为O(N log K),其中N是所有链表中节点的总数。

总而言之

构建堆和合并链表的时间复杂度为O(N log K)。

空间复杂度

堆空间

最小堆所需的空间为O(K),因为在合并过程中,最多会有K个节点同时在堆中。

输出链表

合并后的链表需要额外的空间,相当于节点总数,即O(N)。

总而言之

考虑到堆和合并链表所需的空间,空间复杂度将为O(max(N, K))。

总结

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

空间复杂度: O(max(N, K))

结论

当合并K个已排序链表时,与替代方法相比,最小堆技术显著降低了时间复杂度。通过利用最小堆数据结构的强大功能,我们高效地以排序方式合并链表,这证明了在问题解决场景中选择正确的数据结构和方法的重要性。