快速排序也属于“交换”类的排序。
核心思想可以概括为:通过多次划分操作实现排序。每一趟选择当前所有子序列中的一个关键字(通常是第一个)作为枢轴,将小于它的元素统统放到它的前面,大于它的统统放到它的后面。然后用这种方法去操作“被放在它前面的小于它的序列”和“被放在它后面的大于它的序列”。
具体实现方法一:
思想:将该枢轴后面的序列先进行排序,即先排出小于枢轴的所有元素放在目前枢轴后面整个序列的前半部分,所有大于枢轴的所有元素放在目前枢轴后面整个序列的后半部分。然后将目前枢轴后面整个序列的前半部分的最后一个元素(它是小于枢轴的)和当前枢轴交换,即完成一趟排序。(让枢轴前面的元素都小于它,枢轴后面的元素都大于它)
假设我们把第一个元素设置为枢轴:
第一个元素索引为l,用索引j去追踪小于枢轴那部分序列的最后一个元素,用索引i去追踪待探明的当前元素:
当然初始时紫色和黑色序列元素都为0个。
如果探明索引i指定的元素e为大于v的,那么就直接将它加入到黑色序列中,i++,然后去探查下一个元素。
如果探明索引i指定的元素e为小于v的,那么就先将j后移一位,用当前j指定的元素和它交换位置从而使得e加入到紫色序列中,i++,然后去探查下一个元素。
索引i遍历完最后一个元素时,将索引j所指向的元素(即紫色序列最后一个)与v进行交换位置,完成一趟快速排序。
一趟快速排序的结果为:
代码实现:
package com.quickSort;
public class QuickSort {
//快速排序的对外公共接口
public static void quickSort(int[] arr,int n){
privateQuickSort(arr,0,n-1);
}
//快速排序的实现
//对arr[l..r]的部分进行快速排序
private static void privateQuickSort(int[] arr, int l, int r) {
if(l>=r)
return;
int p=partition(arr,l,r);
privateQuickSort(arr,l,p-1);
privateQuickSort(arr,p+1,r);
}
//快速排序算法的核心部分
//对arr[l..r]的部分进行一次partition操作
//返回p,使得arr[l...p-1]<arr[p];arr[p+1...r]>arr[p]
private static int partition(int[] arr, int l, int r) {
int v=arr[l];
//arr[l+1...j]<v;arr[j+1,i)>v
//特别注意,arr[j]<v;而arr[i]是正在讨论的元素对象
//最后将索引j所在的元素和最前端索引l所在的元素进行交换
int j=l; //别忘了j指的是小于枢轴的序列的最后一个元素,所以一开始定义时就把它定在了要探索元素前面
//这犯过一个错误:把判断条件写成了i<r;这样就漏掉了一个处于尾部的元素
for(int i=l+1;i<=r;i++){
//通过i和j的初始值的设定,保证两段区间在初始时都为空
if(arr[i]<v){
int temp=arr[j+1];
arr[j+1]=arr[i];
arr[i]=temp;
j++;
}
}
int temp=arr[j];
arr[j]=arr[l];
arr[l]=temp;
return j;
}
public static void main(String[] args) {
int[] arr=new int[]{10,9,8,7,6,5,4,3,2,1};
quickSort(arr,10);
for(int i=0;i<arr.length;i++)
System.out.print(arr[i]+" ");
}
}
具体实现方法二:
思想:相较于上面的实现方法先把后面的元素排好序,然后再把枢轴元素与前段序列最后一个元素交换的方式实现一趟快速排序。
我们可以先把枢轴元素复制出来,这样整个序列中就有了一个空位。那么从两端开始(先从后端)依靠这个空位,将小于枢轴的元素放到整个序列的前半段,大于的放在序列的后半段。中间会因此空出一个空位,这时我们将复制出来的枢轴元素放进去,即完成一趟快速排序。
因为先从末端开始,当探明索引j所指的元素大于v时,不用管它,只是将j向前移动一位,即j--。
继续从末端方向向前探索比较,当所探索元素小于v时,则将其放到前段空出的位置上,然后i后移一位,即i++。
然后转到从序列前端开始往后进行元素的探明。当发现比v小的元素不用管它,直接把i向后移一位。
当遇到大于v的元素时,将其放到后面的空位上,然后j向前移动一位,转到从末端方向进行探索。
以此类推。
当i==j时,将复制出来的枢轴元素放到空位上,即完成一趟快速排序。
代码实现:
package com.quickSort;
public class QuickSort3 {
public static void quickSort(int[] arr,int l,int r){
if(l<r){
int p=partition(arr,l,r);
quickSort(arr,l,p-1);
quickSort(arr,p+1,r);
}
}
private static int partition(int[] arr, int l, int r) {
int v=arr[l];
while(l<r){
//从末端开始
//若遍历到的元素大于枢轴,那么不用管它,继续往前遍历
//必须在while的判断语句中加入l<r,以增强健壮性,防止数组溢出
while(l<r&&arr[r]>=v) --r;
//将arr[r](小于v)放到v的前面
arr[l]=arr[r];
//转到前端方向向后遍历
while(l<r&&arr[l]<v) ++l;
//将arr[l](大于v)放到v的后面
arr[r]=arr[l];
}
arr[l]=v;
return l;
}
public static void main(String[] args) {
int[] arr=new int[]{10,9,8,7,6,5,4,3,2,1};
quickSort(arr,0,9);
for(int i=0;i<arr.length;i++)
System.out.print(arr[i]+" ");
}
}
说明:快速排序时间复杂度为O(nlog2n),且待排序序列越无序,本算法效率越高。
对于快速排序算法的相关优化(针对本篇第一种具体算法实现方式):
1)循环到底的时候,采用插入算法;(相关介绍在二路归并算法的java实现)
几乎对于所有的高级排序算法,有一种通用的优化,那就是在递归到底的情况下,可以使用直接插入排序进行优化。
即将代码片段:
if(l>=r)
return;
替换为:
if(r-l<=15){
insertionSort(arr,l,r);//已经定义的直接插入排序算法
return;
}
2)对比小编前面介绍的二路归并算法的实现,我们来看看当面对一个*乎有序的序列时,两种算法的表现。
1.对于归并排序,每一次都将当前序列划分为元素数量基本相*(偶数个元素时,两部分元素数量相等)的两部分序列。
那么当前数列划分到每部分只有一个元素时,共分了log2n层。每层排序时遍历一遍,即n。所以归并排序算法的时间复杂度为O(nlog2n)。
2.对于快速排序,当序列*乎有序时。每一次都会将当前序列划分为极度不均衡的两部分序列。
这样在序列已经有序的时候就会分成n层,每一层遍历一下。那么快速排序算法的时间复杂度就退回到了O(n2)。运行速度大大变慢。
由于快速排序调用递归的过程生成的这颗递归树,相较于归并排序,*衡度差太多。在待处理序列*乎有序的时候,快速排序的递归树高度一定大大地大于log2n,最差达到n。
那么怎么优化呢?
我们现在使用的是序列左边的第一个元素作为快速排序的枢轴,但我们希望的是尽可能地选择一个处于最后结果序列中间的元素作为枢轴元素。
同时我们又不能直接锁定并选择那个元素,怎么办?我们可以直接随机选择一个元素作为枢轴元素,此时,这个随机选择的方式生成的递归树的高度的数学期望就是log2n。也就大大地优化了快速排序算法。
在上面代码中partition()方法体中的最靠前位置插入以下代码:
//生成随机位置索引
int k=(int)(Math.random()*(r-l+1)+l);//别忘了加上偏移量l
//将原来最左边的元素和随机选择的元素交换一下位置,以下的代码不变
int tem=arr[k];
arr[k]=arr[l];
arr[l]=tem;
此时我们整个优化完的快速排序算法的最坏情况依旧是O(n2),但是退化到O(n2)的概率是非常非常低的。(这需要每次随机选中的那个元素是当前序列中的最小或者最大值,这有多难退回到O(n2)的概率就有多低)
3)下面我们再来看一种情况:当我们面对的序列是一个拥有非常多重复元素的序列时,我们的算法还高效么?
回去审视一下我们前面的代码就会发现,我们的隐含逻辑是将枢轴元素后面的序列分成小于枢轴的一部分和大于等于枢轴的一部分。
但是当我们面对的序列出现大量重复的元素时,会变成什么样?
像这样!
亦或是这样!
无论我们怎么样费尽心机地去将与枢轴相等的元素们放在合适的位置,无论我们多么尽心尽力地去寻找那个*衡点,以使得我们的算法的递归树均衡。
但是我们却难以如愿,这种情况在最坏的时候,依旧会让我们的算法退回到O(n2)。
那么,怎么优化呢?
优化方案一(双路快速排序法):
我们可以试一下将这些大量重复的元素*均地分散到两部分,这样我们的算法递归树就均衡了,提高到了O(nlog2n)。
我们从两边开始相向地检测当前元素和枢轴的大小关系,当左边检测到的元素大于等于枢轴时停住,当右边检测到的元素小于等于枢轴时停住。交换两个元素位置,继续向前检测,直到i和j相等。
这样我们就实现了将重复的元素均摊到了两边,优化了快速排序算法。
partition()方法优化后代码:
//快速排序算法的核心部分
//对arr[l..r]的部分进行一次partition操作
//返回p,使得arr[l...p-1]=<arr[p];arr[p+1...r]>=arr[p]
private static int partition(int[] arr, int l, int r) {
//生成随机位置索引
int k=(int)(Math.random()*(r-l+1)+l);//别忘了加上偏移量l
//将原来最左边的元素和随机选择的元素交换一下位置,以下的代码不变
int tem=arr[k];
arr[k]=arr[l];
arr[l]=tem;
int v=arr[l];
int i=l+1,j=r;
//arr[l+1...i]<=v;arr[j...r]>=v
//注意当这段程序运行结束时,arr[i]是从前往后看第一个大于等于v的元素
//arr[j]是从后往前看第一个小于等于v的元素,也就是整个序列最后一个小于等于v的位置
while(true){
while(i<=j&&arr[i]<v) i++;//当左边检测到的元素大于等于枢轴时停住
while(j>=i+1&&arr[j]>v) j--;//当右边检测到的元素小于等于枢轴时停住
//设置终结条件
if(i>j) break;
//交换一下两个元素的位置
int temp=arr[j+1];
arr[j+1]=arr[i];
arr[i]=temp;
//继续向前检测
i++;
j--;
}
//由于arr[j]是整个序列最后一个小于等于v的位置
//交换一下枢轴和arr[j]两个元素的位置
int temp=arr[l];
arr[l]=arr[j];
arr[j]=temp;
return j;
}
优化方案二(三路快速排序算法 Quick Sort 3 Ways):
在优化方案一中,我们将大量的重复元素*乎*均的分到了大于枢轴和小于枢轴的两部分中。但是我们想一想,既然这些等于枢轴的元素在两部分都存在,那我们能不能单独把它们分成一类放在序列的中间位置,这样的不就更加优化了么。
我们对整个序列采用三路快速排序算法进行处理,处理到一半时,应该是这种情况:
其中,l指向当前序列的最左端元素,lt指向小于枢轴的这个子序列的最后一个元素,i指向当前检测元素,gt指向大于枢轴的这个子序列的第一个元素,r指向整个序列的最后一个元素。
当前要检测的元素(即i指向元素)的大小有三种情况:
1.等于枢轴v时(最简单),直接i++向后移动一位i指针。
2.小于枢轴v,需要lt指针向后移动一位,然后此时lt指针所指对象和当前i指针所指元素进行交换位置,最后i++。
3.大于枢轴v,需要gt--后移一位,然后gt所指元素和当前i指针所指位置(当前检测元素)交换位置,注意,此时i位置不动。
以以上三种情况对整个序列进行逐个检测,得到最后序列为:
此时再将枢轴元素和lt指针所指定的元素交换一下位置
得到最终序列,再去递归arr[l..lt]和arr[gt...r]。
注意,别忘了最后要将lt指针往前移动一位,即lt--。
代码:
package com.quickSort;
public class QuickSort3Ways {
public static void QuickSort3(int arr[],int n){
QuickSort3Core(arr,0,n-1);
}
//三路快速排序处理arr[l...r]
//将arr[l...r]分为<v;==v;>v三部分
//之后递归对<v;>v两部分继续进行三路快速排序
private static void QuickSort3Core(int[] arr, int l, int r) {
int temp;
//partition
//arr[l+1,lt]<v;arr[lt+1,i)==v;arr[gt,r]>v
int v=arr[l];
int lt=l,i=l+1,gt=r+1;//保证arr[l+1,lt],arr[lt+1,i),arr[gt,r]初始都为空
while(i<gt){
if(arr[i]>v) { //检测元素大于枢轴时
temp=arr[gt-1];
arr[gt-1]=arr[i];
arr[i]=temp;
gt--;
}
else
if(arr[i]<v){ //检测元素小于枢轴时
temp=arr[lt+1];
arr[lt+1]=arr[i];
arr[i]=temp;
i++;
lt++;
}else
i++;//检测元素等于枢轴时
}
temp=arr[l];
arr[l]=arr[lt];
arr[lt]=temp;
// lt--;
QuickSort3Core(arr,l,lt-1);
QuickSort3Core(arr,gt,r);
}
public static void main(String[] args) {
int[] arr=new int[]{10,9,8,7,6,5,4,3,2,1};
QuickSort3(arr,10);
for(int i=0;i<arr.length;i++)
System.out.print(arr[i]+" ");
}
}