【算法】---归并排序(递归非递归实现)

news2024/11/28 8:42:21

参考

左程云算法
算法导论

前言

本篇介绍

  • 归并排序
  • 分治法

前置知识

  • 了解递归, 了解数组。

引入

归并排序
归并排序最早是由公认的现代计算机之父John von Neumann发明的, 这是一种典型的分治思想应用。

我们先介绍分治思想
分治思想
分治思想的想法基于递归, 许多算法可以通过自身调用, 来降低问题的规模, 又获得与原问题类似的子问题。可见, 分治法本质是递归思想的一个分支,分治法还要额外进行处理, 先进行递归地求解子问题, 然后合并这些子问题的解求出原问题的解。

通过一个简单的例子, 来讲解分治思想。
给定一个int类型的数组, 求解该数组的最大值。
你可能已经很熟悉了, 线性遍历即可

public static int getMaxValue1(int[] arr) {
		int n = arr.length;
		//max,初始默认为系统最小值。
		int max = Integer.MIN_VALUE;
		//无序数组, 直接遍历
		for(int i=0;i<n;i++) {
			max = Math.max(max, arr[i]);
		}
		return max;
	}

分治思想如何运用呢? 原数组区间范围在[0, arr.length - 1],求解原数组的大小可以被递归解决吗?
很有可能的想法, 直观上, 我们求解原数组的最大值就是求解在原数组序列中最大值。只需要等分序列即可, 将原数组序列的最大值,分解成左右子序列的最大值问题, 从递归上,对子序列可以同样这样处理,直到不需要递归继续降解规模了(递归模式结束, 处理基线条件,以防止死递归了)。
问题在于, 规模确实减小了,但左序列的最大值不一定是整个数组的最大值。 直觉上, 将左右序列的最大值进行比较,决定合并两个序列的最大值。
为此, 我们可以写出分治法的求解子问题的写法。

public static int getMaxValue(int[] arr) {
		if(arr==null||arr.length==0) {
			return Integer.MIN_VALUE;//处理值为null,传参为空数组的情况。
		}
		return process(arr, 0, arr.length-1);
	}
	
	public static int process(int[] arr, int l, int r) {
		if(l>r) {
			return Integer.MIN_VALUE;
		}
		if(l==r) {
			return arr[l];//直接返回值
		}
		//递归条件, 降低规模
		
		//分治的过程
		int mid = (l+r)/2;
		int lmax = process(arr, l, mid);
		int rmax = process(arr, mid+1, r);
		
		//合并的过程。
		return Math.max(lmax, rmax);
	}
	
	public static void main(String[] args) {
		int[] arr = {3,5,1,7,8,9,11,2};
		//对比两种方法, 观察结果是否一致。 相互验证!
		System.out.println("最大值:"+getMaxValue1(arr));
		System.out.println("最大值:"+ getMaxValue(arr));
	}
/**
 *  output:
 *  最大值:11
 *  最大值:11
 */

总结
对于每层递归:
分治法有三个步骤

  • 分解原问题为若干子问题(不一定像上述例子二等分), 子问题是规模减小的原问题。
  • 递归地处理这些子问题, 核心是什么时候继续递归分解, 什么时候直接求解。—写递归时必须想明白, 否则StackOverflow等着你。
  • 合并处理子问题的解进而求出原问题的解。

能不能降低规模, 处理好递归,以及合并这个操作具体怎么写。写好分治法的难点。

归并排序

归并排序也是一种分治思想的体现。
基本思想就是,左边有序,右边有序。然后调整为整体有序。

  • 分割: 将数组序列二等分, 利用递归不断分解。
  • 合并: 合并两个有序序列,保持原来的元素的相对顺序不变。

结合代码和下面图片

	public static void mergeSort(int[] arr) {
		//无效值null, 数组元素不为2,不需要排序。
		if(arr==null || arr.length<2) {
			return ;
		}
		//开始
		process(arr,0, arr.length-1);
	}
	
	public static void process(int[] arr, int l, int r) {
		//基线条件处理
		if(l>=r) {
			return ;
		}
		//选中分割下标, 直接取中值即可
		int mid = (l+r)/2;
		//左边递归调用
		process(arr,l, mid);
		//右边递归调用, 注意参数
		process(arr,mid+1,r);
		//合并操作
		merge(arr,l,mid,r);
	}
	
	public static void merge(int[] arr,int l,int m, int r) {
		int i = 0;
		int a = l;
		int b = m+1;
		//拷贝一个临时数组
		int[] help = new int[r-l+1];
		//比大小的过程
		while(a<=m && b<=r ) {
			help[i++]=arr[a]<=arr[b]?arr[a++]:arr[b++];
		}
		//处理剩余的序列
		while(a<=m) {
			help[i++] = arr[a++];
		}
		while(b<=r) {
			help[i++] = arr[b++];
		}
		//将数据拷贝回原序列。
		for(i=l;i<=r;i++) {
			arr[i] = help[i-l];
		}
	}
	//测试用例。
	public static void main(String[] args) {
		int[] arr= {4,1,6,23,8,9,11,0,2,3,4,4,4,4,10};
		System.out.println("排序前:"+Arrays.toString(arr));
		mergeSort(arr);
		System.out.println("排序后:"+Arrays.toString(arr));
	}
	/**
	 * output:
	 * 排序前:[4, 1, 6, 23, 8, 9, 11, 0, 2, 3, 4, 4, 4, 4, 10]
	 * 排序后:[0, 1, 2, 3, 4, 4, 4, 4, 4, 6, 8, 9, 10, 11, 23]
	 */

在这里插入图片描述
开始有一个主方法,mergeSort,只需要传递一个int数组即可。
process这一函数是不断递归地分解问题。merge函数是服务当前的process函数。
比如,[4,8],[5,7]给定两个子序列, 通过分离双指针和临时数组help进行比较,原理是拷贝完较小的数组,然后拷贝完剩余的数组。
先将两个序列的比较结果拷贝进help数组,help=[4,5,7],此时还剩下一个元素8,因为没有数可比了,序列[5,7]的指针已经走到了尽头。只需要挨个检查将剩下的元素依次拷贝进help数组即可。
以上就是对这行代码的解释:

		//比大小的过程
		while(a<=m && b<=r ) {
			help[i++]=arr[a]<=arr[b]?arr[a++]:arr[b++];
		}
		//处理剩余的序列
		while(a<=m) {
			help[i++] = arr[a++];
		}
		while(b<=r) {
			help[i++] = arr[b++];
		}

最后, 将临时数组help存储的有序序列依次拷贝回原数组的对应序列即可。注意这里是原数组进行修改。

		//将数据拷贝回原序列。
		for(i=l;i<=r;i++) {
			arr[i] = help[i-l];
		}

递归版的复杂度

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn), 系统压栈高度为logn,merge函数时间 O ( n ) O(n) O(n), 乘起来就是 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度: O ( n ) O(n) O(n), 借助了一个临时数组help.

非递归实现

public static void mergeSort(int[] arr) {
		int n = arr.length;
		//step分组数, step为1,说明左右区间各有一个数(除非区间已经越界, 则相应调整)
		//先两两分组, 再以4个为一组, 8个为一组...直到单次分组已经超过数组总的元素个数就终止。
		for (int l, m, r, step = 1; step < n; step <<= 1) {
			l = 0;
			//后面就是讨论区间
			while (l < n) {
				//确定区间的中间下标
				m = l + step - 1;
				//判断右边界是否存在
				if (m + 1 >= n) {
					//无右侧, 不用后续merge了。
					break;//不存在说明单层的归并排序结束。
				}
				//求右边界
				r = Math.min(l + (step << 1) - 1, n - 1);
				
				//确定好了,l,m,r的值,进行合并
				merge(arr, l, m, r);
				//内层while循环进行调整
				l = r + 1;
			}
		}
	}
	public static void merge(int[] arr,int l, int m,int r) {
		int a = l;
		int b = m+1;
		int[] help = new int[r-l+1];
		int i = 0;
		while(a<=m && b<=r) {
			help[i++] = arr[a]<=arr[b]?arr[a++]:arr[b++];
		}
		
		while(a<=m) {
			help[i++] = arr[a++];
		}
		
		while(b<=r) {
			help[i++] = arr[b++];
		}
		
		for(i=l;i<=r;i++) {
			arr[i] = help[i-l];
		}
	}
	
	//测试用例。
	public static void main(String[] args) {
		int[] arr= {4,1,6,23,8,9,11,0,2,3,4,4,4,4,10};
		System.out.println("排序前:"+Arrays.toString(arr));
		mergeSort(arr);
		System.out.println("排序后:"+Arrays.toString(arr));
	}
	/**
	 * output:
	 * 排序前:[4, 1, 6, 23, 8, 9, 11, 0, 2, 3, 4, 4, 4, 4, 10]
	 * 排序后:[0, 1, 2, 3, 4, 4, 4, 4, 4, 6, 8, 9, 10, 11, 23]
	 */

归并排序为什么如此高效, 左神说过是因为比较排序中的比较次数没有浪费。 确实如此, 比较排序可以抽象为决策树模型, 比较次数最少就是 n l o g n nlogn nlogn, 而对于三大平方的‘傻瓜式’排序算法, 因为浪费了比较次数,导致时间复杂度变高了。

练习

	//请用递归和非递归方法实现。
	//阐述一下归并排序的思想。
	public static void mergeSort(int[] nums) {
		/*write code here! */
	}

你已经学会了归并排序了, 快速试试吧!。

总结

本篇并不涉及算法的严格分析, 因为算法导论一书中已经写好了严谨有力的证明(算法导论第二章和第4章)。
下次见!

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

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

相关文章

java:pdfbox 3.0 去除扫描版PDF中文本水印

官网下载 https://pdfbox.apache.org/download.html下载 pdfbox-app-3.0.3.jar cd D:\pdfbox 运行 java -jar pdfbox-app-3.0.3.jar java -jar pdfbox-app-3.0.3.jar Usage: pdfbox [COMMAND] [OPTIONS] Commands:debug Analyzes and inspects the internal structu…

(C语言贪吃蛇)7.显示贪吃蛇完整身体改进

前言 上节显示了贪吃蛇身子的三个节点&#xff0c;但是吃了食物后蛇身变长应该如何操作&#xff0c;本节给出答案。 一、贪吃蛇身体是什么&#xff1f; 使用链表这个数据结构来动态的显示贪吃蛇的身体。 二、对贪吃蛇身体进行改进 1.贪吃蛇身子显示 代码如下&#xff1a; …

信息学奥赛使用的编程IDE:Dev-C++ 安装指南

信息学奥赛&#xff08;NOI&#xff09;作为全国性的编程竞赛&#xff0c;要求参赛学生具备扎实的编程能力&#xff0c;而熟练使用适合的编程工具则是学习与竞赛的基础。在众多编程环境中&#xff0c;Dev-C IDE 因其简洁、轻量、支持C编程等特点&#xff0c;成为许多参赛者的常…

最新版的dubbo服务调用(用nacos做注册中心用)

一、介绍 1.1、什么是 nacos Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集&a…

Java 每日一刊(第21期):反射机制

文章目录 前言动态插件系统面临的问题如何在运行时动态加载和调用类与方法设计模式的尝试引入反射 Java 反射的核心概念Class 类Constructor 类Method 类Field 类 Java 反射的应用场景框架开发插件系统序列化与反序列化动态代理测试工具 反射的优缺点反射实战动态加载类并调用方…

【hot100-java】【将有序数组转换为二叉搜索树】

二叉树篇 BST树 递归直接实现。 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNo…

【C++差分数组】2381. 字母移位 II|1793

本文涉及知识点 C差分数组 LeetCode2381. 字母移位 II 给你一个小写英文字母组成的字符串 s 和一个二维整数数组 shifts &#xff0c;其中 shifts[i] [starti, endi, directioni] 。对于每个 i &#xff0c;将 s 中从下标 starti 到下标 endi &#xff08;两者都包含&#…

STM32的串行外设接口SPI

一、SPI简介 1.SPI总线特点 &#xff08;1&#xff09;四条通信线 SPI需要SCK、MISO、MOSI、NSS四条通信线来完成数据传输 &#xff0c;每增加一个从机&#xff0c;多一条NSS通信线。 &#xff08;2&#xff09;多主多从 SPI总线允许有多个主机和多个从机。 &#xff08;3&…

再见 ESNI,你好 ECH!—— ECH的前世今生

译者注&#xff1a;2024 年 9 月 25 日&#xff0c;Cloudflare 宣布再次推出 ECH 功能。借此契机&#xff0c;本人翻译了 Cloudflare 介绍 ECH 的博文 Good-bye ESNI, hello ECH! &#xff0c;以便科普ECH的发展历程。 现代互联网上的大多数通信都经过加密&#xff0c;以确保其…

Flink源码剖析

写在前面 最近一段时间都没有更新博客了&#xff0c;原因有点离谱&#xff0c;在实现flink的两阶段提交的时候&#xff0c;每次执行自定义的notifyCheckpointComplete时候&#xff0c;好像就会停止消费数据&#xff0c;完成notifyComplete后再消费数据&#xff1b;基于上述原因…

在Stable Diffusion WebUI中安装SadTalker插件时几种错误提示的处理方法

SD中的插件一般安装比较简单&#xff0c;但也有一些插件安装会比较难。比如我在安装SadTalker时&#xff0c;就遇到很多问题&#xff0c;一度放弃了&#xff0c;后来查了一些网上攻略&#xff0c;自己也反复查看日志&#xff0c;终于解决&#xff0c;不吐不快。 一、在Stable …

ElasticSearch高级功能详解与读写性能调优

目录 1. ES数据预处理 1.1 Ingest Node Ingest Node VS Logstash 1.2 Ingest Pipeline Pipeline & Processor 创建pipeline 使用pipeline更新数据 借助update_by_query更新已存在的文档 1.3 Painless Script Painless的用途&#xff1a; 通过Painless脚本访问字…

(17)MATLAB使用伽马(gamma)分布生成Nakagami-m分布的方法1

文章目录 前言一、使用伽马分布生成Nakagami分布随机变量的方法一二、MATLAB仿真代码后续 前言 MATLAB在R2013a版本中引入Nakagami分布对象&#xff0c;可以用来生成Nakagami随机变量。但是在更早的MATLAB版本中&#xff0c;并没有可以直接生成 Nakagami分布的随机变量的内置的…

C++之多态篇(超详细版)

1.多态概念 多态就是多种形态&#xff0c;表示去完成某个行为时&#xff0c;当不同的人去完成时会有不同的形态&#xff0c;举个例子在车站买票&#xff0c;可以分为学生票&#xff0c;普通票&#xff0c;军人票&#xff0c;每种票的价格是不一样的&#xff0c;当你是不同的身…

【JAVA开源】基于Vue和SpringBoot的旅游管理系统

本文项目编号 T 063 &#xff0c;文末自助获取源码 \color{red}{T063&#xff0c;文末自助获取源码} T063&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

【STM32开发之寄存器版】(二)-USART

一、前言 串口作为STM32的重要外设&#xff0c;对程序调试具有不可替代的作用。通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。其主要具备以下特性&am…

Nacos入门指南:服务发现与配置管理的全面解析

Nacos 是一个用于动态服务发现、配置管理和服务管理的平台。它由阿里巴巴开源&#xff0c;旨在帮助开发者更轻松地构建云原生应用。Nacos 支持多种环境下的服务管理和配置管理&#xff0c;包括但不限于 Kubernetes、Docker、虚拟机等。 一、Nacos的主要功能 1. **服务发现与健康…

GS-SLAM论文阅读笔记-CaRtGS

前言 这篇文章看起来有点像Photo-slam的续作&#xff0c;行文格式和图片类型很接近&#xff0c;而且貌似是出自同一所学校的&#xff0c;所以推测可能是Photo-slam的优化与改进方法&#xff0c;接下来具体看看改进了哪些地方。 文章目录 前言1.背景介绍GS-SLAM方法总结 2.关键…

认知杂谈97《兼听则明,偏听则暗》

内容摘要&#xff1a; 在信息爆炸的时代&#xff0c;我们被各种信息包围&#xff0c;这些信息往往经过精心设计以吸引注意力和影响观点。为了避免被操控&#xff0c;我们需要从多个渠道获取信息&#xff0c;并培养批判性思维来分析信息的真实性和偏见。 提高信息素养&#xff0…

读数据湖仓07描述性数据

1. 描述性数据 1.1. 基础数据中包含不同类型的数据&#xff0c;而不同类型数据的描述性数据也存在显著的差异 1.2. 尽管这些描述性数据存在根本性的差异&#xff0c;但通过描述性数据&#xff0c;我们可以全面了解基础数据中的数据 1.3. 通过分析基础设施中提供的描述性数据…