深入理解归并排序

news2025/2/24 23:24:11

目录

一、概念

二、递归版实现 

三、非递归实现

三、文件归并排序

小结


一、概念

        归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

        其思想可用下图来表示:

 

        从上图我们可以看到,归并的大体思路为:先保证小区间有序,再保证大区间有序。在思想上体现出了:分而治之的理念。

        可总结为以下两点:

  1. 将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
  2. 将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。

二、递归版实现 

        对于用递归实现这个排序,我们可这样解决:

        1. 开辟一个新数组,用于存放每次排完序的值。

        2. 找到这个数组的最小单位,两两比较。

        3. 每完成一组排序,便把新数组拷贝给原数组。

        4. 重复以上操作,直到排序完成。

        代码实现: 

void _MergeSort(int* a, int* tmp, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) / 2;
	// 如果[begin, mid][mid+1, end]有序就可以进行归并了
	_MergeSort(a, tmp, left, mid);
	_MergeSort(a, tmp, mid + 1, right);
	//归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);

	free(tmp);
	tmp = NULL;
}

三、非递归实现

        我们用递归解决这个排序似乎是件较容易的事情,但对于我们想要用非递归实现来说,仍有不小的挑战。我们说一下实现思路:

        1.我们要解决如何实现分组问题

        2.我们引入gap变量用它来进行控制分组

        3.分组运用gap不同的值来确定每个组的大小,从小往大依次来实现归并。

        注意点:

        1. 当第二组开始位置 超过 / 等于 该数组长度时,我们此时可认为以排序完成,break即可。

        2. 当第二组结束位置  超过 / 等于 该数组长度时,我们要将其大小置为n-1。

        代码实现如下:

void MergeSortNonR(int* a, int n)
{
	int* tmp = malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			// [begin1, end1][begin2, end2]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 第二组都越界不存在,这一组就不需要归并
			if (begin2 >= n)
			{
				break;
			}
			// 第二的组begin2没越界,end2越界了,需要修正一下,继续归并
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			
		}
		gap *= 2;
	}
	memcpy(a + n - 1, tmp + n - 1, sizeof(int) * (n - 1));
	free(tmp);
	tmp = NULL;
}

        这里大家估计会有点小疑惑,疑惑什么呢?为什么不能tmp归并完我们在把它拷给a,一步一步拷不麻烦吗?

        我们能不能直接拷呢?大家可以去操作一下,答案很显然:不可以! 原因如下:

        这个本身的话,就是每次循环结束,在拷贝数组和临时数组的值进行交换,之后就是在临时数组改变之后的情况下,在进行第二次循环排序,之后。把拷贝后的数据在进行分组合并,每次循环里面都是对a合并后的数据在做处理,如果说全部执行完再拷贝,那a每次并没有啥变化,当然就不可能完成归并排序整个过程。

        各位感兴趣的话可以打印验证一下。 

三、文件归并排序

        关于这个问题,我们给出以下情景:在今年,你怀着忐忑的心情去参加秋招,顺利通过了笔试,在面试时,面试官的问题你都对答入流,直到最后一题:给你1G的空间,你如何使10G的数据有序,这时,你看过本博主写得TOP-K问题(二叉树——堆详解_堆 二叉树-CSDN博客),你自信满满的回答了这个问腿,面试官觉得你很不错,便提问到:如果用归并该如何解决呢?你不由想起了这篇博客,也就是目前各位读者所看的这篇,以下是解题思路:

        1. 首先,先创建三个文件:file1,file2,mfine。

        2.读取n个值排序后写到file1,再读取n个值排序后写到file2

        3. file1和file2利⽤归并排序的思想,依次读取⽐较,取⼩的尾插到mfile,mfile归并为⼀个有序⽂件

        4. 将file1和file2删掉,mfile重命名为file1

        5. 再次读取n个数据排序后写到file2

        6. 继续⾛file1和file2归并,重复步骤2,直到⽂件中⽆法读出数据。最后归并出的有序数据放到了 file1中

        对于删掉文件和改文件名,我们可通过remove 和rename 函数来完成(可点击查看其用法)。

        代码实现:

//造数据
void CreateNDate()
{
	const char* file = "text.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fail error");
		return;
	}
	srand((unsigned)time(NULL));
	int n = 100;
	for (int i = 0; i < n; i++)
	{
		int x = rand() + i;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}
int comper(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

// 返回实际读到的数据个数,没有数据了,返回0
int ReadNDataSortToFile(FILE* fout, int n, const char* file)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		return 0;
	}
	int x = 0;
	// 想读取n个数据,如果遇到文件结束,应该读到j个
	int j = 0;
	for (int i = 0; i < n; i++)
	{
		if (fscanf(fout, "%d", &x) == EOF)
		{
			break;
		}
		tmp[j++] = x;
	}
	if (j == 0)
	{
		free(tmp);
		return 0;
	}
	//快排
	qsort(tmp, j, sizeof(int), comper);

	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("file error");
		return 0;
	}
	// 写回file1文件
	for (int i = 0; i < j; i++)
	{
		fprintf(fin, "%d\n", tmp[i]);
	}

	free(tmp);
	fclose(fin);
	return j;
}

void MergeFile(const char* file1, const char* file2, const char* mfile)
{
	FILE* fin1 = fopen(file1, "r");
	if (fin1 == NULL)
	{
		perror("file error");
		return;
	}
	FILE* fin2 = fopen(file2, "r");
	if (fin2 == NULL)
	{
		perror("file error");
		return;
	}
	FILE* mfin = fopen(mfile, "w");
	if (mfin == NULL)
	{
		perror("file fail");
		return;
	}
	//归并逻辑
	int x1 = 0, x2 = 0;
	int ret1 = fscanf(fin1, "%d", &x1);
	int ret2 = fscanf(fin2, "%d", &x2);
	while (ret1 != EOF && ret2 != EOF)
	{
		if (x1 < x2)
		{
			fprintf(mfin, "%d\n", x1);
			ret1 = fscanf(fin1, "%d", &x1);
		}
		else
		{
			fprintf(mfin, "%d\n", x2);
			ret2 = fscanf(fin2, "%d", &x2);
		}
	}

	while (ret1 != EOF)
	{
		fprintf(mfin, "%d\n", x1);
		ret1 = fscanf(fin1, "%d", &x1);
	}
	while (ret2 != EOF)
	{
		fprintf(mfin, "%d\n", x2);
		ret2 = fscanf(fin2, "%d", &x2);
	}

	fclose(fin1);
	fclose(fin2);
	fclose(mfin);
}
void test()
{
	/*CreateNDate();*/
	const char* file1 = "file1.txt";
	const char* file2 = "file2.txt";
	const char* mfile = "mfile.txt";
	FILE* fout = fopen("text.txt", "r");
	if (fout == NULL)
	{
		perror("file error");
		return;
	}
	int m = 10;
	ReadNDataSortToFile(fout, m, file1);
	ReadNDataSortToFile(fout, m, file2);
	while (1)
	{
		MergeFile(file1, file2, mfile);
		remove(file1);
		remove(file2);
		rename(mfile, file1);
		if (ReadNDataSortToFile(fout, m, file2) == 0)
		{
			break;
		}
	}
}

小结

        本文对于归并排序做了较为深入的讲述。主要讲述了:归并排序的递归版、非递归版以及文件归并排序问题。大家重点掌握归并排序即可,对于学有余力者,可研究其文件归并排序。好了,本文的内容到这里就结束了,如果觉得有帮助,还请一键三连多多支持一下吧!

完!

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

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

相关文章

海外新闻稿发布:企业如何充分利用数字化媒体进行

在全球数字化进程加速的时代&#xff0c;企业要在激烈的国际市场中脱颖而出&#xff0c;利用数字化媒体进行海外新闻稿发布是一个不可或缺的战略。精确的策略和有效的执行能够帮助企业获得更高的曝光度和市场份额。以下将从多个角度探讨如何充分利用数字化媒体进行海外新闻稿发…

万亿生成式AI市场,商汤迎来“长坡厚雪”

AI掀起了全球科技玩家的军备竞赛&#xff0c;然而声浪越强噪音越多&#xff0c;这个领域的混乱程度也变得远超以往。就连刚刚公布财报的英伟达&#xff0c;市场也没有买账&#xff0c;因为担心AI驱动的增长高峰已过&#xff0c;接下来&#xff0c;下游会更看重实际成果。 “囤…

【电子数据取证】微信8.0.50版本数据库解密

文章关键词&#xff1a;电子数据取证、手机取证、微信取证、数据库解密 通过对8.0.50这一特定版本的分析&#xff0c;我们期望揭示软件迭代背后的逻辑思考&#xff0c;以及安全策略的演进方向。这不单纯是对技术细节的揭秘&#xff0c;更是一次关于未来通信安全趋势的展望&…

在Linux中如何安装JDK

一、卸载JDK &#xff08;可以不删除&#xff0c;直接安装新的JDK&#xff0c;然后修改环境变量&#xff09; 1.1卸载使用yum安装的jdk 1.1.1卸载系统预安装的JDK 使用命令&#xff1a;yum list installed |grep java 注意&#xff1a;该命令只能查看使用yum命令安装的jav…

python模块和包的区别有哪些

模块&#xff1a;就是.py文件&#xff0c;里面定义了一些函数和变量&#xff0c;需要的时候就可以导入这些模块。 包&#xff1a;在模块之上的概念&#xff0c;为了方便管理而将文件进行打包。包目录下第一个文件便是 __init__.py&#xff0c;然后是一些模块文件和子目录&…

pytorch 均方误差损失函数

均方误差损失函数主要用于回归问题。它计算预测值与真实值之间差的平方&#xff0c;然后取平均值。这个损失函数通过惩罚大的误差&#xff0c;使得模型在训练时更加注重减少较大的偏差。 import torch import torch.nn as nn# 创建预测值和实际值张量 predicted torch.tensor(…

Spring Boot 中的 “依赖管理和自动配置” 详解透彻到底(附+详细代码流程)

1. 如何理解 “ 约定优于配置 ” 约定优于配置&#xff08;Convention over Configuration / CoC&#xff09;,又称约定编程&#xff0c;是一种软件设计规范&#xff0c;本质上是对系统&#xff0c;类库或框架中一些东西。 一个大众化合理的默认值&#xff08;缺省值&#xff0…

VSCode+Keil协同开发之Keil Assistant

VSCodeKeil协同开发之Keil Assistant 目录 VSCodeKeil协同开发之Keil Assistant1. 效果展示2. Keil Assistant简介3. Keil Assistant功能特性4. 部署步骤4.1. 1.部署准备4.2. 2.安装Keil Assistant插件4.3. 3.配置Keil Assistant插件 5. Keil Assistant使用6. 总结 大家在单片机…

java 使用网易邮箱发送邮件

java 使用网易邮箱发送邮件 准备条件 网易邮箱账号开通邮箱的POP3/SMPT服务&#xff0c;申请授权码 引入工具包 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version></de…

深入浅出Entity-Component-System:重塑游戏开发的未来

引言 在游戏开发领域&#xff0c;架构设计往往决定了项目的成败。随着游戏规模和复杂度的不断增加&#xff0c;传统的面向对象编程(OOP)模式逐渐显露出其局限性。而ECS(Entity-Component-System)架构作为一种新兴的设计模式&#xff0c;正在彻底改变游戏开发的方式。本文将深入…

C# 安卓开发1(基于MAUI框架)

1&#xff1a;什么是 .NET MAUI&#xff1f;这里参考一下官方文档&#xff0c;因为解释的更详细&#xff0c;点击这里&#xff1b; 2&#xff1a;开发工具VS2022&#xff0c;下载下图开发框架 3: 安装完成后创建新项目,选择net6.0长期支持(创建文件的存放路径不要有中文): 4:…

FPGA速度优化

速度优化 文章目录 速度优化前言一、时序优化1.1 减少关键路径上的时序1.1.1 关键路径重组1.1.2 解决扇出问题1.1.3 路径上插入寄存器1.1.4 寄存器平衡1.1.5 并行结构1.1.6 消除代码优先级 总结 前言 速度优化&#xff0c;主要就是设计时序进行优化 吞吐量&#xff1a;每个时…

bbr 随机 phase 的麻烦与 inflight 守恒算法的动机

bbr 有个要点&#xff0c;要把 probebw 的 phase 错开&#xff1a; static void bbr_reset_probe_bw_mode(struct sock *sk) {struct bbr *bbr inet_csk_ca(sk);bbr->mode BBR_PROBE_BW;bbr->cycle_idx CYCLE_LEN - 1 - prandom_u32_max(bbr_cycle_rand);bbr_advance…

炒现货黄金白银通用的技术

要在现货黄金、现货白银等市场&#xff0c;甚至是股票、期货等其他以市场走势为分析对象的市场&#xff0c;我们都需要熟练的掌握一些交易的技术&#xff0c;这些技术可以为我们入场交易打好基础&#xff0c;让我们获得比随机交易更高一点的概率。下面我们就来讨论一下炒现货黄…

关于el-table的show-summary,合计栏不显示以及保留两位小数问题

<el-tableref"table1"v-loading"loading":data"":stripe"true"height"600"show-summary:summary-method"getSummaries":show-overflow-tooltip"true">...</el-table>合计部分不显示的问题 …

安全升级:Docker部署Redis,启用密码验证

1.在自己选定的目录中创建文件夹 在redis文件夹里面创建&#xff1a;data文件夹和conf文件夹&#xff08;文件夹名称随意&#xff09; 2.在conf文件夹中创建redis.conf文件&#xff1a; vim redis.conf 2.1.redis.conf里面编写内容可以根据官网&#xff08;Index of /releases…

Threejs之OrbitControls轨道控制器

本文目录 前言一、Orbitcontrols&#xff08;轨道控制器&#xff09;1.1 基础使用1.2 代码演示 二、效果展示 前言 Orbitcontrols&#xff08;轨道控制器&#xff09;可以使得相机围绕目标进行轨道运动。 一、Orbitcontrols&#xff08;轨道控制器&#xff09; 1.1 基础使用 C…

Hreflang 和 SEO:新手完整指南

每天&#xff0c;数以百万计的法国用户访问像 Amazon.com 这样的全球网站。虽然 Amazon.com 的官方页面是英文的&#xff0c;但用户仍然可以看到法语的文本和产品描述。这是因为亚马逊的全球网站有针对法国的本地化版本&#xff0c;确保所有法国用户都可以自动看到法语的网站内…

ELK学习笔记——如何给Kibana新增用户和角色

Kibana新增用户和角色 首先用超管账号登录上Kibana&#xff0c;按照下面步骤操作 1、创建角色 按图操作 2、创建用户 按图操作 3、给用户分配角色 至此&#xff0c;角色和用户绑定成功&#xff1b; 最后&#xff0c;可以退出管理员账号&#xff0c;登录这个新…

Vue的状态管理——Vuex34Pinia

Vue3中Vuex的使用_vue3 vuex-CSDN博客 VueX详解_组合式vuex-CSDN博客 15分钟学会Pinia Vuex 3和4详解 Vuex 3 Vuex 3是Vue.js 2.x版本的状态管理库&#xff0c;它提供了一种集中式存储和管理组件状态的方式。以下是Vuex 3的一些关键特性&#xff1a; 状态集中管理&#x…