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:        这里为什么要检查一下链表是否存在呢?之前两个合并时并没有检查呢?因为这里定义时K个链表是不确定的,所以要进行检查,像之前那样两个或者几个已经确定的就不需要啦
            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))
这行代码将三元组 (head.val, i, head) 压入最小堆 pq 中。堆会根据节点值 head.val 自动排序,确保值最小的节点位于堆顶。若多个节点值相同,则通过索引 i 决定顺序(索引小的在前)。加I的另一个原因就是如果head.val相等,那么其就会去对照head的值,又因为节点的大小取决于内存地址所以不是很确定,所以要加i这也就是为什么用到了enumerate函数的原因,其可以获得I以及head,进一步通过head.val获取相关的值。
这里往里面放的是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 = node
            if node.next is not None:
                heapq.heappush(pq, (node.next.val, i, node.next))
            # p 指针不断前进
            p = p.next
文字步骤
先弹出堆顶元素:从堆中取出当前最小节点。
连接到结果链表:将该节点接到 p 指针后。
处理后继节点(可选):若当前节点有下一个节点,将其压入堆。
移动 p 指针:无论是否压入后继节点,p 都需要后移到新连接的节点。


        return 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 = val
        self.next = next

# 文件2:solution.py(实现算法)
from list_node import ListNode

class 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 = val
        self.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 -> A5
  list 1: B1 -> B3 -> B4
  list 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 = val
        self.next = next

    def __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.next
                heapq.heappush(pq, (next_node.val, i, next_node))
            
            # 移动结果链表的指针
            p = p.next
            
        return 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 = node
            p = node
        else: # 如果不是第一个节点
            p.next = node
            p = p.next
        

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


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值