手撕排序算法:归并排序
文章目录
归并是一种常见的算法思想,在许多领域都有广泛的应用。归并排序的主要目的是将两个已排
序的序列合并成一个有序的序列。
1.算法思想
当然,对于一个非有序的序列,可以拆成两个非有序的序列,然后分别调用归并排序,然后对
两个有序的序列再执行合并的过程。所以这里的“归”其实是递归,“并”就是合并。
整个算法的执行过程用mergeSort(a[],l,r)描述,代表当前待排序数组a,左区间下标l,右区
间下标r,分以下几步:
第一步、计算中点mid=(l+r)/2;
第二步、递归调用mergeSort(a[,l,mid)和mergeSort(a[],mid+1,r);
第三步、第二步中两个有序数组进行有序合并,再存储到a[l:];调用时,调用mergeSort(a[],0,n-1)就能得到整个数组的排序结果了。
2.算法分析
2.1时间复杂度
我们假设「比较」和「赋值」的时间复杂度为O(1)。
我们首先讨论「归并排序」算法的最重要的子程序:O()的合并,然后解析这个归并排序算法。
给定两个大小为n1和2的排序数组A和B,我们可以在O(n)时间内将它们有效地归并成一个大
小为n=n1+n2的组合排序数组。可以通过简单地比较两个数组的前面的元素,并始终取两个中较小
的一个来实现的。
问题是这个归并过程被调用了多少次?
由于每次都是对半切,所以整个归并过程类似于一颗二叉树的构建过程,次数就是二叉树的高
度,即log2n,所以归并排序的时间复杂度为O(nlog2n)。
2.2空间复杂度
由于归并排序在归并过程中需要额外的一个「辅助数组」,并且最大长度为原数组长度,所
以「归并排序」的空间复杂度为O(n)。
3.算法优缺点
3.1算法的优点
1.稳定性:归并算法是一种稳定的排序算法,这意味着在排序过程中,相同元素的相对顺序保持不
变。
2.可扩展性:归并算法可以很容易地扩展到并行计算环境中,通过并行归并来提高排序效率。
3.2算法的缺点
1.额外空间:归并算法需要使用额外的辅助空间来存储合并后的结果,这对于内存受限的情况可自
是一个问题。
2.复杂性:归并算法的实现相对复杂,相比于其它一些简单的排序。
4.优化方案
「归并排序」在众多排序算法中效率较高,时间复杂度为O(nlog2n)。
但是,由于归并排序在归并过程中需要额外的一个「辅助数组」,所以申请「辅助数组」内存空间带来的时间消耗会比较大,比较好的做法是,实现用一个和给定元素个数一样大的数组,作为函数传参传进去,所有的「辅助数组」干的事情,都可以在这个传参进去的数组上进行操作,这样就免去了内存的频繁申请和释放。
5.代码演示
void merge(int arr[], int left, int mid, int right) {
int i, j, k;
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组
int L[n1], R[n2];
// 将数据复制到临时数组 L[] 和 R[] for (i = 0; i < n1; i++)
L[i] = arr[left + i];
for (j = 0; j < n2; j++)
R[j] = arr[mid + 1 + j];
// 归并临时数组到 arr[] i = 0; // 初始化第一个子数组的索引
j = 0; // 初始化第二个子数组的索引
k = left; // 初始化合并后子数组的索引
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
}
else {
arr[k] = R[j];
j++;
}
k++;
}
// 将 L[] 的剩余元素复制到 arr[] while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// 将 R[] 的剩余元素复制到 arr[] while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
// 归并排序函数
void merge_sort(int arr[], int left, int right) {
if (left < right) {
// 找到中间点
int mid = left + (right - left) / 2;
// 递归地对左右两半进行排序
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);
// 合并已排序的两个子数组
merge(arr, left, mid, right);
}
}
6.实战
6.1力扣912 排序数组
给你一个整数数组 nums
,请你将该数组升序排列。
void merge(int arr[], int left, int mid, int right) {
int i, j, k;
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组
int L[n1], R[n2];
// 将数据复制到临时数组 L[] 和 R[]
for (i = 0; i < n1; i++)
L[i] = arr[left + i];
for (j = 0; j < n2; j++)
R[j] = arr[mid + 1 + j];
// 归并临时数组到 arr[]
i = 0; // 初始化第一个子数组的索引
j = 0; // 初始化第二个子数组的索引
k = left; // 初始化合并后子数组的索引
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
}
else {
arr[k] = R[j];
j++;
}
k++;
}
// 将 L[] 的剩余元素复制到 arr[]
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// 将 R[] 的剩余元素复制到 arr[]
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
// 归并排序函数
void merge_sort(int arr[], int left, int right) {
if (left < right) {
// 找到中间点
int mid = left + (right - left) / 2;
// 递归地对左右两半进行排序
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);
// 合并已排序的两个子数组
merge(arr, left, mid, right);
}
}
int* sortArray(int* nums, int numsSize, int* returnSize) {
merge_sort(nums,0,numsSize - 1);
*returnSize = numsSize;
return nums;
}
6.2力扣148 排序链表
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表。
struct ListNode* sortList(struct ListNode* head) {
//排序板子
struct ListNode *radix[10], *ptr[10];
for(int i = 0; i < 10; ++i){
radix[i] = (struct ListNode*)malloc(sizeof(struct ListNode));
radix[i]->next = NULL;
ptr[i] = radix[i];
}
//开始基数排序,cur为当前待排序节点
struct ListNode* cur = head, *temp;
for(int bit = 1; bit <= 100000; bit *= 10){
//插到板子上
while(cur){
temp = cur->next;
int px = ((cur->val+100000)/ bit) % 10;
ptr[px]->next = cur;
cur->next = NULL;
ptr[px] = ptr[px]->next;
cur = temp;
}
//收集
struct ListNode *prev = ptr[0];
for(int i = 1; i < 10; ++i){
if(radix[i]->next){
prev->next = radix[i]->next;
prev = ptr[i];
}
}
//新的开始节点
cur = radix[0]->next;
//恢复板子为空
for(int i = 0; i < 10; ++i){
radix[i]->next = NULL;
ptr[i] = radix[i];
}
}
return cur;
}