5. 其他非基于比较的排序
5.1 计数排序
有n个数,取值范围是 0~n,写出一个排序算法,要求时间复杂度和空间复杂度都是O(n)的
我们知道,前面介绍的基于比较的排序算法中,最好的算法,其平均时间复杂度都在O(N),达到线性的时间复杂度就要使用新的排序算法,而这种方法,就称为是计数排序。
计数排序的思路:对于每一待排序元素a,如果知道了待排序数组中有多少比它小的数,就可以直接知道排序后的数组中,a在什么位置上。比如,如果一个数组中有三个数比a小,那么排序后的数组中,a一定会出现在第4位。
那么现在问题转化成,堆排序数组里的数,如何能快速的指导比它小的数字有多少。要解决这个问题,我们不能用比较的办法,因为时间复杂度会高于O(N),只有一个思路,就是用空间来换取时间。不妨设一个大小为(max - min + 1)的数组(其中max是数组中的最大值,min是数组中的最小值)来统计每个数字出现的次数。这就类似于直方图,因此计数排序被称作是基于统计的排序。
假设计数排序算法的输入是数组arr,大小为n,而存放排序结果的数组为B,还需要一个 存放临时结果,也就是我们上面提到的直方图结果的数组count。那么计算排序的步骤如下:
-
在C中记录A中各值元素的数目
for (int i = 0; i < len; i++) { count[arr[i]]++; }
-
将count[i]转换成小于等于i的元素个数
for (int i = 1; i < max + 1; i++) { count[i] += count[i-1]; }
-
为A数组从后向前的每个元素找到对应的B中的位置。每次从A中复制一个元素到B中,C中相应的数-1。
for (int i = len - 1; i >= 0; i--) { int n = count[arr[i]]; b[n - 1] = arr[i]; count[arr[i]]--; }
-
当A中的元素都复制到B中后,B就是排好序的结果。然后再将结果赋值给A。
下面为完整代码
public void countSort(int[] arr) {
int len = arr.length;
int max = arr[0];
for(int i = 1; i < len; i++) {
if(arr[i] > max) {
max = arr[i];
}
}
int[] count = new int[max + 1];//以数据的范围作为计数数组的大小
int[] b = new int[len];
for (int i = 0; i < len; i++) {
count[arr[i]]++;
}
for (int i = 1; i < max + 1; i++) {
count[i] += count[i-1];
}
for (int i = len - 1; i >= 0; i--) {
int n = count[arr[i]];
b[n - 1] = arr[i];
count[arr[i]]--;
}
for (int i = 0; i < len; i++) {
arr[i] = b[i];
}
}
需要注意的是,出现多个元素相同的情况时,每当arr[i]放入b中,count[arr[i]]都要减一,这样,下一个与arr[i]相等的元素出现的时候,被放在arr[i]的前面,从而保证了数组的稳定性。
总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(N)
- 空间复杂度:O(N)
- 稳定。
计数排序还有另外一种做法:只需要一个待排数组和一个计数数组就能做。
大体思路就是:
-
找出待排序元素中的最大值和最小值,确定待排序元素的数据范围,然后根据范围确定计数数组的大小。
int min = arr[0]; int max = arr[0]; for (int i = 0; i < arr.length; i++) { if(arr[i] < min) { min = arr[i]; } if (arr[i] > max) { max = arr[i]; } } int len = max - min + 1; int[] count = new int[len];
-
遍历arr数组,记录每个待排序元素出现的次数。这里要注意的是,怎么将count数组下标和arr[i]的之联系起来。
举个例子:
arr = [91,92,99,92,93,92,99,98,94], max = 99, min = 91
我们应该这样进行计数:
我现在要记录
91
出现的次数,91
应该放在计数数组的0下标,也就是count[0],91 - min = 0
,所以我们可以推出来一个等式,设count下标为n,设当前待排序元素的数值为val,n + min = val
for (int i = 0; i < arr.length; i++) { count[arr[i] - min]++; }
-
上述循环走完,计数数组已经存好了对应关系,遍历计数数组,给arr数组重新赋值。
int k = 0; for (int i = 0; i < len; i++) { int n = count[i];//记录元素出现的次数 while (n != 0) { arr[k++] = i + min;//运用等式关系 n--; } }
下面为完整代码:
public static void countArray(int[] arr) {
int min = arr[0];
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i] < min) {
min = arr[i];
}
if (arr[i] > max) {
max = arr[i];
}
}
int len = max - min + 1;
int[] count = new int[len];
for (int i = 0; i < arr.length; i++) {
count[arr[i] - min]++;
}
int k = 0;
for (int i = 0; i < len; i++) {
int n = count[i];
while (n != 0) {
arr[k++] = i + min;
n--;
}
}
}
这个方法写出来的排序不一定是稳定的,但是计数排序一定是稳定的。
5.2 桶排序
桶排序的思想就是:划分成多个范围相同的区间,将待排序元素放入对应的区间,每个区间内部进行排序,最后合并。
桶排序可以看成计数排序的扩展版本,计数排序可以看作每个桶放相同的元素,而桶排序中每个桶储存一定范围内的元素。
知道了思想,我们就可以写出相应代码:
-
得出最大值和最小值。
int max = arr[0]; int min = arr[0]; for(int i = 1; i < arr.length; i++){ max = Math.max(max, arr[i]); min = Math.min(min, arr[i]); }
-
计算桶的数量,桶可以用ArrayList来表示。
int count = (max - min) / arr.length + 1; ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(count); for(int i = 0; i < count; i++){ bucketArr.add(new ArrayList<>());//分配内存 }
-
将每个元素放入相应桶内
for(int i = 0; i < arr.length; i++){ int num = (arr[i] - min) / (arr.length); bucketArr.get(num).add(arr[i]); }
-
对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){ Collections.sort(bucketArr.get(i)); //这个方法是专门对实现List接口的数据结构进行排序 }
-
将桶中的元素赋值给原数组
int index = 0; for(int i = 0; i < bucketArr.size(); i++){ for(int j = 0; j < bucketArr.get(i).size(); j++){ arr[index++] = bucketArr.get(i).get(j); } }
完整代码:
public static void bucketSort(int[] arr){
int max = arr[0];
int min = arr[0];
for(int i = 1; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
int count = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(count);
for(int i = 0; i < count; i++){
bucketArr.add(new ArrayList<>());//分配内存
}
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
//这个方法是专门对实现List接口的数据结构进行排序
}
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
}
总结
-
时间复杂度:O(N*(log(N/M)+1))
设数组元素有N个,桶有M个,平均每个桶有N/M个元素。
主要步骤有
- 每个元素放入桶中O(N)
- 对每个桶进行排序,一共有M次,每次复杂度为O((N/M)*log(N/M)),所以为O(M*(N/M)*log(N/M))
O(N) + O(M*(N/M)*log(N/M)) = O(N*(log(N/M)+1))
当N == M时,复杂度为O(N)
-
空间复杂度:O(M + N)
-
稳定性:取决于桶内排序的算法
5.3 基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序也运用到了桶,根据元素的每位数字来分配桶
下面是具体步骤:
-
获得待排序元素中,最大元素的位数
int maxDigit = getMaxDigit(arr); private static int getMaxDigit(int[] arr) { int maxValue = getMaxValue(arr);//获取最大元素 return getNumLength(maxValue);//获取最高位数 } private static int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int value : arr) { if (maxValue < value) { maxValue = value; } } return maxValue; } private static int getNumLength(long num) { if (num == 0) { return 1; } int length = 0; for (long temp = num; temp != 0; temp /= 10) { length++; } return length; }
-
根据元素的每一位数字,对其进行排序。
1.怎么遍历元素的每一位数字呢?
我们可以创建一个循环,循环的次数是最大位数,在每一次循环中遍历元素的每位数字。
2.那如何得到元素的每位数字呢?
举个例子,假如我们要得到361这个数字的个位数字1,先%10,得到1,再/1;要得到十位数字6,先%100,得到61,再/10,得到6;要得到百位数字3,先%1000,得到361,再/100,得到3。
发现规律了吗?随着位数从个位到百位,我们%的数字和/的数字也以10倍增长,这样我们便可以利用循环,在每次循环中,对此进行*10操作。
3.现在我们得到了每位数字,如何对他进行排序呢?
就如同上面的动图,我们可以创建一个二维数组arr[10][],分别对应0~9,然后把相应的数字放入数组中,然后再从arr[0][]开始,将数组中的元素一个个放回到原数组,就完成了本轮排序。
现在我们把比较重要的3步都已经理解清楚了,下面就是实现代码:
private static int[] sort(int[] arr, int maxDigit) { int mod = 10; int dev = 1; for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { int[][] counter = new int[20][0]; //这里设定成20,是因为考虑到了负数,[0,10)存储负数,[10,20)存储正数 for (int j = 0; j < arr.length; j++) { int bucket = ((arr[j] % mod) / dev) + 10; //(arr[j] % mod) / dev:得到每位数字 //+10是因为考虑了负数,比如说得到的该位数字为-1,就把他存储到arr[9]中。 counter[bucket] = arrayAppend(counter[bucket], arr[j]); //给数组扩容,因为一开始创建没有分配内存。然后添加元素 } int pos = 0; for (int[] bucket : counter) { for (int value : bucket) { arr[pos++] = value;//重新赋值 } } } return arr; } private static int[] arrayAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; }
完整代码如下:
public static void radixSort (int[] arr){
int maxDigit = getMaxDigit(arr);
sort(arr, maxDigit);
}
private static int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLength(maxValue);
}
private static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
protected static int getNumLength(long num) {
if (num == 0) {
return 1;
}
int length = 0;
for (long temp = num; temp != 0; temp /= 10) {
length++;
}
return length;
}
private static int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
private static int[] sort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
int[][] counter = new int[20][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + 10;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;//重新赋值
}
}
}
return arr;
}
总结
-
时间复杂度:O(k*N)
设最大位数为k位
外层循环O(k),内层循环O(N),整体是O(k*N)
-
空间复杂度:O(N)
-
稳定性:稳定
十大排序算法已经全部更新完啦!🎉累鼠了累鼠了💀
如果想了解其他的排序算法,可以移步到我的前两篇文章呦
如果有什么疑问,欢迎在评论区给我留言,或者是私信我~