在学习左神的算法课程,关于归并排序有些收获,在此记录,以备后查。
分治的思想,将对数组arr[]排序的任务(规模为N),分为对左、右半边排序(规模各位N/2)、合并(规模为N)这三步操作。对左(或右)半边排序又可以拆分为两个更小规模的排序与一个合并的操作。如此递归,直到待排序的规模为1.
/**
* 假定左边和右边分别已经排序好,将左右两边合并成有序的序列
* @param arr 待排序的数组
* @param left arr左边界 inclusive
* @param right arr右边界 inclusive
*/
public static void mergeSort(int[] arr,int left,int right){
//边界条件
if(left>=right){
return;
}
int mid =left+((right-left)>>1);
//左边排序
sort(arr, left, mid);
//右边排序
sort(arr, mid + 1, right);
//左右合并
merge(arr,left,mid,right);
}
/**
* 假定左边和右边分别已经排序好,将左右两边合并成有序的序列
* @param arr
* @param left
* @param mid
* @param right
*/
private static void merge(int[] arr, int left, int mid, int right) {
int i = left, j = mid + 1, k=0;
int[] tmp = new int[right - left + 1];
while (i<=mid&&j<=right){
tmp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) tmp[k++] = arr[i++];
while (j <= right) tmp[k++] = arr[j++];
for (int l = 0; l < tmp.length; l++) {
arr[left + l] = tmp[l];
}
}
所有递归都可以改成非递归写法。递归是自上而下的,那么对应的非递归写法就是自下而上的逻辑。
/**
* 归并排序的非递归版本
* @param arr
*/
public static void mergeSort2(int[] arr){
if(arr==null||arr.length<2){
return;
}
//i指左边要合并的数据的size,以2^i的趋势变化,它控制了归并操作要进行几趟
for (int i = 1; i < arr.length; i=i<<1) {
//j指每一趟归并过程中,左半边的开始的指针,所以左半边的范围是[j,j+i-1],右半边的范围要考虑到长度不够的情况,所以是[j+i,min(j+2*i-1,arr.length-1)]
for (int j = 0; j +i < arr.length; j=j+2*i) {
merge(arr,j,j+i-1,Math.min(j+2*i-1,arr.length-1));
}
}
}
/**
* 假定左边和右边分别已经排序好,将左右两边合并成有序的序列
* @param arr
* @param left
* @param mid
* @param right
*/
private static void merge(int[] arr, int left, int mid, int right) {
int i = left, j = mid + 1, k=0;
int[] tmp = new int[right - left + 1];
while (i<=mid&&j<=right){
tmp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) tmp[k++] = arr[i++];
while (j <= right) tmp[k++] = arr[j++];
for (int l = 0; l < tmp.length; l++) {
arr[left + l] = tmp[l];
}
}
设数组arr={1,3,4,2,5},数组的小和指每一个数左边比它小的数的和。如
1:0
3:1
4:1、3
2:1
5:1、3、4、2
数组arr的小和为1+1+3+1+1+3+4+2=16。
简答方法,可以通过二重遍历进行暴力破解,时间复杂度是N2。这在面试中显然是没分的,有没有更好地方法呢?整个题目,是要获知数组中每一个数左边有哪些数比它小,并把他们累加。用同样分治的思想,总的小和应该等于左半边的小和+右半边的小和+merge过程中的小和。
其中实现优化的关键点在于merge过程,这一步求的小和,是指对右边每个数而言,左边有哪些数比它小,因此左边或右边自己内部的顺序不影响merge求小和的结果,如此也就可以利用上面的归并排序,压榨出这一步的小和。
归并过程中,左边范围[left,mid],左指针为i,右边范围[mid+1,right],右指针为j。当arr[i]
/**
* 求数组的小和
*
* @param arr
* @param left
* @param right
* @return
*/
public static int getArrSmallerSum(int[] arr, int left, int right) {
if (left >= right) {
return 0;
}
int mid = left + ((right - left) >> 1);
int leftSum = getArrSmallerSum(arr, left, mid);
int rightSum = getArrSmallerSum(arr, mid + 1, right);
int mergeSum = merge2(arr, left, mid, right);
return leftSum + rightSum + mergeSum;
}
/**
* 假定左边和右边分别已经排序好,将左右两边合并成有序的序列,
* 并返回合并过程中的小和
*
* @param arr
* @param left
* @param mid
* @param right
*/
private static int merge2(int[] arr, int left, int mid, int right) {
int sum = 0;
int i = left, j = mid + 1, k = 0;
int[] tmp = new int[right - left + 1];
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
//关键是这一行!!!
sum = sum + arr[i] * (right - j + 1);
tmp[k++] = arr[i++];
} else {
tmp[k++] = arr[j++];
}
}
while (i <= mid) tmp[k++] = arr[i++];
while (j <= right) tmp[k++] = arr[j++];
for (int l = 0; l < tmp.length; l++) {
arr[left + l] = tmp[l];
}
return sum;
}
数组arr={3,1,4,2,5,7,4}中,降序对指左边大于右边的数组成的对,如:
3:{3,1}、{3,2}
1:{}
4:{4,2}
5:{5,4}
7:{7,4}
数组arr降序对的数量为2+0+1+1+1=5
上一题,是要获知数组中,每一个数左边有哪些数比它小。这一题,是要获知数组中,每个数右边有哪些数比它小。换汤不换药,还是基于归并排序修改,只不过这次关注的是归并过程中arr[i]>arr[j]时,应该有(mid-i+1)个数可以和arr[j]组成降序对
/**
* 统计数组中降序对的数量
* @param arr
* @param left
* @param right
* @return
*/
public static int getArrDesCount(int[] arr, int left, int right) {
if (left >= right) {
return 0;
}
int mid = left + ((right - left) >> 1);
int leftSum = getArrDesCount(arr, left, mid);
int rightSum = getArrDesCount(arr, mid + 1, right);
int mergeSum = merge3(arr, left, mid, right);
return leftSum + rightSum + mergeSum;
}
/**
* 假定左边和右边分别已经排序好,将左右两边合并成有序的序列,
* 并返回合并过程降序对的数量
*
* @param arr
* @param left
* @param mid
* @param right
*/
private static int merge3(int[] arr, int left, int mid, int right) {
int sum = 0;
int i = left, j = mid + 1, k = 0;
int[] tmp = new int[right - left + 1];
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tmp[k++] = arr[i++];
} else {
//关键是这一行!!!
sum=sum+(mid-i+1);
tmp[k++] = arr[j++];
}
}
while (i <= mid) tmp[k++] = arr[i++];
while (j <= right) tmp[k++] = arr[j++];
for (int l = 0; l < tmp.length; l++) {
arr[left + l] = tmp[l];
}
return sum;
}