引言
文章相关代码已收录至我的github,欢迎star:lsylulu/myarticle
以前初学快排的时候,网上的资源太乱。感觉每个人的思路都不一样,实现上好像千差万别,看着挺懵。所以就会导致自己觉得快排的实现方式好像有很多种(其实不然)。很久没抠算法了,想抠一下,系统的把快排抠干净。
文章导读
- 快排分类与理解
- 经典快排的实现
- 优化后快排的实现
- 总结
一、快排分类
从实现思想上来说,快排分为经典快排和优化后的快排。快排可以优化成一趟排序,排完所有相同的元素及随机快排。
从确定基准数策略上来说,可以分为头元素,尾元素和随机元素。本文经典快排采用尾元素的方式,即每趟排序都与数组的最后一个元素比较。优化后则用尾元素或随机的方式。
从基准数在一趟排序中的位置来说,可以分为基准数位置不变的和变化的。对于基准数位置不变的排序,可以省一个变量,但是理解上绕了一点点;基准数位置变化的排序应该是大家经常看到的。
简单说下我对经典快排与优化后快排的理解,对于经典快排来说,数据状况的对排序时间复杂度的影响比较大,一次只确定一个元素。将小于等于基准数的放前面,大于基准数的放后面;而优化后的快排一次能排所有值相同的元素,一趟排序后将数组分为3段,小于区域,等于区域,大于区域。而等于区域的划分就是相对于经典快排加速的过程。
如果将基准数换成随机数则,复杂度与期望的n*logn非常接近。
二、经典快排的实现
本文基准数用x代替吧。对于每一趟排序,x位置会变化,所以需要先记录下来。
递归策略是一趟排序之后获取x的索引,然后排它之前的和它之后的。
每一趟排序(partition)的策略是将<=x放左边,>x放右边。意味着需要有一个变量记录着边界,我采用more记录>x那一段的左边界。保证more及more右边的元素必须大于x。
public static void quickSort(int[] arr){
if (arr == null || arr.length < 2) {
return;
}
//开启快速排序
quickSort(arr,0,arr.length-1);
}
public static void quickSort(int[] arr,int l,int r){
if(l<r){
//cur是基准数在一趟反序后的索引
int cur=partition(arr,l,r);
//排序cur左边的
quickSort(arr,l,cur-1);
//排序cur右边的
quickSort(arr,cur+1,r);
}
}
/**
* 一趟排序,以r为基准数,给l-r上的数排序
* 小于等于r的方左边;大于r的放右边
* 保证more和more的右边的数大于arr[r]
* @param arr
* @param l 当前的索引
* @param r
* @return
*/
public static int partition(int[] arr,int l,int r){
//大于基准数的边界
int more=r+1;
//记录基准数
int num=arr[r];
while (l<more){
//小于等于基准数,l向右走
if(arr[l]<=num){
l++;
}else{
//--more,保证每一次排序,从more开始一定是大于num的
//交换后并不知道arr[l]>x还是arr[l]<=x
//因此l还是在原地
swap(arr,l,--more);
}
}
//more-1必然是最终基准数所在的索引
return more-1;
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
对于经典快排时间复杂度及缺点的分析
我们都知道,经典快排每一趟把数组划分成2个部分,因此最好情况是每一次都能均分数组,只需要递归logn次,一次递归的时间复杂度是n,因此T(n)=n*logn。最坏情况是每次都划分成一个元素和其他元素,需要递归n次,一次递归的时间复杂度是n,因此T(n)=n^2。
经典快排受数据状况的影响比较大,比如排好序的数组就会遇到最坏的情况。那么,怎么才能尽量往好的情况发展呢?解决这个问题就需要随机快排。
三、优化后的快排的实现
在实现上我让x的位置不变,x取数组最右边的元素,一趟排序后再将x换到对应位置。
递归策略是每次获取x的数组,数组中两个元素分别表示=x区域的左边界和右边界。
每一趟排序的策略是将数组按照基准数分为3段,即小于,等于,大于。
public static void quickSort(int[] arr){
if (arr == null || arr.length < 2) {
return;
}
//开启快速排序
quickSort(arr,0,arr.length-1);
}
public static void quickSort(int[] arr,int l,int r){
if(l<r){
//加上这一行就是随机快排
//每次都把r与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);
}
}
/**
* 一趟排序,类似于荷兰国旗的套路
* 以r为基准数,l是遍历时走的索引
* less是左边界,more是右边界
* 最终返回的数组是等于arr[r]的左边界和右边界
* 一次partition确定了所有等于arr[r]的位置
* @param arr
* @param l 当前的索引
* @param r
* @return 返回的是基准数的左边界与右边界
*/
public static int[] partition(int[] arr,int l,int r){
//相等区域左边界
int less=l-1;
//相等区域右边界
int more=r;
while (l<more){
//当前索引小于基准数,则将当前arr[l]与左边界的后一个数交换
//这个if主要是将左边界右扩一个,理解不了的话,下面有图说明
if(arr[l]<arr[r]){
swap(arr,l++,++less);
}else{
//当前索引大于等于基准数,则将当前索引与右边界的前一个数交换
swap(arr,--more,l);
}
}
//最后把arr[r]放到正确位置
swap(arr,more,r);
//返回左右两个边界
return new int[]{less+1,more};
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
简单分析一下less右扩的过程:
此时arr[l]<x,需要执行swap(arr,l++,++less);
交换完之后应该是这样
就是这个if可能会有一点点绕,其他情况依次类比就好啦~
总结
经典快排最好情况T(n)=n*logn,最坏情况T(n)=n*n;优化后的快排一趟下来能把相同的元素都排好序,如果做了基准点随机交换,则每一次对于数组的划分都会趋于平均,可以认为时间复杂度就是n*logn。
快排的稳定性都是不稳定的。
在比较排序中,T(n)=n*logn是最少的了,不可能找到时间复杂度比这小的了。
文章若有不当之处,欢迎评论指出~
如果喜欢我的文章,欢迎关注知乎专栏Java修仙道路~