写在前面:今天主要讨论时间复杂度为O(n)的排序算法。
- 计数排序
- 桶排序
- 基数排序
1.计数排序
算法分析:基于分布的排序要找到待排序数组中最大和最小的元素,辅助count数组,这里的意义是用来记录相对顺序下每个元素出现了多少次。
举个例子就能解释明白:
step1找最大最小是为了确定统计的区间。
nums={2,3,1,4,6,4,3,5,3,2},在step2计数完成后,count={1,2,3,2,1,1},它的意思是,应该放在第一个位置的元素出现了1次(也就是nums中的元素“1”),放在第二个位置的元素出现了2次(也就是nums中出现了两个2),以此类推。nums[i]-min表示nums[i]这个元素大小的相对位置。
step3是为了保证元素的相对顺序。
public int[] countSort(int[] arr){
//step1:找到最大最小
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max,arr[i]);
min = Math.min(min,arr[i]);
}
//step2:计数
int[] count = new int[max - min + 1];
for(int i = 0; i < arr.length; i++){
count[arr[i] - min]++;
}
//step3:累计
for(int i = 0; i < count.length - 1; i++){
count[i + 1] = count[i] + count[i + 1];
}
//step4:排序
int[] sorted = new int[arr.length];
for(int i = arr.length - 1; i >=0; i--){
int index = arr[i] - min;
sorted[count[index] - 1] = arr[i];
count[index]--;
}
return sorted;
}
如果数组大小为N,差值为M
时间复杂度:O(N+M)。step1,step2,step4的时间复杂度都是O(N),step3的复杂度是O(M)
空间复杂度:O(N + M)。统计数组count占用O(M),最后用来存储结果的数组O(N).
局限性:
1.当数列最大最小值差距过大时,并不适用计数排序。
比如给定20个随机整数,范围在0到1亿之间,这时候如果使用计数排序,需要创建长度1亿的数组。不但严重浪费空间,而且时间复杂度也随之升高。
2.当数列元素不是整数,并不适用计数排序。
如果数列中的元素都是小数,比如25.213,或是0.00000001这样子,则无法创建对应的统计数组。这样显然无法进行计数排序。
2.桶排序
当数列取值范围过大或者不是整数时,计数排序就不适用了,可以使用桶排序。
桶排序是一种线性时间复杂度的排序方法,需要桶来辅助排序。
下面的方法创建和待排序元素数量相同的桶,每个桶的取值区间为(max - min) / (bucketNum - 1);
定位数组中某个元素所在的桶:(arr[i] - min) * (bucketNum - 1) / (max - min ),是按比例计算的。
public double[] bucketSort(double[] arr){
//step1:找最大最小计算差值
double max = arr[0];
double min = arr[0];
for(int i = 1; i < arr.length; i++){
if(max < arr[i]) max = arr[i];
if(min > arr[i]) min = arr[i];
}
double d = max - min;
//step2:创建桶
int bucketNum = arr.length;
ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketList.add(new LinkedList<Double>());
}
//step3:元素放入桶中
for(int i = 0; i < arr.length; i++){
int index = (int)((arr[i] - min) * (bucketNum - 1) /d);
bucketList.get(index).add(arr[i]);
}
//step4:对每个桶中的元素分别排序
for(int i = 0; i < bucketList.size(); i++){
Collections.sort(bucketList.get(i));
}
//step5:得到排序后的元素
double[] sorted = new double[arr.length];
int index = 0;
for(List<Double> list : bucketList){
for(double num : list){
sorted[index++] = num;
}
}
return sorted;
}
复杂度分析:
假设原始数列有n个元素,分成m个桶(这里 m=n),平均每个桶的元素个数为n/m。
step1:求最大值和最小值,运算量为n。step2:创建桶,运算量为m。
step3:将所有元素放入桶中,运算量为n。
step4:在每个桶内部做排序,由于使用了O(nlogn)的排序算法,所以运算量为 n/m * log(n/m ) *m。
step5:构造有序数组,运算量为n。
总的运算量为 3n+m+ n/m * log(n/m ) * m = 3n+m+n(logn-logm) 。
去掉系数,时间复杂度为:
O(n+m+n(logn-logm))
这里n = m,因此时间复杂度为O(n)
如果桶内元素分布情况不均衡,极端情况下所有元素都在一个桶内,会退化成O(nlogn)。
空间复杂度:
桶占用的空间O(m) + 待排序元素在桶中占用的空间O(n)
O(m+n)。
这里n = m,因此空间复杂度为O(n)
3.基数排序
根据待排序元素每一位进行排序,有低位到高位以及从高位到低位的方法。
借助“分配”和“收集”两个操作对关键字的每一位进行排序
//基数排序
public int[] radixSort(int[] arr,int d){
int n = 1;//位数
int len = arr.length;
int[][] bucket = new int[10][len];//桶子数组
int[] count = new int[10];//每一位有几个元素出现
while(n < d){
//step1:分配
for(int i = 0; i < len; i++){
int digit = (arr[i]/n) % 10;//当前排序位数上是几
bucket[digit][count[digit]] = arr[i];
count[digit]++;
}
//step2:收集
int k = 0;
for(int i = 0; i < 10; i++){
if(count[i] != 0){
for(int j = 0; j < count[i];j++){
arr[k++] = bucket[i][j];
}
}
count[i] = 0;
}
k = 0;
n *= 10;
}
return arr;
}
空间复杂度:用了一个二维数组10*n,复杂度为O(n)
时间复杂度:假设位数为s,r为基数(10进制r=10),基数排序需要s趟分配和收集,一趟分配需要O(n),一趟收集需要O(r),最多O(n),所以时间复杂度为O(s(n+r)),与序列初始状态无关
稳定性:稳定。按位排序是稳定的。