一、Partition过程
给定一个数组arr,和一个整数num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。额外空间复杂度O(1),时间复杂度O(N)
二、荷兰国旗问题
给定一个数组arr,和一个整数num。请把小于num的数放在数组的左边,等于num的数放在中间,大于num的数放在数组的右边。额外空间复杂度O(1),时间复杂度O(N)
三、快速排序1.0
在arr[L..R]范围上,进行快速排序的过程:
1)用arr[R]对该范围做partition,<=arr[R]的数在左部分并且保证arr[R]最后来到左部分的最后一个位置,记为M; > arr[R]的数在右部分(arr[M+1..R])
2)对arr[L..M-1]进行快速排序(递归)
3)对arr[M+1..R]进行快速排序(递归)
因为每一次partition都会搞定一个数的位置且不会再变动,所以排序能完成
四、快速排序2.0
在arr[L..R]范围上,进行快速排序的过程:
1)用arr[R]对该范围做partition,<arr[R]的数在左部分,== arr[R]的数中间,>arr[R]的数在右部分。假设== arr[R]的数所在范围是[a,b]
2)对arr[L..a-1]进行快速排序(递归)
3)对arr[b+1..R]进行快速排序(递归)
因为每一次partition都会搞定一批数的位置且不会再变动,所以排序能完成
五、快速排序3.0(随机快排+荷兰国旗技巧优化)
在arr[L..R]范围上,进行快速排序的过程:
1)在这个范围上,随机选一个数记为num,
1)用num对该范围做partition,<num的数在左部分,== num的数中间,>num的数在右部分。假设== num的数所在范围是[a,b]
2)对arr[L..a-1]进行快速排序(递归)
3)对arr[b+1..R]进行快速排序(递归)
因为每一次partition都会搞定一批数的位置且不会再变动,所以排序能完成
六、时间复杂度分析
1.快速排序1.0和2.0的时间复杂度分析:
数组已经有序的时候就是复杂度最高的时候
时间复杂度O(N^2)
2.随机快排的时间复杂度分析:
1)通过分析知道,划分值越靠近中间,性能越好;越靠近两边,性能越差
2)随机选一个数进行划分的目的就是让好情况和差情况都变成概率事件
3)把每一种情况都列出来,会有每种情况下的时间复杂度,但概率都是1/N
4)那么所有情况都考虑,时间复杂度就是这种概率模型下的长期期望!
时间复杂度O(N*logN),额外空间复杂度O(logN)都是这么来的。
七、代码演示partition、荷兰国旗技巧、递归快排
package class05;
import java.util.logging.Level;
import static class05.Code02_PartitionAndQuickSort.process2;
public class PartitionAndQuickSort {
public static void swap(int[] arr,int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//分区动作 ,arr[L..R]上,以arr[R]位置的数做划分值,按 >x x >x的顺序排序
//即将划分值放中间,左边的数比它小,右边的数比它大 额外空间复杂度O(1),时间复杂度O(N)
//将[l,r-1] 依次与划分值比较,设定l-1一个小于等于区间,小于的则放入这个区间,指针前移
//最后遍历完,再将划分值与小于等于区间的右侧索引交换。这样就把划分值排好位置了
public static int partition(int[] arr,int l ,int r){
if(l > r) return -1; //越界
if(l == r) return l; //只有一个数,直接返回自己
int lessEqual = l-1; //定义一个小于等于区间 起点在l-1
int index = l; //定义指针遍历
//从左向右遍历 比较与划分值r位置情况
while(index < r){
if(arr[index] <= arr[r]){
//假设位于0,小于划分值,lessEqual区间在-1,先+1,在于当前值交换,此时都是同位置
//来到1,2 都大于划分值,则不交换,接着下移,来到3此时符合 条件,那么lessEqual区间在0
//就往左扩大小于等于区间,先+1,在与当前3索引值交换,....
//整个过程像是小于等于区间从左往右逼近大于区间,推着大于区间往前走
swap(arr,index,++lessEqual);
}
index++;
}
//跳出循环时,还有一步,就是把划分值,插入到小于等于区间的右一个索引位置,这样才是达到左侧小,右侧大的排序
swap(arr,++lessEqual,r);
//最后返回当前划分值所在所排好的位置
return lessEqual;
}
/**荷兰国旗问题
* 给定一个数组arr,和一个整数num。请把小于num的数放在数组的左边,等于num的数放在中间,大于num的数放在数组的右边。
* 要求额外空间复杂度O(1),时间复杂度O(N)
* 返回的时arr[r]作为划分值,最终时排序排在哪个区间上
* // arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
* // <arr[R] ==arr[R] > arr[R]
*/
public static int[] netherlandsFlag(int[] arr, int l, int r) {
if(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]){
//如果当前元素小于划分值arr[r] 那么就当前值与小于区的下个位置交换,小于区外扩+1
swap(arr,index++,++less);
}else if(arr[index] == arr[r]){
index++; //当前值等于划分值,则不交换,进行指针下移 等小于的时候,就会与这个相等的索引进行交换 就是上面的判断
}else{
//当前元素大于划分值,那么就需要将当前值与 大于区的前一个位置交换,大于区向左左扩相当于把小的交换到当前值,注意该值交换过来
//还需要进行判断,index保持不动,因为该值还没判断刚交换过来的,
swap(arr,index,--more);
}
}
//跳出循环,则表示小于 大于区间边界相遇。 那么需要把划分值,放置到中间的这个位置,需要放置在大于区的左边界 就能保证划分值左边的小,右边大
swap(arr,more,r);
//返回划分值的区间,有可能1个 有可能多个,左边界就是小于区有边界+1 ,右边界就是在大于区的左边界more,这里因为前面与r交换,所以more位置也是划分值
return new int[]{less+1,more};
}
/**快速排序1.0
* 在arr[L..R]范围上,进行快速排序的过程:
* 1)用arr[R]对该范围做partition,<= arr[R]的数在左部分并且保证arr[R]最后来到左部分的最后一个位置,记为M; > arr[R]的数在右部分(arr[M+1..R])
* 2)对arr[L..M-1]进行快速排序(递归)
* 3)对arr[M+1..R]进行快速排序(递归)
* 因为每一次partition都会搞定一个数的位置且不会再变动,所以排序能完成
*
*/
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;//越界判断
//执行分区操作
int mid = partition(arr, l, r);
process1(arr,l,mid-1);
process1(arr,mid+1,r);
}
/**快速排序2.0
* 在arr[L..R]范围上,进行快速排序的过程:
* 1)用arr[R]对该范围做partition,< arr[R]的数在左部分,== arr[R]的数中间,>arr[R]的数在右部分。假设== arr[R]的数所在范围是[a,b]
* 2)对arr[L..a-1]进行快速排序(递归)
* 3)对arr[b+1..R]进行快速排序(递归)
* 因为每一次partition都会搞定一批数的位置且不会再变动,所以排序能完成
*
* * 快速排序1.0和2.0的时间复杂度分析
* * 数组已经有序的时候就是复杂度最高的时候
* * 时间复杂度O(N^2)
*/
public static void quickSort2(int[] arr){
if(arr == null || arr.length <2) return;
process2(arr,0,arr.length-1);
}
public static void process2(int[] arr,int l ,int r){
if(l >= r ) return ; //越界判断
int[] ans = netherlandsFlag(arr, l, r); //调用荷兰国旗方法返回划分值区间
//中间区间ans已经拍好,所以左边需要排序的时l,ans[0]-1,左边数组右边界即划分值
// 左区间的前一个索引, 右边同理
process2(arr,l,ans[0]-1);
process2(arr,ans[1]+1,r);
}
/**
* 快速排序3.0(随机快排+荷兰国旗技巧优化)
* 在arr[L..R]范围上,进行快速排序的过程:
* 1)在这个范围上,随机选一个数记为num,
* 1)用num对该范围做partition,< num的数在左部分,== num的数中间,>num的数在右部分。假设== num的数所在范围是[a,b]
* 2)对arr[L..a-1]进行快速排序(递归)
* 3)对arr[b+1..R]进行快速排序(递归)
* 因为每一次partition都会搞定一批数的位置且不会再变动,所以排序能完成
*
* 随机快排的时间复杂度分析
* 1)通过分析知道,划分值越靠近中间,性能越好;越靠近两边,性能越差
* 2)随机选一个数进行划分的目的就是让好情况和差情况都变成概率事件
* 3)把每一种情况都列出来,会有每种情况下的时间复杂度,但概率都是1/N
* 4)那么所有情况都考虑,时间复杂度就是这种概率模型下的长期期望!
* 时间复杂度O(N*logN),额外空间复杂度O(logN)都是这么来的。
*/
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 ; //越界判断
//优化点:从l,r上等概率随机取一个索引跟划分值r索引交换,这样可以得到一个O(N*logN)复杂度
//假设 l=1 r=5 随机等概率得到 [0,4] 加上l左边界 , 即等概率返回[1,5] 与r=5交换
//避开一些r划分值很大,一直做无效的比较,随机返回把这个变成是概率性事件
swap(arr, (int) (l + (Math.random()*(r - l + 1))),r);
int[] ans = netherlandsFlag(arr, l, r);
process3(arr,l,ans[0]-1);
process3(arr,ans[1]+1,r);
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
int[] arr3 = copyArray(arr1);
quickSort1(arr1);
quickSort2(arr2);
quickSort3(arr3);
if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Oops!");
}
}
八、非递归实现快排3.0(栈和队列两种结构实现)
核心点:递归实际上是系统帮我们记录了每一次递归的边界范围信息,是系统栈保存了这些信息,那么要用非递归的形式,那么就需要我们自己定义一个栈,或者队列也可以,保存每一次的边界范围信息,才能遍历进行下一次的数组快排
package class05;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class QuickSortRecursiveAndUnrecursive {
// 荷兰国旗问题
public static int[] netherlandsFlag(int[] arr, int l, int r) {
if (l > r) return new int[]{-1, -1};
if (l == r) return new int[]{l, r};
int index = l;
int less = l - 1;
int more = r;
while (index < more) {
if (arr[index] < arr[r]) {
swap(arr, index++, ++less);
} else if (arr[index] == arr[r]) {
index++;
} else {
swap(arr, index, --more);
}
}
swap(arr, more, r);
return new int[]{less + 1, more};
}
public static void swap(int[] arr, int l, int r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
// 快排递归版本
public static void quickSort1(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;
swap(arr,l + (int)(Math.random()*(r-l+1)),r);
int[] ints = netherlandsFlag(arr, l, r);
process(arr, l, ints[0] - 1);
process(arr, ints[1] + 1,r);
}
// 快排非递归版本需要的辅助类
// 要处理的是什么范围上的排序
public static class Op{
public int l;
public int r;
public Op(int l ,int r){
this.l = l;
this.r = r;
}
}
// 快排3.0 非递归版本 用栈来执行
public static void quickSort2(int[] arr) {
if(arr == null || arr.length < 2) return;
int N = arr.length; //定义数组的长度,后续调用
//(int)(Math.random()*N) 随机得到的是[0,n-1]等概率随机返回一个数
swap(arr,(int)(Math.random()*N),N-1);
//先进行第一次0,N-1的随机分区操作,然后将得到新的l r入栈里面
int[] ans = netherlandsFlag(arr, 0, N - 1);
//取出划分区间返回的左右边界
int ansL = ans[0];
int ansR = ans[1];
//分别将区间左侧与右侧的范围保存入栈
Stack<Op> stack = new Stack<>();
stack.push(new Op(0,ansL-1));
stack.push(new Op(ansR+1,N-1));
//进行出栈以及入栈当次的左右数组边界范围 直到栈空最后也就排序好了
while (!stack.isEmpty()){
//出栈一个范围边界
Op op = stack.pop();
//判断左边界要小于右边界,才执行逻辑
if(op.l < op.r) {
int opL = op.l;
int opR = op.r;
//同样是进行随机交换opR 划分值
swap(arr, opL + (int) (Math.random() * (opR - opL + 1)), opR);
ans = netherlandsFlag(arr, opL, opR);
stack.push(new Op(opL, ans[0] - 1));
stack.push(new Op(ans[1] + 1, opR));
}
}
}
// 快排3.0 非递归版本 用队列来执行
public static void quickSort3(int[] arr) {
if(arr == null || arr.length < 2) return;
int N = arr.length; //定义数组的长度,后续调用
//(int)(Math.random()*N) 随机得到的是[0,n-1]等概率随机返回一个数
swap(arr,(int)(Math.random()*N),N-1);
//先进行第一次0,N-1的随机分区操作,然后将得到新的l r入栈里面
int[] ans = netherlandsFlag(arr, 0, N - 1);
//取出划分区间返回的左右边界
int ansL = ans[0];
int ansR = ans[1];
//分别将区间左侧与右侧的范围保存入栈
Queue<Op> queue = new LinkedList<>();
queue.offer(new Op(0,ansL-1));
queue.offer(new Op(ansR+1,N-1));
//进行出栈以及入栈当次的左右数组边界范围 直到栈空最后也就排序好了
while (!queue.isEmpty()){
//出栈一个范围边界
Op op = queue.poll();
//判断左边界要小于右边界,才执行逻辑
if(op.l < op.r) {
int opL = op.l;
int opR = op.r;
//同样是进行随机交换opR 划分值
swap(arr, opL + (int) (Math.random() * (opR - opL + 1)), opR);
ans = netherlandsFlag(arr, opL, opR);
queue.offer(new Op(opL, ans[0] - 1));
queue.offer(new Op(ans[1] + 1, opR));
}
}
}
// 生成随机数组(用于测试)
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// 拷贝数组(用于测试)
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// 对比两个数组(用于测试)
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// 打印数组(用于测试)
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 跑大样本随机测试(对数器)
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
System.out.println("test begin");
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
int[] arr3 = copyArray(arr1);
quickSort1(arr1);
quickSort2(arr2);
quickSort3(arr3);
if (!isEqual(arr1, arr2) || !isEqual(arr1, arr3)) {
succeed = false;
break;
}
}
System.out.println("test end");
System.out.println("测试" + testTime + "组是否全部通过:" + (succeed ? "是" : "否"));
}
}