Java中的排序

之前總結過排序的種種知識 java

那麼在Java中的Arrays.sort()是如何寫的呢? 算法

JDK5中的Arrays.sort(int[])

JDK5基本類型的排序是使用優化了的快速排序,咱們來看看JDK5中的優化點 數組

  1. /** 
  2.     * 將指定範圍的整形數組升序排序。 
  3.     * x[] 待排數組 
  4.     * off 從數組的第off個元素開始排序 
  5.     * len 數組長度 
  6.     */  

1. 在小規模(size<7)數組中,直接插入排序的效率要比快速排序高

// Insertion sort on smallest arrays
if (len < 7)
{
    for (int i = off; i < len + off; i++)
    for (int j = i; j > off && x[j - 1] > x[j]; j--)
            swap(x, j, j - 1);
    return;
		}

沒有一種排序在任何狀況下都是最優的。O(N^2)級別的排序看起來彷佛比全部先進排序要差的多。但實際上也並不是如此,Arrays中的sort()算法就給了咱們一個很好的例子。當待排數組規模很是小的時候(JDK中規模的閾值爲INSERTIONSORT_THRESHOLD=7),直接插入排序反而要比快排,歸併排序要好。 數據結構

這個道理很簡單。數組規模小,簡單算法的比較次數不會比先進算法多多少。相反,諸如快排,歸併排序等先進算法使用遞歸操做,所付出的運行代價更高。 dom

2. 精心選擇劃分元素,即pivot

// Choose a partition element, v
int m = off + (len >> 1); // Small arrays, middle element
if (len > 7)
{
    int l = off;
    int n = off + len - 1;
    if (len > 40)
    { // Big arrays, pseudomedian of 9
        int s = len / 8;
        l = med3(x, l, l + s, l + 2 * s);
        m = med3(x, m - s, m, m + s);
        n = med3(x, n - 2 * s, n - s, n);
    }
    m = med3(x, l, m, n); // Mid-size, med of 3
}
int v = x[m];

快排有一種最差的狀況,即蛻化成效率最差的冒泡排序。 緣由請查看這裏快排相關知識。 性能

既然如此,咱們能夠看看JDK5中的Arrays.sort()是如何爲咱們選擇pivot的。 優化

● 若是是小規模數組(size==7),直接取中間元素做爲pivot(<7時使用插入排序)。       ui

● 若是是中等規模數組(7<size<=40),則在數組首、中、尾三個位置上的數中取中間大小的數做爲pivot this

● 若是是大規模數組(size>40),則在9個指定的數中取一個僞中數(中間大小的數s)中小規模時,這種取法儘可能能夠避免數組的較小數或者較大數成爲樞軸。值得一提的是大規模的時候,首先在數組中尋找9個數據(能夠經過源代碼發現這9個數據的位置較爲平均的分佈在整個數組上);而後每3個數據找中位數;最後在3箇中位數上再找出一箇中位數做爲樞軸。 spa

這種精心選擇的樞軸,使得快排的最差狀況成爲了極小機率事件了。

代碼中的v就是最終選擇的pivot

3. 根據pivot v劃分,造成一個形如  (<v)*   v* (>v)* 的數組

// Establish Invariant: v* (<v)* (>v)* v*
int a = off, b = a, c = off + len - 1, d = c;
while (true)
{
    while (b <= c && x[b] <= v)
    {
        if (x[b] == v)
        swap(x, a++, b);
        b++;
    }
    while (c >= b && x[c] >= v)
    {
        if (x[c] == v)
        swap(x, c, d--);
        c--;
    }
    if (b > c)
        break;
    swap(x, b++, c--);
}

普通快排算法,都是使得pivot移動到數組的較中間位置。pivot以前的元素所有小於或等於pivot,以後的元素所有大於pivot。但與pivot相等的元素並不能移動到pivot附近位置。這一點在Arrays.sort()算法中有很大的優化。

解釋下上述代碼的變量的意思,a表明着左邊和pivot相等的數應該交換的位置,同理d表明着右邊和pivot相等的數應該交換的位置(因此最後和pivot相等的數字會集中在兩邊),b就是咱們一般快排中的i指針,c就是j指針。

咱們舉個例子來講明Arrays的優化細節  1五、9三、1五、4一、六、1五、2二、七、1五、20

第一次pivot:v=15

階段一,造成 v* (<v)* (>v)* v* 的數組:

                                          1五、1五、 七、六、 4一、20、2二、9三、 1五、15

咱們發現,與pivot相等的元素都移動到了數組的兩邊。而比pivot小的元素和比pivot大的元素也都區分開來了。

// Swap partition elements back to middle
    int s, n = off + len;
    s = Math.min(a - off, b - a);
    vecswap(x, off, b - s, s);
    s = Math.min(d - c, n - d - 1);
    vecswap(x, b, n - s, s);

階段二,將pivot和與pivot相等的元素交換到數組中間的位置上

                                          七、六、 1五、1五、 1五、1五、 4一、20、2二、93

// Recursively sort non-partition-elements
    if ((s = b - a) > 1)
        sort1(x, off, s);
    if ((s = d - c) > 1)
        sort1(x, n - s, s);

階段三,遞歸排序與pivot不相等都元素區間{七、6}和{4一、20、2二、93}

對於重複元素較多的數組,這種優化無疑能到達更好的效率。

JDK5中的Arrays.sort(Object[])

對象數組的排序,如Arrays.sort(Object[])等。採用了一種通過修改的歸併排序 。

  1.   /** 
  2.     * 將指定範圍的對象數組按天然順序升序排序。 
  3.     * src[] 原待排數組 
  4.     * dest[] 目的待排數組 
  5.     * low 待排數組的下界位置 
  6.     * high 待排數組的上界位置 
  7.     * off 從數組的第off個元素開始排序 
  8.     */  

1. 同上面的快速排序

// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
    for (int i=low; i<high; i++)
    for (int j=i; j>low &&((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
        swap(dest, j, j-1);
    return;
}
如同上面的快速排序同樣,當排序規模小於7時,插入排序的效率反而比歸併排序高,緣由同上。

2. 若是低子列表中的最高元素小於高子列表中的最低元素,則忽略合併

       // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;//不用去看off變量,off的做用是,排序部分的位置。
        high += off;
        int mid = (low + high) >> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);

        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }

        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }

這個優化措施無疑對基本有序序列是極大的效率改進。

這兩個優化都很簡單,實際上效率並未提升多少。因此在JDK7中將其替換爲TimSort

JDK7中的Arrays.sort(int[])

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

通常的快速排序採用一個樞軸來把一個數組劃分紅兩半,而後遞歸之。

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

DualPivotQuicksort流程圖:


接下來,咱們經過源碼一步步得查看jdk7中是如何作的。

// Use Quicksort on small arrays
        if (right - left < QUICKSORT_THRESHOLD) {
            sort(a, left, right, true);
            return;
        }
當數據規模小於286時,才使用快排,大於等於286時,將使用TimSort,這咱們接下來說,咱們先看看這裏的快排是如何寫的。

// Use insertion sort on tiny arrays
  if (length < INSERTION_SORT_THRESHOLD) {
           ...
  }
這JDK5同樣,當規模小於47時使用插入排序(JDK5中是小於7),緣由同JDK5中所說的。
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;
    }
}

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,同理可得<>區間的相應參數

當leftmost爲true時,它會採用傳統的插入排序(traditional insertion sort),代碼也較簡單,其過程解析查看這篇 blog中的插入排序章節。
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;
}
當leftmost爲false時,它採用一種新型的插入排序(pair insertion sort),改進之處在於每次遍歷前面已排好序的數組須要插入兩個元素,而傳統插入排序在遍歷過程當中只須要爲一個元素找到合適的位置插入。對於插入排序來說,其關鍵在於爲待插入元素找到合適的插入位置,爲了找到這個位置,須要遍歷以前已經排好序的子數組,因此對於插入排序來說,整個排序過程當中其遍歷的元素個數決定了它的性能。很顯然,每次遍歷插入兩個元素能夠減小排序過程當中遍歷的元素個數。
爲左邊區間時,pair insertion sort在左邊元素比較大時,會越界。


總結:

賽德維克在紅色的《算法》裏講過這樣一段話:

Java的標準庫應該是對抽象類型的數據結構使用歸併排序的一種變種,而對於基本類型採起三向切分的快排變種。

那麼爲何對於基本類型使用快排,而對於抽象類型則使用歸併呢?

1. 歸併排序穩定,快速排序不穩定。

對於對象排序而言,穩定性是很重要的,一個對象每每有多個屬性,假如兩個對象的compare值是對等的,可是排序事後相互順序卻變了,是看得出來的,並且在內存上是有意義的。

2. compare的成本

對象不能用><=符號去比較,要使用compare,equals比較,可是compareequals之類的操做成本有可能會很大,由於有時計算hashcode將會產生很大成本。因此在對象的排序中,移動的成本遠低於比較的成本。那麼在排序算法的選擇中,更加傾向於選擇比較次數較少的歸併排序。


參考資料:

1. http://hxraid.iteye.com/blog/665095

2. http://www.zhihu.com/question/24727766

3. http://blog.csdn.net/jy3161286/article/details/23361191?

相關文章
相關標籤/搜索