力扣算法前置知识——复杂度+简单排序算法(2)

114 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

简单递归

传送门

/**
 * 返回数组最大值 - 递归
 */
public class GetMax {

    public static void main(String[] args) {
        int[] arr = new int[]{3, 2, 5, 6, 7, 4};
        System.out.println(getMax(arr));
    }

    public static int getMax(int[] arr) {
        return process(arr, 0, arr.length - 1);
    }

    private static int process(int[] arr, int L, int R) {
        if (L == R) {
            return arr[L];
        }
        int mid = L + ((R - L) >> 1); //求中点,考虑R+L会溢出,所以采用R-L并且除2(这里采用右移一位比除2要快)
        int leftMax = process(arr, L, mid);
        int rightMax = process(arr, mid + 1, R);
        return Math.max(leftMax, rightMax);

    }
}

递归调用过程:

  • ​ P(0,5)
  • ​ P(0,2) P(3,5)
  • ​ P(0,1) P(2,2) P(3,4) P(5,5)
  • P(0,0) P(1,1) P(3,3) P(4,4)

master公式

master公式:满足子问题等规模的递归 T(N) = a * T(N/b) + O(N^d)

该公式需要满足子问题等规模,上文递归代码就是均分为2,如果变成2/3,2/3递归也行,但出现1/3,2/3就不行,需要满足等规模

a是调用的次数,b是等规模计算的数目,d是除了子问题外的时间复杂度

则,上文简单递归结果就是T(N)= 2 * T(N/2) + O(1)

满足master公式递归,整体时间复杂度是

  • 若log以b为底的a<d,时间复杂度是O(N^d)
  • 若log以b为底的a>d,时间复杂度是O(N^(log以b为底的a))
  • 若log以b为底的a=d,时间复杂度是O(N^d * logN)

上文简单递归计算结果后是1>0,时间复杂度为O(N),说明和遍历一遍数组等效

归并排序

归并排序过程:

  • 将数组均分,一分为二,左边右边分别排序,排序完,左边有序,右边有序
  • 申请一个新的等长数组空间,定义两个变量分别指向左右两边第一个数
  • 如果左边第一个数小于右边第一个数,就填入新申请的空间,反之则填入右边数;(左边先拷贝)
  • 填入左边数的变量指向加1,再次和右边第一个数比较,反之同理

......

循环往复,最终有序

/**
 * 归并排序
 */
public class code02 {

    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[] temp = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M + 1;
        //正常排序
        while (p1 <= M && p2 <= R) {
            temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        //剩下未插入的数
        while (p1 <= M) {
            temp[i++] = arr[p1++];
        }
        while (p2 <= R) {
            temp[i++] = arr[p2++];
        }

        for (i = 0; i < temp.length; i++) {
            arr[L + i] = temp[i];
        }
    }
}

满足master公式,最终计算时间复杂度为O(N*logN),但注意这里的额外空间复杂度是O(N)

补充:

  • 小和问题:数组找出每一个数左边比他小的数字之和再求总和,[1,3,4,2,5],1的左边没有为0,3的左边比他小的累加为1,4的的左边比他小的累加为4...... ,总和就是16
    • 暴力方式当然可以,但时间复杂度为O(N^2)
    • 如何将其时间复杂度降低?可使用归并排序
      • 可以讲问题用逆向思维,求左边小的求和,不久等同于右边比我大的我自己求和
      • 利用归并排序,在开始排序阶段,拿左边二分1,3,4说例,134再划分变成13,4;13再划分变成1,3;1作为左边和3比较,记录1个1,之后13有序和4比较,记录1个1,1个3;同理2比较5记录1个2
      • 归并阶段,因为此时左边和右边都有序,所以我们拿1和右边比较时,只需比较第一个就知道后面都是比1大的,也就是此时1---2个1,其余同理3---1个3,4---1个4,拷贝还是同上文归并一样,拷贝该小的数,指向移动一格(左边先拷贝和正常归并一致)
      • 注意这里要补充,如果右边和左边数相同,需要拷贝右边的数,移动右边的指向(右边先拷贝)
      • 最后总和还是16

注意一点刚开始不断划分的时候,也是左边一直和右边

所以总结下,左边要求小和、右边要求小和,合并的时候左边要求,此处亮点主要是merge(归并)的逻辑

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, l, mid, r);
}

public static int merge(int[] arr, int L, int m, int r) {
    int[] temp = new int[r - L + 1];
    int i = 0;
    int p1 = L;
    int p2 = m + 1;
    int res = 0;
    //归并的时候排序插入,计算小和
    while (p1 <= m && p2 <= r) {
        res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
        temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }

    ////剩下未插入的数
    while (p1 <= m) {
        temp[i++] = arr[p1++];
    }

    while (p2 <= r) {
        temp[i++] = arr[p2++];
    }

    for (i = 0; i < temp.length; i++) {
        arr[L + i] = temp[i];
    }

    return res;
}
  • 逆序对问题:数组中,左边数比右边大,则这两个数构成逆序对,请找出逆序对的数量
    • 实际上就是求右边有多少数比左边数小

快速排序

引入快排

数组[3,5,6,3,4,5,3,6,9,0],使得数组比5小的数都在左边,等于5的在中间,比5大的数都在右边,分三种情况

  • [i]下标 < num(5) ,[i] 和小于区域的下一个做交换,小于区域右扩i++;
  • [i] = num ,i++
  • [i] > num ,[i] 和 大于区域前一个做交换,大于区域左扩,i不动

排序过程:

第一次排序:3小于5,3和3自己交换,位置不变,i+1

第二次排序:5和5相关,位置不变,i+1

第三次排序:6大于5,6和0交换,此时数组变成[3,5,0,3,4,5,3,6,9,6],i不变

第四次排序:上文为什么i不变,因为0换位置后是新数据,0小于5,0和第二个位置的5交换位置,此时数组变成[3,0,5,3,4,5,3,6,9,6],i+1

第五次排序:3下雨5,和第三个位置的5交换位置吗,此时数组变成[3,0,3,5,4,5,3,6,9,6],i+1

......知道i和大于区域碰上停止,最后排序结果为[3,0,3,4,2, 5,5,9,6,6]

快排1.0

第一次排序:数组最后一个数作为num,前面的数进行排序分为两个部分,左边比num小,右边比num大,num不变,结束后,num和比num大的区域第一个数交换位置

第二次排序:左边区域最后一个数作为num,重复前面操作,右边数最后一个数作为num,重复操作

......

快排2.0

2.0对比1.0的改进点就是划分三个部分,左边还是小于,右边还是大于,但中间是等于,交换还是一样和右边第一个数交换,循环往复,但由于是一次性固定了一批相同的数,也就是等于的数,比1.0稍快

时间复杂度1.0和2.0都是O(N^2),最差情况就是[1,2,3,4,5,6,7,8],要去不停的迭代递归,以8为num,7为num.....,明显等差数列所以O(N^2)

快排3.0

随机事件,随机选择一个数做划分,最后求出的期望解是O(N*logN)

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); // p[0] - 1 小于区域的右边界
        quickSort(arr, p[1] + 1, R); // p[1] + 1 大于区域的左边界
    }
}

public static int[] partition(int[] arr, int L, int R) {
    int less = L - 1; // 小于区右边界
    int more = R; // 大于区左边界
    while (L < R) { // 表示当前数的位置 arr[R] -> 划分值
        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) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}