【刷力扣】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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/853517.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 工程副总裁卸任

Android 工程副总裁卸任 Android工程副总裁Dave Burke宣布&#xff0c;他将辞去领导Android工程的职位&#xff0c;将重心转向“AI/生物”项目。不过&#xff0c;他并没有离开Alphabet&#xff0c;目前仍将担任Android系统开发顾问的角色。 Burke参与了Android系统的多个关键…

【TypeScript】类型兼容(协变、逆变和双向协变)

跟着小满zs 学习 ts&#xff0c;原文&#xff1a;学习TypeScript进阶类型兼容_typescript进阶阶段类型兼容 小满-CSDN博客 类型兼容&#xff0c;就是用于确定一个类型是否能赋值给其他的类型。如果A要兼容B 那么A至少具有B相同的属性。 // 主类型 interface A {name: string,a…

边界内聚和耦合

内聚 功能内聚 功能内聚是软件工程中一个重要的概念&#xff0c;它描述了一个模块内部各个元素之间的紧密程度。一个具有高功能内聚的模块意味着其内部的各个组件都共同完成一个具体的、明确的功能&#xff0c;并且这些组件之间的联系不是偶然的&#xff0c;而是因为它们共同服…

快手爬票概述

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 无论是出差还是旅行&#xff0c;都无法离开交通工具的支持。现如今随着科技水平的提高&#xff0c;高铁与动车成为人们喜爱的交通工具。如果想要知道…

【C语言】联合(共用体)

目录 一、什么是联合体 二、联合类型的声明 三、联合变量的创建 四、联合的特点 五、联合体大小的计算 六、联合的应用&#xff08;判断大小端&#xff09; 七、联合体的优缺点 7.1 优点 7.2 缺点 一、什么是联合体 联合也是一种特殊的自定义类型。由多个不同类型的数…

LVS三种负载均衡模式:NAT、Tunneling和DR的技术对比

1. LVS-NAT 模式的特性 IP使用&#xff1a;RS&#xff08;Real Server&#xff09;应使用私有地址&#xff0c;RS的网关必须指向DIP&#xff08;Director IP&#xff09;。网络范围&#xff1a;DIP和RIP必须在同一个网段内。数据包处理&#xff1a;请求和响应报文都需要经过Di…

数据库开发——并发控制(第十一章)

文章目录 前言并发执行例题一、封锁二、封锁协议三、可串行调度四、总结 学习目标&#xff1a;重点为并发控制的基本概念及几个基本协议 前言 数据库管理系统必须提供并发控制机制&#xff0c;保证事务的隔离性和一致性 并发执行例题 一、封锁 排他锁称为写锁&#xff0c;共…

atmega8 上传程序

使用icsp 烧写时先关闭串口程序&#xff0c;与串口uart连接相关的电路勿于电脑连接 接触不良 1.使用icsp 上传 1&#xff09;可以直接上传程序 如官方示例blink 或是 serial示例 2&#xff09;可以先烧录bootload 方便下次使用串口上传程序代码 A)使用专门的icsp 上传器上传…

关于二分法的理解(以JS为例)

算法介绍 基本概念 二分查找算法&#xff0c;又称折半查找算法&#xff0c;是一种在有序数组中查找特定元素的高效方法。它的核心思想是将数组分成两半&#xff0c;然后根据目标值与中间元素的比较结果来决定是继续在左半部分还是右半部分进行搜索。 工作原理 初始化&#…

【iOS】如何断点看系统方法在哪一个库

如何断点看系统方法在哪一个库 开源网站如何断点看系统方法在哪一个库1 下符号断点2 符号断点中输入 load &#xff0c;并且开启断点&#xff08;Enable&#xff09;。3 运行程序&#xff0c;触发断点&#xff0c;在堆栈信息中找到load方法&#xff0c;可以看到其在libobjc.A.d…

程序员的wsl2

坑 wsl无法打开 上班时打开wsl2提示 WSL 正在完成升级... Could not write value to key \SOFTWARE\Classes\Directory\shell\WSL. Verify that you have sufficient access to that key, or contact your support personnel. 原因未知&#xff0c;之前并没有更新过&#xff…

【AI+编程】工作日常场景随时可以AI编程,记一个问答SQL快速导出数据日常示例

今天有个场景&#xff0c;我们有个老项目&#xff0c;由于历史原因差不多1年多没使用了&#xff0c;恰巧客户紧急情况要使用。因为当时没有需求&#xff0c;所以V1.0上线后 就没做更新。 需求很简单&#xff1a;我们假定 项目问题表、客户表、问题答案表&#xff0c; 实时查询…

TCP协议报头详解

目录 前言 TCP特点 TCP报头 1.源端口和目的端口 2.序号 3.确认号 4.数据偏移 5.保留 6.控制位 ① 紧急URG&#xff08;URGent&#xff09; ② 确认ACK&#xff08;ACKnowledgment&#xff09; ③ 推送PSH&#xff08;PuSH&#xff09; ④复位RST&#xff08;ReSeT&…

Hexo 搭建个人博客(ubuntu20.04)

1 安装 Nodejs 和 npm 首先登录NodeSource官网&#xff1a; Nodesource Node.js DEB 按照提示安装最新的 Node.js 及其配套版本的 npm。 &#xff08;1&#xff09;以 sudo 用户身份运行下面的命令&#xff0c;下载并执行 NodeSource 安装脚本&#xff1a; sudo curl -fsSL…

【牛客面试必刷TOP101】Day32.BM68 矩阵的最小路径和和BM69 把数字翻译成字符串

文章目录 前言一、BM68 矩阵的最小路径和题目描述题目解析二、BM69 把数字翻译成字符串题目描述题目解析总结 前言 一、BM68 矩阵的最小路径和 题目描述 描述&#xff1a; 给定两个字符串str1和str2&#xff0c;输出两个字符串的最长公共子序列。如果最长公共子序列为空&#x…

webpack逆向

声明&#xff1a;个人总结记录一下&#xff0c;避免忘记 1、webpack 类型 单文件 直接可以在文件顶部找到加载器 多文件 顶部找不到加载器 如图所示 多文件的这话&#xff0c;全局搜所 69725 找到类似n(69725) ,单点n进去&#xff0c;可以找到加载器 2、调用 通过赋值的方…

Linux 并发与竞争实验学习

Linux 并发与竞争实验学习 原子操作实验 这里原子操作就是采用原子变量来保护一个程序运行的完整过程&#xff0c;使用atomic 来实现一次只能允许一个应用访问 LED&#xff0c;创建atomic.c文件&#xff0c;其实改动内容就是添加原子变量&#xff0c; 要在设备结构体数据添加…

vue2 + element-ui,前端配置化表单封装(2024-06-14)

技术栈是 vue2 element-ui&#xff0c;主要能解决的问题就是 提高代码复用能力、提升开发效率&#xff0c;特别是需要开发多个大型表单系统的&#xff0c;配置化可以极大的提升效率&#xff0c;让你上班摸鱼不再是梦想&#xff01;为了早点下班&#xff0c;我们接着往下看吧&a…

(Java微服务项目实战)dtpay聚合支付系统对账管理模块系统设计

1 聚合支付系统对账流程 dtpay聚合支付系统对账模块主要涵盖商户侧对账和渠道侧对账、平台侧对账&#xff0c;本文主要分析渠道侧对账。dtpay聚合支付系统通过支付渠道微信、支付宝等产生的支付退款交易数据需要和平台侧产生的数据进行交易数据比对。接下来我们具体分析对账流…

一款不写代码的开源爬虫工具!!【送源码】

爬虫&#xff0c;也被称为网络爬虫或网络蜘蛛&#xff0c;是一种自动化的网络机器人&#xff0c;其主要功能是按照一定的规则&#xff0c;自动浏览互联网并从网页中提取信息。 作为一个开发人员&#xff0c;相信大家都尝试过写一些爬虫&#xff0c;合理的利用一些爬虫工具&…