排序2:快速排序的优化方案

上一篇文章简单的讨论了快速排序的实现,但这种实现在分区不均衡时,有可能退化成时间复杂度为O(n2)的算法。
上一版实现代码如下:

public class QuickSort {
    private QuickSort(){}
    public static <T extends Comparable<T>> void sort(T[] arr) {
        partition(0, arr.length-1, arr);
    }
    private static <T extends Comparable<T>> void partition(int l, int r, T[] arr) {
        if (r<=l) {
            return;
        }
        // 降低分区不均衡的可能性,还可以采取三点取中法、十点取中法。
        int randIndex = (int) (Math.random()*(r-l+1) + l);  
        SortHelper.swap(l, randIndex, arr);
        // 最后一个小于arr[l]的元素的索引
        int k=l; 
        for (int i=l+1; i<=r; i++) {
            if (arr[i].compareTo(arr[l])<0) {
                SortHelper.swap(++k, i, arr);
            }
        }
        SortHelper.swap(l, k, arr);
        partition(l, k-1 ,arr);
        partition(k+1, r, arr);
    }
}

现在我们通过分析分区不均衡的原因,来寻找解决快速排序退化的问题。

双路快排

我们上面实现的排序,其实是将数组分成了小于标定元素和大于等于标定元素两部分。假设这一组元素中有大量重复元素,由于等于标定元素的元素很多,且都被放到了同一区域,就很容易造成分区不均衡。如果我们可以把等于标定元素的元素相对平均的分到两区域,那么就可以一定程度上解决分区不均衡的问题。
如下图,e是标定元素,其索引为l,两个指针i,j分别从数组的头部和尾部开始遍历,如果arr[i]<=e,则i++;如果arr[j]>=e,则j–。当i和j均停住时,i指向的元素大于e,j指向的元素小于e,此时交换两个元素的位置,并重复上面的操作,直到i>j,则退出遍历。此时j指向的元素就是<=e的分区中最后一个元素,交换e和arr[j],e就到了最终排好序后在的位置。然后,e左右两侧的数组分别调用partition方法,直到待排序的数组大小为1,即r==l。
在这里插入图片描述

public class QuickSort2Ways {
    private QuickSort2Ways(){}
    public static <T extends Comparable<T>> void sort(T[] arr) {
        int n = arr.length;
        partition(0, n-1, arr);
    }
    private static <T extends Comparable<T>> void partition(int l, int r, T[] arr) {
        if (l>=r) {
            return;
        }
        int index = sort(l ,r ,arr);
        partition(l, index-1, arr);
        partition(index+1, r, arr);
    }
    private static <T extends Comparable<T>> int sort(int l, int r, T[] arr) {
        int randIndex = (int) (Math.random()*(r-l+1) + l);
        SortTestHelper.swap(arr, l, randIndex);
        int i = l+1;
        int j = r;
        while (true) {
            while (i<=r && arr[i].compareTo(arr[l])<=0) {
                i++;
            }
            while (j>=l+1 && arr[j].compareTo(arr[l])>=0) {
                j--;
            }
            if (i>j) {
                break;
            }
            SortTestHelper.swap(arr, i++, j--);
        }
        SortTestHelper.swap(arr, l, j);
        return j;
    }
}

双路快排的实现方案,对于任意情况(完全随机,近乎有序,大量重复)的数据,都不会退化,其实就是通常说的快速排序算法。其实,对于有大量重复元素的测试用例,如果能将所有等于e的元素从待排序的区域中全部挑选出来,则可以进一步提高效率。
首先我们声明两个指针th、gt,分别指向小于e区域的后一个元素,arr[l+1,th)中的元素全部小于e;大于e区域的前一个元素,arr(gt,r]中的元素全部大于e。同时再声明一个指针i指向正在考察的元素,arr[th,i)中的元素全部等于e。指针i向前遍历,如果arr[i]<e,则交换th和i指向的元素,th++,此时i指向的元素等于e,那么i++;如果arr[i]>e,则交换i和gt指向的元素,此时i指向的元素依然是待考察的元素;如果arr[i]=e,则i++,即可将此元素纳入等于e的区域。循环结束后,则小于e和大于e两部分分别递归调用partition,直到待排序的数组大小为1,即r==l。

三路快排

在这里插入图片描述

public class QuickSort3Ways {
    private QuickSort3Ways(){}
    public static <T extends Comparable<T>> void sort(T[] arr) {
        int n = arr.length;
        partition(0, n-1, arr);
    }
    private static <T extends Comparable<T>> void partition(int l, int r, T[] arr) {
        if (l>=r) {
            return;
        }
        int randIndex = (int) (Math.random()*(r-l+1) + l);
        SortTestHelper.swap(arr, l, randIndex);
        int lt = l+1;
        int gt = r;
        int i=l+1;
        T v = arr[l];
        while (i<=gt && i>=lt && lt<=gt) {
            if (arr[i].compareTo(v)<0) {
                SortTestHelper.swap(arr, lt++, i++);
            } else if (arr[i].compareTo(v) > 0) {
                SortTestHelper.swap(arr, i, gt--);
            } else {
                i++;
            }
        }
        SortTestHelper.swap(arr, l, --lt);
        partition(l, lt-1, arr);
        partition(gt+1, r, arr);
    }
}
O(nlogn)算法的进一步优化

当我们讨论一个算法的时间复杂度时,一般使用大O表示法, 如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n),T[n] = O(f(n)),当输入量n逐渐加大时,时间复杂度的极限情形称为算法的“渐近时间复杂度”。也就是说我们讨论的时间复杂度O(n2)或者O(nlogn),都是基于n无穷大的假设,且忽略了低阶项和系数。
其实在n比较小时,一些O(n2)排序要快于O(nlogn)级别的排序。基于此,我们在递归过程中,对小数组使用O(n2)排序。
只需要将递归终止条件:

if (l>=r) {
	return;
}

改为:

if (r-l<16) {
    insertionSort(arr, l ,r);
    return;
}

其中insertionSort为:

private static void insertionSort(Comparable[] arr, int l, int r){
    for( int i = l + 1 ; i <= r ; i ++ ){
        Comparable e = arr[i];
        int j = i;
        for( ; j > l && arr[j-1].compareTo(e) > 0 ; j--)
            arr[j] = arr[j-1];
        arr[j] = e;
    }
}

通过改进,递归子过程中,如果待排序数组的元素数小于等于16个,那么就改用插入排序,以此获得更快的执行速度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章