这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0
1. 不基于比较的排序
1.1 基于比较的排序
就是制定一个排序的标准, 定义一个比较器, 是通用的. 最小最小的时间复杂度也有:O(n * log(n))
.
1.2 不给予比较的排序
对于数据特征是有要求的, 但是可以将时间复杂度压到 O(n)
. 比如计数排序(对范围很敏感).
2. 基数排序
基数排序所需要的数字特征和要求都在上面的图片中说明了.
2.1 逻辑实现
设置一个数组:arr[] = [23, 51, 42, 24, 6]
. 这个数字中最大的数字是:51
, 这个数字有两个位数:十位和个位, 所以我们要分两次准备桶, 第一次准备一个装个位数字的桶, 第二次准备一个装十位数字的桶.
- 先将所有的数字的个位数字设置桶:
- 装
1
的桶:51
, - 装
2
的桶:42
, - 装
3
的桶:23
, - 装
4
的桶:24
, - 装
6
的桶, - 然后按照桶由小到大的顺序将所有的数字倒出:
arr[] = [51, 42, 23, 24, 6]
.
- 装
- 然后将所有数字的十位数字设置桶:
- 装
0
的桶:6
, - 装
2
的桶:23, 24
, (将来倒出的时候:先进去的数字先出去). - 装
4
的桶:42
, - 装
5
的桶:51
. - 然后将桶的顺序由小到大的顺序将所有的数字倒出:
arr[] = [6, 23, 24, 42, 51]
.
- 装
- 最后排好序:
arr[] = [6, 23, 24, 42, 51]
.
注意:在个位数字排好序之后, 在十位数字排序的时候, 会保留本身的相对次序,
例子:arr[] = [21, 22, 23, 24, 25]
,
- 先进行个位数字:
- 装
1
的桶:21
, - 装
2
的桶:22
, - 装
3
的桶:23
, - 装
4
的桶:24
, - 装
5
的桶:25
. - 将所有的数字倒出:
arr[] = {21, 22, 23, 24, 25}
.
- 装
- 然后进行十位数字:
- 装
2
的桶:21, 22, 23, 24, 25
.(是先进先出的). - 将所有的数字倒出:
arr[] = {21, 22, 23, 24, 25}
.
- 装
2.2 如何设置桶, 优化实现
2.2.1 第一个优化:前缀和
有一个数组:arr[] = {3, 0, 3, 2, 1, 3, 0, 0, 1, 2, 2}
. 有 11
个数字, 此时我们希望将所有的数字都放到 help数组
中,
此时我们先统计个位中小于等于自己的有几个数字:0位:3个, 1位:5个, 2位:8个, 3位:11个
, 然后我们将 arr数组
中的数字从后往前遍历,
2
放到help数组的 7 位置
, 因为已经统计过了:2位有 8 个
, 所以对应的可以将2
放到7
位置, 因为这个2
是所有数字中最后一个位置的2
, 因为我们想要保持相对次序, 所以对应的, 我们要将2
放到2
最后的位置.- 然后将
2位置的数字 - 1
, 此时2位:7个
. - 此时来到倒数第二个数字, 还是
2
, 此时将其放到6位置
, 因为这个2
是剩下的所有2
里最后一个2
, 所以要和原来一样, 放到6位置
, 然后将2位置的数字 - 1
, 此时2位:6个
. - 此时来到倒数第三个数字, 是
1
, 此时将其放到4
位置, 因为这个1
是剩下的所有1
里最后一个1
, 所以放到4位置
, - 然后将
1位置的数字 - 1
, 此时:1位:4个
. - 之后的操作一直和上述一样. 可以是所有的数字都保持相对次序.
这个方法扩展之后:比如:arr[] = {290, 45, 33, 111}
, 还是只看个位, 按照上述方式进行排序. 然后按照十位, 还是安装上述方式进行排序.
2.2.2 如何得到每一个数字的位数
比如:提取 17293
这个数字的个位, 十位, 百位, 千位, 万位.
先设置一个变量:offset = 1
, 和个位数字对齐, 然后将 (17293 / 3) % 10
. 这样就能提取出个位数字:3
.
然后将 offset * 10
, 此时和十位数字对齐, 然后还是将 (17293 / 3) % 10
. 这样就能提取出十位数字:9
.
以此类推, 每一次都 将 offset * 10
.
3. 基数排序的代码实例
// 可以设置进制,不一定10进制,随你设置
public static int BASE = 10;
public static int MAXN = 100001;
public static int[] arr = new int[MAXN];
public static int[] help = new int[MAXN];
public static int[] cnts = new int[BASE];
public static int n;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
n = (int) in.nval;
for (int i = 0; i < n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
sort();
for (int i = 0; i < n - 1; i++) {
out.print(arr[i] + " ");
}
out.println(arr[n - 1]);
out.flush();
out.close();
br.close();
}
public static void sort() {
// 如果会溢出,那么要改用long类型数组来排序
// 找到数组中的最小值
int min = arr[0];
for (int i = 1; i < n; i++) {
min = Math.min(min, arr[i]);
}
int max = 0;
for (int i = 0; i < n; i++) {
// 数组中的每个数字,减去数组中的最小值,就把arr转成了非负数组
arr[i] -= min;
// 记录数组中的最大值
max = Math.max(max, arr[i]);
}
// 根据最大值在BASE进制下的位数,决定基数排序做多少轮
radixSort(bits(max));
// 数组中所有数都减去了最小值,所以最后不要忘了还原
for (int i = 0; i < n; i++) {
arr[i] += min;
}
}
// 返回number在BASE进制下有几位
public static int bits(int number) {
int ans = 0;
while (number > 0) {
ans++;
number /= BASE;
}
return ans;
}
// 基数排序核心代码
// arr内要保证没有负数 // 要是有负数, 就将数组中所有的数字加上数组中的最小值, 要是溢出了就用long类型.
// m是arr中最大值在BASE进制下有几位, BASE的意思是进制的意思, 此时我们先将BASE理解为 10 进制.
public static void radixSort(int bits) {
// 理解的时候可以假设BASE = 10
for (int offset = 1; bits > 0; offset *= BASE, bits--) { // bits就代表位数, 而且是所有数字中最大的位数.
Arrays.fill(cnts, 0); // 先将cnts数组的所有数字都填充为0.用来为以后记录每一个位数出现的次数做铺垫.
for (int i = 0; i < n; i++) {
// 数字提取某一位的技巧
cnts[(arr[i] / offset) % BASE]++; // 将个位数字对应的数字提取出来然后将数组对应的数字++.
}
// 处理成前缀次数累加的形式
for (int i = 1; i < BASE; i++) {
cnts[i] = cnts[i] + cnts[i - 1];
}
for (int i = n - 1; i >= 0; i--) { // 一定要从右往左遍历,
// 前缀数量分区的技巧
// 数字提取某一位的技巧
help[--cnts[(arr[i] / offset) % BASE]] = arr[i];
} // 根据每一个数字不同位数的值放到help数组中, 将cnts数组中对应的位置的数字 “--” 之后, 将arr数组中的数字放到help数组中.
for (int i = 0; i < n; i++) {
arr[i] = help[i]; // 将help数组中数字刷回到arr数组中.
}
}
}
3.1 分析复杂度
时间复杂度:O(n)
.
空间复杂度:O(m)
. m
指的是自己要决定的进制. 就是需要几个桶.
4. 计数排序的代码实例
public class CountingSort {
public static void main(String[] args) {
int[] arr = {4, 2, 2, 8, 3, 3, 1};
System.out.println("原始数组: " + Arrays.toString(arr));
countingSort(arr);
System.out.println("排序后的数组: " + Arrays.toString(arr));
}
public static void countingSort(int[] arr) {
if (arr == null || arr.length == 0) {
return; // 如果数组为空,直接返回
}
// 找到数组中的最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 创建计数数组
int[] count = new int[max + 1];
// 统计每个元素的出现次数
for (int num : arr) {
count[num]++;
}
// 将计数数组中的元素按顺序填回原数组
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
arr[index++] = i; // 根据计数填充原数组
count[i]--;
}
}
}
}