深入浅出多路归并:原理、实现与实战案例解析

文章目录

  • 二路归并
  • 多路归并
    • 方法一:指针遍历(多指针比较法)
    • 方法二:小根堆法(最小堆归并)
  • 实际场景
    • 外部排序
  • 经典题目
    • 丑数Ⅱ
      • 方法一:三指针法
      • 方法二:优先队列法(K路归并)
      • 方法三:优先队列法(BFS)(非多路归并)
    • 其他题目
  • 总结

归并,在计算机科学中,一般是以归并排序出现的,就是将两个或者多个有序的序列合并成一个序列。

二路归并

举个二路归并的例子:

输入两个有序数组:
[1, 4, 7]
[2, 5, 6, 8]归并后得到:
[1, 2, 4, 5, 6, 7, 8]

二路归并实现上很简单,我们很容易就能想到用两个指针,分别指向两个数组的起始位置,然后不断地两两比较指针所指地数值,将小的放到新数组中去,最终遍历完两个数组就行了。

多路归并

但是多路归并涉及到了多个有序的数组,我们要将这多个有序数组合并成一个。多路归并一共有两个方法。

方法一:指针遍历(多指针比较法)

每个数组维护一个指针,指向当前元素。每次在这 k 个数组当前指向的元素中找到最小值,将其加入结果数组,并将该元素所属数组的指针后移。这个过程重复进行,直到所有数组遍历完毕。

  • 每次选择最小值需要遍历所有指针,时间复杂度为 O(k)
  • 如果总共有 n 个元素需要合并,时间复杂度为 O(n × k)

方法二:小根堆法(最小堆归并)

我们使用一个最小堆(优先队列)来优化每次找最小值的操作:

  1. 首先将每个数组的第一个元素(及其数组索引、元素索引)加入小根堆;
  2. 每次弹出堆顶元素(即当前最小值),将其加入结果数组;
  3. 如果该元素所属数组还有剩余元素,则将下一个元素加入堆;
  4. 重复直到堆为空。

由于堆的大小最多为 k,每次插入和删除的时间复杂度是 O(log k),总共处理 n 个元素,因此整体时间复杂度为 O(n × log k),明显优于指针遍历法。

假设我们有如下三个有序数组:

text复制编辑arr1 = [1, 4, 7]
arr2 = [2, 5, 8]
arr3 = [3, 6, 9]

使用最小堆归并过程如下:

  • 初始堆:[1(arr1), 2(arr2), 3(arr3)] → 弹出 1,加入结果,arr1 指针后移,入堆 4;
  • 堆变为:[2(arr2), 3(arr3), 4(arr1)] → 弹出 2,加入结果,入堆 5;
  • 堆变为:[3(arr3), 4(arr1), 5(arr2)] → 弹出 3,加入结果,入堆 6;

实际场景

当我们需要对超大文件(如几个 GB、甚至 TB 的日志、数据库快照等)进行排序时,它们无法一次性加载进内存,常规排序算法(如快排、归并排序)在内存中就不适用了。

于是我们采用 外部排序(External Merge Sort)

外部排序

步骤1: 分段排序

1.先将超大文件分成多份可以加载进内存的小块。

2.将这k个小块每一块都读进内存进行快速排序,保证每一块都是有序的。

3.将排序后的每块都重新记录回磁盘。

步骤2: 多路归并

通过步骤1我们得到了k组有序的文件,现在是要把它们合并成一个有序文件。

1.k个文件各自都有一个指针指向当前最小的数据。

2.每次都读取k个指针所指的数据进入内存。

3.比较出最小的写入最终的输出文件sorted_output.txt中。

4.将最小的那个文件的指针后移一位。

重复以上步骤,最终就可以得到排序好的最终输出文件sorted_output.txt。

经典题目

丑数Ⅱ

让我们通过一个经典的 LeetCode 问题来看多路归并的实际应用。

问题描述: 丑数是只包含质因数 2、3、5 的正整数。给定整数 n,返回第 n 个丑数。前几个丑数序列:1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, …

**问题分析:**分析题目我们得知,丑数是由2,3,5中的任意几个乘起来得到的。每个丑数都可以由更小的丑数乘以2、3或5得到,并且不会遗漏。

这意味着我们可以构造三个"虚拟"的有序序列:

  • 序列1:所有丑数 × 2
  • 序列2:所有丑数 × 3
  • 序列3:所有丑数 × 5

我们的任务就是对这三个序列进行多路归并!

方法一:三指针法

维护一个数组 ugly[] 存储前 n 个丑数,并使用三个指针分别追踪当前乘以 2、3 和 5 所得到的候选值。每次从候选中选出最小值作为下一个丑数,同时相应地移动对应指针,以避免重复并保持有序。该方法总共找n次,每次需要进行k次的比较,所以最终时间复杂度为 O(n*k),但是因为这道题目的序列个数只有3个,所以其实最终的时间复杂度只有O(n)。

public static int nthUglyNumber(int n) {int[] ugly = new int[n];ugly[0]=1;int i2=0,i3=0,i5=0;int next2=1,next3=1,next5=1;for(int i=1;i<n;i++){next2=ugly[i2]*2;next3=ugly[i3]*3;next5=ugly[i5]*5;ugly[i]=Math.min(next2,Math.min(next3,next5));if(ugly[i]==next2){i2++;}if(ugly[i]==next3){i3++;}if(ugly[i]==next5){i5++;}}return ugly[n-1];
}

方法二:优先队列法(K路归并)

上面的方法使用三个指针记录每一组数当前访问的元素,然后每次需要找最小数的时候我们进行挨个比较找出最小数。这里我们使用优先队列来替换掉这个挨个比较找出最小值的过程,我们在优先队列中保存着三组数当前指针所指的位置,每次都出堆,就能很快地找出最小值了。保证这个堆的大小一直都是k(k为数组个数),总共需要找n次,所以最终的时间复杂度是O(nlogk)。

public static int nthUglyNumber(int n) {int[] ugly = new int[n];ugly[0] = 1;int i2 = 0, i3 = 0, i5 = 0;PriorityQueue<Integer> pq = new PriorityQueue<>();pq.add(2);pq.add(3);pq.add(5);Set<Integer> set = new HashSet<>();set.add(2);set.add(3);set.add(5);for (int i = 1; i < n; i++) {int nextUgly = pq.poll();ugly[i] = nextUgly;if (ugly[i] == ugly[i2] * 2) {i2++;if (!set.contains(ugly[i2] * 2)) {pq.add(ugly[i2] * 2);set.add(ugly[i2] * 2);}}if (ugly[i] == ugly[i3] * 3) {i3++;if (!set.contains(ugly[i3] * 3)) {pq.add(ugly[i3] * 3);set.add(ugly[i3] * 3);}}if (ugly[i] == ugly[i5] * 5) {i5++;if (!set.contains(ugly[i5] * 5)) {pq.add(ugly[i5] * 5);set.add(ugly[i5] * 5);}}}return ugly[n - 1];}

方法三:优先队列法(BFS)(非多路归并)

还有一种使用优先队列的方法来解决这道题目,一开始其实我是用这个方法解决的这道题目,我以为这是多路归并,但是怎么想怎么不对,现在我认为这是一种广度优先遍历的思想。

我设置一个优先队列,我们从 1 开始,把它看作丑数生成树的“根节点”。接着每次取出当前最小的一个丑数 ugly,再扩展它的三个“邻居”:ugly*2, ugly*3, ugly*5,把还没见过的加入堆中,保证后续访问。如此往复。直到循环了n次,找到了第n小的丑数。

图中的 BFS这段丑数代码中的等价含义
从起点开始遍历邻居1 开始扩展出 1*2, 1*3, 1*5
使用队列维护访问顺序使用 最小堆 PriorityQueue 保证顺序
访问节点后标记为已访问使用 Set<Long> seen 做去重
一层一层按顺序扩展丑数是从小到大、层层构造的
不重复访问同一节点seen 避免重复乘积
public static int nthUglyNumber(int n) {int ulgy=1;PriorityQueue<Long> pq = new PriorityQueue<>();Set<Long> seen = new HashSet<>();pq.add(1L);seen.add(1L);for(int i=1;i<n;i++){long nextUlgy = pq.poll();if(seen.add(nextUgly*2)){pq.add(nextUgly*2);}if(seen.add(nextUgly*3)){pq.add(nextUgly*3);}if(seen.add(nextUgly*5)){pq.add(nextUgly*5);}}return pq.poll().intValue();
}

其他题目

373. 查找和最小的 K 对数字

313. 超级丑数

632. 最小区间

719. 找出第 K 小的数对距离

786. 第 K 个最小的质数分数

1439. 有序矩阵中的第 k 个最小数组和

1508. 子数组和排序后的区间和

总结

总结来说,多路归并是一种非常实用的思想,不仅能在传统排序问题中提升效率,更在很多“按顺序生成”类的问题中大放异彩。特别是在内存受限、数据量庞大的现实场景中,它往往是不可替代的选择。掌握它,也就掌握了不少高频题的核心解法。

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

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

相关文章

Koji构建系统宏定义注入与Tag体系解析

在Red Hat生态的持续集成链条中&#xff0c;Koji作为核心构建系统&#xff0c;其灵活的宏定义机制与精密的Tag体系是保障软件包高效流转的关键。本文将系统阐述在既有构建目标中注入宏定义的技术路径&#xff0c;并深度解析Koji中Target与Tag的概念架构及其版本演进差异。 一、…

【Kubernetes】架构与原理:核心概念、组件协同及容器化部署解析

文章目录 一、前言二、为什么需要 Kubernetes1. 传统部署方法2. 虚拟化部署3. 容器化部署Ⅰ. 基本概念Ⅱ. 容器编排的必要性Ⅲ. 容器化部署的优势4. k8s 的历史与发展三、Kubernetes 基本概念1. k8s 核心架构解析Ⅰ. 控制平面与工作节点Ⅱ. 各组件协同工作原理2. k8s 核心概念Ⅰ…

Pip Manager本地Python包管理器

在Python开发领域&#xff0c;包管理是每个开发者日常工作中不可或缺的一部分。虽然命令行工具pip功能强大&#xff0c;但对于初学者和非技术背景的用户来说&#xff0c;命令行界面往往显得不够友好。如果使用PyCharm&#xff0c;则可以非常简单的管理安装的Python包&#xff1…

vscode界面设置透明度--插件Glasslt-VSC

【快捷键:透明度提高(CtrAlt Z)&#xff0c;透明度降低(CtrAlt C)】

OPENCV形态学基础之一膨胀

一.膨胀的原理 数学表达式&#xff1a;dst(x,y) dilate(src(x,y)) max(x,y)src(xx,yy) 膨胀是图像形态学的基本功能之一&#xff0c;膨胀顾名思义就是求图像的局部最大值操作&#xff0c;它的数学表达式是dst(x,y) dilate(src(x,y)) max(x,y)src(xx,yy)。 从数学的角度来看…

彻底禁用Windows Defender通知和图标

方法 一&#xff1a;通过注册表强制隐藏 Defender 图标&#xff08;永久生效&#xff09;​​ &#xff08;适用于彻底隐藏图标&#xff0c;但需谨慎操作&#xff09; ​​打开注册表编辑器​​ 按 Win R&#xff0c;输入 regedit 回车。 ​​导航到 Defender 相关注册表项​…

Kafka 2.7.0 单节点安装与启动教程(适配 JDK 1.8)

1. 下载与解压 官方下载 Kafka 2.7.0 https://archive.apache.org/dist/kafka/2.7.0/kafka_2.13-2.7.0.tgz 上传到虚拟机&#xff08;如 /home/wang/soft/kafka&#xff09;解压&#xff1a; tar -zxvf kafka_2.13-2.7.0.tgz 2. 配置环境变量&#xff08;可选&#xff0c;便…

23、Python字符串核心机制解析:驻留原理、对象比较与成员检测实战

适合人群&#xff1a;零基础自学者 | 编程小白快速入门 阅读时长&#xff1a;约5分钟 文章目录 一、问题&#xff1a;Python的字符串驻留机制&#xff1f;1、例子1&#xff1a;字符串驻留现象2、答案&#xff1a;&#xff08;1&#xff09;字符串驻留 二、问题&#xff1a;Pyth…

pikachu靶场通关笔记22-2 SQL注入05-2-update注入(报错法)

目录 一、SQL注入 二、update注入 三、报错型注入 四、源码分析 1、代码审计 2、渗透思路 五、渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关&#xff…

【prometheus+Grafana篇】基于Prometheus+Grafana实现Redis数据库的监控与可视化

&#x1f4ab;《博主主页》&#xff1a; &#x1f50e; CSDN主页 &#x1f50e; IF Club社区主页 &#x1f525;《擅长领域》&#xff1a;擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控&#xff1b;并对SQLserver、NoSQL(MongoDB)有了…

R语言速释制剂QBD解决方案之四

本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》速释制剂混合和润滑工艺研究的R语言解决方案。 原料粒径分布与混合次数对混合均一性的影响 由于acetriptan 的溶解度低&#xff0c;acetriptan 需要粉碎以提高生物利用度。粉碎后的原料…

用python玩转大语言模型——从 RNN 到文本生成大语言模型的奇幻之旅

用python玩转大语言模型——从 RNN 到文本生成大语言模型的奇幻之旅 第一部分:RNN原理及其结构(魔法师的记忆水晶球) 1.1 经典RNN结构(时光旅行者的备忘录) 核心概念 时间循环:RNN通过隐藏状态h在时间步之间传递信息,形成闭环结构参数共享:每个时间步使用相同的权重…

数据结构(9)排序

一、常见排序算法 排序在生活中无处不在&#xff0c;上学这么多年班级排名啥的总有吧&#xff0c;不可能一次都没见过&#xff1b;打游戏有的排行榜不也是有排序的思想在里面&#xff0c;排序倒不是什么特殊的数据结构&#xff0c;但是是非常重要的算法思想&#xff0c;所以在初…

量子计算导论课程设计 之 PennyLane环境搭建

文章目录 具体配置conda 虚拟环境配置Pennylane 正所谓&#xff0c;磨刀不误砍柴工&#xff0c;想要进行量子计算导论的课程设计&#xff0c;首先就是搭建好平台&#xff0c;推荐大家就是本地搭建&#xff0c;那么下面有三种选择 QiskitTensorFlow QuantumPennylane 具体配置…

nginx ./nginx -s reload 不生效

问题 nginx ./nginx -s reload 不生效 解决 不是改opt/nginx下的配置文件是改/usr/local/nginx下的配置文件改之前做好备份

建造者模式深度解析与实战应用

作者简介 我是摘星&#xff0c;一名全栈开发者&#xff0c;专注 Java后端开发、AI工程化 与 云计算架构 领域&#xff0c;擅长Python技术栈。热衷于探索前沿技术&#xff0c;包括大模型应用、云原生解决方案及自动化工具开发。日常深耕技术实践&#xff0c;乐于分享实战经验与…

VScode - 我的常用插件01 - 主题插件Noctis

导言 Noctis 是一款为 Visual Studio Code 提供的主题插件&#xff0c;主打高对比度、护眼、美观。它有多种配色风格&#xff0c;适合不同的开发者审美和工作场景。 一、安装Noctis 二、设置颜色主题 三、测试主题 如上所示&#xff0c;有11种主题背景可以选择。这里&#xff…

【IQA技术专题】图像质量评价IQA技术和应用综述(万字长文!!)

专题介绍 图像质量评价&#xff08;Image Quality Assessment, IQA&#xff09;是图像处理、计算机视觉和多媒体通信等领域的关键技术之一。IQA不仅被用于学术研究&#xff0c;更在影像相关行业内实现了完整的商业化应用&#xff0c;涉及影视、智能手机、专业相机、安防监控、…

突然虚拟机磁盘只剩下几十K

第一步&#xff1a;查找哪些文件大于 100M find / -size 100M 第二步&#xff1a;删除掉无用的 log 发现&#xff0c;磁盘剩余空间并没有变大 假如一个文件正在被使用&#xff0c;你删除之后也是不会释放存储空间的。需要关闭相应的服务才能释放。

黑马教程强化day2-1

目录 一、Set集合1.Set集合特点2.Set集合分类3.hashSet底层原理&#xff1a;(基于哈希表存储数据的&#xff09;代码演示 5.hashSet集合元素的去重操作&#xff08;有些情况搞不动&#xff09;代码演示 6.LinkedHashSet的底层原理&#xff08;不常用&#xff0c;所以没有代码演…