归并排序及其应用
归并排序采用分治和递归的思想完成排序,简单来说就是,将一个规模较大的排序问题转换成两个较小规模的问题,最终形成两个已经排好的序的子数组,进而进行归并即可。
- 左路归并
- 右路归并
- 合并左右两路已排好序的数组
具体代码如下:
public class MergeSort {
/**
* 归并排序 分治,整体排序=左右两边有序+合并后再排序
* @param arr
* @param left
* @param right
*/
public static void mergeSort(int[] arr,int left,int right){
//递归结束条件,只有一个元素不用排序
if(left==right){
return ;
}
//左右两边排序,确定终中点
int mid=left+((right-left)>>1);
mergeSort(arr,left,mid);
mergeSort(arr,mid+1,right);
merge(arr,left,mid,right);
}
/**
* 合并两个有序数组,需要一个额外的数组来进行存储
* 两个指针分别指向两个有序数组的初始位置,一个指针指向新生成数组的位置
* @param arr
* @param left
* @param mid
* @param right
*/
public static void merge(int[] arr,int left,int mid,int right){
int[] help=new int[right-left+1];
int i=0;
int p1=left;
int p2=mid+1;
//两个数组都没有越界,就比较大小,将指针指向的最小的那个元素放在新数组中
while(p1<=mid && p2<=right){
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
//当一个数组的指针越界后,将另外的拷贝过来即可,两个中只有一个会越界
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=right){
help[i++]=arr[p2++];
}
//重新写入原数组 注意数组从left开始
for(i=0;i<help.length;i++){
arr[left+i]=help[i];
}
}
public static void main(String[] args) {
int[] arr=new int[]{4,1,5,6,2,8,7};
Common.printArr(arr);
mergeSort(arr,0,arr.length-1);
Common.printArr(arr);
}
}
##应用
小和问题以及逆序对
求解一个数组中有多少逆序对,实际在归并过程中,会形成两个排好序的数组,可以进行加速。如果A中一个元素i大于B中一个元素j,那么A中从元素i之后的所有数都会与j形成逆序对。
整个逆序对的个数=左路逆序对的个数+右路个数+合并形成的个数
代码如下:
public class SmallSum {
//应用 小和问题 与 逆序对
//在一个数组中,每一个数左边比当前数小的数的和,称为小和,求一个数组的小和
//笨办法,每个位置都去遍历一遍
//使用归并排序加速
/**
* 归并 分治,整体的小和=左边排列产生的小和+右边的小和+左右两边有序产生的小和
* @param arr
* @param left
* @param right
*/
public static int mergeSort(int[] arr,int left,int right){
//递归结束条件,只有一个元素不用排序
if(left==right){
return 0 ;
}
//左右两边排序,确定终中点
int mid=left+((right-left)>>1);
return mergeSort(arr,left,mid)
+mergeSort(arr,mid+1,right)
+merge(arr,left,mid,right);
}
/**
* 合并两个有序数组,需要一个额外的数组来进行存储
* 两个指针分别指向两个有序数组的初始位置,一个指针指向新生成数组的位置
* @param arr
* @param left
* @param mid
* @param right
*/
public static int merge(int[] arr,int left,int mid,int right){
int sum=0;
int[] help=new int[right-left+1];
int i=0;
int p1=left;
int p2=mid+1;
//两个数组都没有越界,就比较大小,将指针指向的最小的那个元素放在新数组中
while(p1<=mid && p2<=right){
//p1 < p2 p2到后面全部是比p1大的数
//逆序 左边的数比右边的数大,构成一组逆序对,左边部分有序后,后面所有数都能和其组成逆序对
sum+=arr[p1]>arr[p2]?(mid-p1+1):0;
//小和
// sum+=arr[p1]<arr[p2]?arr[p1]*(right-p2+1):0;
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
// if(arr[p1]<arr[p2]){
// help[i]=arr[p1];
// sum+=help[i]*(right-p2+1);
// p1++;i++;
// }else{
// help[i]=arr[p2];
// p2++;i++;
// }
}
//当一个数组的指针越界后,将另外的拷贝过来即可,两个中只有一个会越界
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=right){
help[i++]=arr[p2++];
}
//重新写入原数组 注意数组从left开始
for(i=0;i<help.length;i++){
arr[left+i]=help[i];
}
return sum;
}
public static void main(String[] args) {
int[] arr=new int[]{2,7,5,3,4};
int a=mergeSort(arr,0,arr.length-1);
System.out.println(a);
}
}
可以发现与归并排序的代码几乎相同,因此基础排序算法很重要,尤其是其思想,这里主要是分治,治众如治寡,分数也。
复杂度分析与证明:
T(n)=2T(n/2)+O(n)
T(n)/n=T(n/2)/(n/2) +1;
递推得 T(n)/n=lgn;
空间复杂度:
由于需要拷贝数组,进行排序,消耗空间,当数据量特别大的时候,内存消耗较大
优化:
在数据量比较小的时候,可以使用插入排序这种原地排序的算法,不占空间,时间复杂度也不高
还可能存在的情况是,归并的时候左右两边的子排列已经构成一个有序的数组,那么可以通过判断左边最大的那个数是否小于右边最小的那个数,来加速
除了递归实现外还可以自底向上去实现
public class MergeBU
{
private static void merge(...)
{ /* as before */ }
public static void sort(Comparable[] a)
{
int N = a.length;
Comparable[] aux = new Comparable[N];
//每次扩大一倍的距离
for (int sz = 1; sz < N; sz = sz+sz)
//最底层排好
for (int lo = 0; lo < N-sz; lo += sz+sz){
merge(a, aux, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
}
}
详细的非递归实现归并排序
public static void sort(Comparable[] a ){
Comparable[] b=new Comparable[a.length];
int left=0,right=a.length-1;
for(int size=1;size<a.length;size=2*size){
for(left=0;left+size<a.length;left+=size*2){
//left+2*size-1可能会越界 ***** [0,size-1] [size,2*size-1],[2*size,2*size-1+size-1+1,2*size+size*2-1]
//[2*size,3size-1,4size-1]
//[0,2size-1,4size-1]
merge(a,b,left,left+size-1,Math.min(left+size*2-1,a.length-1));
}
}
}
public static int less(Comparable a,Comparable b){
return a.compareTo(b);
}
public static void merge(Comparable[] a,Comparable[] b,int left,int mid,int right){
for(int i=0;i<a.length;i++){
b[i]=a[i];
}
int i=left,j=mid+1;
//加速
if(less(a[mid],a[mid+1])<0){
return;
}
//注意K的取值,从left开始
for(int k=left;k<=right;k++){
if(i>mid){
a[k]=b[j++];
}else if(j>right){
a[k]=b[i++];
}else if(less(b[i],b[j])<0){
a[k]=b[i++];
}else{
a[k]=b[j++];
}
}
}
public static void show(Comparable[] a){
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public static void main(String[] args) {
Integer[] a1={5,2,12,8,3,9,0};
// String[] a1={"anbd","dafe","add","ee"};
sort(a1);
show(a1);
}