认识O(NlogN)的排序
一、归并排序
归并排序图例,就是一个简单的分治思想
基本操作:
- 找出数组的基准值位置mid=(l+r)/2,其中l,r分别表示当前数组的头尾位置
- 分别递归调用merge_sort(arr ,l ,mid)和merge(arr,mid+1,r)
- 从基准值将数组一分为二
- 构造一个临时数组对两个数组进行从小到大的合并
- 将临时数组里面的元素放到arr数组里面
//代码很简单清晰,就不解释了
public class MergeSort {
public static void mergeSort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length -1);
}
public static void process(int[] arr, int i, int R) {
if(L == R)
return;
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
preocess(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++];
}
for(i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
}
用master公式来分析归并排序的时间复杂度:
将数组等分为两部分,b=2
,对两部分进行递归,a=2
因此log(b,a) = d = 1
,归并排序的时间复杂度为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
如果我们从左至右依次遍历数组求小数和,那么时间复杂度很明显会是O(N^2)
想要优化算法,可以用到归并排序的思想,将时间复杂度变为O(NlogN)
public class SmallSum {
//求小数和函数
public static int samllSum(int[] arr) {
if(arr == null || 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 m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while(p1 <= m && p2 <= r) {
//这里的res是小数和,具体的求法见下文
res += arr[p1] < arr[p2] ? (r - p2 +1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= m) {
help[i++] = arr[p1++];
}
while(p2 <= r) {
help[i++] = arr[p2++];
}
for(i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
}
草,这可太难解释了,让我憋一会
当我们看到题目时,可能第一思路是遍历数组,让每个元素与其左边的数对比,来找出小数,这样操作的时间复杂度为O(N^2)。如果我们换一种思路,同样是遍历数组,但是这次是找每个元素右边有几个数比当前元素大,然后这个个数再乘以当前元素,依次相加之后得到的依然是小数和。
我感觉我在这干说可能也说不明吧,各位直接看视频吧
一小时零四分开始
逆序对问题
各位考研的朋友可能对这个概念很熟悉,老线代人了。
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
这个问题就是上面问题的相反求解,上面是求左边比右边数小的数,这个问题是求左边比右边数大的数。求解过程这里就不解释了。
二、快速排序
荷兰国旗问题
(1)给定一个数组arr
,和一个数num
,请把小于等于num的数放在数组左边,大于num
的数放在数组的右边,要求额外空间复杂度O(1)
,时间复杂度O(N)
。
设置一个小于区域,从数组的左边开始,对给定的数组进行遍历:
- 当数组元素小于
num
时,将这个元素与小于区域右边的第一个数进行交换,小于区域右移一位,指针后移一位。 - 当数组元素等于
num
时,指针直接右移。 - 直到指针越界停止。
(2)给定一个数组arr
,和一个数num
,请把小于num
的数放在数组的左边,等于num
的数放在数组中间,大于num
的数放在数组右边。要求额外空间复杂度O(1)
,时间复杂度O(N)
。
设置一个小于区域,从数组左边开始;设置一个大于区域,从数组右边开始,对给定数组进行遍历:
- 当数组元素小于
num
时,将这个元素与小于区域右边的第一个数进行交换,小于区域右移一位,指针后移一位。 - 当数组元素等于
num
时,指针直接右移。 - 当数组元素大于num时,将这个元素与大于区域左边的第一个数进行交换,大于区域左移一位,指针不动(这是因为交换过来的元素还没有被遍历过)。
这两个算法都可以使用快速排序的思想进行:
- 把数组范围中的最后一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧 < 划分值、中间 == 划分值、右值 > 划分值 - 对左侧范围和右侧范围,递归执行
分析:
- 划分值越靠近两侧复杂度越高;划分值越靠近中间,复杂度越低
- 可以轻而易举的举出最差的例子,所以不改进的快速排序时间复杂度为
O(N^2)
。
因此如果想要降低时间复杂度,需要的是在数组中随机找出一个数作为划分值。
- 在数组范围中,等概率随机选一个数作为划分值,然后把数组通过荷兰国旗问题分为三个部分:
左侧 < 划分值、中间 == 划分值、右值 > 划分值 - 对左侧范围和右侧范围分别进行递归
- 时间复杂度为O(NlogN)
//快速排序代码
public class QuickSort {
public static void quickSort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
//arr[l..r]排序
public static void quickSort(int[] arr, int L, int R) {
if(L < R) {
//数组中随机选一个数与最右端数进行交换
swap(arr, L, (int)(Math.random{} * (R - L + 1)), R);
//这里数组p只有两个值,表示的是划分之后的等于划分数的左右两个边界的指针p[0],p[1]
int[] p = partition(arr, L, R);
quickSort(arr, L, p[0] - 1); // < 区
quickSort(arr, p[1] + 1, R); // > 区
}
}
//这是一个处理arr[l..r]的函数
//默认以arr[r]作为划分,arr[r] -> p <p ==p >p
//返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0] res[1]
public static int[] partition(int[] arr, int L, int R) {
//小于区域边界,从L的左边一位开始
int less = L - 1;
//大于区域边界,从R开始
int more = R;
while(L < more) {
if(arr[L] < arr[R]) {
//小于时,将当前位置的数和小于区域边界的下一个数进行交换
//小于区域右移一位,遍历下一元素
swap(arr, ++less, L++);
} else if(arr[L] > arr[R]) {
//大于时,将当前位置的数和大于区域边界前一个数进行交换
//大于 区域左移一位,遍历指针不移动
swap(arr, --more, L);
} else {
//等于是,直接开始遍历下一个数
L++;
}
}
//将划分值位置的数和大于区域边界的数交换
swap(arr, more, R);
//最后整个数组中,等于划分值的左右边界分别为less+1,more
return new int[] {less + 1, more};
}
}