对比常见排序算法,从DualPivotQuicksort学习高性能排序算法

2,209 阅读18分钟

Comparable

注释

该接口对实现它的每个类的对象进行了总排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。

实现此接口的对象列表(和数组)可以通过 Collections.sort(List list) 和 Arrays.sort() 自动排序

Collections.sort(List list)

定义

public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
}

这里实际调用 List 类的 sort()

sort(Comparator<? super E> c)

定义

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

List 的子类会实现 listIterator() 方法:

  • ArrayList 类的实现

    image-20220310160919492.png

    • ListItr

      image-20220310161113913.png

      • set() image-20220310161654121.png
        • 实际调用 ArrayList 的 set() 方法设置值
    • Itr

      • next() image-20220310161325976.png
        • cursor 用来记录下一个要遍历的下标
        • lastRet 用来记录当前下标
  • LinkedList 类的实现

    image-20220310163007109.png

    • ListItr

      image-20220310163048646.png

      • next()

        image-20220310163305649.png

        • next 为下一节点,nextIndex 为下一节点的下标
        • lastReturned 为当前节点
      • set()

        • image-20220310163532105.png 排序实际由 Arrays.sort() 方法实现:
Arrays.sort()

有多个重载的方法

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

可以看到,这些方法实际调用 DualPivotQuicksort 里的方法实现排序的功能。

image-20220311094741086.png 涉及到 插入快速归并计数 排序。

插入排序

按照升序排列

算法描述

  1. 从第一个元素开始,该元素可以认为已经被排序

  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描 Insertionsort-before.png

  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置

  5. 将新元素插入到该位置后

    Insertionsort-after.png

  6. 重复步骤 2~5

复杂度

描述
平均时间复杂度O(n^2)
最坏时间复杂度序列是降序排列O(n^2)
最优时间复杂度序列是升序排列O(n)
空间复杂度O(1)

算法特点

对于部分有序的数组十分高效,也很适合小规模数组。如果数组中倒置的数量小于数组大小的某个倍数,那么我们说这个数组是部分有序的。倒置是指数组中的两个顺序颠倒的元素,比如 EXAMPLE 中有 11 对倒置:E-A, X-A, X-M, X-P, X-L, X-E, M-L, M-E, P-L, P-E 以及 L-E。下面是几种典型的部分有序的数组:

  • 数组中每个元素距离它的最终位置都不远
  • 一个有序的大数组接一个小数组
  • 数组中只有几个元素的位置不正确

插入排序需要的交换次数和数组中的倒置的数量相同。需要的比较次数大于等于倒置数量,小于等于倒置的数量加上数组的大小再减一

算法实现

image-20220315190558068.png

private int[] insertSort(int[] array) {
    int j, temp;
    int size = array.length;
​
    for (int i = 1; i < size; i++) {
        temp = array[i];
        for (j = i - 1; j >= 0 && array[j] > temp; j--) {
            array[j + 1] = array[j];
        }
        array[j + 1] = temp;
    }
    return array;
}

插入排序和选择排序非常相像,下面讲一下选择排序

DualPivotQuicksort 的实现

if (length < INSERTION_SORT_THRESHOLD) {
    // leftmost 指定的范围是否在数组的最左边
    if (leftmost) {
        // 在最左边的部分使用针对服务器 VM 优化的传统(无标记)插入排序
        for (int i = left, j = i; i < right; j = ++i) {
            // 保存要插入的值到 ai
            int ai = a[i + 1];
            while (ai < a[j]) {
                a[j + 1] = a[j];
                if (j-- == left) {
                    break;
                }
            }
            a[j + 1] = ai;
        }
    } else {
        // 跳过最长的升序序列
        do {
            if (left >= right) {
                return;
            }
        } while (a[++left] >= a[left - 1]);
​
        // 这里使用了 pair insertion sort 算法,在下面详细介绍
        for (int k = left; ++left <= right; k = ++left) {
            int a1 = a[k], a2 = a[left];
​
            // 确保 a1 大于 a2
            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 (结伴插入排序) 的伪代码参考:formal.kastel.kit.edu/ulbrich/ver…,并据此写出 Java 版本:

private void myInsertSortNotLeftMost(int[] a) {
    int i = 0;
    while (i < a.length - 1) {
        // 让 x 和 y 保持 a 中相邻的元素
        int x = a[i];
        int y = a[i + 1];
​
        // 确保 x 大于 y
        if (x < y) {
            int temp = x;
            x = y;
            y = temp;
        }
​
        // j 是用于查找插入点的索引
        int j = i - 1;
        // 找到 x 的插入点
        while (j >= 0 && a[j] > x) {
            // 将现有内容移动 2
            a[j + 2] = a[j];
            j = j - 1;
        }
        // 将 x 存储在其插入位置
        a[j + 2] = x;
        // a[j + 1] 现在是一个可用的空间
​
        // 找到 y 的插入点
        while (j >= 0 && a[j] > y) {
            // 将现有内容移动 1
            a[j + 1] = a[j];
            j = j -1;
        }
        // 将 y 存储在其插入位置
        a[j + 1] = y;
​
        i = i + 2;
    }
​
    // 如果数组长度是奇数,最后一个元素还需要进行一次插入。之所以要做这个步骤,是因为结伴插入的结尾每次都是下标后移两位,数组元素个数只有偶数个时才能把全部元素遍历一次
    if (i == a.length - 1) {
        int y = a[i];
        int j = i - 1;
        while (j >= 0 && a[j] > y) {
            a[j + 1] = a[j];
            j = j - 1;
        }
        a[j + 1] = y;
    }
    System.out.println(Arrays.toString(a));
}

对比结伴插入排序的两段代码块和传统的插入排序的代码的逻辑,发现十分相似。

image-20220311160155431.png

因此总结出结伴插入的算法描述:

  1. 将 a[i] 和 a[i + 1] 中的较大值存到 x,较小值存到 y
  2. 取出 a[i] 的上一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于 x,将该元素移到下 2 个位置
  4. 重复步骤 3,直到找到已排序的元素小于或者等于 x 的位置 (j + 2)
  5. 如果元素 a[j] 大于 y,则该元素移到下 1 个位置
  6. 重复步骤 5,直到找到已排序的元素小于或者等于 y 的位置
  7. 下标后移 2 位,重复步骤 1,直到数组遍历完毕
  8. 若数组长度长度为奇数,最后一个元素还需要进行一次插入

对比我写的 Java 版和 JDK 的实现,还需要在我写的基础上优化一下才能变成 JDK 那样的:

结伴插入转化.png

选择排序

按照升序排列

算法描述

  1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
  3. 重复步骤 2

复杂度

描述
平均时间复杂度O(n^2)
最坏时间复杂度序列逆序O(n^2)
最优时间复杂度序列已经有序O(n^2)
空间复杂度O(1)

算法特点

运行时间与输入无关,数据移动是最少的

算法实现

image-20220315190533867.png

private int[] selectionSort(int[] array) {
    int min, temp;
    int size = array.length;
​
    for (int i = 0; i < size; i++) {
        min = i;
        for (int j = i + 1; j < size; j++) {
            if (array[j] < array[min]) {
                min = j;
            }
        }
        if (min != i) {
            temp = array[i];
            array[i] = array[min];
            array[min] = temp;
        }
    }
​
    return array;
}

插入排序、结伴插入排序和选择排序的性能比较 image-20220316214517658.png

希尔排序

复杂度

平均时间复杂度根据步长序列的不同而不同
最坏时间复杂度根据步长序列的不同而不同。 已知最好的:O(n log^2 n)
最优时间复杂度O(n)
空间复杂度O(1)

算法特点

希尔排序比插入排序和选择排序要快得多,并且数组越大,优势越大。运行时间达不到平方级别

算法实现

image-20220316163307725.png

public static void shellSort(int[] arr) {
    int length = arr.length;
    int temp;
    for (int step = length / 2; step >= 1; step /= 2) {
        for (int i = step; i < length; i++) {
            temp = arr[i];
            int j = i - step;
            while (j >= 0 && arr[j] > temp) {
                arr[j + step] = arr[j];
                j -= step;
            }
            arr[j + step] = temp;
        }
    }
}

插入排序、选择排序和希尔排序性能比较

image-20220316224733467.png

快速排序

按照升序排列

算法描述

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot)
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序

复杂度

描述
平均时间复杂度O(nlogn)
最坏时间复杂度序列逆序O(n^2)
最优时间复杂度序列已经有序O(nlogn)
空间复杂度O(logn)

算法特点

通常是实际排序应用中最好的选择,因为平均性能非常好:它的期望时间复杂度是 O(n lgn),而且 O(n lgn) 中隐含的常数因子非常小。

算法实现

private int[] quickSort(int[] arr, int left, int right) {
    if (left < right) {
        int p = partition(arr, left, right);
        quickSort(arr, left, p - 1);
        quickSort(arr, p + 1, right);
    }
    return arr;
}
​
private void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

Lomuto 分区方案

这个方案是在《算法导论》里介绍的,其效率其实比 Hoare 分区方案低 image-20220315225547013.png

private int partition(int[] arr, int left, int right) {
    int pivot = arr[right];
    int i = left;
    for (int j = left; j < right; j++) {
        if (arr[j] <= pivot) {
            swap(arr, j, i);
            i++;
        }
    }
    swap(arr, i, right);
    return i;
}

Hoare 分区方案

image-20220315225713077.png

private static int partition(int[] arr, int left, int right) {
        int pivot = arr[left];
        while (left < right) {
            while (left < right && arr[right] >= pivot) {
                right--;
            }
            arr[left] = arr[right];
            while (left < right && arr[left] <= pivot) {
                left++;
            }
            arr[right] = arr[left];
        }
        arr[left] = pivot;
        return left;
    }

算法优化

一般来说,使用以下改进措施可以将快速排序的性能提升 20% ~ 30%

切换到插入排序
  • 对于小数组,快速排序比插入排序慢
  • 因为递归,快速排序的 sort() 方法在小数组也会调用自己

三取样切分

《算法》里面提到:“使用子数组的一小部分元素的中位数来切分数组,这样做得切分更好,但是代价是需要计算中位数。人们发现取样大小设为 3 并用大小居中的效果最好。”

三切分:

// 给 array[low]、array[middle] 和 array[high] 排序,然后交换 array[low] 和 array[middle] 的值
int middle = low + (high - low) / 2;
​
if (ArrayUtil.less(array[middle], array[low])) {
    ArrayUtil.exchange(array, middle, low);
}
if (ArrayUtil.less(array[high], array[low])) {
    ArrayUtil.exchange(array, high, low);
}
if (ArrayUtil.less(array[high], array[middle])) {
    ArrayUtil.exchange(array, high, middle);
}
​
//Swap median with low
ArrayUtil.exchange(array, middle, low);
Comparable pivot = array[low];

五切分(五个随机数):

if (high - low + 1 < 5) {
    return;
}
​
int randomIndex1 = StdRandom.uniform(low, high + 1);
int randomIndex2 = StdRandom.uniform(low, high + 1);
int randomIndex3 = StdRandom.uniform(low, high + 1);
int randomIndex4 = StdRandom.uniform(low, high + 1);
int randomIndex5 = StdRandom.uniform(low, high + 1);
​
medianOf5Array[0] = array[randomIndex1];
medianOf5Array[1] = array[randomIndex2];
medianOf5Array[2] = array[randomIndex3];
medianOf5Array[3] = array[randomIndex4];
medianOf5Array[4] = array[randomIndex5];
​
Map<Comparable, Integer> originalIndexes = new HashMap<>();
originalIndexes.put(medianOf5Array[0], randomIndex1);
originalIndexes.put(medianOf5Array[1], randomIndex2);
originalIndexes.put(medianOf5Array[2], randomIndex3);
originalIndexes.put(medianOf5Array[3], randomIndex4);
originalIndexes.put(medianOf5Array[4], randomIndex5);
​
Comparable median;
​
//1st compare
if (!ArrayUtil.less(medianOf5Array[0], medianOf5Array[1])) {
    ArrayUtil.exchange(medianOf5Array, 0, 1);
}
//2nd compare
if (!ArrayUtil.less(medianOf5Array[3], medianOf5Array[4])) {
    ArrayUtil.exchange(medianOf5Array, 3, 4);
}
//3rd compare
if (!ArrayUtil.less(medianOf5Array[0], medianOf5Array[3])) {
    ArrayUtil.exchange(medianOf5Array, 0, 3);
}
​
//4th compare
if (ArrayUtil.less(medianOf5Array[1], medianOf5Array[2])) {
    //5th compare
    if (ArrayUtil.less(medianOf5Array[1], medianOf5Array[3])) {
        //6th compare
        if (ArrayUtil.less(medianOf5Array[2], medianOf5Array[3])) {
            median = medianOf5Array[2];
        } else {
            median = medianOf5Array[3];
        }
    } else {
        //6th compare
        if (ArrayUtil.less(medianOf5Array[1], medianOf5Array[4])) {
            median = medianOf5Array[1];
        } else {
            median = medianOf5Array[4];
        }
    }
} else {
    //5th compare
    if (ArrayUtil.less(medianOf5Array[3], medianOf5Array[2])) {
        //6th compare
        if (ArrayUtil.less(medianOf5Array[2], medianOf5Array[4])) {
            median = medianOf5Array[2];
        } else {
            median = medianOf5Array[4];
        }
    } else {
        //6th compare
        if (ArrayUtil.less(medianOf5Array[1], medianOf5Array[3])) {
            median = medianOf5Array[1];
        } else {
            median = medianOf5Array[3];
        }
    }
}
​
int originalMedianIndex = originalIndexes.get(median);
​
//Swap median with low
ArrayUtil.exchange(array, originalMedianIndex, low);

以下是我实测的数据: 随机无序数组

image-20220316114720909.png

完全有序的序列

image-20220316113742053.png

可见三取样切分取轴可以比取第一个元素为轴更快。而五取样切分取轴比取第一个元素还慢,可能是我的代码效率不够高,这里就不深入探究了。

熵最优的排序

三向切分的快速排序

算法特点:

三向切分的最坏情况是所有主键均不相同。当存在重复主键时,它的性能就会比归并排序好得多。

是排序库函数的最佳算法选择。

算法描述:

指针 lo 指向数组最左的元素,hi 指向数组最右的元素,lt 使得 a[lo .. lt - 1] 中的元素都小于 v,gt 使得 a[gt + 1 .. hi] 中的元素都大于 v,i 使得 a[lt .. i - 1] 中的元素都等于 v,a[i .. gt] 中的元素还未确定。

  • a[i] 小于 v,将 a[lt] 和 a[i] 交换,将 lt 和 i 加一
  • a[i] 大于 v,将 a[gt] 和 a[i] 交换,将 gt 减一
  • a[i] 等于 v,将 i 加一

算法实现: image-20220316132220073.png

private static final Random RANDOM = new Random();
​
private void quickSort(int[] nums, int left, int right) {
    int pIndex = partition(nums, left, right);
    quickSort(nums, left, pIndex - 1);
    quickSort(nums, pIndex + 1, right);
}
​
private int partition(int[] nums, int left, int right) {
    int randomIndex = left + RANDOM.nextInt(right - left + 1);
    swap(nums, randomIndex, left);
​
    int pivot = nums[left];
    int lt = left + 1;
    int gt = right;
​
    while (true) {
        while (lt <= right && nums[lt] < pivot) {
            lt++;
        }
​
        while (gt > left && nums[gt] > pivot) {
            gt--;
        }
​
        if (lt >= gt) {
            break;
        }
​
        swap(nums, lt, gt);
        lt++;
        gt--;
    }
    swap(nums, left, gt);
    return gt;
}
​
private void swap(int[] nums, int index1, int index2) {
    int temp = nums[index1];
    nums[index1] = nums[index2];
    nums[index2] = temp;
}

DualPivotQuicksort 的实现

image-20220312181237333.png 此数组为 T[] aT 是指基本数据类型(如 int, float, byte, char, double, long, short),两个轴 P1P2,三个指针 LKGleft 指向数组最左边的元素,right 指向数组最右边的元素。

算法描述

  1. 对于长度小于 17 的数组,使用插入排序

  2. 选择两个数组元素当作轴,比如我们可以把数组第一个元素 a[left] 当作 P1,最后一个元素 a[right] 当作 P2

  3. P1 必须小于 P2,否则互换两者的值。现在数组被分为以下几个部分:

    • part I 的下标从 left + 1 到 L - 1,元素大小 < P1
    • part I的下标从 LK - 1,P1 <= 元素大小 <= P2
    • part III 的下标从 G + 1 到 right - 1,元素大小 < P2
    • part IV 的下标从 KG,包含要检查的其他元素,即这一 part 存放还未排序的元素
  4. part IV 中下一个要被排序的元素 a [ k ] 与两个轴 P1P2 进行比较,并放置到相应的 part IIIIII

  5. 指针 LKG 在相应的方向上发生变化

  6. K <= G 时,重复步骤 4-5

  7. P1part I 的最后一个元素交换,P3part III 的最后一个元素交换

  8. part Ipart IIpart III 中递归重复步骤 1-7

算法特点

比传统的快速排序(单轴)更快。实验证明了轴越多,排序速度越快,且在快速排序算法中实现更多支点所带来的速度增长趋于缓慢减少:iopscience.iop.org/article/10.…epubs.siam.org/doi/pdf/10.…

image-20220312175909417.png

算法实现

/**
* @author Vladimir Yaroslavskiy
* @version 2009.09.17 m765.817
*/private static final int DIST_SIZE = 13;
private static final int TINY_SIZE = 17;
​
private static void dualPivotQuicksort(int[] a, int left, int right) {
    int len = right - left;
    int x;
    // 属于切换到插入排序
    if (len < TINY_SIZE) {
        for (int i = left + 1; i <= right; i++) {
            for (int j = i; j > left && a[j] < a[j - 1]; j--) {
                x = a[j - 1];
                a[j - 1] = a[j];
                a[j] = x;
            }
        }
        return;
    }
    // 属于五取样切分
    int sixth = len / 6;
    int m1 = left + sixth;
    int m2 = m1 + sixth;
    int m3 = m2 + sixth;
    int m4 = m3 + sixth;
    int m5 = m4 + sixth;
    // 5-element sorting network
    if (a[m1] > a[m2]) { x = a[m1]; a[m1] = a[m2]; a[m2] = x; }
    if (a[m4] > a[m5]) { x = a[m4]; a[m4] = a[m5]; a[m5] = x; }
    if (a[m1] > a[m3]) { x = a[m1]; a[m1] = a[m3]; a[m3] = x; }
    if (a[m2] > a[m3]) { x = a[m2]; a[m2] = a[m3]; a[m3] = x; }
    if (a[m1] > a[m4]) { x = a[m1]; a[m1] = a[m4]; a[m4] = x; }
    if (a[m3] > a[m4]) { x = a[m3]; a[m3] = a[m4]; a[m4] = x; }
    if (a[m2] > a[m5]) { x = a[m2]; a[m2] = a[m5]; a[m5] = x; }
    if (a[m2] > a[m3]) { x = a[m2]; a[m2] = a[m3]; a[m3] = x; }
    if (a[m4] > a[m5]) { x = a[m4]; a[m4] = a[m5]; a[m5] = x; }
    // pivots: [ < pivot1 | pivot1 <= && <= pivot2 | > pivot2 ]
    int pivot1 = a[m2];
    int pivot2 = a[m4];
    boolean diffPivots = pivot1 != pivot2;
    a[m2] = a[left];
    a[m4] = a[right];
    // center part pointers
    int less = left + 1;
    int great = right - 1;
    // 三向切分快速排序
    if (diffPivots) { // 先处理两边 < pivot1 和 x > pivot2 的元素,遇到 == pivot1 或 == pivot2 则 less 不 ++ 或 great 不 --
        for (int k = less; k <= great; k++) {
            x = a[k];
            if (x < pivot1) {
                a[k] = a[less];
                a[less++] = x;
            }
            else if (x > pivot2) {
                while (a[great] > pivot2 && k < great) {
                    great--;
                }
                a[k] = a[great];
                a[great--] = x;
                x = a[k];
                if (x < pivot1) {
                    a[k] = a[less];
                    a[less++] = x;
                }
            }
        }
    }
    else { // 两个轴的值相同
        for (int k = less; k <= great; k++) {
            x = a[k];
            if (x == pivot1) {
                continue;
            }
            if (x < pivot1) {
                a[k] = a[less];
                a[less++] = x;
            }
            else { // x > pivot1,pivot2
                while (a[great] > pivot2 && k < great) {
                    great--;
                }
                a[k] = a[great];
                a[great--] = x;
                x = a[k];
                if (x < pivot1) {
                    a[k] = a[less];
                    a[less++] = x;
                }
            }
        }
    }
    // swap
    a[left] = a[less - 1];
    a[less - 1] = pivot1;
    a[right] = a[great + 1];
    a[great + 1] = pivot2;
    // 递归处理左右部分
    dualPivotQuicksort(a, left, less - 2);
    dualPivotQuicksort(a, great + 2, right);
    // 处理 == pivot1 或 == pivot2
    if (great - less > len - DIST_SIZE && diffPivots) {
        for (int k = less; k <= great; k++) {
            x = a[k];
            if (x == pivot1) {
                a[k] = a[less];
                a[less++] = x;
            }
            else if (x == pivot2) {
                a[k] = a[great];
                a[great--] = x;
                x = a[k];
                if (x == pivot1) {
                    a[k] = a[less];
                    a[less++] = x;
                }
            }
        }
    }
    // center part
    if (diffPivots) {
        dualPivotQuicksort(a, less, great);
    }
}

JDK 的实现

判断单双轴
// length / 7 的近似值,这里使用到无穷级数,下面讲解一下
int seventh = (length >> 3) + (length >> 6) + 1;
​
// 属于五取样切分
int e3 = (left + right) >>> 1;
int e2 = e3 - seventh;
int e1 = e2 - seventh;
int e4 = e3 + seventh;
int e5 = e4 + seventh;
​
// 使用插入排序,升序
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; }
                                                   }
                                   }
                   }
​
int less  = left;
int great = right;
​
// 双轴实现属于快速排序算法改进中的“熵最优排序”
if (a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]) {
    // 双轴实现的代码
} else {
    // 单轴实现的代码
}

解释 seventh = (length >> 3) + (length >> 6) + 1

  • 因为 length / 7 = (length >> 3) + (length >> 6) = length / 2^3 + length / 2^6 = length (1 / 2^3 + 1 / 2^6) 即 1 / 2^3 + 1 / 2^6 = 1 / 7 <式子1>

  • 且注释说该代码涉及到无穷级数

    image-20220312223948408.png

    无穷级数表示为 image-20220312224547990.png

  • 因此可以把 <式子1> 化为无穷级数:image-20220312225102814.png = 1 / 2^k + 1 / 2^2k + 1 / 2^3k + ... = 1 / (2^k - 1) <式子2>

  • 接下来证明 <式子2> 的等式成立

  • <式子2> 可以化为 1 / 2^k + 1 / 2^2k + 1 / 2^3k + ... = (1 / 2^k) / ((2^k - 1) / 2^k) 1 / 2^k + 1 / 2^2k + 1 / 2^3k + ... = (1 / 2^k) / (1 - 1 / 2 ^ k) <式子3>

  • 设 x = 1 / 2 ^ k,则 <式子3> 可以转化为 x + x^2 + x^3 + ... = x / (1 - x),等式两边同时除以 x 1 + x + x^2 + x^3 + ... = 1 / (1 - x),设等式左边为 f(x) f(x) = 1 + x + x^2 + x^3 + ... f(x) = 1 + x(1 + x^2 + ...) f(x) = 1 + x*f(x) f(x)(1 - x) = 1 f(x) = 1 / (1 - x),证得 <式子2> 的等式成立

  • 综上所述 1 / 2^k + 1 / 2^2k + 1 / 2^3k + ... = 1 / (2^k - 1)

为什么是除以 7?

length / 7 是作为 e1、e2、e3、e4、e5 之间的间距,而这间距是经验值。

image-20220313153012441.png

调试代码

Random random = new Random();
int[] a = new int[250];
for (int i = 0; i < 250; i++) {
    a[i] = random.nextInt(100);
}
Arrays.sort(a);
双轴

image-20220313164959829.png

// 使用五个已排序元素中的第二个和第四个作为轴
int pivot1 = a[e2];
int pivot2 = a[e4];
​
a[e2] = a[left];
a[e4] = a[right];
​
while (a[++less] < pivot1);
while (a[--great] > pivot2);
​
// 将 less 到 great 之间的元素一一与 pivot1 和 pivot2 比较
// 小于 pivot1 的放在 less 左边 less++
// 大于 pivot2 的放在 great 右边,great--
outer:
// 循环一
for (int k = less - 1; ++k <= great; ) {
    int ak = a[k];
    if (ak < pivot1) { // 将小于 pivot1 的元素放到 left part
        a[k] = a[less];
        a[less] = ak;
        ++less;
    } else if (ak > pivot2) { // 将大于 pivot2 的元素放到 right part
        // 循环 A
        while (a[great] > pivot2) {
            if (great-- == k) {
                break outer;
            }
        }
        // 因为如果执行了循环 A 后 a[great] 会小于 pivot2,又因为 pivot2 > pivot1,所以 a[great] 可能会小于 pivot1,使用这个 if 语句块判断 a[great] 是否小于 pivot1
        if (a[great] < pivot1) {
            a[k] = a[less];
            a[less] = a[great];
            ++less;
        } else {
            a[k] = a[great];
        }
​
        a[great] = ak;
        --great;
    }
}
​
// 将轴交换到最终位置
a[left]  = a[less  - 1]; a[less  - 1] = pivot1;
a[right] = a[great + 1]; a[great + 1] = pivot2;
​
sort(a, left, less - 2, leftmost);
sort(a, great + 2, right, false);
​
// 如果中间的部分大于数组长度的 4 / 7
// 将等于 pivot1 的值放到 less 左边,less++
// 将等于 pivot2 的值放到 great 右边,great--
if (less < e1 && e5 < great) {
    
    while (a[less] == pivot1) {
        ++less;
    }
    
    while (a[great] == pivot2) {
        --great;
    }
​
    outer:
    // 循环二,处理与轴值相同的元素
    for (int k = less - 1; ++k <= great; ) {
        int ak = a[k];
        if (ak == pivot1) {
            a[k] = a[less];
            a[less] = ak;
            ++less;
        } else if (ak == pivot2) {
            while (a[great] == pivot2) {
                if (great-- == k) {
                    break outer;
                }
            }
            if (a[great] == pivot1) {
                a[k] = a[less];
                a[less] = pivot1;
                ++less;
            } else {
                a[k] = a[great];
            }
            a[great] = ak;
            --great;
        }
    }
}
​
sort(a, less, great, false);

调试

250个随机数的数组

[46, 14, 67, 14, 96, 1, 46, 97, 7, 11, 25, 88, 86, 69, 56, 34, 22, 53, 59, 43, 56, 33, 20, 45, 40, 92, 35, 38, 17, 2, 54, 29, 97, 29, 60, 19, 39, 65, 17, 10, 41, 24, 59, 82, 85, 26, 34, 92, 80, 45, 67, 57, 85, 17, 15, 23, 81, 65, 20, 95, 31, 14, 61, 39, 55, 0, 83, 2, 16, 26, 21, 43, 54, 50, 0, 35, 16, 87, 52, 96, 31, 87, 94, 10, 12, 66, 82, 0, 7, 46, 34, 9, 35, 79, 54, 33, 60, 47, 72, 84, +150 more]

插入排序前

a[e1] = 59
a[e2] = 81
a[e3] = 46
a[e4] = 15
a[e5] = 85

插入排序后

a[e1] = 15
a[e2] = 46
a[e3] = 59
a[e4] = 81
a[e5] = 85

开始循环一前 image-20220314152328822.png

开始循环二前 image-20220314161841276.png

因为我这里调试时 great - less 小于数组长度的 4 / 7,所以会跳过循环二的执行,而是递归中间部分 image-20220314162011034.png

单轴

在单轴的第一行代码处打断点

image-20220314163524088.png

因为我写的测试代码是生成随机数数组,所以有概率会出现单轴的情况,只要多点几次 debug,就有可能进入到单轴的代码块中

int pivot = a[e3];
​
// 循环三
for (int k = less; k <= great; ++k) {
    if (a[k] == pivot) {
        continue;
    }
    int ak = a[k];
    if (ak < pivot) {
        a[k] = a[less];
        a[less] = ak;
        ++less;
    } else {
        while (a[great] > pivot) {
            --great;
        }
        if (a[great] < pivot) {
            a[k] = a[less];
            a[less] = a[great];
            ++less;
        } else {
            a[k] = pivot;
        }
        a[great] = ak;
        --great;
    }
}
​
sort(a, left, less - 1, leftmost);
sort(a, great + 1, right, false);

可以看到这里的单轴快速排序实现和传统的完全不一样

调试

250个随机数的数组

[92, 12, 25, 31, 92, 79, 62, 49, 28, 79, 35, 7, 34, 23, 59, 14, 98, 72, 65, 85, 24, 60, 79, 25, 23, 46, 34, 25, 27, 86, 23, 81, 87, 40, 70, 55, 94, 89, 88, 17, 67, 91, 84, 18, 84, 31, 31, 3, 79, 92, 70, 14, 88, 66, 15, 95, 52, 92, 11, 92, 28, 15, 1, 63, 67, 45, 92, 86, 33, 63, 64, 34, 60, 87, 42, 65, 92, 54, 17, 63, 59, 58, 17, 0, 48, 36, 95, 77, 97, 15, 87, 20, 53, 2, 31, 29, 36, 42, 77, 19, +150 more]

插入排序后

a[e1] = 15
a[e2] = 15
a[e3] = 16
a[e4] = 23
a[e5] = 97

开始循环三之前 image-20220314165410024.png

执行完循环三

image-20220314170109277.png

传统快速排序和 DualPivotQuicksort 的性能比较

image-20220316232312084.png

归并排序

按照升序排列

算法描述

递归法(Top-down)

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤 3 直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

迭代法(Bottom-up)

  1. 将序列每相邻两个数字进行归并操作,形成 ceil(n/2) 个序列,排序后每个序列包含两/一个元素
  2. 若此时序列数不是1个则将上述序列再次归并,形成 ceil(n/4) 个序列,每个序列包含四/三个元素
  3. 重复步骤 2,直到所有元素排序完毕,即序列数为 1

复杂度

平均时间复杂度O(n logn)
最坏时间复杂度O(n logn)
最优时间复杂度O(n logn)
空间复杂度O(n)

算法特点

迭代法比较适用于链表组织的数据

算法实现

递归法(Top-down)

image-20220317103645552.png

public void mergeSort(int[] arr, int[] result, int start, int end) {
    if (start >= end)
        return;
    int len = end - start, mid = (len >> 1) + start;
    int start1 = start;
    int start2 = mid + 1;
    merge_sort(arr, result, start1, mid);
    merge_sort(arr, result, start2, end);
    // 合并两子数组
    int k = start;
    while (start1 <= mid && start2 <= end)
        result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
    while (start1 <= mid)
        result[k++] = arr[start1++];
    while (start2 <= end)
        result[k++] = arr[start2++];
    for (k = start; k <= end; k++)
        arr[k] = result[k];
}
算法优化
  • 对于小规模子数组使用插入排序

    • 因为递归会使小规模问题中方法得调用过于频繁,所以改进对它们的处理方法就能改进整个算法。而插入排序非常简单,因此很可能在小数组上比归并排序更快。使用插入排序处理小规模数组一般可以将归并排序的运行时间缩短 10% ~ 15%
  • 测试数组是否有序

    • 添加一个判断:如果 a[mid] 小于等于 a[mid+1],我们就认为数组已经是有序的并跳过合并操作,这可以将任意有序的子数组算法的运行时间变为线性的
  • 不将元素复制到辅助数组

迭代法(Bottom-up)

image-20220317112224288.png

public void merge_sort(int[] arr) {
    int[] orderedArr = new int[arr.length];
    for (int i = 2; i < arr.length * 2; i *= 2) {
        for (int j = 0; j < (arr.length + i - 1) / i; j++) {
            int left = i * j;
            int mid = left + i / 2 >= arr.length ? (arr.length - 1) : (left + i / 2);
            int right = i * (j + 1) - 1 >= arr.length ? (arr.length - 1) : (i * (j + 1) - 1);
            int start = left, l = left, m = mid;
            while (l < mid && m <= right) {
                if (arr[l] < arr[m]) {
                    orderedArr[start++] = arr[l++];
                } else {
                    orderedArr[start++] = arr[m++];
                }
            }
            while (l < mid)
                orderedArr[start++] = arr[l++];
            while (m <= right)
                orderedArr[start++] = arr[m++];
            System.arraycopy(orderedArr, left, arr, left, right - left + 1);
        }
    }
}

递归法和迭代法性能比较

image-20220317153216993.png

DualPivotQuicksort 的实现

在 TimSort 内

TimSort

定义

N

表示待排序的数组的长度

run

待排序的数组的有序子数组,顺序是非递减的(a0 <= a1 <= a2 <= ...),或者是严格递减的(a0 > a1 > a2 > ...),严格递减是因为子数组如果有相等的元素会破坏稳定性

根据科林斯词典的解释:

image-20220317214837950.png

翻译过来的意思是:如果你说某个长的东西,比如一条路,朝着一个特定的方向 run,你就是在描述它的路线或位置。简单来说,可以把 run 理解成有方向的路径,如果你觉得这个解释还是太长,就叫它“向径”吧。

minrun

表示 run 的最小长度

image-20220318112222847.png

算法描述

  1. 计算 minrun

    • minrun 的取值原则

      • minrun 不能太长,因为 run 要用到插入排序,而插入排序对于短数组效率才高

      • minrun 不能太短,因为 run 太短会导致在下一步操作时有更多的 run 需要被合并

      • 在 (32, 65) 中选择一个 minrun 使得 N/minrun 等于或者接近d但小于 2 的幂,因为 N/minrun 等于 2 的幂时,每个 run 都是归并排序的叶子节点,这棵归并排序的树是满二叉树,如果不为满二叉树会导致更多的数据移动,以下举个例子: 这是求 minrun 的代码

        assert n >= 0;
        int r = 0;
        while (n >= 32) {
            // 代码行 1,二进制最后一位为 1 的是奇数
            r |= (n & 1); 
            n >>= 1;
        }
        return n + r;
        

        假设数组长度为 2112,如果不使用代码行 1 则返回 16,即 132 个run,否则返回 17,即 124 个 run

        image-20220318220647847.png

        可以看到,run 的个数大于 2 的幂会导致 run 的合并次数比个数为幂等于 2 的更多

  2. 合并的条件 记最上边的三个 run 的长度从下到上分别是 A, B, C

    • 不能先 A + C,因为如果 A,B,C 对应的数组中都有同一个数字 p,那么 A + C 会导致可能 C 的 p 出现在 B 的后面,或者 A 的 p 出现在 B 的前面,这破坏了稳定性。因此只能考虑先 (A+B) 或者 (B+C)

    • 若 A > B + C,B > C 则不进行合并

      • B > C 意味着待合并的 run 栈中的 run 的长度从栈底到栈顶是递减
      • A > B + C 意味着从栈顶到栈底的 run 长度增长至少和斐波那契数列一样快
    • 若 A <= B + C,则合并 B 与 A、C 中长度较短的 run

    • 若 B <= C,则合并 B 和 C

  3. 有利于内存优化的操作

    • 在合并 A 和 B 前,使用二分查找算法查找 B[0] 在 A 中的位置 p1,在 p1 之前的元素的位置是已经确定了的;接着同样使用二分查找法查找 A[-1] 在 B 的位置 p1,在 p2 之后的元素可以被忽略

      image-20220319205726391.png

    • 使用临时内存等于 min(A, B)

      • 如果 A 比 B 小,将 A 对应的子数组复制到一个临时数组 temp,对 temp 和 B 对应的子数组进行归并算法,归并结果从左往右放置到原本 A 待的地方
      • 如果 B 比 A 小,则做类似上一步的操作(与上一步的操作是镜像对称的)
  4. 合并算法

    • 当 A <= B 时的操作

      • “一次比较一对”模式(one pair at a time)

        • 比较 A 和 B 的首个元素,如果 B[0] < A[0] 则将 B[0] 覆盖到合并的区域,否则将 A[0] 覆盖到合并的区域
        • 如果 A 或 B 中的某一个连续将元素覆盖到合并区域的次数达到 MIN_GALLOP(进入急速移动的阈值),那么就进入下面的“急速移动”模式
      • “急速移动”模式(galloping mode)

        • 查找 A[0] 在 B 中的位置 q1,将 q1 前的元素(这些元素的大小都小于 A[0])都覆盖到合并区域,接着将 A[0] 覆盖到合并区域
        • 查找 B[0] 在 A 中的位置 q2,将 q2 后的元素(这些元素都大于 B[0])都覆盖到合并区域,接着将 B[0] 覆盖到合并区域
        • 当要覆盖到合并区域的数组元素个数小于 MIN_GALLOP 时,就会回到“一次比较一对”模式
    • 当 A > B 时的操作

      • 略过,和前者很相似

复杂度

平均时间复杂度O(n logn)
最坏时间复杂度O(n logn)
最优时间复杂度O(n)
空间复杂度O(n)

算法实现

jdk 中的 TimSort

private T[] tmp;
private int tmpBase;
private int tmpLen;
​
// 这三个变量用于描述一个栈,该栈存储等待被合并的多个 run
// runBase[i] 是第 i 个 run 在输入数组的起始下标,runLen[i] 是其对应的长度
// runBase[i] + runLen[i] == runBase[i + 1]
private int stackSize = 0;
private final int[] runBase;
private final int[] runLen;

sort()

static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                     T[] work, int workBase, int workLen) {
    assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
​
    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // 长度小于 2 就不需要排序了
​
    // 如果数组长度小于 32,就用二分插入排序算法
    if (nRemaining < MIN_MERGE) {
        int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
        binarySort(a, lo, hi, lo + initRunLen, c);
        return;
    }
​
    // 从左到右遍历数组一次,找到 run,将长度不足 minrun 的 run 扩展到 minrun 长度,然后合并多个 run 以保持堆栈不变
    TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
    int minRun = minRunLength(nRemaining);
    do {
        // 找到下一个 run 并返回其长度
        int runLen = countRunAndMakeAscending(a, lo, hi, c);
​
        // 如果 run 太短,则将其长度扩展到 min(minRun, nRemaining)
        if (runLen < minRun) {
            int force = nRemaining <= minRun ? nRemaining : minRun;
            binarySort(a, lo, lo + force, lo + runLen, c);
            runLen = force;
        }
​
        // 把已经有序的数组压入堆栈中
        ts.pushRun(lo, runLen);
        ts.mergeCollapse();
​
        // Advance to find next run
        lo += runLen;
        nRemaining -= runLen;
    } while (nRemaining != 0);
​
    // Merge all remaining runs to complete sort
    assert lo == hi;
    ts.mergeForceCollapse();
    assert ts.stackSize == 1;
}

countRunAndMakeAscending()

// 返回从指定数组中指定位置开始的 run 的长度,如果是递减则反转run(确保方法返回时 run 总是升序)
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
                                                Comparator<? super T> c) {
    assert lo < hi;
    int runHi = lo + 1;
    if (runHi == hi)
        return 1;
​
    // 找出最长的升序子数组,如果是严格降序则反转该子数组
    if (c.compare(a[runHi++], a[lo]) < 0) { // 严格递减
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
            runHi++;
        reverseRange(a, lo, runHi);
    } else {                              // 升序
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
            runHi++;
    }
​
    return runHi - lo;
}

TimSort()

private TimSort(T[] a, Comparator<? super T> c, T[] work, int workBase, int workLen) {
    this.a = a;
    this.c = c;
​
    // Allocate temp storage (which may be increased later if necessary)
    int len = a.length;
    int tlen = (len < 2 * INITIAL_TMP_STORAGE_LENGTH) ?
        len >>> 1 : INITIAL_TMP_STORAGE_LENGTH;
    if (work == null || workLen < tlen || workBase + tlen > work.length) {
        @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
        T[] newArray = (T[])java.lang.reflect.Array.newInstance
            (a.getClass().getComponentType(), tlen);
        tmp = newArray;
        tmpBase = 0;
        tmpLen = tlen;
    }
    else {
        tmp = work;
        tmpBase = workBase;
        tmpLen = workLen;
    }
​
    
    int stackLen = (len <    120  ?  5 :
                    len <   1542  ? 10 :
                    len < 119151  ? 24 : 49);
    runBase = new int[stackLen];
    runLen = new int[stackLen];
}

minRunLength()

// 返回指定长度数组的最小可接受的长度,在 [16, 32)之间
private static int minRunLength(int n) {
    assert n >= 0;
    int r = 0;      // Becomes 1 if any 1 bits are shifted off
    while (n >= MIN_MERGE) {
        r |= (n & 1); // n 最后一位是否为 1
        n >>= 1; // 缩小两倍
    }
    return n + r;
}
​
private static void reverseRange(Object[] a, int lo, int hi) {
    hi--;
    while (lo < hi) {
        Object t = a[lo];
        a[lo++] = a[hi];
        a[hi--] = t;
    }
}
​
// 将指定的 run 推入待办的 run 堆栈
// runBase 是 run 中第一个元素的下标
// runLen 表示 run 的数组元素个数
private void pushRun(int runBase, int runLen) {
    this.runBase[stackSize] = runBase;
    this.runLen[stackSize] = runLen;
    stackSize++;
}

mergeCollapse()

// 检查等待合并的 run 堆栈并合并相邻的 run,直到重新建立堆栈不变量
private void mergeCollapse() {
    while (stackSize > 1) {
        int n = stackSize - 2;
        // 若 A <= B + C,则合并 B 与 A、C 中长度较短的 run
        if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
            if (runLen[n - 1] < runLen[n + 1])
                n--;
            mergeAt(n);
        } else if (runLen[n] <= runLen[n + 1]) { // 若 B <= C,则合并 B 和 C
            mergeAt(n);
        } else { // A > B + C 且 B > C
            break;
        }
    }
}

mergeAt()

// 合并栈中下标为 i 和 i + 1 的两个 run,i 必须是栈的倒数第二个或者倒数第三个 run
// 如果是合并 B、C,则 i == stackSize - 2
// 如果是合并 A、B,则 i == stackSize - 3
private void mergeAt(int i) {
    assert stackSize >= 2;
    assert i >= 0;
    assert i == stackSize - 2 || i == stackSize - 3;
​
    int base1 = runBase[i];
    int len1 = runLen[i];
    int base2 = runBase[i + 1];
    int len2 = runLen[i + 1];
    assert len1 > 0 && len2 > 0;
    assert base1 + len1 == base2;
​
    // 合并两个 run 的长度并存到 runLen[i] 中
    runLen[i] = len1 + len2;
    // 如果是合并 A、B,因为 B 会合并进 A 中,所以 C 对应的 run 的信息可以往 B 处移
    if (i == stackSize - 3) {
        runBase[i + 1] = runBase[i + 2];
        runLen[i + 1] = runLen[i + 2];
    }
    stackSize--;
​
    // 从左往右查找 run2 的第一个元素在 run1 中的位置 k
    // run1 中在 k 之前的元素可以忽略(因为它们已经就位)
    int k = gallopRight(a[base2], a, base1, len1, 0, c);
    assert k >= 0;
    base1 += k;
    len1 -= k;
    if (len1 == 0)
        return;
​
    // 从右到左查找 run1 的最后一个元素在 run2 中的位置 l,这个 l 可以当作 run2 的长度
    // 具体实现和 gallopRight() 很像,后面就略过不讲了
    len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
    assert len2 >= 0;
    if (len2 == 0)
        return;
​
    // 合并剩余的 run,对 len1、len2 中较小的 run 使用 tmp 数组
    if (len1 <= len2)
        mergeLo(base1, len1, base2, len2);
    else
        mergeHi(base1, len1, base2, len2);
}

gallopRight()

// 假设合并 run1、run2,run2 在 run1 右边
// key 为 run2 中的值,目的是为了查找其要插入 run1 的位置到 base 的距离
// a 是输入的数组,即待排序的数组
// base 是 run1 的起始下标
// len 是 run1 的长度
// hint 是开始搜索的索引
private static <T> int gallopRight(T key, T[] a, int base, int len,
                                   int hint, Comparator<? super T> c) {
    assert len > 0 && hint >= 0 && hint < len;
​
    // 移动的步长,比如从 a[0] 开始移动,步长为2,则到达 a[2]
    int ofs = 1;
    int lastOfs = 0;
    if (c.compare(key, a[base + hint]) < 0) {
        // Gallop left until a[b+hint - ofs] <= key < a[b+hint - lastOfs]
        int maxOfs = hint + 1;
        while (ofs < maxOfs && c.compare(key, a[base + hint - ofs]) < 0) {
            lastOfs = ofs;
            ofs = (ofs << 1) + 1;
            if (ofs <= 0)   // int overflow
                ofs = maxOfs;
        }
        if (ofs > maxOfs)
            ofs = maxOfs;
​
        // Make offsets relative to b
        int tmp = lastOfs;
        lastOfs = hint - ofs;
        ofs = hint - tmp;
    } else { // 这个 else 代码块内的逻辑是用在 mergeAt() 方法内,用于查找 run2 中的第一个元素要插入 run1 的具体下标
        // 当 a[base + hint] <= key
        // 急速往右移动到 [base + hint + lastOfs] <= key < a[base + hint + ofs]
        int maxOfs = len - hint;
        while (ofs < maxOfs && c.compare(key, a[base + hint + ofs]) >= 0) {
            lastOfs = ofs;
            // 移动步长为递增的奇数序列:1,3,5,...,2k + 1
            ofs = (ofs << 1) + 1;
            if (ofs <= 0)   // int 溢出
                ofs = maxOfs;
        }
        if (ofs > maxOfs)
            ofs = maxOfs;
​
        // 要插入的值在 run1 的下标在区间 [lastOfs,ofs)
        lastOfs += hint;
        ofs += hint;
    }
    assert -1 <= lastOfs && lastOfs < ofs && ofs <= len;
​
    // 使用二分查找搜索 [lastOfs,ofs),找到 key 在 run1 的下标
    lastOfs++;
    while (lastOfs < ofs) {
        int m = lastOfs + ((ofs - lastOfs) >>> 1);
​
        if (c.compare(key, a[base + m]) < 0)
            ofs = m;          // key < a[base + m]
        else
            lastOfs = m + 1;  // a[base + m] <= key
    }
    assert lastOfs == ofs;
    return ofs;
}

mergeLo()

// 以稳定的方式合并两个相邻的 run,len1 <= len2
private void mergeLo(int base1, int len1, int base2, int len2) {
    assert len1 > 0 && len2 > 0 && base1 + len1 == base2;
​
    // 处于性能考虑,将 run1 复制到 temp 数组中
    T[] a = this.a;
    T[] tmp = ensureCapacity(len1);
    int cursor1 = tmpBase; // Indexes into tmp array
    int cursor2 = base2;   // Indexes int a
    int dest = base1;      // Indexes int a
    // 将 a 从下标 bas1 开始 len1 长的数组元素复制到 tmp 的 cursor1 下标
    System.arraycopy(a, base1, tmp, cursor1, len1);
​
    // Move first element of second run and deal with degenerate cases
    a[dest++] = a[cursor2++];
    if (--len2 == 0) {
        System.arraycopy(tmp, cursor1, a, dest, len1);
        return;
    }
    if (len1 == 1) {
        System.arraycopy(a, cursor2, a, dest, len2);
        a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
        return;
    }
​
    Comparator<? super T> c = this.c;  // Use local variable for performance
    int minGallop = this.minGallop;    //  "    "       "     "      "
    outer:
    while (true) {
        int count1 = 0; // Number of times in a row that first run won
        int count2 = 0; // Number of times in a row that second run won
​
        // “一次比较一对”模式(one pair at a time)
        do {
            assert len1 > 1 && len2 > 0;
            // run2 与 tmp 数组比较
            // 如果 run2 的元素小于 tmp 的,则将 run2 的元素覆盖在 a[dest] 上,否则将 tmp 的元素覆盖在 a[dest] 上。run2 和 tmp 中有哪一方连续覆盖了 7 次,则跳出该循环
            if (c.compare(a[cursor2], tmp[cursor1]) < 0) {
                a[dest++] = a[cursor2++];
                count2++;
                count1 = 0;
                if (--len2 == 0)
                    break outer;
            } else {
                a[dest++] = tmp[cursor1++];
                count1++;
                count2 = 0;
                if (--len1 == 1)
                    break outer;
            }
        } while ((count1 | count2) < minGallop);
​
        // “急速移动”模式(galloping mode)
        do {
            assert len1 > 1 && len2 > 0;
            // 查找 a[cursor2] 要插入 tmp 的位置到下标 cursor1 的距离 
            count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);
            if (count1 != 0) {
                // 将 tmp 中比 a[cursor2] 小的多个数组元素复制到 a 中的 dest 处
                System.arraycopy(tmp, cursor1, a, dest, count1);
                dest += count1;
                cursor1 += count1;
                len1 -= count1;
                if (len1 <= 1) // len1 == 1 || len1 == 0
                    break outer;
            }
            // 将 a[cursor2] 放入 a[dest]
            a[dest++] = a[cursor2++];
            if (--len2 == 0)
                break outer;
​
            // 与上面 gallopRight() 处理类似
            count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);
            if (count2 != 0) {
                System.arraycopy(a, cursor2, a, dest, count2);
                dest += count2;
                cursor2 += count2;
                len2 -= count2;
                if (len2 == 0)
                    break outer;
            }
            a[dest++] = tmp[cursor1++];
            if (--len1 == 1)
                break outer;
            minGallop--;
        } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
        if (minGallop < 0)
            minGallop = 0;
        minGallop += 2;  // Penalize for leaving gallop mode
    }  // End of "outer" loop
    this.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to field
​
    if (len1 == 1) {
        assert len2 > 0;
        // 将 run2 的剩余元素复制到 a[dest]
        System.arraycopy(a, cursor2, a, dest, len2);
        a[dest + len2] = tmp[cursor1]; //  Last elt of run 1 to end of merge
    } else if (len1 == 0) {
        throw new IllegalArgumentException(
            "Comparison method violates its general contract!");
    } else {
        assert len2 == 0;
        assert len1 > 1;
        System.arraycopy(tmp, cursor1, a, dest, len1);
    }
}

ensureCapacity()

// 确保外部数组 tmp 至少具有指定数量的元素,必要时增加其大小。大小呈指数增长,以确保摊销线性时间复杂度
private T[] ensureCapacity(int minCapacity) {
    if (tmpLen < minCapacity) {
        // 计算大于且最接近于32位int的 minCapacity 的2的幂
        // 具体过程后面会解释“如何求接近某个数的下一个二的幂”
        int newSize = minCapacity;
        newSize |= newSize >> 1;
        newSize |= newSize >> 2;
        newSize |= newSize >> 4;
        newSize |= newSize >> 8;
        newSize |= newSize >> 16;
        newSize++;
​
        if (newSize < 0) // 不太可能
            newSize = minCapacity;
        else
            newSize = Math.min(newSize, a.length >>> 1);
​
        @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"})
        T[] newArray = (T[])java.lang.reflect.Array.newInstance
            (a.getClass().getComponentType(), newSize);
        tmp = newArray;
        tmpLen = newSize;
        tmpBase = 0;
    }
    return tmp;
}

如何求接近某个数 k 的下一个二的幂?

现在假设 k 只有一位为 1,取 64

kk>> 1,2,4,8,16|=
100000001000001100000
110000000110001111000
111100000011111111111
111111100000001111111
111111100000001111111

通过上面的测试,可以知道该算法的原理是:按照序列 {1,2,4,8,16} 依次右移再按位或,最多可以让 1 的个数 *2。到右移 16 时,1 的个数最多可以达到 32 个,而 Java 中 int 最多取 31 位,所以该算法完全够处理 int 型。既然只有一个 1 都能得到结果,那么如果数字 k 的二进制不止一个 1,肯定也可以得出结果。

DualPivotQuicksort 的实现

/*
 * run[i] 是第 i 次 run 的起始索引 
 */
int[] run = new int[MAX_RUN_COUNT + 1];
// count 表示 run 数组的个数
int count = 0; run[0] = left;
​
// 检查数组是否部分有序,计算 run 数组
for (int k = left; k < right; run[count] = k) {
    if (a[k] < a[k + 1]) { // 升序
        while (++k <= right && a[k - 1] <= a[k]);
    } else if (a[k] > a[k + 1]) { // 降序
        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 { // 相等
        for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
            if (--m == 0) {
                sort(a, left, right, true);
                return;
            }
        }
    }
​
    /*
     * 数组不是高度结构化的,使用快速排序而不是归并排序
     */
    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) { // 只有一个 run 了表示排序完毕
    return;
}
​
// Determine alternation base for merge
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);
​
// Use or create temporary array b for merging
int[] b;                 // temp array; alternates with a
int ao, bo;              // array offsets from 'left'
int blen = right - left; // space needed for b
if (work == null || workLen < blen || workBase + blen > work.length) {
    work = new int[blen];
    workBase = 0;
}
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
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;
}

TimSort 与完整的 DualPivotQuicksort 性能比较

随机数数组:

image-20220319221617626.png

有序数组:

image-20220319223829038.png

计数排序

按照升序排列

算法描述

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项
  3. 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项,每放一个元素就将 C[i] 减去1

复杂度

平均时间复杂度O(n + k)
最坏时间复杂度O(n + k)
最优时间复杂度O(n + k)
空间复杂度O(n + k)

算法特点

不是比较排序

算法实现

public static int[] countSort(int[] a) {
    int b[] = new int[a.length];
    int max = a[0], min = a[0];
    for (int i : a) {
        if (i > max) {
            max = i;
        }
        if (i < min) {
            min = i;
        }
    }
    int k = max - min + 1;
    int c[] = new int[k];
    // 记录第 i 个数出现的次数到 c[i]
    for (int i = 0; i < a.length; ++i) {
        c[a[i] - min] += 1; 
    }
    // 将 c[i] 更新为要放到 b 时的起始下标
    for (int i = 1; i < c.length; ++i) {
        c[i] = c[i] + c[i - 1];
    }
    // 将元素存到 b 中
    for (int i = a.length - 1; i >= 0; --i) {
        b[--c[a[i] - min]] = a[i]; 
    }
    return b;
}

DualPivotQuicksort 的实现

int NUM_SHORT_VALUES = 1 << 16;
​
int[] count = new int[NUM_SHORT_VALUES];
​
for (int i = left - 1; ++i <= right;
     count[a[i] - Short.MIN_VALUE]++);
for (int i = NUM_SHORT_VALUES, k = right + 1; k > left; ) {
    while (count[--i] == 0);
    short value = (short) (i + Short.MIN_VALUE);
    int s = count[i];
​
    do {
        a[--k] = value;
    } while (--s > 0);
}

传统计数排序与 DualPivotQuicksort 的性能比较

因为 DualPivotQuicksort 中的计数排序是针对 short 型数组的,所以测试的时候把传统计数排序的类型也改成 short

image-20220321113815393.png

算法时间复杂度总结

来源:www.bigocheatsheet.com/

image-20220317222256919.png

参考

[1]Robert Sedgewick, Kevin Wayne.Algorithms (4th Edition)[M].America: Addison-Wesley Professional, 2011.

[2]Vladimir Yaroslavskiy, Replacement of quicksort in java.util.arrays with new dual-pivot quick- sort, mail.openjdk.java.net/pipermail/c…. html, 2009, Archived version of the discussion in the OpenJDK mailing list.

[3]Oracle.Java Source Code[DB]

本账号所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!