归并排序与其例题

news/2024/9/18 12:25:37/ 标签: 算法, 排序算法, 数据结构

一、归并排序的简述

归并排序(Merge Sort)是一种高效的排序算法,采用分治法(Divide and Conquer)的策略。它的基本思想是将一个大的问题分解成多个小问题,然后解决这些小问题,最后将结果合并起来得到最终的答案。具体来说,归并排序的算法思想可以分为以下几个步骤:

1. 分解(Divide)

将待排序的数组或列表分成两个大小相等或接近相等的子数组。这个过程会递归地进行,直到子数组的大小为1或0(即子数组已经排序好)。

2. 解决(Conquer)

当子数组的大小为1或0时,这些子数组自然是已排序的。接下来,逐步合并这些子数组,以形成较大的排序数组。

3. 合并(Merge)

将两个已排序的子数组合并成一个新的已排序的数组。这个合并操作是归并排序的关键部分,它可以通过比较两个子数组的元素并将较小的元素添加到结果数组中来完成。

详细的归并排序算法步骤:

  1. 分解

    • 如果数组的长度大于1,找到数组的中间位置,将数组分成两半。
  2. 递归排序

    • 对每个子数组递归地调用归并排序算法,直到子数组的长度为1。
  3. 合并

    • 将两个已排序的子数组合并成一个排序后的数组。可以使用两个指针分别指向两个子数组的当前元素,比较这两个元素,将较小的元素添加到结果数组中,并移动指针。

示例

假设我们要对数组 [38, 27, 43, 3, 9, 82, 10] 进行归并排序:

  1. 分解

    • [38, 27, 43, 3, 9, 82, 10] 被分成 [38, 27, 43] 和 [3, 9, 82, 10]
    • [38, 27, 43] 进一步分解为 [38] 和 [27, 43]
    • [27, 43] 进一步分解为 [27] 和 [43]
    • [3, 9, 82, 10] 被分解为 [3, 9] 和 [82, 10]
    • [3, 9] 进一步分解为 [3] 和 [9]
    • [82, 10] 进一步分解为 [82] 和 [10]
  2. 递归排序

    • 单个元素 [38][27][43][3][9][82][10] 都是已排序的。
  3. 合并

    • 合并 [27] 和 [43] 变成 [27, 43]
    • 合并 [38] 和 [27, 43] 变成 [27, 38, 43]
    • 合并 [3] 和 [9] 变成 [3, 9]
    • 合并 [82] 和 [10] 变成 [10, 82]
    • 合并 [3, 9] 和 [10, 82] 变成 [3, 9, 10, 82]
    • 最后合并 [27, 38, 43] 和 [3, 9, 10, 82] 变成 [3, 9, 10, 27, 38, 43, 82]

时间复杂度

  • 归并排序的时间复杂度为 (O(n \log n)),其中 (n) 是待排序元素的数量。这个复杂度是因为分解和合并操作都在对数级别的层次上进行,每一层需要对整个数组进行操作。

空间复杂度

  • 归并排序的空间复杂度为 (O(n)),因为需要额外的空间来存储合并后的数组。

归并排序是一种稳定的排序算法,不会改变相同元素的相对顺序。这使得它在某些需要稳定排序的场景中非常有用。

二、模板题

给定你一个长度为 n 的整数数列。

请你使用归并排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式

输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1∼10^{9} 范围内),表示整个数列。

输出格式

输出共一行,包含 n 个整数,表示排好序的数列。

数据范围

1≤n≤100000

输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5

#include<iostream>using namespace std;const int N=1e5+10;
int q[N],tmp[N];void merge_sort(int q[],int l,int r){if(l>=r)  return; //当只有一个元素时,没必要递归,直接返回退出。int mid=l+r>>1,i=l,j=mid+1,k=0;//先找中点。merge_sort(q,l,mid); merge_sort(q,mid+1,r);/*找到中点后将整个要排序的序列分成了两半,然后左边右边归并排序使得左右两边的序列变成有序的,所以这里递归一下。*//*当分成的左右两个序列恰好是长度相等的时候,就执行下面while里面的操作。*/while(i<=mid&&j<=r){if(q[i]<=q[j])  tmp[k++]=q[i++];/*在左右两个序列中找更小的值,然后把这个更小的值放入已经准备好的临时数组tmp[N]。tmp[k++]=q[i++];   这句话其实等价于tmp[k]=q[i]; k++,i++;  这样只写一句话是为了代码更简洁。*/else tmp[k++]=q[j++];}/*当分成的左边的序列大于右边的序列时,直接将左边序列剩余的部分接在tmp数组后面就是。因为本来左右两边的序列就已经是有序的了,所以左边序列剩下来的数都是要大于整体的数,因为比他们小的数已经放进去tmp临时数组了,这一点要注意。*/while(i<=mid) tmp[k++]=q[i++];while(j<=r) tmp[k++]=q[j++];for(int i=l,j=0;i<=r;i++,j++)  q[i]=tmp[j];/*这是将tmp数组里面的值复制给原本的q[N],因为tmp本就是设立的临时数组,最后输出的话还得是q[N]这个实体的。*/
}int main(){int n;scanf("%d",&n);for(int i=0;i<n;i++) scanf("%d",&q[i]); merge_sort(q,0,n-1);for(int i=0;i<n;i++) printf("%d ",q[i]);return 0;
}

归并排序是一种经典的排序算法,其基本思想是分治法。它的主要步骤是将一个大问题分解成若干个小问题,对每个小问题进行解决,然后将结果合并成一个整体。下面是代码的逐行详细解释:

#include<iostream> using namespace std; const int N=1e5+10; int q[N],tmp[N];

  • #include<iostream>: 包含输入输出流库,用于输入和输出操作。
  • using namespace std;: 使用 std 命名空间中的所有符号,避免写 std:: 前缀。
  • const int N=1e5+10;: 定义常量 N,表示数组 q 和 tmp 的最大大小。1e5+10 即 100010
  • int q[N], tmp[N];: 声明两个整型数组 q 和 tmp,分别用于存储待排序的数组和临时数组。
 

void merge_sort(int q[], int l, int r){ if(l >= r) return; //当只有一个元素时,没必要递归,直接返回退出。

  • void merge_sort(int q[], int l, int r): 定义一个名为 merge_sort 的函数,接收待排序数组 q、左边界 l 和右边界 r 作为参数。
  • if(l >= r) return;: 当子数组的左边界 l 不小于右边界 r 时,说明子数组只有一个或零个元素,此时已经不需要进一步分割,直接返回。

int mid = l + r >> 1, i = l, j = mid + 1, k = 0; // 先找中点。 merge_sort(q, l, mid); merge_sort(q, mid + 1, r);

  • int mid = l + r >> 1;: 计算中点,l + r >> 1 是 (l + r) / 2 的位运算实现。
  • int i = l, j = mid + 1, k = 0;: 初始化指针 ij 和 ki 指向左子数组的起始位置,j 指向右子数组的起始位置,k 用于标记临时数组 tmp 的位置。
  • merge_sort(q, l, mid); merge_sort(q, mid + 1, r);: 对左半部分 [l, mid] 和右半部分 [mid + 1, r] 递归调用 merge_sort 函数,分别排序这两部分。
 

while(i <= mid && j <= r){ if(q[i] <= q[j]) tmp[k++] = q[i++]; else tmp[k++] = q[j++]; }

  • while(i <= mid && j <= r): 当左子数组和右子数组都有剩余元素时,比较两者的当前元素。
  • if(q[i] <= q[j]) tmp[k++] = q[i++];: 如果左子数组的当前元素 q[i] 小于或等于右子数组的当前元素 q[j],将 q[i] 复制到 tmp[k],然后移动指针 i 和 k
  • else tmp[k++] = q[j++];: 如果右子数组的当前元素 q[j] 小于左子数组的当前元素 q[i],将 q[j] 复制到 tmp[k],然后移动指针 j 和 k
 

while(i <= mid) tmp[k++] = q[i++]; while(j <= r) tmp[k++] = q[j++];

  • while(i <= mid) tmp[k++] = q[i++];: 当左子数组有剩余元素时,将这些元素复制到 tmp 中。
  • while(j <= r) tmp[k++] = q[j++];: 当右子数组有剩余元素时,将这些元素复制到 tmp 中。

for(int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];

  • for(int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];: 将临时数组 tmp 中的排序结果复制回原数组 qi 遍历原数组的范围 [l, r]j 遍历临时数组 tmp 的范围。
 

int main(){ int n; scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%d", &q[i]); merge_sort(q, 0, n - 1); for(int i = 0; i < n; i++) printf("%d ", q[i]); return 0; }

  • int main(): 主函数的开始。
  • int n;: 定义整型变量 n,用于存储数组的大小。
  • scanf("%d", &n);: 从标准输入读取数组的大小 n
  • for(int i = 0; i < n; i++) scanf("%d", &q[i]);: 从标准输入读取 n 个整数并存入数组 q
  • merge_sort(q, 0, n - 1);: 调用 merge_sort 函数对数组 q 进行排序。
  • for(int i = 0; i < n; i++) printf("%d ", q[i]);: 打印排序后的数组。
  • return 0;: 主函数返回 0,表示程序正常结束。

总结

归并排序的基本流程是递归地将数组分成两半,分别对这两半进行排序,然后将它们合并成一个有序数组。该代码实现了归并排序的这一过程,通过分割、排序和合并三个主要步骤来完成数组的排序。

那上面的代码就是归并排序的模板,和快速排序一样要理解与记忆,当涉及到归并排序时,就可以用这个模板。

三、实战例题

给定一个长度为 n的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第 i 个和第 j个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。

输入格式

第一行包含整数 n,表示数列的长度。

第二行包含 n个整数,表示整个数列。

输出格式

输出一个整数,表示逆序对的个数。

数据范围

1≤n≤100000,
数列中的元素的取值范围 [1,10^{9}]。

输入样例:
6
2 3 4 5 6 1
输出样例:
5

 

#include<iostream>using namespace std;
typedef long long LL;//这里是将long long类型定义为其他名字,其实质还是一样的,就相当于自己取了一个别名。
const int N=1e5+10;
int q[N],tmp[N];/*如果只用int的话是装不下的,因为int的取值范围最大是2的31次方,这个题超过了这个范围,所以用long long类型定义为其他名字*/
LL merge_sort(int q[],int l,int r){if(l>=r) return 0;/*这里不仅仅是简单的退出,因为要统计逆序对的数量,所以当只有一个元素的时候直接返回0就行了,表示没有逆序对。*/int mid=l+r>>1,i=l,j=mid+1,k=0;LL res=merge_sort(q,l,mid)+merge_sort(q,mid+1,r);/*这个res表示两种情况的逆序对数量,都在左边的和都在右边的。为什么归并排序的结果会是逆序对的数量呢,这是因为在归并排序中虽然是比较谁更小,然后放入结果数组,但是你比较得出谁更小的同时也得到了这两个数谁更大,所以相当于一石二鸟,既能找到小的那个放入结果数组,也知道了谁更大,从而逆序对的数量也就知道了。*/while(i<=mid&&j<=r){if(q[i]<=q[j]) tmp[k++]=q[i++];else{tmp[k++]=q[j++];res+=mid-i+1;/*这是逆序对数量存在的第三种情况就是上面图里面黄色部分的地方。当已经确定i<r且q[i]>q[j]的时候,i指针后面的值是一定会比j指针所指的值大的,因为左右子数组都是已经有序了的,相当于说i指向的后面的值只会比i更大,本身i现在所指的值就已经大于j所指的值,那i后面的肯定也是大于的,也就是说i指针后面所有的值都可以与j所指的值形成逆序对,所以算出i指针及其后面值的个数就是逆序对的数量,也就是直到左子数组的边界。最后才得到这个mid-i+1。*/}}while(i<=mid) tmp[k++]=q[i++];while(j<=r)  tmp[k++]=q[j++];//这些都是归并排序的正常模板,直接写就行。for(int i=l,j=0;i<=r;i++,j++)  q[i]=tmp[j];return res;//注意返回结果res,因为最后要的是逆序对的数量而不是输出一个序列,所以要返回一下。
}int main(){int n;cin>>n;for(int i=0;i<n;i++)  scanf("%d",&q[i]);cout<<merge_sort(q,0,n-1)<<endl;return 0;
}

补充说明:

1. 逆序对的定义

在一个数组中,逆序对是指数组中前面的元素大于后面的元素。给定数组 arr,逆序对的定义是 (i, j),其中 i < jarr[i] > arr[j]

2. 归并排序简介

归并排序是一种分治算法,步骤如下:

  1. 分解:将数组分成两个子数组。
  2. 解决:递归地对这两个子数组进行归并排序。
  3. 合并:将两个已排序的子数组合并成一个有序数组。

3. 计算逆序对的思路

在归并排序的“合并”步骤中,我们可以同时统计逆序对的数量。具体步骤如下:

  1. 分解:递归地对数组进行分解,直到每个子数组只有一个元素(自然是已排序的)。

  2. 合并:在合并过程中统计逆序对。假设我们有两个已排序的子数组 leftright,我们可以用两个指针分别遍历这两个子数组:

    • 如果 left[i] <= right[j],则 left[i] 和 right[j] 不形成逆序对,移动 left 的指针。
    • 如果 left[i] > right[j],则 left[i] 与 right 子数组中的所有元素都形成逆序对。这个原因是 right 子数组中的元素已经排序,并且比 left[i] 小,所以 right 中的所有元素都和 left[i] 形成逆序对。然后移动 right 的指针。
  3. 统计逆序对:在合并的过程中,累加每次发现的逆序对的数量,最终得到整个数组的逆序对数量。

4. 合并过程中逆序对的统计示例

假设我们有两个已排序的子数组 left = [1, 3, 5]right = [2, 4, 6],我们在合并时可以这样统计逆序对:

  • left[0] 和 right[0] 比较:1 <= 2,不形成逆序对。
  • left[1] 和 right[0] 比较:3 > 2,形成逆序对。此时 right 中的所有元素都比 left[1] 小,所以 left[1] 与 right 中的所有元素形成逆序对。
  • 类似地,继续比较,统计所有可能的逆序对。

5. 时间复杂度

归并排序的时间复杂度是 O(n log n),由于在合并阶段我们额外进行逆序对的统计,这个操作也在 O(n log n) 时间内完成。因此,使用归并排序来计算逆序对是非常高效的。

总结来说,逆序对的数量可以通过归并排序来求得,主要是利用了归并过程中对已排序子数组的合并操作来统计逆序对。这样可以在 O(n log n) 的时间复杂度内完成计算。

 


http://www.ppmy.cn/news/1516527.html

相关文章

pnpm快速入门

pnpm快速入门 1.使用pnpm启动项目 pnpm是一个优化的包管理器&#xff0c;它通过锁定工作树的方式来减少依赖安装的开销。要在pnpm环境中启动项目&#xff0c;首先你需要确保已经全局安装了pnpm。然后按照以下步骤操作 克隆项目&#xff1a;如果项目还没有下载&#xff0c;使用…

Linux基础 - yum、rzsz、vim 使用与配置、gcc/g++的详细解说

目录 一、Linux 软件包管理器 yum A.什么是软件包&#xff1f; B.关于rzsz&#xff0c;yum的配置 1.安装 sz&#xff0c;rz 命令&#xff1a; a.执行命令sz可将linux中的文件传输到Windows中 b.执行rz命令可将Windows中的文件传输到linux 2.scp XXX.tgz 用户名另一台lin…

2024最新FL Studio24.1.1.4285破解版中文安装包百度云网盘下载地址

大家好&#xff0c;今天我要给大家介绍一款音乐制作神器——FL Studio 24.1.1.4285中文版。这款软件可是音乐制作界的翘楚&#xff0c;无论是专业人士还是音乐爱好者&#xff0c;都会为它的强大功能和易用性所折服。 我们来看看FL Studio的特点。 这是一款全能型的音乐工作站&…

el-form中使用v-model和prop实现动态校验

如何在Vue的el-form中使用v-model和prop实现动态校验&#xff0c;包括多个变量控制校验、数组循环校验和字段级条件显示。通过实例演示了如何配合rules和自定义验证函数来确保表单的完整性和有效性。 公式&#xff1a; 动态校验项的v-model的绑定值 el-form的属性 :model的值 …

SystemTap(stap)架构和原理介绍,以及脚本编写举例

1 SystemTap简介 SystemTap是一个诊断Linux系统性能或功能问题的开源工具。它允许开发人员和系统管理员深入研究内核甚至用户空间应用程序的行为&#xff0c;以便发现错误状态、性能问题&#xff0c;或者仅仅为了解系统是如何工作的。它使得对运行时的Linux系统进行诊断调式变…

Windows安装Tomcat10

1. 下载Tomcat Tomcat官网 https://tomcat.apache.org/download-10.cgi ​下载安装jdk17 &#xff1a;jdk-17_windows-x64_bin.exe 配置JAVA环境变量 JAVA_HOME&#xff1a;C:\Program Files\Java\jdk-17 PATH&#xff1a;%Java_Home%\bin;%Java_Home%\jre\bin; 2. 设置环境变…

【13.3 python中的高级文件操作】

python中的高级文件操作 在Python中&#xff0c;除了基本的文件读写和目录操作外&#xff0c;还有一些高级的文件和目录操作&#xff0c;如删除文件、重命名文件和目录、以及获取文件的基本信息等。这些操作通常通过os模块和pathlib模块来实现。下面我将详细介绍这些操作&#…

电脑换硬盘怎么全盘克隆?轻松实现数据迁移

随着科技的不断发展&#xff0c;电脑硬盘的存储容量和读写速度也在不断提升。为了获得更好的电脑使用体验&#xff0c;许多用户会选择更换更大容量、更高效的硬盘。然而&#xff0c;在更换硬盘的过程中&#xff0c;一个关键的问题摆在了我们面前&#xff1a;如何将旧硬盘中的所…

物联网---ESP32

物联网---ESP32 一、TCP/IP协议(互联网协议)二、MQTT协议(通信协议)2.1 MQTT基本原理2.2 连接MQTT服务端 三、ESP323.1 ESP介绍3.2 ESP32连接云端3.2.1 ESP32连接WIFI/MQTT3.2.2 OneNET云端 一、TCP/IP协议(互联网协议) TCP/IP是一组用于互联网及其他网络中数据传输的通信协议…

hutool工具类JSONUtil无法映射全是大写的单词,如何解决

背景 在解析第三方接口数据时&#xff0c;发现有的字段数据没有映射到对应的字段上&#xff0c;还有对于有的字段有空格或换行&#xff0c;也会一同存入数据库。 示例 实体类&#xff1a; public class Goods { private String id;private String unit;private Integer US…

HexView 刷写文件脚本处理工具-命令行介绍(八)-文件合并(/MO /MT)

介绍 /MO 和 /MT 参数:用于将一个或多个文件合并到程序的内部数据存储中。文件读取:使用第2.2.1.2.1节中描述的自动检测文件类型机制来读取文件。合并操作类型:需要选择合并操作的类型。可以选择透明模式(/MT)或不透明模式(/MO),两者不能混合使用。透明模式(/MT):加载的文…

黑神话悟空无法登录服务器怎么办

黑神话悟空游戏在登录的时候会遇到无法登录服务器的问题&#xff0c;玩家可以采用一些有效的方法进行解决&#xff0c;其中最主要的措施就是优化网络环境和减少网络干扰。Rak小编为您整理黑神话悟空无法登录服务器如何解决的步骤及注意事项。 优化网络环境 1、当游戏无法登录服…

使用notepad++将shell脚本转为UNIX格式方法(主要差别在换行符)

sh文件尽量在linux上改&#xff0c;因windows和linux换行符不同&#xff0c;在windows上改后&#xff0c;在linux上改可能会出现换行符错误。 windows换行符 linux换行符 windows环境改换行符方法 使用notepad点 编辑–》文档格式转换–》转换未unix格式。 注&#xff1a;tx…

搭建ELK-Filebeat采集系统日志

1、解压到/data/elk/filebeat mkdir -p /data/elk/filebeat tar -zxf filebeat-7.17.7-linux-x86_64.tar.gz -C /data/elk/filebeat --strip-components1 #--strip-components选项表示从目录级别上去除指定的前缀&#xff0c;以实现更加控制解压的效果 2、修改配置文件 vi /…

【长文细说】20个ElementPlus核心组件以及使用技巧

Element Plus 是一个基于 Vue 3 和 Vite 的组件库&#xff0c;它提供了一套丰富的 UI 组件&#xff0c;用于构建高质量的网页应用程序。Element Plus 是 Element UI 的 Vue 3 版本&#xff0c;Element UI 是一个广泛使用的 Vue 2 组件库。Element Plus 继承了 Element UI 的设计…

Qt5.14.2 操作PostgreSQL 记录

在Qt5.14.2中操作PostgreSQL数据库. #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QDebug>// 初始化数据库连接QSqlDatabase db QSqlDatabase::addDatabase("QPSQL");//qDebug() << "aaaa&qu…

构建第一个zk

1 必要步骤 视频学习&#xff1a;5. Circcom 中的基本算术电路_哔哩哔哩_bilibili 文字学习&#xff1a;https://hackmd.io/YlNLZS2ESI21OSqdTW_mPw/S1jqN-h80/edit 第五课&#xff0c;circom实践&#xff0c;需要安装 1 vscode 2 rust&#xff1a;Windows安装Rust环境&…

FFmpeg 实现从设备端获取音视频流并通过RTMP推流

使用FFmpeg库&#xff08;版本号为&#xff1a;4.4.2-0ubuntu0.22.04.1&#xff09;实现从摄像头和麦克风获取音视频流并通过RTMP推流。 RTMP服务器使用的是SRS&#xff0c;我这边是跑在Ubuntu上的&#xff0c;最好是关闭掉系统防火墙&#xff0c;不然连接服务器好像会出问题&a…

python手写了个简易的豆瓣影评爬虫

使用python手写了个简易的豆瓣影评爬虫代码。 __author__ wsximport time import requests from bs4 import BeautifulSoup import os import re import uuiddef clean_windows_filename(string_file_name):invalid_chars r[\\/:*?"<>|]return re.sub(invalid_c…

ZooKeeper 的特性及其在分布式系统中的配置中心的应用

以下是配置管理和服务注册的实现方式&#xff1a; 1. 配置管理 配置管理指的是将系统中各个组件的配置信息集中管理&#xff0c;以便动态更新和统一配置。ZooKeeper 可以用来管理配置文件&#xff0c;通过它的节点结构和数据一致性功能&#xff0c;确保所有客户端都能获得最新…