归并排序算法详解(Merge Sort)
一、简介
归并排序(Merge Sort)是一种分治法(Divide and Conquer)思想的经典应用,是一种稳定的排序算法。它通过将数组递归地拆分为两个子数组,对子数组分别进行排序,然后将它们合并成一个有序数组。归并排序的时间复杂度为 O(nlogn)O(n \log n)O(nlogn),其中 nnn 是待排序元素的数量。
二、算法原理
归并排序的基本思想是将数组不断分成两个小部分,直到每个部分只有一个元素,然后再通过合并操作把这些部分重新组合成一个有序的数组。其过程主要分为两个步骤:
- 拆分(分治):将数组从中间位置不断递归拆分,直到拆分到只有一个元素为止。
- 合并:将已经排好序的子数组两两合并,直到所有子数组合并完成,形成最终的有序数组。
整个过程可以通过递归的方式实现。因为归并排序将问题不断地分解成子问题,递归的深度为 logn\log nlogn,每次合并两个子数组的时间为 O(n)O(n)O(n),因此总的时间复杂度为O(nlogn)O(n \log n)O(nlogn)。
三、归并排序的工作流程
- 分割数组:将一个未排序的数组不断地二分,直到无法继续分割(即每个子数组只有一个元素)。
- 合并数组:将两个已经排序的数组合并成一个有序的数组。合并时,依次比较两个数组的当前元素,取较小的元素放入结果数组中,直到其中一个数组元素全部取完,最后再将另一个数组中剩余的元素依次放入结果数组。
工作流程图解
假设我们有一个数组:[8, 3, 5, 4, 7, 1, 9, 2]。归并排序的流程如下:
- 将数组递归拆分:
[8, 3, 5, 4, 7, 1, 9, 2] 拆分成 [8, 3, 5, 4] 和 [7, 1, 9, 2] 继续拆分: [8, 3] [5, 4] 和 [7, 1] [9, 2] 最终拆分为: [8] [3] [5] [4] [7] [1] [9] [2] - 从最小的子数组开始合并:
合并 [8] 和 [3] 得到 [3, 8] 合并 [5] 和 [4] 得到 [4, 5] 合并 [7] 和 [1] 得到 [1, 7] 合并 [9] 和 [2] 得到 [2, 9] - 继续合并:
合并 [3, 8] 和 [4, 5] 得到 [3, 4, 5, 8] 合并 [1, 7] 和 [2, 9] 得到 [1, 2, 7, 9] - 最后合并两个大数组:
合并 [3, 4, 5, 8] 和 [1, 2, 7, 9] 得到 [1, 2, 3, 4, 5, 7, 8, 9]
最终,排序后的数组为 [1, 2, 3, 4, 5, 7, 8, 9]。
四、代码实现
这里的代码实现我们分为了python实现与C++实现
- 实现包括就地排序和非就地排序
| 就地排序 | 非就地排序 |
|---|---|
| 直接对传入数组本身进行排序,且不反回新的数组 | 重新开辟空间保存并返回排序后的数组 |
以下是用 Python 实现归并排序的代码:
就地排序
def merge(be,en,mid,arr):
i = be
j = mid + 1
t = []
while i <= mid and j <= en:
if arr[i] <= arr[j]:
t.append(arr[i])
i += 1
else:
t.append(arr[j])
j += 1
if i <= mid:
t.extend(arr[i:mid+1])
if j <= en:
t.extend(arr[j:en+1])
i = be
j = 0
while j < len(t):
arr[i] = t[j]
i += 1
j += 1
def merge_sort(be, en, arr):
if be < en:
mid = (be + en) // 2
merge_sort(be,mid,arr)
merge_sort(mid+1,en,arr)
merge(be,en,mid,arr)
arr = [4,5,6,1,2,3,9,6,3,65,12,4,54,3]
merge_sort(0, len(arr)-1, arr)
print(arr)
非就地排序
import os
import sys
# 请在此输入您的代码
def merge(arr1,arr2):
len1 = len(arr1)
len2 = len(arr2)
res = []
i = j = 0
while i < len1 and j < len2:
if arr1[i] < arr2[j]:
res.append(arr1[i])
i += 1
else:
res.append(arr2[j])
j += 1
if i < len1:
res.extend(arr1[i:])
elif j < len2:
res.extend(arr2[j:])
return res
def m_sort(l,r,arr):
if l < r:
# 取中点并切出新的数组
mid = int((l+r)/2)
arr1 = arr[l:mid+1]
arr2 = arr[mid+1:r+1]
# print(l,r,mid)
# print(arr1,arr2)
# 新分配的数组重新分割,这里要注意下标是从0-len(new_arr)
left_arr = m_sort(0,len(arr1)-1,arr1)
right_arr = m_sort(0,len(arr2)-1,arr2)
return merge(left_arr,right_arr)
else:
# print(f"now is l >=r and arr is:{arr}")
return arr
a = [4,5,6,1,2,3,9,6,3,65,12,4,54,3]
print(m_sort(0,len(a)-1,a))
以下是用 C++ 实现归并排序的代码:
#include<bits/stdc++.h>
using namespace std;
int n,Sort_begin,Sort_end,cnt;
void merge(int *a,int s,int m,int t);
//void merge_pass(int *a,int len);
void merge_sort(int *a,int be,int en);
int main(){
cin>>n;
int a[n];
for(int i=0;i<n;i++) cin>>a[i];
merge_sort(a,0,n-1);
for(int i=0;i<n;i++)
cout<<a[i]<<" ";
cout<<endl;
cout<<cnt<<endl;
return 0;
}
void merge(int *a,int s,int m,int t){
int p[t-s+1];
int i=s,j=m+1,index=0;
while(i<=m&&j<=t){
cout<<"mid is:"<<m<<endl;
if(a[i]<=a[j]){p[index++]=a[i]; i++;}
else{p[index++]=a[j]; j++; cout<<"mid is:"<<m<<" i is:"<<i<<endl; cnt+=m-i+1;}
}
//这里为什么是mid-i+1呢,因为前后的序列已经是有序的了,如果前面的序列出现某一位大于后面的序列,那么从这一位开始
//后面的都将大于后面的序列,所以个数自然而然的就出来了,mid-i+1即个数
while(i<=m){p[index++]=a[i]; i++;}
while(j<=t){p[index++]=a[j]; j++;}
for(int i=s,j=0;i<=t;i++,j++) a[i]=p[j];
}
//void merge_pass(int *a,int len){
// int i;
// for(i=0;i+2*len-1<n;i=i+2*len){
// merge(a,i,i+len-1,i+2*len-1);
// }
// if(i+len<n) merge(a,i,i+len-1,n-1);
//}
void merge_sort(int *a,int be,int en){
if(be<en){
int m=(be+en)/2;
merge_sort(a,be,m);
merge_sort(a,m+1,en);
merge(a,be,m,en);
}
}
/*
10
123 345 56765 2342 1231 4564 2342 34564 23432 456
*/
解释:
xxx_sort函数首先通过递归将数组分割为更小的部分,然后调用merge函数来合并两个有序的数组。merge函数中使用了双指针来逐一比较两个子数组中的元素,将较小的元素放入结果数组中。
五、复杂度分析
-
时间复杂度:归并排序的时间复杂度是 O(nlogn)O(n \log n)O(nlogn)。在每次递归中,数组被分成两半,递归的深度是 logn\log nlogn,而在每一层递归中,合并两个子数组的时间是线性的 O(n)O(n)O(n),因此总的时间复杂度是 O(nlogn)O(n \log n)O(nlogn)。
-
空间复杂度:归并排序需要额外的空间来存储中间结果,空间复杂度为 O(n)O(n)O(n),其中 nnn 是数组的长度,因为需要开辟额外的数组来存放合并后的结果。
六、归并排序的优缺点
优点:
- 稳定性:归并排序是一种稳定的排序算法,能够保持相同元素之间的相对位置不变。
- 时间复杂度优良:在所有情况下(无论是最优、最坏还是平均情况),归并排序的时间复杂度始终是 O(nlogn)O(n \log n)O(nlogn)。
- 适用于大数据排序:由于时间复杂度较低且具有稳定性,归并排序在处理大规模数据集时表现优越。
缺点:
- 空间复杂度较高:归并排序需要额外的 O(n)O(n)O(n) 空间来存储合并后的结果,这在一些内存敏感的环境下可能是一个问题。
- 递归实现可能导致栈溢出:在某些编程语言中,如果递归深度过大,可能会导致栈溢出。
七、案例分析
案例 1:已排序数组
假设输入数组是已经排序的:[1, 2, 3, 4, 5, 6, 7, 8]。归并排序会依然进行分割和合并,虽然每次合并时数组已经是有序的,但算法的整体复杂度仍然是 (O(n \log n))。
案例 2:逆序数组
如果输入是一个逆序的数组:[8, 7, 6, 5, 4, 3, 2, 1],归并排序也会首先将数组分成小的子数组,然后逐步进行合并。由于归并过程在合并时依然可以逐个比较元素,算法的时间复杂度依然是 O(nlogn)O(n \log n)O(nlogn)。
案例 3:随机数组
如果输入的是随机的数组:[4, 1, 6, 7, 3, 2, 8, 5],归并排序会先将数组拆分为 [4, 1, 6, 7] 和 [3, 2, 8, 5],再分别进行合并排序,最终得到有序的数组 [1, 2, 3, 4, 5, 6, 7, 8]。整个过程也是按照分治法的思想进行。
八、总结
归并排序是一种基于分治思想的稳定排序算法,它具有时间复杂度 O(nlogn)O(n \log n)O(nlogn),在所有情况下表现一致,适合处理大规模数据集。尽管它的空间复杂度较高,但在某些大数据处理任务中,它依然是非常有效的选择。
1889

被折叠的 条评论
为什么被折叠?



