【刷力扣】23. 合并 K 个升序链表(dummy节点技巧 + 分治思维 + 优先队列)

本文主要是介绍【刷力扣】23. 合并 K 个升序链表(dummy节点技巧 + 分治思维 + 优先队列),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 一、合并升序链表问题
  • 二、题目:[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/description/)
    • 1、掌握dummy节点的技巧
  • 三、题目:[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/description/)
    • 1、分治思维
      • 1.1 插曲
      • 1.2 [代码](https://leetcode.cn/problems/merge-k-sorted-lists/solutions/2811116/jiang-kge-sheng-xu-lian-biao-zhuan-cheng-yffa/)
      • 1.3 分析这种解法的时空复杂度
        • 1.3.1 时间复杂度
        • 1.3.2 空间复杂度
    • 2、优先队列
      • 2.1 PriorityQueue的使用
      • 2.2 本题代码
        • 2.2.1 进一步优化
      • 2.3 分析这种解法的时空复杂度
        • 2.3.1 时间复杂度
        • 2.3.2 空间复杂度

一、合并升序链表问题

  • 合并升序链表问题是链表专题的经典问题。
    • 我们需要掌握:dummy节点的技巧
  • 23. 合并 K 个升序链表在21. 合并两个有序链表基础上,还需要掌握如下技能:
    • (1)分治思维。我们将合并K个升序链表转化为多次合并2个升序链表。归并排序也用到了分治思维。
    • (2)优先队列(小根堆/大根堆)。维护一个序列的最小/大值。

二、题目:21. 合并两个有序链表

1、掌握dummy节点的技巧

  • 在创建新链表时,定义一个dummy节点,在如下代码中,res便是dummy节点,因此,最后答案是:return res.next;
/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {if (list1 == null) {return list2;}if (list2 == null) {return list1;}ListNode p1 = list1, p2 = list2, res = new ListNode(), p = res;while (p1 != null && p2 != null) {if (p1.val <= p2.val) {p.next = p1;p1 = p1.next;} else {p.next = p2;p2 = p2.next;}p = p.next;}if (p1 == null) {p.next = p2;}if (p2 == null) {p.next = p1;}return res.next;}
}

三、题目:23. 合并 K 个升序链表

1、分治思维

1.1 插曲

  • 看到这道题,首先想到的是合并2个升序链表。p1指向链表list1,p2指向链表list2。关键步骤是:
if (p1.val <= p2.val) {...
} else {...
}
  • 很显然,k个升序链表需要想其他办法去求最小值对应的节点。好久没刷算法了。不记得咋求了…(忘记优先队列了,要补上这个技术点)
  • 但想到了归并排序。所以,可以将k个升序链表转成2个升序链表的问题。

1.2 代码

/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode mergeKLists(ListNode[] lists) {if (lists.length == 0) return null;return merge(lists, 0, lists.length - 1);}private ListNode merge(ListNode[] lists, int i, int j) {if (i == j) {return lists[i];}if (j - i == 1) {// 两条链表的合并return merge2Lists(lists[i], lists[j]);}int mid = ((j - i) >> 1) + i;ListNode leftList = merge(lists, i, mid);ListNode rightList = merge(lists, mid + 1, j);// 两条链表的合并return merge2Lists(leftList, rightList);}private ListNode merge2Lists(ListNode l1, ListNode l2) {ListNode dummy = new ListNode(), p = dummy;while (l1 != null && l2 != null) {if (l1.val <= l2.val) {p.next = l1;l1 = l1.next;} else {p.next = l2;l2 = l2.next;}p = p.next;}if (l1 == null) {p.next = l2;}if (l2 == null) {p.next = l1;}return dummy.next;}
}

1.3 分析这种解法的时空复杂度

1.3.1 时间复杂度
  • 图示:4个链表,两两合并的过程。为便于分析,假设每个链表的节点树为a。
    在这里插入图片描述
  • i = 1:有 k 2 \tfrac{k}{2} 2k对合并,每对合并涉及2a个节点。
  • i = 2:有 k 4 \tfrac{k}{4} 4k对合并,每对合并涉及4a个节点。
  • 每一层的计算: k 2 i \tfrac{k}{2 ^ i} 2ik * 2 i ∗ a 2^i *a 2ia = k ∗ a k * a ka
  • 层数为树高:叶子节点为k(k个链表),树高为logk。
  • 因此,时间复杂度为:O(aklogk)。k个链表一共有n个节点,所以,a简化为 n k \tfrac{n}{k} kn时间复杂度简化为:O(nlogk)
1.3.2 空间复杂度
  • 递归调用,栈深度为树高,因此,空间复杂度为O(logk)

2、优先队列

  • 给定一组元素,使得队列的头是最小/大元素。

2.1 PriorityQueue的使用

public class Main {public static void main(String[] args) {ListNode listNode1 = new ListNode(2);ListNode listNode2 = new ListNode(1);listNode1.setNext(listNode2);// 小根堆Queue<ListNode> queue = new PriorityQueue<>(Comparator.comparingInt(ListNode::getVal));// 将指定的元素插入到此优先级队列中。(相当于offer()方法)queue.add(listNode1);queue.add(listNode2);while (!queue.isEmpty()) {// 检索并删除此队列的头,如果此队列为空,则返回 null 。System.out.println(queue.poll());}}
}/*
ListNode(val=1, next=null) 
ListNode(val=2, next=ListNode(val=1, next=null))
*/
  • 既然要对元素进行排序,要么元素的类实现了Comparable接口(这个要求较高),要么就传入一个自定义的Comparator(这个更灵活)。

2.2 本题代码

/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode mergeKLists(ListNode[] lists) {if (lists.length == 0) {return null;}ListNode dummy = new ListNode(), p = dummy;Queue<ListNode> queue = new PriorityQueue<>((node1, node2) -> node1.val - node2.val);for (int i = 0; i < lists.length; i++) {if (lists[i] != null) {ListNode tmp = lists[i];while (tmp != null) {queue.add(tmp);tmp = tmp.next;}}}while (!queue.isEmpty()) {ListNode node = queue.poll();p.next = node;p = p.next;}p.next = null; // 合并升序链表问题,别忘了处理尾节点,否则链表可能成环。return dummy.next;}
}
2.2.1 进一步优化

没必要一次性将所有node都加入优先队列。

class Solution {public ListNode mergeKLists(ListNode[] lists) {if (lists.length == 0) {return null;}ListNode dummy = new ListNode(), p = dummy;Queue<ListNode> queue = new PriorityQueue<>(lists.length, (node1, node2) -> node1.val - node2.val);for (ListNode head : lists) {if (head != null) {queue.offer(head);}}while (!queue.isEmpty()) {ListNode node = queue.poll();p.next = node;p = p.next;if (node.next != null) {queue.offer(node.next);}}p.next = null;return dummy.next;}
}

2.3 分析这种解法的时空复杂度

2.3.1 时间复杂度
  • 一个k个链表,总共有n个节点。
  • 每个节点都会offer和poll优先队列各一次。
  • 每次的时间复杂度为O(logk):队列中最多k个元素,组成的树高为logk。

我们这里用到的优先队列,本质是小根堆,即一种特殊的完全二叉树。一棵由k个元素组成的完全二叉树,其树高为logk。

  • 因此,时间复杂度为O(nlogk)
2.3.2 空间复杂度
  • 队列中最多k个元素,因此空间复杂度为O(k)

这篇关于【刷力扣】23. 合并 K 个升序链表(dummy节点技巧 + 分治思维 + 优先队列)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1065668

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

C#实现一键批量合并PDF文档

《C#实现一键批量合并PDF文档》这篇文章主要为大家详细介绍了如何使用C#实现一键批量合并PDF文档功能,文中的示例代码简洁易懂,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言效果展示功能实现1、添加文件2、文件分组(书签)3、定义页码范围4、自定义显示5、定义页面尺寸6、PDF批量合并7、其他方法

RabbitMQ 延时队列插件安装与使用示例详解(基于 Delayed Message Plugin)

《RabbitMQ延时队列插件安装与使用示例详解(基于DelayedMessagePlugin)》本文详解RabbitMQ通过安装rabbitmq_delayed_message_exchan... 目录 一、什么是 RabbitMQ 延时队列? 二、安装前准备✅ RabbitMQ 环境要求 三、安装延时队

Java实现复杂查询优化的7个技巧小结

《Java实现复杂查询优化的7个技巧小结》在Java项目中,复杂查询是开发者面临的“硬骨头”,本文将通过7个实战技巧,结合代码示例和性能对比,手把手教你如何让复杂查询变得优雅,大家可以根据需求进行选择... 目录一、复杂查询的痛点:为何你的代码“又臭又长”1.1冗余变量与中间状态1.2重复查询与性能陷阱1.

Python内存优化的实战技巧分享

《Python内存优化的实战技巧分享》Python作为一门解释型语言,虽然在开发效率上有着显著优势,但在执行效率方面往往被诟病,然而,通过合理的内存优化策略,我们可以让Python程序的运行速度提升3... 目录前言python内存管理机制引用计数机制垃圾回收机制内存泄漏的常见原因1. 循环引用2. 全局变

MySQL中查询和展示LONGBLOB类型数据的技巧总结

《MySQL中查询和展示LONGBLOB类型数据的技巧总结》在MySQL中LONGBLOB是一种二进制大对象(BLOB)数据类型,用于存储大量的二进制数据,:本文主要介绍MySQL中查询和展示LO... 目录前言1. 查询 LONGBLOB 数据的大小2. 查询并展示 LONGBLOB 数据2.1 转换为十

Python进阶之列表推导式的10个核心技巧

《Python进阶之列表推导式的10个核心技巧》在Python编程中,列表推导式(ListComprehension)是提升代码效率的瑞士军刀,本文将通过真实场景案例,揭示列表推导式的进阶用法,希望对... 目录一、基础语法重构:理解推导式的底层逻辑二、嵌套循环:破解多维数据处理难题三、条件表达式:实现分支

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

Java集合中的链表与结构详解

《Java集合中的链表与结构详解》链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序的通过链表中的引用链接次序实现,文章对比ArrayList与LinkedList的结构差异,详细讲解了链表... 目录一、链表概念与结构二、当向单链表的实现2.1 准备工作2.2 初始化链表2.3 打印数据、链表长

MySQL进行分片合并的实现步骤

《MySQL进行分片合并的实现步骤》分片合并是指在分布式数据库系统中,将不同分片上的查询结果进行整合,以获得完整的查询结果,下面就来具体介绍一下,感兴趣的可以了解一下... 目录环境准备项目依赖数据源配置分片上下文分片查询和合并代码实现1. 查询单条记录2. 跨分片查询和合并测试结论分片合并(Shardin