1. 荷兰国旗问题—三分类问题

题目描述:


给定一个数组和一个给定的值X,数组中的值根据X划分为两种或者三种情况:
(1)小于X的放在左边,等于X的放在中间,大于X的放在右边
(2)或者是小于等于X的放在左边,大于X的放在右边。

使用时间复杂度O(N),空间复杂度为O(1)完成数组的调整, 划分的区域内部可以无序


拿破仑席卷欧洲大陆之后,代表自由,平等,博爱的竖色三色旗也风靡一时。荷兰国旗就是一面三色旗(只不过是横向的),自上而下为红白蓝三色。

该问题本身是关于三色球排序和分类的,由荷兰科学家Dijkstra提出。由于问题中的三色小球有序排列后正好分为三类,Dijkstra就想象成他母国的国旗,于是问题也就被命名为荷兰旗问题(Dutch National Flag Problem)

荷兰国旗问题:现在有若干个红、白、蓝三种颜色的球随机排列成一条直线。现在我们的任务是把这些球按照红、白、蓝排序。这个问题之所以叫荷兰国旗,是因为我们可以将红白蓝三色小球想象成条状物,有序排列后正好组成荷兰国旗。
捕获.PNG

下面是问题的正规描述: 现有n个红白蓝三种不同颜色的小球,乱序排列在一起,请通过两两交换任意两个球,使得从左至右,依次是一些红球、一些白球、一些蓝球。


给定数组arr[L,R]和目标数X,两种划分方式下的算法流程如下:

  1. 当划分为两种情况时:

    初始小于等于区域边界为 L-1 大于区域边界为 R+1
    1) 当前数 <= 目标数,把当前数和小于等于区的下一个数字做交换,然后小于等于区域向右扩,
    当前数跳下一个。
    2) 当前数 > 目标数,当前数直接跳下一个。

  2. 当划分为三种情况时:

    初始小于区域边界为 L-1 大于区域边界为 R+1
    1) 当前数 < 目标数 当前数和小于区的下一个数字做交换,小于区域右扩一位,当前数字跳到下一个
    2) 当前数 = 目标数 当前数当前数直接跳到下一个
    3) 当前数 > 目标数 当前数和大于区的前一个交换,大于区域向左扩一位,当前数字停在原地不变
    当前数和右边界撞上的时候停止。

  3. 写法升级

给定一个数组arr[L,R],直接arr[R]作为这个目标值,也就是相当于在前两者的基础上,实现arr[L,R-1]和目标值arr[R]进行比较排序,排序结束后将arr[R]和大于区域的第一个数做交换,即可实现,arr[L,R]上以arr[R]为划分值来归类排序。
举例:具体流程如下:
4. 快排 荷兰国旗问题 - 图2

  1. 荷兰国旗问题代码实现:
    1. // arr[L...R] 荷兰国旗问题的划分,以arr[R]做划分值
    2. // 实现数组划分为三个区域<arr[R] ==arr[R] >arr[R]
    3. // 返回值为等于区域的坐标
    4. public static int[] netherlandsFlag(int[] arr, int L, int R) {
    5. if (L > R) { // L...R L>R
    6. return new int[] { -1, -1 };
    7. }
    8. if (L == R) {
    9. return new int[] { L, R };
    10. }
    11. int less = L - 1; // < 小于区 大于区左边界为L-1
    12. int more = R; // > 大于区 以arr[R]作为划分值X,划分区域在L~R-1上,大于区左边界为R
    13. int index = L;
    14. while (index < more) { // 当前位置,不能和 大于区 的左边界撞上
    15. if (arr[index] == arr[R]) { // 当前数和划分数相等,当前数字直接跳到下一个
    16. index++;
    17. } else if (arr[index] < arr[R]) { // 当前数小于目标数
    18. // swap(arr, less + 1, index); // 小于区域的下一个数和当前数交换
    19. // less++; // 小于区域向右扩
    20. // index++; // 当前数跳到下一个
    21. swap(arr, index++, ++less);
    22. } else { // > // 当前数大于目标数
    23. // 当前数和大于区的前一个交换,大于区域向左扩,当前数字停在原地
    24. // swap(arr, more - 1, index);
    25. // more--;
    26. swap(arr, index, --more); //
    27. }
    28. }
    29. swap(arr, more, R); // <[R] =[R] >[R] // 最后将划分值和大于区域的第一个数做交换,让arr[R]归为
    30. return new int[] { less + 1, more };
    31. }

    2. 随机快速排序

快排 1.0 O(N2) 1 2 3 4 5 6 7 这种情况时间复杂度为O(N2)

快排 2.0 O(N2)

快排 3.0

3. 快排递归和非递归代码实现

  1. public class Code03_QuickSortRecursiveAndUnrecursive {
  2. // 荷兰国旗问题,arr[L...R] 荷兰国旗问题的划分,以arr[R]做划分值,返回等于区域的坐标
  3. public static int[] netherlandsFlag(int[] arr, int L, int R) {
  4. if (L > R) {
  5. return new int[] { -1, -1 };
  6. }
  7. if (L == R) {
  8. return new int[] { L, R };
  9. }
  10. int less = L - 1;
  11. int more = R;
  12. int index = L;
  13. while (index < more) {
  14. if (arr[index] == arr[R]) {
  15. index++;
  16. } else if (arr[index] < arr[R]) {
  17. swap(arr, index++, ++less);
  18. } else {
  19. swap(arr, index, --more);
  20. }
  21. }
  22. swap(arr, more, R);
  23. return new int[] { less + 1, more };
  24. }
  25. public static void swap(int[] arr, int i, int j) {
  26. int tmp = arr[i];
  27. arr[i] = arr[j];
  28. arr[j] = tmp;
  29. }
  30. // 快排递归版本
  31. public static void quickSort1(int[] arr) {
  32. if (arr == null || arr.length < 2) {
  33. return;
  34. }
  35. process(arr, 0, arr.length - 1);
  36. }
  37. public static void process(int[] arr, int L, int R) {
  38. if (L >= R) {
  39. return;
  40. }
  41. // 每次随机生成一个基准数pivot
  42. swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
  43. int[] equalArea = netherlandsFlag(arr, L, R);
  44. process(arr, L, equalArea[0] - 1);
  45. process(arr, equalArea[1] + 1, R);
  46. }
  47. // 快排非递归版本需要的辅助类
  48. // 要处理的是什么范围上的排序,表示处理 l r 上排序
  49. public static class Op {
  50. public int l;
  51. public int r;
  52. public Op(int left, int right) {
  53. l = left;
  54. r = right;
  55. }
  56. }
  57. // 快排3.0 非递归版本
  58. public static void quickSort2(int[] arr) {
  59. if (arr == null || arr.length < 2) {
  60. return;
  61. }
  62. int N = arr.length;
  63. swap(arr, (int) (Math.random() * N), N - 1); // 随机选择一个数字与最右侧的数作交换。
  64. int[] equalArea = netherlandsFlag(arr, 0, N - 1); // 区域划分 小于 等于 大于
  65. int el = equalArea[0];
  66. int er = equalArea[1];
  67. Stack<Op> stack = new Stack<>(); // 操作栈
  68. stack.push(new Op(0, el - 1)); // 左侧
  69. stack.push(new Op(er + 1, N - 1));// 右侧
  70. while (!stack.isEmpty()) {
  71. Op op = stack.pop(); // op.l ... op.r
  72. if (op.l < op.r) {
  73. swap(arr, op.l + (int) (Math.random() * (op.r - op.l + 1)), op.r);
  74. equalArea = netherlandsFlag(arr, op.l, op.r);
  75. el = equalArea[0];
  76. er = equalArea[1];
  77. stack.push(new Op(op.l, el - 1));
  78. stack.push(new Op(er + 1, op.r));
  79. }
  80. }
  81. }
  82. // 生成随机数组(用于测试)
  83. public static int[] generateRandomArray(int maxSize, int maxValue) {
  84. int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
  85. for (int i = 0; i < arr.length; i++) {
  86. arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
  87. }
  88. return arr;
  89. }
  90. // 拷贝数组(用于测试)
  91. public static int[] copyArray(int[] arr) {
  92. if (arr == null) {
  93. return null;
  94. }
  95. int[] res = new int[arr.length];
  96. for (int i = 0; i < arr.length; i++) {
  97. res[i] = arr[i];
  98. }
  99. return res;
  100. }
  101. // 对比两个数组(用于测试)
  102. public static boolean isEqual(int[] arr1, int[] arr2) {
  103. if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
  104. return false;
  105. }
  106. if (arr1 == null && arr2 == null) {
  107. return true;
  108. }
  109. if (arr1.length != arr2.length) {
  110. return false;
  111. }
  112. for (int i = 0; i < arr1.length; i++) {
  113. if (arr1[i] != arr2[i]) {
  114. return false;
  115. }
  116. }
  117. return true;
  118. }
  119. // 打印数组(用于测试)
  120. public static void printArray(int[] arr) {
  121. if (arr == null) {
  122. return;
  123. }
  124. for (int i = 0; i < arr.length; i++) {
  125. System.out.print(arr[i] + " ");
  126. }
  127. System.out.println();
  128. }
  129. // 跑大样本随机测试(对数器)
  130. public static void main(String[] args) {
  131. int testTime = 500000;
  132. int maxSize = 100;
  133. int maxValue = 100;
  134. boolean succeed = true;
  135. System.out.println("test begin");
  136. for (int i = 0; i < testTime; i++) {
  137. int[] arr1 = generateRandomArray(maxSize, maxValue);
  138. int[] arr2 = copyArray(arr1);
  139. quickSort1(arr1);
  140. quickSort2(arr2);
  141. if (!isEqual(arr1, arr2)) {
  142. succeed = false;
  143. break;
  144. }
  145. }
  146. System.out.println("test end");
  147. System.out.println("测试" + testTime + "组是否全部通过:" + (succeed ? "是" : "否"));
  148. }
  149. }
package class05;

public class Code02_PartitionAndQuickSort {

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    // arr[L..R]上,以arr[R]位置的数做划分值X,
    // <= X 放左, > X放右,而且要求在<=X的区域里最后一个数是X
    // <= X X
    public static int partition(int[] arr, int L, int R) {
        if (L > R) {
            return -1;
        }
        if (L == R) {
            return L;
        }
        int lessEqual = L - 1;
        int index = L;
        while (index < R) {
            if (arr[index] <= arr[R]) {
                swap(arr, index, ++lessEqual);
            }
            index++;
        }
        swap(arr, ++lessEqual, R);
        return lessEqual;
    }

    // arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
    // <arr[R] ==arr[R] > arr[R]
    public static int[] netherlandsFlag(int[] arr, int L, int R) {
        if (L > R) { // L...R L>R
            return new int[] { -1, -1 };
        }
        if (L == R) {
            return new int[] { L, R };
        }
        int less = L - 1; // < 区 右边界
        int more = R; // > 区 左边界
        int index = L;
        while (index < more) { // 当前位置,不能和 >区的左边界撞上
            if (arr[index] == arr[R]) {
                index++;
            } else if (arr[index] < arr[R]) {
//                swap(arr, less + 1, index);
//                less++;
//                index++;                        
                swap(arr, index++, ++less);
            } else { // >
                swap(arr, index, --more);
            }
        }
        swap(arr, more, R); // <[R]   =[R]   >[R]
        return new int[] { less + 1, more };
    }

    public static void quickSort1(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process1(arr, 0, arr.length - 1);
    }

    public static void process1(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        // L..R partition arr[R] [ <=arr[R] arr[R] >arr[R] ]
        int M = partition(arr, L, R);
        process1(arr, L, M - 1);
        process1(arr, M + 1, R);
    }

    public static void quickSort2(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process2(arr, 0, arr.length - 1);
    }

    // arr[L...R] 排有序,快排2.0方式
    public static void process2(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        // [ equalArea[0]  ,  equalArea[0]]
        int[] equalArea = netherlandsFlag(arr, L, R);
        process2(arr, L, equalArea[0] - 1);
        process2(arr, equalArea[1] + 1, R);
    }

    public static void quickSort3(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process3(arr, 0, arr.length - 1);
    }

    public static void process3(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
        int[] equalArea = netherlandsFlag(arr, L, R);
        process3(arr, L, equalArea[0] - 1);
        process3(arr, equalArea[1] + 1, R);
    }


}
    // 切分元素,分成<=指标值(arr[]) 的和 大于指标值(arr[])的两组
    public static int partition(int[] arr, int L, int R) {
        if (L > R) {
            return -1;
        }
        if (L == R) {
            return L;
        }
        int lessEqual = L - 1;
        int index = L;
        while (index < R) {
            if (arr[index] <= arr[R]) {   // 小于等于时,当前数和小于等于区后一位交换, lessEqual++, index++
                swap(arr, index, ++lessEqual);
            }
            index++;
        }
        swap(arr, ++lessEqual, R); // 最后一个数和
        return lessEqual;
    }

    public static void quickSort1(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process1(arr, 0, arr.length - 1);
    }
    // 快排1.0版本
    public static void process1(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        // L..R partition arr[R] [ <=arr[R] arr[R] >arr[R] ]
        int M = partition(arr, L, R);
        process1(arr, L, M - 1);
        process1(arr, M + 1, R);
    }

    public static void quickSort2(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process2(arr, 0, arr.length - 1);
    }

    // arr[L...R] 排有序,快排2.0方式
    public static void process2(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        // [ equalArea[0]  ,  equalArea[1]]  返回等于区域的范围
        int[] equalArea = netherlandsFlag(arr, L, R);
        process2(arr, L, equalArea[0] - 1);
        process2(arr, equalArea[1] + 1, R);
    }

    public static void quickSort3(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process3(arr, 0, arr.length - 1);
    }
    // 随机快排,这个才是学术界所说的快速排序。
    public static void process3(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        // 在之前的版本中,arr中位于R位置的数作为划分值,标志物的选择都是将每次划分数组的最右侧作为划分值来做比较,小于该值的
        // 放在左边,大于该值的放在右边。而随机快速排序也就是三版本,采用的是随机选择一个数字
        // h换到最右边去,选择arr中L-R上随机一个数字与R位置的数做交换,之后在进行2版本的操作。
        // 
        swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
        int[] equalArea = netherlandsFlag(arr, L, R);
        process3(arr, L, equalArea[0] - 1);
        process3(arr, equalArea[1] + 1, R);
    }

Master公式计算递归算法的时间复杂度:
快排的时间复杂度: O(N*logN)
随机快排,比较值或者划分值的选择,在最好情况下是每次选择之后划分左右的数字个数大致是相等的。
因此当划分值选择的比较好的情况下,快排的时间复杂度会向 O(N*logN)收敛,当划分值选择的不好的情况下,时间复杂度会向O(N2) 收敛。
T(N) = O(N2) + 2T(N/2)_ => O(NlogN)
概率事件数学证明:时间复杂度为
O(N*logN_)

快排的额外空间复杂度O(logN)
最差的情况 O(N),最好情况 O(logN)

递归占用的额外空间,中间划分点信息需要记录下来,这个划分值需要记录下来,如果不记录下来,当做完左侧的递归之后,回去找不到划分值,此时就无法进行右侧的递归了,找不到右侧递归范围了;
如果不使用递归的形式,需要自己维护一个栈来记录这个每次划分值的信息
最差情况: 1 2 3 4 5 6 7 ,每一个点都需要记录,O(N)
最好情况: 每次都是中点为划分值,通过重复释放利用,只需要logN的额外空间,通过变量的不断释放、复用、释放、复用达到最好情况.
概率事件最终额外空间复杂度收敛到 O(logN)