快速排序被称为是20世纪10大算法之一,可见其重要性,不明觉厉。
学习快速排序不单单只是一种排序算法,更重要的是其中包含的思想,分治思想。
下面进入正题,本文将给出多种快速排序的方法,并给予评价。
快速排序的思想是分治,分而治之,和归并排序一样,我们将大问题分解同小规模的具有相同解法的小问题。
快速排序的重点在分,归并侧重“并”,所谓分,指的是找到一个位置,这个位置的数将序列划分成两部分,左边比它小,右边比它大,然后分别对左右两边进行同样的操作,因此关键就在于找到这个位置。
通用函数:
public static int less(Comparable a,Comparable b){
return a.compareTo(b);
}
public static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
public static void show(Comparable[] a){
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
/********-------------*********/
//两路快排
public static void partition2(Comparable[] a,int left,int right){
Random r=new Random();
int k=left+r.nextInt(right-left+1);
exch(a,left,k);
Comparable t=a[left];
int less=left;
// 小于区的位置为[left+1,less] 这个区间
for(int i=left+1;i<=right;i++){
//当前数比 t小,小于区扩大
if(less(a[i],t)<0){
exch(a,++less,i);
}
}
exch(a,left,less);
}
public static void sort2(Comparable[] a,int left,int right){
if(left>=right){
return;
}
int[] index=partiton3(a,left,right);
sort2(a,left,index[0]);
sort2(a,index[1],right);
}
public static void sort2(Comparable[] a){
if(a==null|| a.length<=1){
return;
}
int left=0,right=a.length-1;
sort2(a,left,right);
}
其中区间[left+1,less]为小于区的区间,i是游标,遍历整个数组,初始时刻,less=left,因此区间内没有数,i初始为left+1,即第一个数,
如果这个数小于t,那么它属于小于区,小于区扩大,即++less,在与i进行交换
如果这个数不小于t,那么不处理,i++就好
当循环遍历完成后,小于区的最后一个位置是less指向的数,这是一个循环不变式,和我们定义的时候是一样的,此时将它与第一个元素进行交换(less指向的数是小于t的,第一个数为t,交换后less左边全部是小于t的数),返回less。
上述算法虽然可以,但是存在缺陷:
如果数组中存在大量的相同元素,比如
1,1,2,2,2,2,2,2,3,
进行切分的时候,不均衡,全部是相同的话,一次切分就只有一个元素,退化成N*N的复杂度排序。
对此进行改善
public static int partition(Comparable[] a,int left,int right){
Random r=new Random();
int k=left+r.nextInt(right-left+1);
exch(a,left,k);
Comparable t=a[left];
int i=left+1,j=right;
while(i<=j){
//从左往右,如果小于t一直扩,知道扩不动,遇到大于等于
while(less(a[i],t)<0 && i<=right){
i++;
}
//从右往左扩,如果大于一直扩,知道碰到小于等于
while(less(a[j],t)>0 && j>=left+1){
j--;
}
if(i>j){
break;
}
//交换 大于等于 和小于等于
exch(a,i,j);
}
//循环结束后,j碰到的是小于等于t的数,因此第一个数与它交换
exch(a,left,j);
return j;
}
public static void sort(Comparable[] a,int left,int right){
if(left>=right){
return;
}
int index=partition(a,left,right);
sort(a,left,index-1);
sort(a,index+1,right);
}
public static void sort(Comparable[] a){
if(a==null|| a.length<=1){
return;
}
int left=0,right=a.length-1;
sort(a,left,right);
}
<= >= 将相等的数随机分到两边,尽量保证两边切分不至于太过
两边都是交换大于等于 小于等于, 能够更加均衡一些,不至于退化到普通排序
三路排序:划分 小于区,等于区, 大于区
public static int[] partiton3(Comparable[] a,int left,int right){
Random r=new Random();
int k=left+r.nextInt(right-left+1);
exch(a,left,k);
Comparable t=a[left];
int less=left,more=right,i=left+1;
//循环不变条件 [left+1,less] less 初始化为left less左边包括less都是小于他的数
//循环不变条件 (more,right] more 初始化为right,开区间
while(i<=more){
// [ <] [====] i i小于t 将i与小于区的下一个元素也就是等于区的第一个元素交换,小于区扩大
if(less(a[i],t)<0){
exch(a,i++,++less);
}else if(less(a[i],t)==0){
i++;
}else{
exch(a,more--,i);
}
}
//less指向的是小于区的数,与第一个数交换
exch(a,left,less);
return new int[]{less-1,more+1};
}
public static void sort3(Comparable[] a,int left,int right){
if(left>=right){
return;
}
int[] index=partiton3(a,left,right);
sort3(a,left,index[0]);
sort3(a,index[1],right);
}
public static void sort3(Comparable[] a){
if(a==null|| a.length<=1){
return;
}
int left=0,right=a.length-1;
sort3(a,left,right);
}
应用:找第K大的数
//找第k大的数
public static Comparable findKthNum(Comparable[] a,int k) {
Comparable t = findKthNum(a, 0, a.length - 1,k);
return t;
}
public static Comparable findKthNum(Comparable[] a, int left, int right, int k) {
if(left>right){
return -1;
}
int len = a.length;
int index = QuickAll.partition(a, left, right);
if (k == len - index) {
return a[index];
} else if (k > len - index) {
return findKthNum(a, left, index - 1, k);
} else {
return findKthNum(a, index + 1, right, k);
}
}
public static void main(String[] args) {
Integer[] a={3,1,5,4,7,2,6};
// QuickAll.sort(a);
System.out.println(findKthNum(a,8));
}
}
partition后返回一个数,左边比小,右边比它大,返回index,说明这个数是当前数组中第len-index 大的一个数,如果k比这个数大,那么往前去找,如果比他小,往后去找。
复杂度分析:我们假设每次切分都是一半,均匀的
第一次partition的过程,时间复杂度是O(N)
第二次是O(n/2)
第三次是O(n/4)
O(1)
T(n)=n+n/2+n/4+…+1;
=2n
复杂度是O(N)