个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创分治算法(5)_归并排序_排序数组
收录于专栏【经典算法练习】
本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
1. 归并排序简介
分治思想:
分(Divide):将待排序数组分成两半,递归地对每一半进行归并排序。
治(Conquer):当子数组的大小为1时,数组自然是有序的。
合(Combine):将两个已排序的子数组合并成一个排序好的数组。
归并排序的步骤:
分割:将数组不断分割,直到每个子数组只包含一个元素。
合并:将相邻的两个子数组合并成一个排序后的数组。合并的过程需要一个辅助数组来存放合并结果。
归并排序的时间与空间复杂度
时间复杂度
最佳情况:O(n log n)
平均情况:O(n log n)
最坏情况:O(n log n)
归并排序的时间复杂度在所有情况下都是 O(n log n),因为它的合并过程总是需要遍历所有元素,而分割的深度是 log n。
空间复杂度
空间复杂度:O(n)
归并排序需要额外的空间来存储合并后的数组,因此其空间复杂度是 O(n)。
归并排序的优缺点:
优点
稳定性:归并排序是一种稳定的排序算法,即相同元素的相对位置不会改变。
适用性广:对于大规模数据集或链表等数据结构,归并排序表现良好。
并行性:归并排序容易实现并行化,适合在多处理器环境中运行。
缺点
额外空间开销:需要 O(n) 的额外空间来存储合并结果,不适合内存有限的环境。
比较复杂:相比其他简单排序算法(如插入排序),实现起来相对复杂。
2. 题目链接
OJ链接 : 排序数组
3. 题目描述
给你一个整数数组 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
4. 解法
算法思路:
归并排序的流程充分体现了[分而治之]的思想,大体过程为两步:
1. 分 : 将数组一分为二,一直分解到数组的长度为1,使整个数组的排序过程被分为[左半部排序] + [右半部排序];
2. 治 : 将两个较短额[有序数组组合成一个长的有序数组], 一直合并到最初的长度.
代码展示:
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;
//[left, mid] [mid + 1, right]
//2. 把左右区间排序
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
//3. 合并两个有序数组
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];
}
};
结果分析:
示例分析:
5. 快速排序与归并排序的比较
算法思想:
快速排序:
采用分治法(Divide and Conquer)策略。
选择一个“基准”元素,将待排序数组分成两部分:左侧为小于基准的元素,右侧为大于基准的元素。然后对左右两部分分别递归地进行快速排序。
归并排序:
同样采用分治法策略。
将待排序数组递归地分成两半,直到每个子数组只包含一个元素,然后再将两个已排序的子数组合并成一个有序数组。
快速排序类似于二叉树的前序遍历(根左右)--确定好根然后递归遍历左右子树
归并排序类似于二叉树的后序遍历(左右根)--确定好左右子树在确定根
时间与空间复杂度:
时间复杂度:
快速排序:
最佳情况:O(n log n)(当基准元素均匀分布时)
平均情况:O(n log n)
最坏情况:O(n²)(当数组已经是有序的,且每次选择的基准都是最大或最小的元素)
归并排序:
最佳情况:O(n log n)
平均情况:O(n log n)
最坏情况:O(n log n)
空间复杂度:
快速排序:
空间复杂度为 O(log n),主要是递归调用栈的空间消耗。实际上是一个原地排序算法,不需要额外的存储空间。
归并排序:
空间复杂度为 O(n),需要额外的数组来存储合并后的结果。
稳定性:
快速排序:
不稳定。在排序过程中,可能会改变相同元素的相对位置。
归并排序:
稳定。在合并的过程中,保持相同元素的相对位置不变。
实际应用:
快速排序:
通常在实际应用中比归并排序更快,特别是对于小规模数据和随机分布的数据。
适合在内存中排序,通常是许多标准库的默认排序算法(如C++的std::sort)。
归并排序:
适合处理大规模数据,尤其是链表等数据结构,因为它可以在合并的过程中避免内存的重新分配。
在外部排序(如排序大文件)中表现良好,因为可以高效地处理数据流。