DualPivotQuicksort 頂 轉

來自:http://lib.csdn.net/article/datastructure/9282?knId=815

DualPivotQuicksort是JDK1.7開始的採用的快速排序算法。

一般的快速排序採用一個樞軸來把一個數組劃分成兩半,然後遞歸之。

大量經驗數據表面,採用兩個樞軸來劃分成3份的算法更高效,這就是DualPivotQuicksort。

算法思想

選出兩個樞軸P1和P2,需要3個指針L,K,G。

3個指針的作用如下圖:

算法爲以下的步驟:

1、小於27的數組,使用插入排序(或47)。

2、選擇樞軸P1和P2。(假設使用數組頭和尾)。

3、P1需要小於P2,否者交換。

現在數組被分成4份,left到L的小於P1的數,L到K的大於P1小於P2的數,G到rigth的大於P2的數,待處理的K到G的中間的數(逐步被處理到前3個區域中)。

4、L從開始初始化直至不小於P1,K初始化爲L-1,G從結尾初始化直至不大於P2。K是主移動的指針,來一步一步吞噬中間區域。

****當大於P1小於P2,K++。

****當小於P1,交換L和K的數,L++,K++。

****當大於P2,如果G的數小於P1,把L上的數放在K上,把G的數放在L上,L++,再把K以前的數放在G上,G--,K++,完成一次L,K,G的互相交換。否則交換K和G,並G--,K++。

5、遞歸4。

6、交換P1到L-1上。交換P2到G+1上。

7、遞歸之。

JDK源碼 

流程圖:

TimSort

直接調用的sort第一級:

public static void sort(int[] a, int left, int right) {  
    // Use Quicksort on small arrays  
    if (right - left < QUICKSORT_THRESHOLD) {//門限爲286  
        sort(a, left, right, true);  
        return;  
    }  
  
    /* 
     * Index run[i] is the start of i-th run 
     * (ascending or descending sequence). 
     */  
    int[] run = new int[MAX_RUN_COUNT + 1];  
    int count = 0; run[0] = left;  
  
    // Check if the array is nearly sorted  
    for (int k = left; k < right; run[count] = k) {  
        if (a[k] < a[k + 1]) { // ascending  
            while (++k <= right && a[k - 1] <= a[k]);  
        } else if (a[k] > a[k + 1]) { // descending  
            while (++k <= right && a[k - 1] >= a[k]);  
            for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {  
                int t = a[lo]; a[lo] = a[hi]; a[hi] = t;  
            }  
        } else { // equal  
            for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {  
                if (--m == 0) {  
                    sort(a, left, right, true);  
                    return;  
                }  
            }  
        }  
  
        /* 
         * The array is not highly structured, 
         * use Quicksort instead of merge sort. 
         */  
        if (++count == MAX_RUN_COUNT) {  
            sort(a, left, right, true);  
            return;  
        }  
    }  
  
    // Check special cases  
    if (run[count] == right++) { // The last run contains one element  
        run[++count] = right;  
    } else if (count == 1) { // The array is already sorted  
        return;  
    }  
  
    /* 
     * Create temporary array, which is used for merging. 
     * Implementation note: variable "right" is increased by 1. 
     */  
    int[] b; byte odd = 0;  
    for (int n = 1; (n <<= 1) < count; odd ^= 1);  
  
    if (odd == 0) {  
        b = a; a = new int[b.length];  
        for (int i = left - 1; ++i < right; a[i] = b[i]);  
    } else {  
        b = new int[a.length];  
    }  
  
    // Merging  
    for (int last; count > 1; count = last) {  
        for (int k = (last = 0) + 2; k <= count; k += 2) {  
            int hi = run[k], mi = run[k - 1];  
            for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {  
                if (q >= hi || p < mi && a[p] <= a[q]) {  
                    b[i] = a[p++];  
                } else {  
                    b[i] = a[q++];  
                }  
            }  
            run[++last] = hi;  
        }  
        if ((count & 1) != 0) {  
            for (int i = right, lo = run[count - 1]; --i >= lo;  
                b[i] = a[i]  
            );  
            run[++last] = right;  
        }  
        int[] t = a; a = b; b = t;  
    }  
}  

1、當小於286時,直接使用雙樞軸快速排序。

2、大於286時,先使用TimSort的思想,找出正序或者倒序的數(run的棧),然後合併各個run,最後完成TimSort。

3、在找出run的時候,找出的run個數大於67時,即認爲它是一個比較亂序的數組,就直接採用雙樞軸快速排序。

 

雙元素插入排序

下面看看雙樞軸快速排序的實現(代碼太長,分解之):

/** 
 * Sorts the specified range of the array by Dual-Pivot Quicksort. 
 * 
 * @param a the array to be sorted 
 * @param left the index of the first element, inclusive, to be sorted 
 * @param right the index of the last element, inclusive, to be sorted 
 * @param leftmost indicates if this part is the leftmost in the range 
 */  
private static void sort(int[] a, int left, int right, boolean leftmost) {  
    int length = right - left + 1;  
  
    // Use insertion sort on tiny arrays  
    if (length < INSERTION_SORT_THRESHOLD) {//47個  
        if (leftmost) {  
            /* 
             * Traditional (without sentinel) insertion sort, 
             * optimized for server VM, is used in case of 
             * the leftmost part. 
             */  
            for (int i = left, j = i; i < right; j = ++i) {  
                int ai = a[i + 1];  
                while (ai < a[j]) {  
                    a[j + 1] = a[j];  
                    if (j-- == left) {  
                        break;  
                    }  
                }  
                a[j + 1] = ai;  
            }  
        } else {  
            /* 
             * Skip the longest ascending sequence. 
             */  
            do {  
                if (left >= right) {  
                    return;  
                }  
            } while (a[++left] >= a[left - 1]);  
  
            /* 
             * Every element from adjoining part plays the role 
             * of sentinel, therefore this allows us to avoid the 
             * left range check on each iteration. Moreover, we use 
             * the more optimized algorithm, so called pair insertion 
             * sort, which is faster (in the context of Quicksort) 
             * than traditional implementation of insertion sort. 
             */  
            for (int k = left; ++left <= right; k = ++left) {  
                int a1 = a[k], a2 = a[left];  
  
                if (a1 < a2) {  
                    a2 = a1; a1 = a[left];  
                }  
                while (a1 < a[--k]) {  
                    a[k + 2] = a[k];  
                }  
                a[++k + 1] = a1;  
  
                while (a2 < a[--k]) {  
                    a[k + 1] = a[k];  
                }  
                a[k + 1] = a2;  
            }  
            int last = a[right];  
  
            while (last < a[--right]) {  
                a[right + 1] = a[right];  
            }  
            a[right + 1] = last;  
        }  
        return;  
    }  

當小於47個時,使用插入排序。

參數a爲需要排序的數組,left代表需要排序的數組區間中最左邊元素的索引,right代表區間中最右邊元素的索引,leftmost代表該區間是否是數組中最左邊的區間。舉個例子:

  數組:[2, 4, 8, 5, 6, 3, 0, -3, 9]可以分成三個區間(2, 4, 8){5, 6}<3, 0, -3, 9>

  對於()區間,left=0, right=2, leftmost=true

  對於 {}區間, left=3, right=4, leftmost=false,同理可得<>區間的相應參數

  當區間長度小於47時,該方法會採用插入排序;否則採用快速排序。

1、當leftmost爲true時,它會採用傳統的插入排序(traditional insertion sort),代碼也較簡單,其過程類似打牌時抓牌插牌。

2、當leftmost爲false時,它採用一種新型的插入排序(pair insertion sort),改進之處在於每次遍歷前面已排好序的數組需要插入兩個元素,而傳統插入排序在遍歷過程中只需要爲一個元素找到合適的位置插入。對於插入排序來講,其關鍵在於爲待插入元素找到合適的插入位置,爲了找到這個位置,需要遍歷之前已經排好序的子數組,所以對於插入排序來講,整個排序過程中其遍歷的元素個數決定了它的性能。很顯然,每次遍歷插入兩個元素可以減少排序過程中遍歷的元素個數

爲左邊區間時,pair insertion sort在左邊元素比較大時,會越界。

 

雙樞軸快速排序

對於快速排序來講,其每一次遞歸所做的是使需要排序的子區間變得更加有序,而不是絕對有序;所以對於快速排序來說,其性能決定於每次遞歸操作使待排序子區間變得有序的程度,另一個決定因素當然就是遞歸次數。快速排序使子區間變得相對有序的關鍵是pivot,所以我們優化的方向也應該在於pivot,那就增加pivot的個數吧,而且我們可以發現,增加pivot的個數,對遞歸次數並不會有太大影響,有時甚至可以使遞歸次數減少。和insert sort類似的問題就是,pivot增加爲幾個呢?很顯然,pivot的值也不能太大;記住,任何優化都是有代價的,而增加pivot的代價就隱藏在每次交換元素的位置過程中。

下面是尋找樞軸的過程:

// Inexpensive approximation of length / 7,1/7=1/8+1/32  
int seventh = (length >> 3) + (length >> 6) + 1;  
  
/* 
 * Sort five evenly spaced elements around (and including) the 
 * center element in the range. These elements will be used for 
 * pivot selection as described below. The choice for spacing 
 * these elements was empirically determined to work well on 
 * a wide variety of inputs. 
 */  
int e3 = (left + right) >>> 1; // The midpoint  
int e2 = e3 - seventh;  
int e1 = e2 - seventh;  
int e4 = e3 + seventh;  
int e5 = e4 + seventh;  
  
// Sort these elements using insertion sort  
if (a[e2] < a[e1]) { int t = a[e2]; a[e2] = a[e1]; a[e1] = t; }  
  
if (a[e3] < a[e2]) { int t = a[e3]; a[e3] = a[e2]; a[e2] = t;  
    if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }  
}  
if (a[e4] < a[e3]) { int t = a[e4]; a[e4] = a[e3]; a[e3] = t;  
    if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;  
        if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }  
    }  
}  
if (a[e5] < a[e4]) { int t = a[e5]; a[e5] = a[e4]; a[e4] = t;  
    if (t < a[e3]) { a[e4] = a[e3]; a[e3] = t;  
        if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;  
            if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }  
        }  
    }  
}  
  
// Pointers  
int less  = left;  // The index of the first element of center part  
int great = right; // The index before the first element of right part  
  
if (a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]) {  
    /* 
     * Use the second and fourth of the five sorted elements as pivots. 
     * These values are inexpensive approximations of the first and 
     * second terciles of the array. Note that pivot1 <= pivot2. 
     */  
    int pivot1 = a[e2];  
    int pivot2 = a[e4];  
  
    /* 
     * The first and the last elements to be sorted are moved to the 
     * locations formerly occupied by the pivots. When partitioning 
     * is complete, the pivots are swapped back into their final 
     * positions, and excluded from subsequent sorting. 
     */  
    a[e2] = a[left];  
    a[e4] = a[right]; 

1. pivot的選取方式是將數組分成近視等長的七段,而這七段其實是被5個元素分開的,將這5個元素從小到大排序,取出第2個和第4個,分別作爲pivot1和pivot2。

注意:當這個5元素都互不相等時,才採用雙樞軸快速排序!

2. Pivot選取完之後,分別從左右兩端向中間遍歷,左邊遍歷停止的條件是遇到一個大於等於pivot1的值,並把那個位置標記爲less;右邊遍歷的停止條件是遇到一個小於等於pivot2的值,並把那個位置標記爲great

3. 然後從less位置向後遍歷,遍歷的位置用k表示,會遇到以下幾種情況:

  a. k位置的值比pivot1小,那就交換k位置和less位置的值,並是less的值加1;這樣就使得less位置左邊的值都小於pivot1,而less位置和k位置之間的值大於等於pivot1

  b. k位置的值大於pivot2,那就從great位置向左遍歷,遍歷停止條件是遇到一個小於等於pivot2的值,假如這個值小於pivot1,就把這個值寫到less位置,把less位置的值寫道k位置,把k位置的值寫道great位置,最後less++,great--;加入這個值大於等於pivot1,就交換k位置和great位置,之後great--。

4. 完成上述過程之後,帶排序的子區間就被分成了三段(pivot2),最後分別對這三段採用遞歸就行了。

/* 
  * Skip elements, which are less or greater than pivot values. 
  */  
 while (a[++less] < pivot1);  
 while (a[--great] > pivot2);  
  
 /* 
  * Partitioning: 
  * 
  *   left part           center part                   right part 
  * +--------------------------------------------------------------+ 
  * |  < pivot1  |  pivot1 <= && <= pivot2  |    ?    |  > pivot2  | 
  * +--------------------------------------------------------------+ 
  *               ^                          ^       ^ 
  *               |                          |       | 
  *              less                        k     great 
  * 
  * Invariants: 
  * 
  *              all in (left, less)   < pivot1 
  *    pivot1 <= all in [less, k)     <= pivot2 
  *              all in (great, right) > pivot2 
  * 
  * Pointer k is the first index of ?-part. 
  */  
 outer:  
 for (int k = less - 1; ++k <= great; ) {  
     int ak = a[k];  
     if (ak < pivot1) { // Move a[k] to left part  
         a[k] = a[less];  
         /* 
          * Here and below we use "a[i] = b; i++;" instead 
          * of "a[i++] = b;" due to performance issue. 
          */  
         a[less] = ak;  
         ++less;  
     } else if (ak > pivot2) { // Move a[k] to right part  
         while (a[great] > pivot2) {  
             if (great-- == k) {  
                 break outer;  
             }  
         }  
         if (a[great] < pivot1) { // a[great] <= pivot2  
             a[k] = a[less];  
             a[less] = a[great];  
             ++less;  
         } else { // pivot1 <= a[great] <= pivot2  
             a[k] = a[great];  
         }  
         /* 
          * Here and below we use "a[i] = b; i--;" instead 
          * of "a[i--] = b;" due to performance issue. 
          */  
         a[great] = ak;  
         --great;  
     }  
 }  
  
 // Swap pivots into their final positions  
 a[left]  = a[less  - 1]; a[less  - 1] = pivot1;  
 a[right] = a[great + 1]; a[great + 1] = pivot2;  
  
 // Sort left and right parts recursively, excluding known pivots  
 sort(a, left, less - 2, leftmost);  
 sort(a, great + 2, right, false);  

上述的代碼不包含遞歸中間的數,當中間的數過於多時,會作出下面舉動:

/* 
   * If center part is too large (comprises > 4/7 of the array), 
   * swap internal pivot values to ends. 
   */  
  if (less < e1 && e5 < great) {  
      /* 
       * Skip elements, which are equal to pivot values. 
       */  
      while (a[less] == pivot1) {  
          ++less;  
      }  
  
      while (a[great] == pivot2) {  
          --great;  
      }  
  
      /* 
       * Partitioning: 
       * 
       *   left part         center part                  right part 
       * +----------------------------------------------------------+ 
       * | == pivot1 |  pivot1 < && < pivot2  |    ?    | == pivot2 | 
       * +----------------------------------------------------------+ 
       *              ^                        ^       ^ 
       *              |                        |       | 
       *             less                      k     great 
       * 
       * Invariants: 
       * 
       *              all in (*,  less) == pivot1 
       *     pivot1 < all in [less,  k)  < pivot2 
       *              all in (great, *) == pivot2 
       * 
       * Pointer k is the first index of ?-part. 
       */  
      outer:  
      for (int k = less - 1; ++k <= great; ) {  
          int ak = a[k];  
          if (ak == pivot1) { // Move a[k] to left part  
              a[k] = a[less];  
              a[less] = ak;  
              ++less;  
          } else if (ak == pivot2) { // Move a[k] to right part  
              while (a[great] == pivot2) {  
                  if (great-- == k) {  
                      break outer;  
                  }  
              }  
              if (a[great] == pivot1) { // a[great] < pivot2  
                  a[k] = a[less];  
                  /* 
                   * Even though a[great] equals to pivot1, the 
                   * assignment a[less] = pivot1 may be incorrect, 
                   * if a[great] and pivot1 are floating-point zeros 
                   * of different signs. Therefore in float and 
                   * double sorting methods we have to use more 
                   * accurate assignment a[less] = a[great]. 
                   */  
                  a[less] = pivot1;  
                  ++less;  
              } else { // pivot1 < a[great] < pivot2  
                  a[k] = a[great];  
              }  
              a[great] = ak;  
              --great;  
          }  
      }  
  }  
  
  // Sort center part recursively  
  sort(a, less, great, false);  

就是當中間的數超過4/7的時候,按照劃分應該很平均纔對,所以猜想中間的元素有很多等於pivot1和pivot2的數(劃分的時候等於的數放在中間),會設法減少中間的數,就是把中間的等於pivot1的數放在前方,把等於pivot的數放在後方。

這個做法類似單樞軸快速排序的時候,作爲樞軸的元素也有很多相同的,所以在這個時候,應該跳過這些相同元素來進行快速排序。減少遞歸。

這個做法就相當於再次劃分中間的區域,相當於一共根據兩個樞軸劃分成了5種(多了兩種等於p1和p2的)。對其餘3種遞歸。

 

單樞軸快速排序

當5個元素有相當的時候,假定現在的情況是數組中有很多相同的元素。

} else { // Partitioning with one pivot  
    /* 
     * Use the third of the five sorted elements as pivot. 
     * This value is inexpensive approximation of the median. 
     */  
    int pivot = a[e3];  
  
    /* 
     * Partitioning degenerates to the traditional 3-way 
     * (or "Dutch National Flag") schema: 
     * 
     *   left part    center part              right part 
     * +-------------------------------------------------+ 
     * |  < pivot  |   == pivot   |     ?    |  > pivot  | 
     * +-------------------------------------------------+ 
     *              ^              ^        ^ 
     *              |              |        | 
     *             less            k      great 
     * 
     * Invariants: 
     * 
     *   all in (left, less)   < pivot 
     *   all in [less, k)     == pivot 
     *   all in (great, right) > pivot 
     * 
     * Pointer k is the first index of ?-part. 
     */  
    for (int k = less; k <= great; ++k) {  
        if (a[k] == pivot) {  
            continue;  
        }  
        int ak = a[k];  
        if (ak < pivot) { // Move a[k] to left part  
            a[k] = a[less];  
            a[less] = ak;  
            ++less;  
        } else { // a[k] > pivot - Move a[k] to right part  
            while (a[great] > pivot) {  
                --great;  
            }  
            if (a[great] < pivot) { // a[great] <= pivot  
                a[k] = a[less];  
                a[less] = a[great];  
                ++less;  
            } else { // a[great] == pivot  
                /* 
                 * Even though a[great] equals to pivot, the 
                 * assignment a[k] = pivot may be incorrect, 
                 * if a[great] and pivot are floating-point 
                 * zeros of different signs. Therefore in float 
                 * and double sorting methods we have to use 
                 * more accurate assignment a[k] = a[great]. 
                 */  
                a[k] = pivot;  
            }  
            a[great] = ak;  
            --great;  
        }  
    }  
  
    /* 
     * Sort left and right parts recursively. 
     * All elements from center part are equal 
     * and, therefore, already sorted. 
     */  
    sort(a, left, less - 1, leftmost);  
    sort(a, great + 1, right, false);  
}  

注意:這是個改進的單樞軸快速排序。這個時候也是3個指針的算法。因爲,這個算法就像是分成3類,一類小於樞軸,一類大於樞軸,一類等於樞軸。只用對左右兩種進行遞歸。

參考:http://blog.csdn.net/jy3161286/article/details/23361191?utm_source=tuicool

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