排序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個,那麼就改用插入排序,以此獲得更快的執行速度。

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