递归

剖析递归行为和递归行为时间复杂度的估算
用递归方法找一个数组中的最大值,系统上到底是怎么做的?

  1. import java.util.Arrays;
  2. public class GetMax {
  3. public static int getMax(int[] arr) {
  4. return getLocalMax(arr, 0, arr.length - 1);
  5. }
  6. public static int getLocalMax(int[] arr, int L, int R) {
  7. System.out.println("R为 " + R + " L为 " + L);
  8. if (L == R) return arr[L];
  9. // 中点
  10. int mid = L + ((R - L) >> 1);
  11. int lMax = getLocalMax(arr, L, mid);
  12. int rMax = getLocalMax(arr, mid + 1, R);
  13. return Math.max(lMax, rMax);
  14. }
  15. public static void main(String[] args) {
  16. // 生成一个随机数组
  17. int[] arr = ArrayGenerator.getArr(11, 30);
  18. System.out.println(Arrays.toString(arr));
  19. System.out.println("最大值为:" + GetMax.getMax(arr));
  20. }
  21. }

master 公式的使用

T(N)=a*T(N/b)+O(N^d)

归并排序、快速排序 - 图1

  • 归并排序、快速排序 - 图2:母问题一共有 N 个数据,即规模是 归并排序、快速排序 - 图3
  • 归并排序、快速排序 - 图4:是子问题的规模,即每个子问题有 归并排序、快速排序 - 图5归并排序、快速排序 - 图6就是子问题被调用了 a 次,这里就要求必须是等规模的才能用 master 公式来求解
  • 归并排序、快速排序 - 图7:除去子问题之外的其他过程的时间复杂度

上面的例子 用递归方法找一个数组中的最大值:在 getLocalMax 中,每个子问题是解决母问题一般的数据,规模为 归并排序、快速排序 - 图8,一共调用了 2 次,额外的时间复杂度是计算中点 int mid = L + ((R - L) >> 1); 也就是 归并排序、快速排序 - 图9a = 2,b = 2,d = 0,最后的结果为 归并排序、快速排序 - 图10

规律

  • 归并排序、快速排序 - 图11 复杂度为 归并排序、快速排序 - 图12
  • 归并排序、快速排序 - 图13 复杂度为 归并排序、快速排序 - 图14
  • 归并排序、快速排序 - 图15 复杂度为 归并排序、快速排序 - 图16

补充阅读: www.gocalf.com/blog/algorithm-complexity-and-master-theorem.html

归并排序

  1. 整体就是一个简单递归,左边排好序、右边排好序、让其整体有序
  2. 让其整体有序的过程里用了外排序方法
  3. 利用 master 公式来求解时间复杂度
  4. 归并排序的实质
  1. import java.util.Arrays;
  2. public class MergeSort {
  3. private static void sort(int[] arr) {
  4. process(arr, 0, arr.length - 1);
  5. }
  6. private static void process(int[] arr, int L, int R) {
  7. if (L == R) return;
  8. int mid = L + ((R - L) >> 1);
  9. process(arr, L, mid);
  10. process(arr, mid + 1, R);
  11. merge(arr, L, mid, R);
  12. }
  13. // 合并
  14. private static void merge(int[] arr, int L, int M, int R) {
  15. int[] temp = new int[R - L + 1];
  16. int i = 0;
  17. int lP = L;
  18. int rP = M + 1;
  19. // 哪个小就先保存哪个,直到一个数组越界
  20. while (lP <= M && rP <= R) {
  21. temp[i++] = arr[lP] < arr[rP] ? arr[lP++] : arr[rP++];
  22. }
  23. // 将其他的数保存到数组中
  24. while (lP <= M) {
  25. temp[i++] = arr[lP++];
  26. }
  27. while (rP <= R) {
  28. temp[i++] = arr[rP++];
  29. }
  30. // 将排好序的数组保存到 arr 中
  31. for (int j = 0; j < temp.length; j++) {
  32. arr[L + j] = temp[j];
  33. }
  34. }
  35. public static void main(String[] args) {
  36. int[] arr = ArrayGenerator.getArr(5, 30);
  37. System.out.println(Arrays.toString(arr));
  38. MergeSort.sort(arr);
  39. System.out.println(Arrays.toString(arr));
  40. }
  41. }

额外的复杂度是merge操作,merge是数组数据拷贝,复杂度为归并排序、快速排序 - 图17,即归并排序整体规模为 归并排序、快速排序 - 图18a=2,b=2,d=1归并排序、快速排序 - 图19 即时间复杂度 归并排序、快速排序 - 图20,额外空间复杂度 归并排序、快速排序 - 图21

归并排序的扩展

  • 小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子:
[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
思路:这里的左侧的最小和可看做是:右侧有 n 个数比当前数大:
1 右边比 1 大的数,3、4、2、5 共 4 个
3 右边比 3 大的数,4、5 共 2 个
4 右边比 4 大的数,5 一个
2 右边比 2 大的数,5 一个
5 右边比 5 大的数,没有
所以小和为 14 + 32 + 41 + 21 =16,可以使用归并排序来判断右侧有多少个数比当前数大

  1. import java.util.Arrays;
  2. public class SmallSum {
  3. static int sum = 0;
  4. public static int smallSum(int[] arr) {
  5. sum = 0;
  6. process(arr, 0, arr.length - 1);
  7. return sum;
  8. }
  9. private static void process(int[] arr, int L, int R) {
  10. if (L == R) return;
  11. int mid = L + ((R - L) >> 1);
  12. process(arr, L, mid);
  13. process(arr, mid + 1, R);
  14. merge(arr, L, mid, R);
  15. }
  16. // 合并
  17. private static void merge(int[] arr, int L, int M, int R) {
  18. int[] temp = new int[R - L + 1];
  19. int i = 0;
  20. int lP = L;
  21. int rP = M + 1;
  22. while (lP <= M && rP <= R) {
  23. if (arr[lP] < arr[rP]) {
  24. // 说明右侧有 R - rP + 1 个数比 temp[i] 大
  25. temp[i] = arr[lP];
  26. System.out.println(temp[i] + "*" + (R - rP + 1));
  27. sum += temp[i] * (R - rP + 1);
  28. lP++;
  29. } else {
  30. temp[i] = arr[rP];
  31. rP++;
  32. }
  33. i++;
  34. }
  35. // 将其他的数保存到数组中
  36. while (lP <= M) {
  37. temp[i++] = arr[lP++];
  38. }
  39. while (rP <= R) {
  40. temp[i++] = arr[rP++];
  41. }
  42. // 将排好序的数组保存到 arr 中
  43. for (int j = 0; j < temp.length; j++) {
  44. arr[L + j] = temp[j];
  45. }
  46. }
  47. public static void main(String[] args) {
  48. int[] arr = new int[]{1, 3, 4, 2, 5};
  49. System.out.println(Arrays.toString(arr));
  50. System.out.println(smallSum(arr));
  51. int[] arr1 = ArrayGenerator.getArr(5, 20);
  52. System.out.println(Arrays.toString(arr1));
  53. System.out.println(smallSum(arr1));
  54. }
  55. }

另一种写法:思路是一致的

  1. import java.util.Arrays;
  2. public class SmallSum1 {
  3. public static int smallSum(int[] arr) {
  4. return process(arr, 0, arr.length - 1);
  5. }
  6. private static int process(int[] arr, int L, int R) {
  7. if (L == R) return 0;
  8. int mid = L + ((R - L) >> 1);
  9. return process(arr, L, mid) + process(arr, mid + 1, R) + merge(arr, L, mid, R);
  10. }
  11. // 合并
  12. private static int merge(int[] arr, int L, int M, int R) {
  13. int sum = 0;
  14. int[] temp = new int[R - L + 1];
  15. int i = 0;
  16. int lP = L;
  17. int rP = M + 1;
  18. while (lP <= M && rP <= R) {
  19. if (arr[lP] < arr[rP]) {
  20. // 说明右侧有 R - rP + 1 个数比 temp[i] 大
  21. temp[i] = arr[lP];
  22. System.out.println(temp[i] + "*" + (R - rP + 1));
  23. sum += temp[i] * (R - rP + 1);
  24. lP++;
  25. } else {
  26. temp[i] = arr[rP];
  27. rP++;
  28. }
  29. i++;
  30. }
  31. // 将其他的数保存到数组中
  32. while (lP <= M) {
  33. temp[i++] = arr[lP++];
  34. }
  35. while (rP <= R) {
  36. temp[i++] = arr[rP++];
  37. }
  38. // 将排好序的数组保存到 arr 中
  39. for (int j = 0; j < temp.length; j++) {
  40. arr[L + j] = temp[j];
  41. }
  42. return sum;
  43. }
  44. public static void main(String[] args) {
  45. int[] arr = new int[]{1, 3, 4, 2, 5};
  46. System.out.println(Arrays.toString(arr));
  47. System.out.println(smallSum(arr));
  48. int[] arr1 = ArrayGenerator.getArr(5, 20);
  49. System.out.println(Arrays.toString(arr1));
  50. System.out.println(smallSum(arr1));
  51. }
  52. }
  • 逆序对问题

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
例如:[3,2,4,5,0],就存在逆序对 [3,2] [3,0], [2,0] [4,0], [5,0]
思路:和上面的求小和思路一致

import java.util.Arrays;

public class ReverseOrderArr {

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

    private 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);
    }

    private static void merge(int[] arr, int L, int M, int R) {
        int lP = L;
        int rP = M + 1;

        while (lP <= M && rP <= R) {
            if (arr[lP] > arr[rP]) {
                System.out.println("[" + arr[lP] + "," + arr[rP] + "]");
                lP++;
            } else {
                rP++;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[]{3, 2, 4, 5, 0};
        System.out.println(Arrays.toString(arr));
        ReverseOrderArr.reverseOrder(arr);

        int[] arr1 = ArrayGenerator.getArr(5, 20);
        System.out.println(Arrays.toString(arr1));
        ReverseOrderArr.reverseOrder(arr1);
    }
}

荷兰国旗问题

  • 问题一

给定一个数组 arr,和一个数 num,请把小于等于 num 的数放在数组的左边,大于 num 的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

import java.util.Arrays;

public class SplitArr {

    private static void splitArr(int[] arr, int num) {
        // >= num 的第一个数的下标
        int index = -1;
        for (int i = 0; i < arr.length; i++) {
            int cur = arr[i];
            if (cur > num) {
                if (index == -1) index = i;
            } else if (cur == num) {
                if (index == -1) {
                    index = i;
                } else {
                    arr[index] = arr[index] ^ arr[i];
                    arr[i] = arr[index] ^ arr[i];
                    arr[index] = arr[index] ^ arr[i];
                }
            } else {
                if (index != -1 && i > index) {
                    System.arraycopy(arr, index, arr, index + 1, i - index);
                    arr[index++] = cur;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1, 3, 5, 7, 4, 2, 6};
        System.out.println(Arrays.toString(arr));
        splitArr(arr, 4);
        System.out.println(Arrays.toString(arr));
    }
}
  • 问题二(荷兰国旗问题)

给定一个数组 arr,和一个数 num,请把小于 num 的数放在数组的左边,等于 num 的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度 O(1),时间复杂度 O(N)
思路:
确认数组的大于或者小于的边界。遍历数组

  • 当前数小于 num,和小于的边界 + 1 交换,小于区右扩,i++
  • 相等,直接跳过,i++
  • 当前数大于 num,和大于的边界 - 1 交换,大于区左扩,i 不变

这个思路也可以解决问题一

import java.util.Arrays;

public class DutchFlag {

    private static void splitArr(int[] arr, int num) {
        // 小于的边界
        int lIndex = -1;
        // 大于的边界
        int rIndex = arr.length;

        int i = 0;
        while (i < rIndex) {
            int cur = arr[i];
            if (cur < num) {
                // 小于,和小于的边界+1交换,小于区右扩
                lIndex++;
                if (i != lIndex) {
                    arr[i] = arr[i] ^ arr[lIndex];
                    arr[lIndex] = arr[i] ^ arr[lIndex];
                    arr[i] = arr[i] ^ arr[lIndex];
                }
                i++;
            } else if (cur == num) {
                // 相等,直接跳过
                i++;
            } else {
                // 大于,和大于的边界交换-1,大于区左扩,i不变
                rIndex--;
                if (i != rIndex) {
                    arr[i] = arr[i] ^ arr[rIndex];
                    arr[rIndex] = arr[i] ^ arr[rIndex];
                    arr[i] = arr[i] ^ arr[rIndex];
                }
            }
        }
    }

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

快速排序

基本思路是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。

1.0 版本
在整个数组中,拿最后一个数来划分 2 部分,即就将最后一个数来作为 num ,比 num 小的放到 num 前,比 num 小的放到后面

2.0 版本
在 1.0 的基础上,将数组划分为 3 部分,分别为大于 num,等于 num,小于 num。这样后续的划分中就可以不用处理等于 num 的部分

3.0 版本
不论是 1.0 还是 2.0 版本,时间复杂度都是 归并排序、快速排序 - 图22,都有最差的情况:每次划分取的最后一个数都是最大值,如:[1,2,3,4,5,6],每次划分都只是能确定一个数的位置
最差的情况原因:划分数组的值很“偏”,不能很好的划分数组。相反,如果每次划分值都能在中间位置把数组划分成数据量基本相等几个部分,算法的效率就会提高很多

在 2.0 的基础上,每次做数组划分时,在区间范围内随机取一个数和区间最后一个数交换,然后保持原来的逻辑使用最后一个数作为 num 来划分数组。这里使用随机选数来,也就是说所有的情况都会等概率出现,复杂度就会优化为归并排序、快速排序 - 图23

import java.util.Arrays;
import java.util.Random;

public class QuickSort {

    public static void sort(int[] arr) {
        partition(arr, 0, arr.length);
    }

    public static void partition(int[] arr, int L, int R) {
        if (L >= R) return;
//        System.out.println(arr[L] + " " + arr[R-1]);
        // 小于的边界
        int lIndex = L - 1;
        // 大于的边界
        int rIndex = R;

        // 分区参照下标
        int num = arr[L + new Random().nextInt(R - L)];

        int i = L;
        while (i < rIndex) {
            int cur = arr[i];
            if (cur < num) {
                // 小于,和小于的边界+1交换,小于区右扩
                lIndex++;
                if (i != lIndex) {
                    arr[i] = arr[i] ^ arr[lIndex];
                    arr[lIndex] = arr[i] ^ arr[lIndex];
                    arr[i] = arr[i] ^ arr[lIndex];
                }
                i++;
            } else if (cur == num) {
                // 相等,直接跳过
                i++;
            } else {
                // 大于,和大于的边界交换-1,大于区左扩,i不变
                rIndex--;
                if (i != rIndex) {
                    arr[i] = arr[i] ^ arr[rIndex];
                    arr[rIndex] = arr[i] ^ arr[rIndex];
                    arr[i] = arr[i] ^ arr[rIndex];
                }
            }
        }

//        System.out.println(num);
//        System.out.println(Arrays.toString(arr));
        // 这里要注意修改边界
        partition(arr, L, lIndex + 1);
        partition(arr, rIndex, R);
    }

    public static void main(String[] args) {
        int[] arr = ArrayGenerator.getArr(15, 100);
        System.out.println(Arrays.toString(arr));
        sort(arr);
        System.out.println(Arrays.toString(arr));

        // 对数,确保正确
        for (int i = 0; i < 100; i++) {
            arr = ArrayGenerator.getArr(15, 100);
            int[] arr1 = ArrayGenerator.getSortArr(arr);
            sort(arr);
            if (!ArrayGenerator.isSame(arr, arr1)) {
                System.out.println("出错了:" + Arrays.toString(arr));
            }
        }
    }
}

快速排序的空间复杂度的在 3.0 版本情况为归并排序、快速排序 - 图24