前言
DualPivotQuicksort简单介绍
当前版本:1.8
DualPivotQuicksort是Arrays类中提供的给7种基本类型的数据排序的算法,虽然名字翻译是双轴快速排序,但其实是汇集了多种排序算法。这个类包含以下几种排序算法:
- 优化后的归并排序(Timsort)
- 插入排序,成对插入排序
- 单轴快速排序,双轴快速排序
- 计数排序
本篇内容
本篇只给出了整数数组的TimeSort部分的源码讲解
TimeSort前瞻
timesort 是一种改进版的归并排序,它旨在提高对部分有序数据的排序性能。以下是 timesort 和传统的归并排序之间的主要区别:
- 自适应性:
timesort具有自适应性,能够根据输入数据的性质动态地选择不同的排序策略。它会检查输入数据的局部有序性,并在发现数据部分有序时切换到插入排序。这使得对部分有序的数据进行排序时,timesort可以比传统的归并排序更快。 - Run 的概念:
timesort引入了run的概念,一个run是一个局部有序的子序列。timesort会首先将输入数组分割成多个runs,然后对这些runs进行合并排序。通过识别runs,timesort可以更好地利用输入数据的局部有序性。
源码
DualPivotQuicksort的TimeSort排序步骤,如对整数数组a[]{2,4,6,100,95,22,48,55}进行排序:
-
验证
a的长度,小于QUICKSORT_THRESHOLD不应该使用此算法; -
对
a进行有序分组,即从a的第一个元素开始往后逐个检查前两个元素(2,4)是升序的趋势,那么就按照升序的条件往后找,直到找到不满足条件(95)的元素,记录此元素索引,且这部分的元素集合被分为一个run,我们叫它run1(2,4,6,100)- 继续往后查找,
前两个元素(95,22)是降序趋势,那么就按照降序的条件往后查找,直到找到不满足条件(48)的元素,记录此元素索引,且这部分的元素集合被分为一个run,我们叫它run2(95,22),run2需要转换成升序的排序方式,即run2(22,95)
-
重复第二步,那么
a将被分成一堆run,当然run的数量也是有限制的,如果超过MAX_RUN_COUNT,那么则认为a属于杂乱无章类型的,那么应该使用其他的排序算法; -
最后每个
run都是以升序排列的,以两两合并run的方式快速排序。
以下为源码解读
package jdk.sort;
/**
* This class implements the Dual-Pivot Quicksort algorithm by
* Vladimir Yaroslavskiy, Jon Bentley, and Josh Bloch. The algorithm
* offers O(n log(n)) performance on many data sets that cause other
* quicksorts to degrade to quadratic performance, and is typically
* faster than traditional (one-pivot) Quicksort implementations.
*
* All exposed methods are package-private, designed to be invoked
* from public methods (in class Arrays) after performing any
* necessary array bounds checks and expanding parameters into the
* required forms.
*
* @author Vladimir Yaroslavskiy
* @author Jon Bentley
* @author Josh Bloch
*
* @version 2011.02.11 m765.827.12i:5\7pm
* @since 1.7
*/
final class DualPivotQuicksort {
/**
* Prevents instantiation.
* 防止类被实例化
* 因为排序操作通常是静态的,而不需要维护任何特定的状态。
* 在这种情况下,将类设计为 final 或者将构造函数私有化,以防止实例化,可以强调这一点,同时也可以防止无意中创建实例
*/
private DualPivotQuicksort() {}
/*
* Tuning parameters. 下边是一些被验证好的、最优的阈值
*/
/**
* The maximum number of runs in merge sort.
* 控制排序算法中的最大运行次数
* 当超过这个限制时,说明数组杂乱无章,不需要使用 `TimeSort` 归并排序
*/
private static final int MAX_RUN_COUNT = 67;
/**
* The maximum length of run in merge sort.
* 全都用在了 `相邻元素相等` 的情况, 当相等的相邻元素数量查过 `MAX_RUN_LENGTH` 后 直接使用 `快速排序`
*/
private static final int MAX_RUN_LENGTH = 33;
/**
* 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;
/**
* 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;
/**
* If the length of a byte array to be sorted is greater than this
* constant, counting sort is used in preference to insertion sort.
* 如果待排序的 `字节数组` 的长度大于这个常量,那么优先选择计数排序(counting sort),而不是插入排序(insertion sort)
*/
private static final int COUNTING_SORT_THRESHOLD_FOR_BYTE = 29;
/**
* If the length of a short or char array to be sorted is greater
* than this constant, counting sort is used in preference to Quicksort.
* 这个是 `short或char数组` 的启用 `计数排序` 的阈值
*/
private static final int COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR = 3200;
/*
* Sorting methods for seven primitive types.
* 以下是7种基本类型的排序方法
*/
/**
* 整数数组的排序,涉及快速排序、变种归并排序(TimeSort)等多种排序方法
*
* Sorts the specified range of the array using the given
* workspace array slice if possible for merging
*
* @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 work a workspace array (slice)
* 整数数组,用于存储在排序过程中需要进行合并的临时数据。它充当了合并排序中的辅助数组或缓冲区,用于在合并过程中存储中间结果。
* work 数组的大小通常等于排序范围的大小,也就是 right - left。在排序过程中,会在 work 和输入数组 a 之间进行数据交换和复制。
*
* @param workBase origin of usable space in work array
* 整数,表示 work 数组中的起始位置(偏移)。它用于确定从 work 数组的哪个位置开始存储数据。
* 通常,workBase 的值会设置为 0,因为我们希望从 work 数组的开头开始存储数据。但是,如果 work 数组被多次重复使用,可能会根据需要进行偏移,以避免覆盖之前存储的数据。
*
* @param workLen usable size of work array
* 这是一个整数,表示 work 数组的长度或大小。它表示在排序过程中可以使用多少空间来存储临时数据。
* 通常,workLen 的值等于 right - left,因为它需要足够的空间来容纳整个排序范围的数据。如果 work 数组的大小不足以存储整个排序范围,就需要重新分配更大的空间。
*/
static void sort(int[] a, int left, int right,
int[] work, int workBase, int workLen) {
// Use Quicksort on small arrays
// 对于小于286的数组使用 快速排序
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
/*
* Index run[i] is the start of i-th run
* (ascending or descending sequence).
* `run`是个数组,归并排序要将数组分成多个子数组 `count`表示第几次分组 `run[count]`则记录的是第count次分组的起始索引
* `count` 用于记录分组的数量,初始值为 0,run[0] 设置为 left,即数组的起始索引。
*/
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) {
// 首先检查当前元素 a[k] 与下一个元素 a[k + 1] 的大小关系,以确定运行的性质(升序、降序或相等)
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]);
// 用于反转降序运行中的元素,以确保它们升序排列
// 初始化一个指针 lo,它指向降序运行的前半部分的最后一个元素的索引。run[count] 是当前运行的起始索引,减去 1 就是前半部分的最后一个元素。
// 初始化另一个指针 hi,它指向降序运行的后半部分的第一个元素的索引。k 是当前正在检查的元素索引,它是运行的结束位置。
// ++lo < --hi:这是一个循环条件,表示只要 lo 小于 hi,就执行循环体中的操作。循环体中的操作会反复交换 lo 和 hi 指向的元素,直到 lo 不再小于 hi。
// 这个循环会反复执行,直到 lo 不再小于 hi。这个过程实际上是将降序运行的元素反转为升序,从而确保所有运行都是升序的
for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
}
} else { // equal 如果当前元素与下一个元素相等,说明当前运行包含相等的元素。进入相等的处理分支。
// 在相等的运行中,继续遍历数组,直到找到一个不相等的元素为止,或者达到最大运行长度 MAX_RUN_LENGTH。
for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
if (--m == 0) {
// 如果运行长度达到了 MAX_RUN_LENGTH,则说明数组中有大量相等的元素,为了避免低效的排序,会转而使用快速排序。
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
// Implementation note: variable "right" is increased by 1.
// 检查最后一个子数组是否只包含一个元素
if (run[count] == right++) { // The last run contains one element
run[++count] = right;
} else if (count == 1) { // The array is already sorted `count==1`表示只进行了一次排序,说明已经排好了
return;
}
// Determine alternation base for merge
byte odd = 0;
/**
* 在循环开始时,初始化整数变量 n 为 1
* (n <<= 1) < count:这个条件判断语句是循环的条件,它的作用是检查左移操作 n <<= 1 的结果是否小于 count。左移操作将 n 的二进制表示向左移动一位,相当于将其乘以 2。
* odd ^= 1:在每次迭代中,odd 变量的值会发生变化。它使用异或操作符 ^ 将 odd 的值与 1 进行异或运算,这会交替将 odd 的值在 0 和 1 之间切换。
* 最后的分号 ; 表示循环体为空,即不执行具体的操作,只在每次迭代中更新 n 和 odd 的值。
*
*
* count -> 2 -- odd -> 0
* count -> 3 -- odd -> 1
* count -> 4 -- odd -> 1
* count -> 5 -- odd -> 0
* count -> 6 -- odd -> 0
* count -> 7 -- odd -> 0
* count -> 8 -- odd -> 0
* count -> 9 -- odd -> 1
*
*/
for (int n = 1; (n <<= 1) < count; odd ^= 1);
// Use or create temporary array b for merging 使用或创建临时数组 b 以进行合并
int[] b; // temp array; alternates with a 声明一个整数数组 b,它将用作临时数组。在排序算法中,需要一个额外的数组来存储合并过程中的临时数据
int ao, bo; // array offsets from 'left' 声明两个整数变量 ao 和 bo,它们分别表示数组 a 和数组 b 的偏移量(offset)。这些偏移量用于确定在数组中的位置。
int blen = right - left; // space needed for b 计算变量 blen,它表示需要创建的临时数组 b 的长度。这个长度等于排序范围的大小,即 right - left,表示需要多少空间来存储合并的数据。
if (work == null || workLen < blen || workBase + blen > work.length) {
work = new int[blen];
workBase = 0;
}
// 决定a和b的指向,指向原a[]还是work[]
if (odd == 0) {
System.arraycopy(a, left, work, workBase, blen);
b = a;
bo = 0;
a = work;
ao = workBase - left;
} else {
b = work;
ao = 0;
bo = workBase - left;
}
// Merging 合并 用于多次执行合并操作,直到只剩下一个有序的数组
/**
* count 表示当前有多少个有序的子数组需要合并
* 每次循环都会将 count 更新为 last,last 在每次循环开始时被初始化为 0
*/
for (int last; count > 1; count = last) {
// 合并两个相邻 `run` 序列
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) {
// 逐个对比两个 `run` 中的数据进行排序
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;
}
}
}