提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、master公式
T
(
N
)
=
a
∗
T
(
N
/
b
)
+
O
(
N
d
)
T(N) = a * T(N/b) + O(N^d)
T(N)=a∗T(N/b)+O(Nd)
T
(
N
)
T(N)
T(N)指母问题的数据量为
N
N
N级别,
T
(
N
/
b
)
T(N/b)
T(N/b)指子问题的规模为
N
/
b
N/b
N/b,
a
a
a指子问题被调用的次数,
O
(
N
d
)
O(N^d)
O(Nd)指调用子问题之外的算法复杂度。
public static int process(int[] arr, int L, int R) {
if (L == R) {
return arr[L];
}
int mid = L + (R - L)/2;
int leftMax = process(arr, L, mid);
int rightMax = process(arr, mid + 1, R);
return Math.max(leftMax, rightMax);
}
用master公式代入上面的方法:
T
(
N
)
=
2
∗
T
(
N
/
2
)
+
O
(
1
)
T(N) = 2 * T(N/2) + O(1)
T(N)=2∗T(N/2)+O(1)
求解master公式的时间复杂度
将master公式的三个参数a、b、d代入以下式子,若
l
o
g
b
a
<
d
log_b a < d
logba<d 则
O
(
N
d
)
O(N^d)
O(Nd)
l
o
g
b
a
>
d
log_b a > d
logba>d 则
O
(
N
l
o
g
b
a
)
O(N^{log_b a})
O(Nlogba)
l
o
g
b
a
=
d
log_b a = d
logba=d 则
O
(
N
d
∗
l
o
g
N
)
O(N^d * logN)
O(Nd∗logN)
二、归并排序
1、概述
找到一个数组的中点,先让左边的子数组排好序,再让右边的子数组排好序,最后merge两个子数组(可用双指针merge)。
2、代码实例
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 L, int R) {
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;//指向L
int p2 = M + 1;//指向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 (int i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
//for test
public static void comparator(int[] arr){
Arrays.sort(arr);
}
}
3、分析归并排序时间复杂度
对以上代码分析,得到master公式的三个参数为
a
=
2
,
b
=
2
,
d
=
1
a = 2, b = 2, d = 1
a=2,b=2,d=1,所以有:
T
(
N
)
=
2
∗
T
(
N
/
2
)
+
O
(
N
)
T(N) = 2 * T(N/2) + O(N)
T(N)=2∗T(N/2)+O(N)
由于
l
o
g
b
a
=
d
log_b a = d
logba=d,所以归并排序的时间复杂度为
O
(
N
∗
l
o
g
N
)
O(N * logN)
O(N∗logN)
额外空间复杂度为
O
(
N
)
O(N)
O(N)
时间复杂度变低的原因
冒泡排序法和选择排序法浪费了大量时间在数的比较上,而归并排序并没有浪费这种比较行为,而是把这种比较变成一个整体有序的部分,再将这个部分与另一个部分merge得到更大的一个有序的部分。
四、归并排序扩展
1、小和问题
问题描述:在一个数组中,每一个数左边比当前数小的数累加起来
如:数组
[
1
,
3
,
4
,
2
,
5
]
[1, 3, 4, 2, 5]
[1,3,4,2,5]的小和为
0
+
1
+
(
1
+
3
)
+
1
+
(
1
+
3
+
4
+
2
)
=
16
0+1+(1+3)+1+(1+3+4+2)=16
0+1+(1+3)+1+(1+3+4+2)=16
代码实现
可以把小和问题理解为当前数x的后面有多少个数a比当前数x大,从头到尾遍历数组,把a * x的结果累加起来,最终得到小和。
public class SmallSum {
public static int smallSum(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, 1, mid) + process(arr, mid + 1, R) + merge(arr, 1, 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;//指向L
int p2 = M + 1;//指向M + 1
int res = 0;
while(p1 <= M && 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 <= M){
help[i++] = arr[p1++];
}
while(p2 <= R){
help[i++] = arr[p2++];
}
for (int i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
return res;
}
//for test
public static void comparator(int[] arr){
Arrays.sort(arr);
}
}
2、逆序对
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序对。
五、快速排序法
荷兰国旗问题
问题一:给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度 O ( N ) O(N) O(N)。
问题二:给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度 O ( N ) O(N) O(N)。
问题一
从左到右遍历数组arr,把数组分为小于等于区a和大于区b。初始时a区为0。开始遍历数组,数组在位置i的数小于等于num,则小于等于区a的下一个数与arr[i]交换,并且a区长度加一,i++,;若数组在位置i的数大于num,则直接i++。i++越界时停止遍历。
问题二
从左到右遍历数组arr,把数组分为小于区a、等于区b和大于区c。初始时a区在数组最左侧,c区在数组最右侧,长度均为0。
1)arr[i] < num,arr[i]和a区下一个数交换,a区右扩,i++
2)arr[i] == num,i++;
3)arr[i] > num,arr[i]和c区前一个数交换,c区左扩,i不变。
大于区和i相遇时遍历结束。
快速排序1.0
思想
把数组最后一个数设置为num,然后按照问题一的方法遍历数组,让数组左边的数都小于等于num,右边的数都大于num。然后数组左边和右边重复这个过程。
快速排序2.0
思想
把数组最后一个数设置为num,然后按照问题二的方法遍历数组,让数组左边的数都小于num,右边的数都大于num,中间的数等于num。然后数组左边和右边重复这个过程。
快速排序2.0版本比1.0版本效率更高。
复杂度分析
无论是快排1.0还是2.0版本,时间复杂度都是
O
(
N
2
)
O(N^2)
O(N2)。
举例:对数组[1,2,3,4,5,6,7,8]进行排序。每次都需要遍历一次数组。
原因:划分值的选取很偏,导致最差情况。
快速排序3.0
思想
随机在数组选取一个数,把这个数设为num,然后与数组最后一个数交换。这样的选取方法下每种情况(最好、最坏、次好、次坏…)等概率发生。取数学期望后时间复杂度为
O
(
N
∗
l
o
g
N
)
O(N * logN)
O(N∗logN)。
代码实现
public class QuickSort{
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int L, int R) {
if (L < R) {
swap(arr, L + (int)(Math.random() * (R - L + 1)), R);
int[] p = partition(arr, L, R);
quickSort(arr, L, p[0] - 1); //小于区
quickSort(arr, p[1] + 1, R); //大于区
}
}
// 这是一个处理arr[1..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){
int less = L - 1;
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);
return new int[] {less + 1, more};
}
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
int arr[i] = arr[j];
int arr[j] = temp;
}
}