剖析JDK8中Arrays.sort底層原理及其排序算法的選擇

寫這篇文章的初衷,是想寫篇Java和算法的實際應用,讓算法不再玄乎,而Arrays.sort是很好的切入點,即分析Java的底層原理,又能學習裏面的排序算法思想。希望能給在座各位在工作中或面試中一點幫助!轉載請註明出處:Michael孟良

點進sort方法:

 

      // Use Quicksort on small arrays
      if (right - left < QUICKSORT_THRESHOLD) {//QUICKSORT_THRESHOLD = 286
        sort(a, left, right, true);
        return;
      }

數組一進來,會碰到第一個閥值QUICKSORT_THRESHOLD(286),註解上說,小過這個閥值的進入Quicksort (快速排序),其實並不全是,點進去sort(a, left, right, true);方法:

 

// Use insertion sort on tiny arrays
    if (length < INSERTION_SORT_THRESHOLD) {
        if (leftmost) {
        ......

點進去後我們看到第二個閥值INSERTION_SORT_THRESHOLD(47),如果元素少於47這個閥值,就用插入排序,往下看確實如此:

 

            /*
             * 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;

元素少於47用插入排序

至於大過INSERTION_SORT_THRESHOLD(47)的,用一種快速排序的方法:
1.從數列中挑出五個元素,稱爲 “基準”(pivot);
2.重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作;
3.遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

 

快速排序(Quick Sort)

這是少於閥值QUICKSORT_THRESHOLD(286)的兩種情況,至於大於286的,它會進入歸併排序(Merge Sort),但在此之前,它有個小動作:

 

    // 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;
        }
    }

這裏主要作用是看他數組具不具備結構:實際邏輯是分組排序,每降序爲一個組,像1,9,8,7,6,8。9到6是降序,爲一個組,然後把降序的一組排成升序:1,6,7,8,9,8。然後最後的8後面繼續往後面找。。。

每遇到這樣一個降序組,++count,當count大於MAX_RUN_COUNT(67),被判斷爲這個數組不具備結構(也就是這數據時而升時而降),然後送給之前的sort(裏面的快速排序)的方法(The array is not highly structured,use Quicksort instead of merge sort.)。

如果count少於MAX_RUN_COUNT(67)的,說明這個數組還有點結構,就繼續往下走下面的歸併排序:

 

   // Determine alternation base for merge
    byte odd = 0;
    for (int n = 1; (n <<= 1) < count; odd ^= 1);

從這裏開始,正式進入歸併排序(Merge Sort)!

 

    // 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 + ao] <= a[q + ao]) {
                    b[i + bo] = a[p++ + ao];
                } else {
                    b[i + bo] = a[q++ + ao];
                }
            }
            run[++last] = hi;
        }
        if ((count & 1) != 0) {
            for (int i = right, lo = run[count - 1]; --i >= lo;
                b[i + bo] = a[i + ao]
            );
            run[++last] = right;
        }
        int[] t = a; a = b; b = t;
        int o = ao; ao = bo; bo = o;
    }

歸併排序(Merge Sort)

總結:
從上面分析,Arrays.sort並不是單一的排序,而是插入排序,快速排序,歸併排序三種排序的組合,爲此我畫了個流程圖:

 

原創圖,轉發請標明出處

算法的選擇:
PS:關於排序算法的文章,推薦這兩篇,個人覺得寫得挺好,容易入門:
https://mp.weixin.qq.com/s/t0dsJeN397wO41pwBWPeTg
https://www.cnblogs.com/huangbw/p/7398418.html

穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;

不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;

時間複雜度按n越大算法越複雜來排的話:常數階O(1)、對數階O(logn)、線性階O(n)、線性對數階O(nlogn)、平方階O(n²)、立方階O(n³)、……k次方階O(n的k次方)、指數階O(2的n次方)。

 

轉圖

O(nlogn)只代表增長量級,同一個量級前面的常數也可以不一樣,不同數量下面的實際運算時間也可以不一樣。數量非常小的情況下(就像上面說到的,少於47的),插入排序等可能會比快速排序更快。
所以數組少於47的會進入插入排序。

快排數據越無序越快(加入隨機化後基本不會退化),平均常數最小,不需要額外空間,不穩定排序。
歸排速度穩定,常數比快排略大,需要額外空間,穩定排序。
所以大於或等於47或少於286會進入快排,而在大於或等於286後,會有個小動作:“// Check if the array is nearly sorted”。這裏第一個作用是先梳理一下數據方便後續的歸併排序,第二個作用就是即便大於286,但在降序組太多的時候(被判斷爲沒有結構的數據,The array is not highly structured,use Quicksort instead of merge sort.),要轉回快速排序。


這就是jdk8中Arrays.sort的底層原理,自己在研究和分析中學到很多,希望能給各位工作中或面試中一些啓發和幫助!Thanks for watching!

 

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