什么是归并排序
归并排序(Merge Sort)是一种经典的排序算法,它采用分治法(Divide and Conquer)策略,将一个大数组分为两个小数组,分别进行排序,然后将这两个已排序的小数组合并成一个有序的大数组。归并排序在最坏情况下的时间复杂度为 O(nlogn)O(nlogn),并且是一种稳定的排序算法。
核心思想
归并排序的核心思想可以分为以下几个步骤:
- 分解(Divide):将待排序的数组分为两个子数组,直到每个子数组只包含一个元素。因为一个元素的数组是有序的。
- 解决(Conquer):递归地对这两个子数组进行归并排序。
- 合并(Merge):将两个已排序的子数组合并成一个有序的数组。
快速排序与归并排序的比较
快速排序和归并排序都是经典的排序算法,均采用分治法(Divide and Conquer)的思想。尽管它们在基本原理上相似,但在实现细节、性能特征及适用场景上存在显著差异。
1. 基本思路
- 快速排序 :
- 选择一个基准元素(Pivot),通常是数组的第一个元素或随机选择。
- 将数组分为两个部分:左侧部分包含小于基准的元素,右侧部分包含大于基准的元素。
- 递归地对这两个部分进行快速排序,直到整个数组有序。
- 归并排序 :
- 将数组递归分为两个子数组,直到每个子数组只包含一个元素。
- 然后将这两个已排序的子数组合并成一个有序的数组。
- 合并过程是关键,它需要额外的空间来存放临时数组。
2. 稳定性
- 快速排序:不稳定排序。在排序过程中,相等元素的相对顺序可能会被改变。
- 归并排序:稳定排序。相等元素的相对顺序在排序后保持不变,这在某些应用中非常重要。
示例一
题目描述
代码实现
java
import java.util.Scanner;
public class Main {
final static int N = 1000000;
private static int[] temp = new int[N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int arr[] = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
mergeSort(arr, 0, n - 1);
for (int i = 0; i < n; i++) {
System.out.print(arr[i] + " ");
}
}
public static void mergeSort(int[] arr, int l, int r) {
if (l >= r) return;
int mid = (l + r) >> 1;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j])
temp[k++] = arr[i++];
else
temp[k++] = arr[j++];
}
//将剩余元素复制到temp数组中
while (i <= mid) temp[k++] = arr[i++];
while (j <= r) temp[k++] = arr[j++];
//将temp数组中的元素复制回arr数组
for (int t = l; t <= r; t++)
arr[t] = temp[t - l];
}
}
示例二
题目描述
求逆序对的数量
代码实现
java
import java.util.Scanner;
public class Main {
final static int N = 1000000;
private static int[] temp = new int[N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int arr[] = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
scanner.close();
System.out.println(mergeSort(arr, 0, n - 1));
}
public static long mergeSort(int[] arr, int l, int r) {
if (l >= r) return 0;
int mid = (l + r) >> 1;
long res = mergeSort(arr, l, mid)+mergeSort(arr, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j])
temp[k++] = arr[i++];
else{
res += (mid - i + 1);
temp[k++] = arr[j++];
}
}
while (i <= mid)
temp[k++] = arr[i++];
while (j <= r)
temp[k++] = arr[j++];
for(i=l,j=0;i<=r;i++,j++)
arr[i]=temp[j];
return res;
}
}
逆序对数量算法思路与实现
求逆序对的算法是利用了归并排序的思想,在归并排序的过程中会将序列分为两部分,此时逆序对可以分为三种情况:两个数都在左边的(设为s1)。两个数都在右边的(s2),一个数在左边一个数在右边的(s3)。现在假设我们在归并排序的时候写的函数mergeSort(int[] arr, int l, int r)可以返回l到r区间中逆序对数量。那么s1=mergeSort(a, l, mid),s2=mergeSort(a, mid + 1, r);,s3很显然没有直观的答案。
那么核心问题就在于怎么求s3,以及怎么使我们的mergeSort(int[] arr, int l, int r)可以返回l到r区间中逆序对数量。
在归并排序中,左右两边都是排序好的数列。因此,对于右图中的B我们只需要找到左边第一个大于B的数A,那么A后面的数都是大于B的。假设A的下标为i,不难得到我们要统计的数目为:**mid-i+1,**else中的语句即代表左边数列中的数大于右边中的数了,我们可以在此时将mid-i+1加到总答案中去。那么s3的问题就解决了。经过递归之后,res即我们所求。
时间复杂度
- 最佳情况:O(nlogn)O(nlogn)
- 平均情况:O(nlogn)O(nlogn)
- 最坏情况:O(nlogn)O(nlogn)
归并排序在所有情况下的时间复杂度都是 O(nlogn)O(nlogn),这使得它在处理大规模数据时非常高效。
空间复杂度
归并排序的空间复杂度为 O(n)O(n),因为在合并过程中需要额外的存储空间来存放临时数组。
优点:
- 稳定排序,能够保持相等元素的相对顺序。
- 在处理大规模数据时,性能稳定。
缺点:
- 需要额外的存储空间,不适合内存受限的环境。
应用场景
归并排序广泛用于需要稳定排序的场合,如外部排序(处理无法完全放入内存的数据集)和链表排序。由于其稳定性和性能,归并排序在许多编程语言的标准库中被实现为排序算法。
总结
归并排序是一种高效且稳定的排序算法,适用于大规模数据的排序。通过分治法的思想,归并排序能够在最坏情况下保持 O(nlogn)O(nlogn) 的时间复杂度。虽然它需要额外的空间,但在许多实际应用中,归并排序仍然是一种非常有用的算法。