快速排序


  快速排序是應用最廣泛的排序算法,流行的原因是它實現簡單,適用於各種不同情況的輸入數據且在一般情況下比其他排序都快得多。

  快速排序是原地排序(只需要一個很小的輔助棧),將長度為 N 的數組排序所需的時間和 N lg N 成正比。

 

  1.算法

  快速排序也是一種分治的排序算法。它將一個數組分成兩個子數組,將兩部分獨立地排序。

  快速排序和歸並排序是互補:歸並排序是將數組分成兩個子數組分別排序,並將有序數組歸並,這樣數組就是有序的了;而快速排序將數組通過切分變成部分有序數組,然后拆成成兩個子數組,當兩個子數組都有序時整個數組也就有序了。

  歸並排序的遞歸調用發生在處理數組之前,快速排序的遞歸調用是發生在處理數組之后。

  

  快速排序中切分的位置取決於數組的內容。

  

    public class Quick: BaseSort
    {
        public new static long usedTimes = 0;
        public static void Sort(IComparable[] a)
        {
            usedTimes = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            Sort(a,0,a.Length-1);
            timer.Stop();
            usedTimes = timer.ElapsedMilliseconds;
        }

        public static void Sort(IComparable[] a, int lo, int hi)
        {
            if (hi <= lo)
                return;

            //切分
            int j = Partition(a,lo,hi);
            //Console.WriteLine(j);
            Sort(a,lo,j-1);
            Sort(a,j+1,hi);
        }
        public static int CompareCount = 0;
        public static int Partition(IComparable[] a, int lo, int hi)
        {
            int i = lo;
            int j = hi + 1;
            var v = a[lo];

            while (true)
            {
                //從左往右依次和 v 比較,直到找到 >= v 的值 索引i (索引i 左邊的值都小於切分元素)
                while (Less(a[++i], v))
                {
                    CompareCount++;
                    if (i == hi)
                        break;
                }

                //從右往左依次和 v 比較,直到找到 <= v 的值 索引j(索引j 右邊的值都大於於切分元素)
                while (Less(v, a[--j]))
                {
                    CompareCount++;
                    if (j == lo)
                        break;
                }

                
                //當 i >= j 時就找到了切分元素位置,位置為 j ,退出循環
                if (i >= j)
                    break;
                //如果 i 和 j 沒有相遇,將 i 和 j 的值交換,繼續循環,直到相遇
                Exch(a,i,j);
            }

            //將切分元素放到切分位置 j 
            Exch(a,lo,j);
            return j;
        }

    }

  該算法的關鍵在於切分 ,在 Sort(IComparable[] a, int lo, int hi) 方法中,切分后 a[lo] 到 a[j-1] 中的所有元素都不大於 a[ j ] ,a[j+1] 到 a[ hi ] 中的所有元素都不小於 a[ j ] ,然后對左子數組和右子數組進行遞歸。因為切分的過程總能排定一個元素,當切分到剩一個元素時,子數組就是有序的。當左數組和右數組都有序時,整個數組也就有序了。

  切分方法:先隨意取 a[ lo ] 作為切分元素,即那個將被排定的元素。然后從數組的左端向右掃描直到找到大於等於它的元素,再從數組右端向左開始掃描直到找到小於等於它的元素。如果找到的這兩個元素不是同一個元素(索引不同),那么這個元素就還沒被排定;如果相同意味着這個元素已被排定。如果沒被排定,就交換這兩個元素,繼續掃描,直到左右索引相遇,即可返回將切分元素和找到的索引位置的元素交換,返回索引 j 。

  

  排序實列:

  

   

  注意點

   1. 原地切分

  該算法是原地切分,如果使用輔助數組需要將切分后的數組復制回去的額外開銷。

 

  2.越界

  要防止掃描指針跑出數組邊界。

 

  3.保持隨機性

  該算法對所有子數組都是一視同仁的。

 

  4.終止循環

  該算法有三個循環都要注意什么時候終止。

 

  5.處理切分元素值有重復的情況

  該算法左右掃描都會在遇到相等值時停下來,盡管這樣會不必要的等值交換,但在某些情況下能夠避免算法的運行時間變為平方級別。

  

  6.終止遞歸

  任何遞歸調用都要先考慮什么時候終止。

 

  2.性能

     快速排序運行時間的增長量級為 NlogN 。

  快速排序切分方法的內循環用一個遞增的索引將數組元素和一個定值比較,這種循環很簡潔。它比歸並排序和希爾排序都快,因為歸並排序和希爾排序在內循環中移動元素。

  快速排序的另一個速度優勢在於它的比較次數很少(如果每次都對半分的話需要 N/2 * logN 次比較)。但是排序效率還是依賴切分數組的效果,而這依賴於切分元素的值 。切分一個較大的數據組,切分可能發生在任何一個位置。

  快速排序的最好情況是每次都能將數組對半分。在這種情況下快速排序所用的比較次數正好滿足分治遞歸的 C(N) = 2C(N/2) + N 公式。2C(N/2) 表示將兩個子數組排序的成本, N 表示 切分元素和所有數組元素比較的成本。 C(N)  ~ N log N 。平均而言切分元素都能落在數組中間。

  快速排序在最壞情況下需要 ~ N^2 / 2 次比較,即每次切分總有一個數組是空的(逆序),比較次數為: N + (N-1)+ (N-2) ...+2+1 = (N+1)N/2 。這種情況不僅算法所需的時間是平方級別的,所需的空間是線性的

  快速排序平均需要 ~ 2N lnN 次比較(以及1/6的交換)。歸並排序也可以做到這個量級,但是快速排序移動數據次數少(即交換次數),所以快速排序更快,盡管比較次數比歸並排序多了約 39%。

  當數組切分不平衡時(第一次用最小的切分,第二次用第二小的切分...)會倒置一個大數組需要切分很多次(上面說的逆序),所以非重復數組需要隨機打亂;當存在大量重復元素時,排序過程會進行很多次交換,重復數組可以使用稍后提到的三向切分的快速排序。

 

  3.改進

   1.切換到插入排序

   當將一個大數組切分成一定小的數組時使用插入排序給小數組排序,這樣就不需要繼續遞歸調用 Sort() 方法了。

  將 if (hi <= lo) return; 改為

    if(hi <= lo + M){ Insertion.Sort(a, lo, hi); return;}

  轉換參數 M 的最佳值和系統相關,5 ~ 15 最佳。

 

  2.三取樣切分  

   改進快速排序性能的另一個方法是使用子數組的一小部分元素的中位數來切分數組。這樣得到的切分更好,但是需要計算中位數。一般取樣大小為3並用大小居中的元素切分最好。

 

  3.熵最優排序

   實際應用中經常會出現大量重復元素的數組,這會影響快排的性能。例如,一個全部重復的子數組就不需要排序了,但上面的算法會繼續切分。三向切分的快速排序可以將有大量重復元素的數組從線性對數級別提高到線性級別。

  三向切分是將數組切分成小於,等於和大於三部分。它從左到右遍歷數組一次,維護一個指針 lt 使得 a[lo ... lt-1] 中的元素都小於 v(切分元素),一個指針 gt 使得 a[gt+1 ... hi] 中的元素都大於 v ,一個指針 i 使得 a[ lt ... i ] 中元素都等於 v , a[ i ... gt ] 中的元素都還未確定。一開始 lo 和 i 相等:

  • a[i]小於v與 a[i] 交換 a[lt並增加lt 和i
  • a[i]大於v:將 a[i] 與  a[gt] 交換並減少gt
  • a[i]等於v:遞增i

  

    public class Quick: BaseSort
    {
        public new static long usedTimes = 0;

        //三向切分
        public static void Sort3Way(IComparable[] a)
        {
            usedTimes = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            Sort3Way(a, 0, a.Length - 1);
            timer.Stop();
            usedTimes = timer.ElapsedMilliseconds;
        }
        public static void Sort3Way(IComparable[] a, int lo, int hi)
        {
            if (hi < lo)
                return;
            int lt = lo, i = lo + 1, gt = hi;
            IComparable v = a[lo];
            while (i <= gt)
            {
                int cmp = a[i].CompareTo(v);
                if (cmp < 0)
                    Exch(a, lt++, i++);
                else if (cmp > 0)
                    Exch(a, i, gt--);
                else
                    i++;

            }

            Sort3Way(a,lo,lt-1);
            Sort3Way(a,gt+1,hi);
        }
    }

  

 

  對於若干不同主鍵的隨機數組,歸並排序的時間復雜度是線性對數的,而三向切分快速排序是線性的。三向切分最壞的情況是所有鍵值都不同。當存在重復主鍵時,性能回避歸並排序好很多。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM