算法原理
- 先通过第一趟排序,将数组原地划分为两部分,其中一部分的所有数据都小于另一部分的所有数据。原数组被划分为2份。
- 通过递归的处理,再对原数组分割的两部分分别划分为两部分,同样是使得其中一部分的所有数据都小于另一部分的所有数据。这个时候原数组被划分为了4份。
- 步骤1,2被划分后的最小单元子数组来看,它们仍然是无序的,但是,它们所组成的原数组却逐渐向有序的方向前进。
- 这样不断划分到最后,数组被划分为多个由一个元素或多个相同元素组成的单元,这样数组就有序了。
实现分割数组的思路
首先选择基准元素,一般我们选取数组中第一个元素为基准元素(假设数组是随机分布的)。
从两端分别开始,直到左边指针找到比基准元素大的元素,右边指针找到比基准元素小的元素。然后交换两个位置元素。
然后继续移动两个指针,找到满足交换条件的元素,再次交换。
直到左边的指针>=右边的指针。最后交换首个元素和两个指针相遇的位置的元素。
这样,我们就把数组分成了两部分{3,1,2,5,4,6}和{9,7,10,8},然后再对这两个部分做分割处理…
Java代码实现
package advancedsort;
import java.util.Arrays;
public class AdvancedSortTest3 {
public static void main(String[] args) {
int[] data = {77,66,99,88,55,44,33,22,11,9,8,7,6,5,4,3,2,1};
quickSort(data,0,data.length-1);
System.out.println(Arrays.toString(data));
}
//宏观的快速排序算法的逻辑实现,依赖递归算法应用
public static void quickSort(int[] data, int left, int right){
//应用的是递归的思路,边界条件是左边的指针>=右边的指针
if(left >= right){ //递归的边界条件
return;//结束方法
}else {
//递归之前把数组data分割成两部分。
//前半部分所有元素都应小于后半部分,前<base,后>base。
//返回的是中间位置的指针
int partition = partitionLR(data,left,right);
//分割后,就可以进行递归
//在这里,我们跳过了partition位置的元素,因为它比前面的都大,比后面的都小,它的位置是正确的
quickSort(data,left,partition-1);
quickSort(data,partition+1,right);
}
}
//实现数组分割功能
public static int partitionLR(int[] data, int left, int right){
int i = left;
int j = right +1;
int base = data[left];//基准值,默认是被分割的数组的第一个元素
while (true){
while (i<right && data[++i]<base){}//基准是第0个元素,所以从第一个元素开始判断,寻找大于等于基准的元素
while (j>left && data[--j]>base){}//从最后一个元素开始,寻找比基准小的元素
if (i>=j){//如果已经遍历了整个数组,就跳出循环
break;
}else {//交换两个元素
swap(data,i,j);
}
}
//将base处元素和j处元素交换
swap(data,left,j);
return j;
}
//实现数组元素交换的方法
public static void swap(int[] data, int i, int j){
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
快速排序算法提升
快速排序具有最好的平均性能(average behavior),但最坏性能(worst case behavior)和插入排序相同,也是O(n^2)。
比如一个序列5,4,3,2,1,要排为1,2,3,4,5。按照快速排序方法,每次只会有一个数据进入正确顺序,不能把数据分成大小相当的两份,很明显,排序的过程就成了一个歪脖子树,树的深度为n,那时间复杂度就成了O(n^2)。
尽管如此,需要排序的情况几乎都是乱序的,自然性能就保证了。资料显示,在数据量小于20的时候,插入排序具有最好的性能。当大于20时,快速排序具有最好的性能,归并(merge sort)也望尘莫及,尽管复杂度都为nlog2(n)。
针对最坏性能的时候,其实我们也是可以有一些优化的办法:理想状态下,应该选择被排序数组的中值数据作为基准,也就可以让一半的书大于基准数,一半的数小于基准数,这样会使得数组被划分为两个大小相等的子数组,对快速排序来说,拥有两个大小相等的子数组是最优的情况。
但是,判断数组的中值同样会花费时间。所以我们一般采用三项取中划分:
取数组中第一个、中间的、最后一个,选择这三个数中位于中间的数。
提升效率后的算法实现
package advancedsort;
import java.util.Arrays;
public class AdvancedSortTest2 {
public static void main(String[] args) {
int[] data = {66,55,44,33,22,99,88,77,11,9,8,7,6,5,4,3,2,1};
quickSort(data,0,data.length-1);
System.out.println(Arrays.toString(data));
}
//宏观的快速排序算法的逻辑实现,依赖递归算法应用
public static void quickSort(int[] data, int left, int right){
//应用的是递归的思路,边界条件是左边的指针>=右边的指针
if(left >= right){ //递归的边界条件
return;//结束方法
}else {
//递归之前把数组data分割成两部分。前半部分所有元素都应小于后半部分,前<base,后>base。
int partition = partitionLR(data,left,right);
//分割后,就可以进行递归
quickSort(data,left,partition-1);
quickSort(data,partition+1,right);
}
}
//实现数组分割功能
public static int partitionLR(int[] data, int left, int right){
int i = left;
int j = right +1;
int base = data[left];//基准值,默认是被分割的数组的第一个元素
//取出数组第一个,中间,最后,三个数据,取大小位于中间的那个数来做base
//上面的方法暗含一个前提:数组的总的数据项必须大于等于3个
int size = (right-left) +1; //分割的数组的长度
if (size >= 3){
base = getMid(data,left,right);//实现三项取中。这个方法会把最大的数放到right位置,中值放在left位置。
}
while (true){
while (i<right && data[++i]<base){}
while (j>left && data[--j]>base){}
if (i>=j){//如果已经遍历了整个数组,就跳出循环
break;
}else {//交换两个元素
swap(data,i,j);
}
}
//将base处元素和j处元素交换
swap(data,left,j);
return j;
}
//实现数组元素交换的方法
public static void swap(int[] data, int i, int j){
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
//三项取中的方法
public static int getMid(int[] data, int left, int right){
//数组中间的元素对应的索引
int mid = (left+right)/2;
//将三个数中最大的值放到最右面
if(data[left] > data[right]){
swap(data,left,right);
}
if(data[mid]>data[right]){
swap(data,mid,right);
}
//将中指的数放到最前面
if (data[left]<data[mid]){
swap(data,left,mid);
}
return data[left];
}
}
处理小划分
如果使用三数据取中划分方法,则必须遵循快速排序算法不能执行三个或者少于三个的数据,如果大量的子数组都小于3个,那么使用快速排序是比较耗时的。这个时候可以结合前面我们讲过简单的排序(冒泡、选择、插入)组合起来用。
当数组长度小于M时(hight-low <= M),不进行快排,而进行插入排序。转换参数M的最佳值和系统是相关的,一般来说,5-15之间的任意值在多说情况下都能令人满意。