归并排序及数组中逆序对的数量
归并排序
首先我们来回忆一下归并排序的主要思想:
- 首先考虑如何让将两个有序子序列进行合并?这个方法就是下面代码中的 merge 方法,如果我们能把两个子有序序列合并,那么就可以将短子序列合并为更长的子序列,直到完成源数组的排序。
- 如何获取最开始合并的有序子序列呢?通过不断将数组一分为二,直到每个子序列只剩一个元素的时候,子序列就是有序的了,这样一来,我们就可以实现子有序序列的合并了。
具体的理解推荐学习这个视频讲解,老师讲的很仔细,跟着理解一遍自己就能利用熟悉的语言进行实现。
在这个算法里我们需要注意几点:
- 我们为了将原数组排序,需要借助一个临时数组 tmp ,两个有序子序列,每次都需要将排好序的元素放在 tmp 里,然后再将元素从 tmp 中捣回原数组,我们可以在合并开始就将 tmp 作为参数传递进去,不要在合并方法里面新建 tmp,那样的话整个算法运行期间会不断声明数组,销毁数组。
- 对两个有序子序列进行合并,常规思想都是从头往后比较,谁小就放谁放到 tmp 中,tmp的 下标从头开始;但是对于一些变种,我们就是需要从后往前比较,谁大就把谁放在 tmp 的后面,tmp 下标从尾部开始,比如我们之后介绍的扩展题:求数组中逆序对的数量。就会利用到这个细节。
- 算法的最好和最坏时间复杂度都是 O(nlgn),且是稳定的排序。
Java实现
1 public class MyMergeSort {
2 public static void mergeSort(int[] nums) {
3 mSort(nums, new int[nums.length], 0, nums.length-1);
4 }
5
6 /**
7 * 先分:一直划分划分到只剩一个元素为一个子序列
8 * @param nums 原始数组
9 * @param tmp 临时数组
10 * @param leftStart 左边子序列起始下标
11 * @param rightEnd 右边子序列结束下标
12 */
13 private static void mSort(int[] nums, int[] tmp, int leftStart, int rightEnd) {
14 if (leftStart == rightEnd) {
15 return;
16 }
17 // 一分为二
18 int mid = leftStart + (rightEnd-leftStart)/2;
19 // 分左边子数组
20 mSort(nums, tmp, leftStart, mid);
21 // 分右边子数组
22 mSort(nums, tmp, mid+1, rightEnd);
23 // 合并。
24 merge(leftStart, mid+1, rightEnd, tmp, nums);
25 }
26
27 /**
28 * 再治:对两个有序子序列进行合并。
29 * @param leftStart 左边子序列的开始下标
30 * @param rightStart 右边子序列的开始下标
31 * @param rightEnd 右边子序列的结束下标
32 * @param tmp 临时数组,前后两个子数组的结果排序结果会保存在 tmp 中
33 * @param nums 原始数组
34 */
35 private static void merge(int leftStart, int rightStart, int rightEnd, int[] tmp, int[] nums) {
36 int leftEnd = rightStart-1;
37 // tmpStart 代表此次元素放在 tmp 的哪个下标位置,
38 int tmpStart = leftStart;
39 int length = rightEnd-leftStart+1;
40 while (leftStart <= leftEnd && rightStart <=rightEnd) {
41 // 降序
42 //tmp[tmpStart ++] = nums[leftStart] > nums[rightStart] ? nums[leftStart++] : nums[rightStart++];
43
44 // 升序
45 tmp[tmpStart ++] = nums[leftStart] < nums[rightStart] ? nums[leftStart++] : nums[rightStart++];
46 }
47 while (leftStart <= leftEnd) {
48 tmp[tmpStart ++] = nums[leftStart ++];
49 }
50 while (rightStart <= rightEnd) {
51 tmp[tmpStart ++] = nums[rightStart ++];
52 }
53 // 将 tmp 里有序的元素捣回到原数组。
54 for (int i=0; i<length; i++, rightEnd--) {
55 nums[rightEnd] = tmp[rightEnd];
56 }
57 }
58
59 public static void main(String[] args) {
60 int[] nums = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 10 };
61 mergeSort(nums);
62 for (int x : nums) {
63 System.out.print(x+ " ");
64 }
65 }
66 }
拓展
求数组中的逆序对
什么是逆序对呢,对于数组 nums ,若满足
i < j && nums[i] > nums[j]
则称 nums[i] 和 nums[j] 就构成了一对逆序对。举个例子:对于数组:[ 1,5,3,2,6 ],共存在 3 组逆序对,分别是 [5, 3]、[5,2]、[3,2] 。对于最简单的思路就是用两次 for 循环对每个元素查找其构成的逆序对数量,时间复杂度为O(n^2)。如果借用归并排序的思想,可以达到和归并排序一样的时间复杂度O(nlgn)。
对原数组进行归并排序,在合并操作的时候就可以获得前后两个子序列的逆序对。这里是从子序列的尾部往前移动的。
Java实现
1 public class CountReverse {
2 public static int inversePairs(int[] nums){
3 if( nums == null ||nums.length <= 1) {
4 return 0;
5 }
6 int[] tmp = new int[nums.length];
7
8 return mergeCount(nums, tmp, 0, nums.length-1);
9 }
10
11 public static int mergeCount(int[] nums, int[] tmp, int leftStart, int rightEnd){
12 if(leftStart == rightEnd){
13 tmp[leftStart] = nums[leftStart];
14 return 0;
15 }
16 int mid = leftStart + (rightEnd - leftStart)/2;
17
18 int leftCount = mergeCount(nums, tmp, leftStart, mid);
19 int rightCount = mergeCount(nums, tmp, mid+1, rightEnd);
20
21 int leftEnd = mid;// i 初始化为前半段最后一个数字的下标
22
23 int tmpEnd = rightEnd;//辅助数组复制的数组的最后一个数字的下标
24
25 int count = 0; //计数--逆序对的数目
26
27 while(leftStart <= leftEnd && mid+1 <= rightEnd){
28 if(nums[leftEnd] > nums[rightEnd]){
29 tmp[tmpEnd --] = nums[leftEnd --];
30 // 因为 是两个有序的子序列。
31 count += rightEnd - mid;
32 }else{
33 tmp[tmpEnd--] = nums[rightEnd--];
34 }
35 }
36
37 while (leftEnd >= leftStart) {
38 tmp[tmpEnd --] = nums[leftEnd --];
39 }
40
41 while (rightEnd >= mid + 1) {
42 tmp[tmpEnd --] = nums[rightEnd --];
43 }
44
45 return leftCount + rightCount + count;
46 }
47
48 public static void main(String[] args) {
49 int[] nums = new int[] {1, 5, 3, 2, 6};
50 System.out.println(inversePairs(nums));
51 }
52 }