相关文章推荐:
力扣--分治(快速排序)算法题II:数组中的第K个最大元素(Top K问题),LCR159.库存管理III
目录
[1. 分(Divide):盲目拆分,直到不能再拆](#1. 分(Divide):盲目拆分,直到不能再拆)
[2. 治(Conquer):子问题天然解决](#2. 治(Conquer):子问题天然解决)
[3. 合(Combine/Merge):归并排序的灵魂](#3. 合(Combine/Merge):归并排序的灵魂)
1.排序数组
https://leetcode.cn/problems/sort-an-array/description/


理解题意
本题在相关文章中,按照快速排序做过,现在回顾一下之前的代码:
class Solution
{
public:
vector<int> sortArray(vector<int>& nums)
{
srand(time(NULL));
qsort(nums, 0, nums.size() - 1);
return nums;
}
void qsort(vector<int>& nums, int l, int r)
{
if(l >= r) return;
// 数组分三块
int key = getRandom(nums, l, r);
int i = l, left = l - 1, right = r + 1;
while(i < right)
{
if(nums[i] < key) swap(nums[++left], nums[i++]);
else if(nums[i] == key) i++;
else swap(nums[--right], nums[i]);
}
// [l, left] [left + 1, right - 1], [right, r]
qsort(nums, l, left);
qsort(nums, right, r);
}
int getRandom(vector<int>& nums, int left, int right)
{
int r = rand();
return nums[r % (right - left + 1) + left];
}
};
归并与快排区别
| 比较维度 | 归并排序 (Merge Sort) | 快速排序 (Quick Sort) |
|---|---|---|
| 处理顺序 | 自底向上(后序遍历) :先递归拆分到最小单元(单元素),再在回溯时进行合并(干重活)。 | 自顶向下(前序遍历) :先选定基准值进行划分(干重活),再对左右子区间递归。 |
| 空间复杂度 | O(N)(需要辅助数组暂存数据) | O(log N)(原地排序,仅消耗递归栈空间) |
| 时间复杂度 | 始终为 O(N log N) | 平均 O(N log N),最坏退化为 O(N^2) |
| 稳定性 | 稳定(相对顺序不改变) | 不稳定(交换元素时会破坏相对顺序) |
| 缓存友好性 | 较差(频繁操作非连续的辅助数组空间) | 极好(在原数组上双指针滑动,命中 CPU 缓存率高) |
| 适用场景 | 链表排序、外部排序(数据量大到内存放不下时) | 绝大多数内存中的数组常规排序(工业界首选) |
接下来,我们用归并排序完成
算法原理
这段代码背后的核心算法原理,用四个字概括就是:分而治之(Divide and Conquer)。
归并排序(Merge Sort)是分治法最经典的成功应用之一。它的基本逻辑是:如果要排序一个很大的数组,直接排太难了;那我们就把它拆成小数组,小数组排好序之后,再把它们"缝合"起来。
我们可以将这个原理拆解为三个阶段来理解:
1. 分(Divide):盲目拆分,直到不能再拆
面对一个长度为 N 的无序数组,归并排序的策略是"不看数据,直接从中间一刀切"。
-
一个大数组被劈成两个长度为 N/2 的子数组。
-
这两个子数组再被劈成四个长度为 N/4 的子数组。
-
劈砍一直进行(对应代码中的不断递归
mergeSort),直到每个子数组里只剩下 1 个元素。 -
原理重点 :在算法世界里,一个只有一个元素的数组,天然就是已经排好序的。这就是分治法的"基线条件"(Base Case)。
2. 治(Conquer):子问题天然解决
因为拆分到了极致(单元素),我们不需要做任何特殊的排序操作,单个元素的数组本身就是有序的。这时候,递归开始触底反弹(回溯)。
3. 合(Combine/Merge):归并排序的灵魂
这是归并排序真正"干活"的地方。现在我们手里有了一堆极小的、已经排好序的数组,我们需要把它们两两合并。
-
合并原理 :将两个已经有序的数组合并成一个大的有序数组,是非常高效的。我们只需要两个指针分别指向两个数组的开头,每次比较这两个指针指向的元素,谁小就把谁拿出来放到新数组里,然后对应指针后移。
-
这就像两支已经按身高从矮到高排好队的队伍,要合并成一支大队伍。你只需要每次比较两支队伍站在最前面的人,谁更矮,谁就先出列站到新队伍里。
-
这样一层一层地向上合并,最终把两个长度为 N/2 的有序数组合并成一个长度为 N 的完整有序数组。
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
mergeSort(nums, 0, nums.size() - 1);
return nums;
}void mergeSort(vector<int>& nums, int left, int right) { if(left >= right) return; // 选择中间划分点 int mid = (left + right) >> 1; // 左右区间排序 mergeSort(nums, left, mid); mergeSort(nums, mid + 1, right); // 合并两个有序数组 vector<int> tmp(right - left + 1); int cur1 = left, cur2 = mid + 1, i = 0; 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++]; // 还原 for(int i = left; i <= right; i++) { nums[i] = tmp[i - left]; } }};
优化代码
上述代码中,在合并两个有序数组时开辟空间,实际上是有一定的消耗的,所以这次我们将其放在全局辅助数组,这样对效率会有极大的提升。
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;
// 选择中间划分点
int mid = (left + right) >> 1;
// 左右区间排序
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 合并两个有序数组
int cur1 = left, cur2 = mid + 1, i = 0;
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++];
// 还原
for(int i = left; i <= right; i++)
{
nums[i] = tmp[i - left];
}
}
};
2.交易逆序对的总数
https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/


理解题意
按照所给数组,依次寻找前一数比后一数大的组合并返回。
算法原理
本题最基础的思路是,将一数组拆分为一左一右,再从两边取值进行匹配。
更进一步:在排序时,将一左一右的数组进行排序,之后,再从两边取值进行匹配。
因此,我们使用归并排序,并且对本题排序的要求为 升序排序。
策略:升序排序(找出在此数前有多少个比我大的数)

情况一:nums[cur1] <= nums[cur2] -> cur1++
情况二:nums[cur1] > nums[cur2] -> ret += mid - cur1 + 1; cur2++;
代码:
class Solution
{
public:
vector<int> tmp;
int reversePairs(vector<int>& record)
{
tmp.resize(record.size());
return mergeSort(record, 0, record.size() - 1);
}
int mergeSort(vector<int>& record, int left, int right)
{
if(left >= right) return 0;
int mid = left + (right - left) / 2;
int ret = 0;
ret += mergeSort(record, left, mid);
ret += mergeSort(record, mid + 1, right);
int i = 0, cur1 = left, cur2 = mid + 1;
while(cur1 <= mid && cur2 <= right)
{
if(record[cur1] <= record[cur2])
{
tmp[i++] = record[cur1++];
}
else
{
ret += (mid - cur1 + 1);
tmp[i++] = record[cur2++];
}
}
while(cur1 <= mid) tmp[i++] = record[cur1++];
while(cur2 <= right) tmp[i++] = record[cur2++];
for(int i = left; i <= right; i++)
{
record[i] = tmp[i - left];
}
return ret;
}
};
拓展:策略二
降序排序(找出在此数之后,有多少个比我小的数)
本题降序排列代码:
class Solution { public: vector<int> tmp; int reversePairs(vector<int>& record) { tmp.resize(record.size()); return mergeSort(record, 0, record.size() - 1); } int mergeSort(vector<int>& record, int left, int right) { if(left >= right) return 0; int mid = left + (right - left) / 2; int ret = 0; ret += mergeSort(record, left, mid); ret += mergeSort(record, mid + 1, right); int i = 0, cur1 = left, cur2 = mid + 1; while(cur1 <= mid && cur2 <= right) { if(record[cur1] <= record[cur2]) { tmp[i++] = record[cur2++]; } else { ret += (right - cur2 + 1); tmp[i++] = record[cur1++]; } } while(cur1 <= mid) tmp[i++] = record[cur1++]; while(cur2 <= right) tmp[i++] = record[cur2++]; for(int i = left; i <= right; i++) { record[i] = tmp[i - left]; } return ret; } };
本章完。
