算法思想之分治-归并
欢迎拜访:雾里看山-CSDN博客
本篇主题:算法思想之分治-归并
发布时间:2025.4.17
隶属专栏:算法
目录
- 算法介绍
- 核心思想与步骤
- 时空复杂度分析
- C++代码实现
- 关键特性与优化
- 例题
- 排序数组
- 题目链接
- 题目描述
- 算法思路
- 代码实现
- 交易逆序对的总数
- 题目链接
- 题目描述
- 算法思路
- 代码实现
- 升序的版本
- 降序的版本
- 计算右侧小于当前元素的个数
- 题目链接
- 题目描述
- 算法思路
- 代码实现
- 翻转对
- 题目链接
- 题目描述
- 算法思路
- 代码实现
算法介绍
归并排序(Merge Sort)是一种基于分治思想的高效排序算法,其核心思想是通过递归地将数组分解为最小单元,再有序合并子数组完成整体排序。
核心思想与步骤
- 分解(Divide)
将待排序数组递归地分成两个子数组,直到每个子数组仅包含一个元素(天然有序)。 - 解决(Conquer)
递归地对子数组进行排序。 - 合并(Merge)
将两个已排序的子数组合并为一个有序数组。合并时按顺序比较元素,依次放入临时数组,再复制回原数组。
时空复杂度分析
- 最优/平均/最坏时间复杂度:均为
O(n log n)
分解过程产生log n
层递归,每层合并操作时间复杂度为O(n)
。 - 空间复杂度:
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;
}
关键特性与优化
- 稳定性
合并时优先保留左侧子数组的相等元素,保证排序稳定。 - 适用场景
外部排序:适合处理磁盘或网络中的大规模数据(需分块加载)。
链表排序:合并过程无需随机访问,天然适配链表结构。 - 优化策略
小数组切换插入排序:当子数组长度较小时(如 ≤ 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 < j
且nums[i] > 2*nums[j]
我们就将(i, j)
称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
示例 1:输入: [1,3,2,3,1]
输出: 2示例 2:
输入: [2,4,3,5,1]
输出: 3注意:
- 给定数组的长度不会超过
50000
。- 输入数组中的所有数字都在
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;}
};
⚠️ 写在最后:以上内容是我在学习以后得一些总结和概括,如有错误或者需要补充的地方欢迎各位大佬评论或者私信我交流!!!