算法导论-第8章-线性时间排序
前言
此前我们已经学习了几种的排序算法,这些排序算法都有一个有趣性质,在排序的最终结果中,各元素的次序依赖于它们之间的比较,我们将这类排序称为比较排序(comparison sort)。
8.1节将要证明对包含个元素的输入序列,在最坏情况下,任何比较排序都要经过次比较。
8.2节、8.3节、8.4节将要介绍三种线性时间复杂度适用于某些特定输入的的排序:计数排序(counting sort)、基数排序(radix sort)、和桶排序(bucket sort)。这些排序通过其它方法来确定排序顺序。
8.1 排序算法的下界
决策树模型
在决策树中,每个内部结点都以标记,其中和满足,是输入序列中的元素个数。每个叶结点上都标注一个序列。排序算法的执行对应于一条从数的根结点到叶结点的路径。每个内部结点表示一次比较。左子树表示确定之后的后续比较,右子树表示确定的后续比较。当到达叶结点后,表示排序以完成。
例如,对于3个元素的插入排序决策树,输入序列,叶结点表示排序的结果是。对于输入元素来说,共有种可能的排列,因此决策树包含6个叶结点。
定理:在最坏情况下,任何比较排序算法都需要做次比较。
证明:设决策树高度为,具有个叶结点,则,两边取对数,得。
推论:堆排序和归并排序都是渐近最优的比较排序算法。
8.2 计数排序
计数排序假设个输入元素中的每一个都是在到区间内的一个整数,其中为某个整数。当时,运行时间为。
计数排序的基本思想是:对每一个输入元素,确定小于的元素个数。这样就可以直接把放到输出数组中的位置上。例如,有17个元素小于,则就应该放在第18个输出位置上。当有几个元素相同时,需要略作修改即可。
在计数排序算法的代码中,假设输入是一个数组,。我们还需要另外两个数组:存放排序的输出,提供临时存储空间,记录小于等于的元素个数。
下图展示了计数排序算法的运行过程。第2-3行for
循环的初始化操作之后,数组的值全被置为0;第4-5行for
循环遍历输入元素。如果输入元素的值为,就将值加1。于是,在第5行执行完后,中保存的就是等于的元素的个数;第7-8行通过累加计算确定对于每个,有多少输入元素是小于或等于的,操作过后,记录的即是小于等于的元素个数。
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)是一种用在卡片排序机🧐(并不了解这个是什么东西)上的算法。基数排序是先按照最低有效位进行排序的,为了保证基数排序的正确性,一位数排序算法必须是稳定的。
假设个位的元素存放在数组中,其中第1位是最低位,第位是最高位。
基数排序的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))); } }
引理:给定个位数,其中每一个数位有个可能的取值。如果RADIX-SORT使用的稳定排序方法耗时,那么它就可以在时间内完成排序。
8.4 桶排序
桶排序(bucket sort)假设输入数据服从均匀分布,平均情况下它的时间代价为。与计数排序类似,因为对输入数据作了某种假设,桶排序的速度也很快。具体来说,计数排序假设输入数据都属于一个小区间内的整数,而桶排序则假设元素均匀、独立地分布在区间内。
桶排序将区间均匀划分为个相同大小的子区间,称为桶。然后,将个输入数分别放到各个桶中。因为输入数据是均匀、独立地分布在区间上,所以一般不会出现很多数落在同一个桶中的情况。
核心思想:先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。
在桶排序的代码中,假设输入是一个包含个元素的数组,且满足。此外,算法还需要一个临时数组来存放链表(即桶)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了