计数排序

摘抄自:https://www.cnblogs.com/developerY/p/3166462.html
https://blog.csdn.net/gaoruxue918/article/details/61467416

      它是一个不需要比较的,类似于桶排序的线性时间排序算法。该算法是对已知数量范围的数组(元素必须是整数)进行排序。其时间复杂度为O(n),适用于小范围集合的排序。计数排序是用来排序0到100之间的数字的最好的算法。
基本思想:

对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。它创建一个长度为这个数据范围的数组C,C中每个元素记录要排序数组中对应记录的出现个数。

      为了更好的理解计数排序,我们先来想象一下如果一个数组里所有元素都是整数,而且都在0-k(这里的k可不是数组长度也不是数组下标,而是数组中最大的数值)以内。那对于数组里每个元素来说,如果我能知道数组里有多少项小于或等于该元素。我就能准确地给出该元素在排序后的数组的位置。
      假设已知数组a = [1,0,3,1,0,1,1],我知道了a数组的长度为7,且里面有0,1,3三种数值,那么比3小的有6个,是不是就可以确定3应该排在最后呢?比1小的有两个,那么1是不是从第三位开始排列呢?
      这时候我应该循环遍历一次分别得到0,1,3在a数组中出现的次数即2,4,1(0出现了两次,1出现了4次,2没有,3出现了1次),我可以创建一个数组c,长度为4,然后c[0] = 2, c[1] = 4, c[2] = 0, c[3] = 1。这个数组c又是怎么回事呢?
      其实c数组的下标 = a数组的元素(所以要求a数组元素都是整数)。但是由于a数组里面没有2却有3,那总不能让c数组没有下标2吧,所以c[2] = 0但是c[3] = 1且c的长度为4

      通过上面的例子我可以知道,c.length(c是需要给我用来过渡的数组) = k(k是需要排序的数组内最大的数值) + 1,那么a(a是需要排序的数组)里面的所有数值应该都在0~k之间,但是,a可能并不包含所有0~k之间的数值!那么a中没有的数值假设为x,c[x]应该等于0

      到了这一步,我已经看到了胜利的曙光!
      接下来可以创建一个用于保存排序后的数组b,只需要对刚刚过渡的数组c进行遍历然后存数据即可。也就是b[0] = 0, b[1] = 0, b[2] = 1, b[3] = 1, b[4] = 1, b[5] = 1, b[6] = 3

      自己动手实现一遍,看自己是不是真的能融会贯通:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class CountSort {
private static int[] countSort(int[] array,int k) {
int sum = 0;
int B[] = new int[array.length]; // 最终用于返回的数组
int C[] = new int[k + 1]; // 用于过渡的数组
// 接下来我需要通过for循环遍历,得到每个元素在array里面出现的次数
for(int i = 0; i < array.length; i++) {
// 记住:array数组中的元素是 C 数组的下标
// array[i]肯定是存在的,所以默认有一个,至于有多少个,通过遍历不断 +1 即可
// 比如这里array[0]是2,但是array[4]也是2.所以这里 array[0] == array[4] == 2,
// 意味着 C[2] 出现两次,所以 C[2] = 2,所以每当出现过相同的array[i]时就 +1
C[array[i]] += 1;
}
// 这里面有一个巧妙的思路就是利用数组下标,他会自动帮我们排序,毕竟数值是C数组的下标。
// 这个时候我该考虑怎么把数据从 C 数组中取出来放到 B 数组了
for(int i = 0; i < k + 1; i++) {
// 这时候的 C[i], i 其实是 array里面的元素, C[i] 其实是该元素的个数
// i 从0开始递增到 k 即最大值,但是,我能保证i包含 0~k中所有的数值吗?
// 对不起,我不能保证!!!
// 但是,不用担心!编译器会自动帮我们把数组中没有值的但是却必须存在的下标对应的值初始化为 0
if(C[i] != 0) { // 代表存在该值
int x = C[i]; // 过渡下
// 由于C[i] 其实对应的是 A 数组里相同数据存在的个数,那么我只需对个数遍历即可。
// 但是我这里又困住了,第一轮循环从 0 开始确立前几个数组元素是没问题的,比如第一次遍历的是数值为0的个数并进行赋值
// 关键问题是当第二轮循环,开始遍历数值为2的个数进行赋值时,我不可能继续从B[0]开始
// 我应该紧接着上一轮循环后的下标开始,所以这里我需要引入一个变量 sum
for(int j = 0; j < x; j++) {
// 这里一开始我写的是B[j] = i,经过实践发现这是不对的,后来我意识到了不能每次都从0开始,
// 就改成了 B[sum + j] = i,依然是错误的。因为sum++不断自增,最终sum是会跟array.length相等,加上j就明显超过数组长度了
// 我也尝试修改 j < x+sum,继续打了脸,这里的 x 再三强调是相同数值的个数,我不能凭空给它增加啊
B[sum] = i;
sum++;
}
}
}
return B;
}
public static void main(String[] args) {
int[] A = new int[]{2,5,3,0,2,3,0,3};
int[] B = countSort(A, 5);
for(int i = 0; i < B.length; i++) {
System.out.println("B[" + i + "] = " + B[i]);
}
}
}

      但是,仔细看我的实现代码,用到了三个for循环,其中两个是嵌套的,这样效率并不算好吧。都说在小范围内使用计数排序效率高,可按我这样写怎么也跟高效率扯不上关系。而且算法思想说找到统计不大于数值的总数就能确定位置,我这好像有点驴头不对马嘴。那么该怎么做呢?直接贴出网上大神的代码吧。

网上的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.lizz.set;
public class CountSort {
private static int[] countSort(int[] array,int k) {
int[] C = new int[k+1]; // 构造C数组
int length = array.length, sum = 0; // 获取A数组大小用于构造B数组
int[] B = new int[length]; // 构造B数组
for(int i = 0; i < length; i++) {
C[array[i]] += 1; // 统计A中各元素个数,存入C数组
}
for(int i = 0; i < k+1; i++) { // 修改C数组
System.out.println(C[i]);
sum += C[i];
C[i] = sum;
// System.out.println(C[i]);
}
for(int i = length-1; i >= 0; i--) { // 遍历A数组,构造B数组
B[C[array[i]]-1] = array[i]; // 将A中该元素放到排序后数组B中指定的位置
C[array[i]]--; // 将C中该元素-1,方便存放下一个同样大小的元素
}
return B; // 将排序好的数组返回,完成排序
}
public static void main(String[] args) {
int[] A = new int[]{2,5,3,0,2,3,0,3};
int[] B = countSort(A, 5);
for(int i = 0; i < A.length; i++) {
System.out.println((i+1) + "th:" + B[i]);
}
}
}
1th:0
2th:0
3th:2
4th:2
5th:3
6th:3
7th:3
8th:5

      网上的代码中,第二个for循环我没看明白,然后我就打印了下C[i],C[i] = sum之前打印出是2,0,2,3,0,1,分别对应0,1,2,3,4,5出现的次数;重新赋值后打印出来是2,2,4,7,7,8sum最终是8,到了第三个for循环,array[i]3,0,3,2,0,3,5,2,C[array[i]]72641583
      这里我想了半天,为什么要重新赋值?后来在 https://www.cnblogs.com/lustar/p/7308089.html 这里的注释找到了答案。这里修改C[i]其实是为了统计不大于相应数值的总数。因为思想里说了,我要得到不大于某个数值的总数就能得到它在数组中的正确位置了。
      既然理解了这里的赋值修改,那么修改后的C[0]是2,代表不大于0的数有两个。以此类推。
      那么第三个for循环内部的代码是什么意思呢?我将它剖开分析。

  • array[i]其实就是原数组对应的元素同时也是C数组的对应下标!
  • 但是此时的C[array[i]]得到的值其实是不大于array[i]的值的总数假设为x(包含array[i]对应的个数)。那么是不是意味着array数组中 <= array[i]的元素一共有x个?不过我也不能保证array[i]对应的值在数组中唯一且没有相等的!那么是不是意味着与array[i]对应的值相等的数据中一定有一个排序后应该在数组中第x-1位(这里的x是个数,从1开始,而数组下标是从0开始的)?
    如果数组中有其他与array[i]对应的值相等,那么我该怎么确定这些相等的值排序顺序呢?
    这里不得不佩服前辈们的思想了,用for循环从后向前遍历!!!为什么???

          因为i是从后向前遍历的,那么即使数组中存在多个值与array[i]相等也不用慌,直接先排array[i]就好了,毕竟即使几个值相等,我为了保证数组稳定性,那么在原数组中下标较大的自然在新数组中所有相同的值里面排在相对靠后位置
          那么接下来的C[array[i]]--也应该很明白了。
          我们想想,i一开始是8-1=7,array[7] == 3,C[3]代表不大于3的总数一共有7个。而array中除了array[7]等于3外还有array[2]array[5]的值都是3,那么是不是i循环遍历到2和5时,都是C[3]?可我在i == 7时已经将array[7]排好位置了,它不应该继续参与排序了,所以有相同值时,应该排好一个减去一个。

总结

      最后,做个象征性的总结,当我们学习时,一定要手动去实践,有不懂得或者不是特别理解的就去实践,实践不仅是检验真理的唯一标准,还是提升技术的最强方法。

Newer Post

为什么在 Java 中用 (low+high)>>>1 代替 (low+high)/2 或 (low+high)>>1 来计算平均值呢?好在哪里?

抄自:https://www.cnblogs.com/zt007/p/7169735.html?utm_source=itdadao&amp;utm_medium=referral &gt;&gt;&gt;与&gt;&gt;是位运算符,只对整型有效(不能用于浮点型)。当是整型的时候(low+hig …

继续阅读
Older Post

java sort 方法源码学习

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在学习《Thinking in java》集合一章时,快到末尾时讲解了排序和搜索中用到了sort(),点进去看了下源码,发现第一遍看起来还有点不甚了解,遂专开一篇来学习理解。 一、sort(byte[] a, int left, …

继续阅读