Java下的Arrays排序sort算法源码解析(上)

探索起因

这里讲一下我要去关注这块源码的起因,有助于激发兴趣。
这是我在leetcode练习算法时遇到的:

给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。

在解这题时,我想到的方式是排升序后,取偶数索引想加,可解,当时也没过多想,直接上手谢了一个冒泡排序,然后再取。但是提交的时候报出超出时间限制的错误,我当时还一度怀疑自己的解法是否有问题,直到我去看题解时,大多数人的想法跟我一样,只不过上面解法我是靠自己推测的,而别人是有严格的数据公式来证明的,那我就想为什么我的不对呢,之后看了他们的代码,他们的排序都不是自己手写的,都用了自带的工具类,之后我也尝试了下改为Arrays的sort排序方法,果然也通过了。之后我就对这个方法产生了兴趣,因为我知道冒泡排序算法的时间复杂度是n的平方,我想看看官方的排序算法是怎么实现的。

源码解析

之后我怀着好奇的心情点进了源码,我们首先进入的是:

public static void sort(int[] a) {
        DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
    }

在这我们可以知晓真正实现排序的类是DualPivotQuicksort,而且看名字好像是快排,那么我们继续深入。

小规模数组排序

终于我们来到了真正实现排序的地方,由于过程比较复杂,顾我们一部分一部分来看这内容,首先第一步:

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

我们可以看到,当数组长度小于QUICKSORT_THRESHOLD这个阈值时,我们将进入的排序算法是什么?首先首先这个阈值是多少:

   /**
     * If the length of an array to be sorted is less than this
     * constant, Quicksort is used in preference to merge sort.
     */
    private static final int QUICKSORT_THRESHOLD = 286;

我们看这个注释,看到使用的是归并排序,我们继续来看上面遗留的排序算法源码,这个方法也非常的长,我们来进入相关的源码,无关源码我们就先不看了:
首先是:

        int length = right - left + 1;

        // Use insertion sort on tiny arrays
        if (length < INSERTION_SORT_THRESHOLD) {
            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;
                }
            }

这里又出现了INSERTION_SORT_THRESHOLD这个值,我们来看下:

/**
     * If the length of an array to be sorted is less than this
     * constant, insertion sort is used in preference to Quicksort.
     */
    private static final int INSERTION_SORT_THRESHOLD = 47;

这里也说的非常清楚,如果数组长度小于这个值,我们就进行使用插入排序。

插入排序算法回顾

在看到这段的时候,我当初想直接跳过,因为我知道什么是插入排序,但是人往往是自以为是的,所以当我自己按照对这个算法的理解手写时,我没有写出来。

先讲一下我对这个算法的理解:

我个人认为插排和冒泡排序的动作是相反的,插排比较适合于原本顺序比较好的情况(这里有个专用词给忘了),它是从第一个开始,然后和当前之后的元素比较,如果顺序一样,则无需再做迭代,不然则从当前开始向上迭代比较。

这里还是举个理解比较好,也可以结合上面的源码来进行查看,假设我们给定数组[3,2,1],但是我们却是要升序排,这个时候其实性能最差,迭代次数最多。

i=0, j=0, ai=a[i+1]=2,a[j]=3,因为满足ai<a[j],顾a[j+1]=a[1]=3,然后j == 0,顾退出后,a[0] = 2,也就是a[0]和a[1]调换了顺序。

我们总结一下,就是说开始我们会拿a[i]与a[i+1]比,这时的a[i]肯定是比a[m] (m<i) 都要大的,如果是升序排序的话,如果a[i+1]大于a[i]的话,就没必要迭代了,因为比之前的数肯定都大,但是如果不是,则一级级的网上找,找到后进行调换位置。

不清楚大家有没有理解,我先贴出我按照这个思路写出的代码,和官网的不太一样,大家可以比较看看,我觉得我的比较容易理解:

for (int i = 0; i < nums.length - 1; i++) {
      int tmp = nums[i+1];
      int j = i;
      while (j >= 0){
        if (nums[j] > tmp ){
          nums[j+1] = nums[j];
          j--;
        }else {
          break;
        }
      }
      nums[j+1] =tmp;
    }

OK,到这我们把第一个算法解决了,下面的那个算法就比较有难度了。刚开始我只知道是快排,但是没想到快排中的学问那么大,这里首先建议阅读单轴快排(SinglePivotQuickSort)和双轴快排(DualPivotQuickSort)及其JAVA实现

再次强调,如果对快排没有了解过的话,强力建议先看上述文章了解其内容,写的非常好。OK,了解之后我们再来看在Arrays.sort中的快排实现。

快排实现

这里我们引用上面文章的一段话,讲述这个算法的思想:

双轴快速排序,顾名思义,取两个中心点pivot1,pivot2,且pivot≤pivot2,可将序列分成三段:x<pivot1、pivot1≤x≤pivot2,x<pivot2,然后分别对三段进行递归。
既然要两个中心点,我们一般将第一个元素和最后一个元素作为两个中心点。实现大致过程如下:
1.初始化时,i=start,j=end,k=start+1,k负责扫描。序列第一个值大于序列最后一个值,需要进行交换。然后pivot1=items[start],pivot2=items[end]。
2.扫描过程中保持:1~i是小于pivot1的元素,i~k是大于等于pivot1、小于等于pivot2的元素,j~end-1是大于pivot2的元素。

上面是大致介绍了双轴快排的基本思想,而java的在一定基础上又做了优化。我们一步步来深入,首先看上述思想我们知道,第一步其实要做的就是选2个中心点pivot1,pivot2,java在这一步中的优化我们来看下:

        // Inexpensive approximation of length / 7
        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
  • 首先要确认,能到这一步的说明数组长度是大于INSERTION_SORT_THRESHOLD的长度也就是47的,所以int seventh = (length >> 3) + (length >> 6) + 1; 这步说明数组长度至少要7以上。

  • 接下来是5分法,通过e3为数组中间值,在根据seventh值的加减得到

  • 下面这块if区域可以看到我们刚开始基本没用,后面也是对e1到e5进行排序

下面又是一个关键点:

 if (a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]) {

如果值都不相同的情况下我们进行双轴快排,如果不满足我们来看下

else { // Partitioning with one pivot

我们就会实现用单轴快排

我们先来讲双轴。

快排之双轴快排

我们先截下来所有代码,避免读者思路断续

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

            /*
             * 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);
  • 第一步我们确定了pivot1和pivot2点,并把它放在初始两端
  • 第二步开始扫描,在less上从左到右扫描索引值是否小于pivot1,直到遇到大于等于pivot1值暂停。而great则从右到左相反。
  • 第三步也就正在到了双轴快排的地方,我们是通过移动指针k来进行的
  • 第四步更换中心店位置,并进行双边排序,这里的排序用的是插入排序
  • 第五步,如果中心值太多,则继续使用快排来进行排序,最后还是使用插入排序

总结:这里的思路都可以离清楚了,但是如果让我自己手写可能有点够呛。不过目前我还是以理解为主。

下面开始介绍单轴快排

快排之单轴快排

我们继续来进行,这里老样子我们直接贴上所有代码:

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

这里经过上面双轴排序的逻辑,这里就松松然了,比较的简单,正常的三分双向扫描算法。

这里我们发现了一种情况,也就是如果在插排中我们传入的那个boolean值是false的情况是怎样的,我们下面的补充。

插排的另外一种情况

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

这里使用的是一种新型的pair insertion sort 结对插入算法,在原本插入算法的基础上一次操作两个元素。在网上搜了下没有很好的文档说明,这里我按照自己读的理解来进行说明。

  • 首先这个算法只适用于数组中部分排序,并且排序部分的开头不能是原数组的开头而且数组前面部分要已经排序好了。
  • 其次在这的a1和a2,因为我们是升序排序的,这里一开始的操作让我有点雾水,这步是干嘛的?后面我想了下,如果不进行这不排序,在扫描是a2是小于a1的,那么a2就会直接插入到a1前面就不进行继续扫描了,这部分应该是为了避免这步。
  • 其他的我感觉就是和普通插排差不多,当然里面的细节我没有过多关注。

今日总结

今天的收货还是非常满的,我们刚开始回顾了最原始的普通的插入排序,之后又经历的快排的双轴快排和单轴快排,最后还经历了一个新型的,建立在特殊数组排序的算法,结对插入排序。
当然,我们也要注意,目前这部分的排序只是针对于数组长度是小于QUICKSORT_THRESHOLD数值286的,如果大于这个值呢?我们将如何进行排序呢? 我们下次再进行分析~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容