线性排序
桶排序、计数排序、基数排序是时间复杂度是 O(n) 的序算法。
因为这些排序算法的时间复杂度是线性的,所以我们把这类排序算法叫作线性排序(Linear sort)。
之所以能做到线性的时间复杂度,主要原因是,这三个算法是非基于比较的排序算法,都不涉及元素之间的比较操作。
此外,线性排序对排序数据的要求很苛刻,要注意线性排序算法的适用场景。
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
桶排序
算法思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。
桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
桶内可能使用别的排序算法(根据桶内数据情况选择排序算法,一般选择时间复杂度为O(nlogn)的算法,桶内数据较少可以选择插入排序算法)或是以递归方式继续使用桶排序进行排序
桶排序的限制(桶排序的适用场景)
桶排序对于数据要求十分苛刻:
一、要排序的数据需要很容易就能划分成 m 个桶
二、桶与桶之间有着天然的大小顺序。(这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。)
三、数据在各个桶之间的分布是比较均匀的。
如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。
桶排序比较适合用在外部排序中。
所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
假如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。
我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到,订单金额最小是 1 元,最大是 10 万元。我们将所有订单根据金额划分到 100 个桶里,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。
理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件中,每个小文件中存储大约 100MB 的订单数据,我们就可以将这 100 个小文件依次放到内存中,用快排来排序。等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大排序的订单数据了。
但是订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的 ,所以 10GB 订单数据是无法均匀地被划分到 100 个文件中的。有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性读入内存。
针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200 元,201 元到 300 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为止。
桶排序的执行效率
最好情况下时间复杂度
如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。
若每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。
当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
最坏情况下时间复杂度
如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。
平均情况下时间复杂度
因为桶排序对数据的要求较为苛刻,当数据符合桶排序的要求的时候我们才会使用桶排序。
桶排序的平均情况下时间复杂度为O(n)。
桶排序的内存消耗
桶排序需要用到“桶”,需要申请额外的桶“空间”
桶排序的空间复杂度为O(n+m),m为桶的数量。
桶排序的稳定性
桶排序的稳定性由桶内适用的排序方法决定,若桶内数据使用插入排序、归并排序等稳定排序算法,则桶排序是稳定的
若桶内数据适用快速排序等不稳定算法,则桶排序就是不稳定的
桶排序的代码实现
public class BucketSort {
/**
* 桶排序
*
* @param arr 数组
* @param bucketSize 桶容量
*/
public static void bucketSort(int[] arr, int bucketSize) {
if (arr.length < 2) {
return;
}
// 数组最小值
int minValue = arr[0];
// 数组最大值
int maxValue = arr[1];
for (int i = 0; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i];
} else if (arr[i] > maxValue) {
maxValue = arr[i];
}
}
// 桶数量
int bucketCount = (maxValue - minValue) / bucketSize + 1;
int[][] buckets = new int[bucketCount][bucketSize];
int[] indexArr = new int[bucketCount];
// 将数组中值分配到各个桶里
for (int i = 0; i < arr.length; i++) {
int bucketIndex = (arr[i] - minValue) / bucketSize;
if (indexArr[bucketIndex] == buckets[bucketIndex].length) {
ensureCapacity(buckets, bucketIndex);
}
buckets[bucketIndex][indexArr[bucketIndex]++] = arr[i];
}
// 对每个桶进行排序,这里使用了快速排序,快排代码省略了,这里理解桶排序即可
int k = 0;
for (int i = 0; i < buckets.length; i++) {
if (indexArr[i] == 0) {
continue;
}
quickSortC(buckets[i], 0, indexArr[i] - 1);
for (int j = 0; j < indexArr[i]; j++) {
arr[k++] = buckets[i][j];
}
}
}
/**
* 数组扩容
*
* @param buckets
* @param bucketIndex
*/
private static void ensureCapacity(int[][] buckets, int bucketIndex) {
int[] tempArr = buckets[bucketIndex];
int[] newArr = new int[tempArr.length * 2];
for (int j = 0; j < tempArr.length; j++) {
newArr[j] = tempArr[j];
}
buckets[bucketIndex] = newArr;
}
计数排序
计数排序可以说是桶排序的一种特殊情况。
当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k。
我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
计数排序的思想:对于给定的输入序列中的每一个元素x,确定该序列中值小于等于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。
一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。
如何计数?
假设只有 8 个考生,分数在 0 到 5 分之间。这 8 个考生的成绩我们放在一个数组 A[8]中,它们分别是:2,5,3,0,2,3,0,3。
考生的成绩从 0 到 5 分,我们使用大小为 6 的数组 C[6]表示桶,其中下标对应分数。不过,C[6]内存储的并不是考生,而是对应的考生个数。
如何计数呢?思路是这样的:我们对 C[6]数组顺序求和,C[k]里存储小于等于分数 k 的考生个数。
我们从后到前依次扫描数组 A(从后到前扫描是为了算法的稳定性)。比如,当扫描到 3 时,我们可以从数组 C 中取出下标为 3 的值 7,也就是说,到目前为止,包括自己在内,分数小于等于 3 的考生有 7 个,也就是说 3 是数组 R 中的第 7 个元素(也就是数组 R 中下标为 6 的位置)。
当 3 放入到数组 R 中后,小于等于 3 的元素就只剩下了 6 个了,所以相应的 C[3]要减 1,变成 6。
计数排序的限制(计数排序的应用场景)
一、计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。
二、计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
如果考生成绩精确到小数后一位,我们就需要将所有的分数都先乘以 10,转化成整数。
如果要排序的数据中有负数,数据的范围是[-1000, 1000],那我们就需要先对每个数据都加 1000,转化成非负整数。
计数排序的执行效率
计数排序的时间复杂度是O(n+k),其中k为数据范围。
计数排序的内存消耗
计数排序的空间复杂度为至少为O(k),若不希望破坏原数组,则空间复杂度为O(n+k)。
计数排序的稳定性
计数排序是稳定的排序算法,在扫描原数组的时候从后往前扫描可以保证算法的稳定性。
(后扫描到的相等数据,也就是说在原数组处于前面的相等数据,排序后仍保持其顺序)
计数排序的代码实现
public class CountingSort {
// 计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数。
public static void countingSort(int[] a, int n) {
if (n <= 1) return;
// 查找数组中数据的范围
int max = a[0];
for (int i = 1; i < n; ++i) {
if (max < a[i]) {
max = a[i];
}
}
// 申请一个计数数组c,下标大小[0,max]
int[] c = new int[max + 1];
// 计算每个元素的个数,放入c中
for (int i = 0; i < n; ++i) {
c[a[i]]++;
}
// 依次累加
for (int i = 1; i < max + 1; ++i) {
c[i] = c[i-1] + c[i];
}
// 临时数组r,存储排序之后的结果
int[] r = new int[n];
// 计算排序的关键步骤了,有点难理解
for (int i = n - 1; i >= 0; --i) {
int index = c[a[i]]-1;
r[index] = a[i];
c[a[i]]--;
}
// 将结果拷贝会a数组
for (int i = 0; i < n; ++i) {
a[i] = r[i];
}
}
}
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过 11 次排序之后,手机号码就都有序了。
这个思路有一个大前提就是:按照每位来排序的排序算法要是稳定的,否则这个实现思路就是不正确的。
因为如果是非稳定排序算法,那最后一次排序只会考虑最高位的大小顺序,完全不管其他位的大小关系,那么低位的排序就完全没有意义了。
根据每一位来排序,我们可以用刚讲过的桶排序或者计数排序,它们的时间复杂度可以做到 O(n)。
基数排序的限制(基数排序的应用场景)
一、基数排序要求数据可以分割出独立的“位”来比较,而且位之间有递进的关系
(如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了)
二、每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。
基数排序的执行效率
根据基数排序的适用场景,使用桶排序或者计数排序来给每一位排序。
如果要排序的数据有 k 位,那我们就需要 k 次桶排序或者计数排序,总的时间复杂度是 O(k*n)。
当 k 不大的时候,比如手机号码排序的例子,k 最大就是 11,所以基数排序的时间复杂度就近似于 O(n)。
基数排序的内存消耗
我们用计数排序对每一位排序,计数排序的空间复杂度为O(n+k),若k很小(如十进制就是0-9,为10),则近似为O(n)。
若数据为d位,则空间度复杂度为O(dn)。
基数排序的稳定性
基数排序是稳定的排序算法,因为基数排序要求按位排序的排序算法一定是要是稳定的。
基数排序的代码实现
public class RadixSort {
/**
* 基数排序
*
* @param arr
*/
public static void radixSort(int[] arr) {
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 从个位开始,对数组arr按"指数"进行排序
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSort(arr, exp); //这里每位用计数排序,基数排序实现也在文中
}
}