一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情。
1.0 版本
整个数组中拿最后一个数作为划分值num,然后参考上面过程让数组左侧是<=num,右侧是>num的,接着拿这个数和右侧第一个值交换,这样一轮就排好了。
接着分别让左侧和右侧重复上述行为:
结果就是5这个值位置不需要动了,位置就是最后的位置,每一次做分层处理之后都会有一个确定的数排好位置,然后左右侧递归处理得到排序后数组
2.0 版本
2.0 的思想就是上面荷兰国旗问题,虽然需要多一个辅助变量,但是一次处理可以搞定一批==5的划分数,会比1.0版本稍微快一点。之后还是递归处理
上述版本时间复杂度
上面时间复杂度都是O(N^2),因为都可以举出最差的例子:
上面这个例子划分的过程只搞定了一个数字,最后耗时是一个等差数列
3.0 版本
上面最差值怎么来的?其实主要原因是划分值选的不好,很偏是一个最大值(选一个最小值也很差,都会退化到N^2)。
如果能够取一个中间的划分值就好了:
3.0 版本就是随机选择一个数字作为num,然后和数组最后一位换了,之后一样处理。
但是这种情况好坏情况变成了概率事件了,最坏情况随机数就是偏值。
但是最终时间复杂度有变化吗?是有的,但是在这种情况下通过数学期望等概率相关方式证明最终的复杂度是O(N*logN)了,具体证明过程比较复杂,略(每一种情况复杂度都求出来,然后求数学上的期望值):
代码实现
public static void quickSort(int[] arr){
if(arr==null || arr.length < 2){
return;
}
quickSort(arr,0,arr.length-1);
}
// arr[L...R]排好序
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); // < 的区域
quickSort(arr,p[1]+1, R); // > 的区域
}
}
// 核心算法-处理arr,完成基准的排序
// 默认以arr[R]作为划分
// 返回=区域的左右边界,放置到res[0]和res[1]
public static int[] partition(int[] arr,int L, int R){
int less = L - 1;
int more = R;
while(L < more){
// L充当游标i的作用 R是之前放到最右侧的基准数 需要注意一下
if(arr[L] < arr[R]){
// 当前数arr[L] < 划分值arr[R]
swap(arr, ++less, L++);
}else if(arr[L] > arr[R]){
// 当前数arr[L] > 划分值arr[R]
swap(arr, --more, L);
}else {
// = 就直接下一轮
L++;
}
}
// 把基准值放到中间 - 和左侧边界交互
swap(arr,more,R);
return new int[]{less+1, more};
}
12和13就是partition的返回
快排的空间复杂度
结论是O(logN)级别不是O(1)级别或者O(N)的
这个的求法跟他的时间复杂度一样也是一个概率累加过程得到的
讨论最差情况,选择基准永远是最大的,那么你的递归区域就需要开N层才能全部解决:
好的情况就是你选的是中点,这就是一个二叉了,并且左侧一半的空间用完了之后其实就可以回收或者给右侧去作了,这个就是logN水平的额外空间,类似一棵二叉树的展开:
最后补充一下即便你不用递归,用其他方式例如迭代方式写快排,也是一样的因为中点位置是必须记录的,但是这个栈需要自己写压栈的,空间省不掉的