目录
01 master公式求递归时间复杂度
master公式(也称主方法)是用来利用分治策略来解决问题经常使用的时间复杂度的分析方法,(补充:分治策略的递归解法还有两个常用的方法叫做代入法和递归树法,以后有机会和亲们再唠),众所周知,分治策略中使用递归来求解问题分为三步走,分别为分解、解决和合并,所以主方法的表现形式:
T [n] = aT[n/b] + f (n)(直接记为T [n] = aT[n/b] + O (N^d))
其中 a >= 1 and b > 1 是常量,其表示的意义是n表示问题的规模,a表示递归的次数也就是生成的子问题数,子问题规模应该一致,b表示每次递归是原来的1/b之一个规模,f(n)表示分解和合并所要花费的时间之和。
解法:
①当d < logb a时,时间复杂度为O(n^(logb a))
②当d == logb a时,时间复杂度为O((n^d)*logn)
③当d > logb a时,时间复杂度为O(n^d)
三个系数一旦确定,时间复杂度就确定了
public class RecursionTest {
// 递归原理分析
public static int getMax(int[] arr){
return process(arr,0, arr.length - 1);
}
// 求arr数组中i 到 j 中的最大值
public static int process(int arr[], int L, int R){
if(L == R){
return arr[L];
}
int mid = L + ((R - L) >> 1); // 求中点:如果直接用(l + r)/2 可能会超出界限
int leftMax = process(arr, L, mid); // 分而治之的思想
int rightMax = process(arr, mid + 1, R);
return Math.max(leftMax,rightMax);
}
}
02 归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
2.1 算法步骤
先让左侧部分有序, 再让右侧部分有序生成两个有序序列
-
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
-
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
-
重复步骤 3 直到某一指针达到序列尾;
-
将另一序列剩下的所有元素直接复制到合并序列尾。
2.2 归并排序代码
import java.util.Arrays;
public class MergeSort {
public static void mSort(int[] arr, int L, int R){ // 归并排序:范围 L 到 R
if(L == R){
return;
}
int mid = L + ((R - L) >> 1); // 求中点
mSort(arr, L, mid); // 递归调用左侧子序列
mSort(arr,mid + 1,R); // 递归调用右侧子序列
// 最终合并两个子序列的方法:
merge(arr,L,mid,R); //不是递归行为
}
//在merge中完成排序
public static void merge(int[] arr, int L , int mid, int R){
int[] help = new int[R - L + 1]; // 新建一个存放有序数组的新空间
//左侧序列: L 到 mid 右侧序列: mid + 1 到 R
int i = 0;
int p1 = L;
int p2 = mid + 1;
while(p1 <= mid && p2 <= R){ // 当两个下标都不越界,一直循环
if(arr[p1] <= arr[p2]){
help[i] = arr[p1];
p1++;
i++;
}else if(arr[p1] > arr[p2]){
help[i++] = arr[p2++];
}
}
// 当有任一下标越界
while(p1 <= mid){ // 直接复制左边剩下的
help[i] = arr[p1];
i++;
p1++;
}
while(p2 <= R){ // 直接复制右边剩下的数
help[i++] = arr[p2++];
}
for(i = 0; i < help.length; i++){ // 将help中的数拷贝回arr
arr[L + i] = help[i];
}
}
// 统一函数接口
public static void MergeSort(int[] arr){
if(arr == null || arr.length < 1){
System.out.println("无需排序");
return;
}
mSort(arr, 0, arr.length - 1);
}
public static void main(String[] args){
int[] arr = new int[]{3,5,2,-2,7,9,-10};
MergeSort(arr);
System.out.println(Arrays.toString(arr));
}
}
2.3 归并排序拓展: 小和问题
import java.util.Arrays;
public class SmallSum {
public static int smallSum(int[] arr){
if(arr == null || arr.length <= 1){
return 0;
}
return process(arr, 0, arr.length - 1);
}
// 排序过程,将arr数组中L 到 R 序列进行排序,同时求小和
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); // merge之后小和的数量
}
// 在merge中进行排序,并求每个序列的小和
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;
int p2 = mid + 1;
int res = 0; // 保存小和结果
while(p1 <= mid && p2 <= R){
// 只有左数比右数小,才产生小和增加的行为
if(arr[p1] < arr[p2]){
res += arr[p1] * (R - p2 + 1);
help[i++] = arr[p1++];
}else{
res += 0;
help[i++] = arr[p2++];
}
}
while(p1 <= mid){
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;
}
public static void main(String[] args){
int[] arr = new int[]{1,2,3,6,4,5};
System.out.println(smallSum(arr));
System.out.println(Arrays.toString(arr));
}
}
03 快速排序
时间复杂度O(Nlog N), 额外空间复杂度 O(Nlog N) 不稳定的排序
问题1
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。
方法:
用x标记小于区域的最后一个数的位置,然后用for循环依次遍历数组中的数,如果大于指定数字则跳过,不进行交换;如果小于或等于num,则与x的下一个位置进行交换。
如果第一个位置的数小于等于num,其实就是和自己交换,i一定永远大于等于x。
public static void main(String[] args) {
int[] arr = new int[]{3,5,4,2,6,7};
int num = 4;
int x = -1;
for (int i = 0; i < arr.length; i++){
if (arr[i] <= num){
swap(arr, i, ++x);
}
}
}
public static void swap(int[] arr, int i, int j){
if (i == j)
return ;
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
3.1 算法步骤
-
从数列中挑出一个元素,称为 "基准"(pivot);
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
3.2 荷兰国旗问题
import java.util.Arrays;
public class HolandFlag {
/*
荷兰国旗问题是把数组分成三个部分,小于 等于 和 大于
*/
public static void HolandFlag(int[] arr,int L, int R){
int less = L - 1; // 限制小于区域右侧指针
int more = R + 1; // 限制大于区域左侧指针
int pivot = arr[L]; // 指定一个pivot
for(int i = 0; i < more; ){ // 这里遍历到大于区域即可停止遍历
// 遍历时有三种情况
// 1:小于基准值
if(arr[i] < pivot){
swap(arr, i, ++less); // 与小于区域后一个数交换,小于区域右扩
i++;
}else if (arr[i] == pivot){
i++; // 直接后移
}else{ // 大于pivot
swap(arr, i, --more); // i保持不动 ,因为换过来的数没有被查看过
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 测试
public static void main(String[] args) {
int[] arr = {3,4,5,6,1,2,3,4,5,4,3};
System.out.println(Arrays.toString(arr));
HolandFlag(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
}
3.3 经典快排
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){ // 要确定L < R 时才执行, 不然会造成空指针异常
// 先在数组中随机取一个数作为pivot, 这样可以将时间复杂度概率性将为 NlogN
int N = L + (int)Math.random() * (R - L + 1); // 随机取数组范围L - R 之间一个整数
// 将这个数与数组最后一个数进行交换
swap(arr,N,R);
// 调用分配函数 得到 小于区域最右侧索引, 和 大于区域最左侧索引
int[] p = partition(arr,L,R);
// 第一轮分配完成后 递归调用左侧和右侧子序列,此时less 和 more 指向相等区域的左右两个端点
QuickSort(arr, L, p[0] - 1);
QuickSort(arr, p[1] + 1, R);
}
}
public static int[] partition(int[] arr, int L, int R){ // 这里要返回 less 和 more
int less = L - 1;
int more = R;
// 进行分类
//for(int i = L; i < more; ){ // 这里遍历到大于区域即可停止遍历
while(L < more){ // 额外空间复杂度O(1),这里不能重新定义变量i进行遍历
// 遍历时有三种情况
// 1:小于基准值 // 基准值pivot被换到了数组最后一个位置
if(arr[L] < arr[R]){
swap(arr, L++, ++less); // 与小于区域后一个数交换,小于区域右扩
}else if (arr[L] == arr[R]){
L++; // 直接后移
}else{ // 大于pivot
swap(arr, L, --more); // i保持不动 ,因为换过来的数没有被查看过
}
}
// 循环完毕后,more指向大于区域最左侧位置,此时将arr[more] 和 arr[R] 交换
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;
}
public static void main(String[] args) {
int[] arr = {3,4,5,6,1,2,3,4,5,4,3};
System.out.println(Arrays.toString(arr));
quickSort(arr);
System.out.println(Arrays.toString(arr));
}
}
依据概率问题,最差情况O(N^2), 平均时间复杂度为O(NLogN)