当前位置: 首页 > news >正文

算法思想之分治-归并

欢迎拜访:雾里看山-CSDN博客
本篇主题:算法思想之分治-归并
发布时间:2025.4.17
隶属专栏:算法

在这里插入图片描述

目录

  • 算法介绍
    • 核心思想与步骤
    • 时空复杂度分析
    • C++代码实现
    • 关键特性与优化
  • 例题
    • 排序数组
      • 题目链接
      • 题目描述
      • 算法思路
      • 代码实现
    • 交易逆序对的总数
      • 题目链接
      • 题目描述
      • 算法思路
      • 代码实现
        • 升序的版本
        • 降序的版本
    • 计算右侧小于当前元素的个数
      • 题目链接
      • 题目描述
      • 算法思路
      • 代码实现
    • 翻转对
      • 题目链接
      • 题目描述
      • 算法思路
      • 代码实现

算法介绍

归并排序(Merge Sort)是一种基于分治思想的高效排序算法,其核心思想是通过递归地将数组分解为最小单元,再有序合并子数组完成整体排序。

核心思想与步骤

  1. 分解(Divide)
    将待排序数组递归地分成两个子数组,直到每个子数组仅包含一个元素(天然有序)。
  2. 解决(Conquer)
    递归地对子数组进行排序。
  3. 合并(Merge)
    将两个已排序的子数组合并为一个有序数组。合并时按顺序比较元素,依次放入临时数组,再复制回原数组。

时空复杂度分析

  1. 最优/平均/最坏时间复杂度:均为 O(n log n)
    分解过程产生 log n 层递归,每层合并操作时间复杂度为 O(n)
  2. 空间复杂度:O(n)
    合并时需要与原始数组等长的临时空间。

C++代码实现

#include <vector>
using namespace std;// 合并两个有序子数组
void merge(vector<int>& arr, int left, int mid, int right) {vector<int> temp(right - left + 1);int i = left, j = mid + 1, k = 0;// 按顺序合并左右子数组while (i <= mid && j <= right) {if (arr[i] <= arr[j]) temp[k++] = arr[i++];else temp[k++] = arr[j++];}// 处理剩余元素while (i <= mid) temp[k++] = arr[i++];while (j <= right) temp[k++] = arr[j++];// 将合并结果复制回原数组for (int p = 0; p < k; p++) {arr[left + p] = temp[p];}
}// 归并排序递归函数
void mergeSort(vector<int>& arr, int left, int right) {if (left >= right) return; // 递归终止条件int mid = left + (right - left) / 2;mergeSort(arr, left, mid);     // 递归排序左半部分mergeSort(arr, mid + 1, right);// 递归排序右半部分merge(arr, left, mid, right); // 合并有序子数组
}// 示例调用
int main() {vector<int> arr = {12, 11, 13, 5, 6, 7};mergeSort(arr, 0, arr.size() - 1);// 输出结果:5 6 7 11 12 13return 0;
}

关键特性与优化

  1. 稳定性
    合并时优先保留左侧子数组的相等元素,保证排序稳定。
  2. 适用场景
    外部排序:适合处理磁盘或网络中的大规模数据(需分块加载)。
    链表排序:合并过程无需随机访问,天然适配链表结构。
  3. 优化策略
    小数组切换插入排序:当子数组长度较小时(如 ≤ 15),插入排序更高效。
    避免频繁内存分配:预分配全局临时数组,减少递归中的内存开销。

例题

排序数组

题目链接

912. 排序数组

题目描述

给你一个整数数组 nums,请你将该数组升序排列。

你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

示例 1

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

算法思路

归并排序的流程充分的体现了分而治之的思想,大体过程分为两步:

  • 分:将数组一分为二两部分,一直分解到数组的长度为 1 ,使整个数组的排序过程被分为左半部分排序 + 右半部分排序
  • 治:将两个较短的有序数组合并成一个长的有序数组,一直合并到最初的长度。

代码实现

class Solution {vector<int>  tmp;
public:vector<int> sortArray(vector<int>& nums) {tmp.resize(nums.size());mergeSort(nums, 0, nums.size()-1);return nums;}void mergeSort(vector<int>& nums, int left, int right){if(left >= right)return ;// 1. 选择中间点int mid = left + (right-left)/2;// 2. 把左右区间排序mergeSort(nums, left, mid);mergeSort(nums, mid+1, right);// 3. 把左右区间合并int cur1 = left, cur2 = mid+1, i = left;while(cur1 <= mid && cur2 <= right){tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];}while(cur1 <= mid)tmp[i++] = nums[cur1++];while(cur2 <= right)tmp[i++] = nums[cur2++];// 4. 还原for(int i = left; i <= right; i++)nums[i] = tmp[i];}
};

在这里插入图片描述

交易逆序对的总数

题目链接

LCR 170. 交易逆序对的总数

题目描述

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个交易逆序对。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的交易逆序对总数。

示例 1

输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

提示

  • 0 <= record.length <= 50000

算法思路

用归并排序求逆序数是很经典的方法,主要就是在归并排序的合并过程中统计出逆序对的数量,也就是在合并两个有序序列的过程中,能够快速求出逆序对的数量。

如果我们将数组从中间划分成两个部分,那么我们可以将逆序对产生的方式划分成三组:

  • 逆序对中两个元素:全部从左数组中选择
  • 逆序对中两个元素:全部从右数组中选择
  • 逆序对中两个元素:一个选左数组另一个选右数组

根据排列组合的分类相加原理,三种种情况下产生的逆序对的总和,正好等于总的逆序对数量。

因此,我们可以利用归并排序的过程,先求出左半数组中逆序对的数量,再求出右半数组中逆序对的数量,最后求出一个选择左边,另一个选择右边情况下逆序对的数量,三者相加即可。

代码实现

升序的版本
class Solution {vector<int> tmp;
public:int reversePairs(vector<int>& record) {tmp.resize(record.size());int ret = 0;return mergeSort(record, 0, record.size()-1);}int mergeSort(vector<int> &nums, int left, int right){int ret = 0;if(left >= right)return ret;// 1.选择中间点int mid = left + (right - left)/2;// 2.对左右区间进行排序ret+=mergeSort(nums, left, mid);ret+=mergeSort(nums, mid+1, right);// 3. 统计并合并左右区间int cur1 = left, cur2 = mid+1, i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){tmp[i++] = nums[cur1++];}else{ret+=mid-cur1+1;tmp[i++] = nums[cur2++];}}while(cur1 <= mid)tmp[i++] = nums[cur1++];            while(cur2 <= right)tmp[i++] = nums[cur2++];// 4. 还原for(int i = left; i <= right; i++){nums[i] = tmp[i];   }return ret;}
};
降序的版本
class Solution {vector<int> tmp;
public:int reversePairs(vector<int>& record) {tmp.resize(record.size());int ret = 0;return mergeSort(record, 0, record.size()-1);}int mergeSort(vector<int> &nums, int left, int right){int ret = 0;if(left >= right)return ret;// 1.选择中间点int mid = left + (right - left)/2;// 2.对左右区间进行排序ret+=mergeSort(nums, left, mid);ret+=mergeSort(nums, mid+1, right);// 3. 统计并合并左右区间int cur1 = left, cur2 = mid+1, i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){tmp[i++] = nums[cur2++];}else{ret+=right-cur2+1;tmp[i++] = nums[cur1++];}}while(cur1 <= mid)tmp[i++] = nums[cur1++];            while(cur2 <= right)tmp[i++] = nums[cur2++];// 4. 还原for(int i = left; i <= right; i++){nums[i] = tmp[i];   }return ret;}
};

在这里插入图片描述

计算右侧小于当前元素的个数

题目链接

315. 计算右侧小于当前元素的个数

题目描述

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1

输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

示例 2

输入:nums = [-1]
输出:[0]

示例 3

输入:nums = [-1,-1]
输出:[0,0]

提示

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

算法思路

这一道题的解法与 求数组中的逆序对 的解法是类似的,但是这一道题要求的不是求总的个数,而是要返回一个数组,记录每一个元素的右边有多少个元素比自己小。

但是在我们归并排序的过程中,元素的下标是会跟着变化的,因此我们需要一个辅助数组,来将数组元素和对应的下标绑定在一起归并,也就是再归并元素的时候,顺势将下标也转移到对应的位置上。

由于我们要快速统计出某一个元素后面有多少个比它小的,因此我们可以利用求逆序对的第二种方法。

代码实现

class Solution {vector<int> ret;vector<int> index;vector<int> tmp_num;vector<int> tmp_index;
public:vector<int> countSmaller(vector<int>& nums) {int n = nums.size();ret.resize(n);index.resize(n);tmp_num.resize(n);tmp_index.resize(n);for(int i = 0; i < n; i++)index[i] = i;mergeSort(nums, 0, n-1);return ret;}void mergeSort(vector<int>& nums, int left, int right){if(left >= right)return ;// 1. 选择中间点int mid = left + (right-left)/2;// 2. 把左右区间排序mergeSort(nums, left, mid);mergeSort(nums, mid+1, right);// 3. 把左右区间合并int cur1 = left, cur2 = mid+1, i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){tmp_num[i] = nums[cur2];tmp_index[i++] = index[cur2++];}else{ret[index[cur1]] += right-cur2+1;tmp_num[i] = nums[cur1];tmp_index[i++] = index[cur1++];}}while(cur1 <= mid){tmp_num[i] = nums[cur1];tmp_index[i++] = index[cur1++];}while(cur2 <= right){tmp_num[i] = nums[cur2];tmp_index[i++] = index[cur2++];}// 4. 还原for(int i = left; i <= right; i++){nums[i] = tmp_num[i];index[i] = tmp_index[i];}}
};

在这里插入图片描述

翻转对

题目链接

493. 翻转对

题目描述

给定一个数组 nums ,如果 i < jnums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

示例 1:

输入: [1,3,2,3,1]
输出: 2

示例 2:

输入: [2,4,3,5,1]
输出: 3

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

算法思路

大思路与求逆序对的思路一样,就是利用归并排序的思想,将求整个数组的翻转对的数量,转换成三部分: 左半区间翻转对的数量,右半区间翻转对的数量,一左一右选择时翻转对的数量。 重点就是在合并区间过程中,如何计算出翻转对的数量。

与上个问题不同的是,上一道题我们可以一边合并一遍计算,但是这道题要求的是左边元素大于右边元素的两倍,如果我们直接合并的话,是无法快速计算出翻转对的数量的。

因此我们需要在归并排序之前完成翻转对的统计。

综上所述,我们可以利用归并排序的过程,将求一个数组的翻转对转换成求 左数组的翻转对数量 +右数组中翻转对的数量 + 左右数组合并时翻转对的数量。

代码实现

class Solution {vector<int>  tmp;
public:int reversePairs(vector<int>& nums) {tmp.resize(nums.size());return mergeSort(nums, 0, nums.size()-1);}int mergeSort(vector<int>& nums, int left, int right){if(left >= right)return 0;int ret = 0; // 1. 选择中间点int mid = left + (right-left)/2;// 2. 把左右区间排序ret += mergeSort(nums, left, mid);ret += mergeSort(nums, mid+1, right);// 3. 计算两部分之间的翻转对int cur1 = left, cur2 = mid+1, i = left;while(cur1 <= mid && cur2 <= right){if((long long)nums[cur1] > (long long)nums[cur2]*2){ret+=right-cur2+1;cur1++;}elsecur2++;}// 4. 把左右区间合并cur1 = left, cur2 = mid+1, i = left;while(cur1 <= mid && cur2 <= right)//降序{tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];}while(cur1 <= mid)tmp[i++] = nums[cur1++];while(cur2 <= right)tmp[i++] = nums[cur2++];// 5. 还原for(int i = left; i <= right; i++)nums[i] = tmp[i];return ret;}
};

在这里插入图片描述

⚠️ 写在最后:以上内容是我在学习以后得一些总结和概括,如有错误或者需要补充的地方欢迎各位大佬评论或者私信我交流!!!

相关文章:

  • 【Linux】第八章 监控和管理Linux进程
  • SpringBoot——配置文件
  • 【机器人创新创业应需明确产品定位与方向指南】
  • EMIF详解
  • RPCRT4!OSF_CCONNECTION::OSF_CCONNECTION函数分析之初始化中的u.ConnSendContext----RPC源代码分析
  • 如何简单几步使用 FFmpeg 将任何音频转为 MP3?
  • 插件架构实践
  • 0.深入探秘 Rust Web 框架 Axum
  • 基于 Django 进行 Python 开发
  • Telecom 源码分析计划
  • JUC学习(1) 线程和进程
  • SQL Server 游标介绍
  • 《MySQL:MySQL表结构的基本操作》
  • webgl入门实例-07顶点缓冲区示例
  • 什么是分库分表?
  • 制作Unoconv项目的Docker镜像
  • 部署若依前后端分离
  • 详细讲解一下Java中的Enum
  • vue常见错误
  • 用idea配置springboot+mybatis连接postersql数据库
  • 男子退机票被收90%的手续费,律师:虽然合规,但显失公平
  • 27岁杨阳拟任苏木镇党委副职,系2020年内蒙古自治区选调生
  • 中国目的地·入境游简报006|外国网红游中国启示录
  • 乘联分会:上半年车市价格竞争温和,下半年价格战或再开启
  • 外交部:愿同拉美国家共同维护多边贸易体制
  • 印度外交秘书:印巴军方将于12日再次对话