算法导论-第8章-线性时间排序

前言

此前我们已经学习了几种 O ( n log ⁡ n ) \Omicron(n \log n) O(nlogn)的排序算法,这些排序算法都有一个有趣性质,在排序的最终结果中,各元素的次序依赖于它们之间的比较,我们将这类排序称为比较排序(comparison sort)

8.1节将要证明对包含 n n n个元素的输入序列,在最坏情况下,任何比较排序都要经过 Ω ( n log ⁡ n ) \Omega(n \log n) Ω(nlogn)次比较

8.2节、8.3节、8.4节将要介绍三种线性时间复杂度适用于某些特定输入的的排序:计数排序(counting sort)、基数排序(radix sort)、和桶排序(bucket sort)。这些排序通过其它方法来确定排序顺序。

8.1 排序算法的下界

决策树模型

在决策树中,每个内部结点都以 i : j i:j i:j标记,其中 i i i j j j满足 1 ≤ i , j ≤ n 1 \le i,j \le n 1i,jn n n n是输入序列中的元素个数。每个叶结点上都标注一个序列 < π ( 1 ) , π ( 2 ) , ⋯   , π ( n ) , > <\pi(1),\pi(2),\cdots,\pi(n),> <π(1),π(2),,π(n),>。排序算法的执行对应于一条从数的根结点到叶结点的路径。每个内部结点表示一次比较 a i ≤ a j a_i \le a_j aiaj。左子树表示确定 a i ≤ a j a_i \le a_j aiaj之后的后续比较,右子树表示确定 a i > a j a_i \gt a_j ai>aj的后续比较。当到达叶结点后,表示排序以完成。

Figure 8.1

例如,对于3个元素的插入排序决策树,输入序列 < a 1 = 6 , a 2 = 8 , a 3 = 5 > <a_1=6,a_2=8,a_3=5> <a1=6,a2=8,a3=5>,叶结点 < 3 , 1 , 2 > <3,1,2> <3,1,2>表示排序的结果是 a 3 = 5 ≤ a 1 = 6 ≤ a 2 = 8 a_3=5 \le a_1=6 \le a_2=8 a3=5a1=6a2=8。对于输入元素来说,共有 3 ! = 6 3!=6 3!=6种可能的排列,因此决策树包含6个叶结点。

定理:在最坏情况下,任何比较排序算法都需要做 Ω ( n log ⁡ n ) \Omega(n \log n) Ω(nlogn)次比较。

证明:设决策树高度为 h h h,具有 l l l个叶结点,则 n ! ≤ l ≤ 2 h n! \le l \le 2^h n!l2h,两边取对数,得 h ≥ log ⁡ ( n ! ) = Ω ( n log ⁡ n ) h \ge \log(n!)=\Omega(n \log n) hlog(n!)=Ω(nlogn)

推论:堆排序和归并排序都是渐近最优的比较排序算法。

8.2 计数排序

计数排序假设 n n n个输入元素中的每一个都是在 0 0 0 k k k区间内的一个整数,其中 k k k为某个整数。当 k = O ( n ) k=\Omicron(n) k=O(n)时,运行时间为 Θ ( n ) \Theta(n) Θ(n)

计数排序的基本思想是:对每一个输入元素 x x x,确定小于 x x x的元素个数。这样就可以直接把 x x x放到输出数组中的位置上。例如,有17个元素小于 x x x,则 x x x就应该放在第18个输出位置上。当有几个元素相同时,需要略作修改即可。

在计数排序算法的代码中,假设输入是一个数组 A [ 1.. n ] A[1..n] A[1..n] A . l e n g t h = n A.length=n A.length=n。我们还需要另外两个数组: B [ 1.. n ] B[1..n] B[1..n]存放排序的输出, C [ 0.. k ] C[0..k] C[0..k]提供临时存储空间, C [ i ] C[i] C[i]记录小于等于 i i i的元素个数

下图展示了计数排序算法的运行过程。第2-3行for循环的初始化操作之后,数组 C C C的值全被置为0;第4-5行for循环遍历输入元素。如果输入元素的值为 i i i,就将 C [ i ] C[i] C[i]值加1。于是,在第5行执行完后, C [ i ] C[i] C[i]中保存的就是等于 i i i的元素的个数;第7-8行通过累加计算确定对于每个 i = 0 , 1 , 2 , ⋯   , k i=0,1,2,\cdots,k i=0,1,2,,k,有多少输入元素是小于或等于 i i i的,操作过后, C [ i ] C[i] C[i]记录的即是小于等于 i i i的元素个数。

import java.util.Arrays;

public class CountingSort {
    public static int[] countingSort(int[] A) {
        if (A == null || A.length == 0) {
            return A;
        }

        // 遍历一次输入数组,找到数组中最大和最小元素
        int max = A[0], min = A[0];
        for (int i = 1; i < A.length; i++) {
            if (A[i] > max) {
                max = A[i];
            }
            if (A[i] < min) {
                min = A[i];
            }
        }

        int[] C = new int[max - min + 1]; // 辅助数组C,C[i]记录小于等于i的元素个数

        for (int j = 0; j < A.length; j++) {
            C[A[j] - min]++; // C[i]存储的是等于A[j]-min=i的元素个数
        }

        for (int i = 1; i < C.length; i++) {
            C[i] += C[i - 1]; // C[i]存储的是小于等于i的元素个数
        }

        int[] B = new int[A.length]; // 输出数组

        for (int j = A.length - 1; j >= 0; j--) {
            B[C[A[j] - min] - 1] = A[j];
            C[A[j] - min]--;
        }
        return B;
    }

    public static void main(String[] args) {
        int[] A = {2, 5, 3, 0, 2, 3, 0, 3};
        int[] countingSorted = countingSort(A);
        System.out.println(Arrays.toString(countingSorted));
    }
}

8.3 基数排序

基数排序(radix sort)是一种用在卡片排序机🧐(并不了解这个是什么东西)上的算法。基数排序是先按照最低有效位进行排序的,为了保证基数排序的正确性,一位数排序算法必须是稳定的

假设 n n n d d d位的元素存放在数组 A A A中,其中第1位是最低位,第 d d d位是最高位。

基数排序的Java代码实现如下:

import java.util.Arrays;

public class RadixSort {
    public static int[] radixSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return arr;
        }
        // 找到数组中的最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        // 根据最大元素的位数确定排序的轮数
        int digit = 1;
        while (max / 10 > 0) {
            digit++;
            max /= 10;
        }
        int exp = 1;
        int[] temp = new int[arr.length];
        int[] bucket = new int[10];
        for (int i = 0; i < digit; i++) {
            System.arraycopy(arr, 0, temp, 0, arr.length);
            Arrays.fill(bucket, 0);
            // 计数排序
            for (int j = 0; j < temp.length; j++) {
                int radix = (temp[j] / exp) % 10;
                bucket[radix]++;
            }
            for (int j = 1; j < bucket.length; j++) {
                bucket[j] += bucket[j - 1];
            }
            for (int j = temp.length - 1; j >= 0; j--) {
                int radix = (temp[j] / exp) % 10;
                arr[--bucket[radix]] = temp[j];
            }
            exp *= 10;
        }
        return arr;
    }

    public static void main(String[] args) {
        int[] A = {329, 457, 657, 839, 436, 720, 355};
        System.out.println(Arrays.toString(radixSort(A)));
    }
}

引理:给定 n n n d d d位数,其中每一个数位有 k k k个可能的取值。如果RADIX-SORT使用的稳定排序方法耗时 Θ ( n + k ) \Theta(n+k) Θ(n+k),那么它就可以在 Θ ( d ( n + k ) ) \Theta(d(n+k)) Θ(d(n+k))时间内完成排序。

8.4 桶排序

桶排序(bucket sort)假设输入数据服从均匀分布,平均情况下它的时间代价为 O ( n ) \Omicron(n) O(n)。与计数排序类似,因为对输入数据作了某种假设,桶排序的速度也很快。具体来说,计数排序假设输入数据都属于一个小区间内的整数,而桶排序则假设元素均匀、独立地分布在 [ 0 , 1 ) [0, 1) [0,1)区间内

桶排序将 [ 0 , 1 ) [0, 1) [0,1)区间均匀划分为 n n n个相同大小的子区间,称为。然后,将 n n n个输入数分别放到各个桶中。因为输入数据是均匀、独立地分布在 [ 0 , 1 ) [0, 1) [0,1)区间上,所以一般不会出现很多数落在同一个桶中的情况。

核心思想:先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。

在桶排序的代码中,假设输入是一个包含 n n n个元素的数组 A A A,且满足 0 ≤ A [ i ] < 1 0 \le A[i] \lt 1 0A[i]<1。此外,算法还需要一个临时数组 B [ 0.. n − 1 ] B[0..n-1] B[0..n1]来存放链表(即桶)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值