探索起因
这里讲一下我要去关注这块源码的起因,有助于激发兴趣。
这是我在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的,如果大于这个值呢?我们将如何进行排序呢? 我们下次再进行分析~~~
网友评论