前言
本文主要介绍了两种排序,归并排序和快速排序,归并排序有递归和非递归2种方式实现,快速排序的升级版为荷兰国旗问题。
1.归并排序
归并排序:
1)整体是递归,左边排好序+右边排好序+ merge让整体有序;
2)让其整体有序的过程里用了排外序方法;
3)利用master公式来求解时间复杂度;
4)可以用非递归实现。
递归方式举例如下:
实现如下:
// 递归方法实现
public static void mergeSort1(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
// base case
if (L == R) {
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int M, int R) {
int[] help = new int[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
System.arraycopy(help, 0, arr, L, help.length);
}
非递归方式举例如下:
实现如下:
// 非递归方法实现
public static void mergeSort2(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
int N = arr.length;
int mergeSize = 1; // 步长
while (mergeSize < N) {
int L = 0; // 当前左组的第一个位置
while (L < N) {
if (L + mergeSize >= N) {
break;
}
int M = L + mergeSize - 1;
int R = Math.min(M + mergeSize, N - 1);
merge(arr, L, M, R);
L = R + 1;
}
// 防止溢出
if (mergeSize > N / 2) {
break;
}
mergeSize <<= 1;
}
}
现在计算时间复杂度:
递归方式:
非递归方式:
综上,归并排序复杂度:T(N)= 2*T(N/2)+ O(N^1)
根据master可知时间复杂度为O(N*logN)
merge过程需要辅助数组,所以额外空间复杂度为O(N)
归并排序的实质是把比较行为变成了有序信息并传递,比O(N^2)的排序快
相比于冒泡排序、选择排序和插入排序O(N2)的时间复杂度,归并排序O(N*LogN)的时间复杂度优化了很多,这是因为减少了比较次数。
用常见面试题再深入理解一下归并排序的精髓。
在一个数组中,一个数组左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。
举例:[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
基本思路:
左组的数小于右组的数时,产生小和,左指针右移;
左组的数等于右组时,直接拷贝右组,不产生小和;
左组的数大于右组时,直接拷贝右移,不产生小和。
小和产生的时候就是merge的时候,如下:
原理是:思路转换,从计算一个数左边更小的数之和转换为一个数右边更大的数之和。
实现如下:
public class SmallSum {
public static int smallSum(int[] arr) {
if (null == arr || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
public static int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + (r - l) >> 1;
return process(arr, l, mid)
+ process(arr, mid+1, r)
+ merge(arr, l, mid, r);
}
public static int merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l, p2 = mid + 1;
int res = 0;
while (p1 <= mid && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
System.arraycopy(help, 0, arr, l, help.length);
return res;
}
}
扩展:在一个数组中求所有的降序对。
如下:
也就是求一个数右边有多少个数比它小,或者说,左边有多少个数比它大。
图示如下:
2.快速排序
Partition过程:
给定一个数组arr,和一个整数num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边;
要求额外空间复杂度O(1),时间复杂度O(N)。
思路如下:
Partition过程升级版(荷兰国旗问题):
给定一个数组arr,和一个整数num。请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边;
要求额外空间复杂度O(1),时间复杂度O(N)。
思路如下:
两种分区实现如下:
public class PartitionAndQuickSort {
/*
普通分区:
给定一个数组arr和一个整数num,
请把小于等于num的数放在数组的左边,
大于num的数放在数组的右边
*/
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) {
return L;
}
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
/*
分区升级版:荷兰国旗:
给定一个数组arr和一个整数num
请把小于num的数放在数组的左边,
等于num的数放在数组的中间,
大于num的数放在数组的右边
*/
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) {
return new int[]{-1, -1};
}
if (L == R) {
return new int[]{L, R};
}
int less = L - 1, more = R;
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else {
swap(arr, index, --more);
}
}
swap(arr, more, R);
return new int[]{less + 1, more};
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
快速排序:
快速排序有3种方式:
(1)利用普通分区算法;
(2)利用荷兰国旗算法;
(3)随机选数与最后一个数交换,再利用荷兰国旗算法。
分析时间复杂度:
随机快排的时间复杂度分析:
1)通过分析知道,划分值越靠近中间,性能越好;越靠近两边,性能越差
2)随机选一个数进行划分的目的就是让好情况和差情况都变成概率事件
3)把每一种情况都列出来,会有每种情况下的时间复杂度,但概率都是1/N
4)那么所有情况都考虑,时间复杂度就是这种概率模型下的长期期望,
时间复杂度O(N*logN),额外空间复杂度O(logN)都是这么来的。
(1)前两种:
最差情况是数组已经有序:
(2)第三种
一般情况是获取到的随机数在数组中间位置附近,就属于较好的情况:
空间复杂度:
最差情况下是O(N),累加求期望收敛于O(LogN)。
3种快速排序实现如下:
public static void quickSort1(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
process1(arr, 0, arr.length-1);
}
public static void process1(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
public static void quickSort2(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
process2(arr, 0, arr.length-1);
}
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int[] equalArea = netherlandsFlag(arr, L, R);
process1(arr, L, equalArea[0] - 1);
process1(arr, equalArea[1]+ 1, R);
}
public static void quickSort3(int[] arr) {
if (null == arr || arr.length < 2) {
return;
}
process3(arr, 0, arr.length-1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
swap(arr, (int) (Math.random() * (R + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process1(arr, L, equalArea[0] - 1);
process1(arr, equalArea[1]+ 1, R);
}
总结
归并排序有很多个应用场景,一般应用在数组中左边的数比右边的数满足某个条件时,进行某个操作,都可以在归并的过程中进行解决。