归并排序:从二路到多路
前言
我们所熟知的快速排序和归并排序都是非常优秀的排序算法。
但是快速排序和归并排序的一个区别就是:快速排序是一种内部排序,而归并排序是一种外部排序。
简单理解归并排序:递归地拆分,回溯过程中,将排序结果进行合并。
大致流程示意图:


假设递归的终止条件就是剩下三个以下的元素就可以排序了。

注意:这步合并的过程用到了额外的存储空间。完成了排序之后再复制回去。


二路归并演示代码
#include <iostream>
using namespace std;void merge_sort(int *arr, int l, int r) {// 递归终止条件:只有一个元素或者没有元素的时候,不需要排序。if (l >= r) return ;// 打印输出排序之前的情况cout << endl;cout << "sort " << l << "<-->" << r << " : ";for (int i = l; i <= r; i++) cout << arr[i] << " ";cout << endl;int mid = (l + r) >> 1;merge_sort(arr, l, mid); // left sortmerge_sort(arr, mid + 1, r); // right sort// 写递归代码,一定不要展开地看,上面两行代码就当做左右子区间已经排序好了。// 下面将对两个区间进行合并,需要开辟新的空间将元素存到temp数组中。int *temp = (int *)malloc(sizeof(int) * (r - l + 1));int k = 0, p1 = l, p2 = mid + 1;while (p1 <= mid || p2 <= r) {if ((p2 > r) || (p1 <= mid && arr[p1] <= arr[p2])) {// 只有当右边为空,或者左边不为空并且左边比右边小,才将左边的元素放入temp[k++] = arr[p1++];} else {temp[k++] = arr[p2++];}}// 最后再拷贝回去即可for (int i = l; i <= r; i++) arr[i] = temp[i - l];// 打印输出排序之后的情况for (int i = l; i <= r; i++) cout << arr[i] << " ";cout << endl;free(temp);return ;
}int main() {int n;int arr[100];cin >> n;for (int i = 0; i < n; i++) cin >> arr[i];merge_sort(arr, 0, n - 1);for (int i = 0; i < n; i++) cout << arr[i] << " ";return 0;
}
输入数据:
10
7 9 0 8 6 4 5 3 1 2
输出:
sort 0<-->9 : 7 9 0 8 6 4 5 3 1 2 sort 0<-->4 : 7 9 0 8 6 sort 0<-->2 : 7 9 0 sort 0<-->1 : 7 9
7 9
0 7 9 sort 3<-->4 : 8 6
6 8
0 6 7 8 9sort 5<-->9 : 4 5 3 1 2sort 5<-->7 : 4 5 3sort 5<-->6 : 4 5
4 5
3 4 5sort 8<-->9 : 1 2
1 2
1 2 3 4 5
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
多路归并
上述演示代码的归并排序只是二路归并。将两个有序数组合并为一个有序数组。
那么多路归并就很好理解了,就是将多个有序数组合并为一个有序数组。
比如三路归并:

关于多路归并排序的应用,有一道很经典的面试题:
意思就是:我的内存太小了,无法通过诸如快速排序这样的内部排序算法,进行数据的直接整体排序。那么为什么这个问题可以由归并算法来解决呢?
归并的时候,外存可以作为归并排序中的那片关键的额外空间,数据是可以直接写回外存的,所以不需要内存有40GB的额外空间来先存放排序完的数据,再写回外存。
其实这40GB的文件可以被拆分成20份2GB的小文件,我们只要分别对20份小文件进行排序之后,进行20路归并操作就可以了
注意:程序执行一定是在内存当中,所有的数据也都需要从辅存或者外存当中调入内存当中,才可以进行CPU的运算。一个2GB大小的内存当然无法调入40GB的数据。
还需注意的是:我们在程序中只存储了相应的文件指针,并没有将文件中的内容一次性全部读满内存,而是需要一个数据就从文件中读一个数据。
读取文件演示代码:
*************************************************************************> File Name: merge_file.cpp> Author: jby> Mail: > Created Time: Sat 12 Aug 2023 11:39:20 PM CST************************************************************************/#include<iostream>
using namespace std;int main(int argc, char *argv[]) {int n = argc - 1; // 读取文件数量FILE **farr = (FILE **)malloc(sizeof(FILE *) * n);for (int i = 1; i <= n; i++) {farr[i - 1] = fopen(argv[i], "r");}for (int i = 0; i < n; i++) {int a;while (~fscanf(farr[i], "%d", &a)) {printf("%d\n", a);}printf("======\n");}return 0;
}
生成俩数据文件:file1、file2
# file1
1
34
56
78
# file2:
3
45
89
100
执行命令行:$./a.out file1 file2
输出结果:
1
34
56
78
======
3
45
89
100
======
这样我们就依次读取了存放在两个文件中的数据。
文件排序演示代码(简单实现,不用归并)
/*************************************************************************> File Name: merge_file.cpp> Author: jby> Mail: > Created Time: Sat 12 Aug 2023 11:39:20 PM CST************************************************************************/#include<iostream>
using namespace std;struct Data {FILE *fin; // fin: 当前文件指针int val, flag; // val: 当前读取的值;flag: 当前文件是否为空
};int main(int argc, char *argv[]) {int n = argc - 1; // 读取文件数量Data *data = (Data *)malloc(sizeof(Data) * n);for (int i = 1; i <= n; i++) {data[i - 1].fin = fopen(argv[i], "r");if (fscanf(data[i - 1].fin, "%d", &data[i - 1].val) == EOF) {data[i - 1].flag = 1;} else {data[i - 1].flag = 0;}}FILE *fout = fopen("output", "w");while (1) {int flag = 0;int ind = -1;int minVal = 0x3f3f3f3f;for (int i = 0; i < n; i++) {if (data[i].flag) continue; // 当前文件为空if (ind == -1 || data[i].val < data[ind].val) {ind = i;}}if (ind != -1) {fprintf(fout, "%d\n", data[ind].val); // 向结果文件中输出内容if (fscanf(data[ind].fin, "%d", &data[ind].val) == EOF) {data[ind].flag = 1;} else {data[ind].flag = 0;}flag = 1;}if (flag == 0) break;}return 0;
}
执行命令行:$./a.out file1 file2
输出结果,保存在output文件中:
1
3
34
45
56
78
89
100
归并排序的算法思想
我们不妨把思维从排序问题当中延展出来,归并排序的算法思想可以看成是以下三个步骤:
- 左边处理一下,得到左边的信息
- 右边处理一下,得到右边的信息
- 最后再处理,横跨左右两边的信息
这就是分而治之的思想。
LeetCode刷题实战
剑指 Offer 51. 数组中的逆序对
在归并排序的过程中,当右边区间的元素放进额外空间的时候,左边剩下的元素个数就是该元素所对应的逆序对个数。所以可以在归并的过程中不断累加。
class Solution {
public:vector<int> temp;int countResult(vector<int>& nums, int l, int r) {if (l >= r) return 0; // 如果只有一个元素,逆序数为0int ans = 0, mid = (l + r) >> 1;ans += countResult(nums, l, mid);ans += countResult(nums, mid + 1, r);int k = l, p1 = l, p2 = mid + 1;while (p1 <= mid || p2 <= r) {if ((p2 > r) || (p1 <= mid && nums[p1] <= nums[p2])) {temp[k++] = nums[p1++];} else {temp[k++] = nums[p2++];ans += (mid - p1 + 1);}}for (int i = l; i <= r; i++) nums[i] = temp[i];return ans;}int reversePairs(vector<int>& nums) {while (temp.size() < nums.size()) temp.push_back(0);return countResult(nums, 0, nums.size() - 1); }
};
23. 合并 K 个升序链表 - 力扣(LeetCode)
这道题其实跟之前的文件排序演示代码的逻辑没有本质区别,只不过这道题可以用到堆来加速。
class Solution {
public:struct CMP {bool operator()(ListNode *p, ListNode *q) {return p->val > q->val;}};ListNode* mergeKLists(vector<ListNode*>& lists) {priority_queue<ListNode *, vector<ListNode *>, CMP> q;for (auto x : lists) {if (x == nullptr) continue;q.push(x);}ListNode ret, *p = &ret;while (!q.empty()) {ListNode *cur = q.top();q.pop();p->next = cur;p = cur;if (cur->next) q.push(cur->next);}return ret.next;}
};
148. 排序链表 - 力扣(LeetCode)
如何用归并排序实现链表的排序呢?下面这段代码还是很具有典型意义的用链表来实现过程。
lass Solution {
public:ListNode *mergeSort(ListNode *head, int n) {if (head == nullptr || head->next == nullptr) return head;int l = n / 2, r = n - l;ListNode *lp = head, *rp = lp, *p;for (int i = 1; i < l; i++, rp = rp->next);p = rp, rp = rp->next;p->next = nullptr;lp = mergeSort(lp, l); // left Sortrp = mergeSort(rp, r); // right SortListNode ret;p = &ret;while (lp || rp) {if (rp == nullptr || (lp && lp->val < rp->val)) {p->next = lp;lp = lp->next;p = p->next;} else {p->next = rp;rp = rp->next;p = p->next;}}return ret.next;}ListNode* sortList(ListNode* head) {int n = 0;ListNode *p = head;while (p) p = p->next, n += 1;return mergeSort(head, n);}
};
1305. 两棵二叉搜索树中的所有元素 - 力扣(LeetCode)
用中序遍历,归并两颗子树,也是具有一定综合性的题。(怎么说的跟考研数学似的。。。)
class Solution {
public:void getResult(TreeNode *root, vector<int> &arr) {if (root == nullptr) return ;getResult(root->left, arr);arr.push_back(root->val);getResult(root->right, arr);return ;}vector<int> getAllElements(TreeNode* root1, TreeNode* root2) {vector<int> lnums, rnums;getResult(root1, lnums);getResult(root2, rnums);vector<int> ret;int p1 = 0, p2 = 0;while (p1 < lnums.size() || p2 < rnums.size()) {if (p2 >= rnums.size() || (p1 < lnums.size() && lnums[p1] < rnums[p2])) {ret.push_back(lnums[p1++]);} else {ret.push_back(rnums[p2++]);}}return ret;}
};
327. 区间和的个数 - 力扣(LeetCode)
一说到区间和值,就能想到前缀和。区间和等于前缀和数组中两项相减的值。问题就变成了,前缀和数组中,有多少对 lower <= sun[i]-sum[j] <= upper (i>j)
利用左右区间的有序性,加速查找的过程。
算法解题过程的封装思维:当我们将问题转化成另一个问题的时候,我们就忘掉前面的问题到底是什么,只需专注解决当前这个独立的问题。而不是脑子里一团乱麻。

class Solution {
public:int countTwoPart(vector<long long> &sum, int l1, int r1, int l2, int r2, int lower, int upper) {int ans = 0, k1 = l1, k2 = l1;for (int j = l2; j <= r2; j++) {long long a = sum[j] - upper;long long b = sum[j] - lower;while (k1 <= r1 && sum[k1] < a) k1++;while (k2 <= r1 && sum[k2] <= b)k2++;ans += k2 - k1;}return ans;}int mergeSort(vector<long long> &sum, int l, int r, int lower, int upper) {if (l >= r) return 0; // 只有一个元素的话,根本找不到数值对。int mid = (l + r) >> 1, ans = 0;ans += mergeSort(sum, l, mid, lower, upper);ans += mergeSort(sum, mid + 1, r, lower, upper);ans += countTwoPart(sum, l, mid, mid + 1, r, lower, upper);int k = l, p1 = l, p2 = mid + 1;while (p1 <= mid || p2 <= r) {if (p2 > r || (p1 <= mid && sum[p1] < sum[p2])) {temp[k++] = sum[p1++];} else {temp[k++] = sum[p2++];}}for (int i = l; i <= r; i++) sum[i] = temp[i];return ans; }vector<long long> temp;int countRangeSum(vector<int>& nums, int lower, int upper) {vector<long long> sum(nums.size() + 1);while (temp.size() < sum.size()) temp.push_back(0);sum[0] = 0;for (int i = 0; i < nums.size(); i++) sum[i + 1] = sum[i] + nums[i];return mergeSort(sum, 0, sum.size() - 1, lower, upper);}
};
本质上还是利用了分治的思想。核心的过程就是如何计算跨左右两半部分的过程
315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)
已经求得左右半边各自比它小的元素。两个区间合并。
class Solution {
public:// 归并排序的思想:分两个区间,统计两个区间的性质。// 在归并的过程中,左右两个有序区间,合并的时候,从大到小的顺序排,左边区间内,如果元素大于右边,则左边的元素比他小的个数应当加上右边r-p2+1struct Data {Data(int val, int ind) : val(val), ind(ind), cnt(0) {}bool operator > (const Data &a) {return val > a.val;}int val, ind, cnt;};void mergeSort(vector<Data> &arr, int l, int r) {if (l >= r) return ; // 如果只剩下一个元素,那就计算不了左右两边的统计性质int mid = (l + r) >> 1;mergeSort(arr, l, mid);mergeSort(arr, mid + 1, r);int k = l, p1 = l, p2 = mid + 1;while (p1 <= mid || p2 <= r) {if (p2 > r || (p1 <= mid && arr[p1] > arr[p2])) {arr[p1].cnt += r - p2 + 1;temp[k++] = arr[p1++];} else {temp[k++] = arr[p2++];}}for (int i = l; i <= r; i++) arr[i] = temp[i];return ;}vector<Data> temp;vector<int> countSmaller(vector<int>& nums) {vector<Data> arr;for (int i = 0; i < nums.size(); i++) arr.push_back(Data{nums[i], i});while (temp.size() < nums.size()) temp.push_back(Data{0, 0});mergeSort(arr, 0, arr.size() - 1);vector<int> ret(nums.size());for (auto x : arr) ret[x.ind] = x.cnt;return ret;}
};
相关文章:
归并排序:从二路到多路
前言 我们所熟知的快速排序和归并排序都是非常优秀的排序算法。 但是快速排序和归并排序的一个区别就是:快速排序是一种内部排序,而归并排序是一种外部排序。 简单理解归并排序:递归地拆分,回溯过程中,将排序结果进…...
【Vue】运行项目报错 This dependency was not found
背景 运行Vue 项目报错,提示This dependency was not found;然后我根据提示 执行 npm install --save vue/types/umd ,执行后发现错误,我一开始一直以为是我本地装不上这个依赖。后来找了资料后,看到应该是自己的代码里面随意的i…...
Shell编程之正则表达式
文本处理器:三剑客:grep查找sed awk shell正则表达式由一类特殊字符以及文本字符所编写的一种模式,处理文本当中的内容,其中的一些字符不表示字符的字面含义表示一种控制或者通配的功能 通配符:匹配文件名和目录名&a…...
QGraphicsView 实例3地图浏览器
主要介绍Graphics View框架,实现地图的浏览、放大、缩小,以及显示各个位置的视图、场景和地图坐标 效果图: mapwidget.h #ifndef MAPWIDGET_H #define MAPWIDGET_H #include <QLabel> #include <QMouseEvent> #include <QGraphicsView&…...
Windows基础安全知识
目录 常用DOS命令 ipconfig ping dir cd net user 常用DOS命令 内置账户访问控制 Windows访问控制 安全标识符 访问控制项 用户账户控制 UAC令牌 其他安全配置 本地安全策略 用户密码策略复杂性要求 强制密码历史: 禁止密码重复使用 密码最短使用期限…...
自定义注解和自定义注解处理器来扫描所有带有某个特定注解的Controller层
在Spring Boot中,您可以使用自定义注解和自定义注解处理器来扫描所有带有某个特定注解的Controller层。 以下是一个简单的示例,演示如何实现这个功能: 首先,创建自定义注解 CustomAnnotation ,用于标记需要被扫描的C…...
浏览器渲染原理 - 输入url 回车后发生了什么
目录 渲染时间点渲染流水线1,解析(parse)HTML1.1,DOM树1.2,CSSOM树1.3,解析时遇到 css 是怎么做的1.4,解析时遇到 js 是怎么做的 2,样式计算 Recalculate style3,布局 la…...
大文本的全文检索方案附件索引
一、简介 Elasticsearch附件索引是需要插件支持的功能,它允许将文件内容附加到Elasticsearch文档中,并对这些附件内容进行全文检索。本文将带你了解索引附件的原理和使用方法,并通过一个实际示例来说明如何在Elasticsearch中索引和检索文件附…...
35_windows环境debug Nginx 源码-CLion配置CMake和启动
文章目录 生成 CMakeLists.txt 组态档35_windows环境debug Nginx 源码-CLion配置CMake和启动生成 CMakeLists.txt 组态档 修改auto目录configure文件,在 . auto/make 上边增加 . auto/cmake, 大概在 106 行。在 auto 目录下创建cmake 文件其内容如下: #!/usr/bin/env bash NG…...
收集的一些比较好的git网址
1、民间故事 https://github.com/folkstory/lingqiu/blob/master/%E4%BC%A0%E8%AF%B4%E9%83%A8%E5%88%86/%E4%BA%BA%E7%89%A9%E4%BC%A0%E8%AF%B4/%E2%80%9C%E6%B5%B7%E5%BA%95%E6%8D%9E%E6%9C%88%E2%80%9D%E7%9A%84%E6%AD%A6%E4%B8%BE.md 2、童话故事 https://gutenberg.org/c…...
容斥原理 博弈论(多种Nim游戏解法)
目录 容斥原理容斥原理的简介能被整除的数(典型例题)实现思路代码实现扩展:用DPS实现 博弈论博弈论中的相关性质博弈论的相关结论先手必败必胜的证明Nim游戏(典型例题)代码实现 台阶-Nim游戏(典型例题&…...
【C++】函数指针
2023年8月18日,周五上午 今天在B站看Qt教学视频的时候遇到了 目录 语法和typedef或using结合我的总结 语法 返回类型 (*指针变量名)(参数列表)以下是一些示例来说明如何声明不同类型的函数指针: 声明一个不接受任何参数且返回void的函数指针…...
VBA技术资料MF45:VBA_在Excel中自定义行高
【分享成果,随喜正能量】可以不光芒万丈,但不要停止发光。有的人陷入困境,不是被人所困,而是自己束缚自己,这时"解铃还须系铃人",如果自己无法放下,如何能脱困? 。 我给V…...
【Git】Git中的钩子
Git Book——Git的自定义钩子 Git中的钩子分为两大类: 1、客户端钩子:由诸如提交和合并这样的操作所调用 2、服务端钩子:由诸如接收被推送的提交这样的联网操作 客户端钩子: 提交工作流钩子 pre-commit:在提交信息前…...
java 工程管理系统源码+项目说明+功能描述+前后端分离 + 二次开发 em
Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下: 首页 工作台:待办工作、消息通知、预警信息,点击可进入相应的列表 项目进度图表:选择(总体或单个)项目显…...
Java # JVM
一、1.8之前 运行时数据区(进程共享) 运行时常量池为什么要有方法区: jvm完成类装载后,需要将class文件中的常量池转入内存,保存在方法区中为什么是常量: 常量对象操作较多,为了避免频繁创建和…...
vscode远程连接Linux失败,提示过程试图写入的管道不存在(三种解决办法)
vscode报错如下: 一、第一种情况 原因是本地的known_hosts文件记录服务器信息与现服务器的信息冲突了,导致连接失败。 解决方案就是把本地的known_hosts的原服务器信息全部删掉,然后重新连接。 二、第二种情况 在编写配置文件config时&…...
elaticsearch(1)
1.简介 Elasticsearch是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。 Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引…...
使用pnpm workspace管理Monorepo架构
在开发项目的过程中,我们需要在一个仓库中管理多个项目,每个项目有独立的依赖、脚手架,这种形式的项目结构我们称之为Monorepo,pnpm workspace就是管理这类项目的方案之一。 一、pnpm简介 1、pnpm概述 pnpm代表performance npm…...
Ubuntu16.04-ros-kinetic环境搭建笔记=1=
tips:搬运资料,留个记录 安装Ubuntu Ubuntu官网下载地址 安装 虚拟机安装Ubuntu 最好断网安装Ubuntu,可以节约时间 Ubuntu基础设置 Ubuntu换国内源 换成清华源 sudo apt upgradeVMwareTool安装 把这个压缩包拖到桌面,否则只读…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...
【免费数据】2005-2019年我国272个地级市的旅游竞争力多指标数据(33个指标)
旅游业是一个城市的重要产业构成。旅游竞争力是一个城市竞争力的重要构成部分。一个城市的旅游竞争力反映了其在旅游市场竞争中的比较优势。 今日我们分享的是2005-2019年我国272个地级市的旅游竞争力多指标数据!该数据集源自2025年4月发表于《地理学报》的论文成果…...
如何做好一份技术文档?从规划到实践的完整指南
如何做好一份技术文档?从规划到实践的完整指南 🌟 嗨,我是IRpickstars! 🌌 总有一行代码,能点亮万千星辰。 🔍 在技术的宇宙中,我愿做永不停歇的探索者。 ✨ 用代码丈量世界&…...
