归并排序
归并排序
1. 基本思想
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
在归并排序中主要注重治(conquer)的阶段,在将数组不断二分为小的数组,再将二分后的小数组再通过归并程序使得其变得有序。通过递归方法得出算法复杂度递推公式为:
通过master公式可得,归并排序的时间复杂度为\(O(nlogn)\),额外空间复杂度为\(O(n)\),实现可以做到稳定性。
2. java代码实现
import java.util.Arrays;
/**
* @author: syx
* @date: 2021/6/4 11:03
* @version: v1.0
*/
public class MergeSort {
public static void main(String[] args) {
int[] nums = new int[]{5, 4, 3, 2, 5, 6, 6};
MergeSort(nums);
System.out.println(Arrays.toString(nums));
}
//归并排序的主要入口
public static void MergeSort(int[] nums) {
//当待排序数组为空,或者只有一个值时,直接返回,不排序。
if (nums == null || nums.length < 2) {
return;
}
//否则开始归并排序
MergeSort(nums, 0, nums.length - 1);
}
/**
* 归并排序的主体
*
* @param nums
* @param start
* @param end
*/
public static void MergeSort(int[] nums, int start, int end) {
//当排序的数组只有一个元素则直接返回
if (start == end) {
return;
}
//取中值
int mid = start + ((end - start) >> 1);
//将数组的两半部分分别归并排序后,再进行归并操作
MergeSort(nums, start, mid);
MergeSort(nums, mid + 1, end);
Merge(nums, start, mid, end);
}
/**
* 归并算法
*
* @param nums
* @param start
* @param mid
* @param end
*/
public static void Merge(int[] nums, int start, int mid, int end) {
int[] help = new int[end - start + 1];// 定义一个辅助空间,空间的大小与需要归并的长度相同
int i = 0;//辅助空间的指针
int p1 = start;//待排序数组中左半部分的指针
int p2 = mid + 1;//待排序数组中右半部分的指针
while (p1 <= mid && p2 <= end) {//当两个指针都没有过界
// 将两个指针指向的数字比较,较小的数字存入辅助数组中,指向小的数字的指针右移一位
if (nums[p1] < nums[p2]) {
help[i++] = nums[p1];
p1++;
} else {
help[i++] = nums[p2];
p2++;
}
}
//进行完上一个循环后,至少由一个指针过界,将另一个没有过界的指针继续移动,将余下的数字放入辅助数组中
while (p1 <= mid) {
help[i++] = nums[p1];
p1++;
}
while (p2 <= end) {
help[i++] = nums[p2];
p2++;
}
// 再将辅助数组中的元素,按位置放回原数组中。
for (int j = 0; j < help.length; j++) {
nums[start + j] = help[j];
}
}
}
2. 归并排序的相关扩展
1. 小和问题
描述
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子
[1,3,4,2,5]
1左边比1小的数:没有
3左边比3小的数:1
4左边比4小的数:1,3
2左边比2小的数:1
5左边比5小的数:1,3,4,2
所以小和为1+1+3+1+1+3+4+2=16
方法与代码
可以通过在归并排序的代码上修改来解出这道题,在归并排序时,每次归并时,因为左右两边的数组都是有序的,在查出左边的数(下标为\(m\))比右边的数(下标为\(n\))小的时候,可以通过下标计算来确定左边的数(下标为\(m\))比右边的\(k\)个数小。
在归并排序使得局部有序的过程中就可以求得小和的局部解\(k*nums[m]\),将部分解相加就可以得到数组的小和。
代码如下:
/**
* @author: syx
* @date: 2021/6/8 11:33
* @version: v1.0
*/
public class minSum {
static int minum = 0;
public static void main(String[] args) {
int[] nums = new int[]{1,3,4,2,5};
MergeSort(nums);
System.out.println(Arrays.toString(nums));
System.out.println(minum);
}
//归并排序的主要入口
public static void MergeSort(int[] nums) {
//当待排序数组为空,或者只有一个值时,直接返回,不排序。
if (nums == null || nums.length < 2) {
return;
}
//否则开始归并排序
MergeSort(nums, 0, nums.length - 1);
}
/**
* 归并排序的主体
*
* @param nums
* @param start
* @param end
*/
public static void MergeSort(int[] nums, int start, int end) {
//当排序的数组只有一个元素则直接返回
if (start == end) {
return;
}
//取中值
int mid = start + ((end - start) >> 1);
//将数组的两半部分分别归并排序后,再进行归并操作
MergeSort(nums, start, mid);
MergeSort(nums, mid + 1, end);
Merge(nums, start, mid, end);
}
/**
* 归并算法
*
* @param nums
* @param start
* @param mid
* @param end
*/
public static void Merge(int[] nums, int start, int mid, int end) {
int[] help = new int[end - start + 1];// 定义一个辅助空间,空间的大小与需要归并的长度相同
int i = 0;//辅助空间的指针
int p1 = start;//待排序数组中左半部分的指针
int p2 = mid + 1;//待排序数组中右半部分的指针
while (p1 <= mid && p2 <= end) {//当两个指针都没有过界
// 将两个指针指向的数字比较,较小的数字存入辅助数组中,指向小的数字的指针右移一位
if (nums[p1] < nums[p2]) {
help[i++] = nums[p1];
minum += nums[p1]*(end-p2+1);// 只修改了一行,就可以实现小和计算
p1++;
} else {
help[i++] = nums[p2];
p2++;
}
}
//进行完上一个循环后,至少由一个指针过界,将另一个没有过界的指针继续移动,将余下的数字放入辅助数组中
while (p1 <= mid) {
help[i++] = nums[p1];
p1++;
}
while (p2 <= end) {
help[i++] = nums[p2];
p2++;
}
// 再将辅助数组中的元素,按位置放回原数组中。
for (int j = 0; j < help.length; j++) {
nums[start + j] = help[j];
}
}
}
3. 逆序对问题
描述
设 \(A\)为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。
如果存在正整数 \(i<j\) 使得 \(i<j\) 而且 \(A[i]>A[j]\),则 \(A[i],A[j]\) 这一个有序对称为\(A\)的一个逆序对,也称作逆序。逆序对的数量称作逆序数。
例子
[1,3,4,2,5,4]
逆序对有 <4,2>,<5,4>
方法与代码
逆序对与小和问题类似,当左边的数大于右边的数的时候,就产生了逆序对。同样的,可以在归并中将逆序对来计数,对于左半部分中的一个数,如果这个数大于右半部分的数时,由于左右两边数组都是有序的,所以可以计算出左边有多少个数字大于右边的这个数。
package syx.com;
import java.util.Arrays;
/**
* @author: syx
* @date: 2021/6/8 11:53
* @version: v1.0
*/
public class ReversePair {
static int count = 0;
public static void main(String[] args) {
int[] nums = new int[]{3,2,1};
MergeSort(nums);
System.out.println(Arrays.toString(nums));
System.out.println(count);
}
//归并排序的主要入口
public static void MergeSort(int[] nums) {
//当待排序数组为空,或者只有一个值时,直接返回,不排序。
if (nums == null || nums.length < 2) {
return;
}
//否则开始归并排序
MergeSort(nums, 0, nums.length - 1);
}
/**
* 归并排序的主体
*
* @param nums
* @param start
* @param end
*/
public static void MergeSort(int[] nums, int start, int end) {
//当排序的数组只有一个元素则直接返回
if (start == end) {
return;
}
//取中值
int mid = start + ((end - start) >> 1);
//将数组的两半部分分别归并排序后,再进行归并操作
MergeSort(nums, start, mid);
MergeSort(nums, mid + 1, end);
Merge(nums, start, mid, end);
}
/**
* 归并算法
*
* @param nums
* @param start
* @param mid
* @param end
*/
public static void Merge(int[] nums, int start, int mid, int end) {
int[] help = new int[end - start + 1];// 定义一个辅助空间,空间的大小与需要归并的长度相同
int i = 0;//辅助空间的指针
int p1 = start;//待排序数组中左半部分的指针
int p2 = mid + 1;//待排序数组中右半部分的指针
while (p1 <= mid && p2 <= end) {//当两个指针都没有过界
// 将两个指针指向的数字比较,较小的数字存入辅助数组中,指向小的数字的指针右移一位
if (nums[p1] <= nums[p2]) {
help[i++] = nums[p1];
p1++;
} else {
help[i++] = nums[p2];
count += mid - p1 +1;
p2++;
}
}
//进行完上一个循环后,至少由一个指针过界,将另一个没有过界的指针继续移动,将余下的数字放入辅助数组中
while (p1 <= mid) {
help[i++] = nums[p1];
p1++;
}
while (p2 <= end) {
help[i++] = nums[p2];
p2++;
}
// 再将辅助数组中的元素,按位置放回原数组中。
for (int j = 0; j < help.length; j++) {
nums[start + j] = help[j];
}
}
}
4. 总结
在归并排序的扩展题目中,小和问题的核心是计算出右边有多少个数比一个数大,逆序对问题是计算左边有多少个数比一个数大,利用归并算法的框架可以快速的将这些问题计算出来。在排序的分治过程中,对归并部分的比较做一些改动,根据比较结果做一个计数,利用下标来计算个数多少从而得出问题的解。