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

前言

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

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

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

8.1 排序算法的下界

决策树模型

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

Figure 8.1

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

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

证明:设决策树高度为h,具有l个叶结点,则n!l2h,两边取对数,得hlog(n!)=Ω(nlogn)

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

8.2 计数排序

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

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

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

下图展示了计数排序算法的运行过程。第2-3行for循环的初始化操作之后,数组C的值全被置为0;第4-5行for循环遍历输入元素。如果输入元素的值为i,就将C[i]值加1。于是,在第5行执行完后,C[i]中保存的就是等于i的元素的个数;第7-8行通过累加计算确定对于每个i=0,1,2,,k,有多少输入元素是小于或等于i的,操作过后,C[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)是一种用在卡片排序机🧐(并不了解这个是什么东西)上的算法。基数排序是先按照最低有效位进行排序的,为了保证基数排序的正确性,一位数排序算法必须是稳定的

假设nd位的元素存放在数组A中,其中第1位是最低位,第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)));
}
}

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

8.4 桶排序

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

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

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

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

posted @   gengduc  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示