快速排序–原始快排、二路快排、三路快排、三路快排优化
思路:在原始数组中找到一个区分点,然后遍历数组将大于区分点的元素放置区分点右边,小于等于区分点的元素放置区分点左边,每当发现比基准值小的元素就放在基准值左边,大于等于时放在基准值右边。当结束一次遍历时,基准值元素一定在最终位置。按照基准值左边与右边待排序数组重复上述过程。
快排整体思路图解:
快排具体执行过程图解:
快速排序源代码:
import java.util.Random;
public class Inc {
public static void main(String[] args) {
int[] data=generateRandomArray(10000,10,20000);//随机生成一万个整数,在10-20000区间
quickSort(data);
}
public static void quickSort(int[] array){//快排
long start=System.currentTimeMillis();
int n=array.length;
if(n<=1){//判断待排序数组是否为空数组或只含有一个元素,若是则直接返回
return;
}
quickSortInternal(array,0,n-1);
long end=System.currentTimeMillis();
System.out.println("基础快排耗时:"+(end-start)+"毫秒");//测试程序耗时
}
//快排递归
private static void quickSortInternal(int[] array,int low,int high){
if(low>=high){//递归推出条件
return;
}
int q=partition(array,low,high);//确定分区点最终位置
quickSortInternal(array,low,q-1);//左边区间(小于区分点元素)递归排序
quickSortInternal(array,q+1,high);//右边区间(大于等于区分点元素)递归排序
}
/**
* 对数组array[l...r]部分进行partition操作
* 返回p,使得array[l+1...p-1]<array[p],array[p+1...r]>=array[p]</array[p],array[p+1...r]>
* @param array 待排序数组
* @param l 数组开始点
* @param r 数组结束点
* @return 分区点下标
*/
private static int partition(int[] array,int l,int r){
//默认比较元素为待排序数组的第一个元素
int v=array[l];
//[l+1...j]为<v的元素区间(刚开始为空区间,随着<v元素的加入区间加长)
int j=l;
//[j+1...i-1]为>=v的元素区间(刚开始为空区间,随着>=v元素的加入区间加长)
int i=l+1;//i下标作用在于遍历除比较元素外的其他元素,即从第二个元素开始到结尾
for(;i<=r;i++){//遍历数组
if(array[i]<v){//当大于等于v时保持不动即可,<v时放置j下标区间
swap(array,i,j+1);
j++;
}
}
//此时<v元素在前面,>=v元素在后面区间,将<v元素区间的最后一个元素与比较元素交换即可以达到最终效果(区分点元素到达最终位置,<v的元素在区分点左边区间,>=v的元素在区分点右边区间)
swap(array,l,j);
return j;//返回区分点下标
}
private static void swap(int[] array,int i,int j){//交换
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
//随机生成数字
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
}
快排时间复杂度:O(nlogn)(左右分区极度均衡时),最坏时时间复杂度退化到O(n^2)(比如极端情况下,数组已经升序有序时,每次选择第一个元素为区分点导致左右两个区间极度不平衡,全部元素位于右边区间,此时就需要大约n次分区操作,每次分区我们大约要扫描n/2个元素)
**空间复杂度:O(1),为原地排序,**因快排优化了归并排序需要另外开辟空间的问题,解决占用内存多的问题,采用原地分区方法,实现原地排序算法。
稳定性:否。(二路快排会详细解释)
以上就是简单的快速排序过程,那么快排可以优化吗?
假设此时有个待排序数组为int[]=new int[]{1,2,3,4,5,6,7,8,9},若每次都默认选择数组第一个元素为比较元素时,就容易出现>=v与<v的区间元素个数的极度不均衡现像(因每次都默认选择第一个元素为区分点),那么此时运算大量数据时就会影响程序整体效率。
仔细思考一下,上述问题的出现是因为分区点的选择太过于固定,若每次能够随机选取区分点的话两区间元素极度不均衡的概率就会大大降低(若随机选取分区点,int[]=new int[]{1,2,3,4,5,6,7,8,9}此数组每次选到第一个元素的概率大约为1/81),从而提高程序效率。
待排数组几乎有序情况图解:
优化后快排源代码:
import java.util.Random;
public class Inc {
public static void main(String[] args) {
int[] data=generateRandomArray(10000,10,20000);
quickSort(data);
}
public static void quickSort(int[] array){
long start=System.currentTimeMillis();
int n=array.length;
if(n<=1){
return;
}
quickSortInternal(array,0,n-1);
long end=System.currentTimeMillis();
System.out.println("优化快排耗时:"+(end-start)+"毫秒");
}
private static void quickSortInternal(int[] array,int low,int high){
if(low>=high){
return;
}
int q=partition2(array,low,high);
quickSortInternal(array,low,q-1);
quickSortInternal(array,q+1,high);
}
private static int partition2(int[] array,int l,int r){//每次快排随机生成分区点
//Math.random()为产生一个[0,1)的随机数(double)
int randomIndex=(int)(Math.random()*(r-l+1)+l);//随机数*数组长度+左下标然后强转确保区分点下标在数组合理下标范围内
swap(array,randomIndex,l);//将第一个元素与随机区分点元素交换
int v=array[l];//每次获得随机区分点元素
int j=l;
int i=l+1;
for(;i<=r;i++){
if(array[i]<v){
swap(array,i,j+1);
j++;
}
}
swap(array,j,l);
return j;
}
private static void swap(int[] array,int i,int j){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
}
二路快排:适合待排序数组中有大量重复元素时使用
二路快排的出现主要是为了解决当待排序数组元素重复度过高时,单纯随机选取分区点已经没有意义,极大可能容易产生两区间极度不均衡的情况,影响程序性能。
二路快排思路:故我们可设立两个指针,分别让从前到后、从后到前遍历;从前到后遍历的元素遇到>=v的停下来,<v的i++即可,从后向前便利的元素遇到<=v的元素停下来,>v的j–即可;最终遍历出口为i>j,当i=j时还需要比较与区分点元素的大小。这样遍历一遍后可以保证左右两个区间元素个数的平衡(因与区分点元素相等的其他元素分布于左右区间,跟单路快排相等元素只存在于某一区间不同)。
二路快排思路图解:
二路快排源代码:
import java.util.Random;
public class Inc {
public static void main(String[] args) {
int[] data=generateRandomArray(10000,10,100);//待排序数组由一万个元素组成,在10-100之间随机选取,重复度很高
quickSort(data);
}
public static void quickSort(int[] array){
long start=System.currentTimeMillis();
int n=array.length;
if(n<=1){
return;
}
quickSortInternal(array,0,n-1);
long end=System.currentTimeMillis();
System.out.println("二路快排耗时:"+(end-start)+"毫秒");
}
private static void quickSortInternal(int[] array,int low,int high){
if(low>=high){
return;
}
int q=partition3(array,low,high);
quickSortInternal(array,low,q-1);
quickSortInternal(array,q+1,high);
}
private static int partition3(int[] array,int l,int r){
//每次快排随机选取区分点
int randomIndex=(int)(Math.random()*(r-l+1)+l);
swap(array,l,randomIndex);
int v=array[l];
int i=l+1;//[l+1...i-1]区间为<=v(刚开始区间长度为空,随着元素加入长度变长)
int j=r;//[j...r]区间为>=v(刚开始区间长度为空,随着元素加入长度变长)
while (true){//死循环
//若左边区间下标合理且元素<v就一直i++即可,遇到>=v的元素时停止
while (i<=r&&array[i]<v)i++;
//若右边区间下标合理且元素>v就一直j--即可,遇到<=v的元素时停止
while (j>=0&&array[j]>v)j--;
if(i>j){//遍历终止条件
break;
}
//此时i下标元素为大于等于v,j下标元素为小于等于v,此时i下标元素与j下标元素交换即可
swap(array,i,j);
//交换完成后接着遍历剩余元素,直到条件不满足时退出循环
i++;
j--;
}
//循环退出条件为i>j,即此时j下标为小于v区间的最后一个元素,只需要将区分点元素与其交换即可达到j下标元素左边为小于等于区分点元素的元素集合,右边区间为大于等于区分点元素的元素集合
swap(array,l,j);
return j;//返回区分点下标
}
private static void swap(int[] array,int i,int j){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
二路快排10000个元素耗时:
相同情况下直接快排耗时:
相同情况下优化快排(随机选取区分点)耗时:
三路排序:优化二路排序,增加了==v区间
思路:在二路排序的基础上,新增==v区间,在待排序数组存在大量元素重复度过高的情况下,可达到一次性确定所有与区分点元素相同的最终位置,大幅度减少遍历元素,提高程序性能。
思路图解:
三路排序源代码:
import java.util.Random;
public class Inc {
public static void main(String[] args) {
int[] data=generateRandomArray(100000,1,10);//测试十万个元素,在1-100区间随机生成,重复度极高
quickSort(data);
}
public static void quickSort(int[] array){
long start=System.currentTimeMillis();
int n=array.length;
if(n<=1){
return;
}
quickSortInternal3(array,0,n-1);
long end=System.currentTimeMillis();
System.out.println("三路排序耗时:"+(end-start)+"毫秒");
}
private static void quickSortInternal3(int[] array,int l,int r){//三路排序执行过程
if(l>=r){//判断递归退出条件
return;
}
//随机生成区分点
int randomIndex=(int)(Math.random()*(r-l+1)+l);
swap(array,l,randomIndex);
int v=array[l];
//[l+1...lt-1]为<v(刚开始为空)
int lt=l;
//[gt...r]为>v(刚开始为空)
int gt=r+1;
//[lt=1...i-1]为==v(刚开始为空)
int i=l+1;
while (i<gt){//遍历数组条件(因现存gt下标区间的都是遍历数组时换到里面的,故不用再次遍历)
if(array[i]<v){//若小于v时,i下标元素与lt+1下标元素交换
swap(array,i,lt+1);
lt++;//<v区间长度加长
i++;//遍历下一个元素,因遍历过的元素大于的已经交换到>v的区间,i下标元素与lt+1元素交换,lt+1元素必定为==v,故需i++
}else if(array[i]>v){//若大于v时,i下标元素与gt-1下标元素交换,因刚开始要将此区间置空,故gt=r+1,此时空间不存在,gt指针从后向前遍历,故与gt-1下标元素交换
swap(array,i,gt-1);
gt--;//从后向前遍历,下标--表明区间长度加长,此时i下标不动时因为从后面区间换过来的元素还没有遍历过,故需要继续判断其大小
}else{//若==v时,直接i++继续遍历下一个元素即可
i++;
}
}
//此时一次快排遍历结束,将区分点元素与<v区间的最后一个元素交换,达到左边区间为<v,右边区间为>v集合
swap(array,l,lt);
//递归<v区间
quickSortInternal3(array,l,lt-1);
//递归>v区间
quickSortInternal3(array,gt,r);
//此时与区分点元素数值相同的所有元素均到达最终位置
}
private static void swap(int[] array,int i,int j){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
public static int[] generateRandomArray(int n,int rangeL,int rangeR){
if(rangeL>rangeR){
throw new IndexOutOfBoundsException("越界异常");
}
int[] arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
}
return arr;
}
}
测试程序耗时:
同种情况下二路排序耗时:
三路快排优化:
当元素个数比较小时,调用直接插入排序。(一般是<=15,当剩余16个元素时直接插入排序速度最快),使用位置在于判断递归退出条件时,此优化百万数量级上只能优化三十毫秒左右,属于小优化。
快排总结:
原始快排方法时间复杂度取决于区分点导致两边区间是否均衡,若趋于均衡时间复杂度为O(nlogn),若极端情况出现两边区间一边元素过多另一边过少或者没有,时间复杂度就会退化到O(n^2);
二路排序利用原地分区巧妙的解决了原始快排的两区间不均衡的问题,很大程度上优化了原始快排;
三路排序是在二路排序的基础上再次优化,新增==v区间,一次可确定多个元素到达最终位置,更大程度上优化了原始快排。
但三者相比,三路快排在待排序数组元素重复度过高时效率最高,但在待排序数组近乎无序时相对于二路排序较慢(因三个指针),但在可接受范围内,故建议使用三路快排。
快排与归并排序对比:
两者都采用分治思想,但归并排序是从下至上解决问题,即将待排序数组分解到单个元素时再合并数组,且开辟新空间非原地排序,在运算大量数据时占用空间;快排则是完美解决了归并排序的问题,采用原地分区思想,节省空间,故相同情况下采用快排。