总结 / 算法 · 2020年10月15日 0

算法笔记 – 高级排序

内容纲要

[lwptoc]

排序升级

上一篇文章总结了一些基础的排序算法,但是那些算法并不能应用在实际的程序设计中。因为除了希尔排序,其他算法的时间复杂度均为 $ O(n^2) $,若输入的数据很复杂,则算法的时间消耗是不可接受的。
本篇将会总结一些实用的排序算法,其中一些已经被一些系统的 API 使用了(比如 JDK 中的 Arrays.sort() 使用的就是升级版的快速排序)。

归并排序

归并排序的主要思想是:将两个已经排序的序列合并成一个序列。

步骤

  1. 把 $n$ 个记录看成 $n$ 个长度为 $1$ 的有序子表;
  2. 进行两两归并使记录值有序,得到 $ \frac{n}{2} $ 个长度为 $2$ 的有序子表;
  3. 重复第二步直到所有记录归并成一个长度为 $n$ 的有序表为止。

轨迹

索引 0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0
8 9 7 6 5 4 3 2 1 0
8 9 6 7 5 4 3 2 1 0
8 9 6 7 4 5 3 2 1 0
8 9 6 7 4 5 2 3 1 0
8 9 6 7 4 5 2 3 0 1
6 7 8 9 4 5 2 3 0 1
6 7 8 9 2 3 4 5 0 1
2 3 4 5 6 7 8 9 0 1
完成 0 1 2 3 4 5 6 7 8 9

可以看到这个算法先两两排序,再将它们 4 个一归并,再扩大范围归并。事实上两两排序也是在归并中完成的,算法的核心是归并,所以叫做归并排序。另外,排序的间隔序列也可以反过来形成递归调用,叫做“自顶向下的归并”,上述叫做“自底向上的归并”。

代码

import java.util.Arrays;

public class MergeSort {

    private static int[] aux;

    public static void main(String[] args) {
        int[] testArr = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; // 测试用例
        mergeSort(testArr);
        System.out.println(Arrays.toString(testArr));
    }

    private static void mergeSort(int[] arr) {
        // 进行lgN次两两归并
        int N = arr.length;
        aux = new int[N];
        for (int sz = 1; sz < N; sz <<= 1) // sz子数组大小
            for (int lo = 0; lo < N - sz; lo += sz + sz) // lo:子数组索引
                merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
    }

    private static void merge(int[] arr, int lo, int mid, int hi) { // 将a[lo..mid] 和 a[mid+1..hi] 归并
        int i = lo, j = mid + 1;
        // 将a[lo..hi]复制到aux[lo..hi]
        if (hi + 1 - lo >= 0) {
            System.arraycopy(arr, lo, aux, lo, hi + 1 - lo);
            for (int k = lo; k <= hi; k++) // 归并回到a[lo..hi]
                if (i > mid)                arr[k] = aux[j++];
                else if (j > hi)            arr[k] = aux[i++];
                else if (aux[j] < aux[i])   arr[k] = aux[j++];
                else                        arr[k] = aux[i++];
        }
    }
}

快速排序

快速排序也是采用分治的思想,它和归并排序不同的是它不需要归并排序的两个部分,而是逐步将要排序的元素放到合适的地方。

步骤

  1. 随机地取出一个元素 K
  2. 找出不大于 K 的元素放到 K 的左边,不小于 K 的元素放到 K 的右边
  3. 将 K 左边的元素和 K 右边的元素递归进行 1 操作

由于需要大量比较 K 的大小,所以输入的 K 必须足够随机,而每一遍递归都需要取一个随机的 K,所以实现时干脆先把输入打乱,这样可以尽可能消除输入数据对所需时间的影响。

轨迹

索引 0 1 2 3 4 5 6 7 8 9
输入 9 8 7 6 5 4 3 2 1 0
打乱 2 0 9 7 8 3 4 5 6 1
切分(K=2) 1 0 2 7 8 3 4 5 6 9
切分(K=1) 0 1 2 7 8 3 4 5 6 9
切分(K=7) 0 1 2 5 6 3 4 7 8 9
切分(K=5) 0 1 2 3 4 5 6 7 8 9
完成 0 1 2 3 4 5 6 7 8 9

(省略了无交换的比较)

对于此算法来说,核心的步骤就是如何切分,把 K 放到合适的位置。
我们可以取每个子数组的第一位作为 K,与其后面的每个元素比较,找出从前往后大于或等于 K 的元素与从后往前小于或等于 K 的元素交换,当然了,如果找不到这样的元素就马上停止比较。最后把 K 放在最后一次比较的元素的位置(原本 K 在开始处)。

代码

import java.util.Arrays;
import java.util.Random;

/**
 * 快速排序基础版
 */
public class QuickSort {

    private static final Random random = new Random();

    public static void main(String[] args) {
        int[] testArr = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; // 测试用例
        quickSort(testArr);
        System.out.println(Arrays.toString(testArr));
    }

    private static void quickSort(int[] arr) {
        shuffle(arr); // 消除对输入的依赖
        quickSort(arr, 0, arr.length - 1);
    }

    private static void quickSort(int[] arr, int lo, int hi) {
        if (hi <= lo) return;
        int j = partition(arr, lo, hi); // 切分
        quickSort(arr, lo, j - 1); // 将左半部分a[lo .. j-1]排序
        quickSort(arr, j + 1, hi); // 将右半部分a[j+1 .. hi]排序
    }

    /**
     * 切分数组并找出切分的元素的索引
     *
     * @param arr 数组
     * @param lo  开始
     * @param hi  结束
     * @return 用于切分的元素的索引
     */
    private static int partition(int[] arr, int lo, int hi) {
        int i = lo;
        int j = hi + 1;
        int k = arr[i];
        while (true) {
            while (arr[++i] < k) if (i == hi) break;
            while (arr[--j] > k) /*if (j == lo) break*/ ;
            if (i >= j) break;
            swap(arr, i, j);
        }
        swap(arr, lo, j);
        return j;
    }

    /**
     * 交换数组中的元素
     *
     * @param arr 输入数组
     * @param i   交换源
     * @param j   交换目标
     */
    private static void swap(int[] arr, int i, int j) {
        int t = arr[j];
        arr[j] = arr[i];
        arr[i] = t;
    }

    /**
     * 打乱一个数组
     *
     * @param arr 输入数组
     */
    private static void shuffle(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            int r = i + random.nextInt(n - i);     // between i and n-1
            int temp = arr[i];
            arr[i] = arr[r];
            arr[r] = temp;
        }
    }
}

TODO: 改进快速排序

堆排序

总结

复杂度和稳定性如下表

算法 插入(冒泡)排序 选择排序 希尔排序 归并排序 快速排序 堆排序
稳定性 稳定 非稳定 非稳定 稳定 非稳定 非稳定
时间复杂度 N~N2 N2 NlogN NlogN NlogN NlogN
空间复杂度 1 1 1 N lgN 1