常见排序算法总结 (三) - 归并排序与归并分治
归并排序
算法思想
将数组元素不断地拆分,直到每一组中只包含一个元素,单个元素天然有序。之后用归并的方式收集跨组的元素,最终形成整个区间上有序的序列。
稳定性分析
归并排序是稳定的,拆分数组时会自然地将元素分成有先后顺序的子数组,在归并的过程中如果遇到相等的值,优先收集早出现的子数组中的那个即可。
具体实现
递归
// 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组
public static int MAX_N = 50010;
public static int[] temp = new int[MAX_N];// 归并,用于将已经有序的两个数组合并成更大的有序数组
private void merge(int[] arr, int left, int mid, int right) {int index1 = left, index2 = mid + 1, index = left;// 两个数组都有剩余元素,每次收集值较小的元素while(index1 <= mid && index2 <= right) {temp[index++] = arr[index1] <= arr[index2] ? arr[index1++] : arr[index2++];}// 其中一个数组为空,将另一个数组剩余的所有元素都添加到结果数组的末尾while(index1 <= mid) {temp[index++] = arr[index1++];}while(index2 <= right) {temp[index++] = arr[index2++];}// 结果回写到原数组System.arraycopy(temp, left, arr, left, right - left + 1);
}// 归并排序
private void mergeSort(int[] arr, int left, int right) {// 左右端点相同,数组中只有单个元素认为天然有序if (left == right) {return;}int mid = left + ((right - left) >>> 1); // 计算区间中点作为划分数组的依据// 递归地对左半边数组进行排序mergeSort(arr, left, mid);// 递归地对右半边数组进行排序mergeSort(arr, mid + 1, right);// 归并收集结果merge(arr, left, mid, right);
}
非递归
// 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组
public static int MAX_N = 50010;
public static int[] temp = new int[MAX_N];// 归并方法,用于将已经有序的两个数组合并成更大的有序数组
private void merge(int[] arr, int left, int mid, int right) {int index1 = left, index2 = mid + 1, index = left;// 两个数组都有剩余元素,每次收集值较小的元素while(index1 <= mid && index2 <= right) {temp[index++] = arr[index1] <= arr[index2] ? arr[index1++] : arr[index2++];}// 其中一个数组为空,将另一个数组剩余的所有元素都添加到结果数组的末尾while(index1 <= mid) {temp[index++] = arr[index1++];}while(index2 <= right) {temp[index++] = arr[index2++];}// 结果回写到原数组System.arraycopy(temp, left, arr, left, right - left + 1);
}// 归并排序
private void mergeSort(int[] arr) {int n = arr.length;// 根据步长来迭代,每一轮扩大一倍for (int left, mid, right, step = 1; step < n; step <<= 1) {left = 0; // 左端点从 0 开始while (left < n) {mid = left + step - 1; // 计算区间中点的位置// 另一半区间无法构成,直接进行下一轮if (mid >= n - 1) {break;}// 数组内剩余元素不一定够填满右半边数组,右端点要根据情况来取值right = Math.min(left + (step << 1) - 1, n - 1);merge(arr, left, mid, right);// 移动指针,准备归并右半边数组left = right + 1;}}
}
归并分治
分治是一种算法思想,归并分治顾名思义就是涉及到了归并的分治策略。这部分内容其实和排序关系不大,但是作为归并应用的扩展放在一起整理比较合适的。
算法思想
如果一个问题满足两个条件,那么大概率可以使用归并分治解决:
- 全局的结果相当于划分成两部分之后,左半边、右半边与跨左右三部分的结果的并集,也就是这个问题可以总中间拆分成子问题。
- 如果拆分之后小范围上有序,能够使得计算跨左右的答案时更方便。
实践案例:Leetcode 493. 翻转对
递归
class Solution {// 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组public static int MAX_N = 50010;public static int[] temp = new int[MAX_N];public int reversePairs(int[] nums) {return reversePairs(nums, 0, nums.length - 1);}// 重载一个同名方法,将它改造成方便递归的形式private int reversePairs(int[] nums, int left, int right) {// 只有一个元素的情况下没有答案if(left == right) {return 0;}int mid = left + ((right - left) >>> 1);// 结果等于左半边、右半边与跨左右的结果的并集return reversePairs(nums, left, mid) + reversePairs(nums, mid + 1, right) + merge(nums, left, mid, right);}private int merge(int[] nums, int left, int mid, int right) {int res = 0;// 当前跨小范围的情况下,更小范围内已经有序for(int i = left, j = mid + 1; i <= mid; i++) {// 确定了当前轮 j 的位置之后,这个指针不会回退,这也是提升性能的根本原因while(j <= right && (long) nums[i] > (long) 2 * nums[j]) {j++;}res += j - mid - 1;}// 常规归并流程int index1 = left, index2 = mid + 1, index = left;while(index1 <= mid && index2 <= right) {temp[index++] = nums[index1] <= nums[index2] ? nums[index1++] : nums[index2++];}while(index1 <= mid) {temp[index++] = nums[index1++];}while(index2 <= right) {temp[index++] = nums[index2++];}System.arraycopy(temp, left, nums, left, right - left + 1);return res;}
}
非递归
class Solution {// 注意预先定义辅助数组,防止递归层数深的情况下花大量时间空间去开数组public static int MAX_N = 50010;public static int[] temp = new int[MAX_N];public int reversePairs(int[] nums) {int res = 0;int n = nums.length;for (int left, mid, right, step = 1; step < n; step <<= 1) {left = 0;while (left < n) {mid = left + step - 1;if (mid >= n - 1) {break;}right = Math.min(left + (step << 1) - 1, n - 1);res += merge(nums, left, mid, right); // 累计每次归并的结果left = right + 1;}}return res;}private int merge(int[] nums, int left, int mid, int right) {int res = 0;// 当前跨小范围的情况下,更小范围内已经有序for(int i = left, j = mid + 1; i <= mid; i++) {// 确定了当前轮 j 的位置之后,这个指针不会回退,这也是提升性能的根本原因while(j <= right && (long) nums[i] > (long) 2 * nums[j]) {j++;}res += j - mid - 1;}// 常规归并流程int index1 = left, index2 = mid + 1, index = left;while(index1 <= mid && index2 <= right) {temp[index++] = nums[index1] <= nums[index2] ? nums[index1++] : nums[index2++];}while(index1 <= mid) {temp[index++] = nums[index1++];}while(index2 <= right) {temp[index++] = nums[index2++];}System.arraycopy(temp, left, nums, left, right - left + 1);return res;}
}
梳理总结
分治是一种通过不断划分来减小问题规模,而归并是用来收集得到全局结果的方法。上面的例子所使用的都是一分为二的做法,其实每一轮可以划分成更多子问题,演变成多路归并。
正是上面这种不停划分的过程,使得无论是归并排序还是归并分治,都能有效地将暴力搜索需要 O ( N 2 ) O(N ^ 2) O(N2) 量级的方法,优化到 O ( N l o g N ) O(NlogN) O(NlogN) 这个级别。
当然,本质上来说还是空间换时间,这样的操作很明显需要 O ( N ) O(N) O(N) 量级的额外空间。
从使用上来说,归并排序一般不会成为手写排序的选择。但是归并分治则是很多问题的优秀解决方案,需要注意它的使用前提和具体实现。
后记
使用 Leetcode 912. 排序数组 进行测试,归并排序能够比较高高效地完成任务,略逊于计数排序和基数排序(不过其实题目要求使用尽可能少的额外空间,归并排序肯定不属于首选的方案)。
相关文章:
常见排序算法总结 (三) - 归并排序与归并分治
归并排序 算法思想 将数组元素不断地拆分,直到每一组中只包含一个元素,单个元素天然有序。之后用归并的方式收集跨组的元素,最终形成整个区间上有序的序列。 稳定性分析 归并排序是稳定的,拆分数组时会自然地将元素分成有先后…...
【后端开发】Go语言编程实践,Goroutines和Channels,基于共享变量的并发,反射与底层编程
【后端开发】Go语言编程实践,Goroutines和Channels,基于共享变量的并发,反射与底层编程 【后端开发】Go语言高级编程,CGO、Go汇编语言、RPC实现、Web框架实现、分布式系统 文章目录 1、并发基础, Goroutines和Channels2、基于共享…...

PyTorch 2.5.1: Bugs修复版发布
一,前言 在深度学习框架的不断迭代中,PyTorch 社区始终致力于提供更稳定、更高效的工具。最近,PyTorch 2.5.1 版本正式发布,这个版本主要针对 2.5.0 中发现的问题进行了修复,以提升用户体验。 二,PyTorch 2…...

【Android】组件化嘻嘻嘻gradle耶耶耶
文章目录 Gradle基础总结:gradle-wrapper项目根目录下的 build.gradlesetting.gradle模块中的 build.gradlelocal.properties 和 gradle.properties 组件化:项目下新建一个Gradle文件定义一个ext扩展区域config.gradle全局基础配置(使用在项目…...

vulnhub靶场【哈利波特】三部曲之Aragog
前言 使用virtual box虚拟机 靶机:Aragog : 192.168.1.101 攻击:kali : 192.168.1.16 主机发现 使用arp-scan -l扫描,在同一虚拟网卡下 信息收集 使用nmap扫描 发现22端口SSH服务,openssh 80端口HTTP服务,Apach…...

HarmonyOS开发中,如何高效定位并分析内存泄露相关问题
HarmonyOS开发中,如何高效定位并分析内存泄露相关问题 (1)Allocation的应用调试方式Memory泳道Native Allocation泳道 (2)Snapshot(3)ASan的应用使用约束配置参数使能ASan方式一方式二 启用ASanASan检测异常码 (4)HWASan的应用功能介绍约束条件使能HWASan方式一方式…...

java调用ai模型:使用国产通义千问完成基于知识库的问答
整体介绍: 基于RAG(Retrieval-Augmented Generation)技术,可以实现一个高效的Java智能问答客服机器人。核心思路是将预先准备的问答QA文档(例如Word格式文件)导入系统,通过数据清洗、向量化处理…...

2023年第十四届蓝桥杯Scratch国赛真题—推箱子
推箱子 程序演示及其源码解析,可前往: https://www.hixinao.com/scratch/creation/show-188.html 若需在线编程,在线测评模考,助力赛事可自行前往题库中心,按需查找: https://www.hixinao.com/ 题库涵盖…...
银河麒麟V10-SP1设置redis开机自启
前言: redis安装请看:银河麒麟V10-SP1离线安装redis5.0.1_银河麒麟v10 redis5.0-CSDN博客 一、编辑自启文件 vim /etc/systemd/system/redis.service [Unit] DescriptionRedis In-Memory Data Store Afternetwork.target [Service] Typeforking ExecS…...

释放超凡性能,打造鸿蒙原生游戏卓越体验
11月26日在华为Mate品牌盛典上,全新Mate70系列及多款全场景新品正式亮相。在游戏领域,HarmonyOS NEXT加持下游戏的性能得到充分释放。HarmonyOS SDK为开发者提供了软硬协同的系统级图形加速解决方案——Graphics Accelerate Kit(图形加速服务…...

Node.js 实战: 爬取百度新闻并序列化 - 完整教程
很多时候我们需要爬取一些公开的网页内容来做一些数据分析和统计。而多数时候,大家会用到python ,因为实现起来很方便。但是其实Node.js 用来爬取网络内容,也是非常强大的。 今天我向大家介绍一下我自己写的一个百度新闻的爬虫,可…...

106.【C语言】数据结构之二叉树的三种递归遍历方式
目录 1.知识回顾 2.分析二叉树的三种遍历方式 1.总览 2.前序遍历 3.中序遍历 4.后序遍历 5.层序遍历 3.代码实现 1.准备工作 2.前序遍历函数PreOrder 测试结果 3.中序遍历函数InOrder 测试结果 4.后序遍历函数PostOrder 测试结果 4.底层分析 1.知识回顾 在99.…...

qt QToolButton详解
1、概述 QToolButton是Qt框架中的一个控件,它继承自QAbstractButton。QToolButton通常用于工具栏(QToolBar)中,提供了一种快速访问命令或选项的方式。与普通的QPushButton按钮相比,QToolButton通常只显示一个图标而不…...

2024年大热,Access平替升级方案,也适合Excel用户
欢迎各位看官,您来了,就对了! 您多半是Access忠实粉丝,至少是excel用户,亦或是WPS用户吧。那就对了,今天的分享肯定对您有用。 本文1100字,阅读时长2分50秒! 现实总是不尽人意&am…...
探索Scala的模式匹配:身份证识别与等级判定!!! #Scala # scala #匹配模式
在Scala编程语言中,模式匹配是一个强大且表达力丰富的特性,它允许我们以声明式的方式处理多种情况。今天,我们将通过两个有趣的例子来展示Scala模式匹配的魅力:身份证号识别和等级判定。 1. 身份证号识别:定位你的家乡…...
python数据分析之爬虫基础:爬虫介绍以及urllib详解
前言 在数据分析中,爬虫有着很大作用,可以自动爬取网页中提取的大量的数据,比如从电商网站手机商品信息,为市场分析提供数据基础。也可以补充数据集、检测动态变化等一系列作用。可以说在数据分析中有着相当大的作用!…...
【星海随笔】syslinux
Ubuntu相关资料 https://www.pugetsystems.com/labs/hpc/ubuntu-22-04-server-autoinstall-iso/#Step_2_Unpack_files_and_partition_images_from_the_Ubuntu_2204_live_server_ISO https://launchpad.net/ubuntu/source/squashfs-tools/1:4.6.1-1build1 sudo tar -xf my_compu…...
力扣C语言刷题记录 (二)移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。 假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作: 更改…...
【Vue3】【Naive UI】<NAutoComplete>标签
【Vue3】【Naive UI】标签 <NAutoComplete> 是 Naive UI 库中的一个组件,用于实现自动完成或联想输入功能。 它允许用户在输入时看到与当前输入匹配的建议列表,从而帮助用户更快地填写表单字段。 这个组件通常用于搜索框、地址输入等场景ÿ…...
【Halcon】使用均值滤波出现假边怎么办?
在图像处理过程中,均值滤波是一种常见的平滑技术,用于减少图像中的噪声。然而,当应用于具有显著边缘或对比度变化的图像时,均值滤波可能会导致“假边”现象,即原本不存在的边缘在滤波后变得明显。以下是如何在Halcon中处理这一问题,并提供一个完整的示例代码。 示例背景…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...

【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...