1.一路快排:
- 思路分析:在无序数组中选择一个基准值作为分区点,遍历数组之后,将小于该基准值的数放在左边,大于等于基准值的数放在右边。这样,将数组分为了小于区和大于等于区。根据分治思想,将两个区域分别进行排序,直到区间长度为1,说明此时数组有序。
- 代码实现:
package www.first;
public class QuickSortTest{
public static void quickSort(int[] arr){
quickSortInternal(arr,0,arr.length-1);
}
public static void quickSortInternal(int[] arr,int l,int r){
if(l>=r){
return;
}
int p = patition(arr,l,r);
quickSortInternal(arr,l,p-1);
quickSortInternal(arr,p+1,r);
}
public static int patition(int[] arr,int l,int r){
//默认选择第一个元素为基准值
int value = arr[l];
//小于区的位置
int j = l;
//从第二个元素开始比较
int i = l+1;
while(i<=r){
if(arr[i]<value){
//将大于区的第一个元素与当前元素进行交换
//即:将小于区向后扩张
swap(arr,++j,i++);
}else{
i++;
}
}
//遍历结束后,即小于区元素已经全部找完,交换基准值到相应的位置
//此时,[l,j-1]为小于区,[j+1,r]为大于等于区,可见j为分区点
swap(arr,l,j);
return j;
}
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] data = new int[]{2,1,2,43,5,3,42,5,35};
quickSort(data);
for(int a:data){
System.out.print(a+" ");
}
}
}
结果如下:
- 一路快排的优化:每次取第一个元素为基准值,这样当数组近乎有序时,会导致我们的排序算法的时间复杂度退化为O(n^2)。此时,我们可以采取随机取基准值的方法来优化排序算法。优化部分代码如下:
public static int patition(int[] arr,int l,int r){
//产生数组区间内的随机下标,并与第一个元素交换
//此时的基准值是数组元素的随机元素
int random = (int) (Math.random()*(l-r+1)+l);
swap(arr,l,random);
int value = arr[l];
2.二路快排:
- 思路分析:一路快排在极端情况下使得我们的分区非常不平衡,会导致时间复杂度退化。优化的另一种方法就是双路快排,定义两个指针,一个从前往后遍历找到小于基准值的元素,一个从后往前遍历找到大于基准值的元素,最后将基准值放回等于区的起始位置。这样不仅大大提高我们的遍历速度,也会使我们的分区变得平衡。
- 代码实现:
public static int patition(int[] arr,int l,int r){
//选取数组一随机元素为基准值
int random = (int) (Math.random()*(r-l+1)+l);
swap(arr,l,random);
int value = arr[l];
//前指针,扫描并找出小于元素
int i = l+1;
//后指针,扫描并找出大于元素
int j = r;
while(true){
//从前往后遍历,若小于基准值则扩大小于区
//若大于基准值则停止
while(i<=r&&arr[i]<value){
i++;
}
//从后往前遍历,若大于基准值则扩大大于区
//若小于基准值则停止
while(j>=l+1&&arr[j]>value){
j--;
}
//每次停止扫描之后进行判断,看是否扫描结束
//未结束再交换,结束则跳出
if(i>j){
break;
}
//交换此时停止位置的数,然后继续扫描
swap(arr,i++,j--);
}
//扫描结束,将基准值放到等于区的起始位置
//此时j处于小于区的最后一个位置
swap(arr,l,j);
//交换之后,j是大于等于区的起始位置
return j;
}
3.三路快排:
- 思路分析:前两种方式其实还是对相等的元素进行了比较交换,做了处理,若等于区元素很多,会耗费大量时间。三路快排就是为了解决这类问题,对等于区不做处理,选定基准值后,将数据分为三个区域:大于区、等于区、小于区。只对大于区和小于区进行分区排序处理,大大节省了时间。
- 代码实现:
public static int[] patition(int[] arr,int l,int r){
//选取随机元素作为基准值
//并与最后一个元素进行交换
int random = (int) (Math.random()*(r-l+1)+l);
swap(arr,random,r);
int value = arr[r];
//定义小于区的起始位置
int less = l-1;
//定义大于区的起始位置
int more = r+1;
//l的最后位置是等于区的最后一个位置,即:大于区的前一个位置
while(l<more){
//若小于基准值,则less扩张,将此元素与等于区的第一个元素进行交换,l向后扫描
if(arr[l]<value){
swap(arr,++less,l++);
//若大于基准值,则more扩张,将此元素与等于区的最后一个元素进行交换,
//!!!注意:此时l不会向后扫描,而是会判断交换过来的元素
}else if(arr[l]>value){
swap(arr,--more,l);
}else{
//等于区不作处理
l++;
}
}
//返回等于区间
return new int[]{less+1,more-1};
}
public static void quickSortInternal(int[] arr,int l,int r){
if(l>=r){
return;
}
int[] p = patition(arr,l,r);
//等于区间起始位置的前一个即是小于区的最后一个位置
quickSortInternal(arr,l,p[0]-1);
//等于区间末尾位置的后一个即使大于区的第一个位置
quickSortInternal(arr,p[1]+1,r);
}
4.快速排序总结:
1)时间复杂度:一般情况下,时间复杂度为O(nlogn)。但在极端情况下,例如:{1,2,3,3,4,5,6},选择第一个元素为基准值进行分区,造成大于等于区的元素太多,所以需要n次分区,才能完成快排,此时时间复杂度退化成了O(n^2)。
2)空间复杂度:快排每次递归都会返回一个分区点的位置,必须使用栈,所以空间复杂度就是栈使用的空间,分区次数一般情况下为logn,故快排的空间复杂度最好是O(logn),最坏是O(n)。
3)稳定性:确定分区点的过程中不断的进行交换,将小于的元素与大于区的第一个进行交换,可能会把相等的元素位置打乱,因此是一种不稳定的排序算法。