当前位置: 首页 > news >正文

LeetCode中K个链表的链接的解法

import heapq   首先呢我们导入一个模块heapq——堆模块。 英文中heap是堆的意思,而q是队列(queue)的缩写,这个模块专门用来实现优先队列,核心数据结构是堆(Heap),因此用heap命名。
为什么会用到这个模块呢?因为我们的任务是在K个链表中不断的获取最小值,然后进行排序。
所以合并k个有序链表要用堆,最终的链表也是有序的,所以第一个值最小,又因为是K个所以要不断的进行获取,自然而然想到了heapq在这里我们引申一下堆的本质,堆是一种特殊的树结构,其又分为最大堆与最小堆。最大堆的父节点的值始终大于子节点,堆顶是最大值。创建链表节点的车间:
class ListNode:def __init__(self, val=0, next=None):   self代表节点self.val = val  初始化节点值  self.next = next   初始化节点指针
为什么要创建一个链表创建的模板?因为之后我们要创建一个新的链表从而链接K个节点。因为要进行排序,所以要对节点之间进行比较,但是在python中,其并不知道如何对节点进行比较,所以使用了__lt___重载比较符的函数,将节点之间的比较转换成了节点之间值的比较就是这个函数的作用。而这也符合我们的初衷。# 重载比较运算符,方便将 ListNode 加入最小堆def __lt__(self, other):    __lt__是__less than__即“小于”运算符的重载函数return self.val < other.val说解题思路:
class Solution:def mergeKLists(self, lists):   定义一个链表集合函数,参数是链表在class Solution中可以视self不存在if not lists:return None   如果不存在链表返回什么都没有# 虚拟头结点dummy = ListNode(-1)   创建一个虚拟头节点。作为牌子,方便后续节点的链接p = dummy   给其一个指针。让p指针在虚拟节点那里# 优先级队列,最小堆pq = []   创建一个列表,作为优先级队列。存储那些较小的节点。# 将 k 个链表的头结点加入最小堆for i, head in enumerate(lists):if head is not None:heapq.heappush(pq, (head.val, i, head))
这里往里面放的是k个链表的头节点,因为K个链表是按照顺序排列的。heapq.heappush是一个heapq模块中的函数,其作用是智能排序!!!,将元素插入堆中并且自动调整堆元素,确保堆顶是最小元素。而后面是其对应的三个参数pq:堆(优先队列),初始为空。
head.val:节点值
i链表索引
head 节点本身当pq的第一个节点(比如是链表1的)被push入那个新链表之后,链表1的第二大元素就会变成头节点进入pq,其后也是依次的,之前的头节点不断从pq进入新的那个链表,原先的旧的头节点不断变化,最终k个链表全部变成0然后新的链表排序成功。while pq:# 获取最小节点,接到结果链表中val, i, node = heapq.heappop(pq)p.next = nodeif node.next is not None:heapq.heappush(pq, (node.next.val, i, node.next))# p 指针不断前进p = p.nextreturn dummy.next那为什么ListNode在class solution之外而不在其之内呢?因为class soulution好比是生产车间组装零件的地方而listnode是制造零件的地方所以两个必须分开。代码中没有出现ListNode定义,是因为它通常作为前置条件或通用模块存在,无需在每个具体问题中重复定义。就像拼图游戏不需要每次都重新设计零件形状一样,算法实现只需关注具体的逻辑,而不是基础数据结构的定义。
2. 代码复用原则
ListNode是链表问题的通用结构,在多个题目中都会用到。
如果每个题目都重复定义ListNode,会导致代码冗余。
类比:拼图游戏中,不需要每次玩都重新定义零件的形状。
3. 模块化设计思想
在实际编程中,我们通常将数据结构(如ListNode)和算法逻辑(如Solution)分开:
python
运行
# 文件1:list_node.py(定义数据结构)
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = next# 文件2:solution.py(实现算法)
from list_node import ListNodeclass Solution:def partition(self, head: ListNode, x: int) -> ListNode:# ... 算法逻辑 ...

标题:【算法精粹】巧用“堆”解决 Top K 问题:合并K个有序链表

你好,算法探索者!

在面试和实际开发中,我们经常遇到需要从多个有序集合中找出“最优”元素的问题。今天,我们将深入探讨一个经典的 LeetCode 难题——合并K个有序链表

这不仅仅是一道题,更是一种思想的体现。通过它,你将彻底掌握一种高效处理“Top K”问题的利器——优先队列(堆)

一、问题引入:为何此题与“堆”结缘?

题目描述: 给你一个链表数组 lists,每个链表都已经按照升序排列。请你将所有链表合并到一个升序链表中,并返回合并后链表的头节点。

直觉分析:
我们的目标是构建一个全新的、有序的链表。这意味着,新链表的第一个节点,必须是所有 K 个链表头节点中的最小值

取走这个最小节点后,它的下一个节点就成了其所在链表的“新头节点”。接下来,我们又要从 K 个“新头节点”中找到最小值...如此循环往复。

这个过程的本质是什么?——在 K 个元素中,不断地找出最小值,并动态地加入新元素。

这正是最小堆(Min-Heap)的完美应用场景!heapq 模块在 Python 中就是最小堆的官方实现,它能以 O(log k) 的高效时间复杂度帮我们完成“找出最小值”和“插入新元素”的操作。

二、核心工具箱:我们的“兵器”与“零件”

在动手之前,我们先来熟悉一下构建解决方案所需要的核心组件。

1. 零件车间:ListNode 节点 (★★★☆☆)

重要性评级: ★★★☆☆ (基础且常用,但非本题核心难点)
一句话解释: 定义链表的基本单元——节点。

我们需要一个标准化的“零件”来构建链表。ListNode 就是我们的零件模板,它包含两个信息:

  1. val: 节点存储的值。

  2. next: 指向下一个节点的指针。

      # 零件定义:链表节点
class ListNode:def __init__(self, val=0, next=None):self.val = val   # 节点值self.next = next # 指向下一个节点的指针
2. 神奇魔法:让节点“可比较” (__lt__) (★★★★☆)

重要性评级: ★★★★☆ (理解对象比较机制,是正确使用堆的关键)
一句话解释: 教会 Python 如何比较两个 ListNode 节点的大小。

当我们试图将 ListNode 对象直接放入 heapq 中时,Python 会一头雾水,因为它不知道该如何比较两个自定义对象的大小。是比内存地址?还是比别的什么?

我们需要明确告诉它:比较节点,就是比较节点的 val 值

通过重载“小于”(Less Than)运算符 __lt__,我们就能赋予 ListNode 可比较的能力。

      class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = next# 重载小于运算符,让堆(heapq)知道如何比较节点def __lt__(self, other):return self.val < other.val

3. 核心引擎:heapq 模块 (★★★★★)

重要性评级: ★★★★★ (本题的灵魂,面试必考)
一句话解释: Python 内置的最小堆,能自动维护一个“最小值”在顶部的队列。

heapq (heap queue) 是我们解决此问题的核心武器。它主要有两个操作:

  • heapq.heappush(heap, item): 将 item 压入堆 heap 中,并自动调整结构,确保堆顶永远是最小元素。

  • heapq.heappop(heap): 从堆 heap 中弹出并返回最小的元素,然后自动将下一个最小的元素调整到堆顶。

这“一进一出”之间,heapq 完美地为我们解决了“动态获取最小值”的需求。

三、解题思路:三步走的“合并大法”

现在,万事俱备,让我们来组装最终的解决方案。

整体思路: 创建一个最小堆,先把 K 个链表的头节点都放进去。然后,不断从堆中取出最小的节点,连接到结果链表上,并将其后继节点(如果存在)再放回堆中,直到堆为空。

第 1 步:初始化
  1. 虚拟头节点 (Dummy Node): 创建一个 dummy 节点。这是一个非常实用的技巧,它可以极大简化链表头部的操作,避免复杂的边界条件判断。我们用一个指针 p 指向它,p 将作为我们构建新链表的“画笔”。

  2. 优先队列 (Min-Heap): 创建一个空列表 pq,它将作为我们的最小堆。

      # 虚拟头结点,方便操作
dummy = ListNode(-1)
p = dummy# 优先队列(最小堆),用于存放各链表的当前节点
pq = []
第 2 步:播种堆(Seeding the Heap)

遍历 K 个链表,将每个链表的头节点(如果非空)加入到优先队列 pq 中。

注意! 为了避免 ListNode 在值相等时进行不必要的对象比较(在 Python 3 中可能引发 TypeError),也为了后续能追溯节点来源(虽然本题不需要),一个更健壮的做法是存入元组 (value, index, node)。heapq 会自动根据元组的第一个元素 value 来排序。

      # 将 k 个链表的头结点加入最小堆
# 使用 enumerate 获取索引 i,作为 tie-breaker(当节点值相同时的比较依据)
for i, head in enumerate(lists):if head:heapq.heappush(pq, (head.val, i, head))

图解此刻的状态:

      pq (Min-Heap)[ (1, 0, nodeA1),(1, 1, nodeB1),(2, 2, nodeC1) ]^|-- 堆会根据元组的第一个元素(val)自动排序lists:list 0: A1 -> A4 -> A5list 1: B1 -> B3 -> B4list 2: C2 -> C6

第 3 步:循环构建链表

这是算法的核心循环。只要堆不为空,就说明还有节点待处理。

  1. 取出最小节点: heapq.heappop(pq) 弹出堆顶的元组,我们得到了全局最小的节点。

  2. 链接到结果: 将这个最小节点链接到 p 指针的后面。

  3. 指针后移: 将 p 指针移动到刚刚链接上的新节点,为下一次链接做准备。

  4. 补充新节点: 如果被弹出的节点还有下一个节点 node.next,则将其加入堆中,维持堆中始终有 K 个(或更少)候选节点。

      while pq:# 1. 取出堆中最小的节点 (注意:我们只关心 node 本身)val, i, node = heapq.heappop(pq)# 2. 将节点链接到结果链表p.next = node# 3. 补充新节点到堆中if node.next:next_node = node.next# 为了健壮性,再次使用元组heapq.heappush(pq, (next_node.val, i, next_node))# 4. 移动指针p = p.next

最终,返回 dummy.next,它才是我们真正想要的合并后链表的头节点。

四、完整代码与总结

      import heapq# ===============================================
# Part 1: 数据结构定义 (通常在独立文件或公共模块中)
# ===============================================
class ListNode:"""链表节点定义"""def __init__(self, val=0, next=None):self.val = valself.next = nextdef __lt__(self, other):"""重载小于运算符,使 ListNode 对象可以直接被 heapq 比较"""return self.val < other.val# ===============================================
# Part 2: 算法实现
# ===============================================
class Solution:def mergeKLists(self, lists: list[ListNode]) -> ListNode:"""使用最小堆合并 k 个有序链表"""if not lists:return None# 虚拟头结点,作为结果链表的起始标志dummy = ListNode(-1)p = dummy# 优先队列(最小堆)pq = []# 将 k 个链表的头结点加入最小堆# 存入元组 (val, index, node) 来避免节点值相同时的比较问题for i, head in enumerate(lists):if head:heapq.heappush(pq, (head.val, i, head))# 当堆不为空时,循环处理while pq:# 弹出当前所有节点中的最小者val, i, current_node = heapq.heappop(pq)# 将其链接到结果链表p.next = current_node# 如果该节点还有后继者,将其推入堆中if current_node.next:next_node = current_node.nextheapq.heappush(pq, (next_node.val, i, next_node))# 移动结果链表的指针p = p.nextreturn dummy.next

为什么 ListNode 的定义在 Solution 类之外?

这体现了模块化关注点分离的设计思想:

  • ListNode (数据结构): 像一个零件,它定义了“什么是链表”。这是一个通用的概念,可以在任何需要链表的地方复用。

  • Solution (算法逻辑): 像一个装配车间,它定义了“如何处理链表”。它的职责是实现算法,而不是定义零件本身。

将两者分开,代码更清晰、更易于维护和复用。

五、随堂测验 (检验你的掌握程度)

问题1: 在这个算法中,为什么要使用堆(Priority Queue),而不是简单地将所有链表的所有节点收集到一个大数组里然后排序?

答案: 效率和空间。

  1. 时间效率:假设总共有 N 个节点,K 个链表。

    • 数组排序法:需要 O(N) 的空间来存储所有节点,排序时间为 O(N log N)。

    • 堆方法:堆的大小最多为 K。每次 push 和 pop 操作的时间复杂度是 O(log K)。总共有 N 个节点需要处理,所以总时间复杂度是 O(N log K)。当 K 远小于 N 时,O(N log K) 远优于 O(N log N)。

  2. 空间效率:堆方法只需要 O(K) 的额外空间来维护堆,而数组排序法需要 O(N) 的空间。在处理海量数据时,这个差距是巨大的。

问题2: 如果我们从 ListNode 类中移除了 __lt__ 方法,但在 heappush 时仍然使用 (head.val, i, head) 这样的元组,代码还能正常工作吗?为什么?

答案: 可以。因为 heapq 在比较元组时,会从左到右依次比较元组中的元素。

  1. 它首先比较 head.val。

  2. 如果 head.val 相同,它会接着比较第二个元素 i(链表的索引)。

  3. 由于每个链表的索引 i 是唯一的,所以比较总会在第二步或第一步就分出胜负,永远不会轮到去比较第三个元素 head (即 ListNode 对象本身)。因此,即使 ListNode 没有 __lt__ 方法,代码在这种元组实现下也能安全运行。这也是推荐使用元组的原因——它更健壮。

问题3: 算法中 dummy 虚拟头节点的作用是什么?如果不用它,代码会变得怎样?

答案: dummy 节点主要用于简化头节点的处理逻辑

  • 使用 dummy:我们可以统一地使用 p.next = ... 来链接每一个节点,包括第一个节点。代码逻辑非常一致。

  • 不使用 dummy:我们需要一个额外的变量来保存最终的头节点(比如 head = None),并且在循环中需要加入 if/else 判断:

     
          # 不用 dummy 的伪代码
    head = None
    p = None
    while pq:node = ...if head is None: # 如果是第一个节点head = nodep = nodeelse: # 如果不是第一个节点p.next = nodep = p.next

    可见,代码变得更复杂,容易出错。dummy 节点是一个优雅的编程模式。


希望这篇精讲能帮助你彻底征服这道题目,更重要的是,让你对“堆”这种数据结构的应用有了更深的理解!

相关文章:

  • 从本地到云端:通过ToolJet和cpolar构建远程开发环境实践过程
  • 操作系统 第九章 部分
  • 详解HarmonyOS NEXT仓颉开发语言中的全局弹窗
  • 2024计算机保研--哈工大、中山、国防科大(二)
  • 前端高频面试题汇总
  • 【入门级-基础知识与编程环境:3、计算机网络与Internet的基本概念】
  • Flask框架index.html里引用的本地的js和css或者图片
  • RK3576 Yolo 部署
  • PyTorch实战(12)——StyleGAN详解与实现
  • 七八章习题测试
  • 从传统Cube到现代化指标体系:物化视图驱动的指标平台升级之路
  • 词编码模型怎么进行训练的,输出输入是什么,标签是什么
  • 计算机网络:(六)超详细讲解数据链路层 (附带图谱表格更好对比理解)
  • 3D模式格式转换工具HOOPS Exchange如何将3D PDF转换为STEP格式?
  • Java面试题027:一文深入了解数据库Redis(3)
  • 新手学习阿里云AI本地大模型搭建
  • 利用mold加快rust程序构建
  • 苹果芯片macOS安装版Homebrew(亲测)
  • mac隐藏文件现身快捷键
  • 全局配置Axios后的api使用指南
  • 北京做网站/建站软件可以不通过网络建设吗
  • 做商城网站需要备案吗/美国婚恋网站排名
  • 平泉县住房和城乡建设局网站/seo是什么单位
  • 培训学校类网站建设方案/咨询网络服务商
  • 网站设计注意事项/迅雷磁力
  • 网站优化怎么做效果才好/google关键词排名优化