📢博客主页:https://blog.csdn.net/2301_779549673
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
- 🏳️🌈归并排序概念
- 🏳️🌈原理与实现
- 🏳️🌈性能分析
- ❤️(一)时间复杂度
- 🧡(二)空间复杂度
- 💛(三)稳定性
- 🏳️🌈例题分析(4题)
- ❤️链接: [912. 排序数组](https://leetcode.cn/problems/sort-an-array/description/)
- 🧡链接: [LCR 170. 交易逆序对的总数](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/)
- 💛链接: [315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/description/)
- 💚链接: [493. 翻转对](https://leetcode.cn/problems/reverse-pairs/description/)
- 👥总结
🏳️🌈归并排序概念
归并排序是一种高效稳定的基于分治思想的排序算法,适用于多种场景。
归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。它将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
当有 n 个记录时,需进行 logn 轮归并排序,每一轮归并,其比较次数不超过 n,元素移动次数都是 n。因此,归并排序的时间复杂度为 O (nlogn)。归并排序时需要和待排序记录个数相等的存储空间,所以空间复杂度为 O (n)。
归并排序适用于数据量大,并且对稳定性有要求的场景。例如,在处理大量数据的数据库系统中,归并排序可以保证数据的稳定性,确保相同值的数据在排序前后的相对位置不变。同时,在一些需要对数据进行多次排序的应用中,归并排序的稳定性也能保证结果的准确性。
在归并排序的过程中,基本的操作是合并两个已排序的数组。取两个输入数组 A 和 B,一个输出数组 C,以及三个计数器 i、j、k,它们初始位置置于对应数组的开始端。A [i] 和 B [j] 中较小者拷贝到 C 中的下一个位置,相关计数器向前推进一步。当两个输入数组有一个用完时候,则将另外一个数组中剩余部分拷贝到 C 中。
总的来说,归并排序以其高效性和稳定性,在各种数据处理场景中都有着广泛的应用。
🏳️🌈原理与实现
归并排序的原理基于分治思想,其主要步骤包括分割和合并。
- 首先,将待排序的数组不断地分割成较小的子数组,直到每个子数组只包含一个元素。此时,每个子数组都是有序的。
- 然后,开始进行合并操作。在合并过程中,将两个已排序的子数组合并成一个更大的有序数组。具体来说,比较两个子数组的首元素,将较小的元素放入新的数组中,然后移动相应子数组的指针。
- 重复这个过程,直到其中一个子数组被遍历完。
- 最后,将另一个子数组中剩余的元素直接复制到新数组中。通过不断地进行分割和合并操作,最终可以得到一个完全有序的数组。
此外,由于归并排序是先排中间,然后再排两边的,所以可以近似看成二叉树的后序遍历,也就是先遍历左子树和右子树,再是根节点。
🏳️🌈性能分析
❤️(一)时间复杂度
归并排序的时间复杂度始终为 O(nlogn)
,这意味着无论数据的初始状态如何,归并排序的运行时间都与数据规模 n
和对数函数 logn
的乘积成正比。
归并排序采用分治法,将问题不断分解为规模更小的子问题进行求解。假设我们需要对一个包含 n个数的序列使用归并排序,并且使用的是递归的实现方式,那么过程如下:
1.递归的第一层,将 n 个数划分为 2 个子区间,每个子区间的数字个数为 n/2
2.递归的第二层,将 n 个数划分为 4个子区间,每个子区间的数字个数为 n/4.
3.递归的第三层,将 n 个数划分为8个子区间,每个子区间的数字个数为 n/8
4.递归的第 logn 层,将 n 个数划分为 n 个子区间,每个子区间的数字个数为 1。
归并排序的总时间 T(n)= 2T(n/2)+o(n)
。假设解决最后的子问题用时为常数 c,则对于 n个待排序记录来说整个问题的规模为 cn 。从递归树可以看出,第一层时间代价为 cn
,第二层时间代价为 cn/2 + cn/2= cn
…每一层代价都是 cn
,总共有 logn+1
层。所以总的时间代价为 cn*(logn+1)
,时间复杂度是 O(nlogn)
在不同规模数据下,归并排序的表现相对稳定。无论是小规模数据还是大规模数据,其时间复杂度都保持在 O(nlogn)。对于小规模数据,虽然归并排序可能会显得有些“大材小用”,但其时间复杂度不会急剧上升。而对于大规模数据,归并排序的优势更加明显,相比一些时间复杂度较高的排序算法,如冒泡排序、选择排序等,归并排序能够在更短的时间内完成排序任务。
🧡(二)空间复杂度
归并排序需要额外的 O(n)空间来保存临时数组。在归并过程中,需要创建临时数组来存储合并后的结果。这意味着归并排序的空间开销与数据规模 成正比。
这种空间开销对算法性能有一定的影响。一方面,额外的空间需求可能会在处理大规模数据时占用较多的内存资源。特别是在内存有限的环境中,可能会导致内存不足的问题。另一方面,创建临时数组和进行数据复制也会带来一定的时间开销。然而,归并排序的稳定性和高效的时间复杂度在很多情况下可以弥补其空间复杂度较高的不足。在一些对稳定性要求较高的场景中,归并排序仍然是一个不错的选择。
💛(三)稳定性
归并排序是稳定排序算法,这意味着在排序过程中,相等元素的相对位置不会改变。
归并排序之所以是稳定的,原因在于其合并过程。在合并两个已排序的子数组时,如果两个元素相等,归并排序会先将位于前面子数组中的元素放入新的数组中,从而保证了相等元素的相对位置不变。
在实际应用中,稳定性具有重要意义。例如,在对学生成绩进行排序时,如果有多个学生得分相同,稳定排序可以保持他们原来的顺序,这对于后续的数据分析和处理非常重要。另外,在一些需要保持数据原有顺序关系的场景中,归并排序的稳定性可以确保排序结果的准确性和可靠性。
🏳️🌈例题分析(4题)
❤️链接: 912. 排序数组
- 明确我们的目的,是通过使用归并排序算法对给定的整数向量进行排序
- 对左右两边的数组进行排序
- 创建临时变量数组,通过双指针,使左右数组合并有序
class Solution {
int tmp[50010];
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
mergesort(nums, 0, n-1);
return nums;
}
void mergesort(vector<int>& nums, int l, int r){
// 1.获取中间点坐标
if(l >= r) return;
int mid = l + ((r - l) >> 1);
// 2. 利用归并排序自身对左右范围的数组进行,是左右两边都有序
mergesort(nums, l, mid);
mergesort(nums, mid + 1, r);
// 3.创建临时变量数组,同时利用双指针,将左右有序数组合并
int index = l;
int cur1 = l, cur2 = mid + 1;
while(cur1 <= mid && cur2 <= r){
if(nums[cur1] < nums[cur2]) tmp[index++] = nums[cur1++];
else tmp[index++] = nums[cur2++];
}
// 4.使左右两边的数组充分排序进入临时数组
while(cur1 <= mid) tmp[index++] = nums[cur1++];
while(cur2 <= r) tmp[index++] = nums[cur2++];
// 5.将临时数组中的元素返回,使原数组有序
for(int i = l; i <= r; ++i) nums[i] = tmp[i];
}
};
🧡链接: LCR 170. 交易逆序对的总数
因为归并排序具有较好的稳定性,所以我们利用归并排序排升序的同时,计算每次右边数组大于左边数组元素的个数,并返回
class Solution {
int tmp[50010];
public:
int reversePairs(vector<int>& record) {
return mergesort(record, 0, record.size() - 1);
}
int mergesort(vector<int>& record, int l, int r){
// 1.获取中间点坐标
if (l >= r) return 0;
int mid = l + ((r - l) >> 1);
int cur1 = l, cur2 = mid + 1, ret = 0;
// 2. 利用归并排序自身对左右范围的数组进行,是左右两边都有序
ret += mergesort(record, l, mid);
ret += mergesort(record, mid+1, r);
// 3.利用双指针合并数组
// 4.利用升序数组的特性,计算当前两组元素中符合前一天的股价高于后一天的股价的组合个数
int index = 0;
while(cur1 <= mid && cur2 <= r){
if(record[cur1] > record[cur2]) ret += mid - cur1 + 1, tmp[index++] = record[cur2++];
else tmp[index++] = record[cur1++];
}
while(cur1 <= mid) tmp[index++] = record[cur1++];
while(cur2 <= r) tmp[index++] = record[cur2++];
// 5.将临时数组中的元素返回,使原数组有序
for(int i=0; i<index; ++i) record[l+i] = tmp[i];
return ret;
}
};
💛链接: 315. 计算右侧小于当前元素的个数
整体做法和上题如出一辙,不过这里需要返回对应的元素个数组成的vector,所以我们需要提前建立可以用来标记元素下标和记录满足元素个数数量的返回数组
class Solution {
// 返回值
vector<int> ret;
// 记录原始下标
vector<int> index;
// 临时变量数组
int tmpnums[100010];
// 临时下标数组
int tmpindex[100010];
public:
vector<int> countSmaller(vector<int>& nums) {
int n = nums.size();
ret.resize(n);
index.resize(n);
// 获取原始下标
for (int i = 0; i < n; ++i)
index[i] = i;
mergesort(ret, index, nums, 0, n - 1);
return ret;
}
void mergesort(vector<int>& ret, vector<int>& index, vector<int>& nums, int left, int right) {
if (left >= right)
return;
int mid = left + ((right - left) >> 1);
mergesort(ret, index, nums, left, mid);
mergesort(ret, index, nums, mid + 1, right);
int cur1 = left, cur2 = mid + 1, i = 0;
while (cur1 <= mid && cur2 <= right) {
if (nums[cur2] >= nums[cur1]) {
tmpindex[i] = index[cur2];
tmpnums[i++] = nums[cur2++];
} else {
ret[index[cur1]] += right - cur2 + 1;
tmpindex[i] = index[cur1];
tmpnums[i++] = nums[cur1++];
}
}
while (cur1 <= mid)
tmpindex[i] = index[cur1], tmpnums[i++] = nums[cur1++];
while (cur2 <= right)
tmpindex[i] = index[cur2], tmpnums[i++] = nums[cur2++];
for (int j = left; j <= right; ++j)
index[j] = tmpindex[j - left], nums[j] = tmpnums[j - left];
}
};
💚链接: 493. 翻转对
大体规则相同,当这里用降序更容易实现
class Solution {
int tmp[50010];
public:
int reversePairs(vector<int>& nums) {
int n = nums.size();
return mergesort(nums, 0, n - 1);
}
int mergesort(vector<int>& nums, int left, int right) {
if (left >= right)
return 0;
int ret = 0;
int mid = (left + right) >> 1;
ret += mergesort(nums, left, mid);
ret += mergesort(nums, mid + 1, right);
int cur1 = left, cur2 = mid + 1, i = left;
while (cur1 <= mid) {
while (cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
cur2++;
if (cur2 > right)
break;
ret += right - cur2 + 1;
cur1++;
}
cur1 = left, cur2 = mid + 1;
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++];
for (int j = left; j <= right; ++j)
nums[j] = tmp[j];
return ret;
}
};
👥总结
本篇博文对 归并排序概念及例题运用 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~