algorithm-base/animation-simulation/数据结构和算法/计数排序.md
2021-07-23 15:44:19 +00:00

225 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 计数排序
> 如果阅读时发现错误或者动画不可以显示的问题可以添加我微信好友 **[tan45du_one](https://raw.githubusercontent.com/tan45du/tan45du.github.io/master/个人微信.15egrcgqd94w.jpg)** ,备注 github + 题目 + 问题 向我反馈
>
> 感谢支持该仓库会一直维护希望对各位有一丢丢帮助
>
> 另外希望手机阅读的同学可以来我的 <u>[**公众号袁厨的算法小屋**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u> 两个平台同步,想要和题友一起刷题,互相监督的同学,可以在我的小屋点击<u>[**刷题小队**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u>进入。
今天我们就一起来看看线性排序里的计数排序到底是怎么回事吧
我们将镜头切到袁记菜馆
因为今年袁记菜馆的效益不错所以袁厨就想给员工发些小福利让小二根据员工工龄进行排序但是菜馆共有 100000 名员工菜馆开业 10 员工工龄从 0 - 10 不等看来这真是一个艰巨的任务啊
当然我们可以借助之前说过的 归并排序 快速排序解决但是我们有没有其他更好的方法呢
了解排序算法的老哥可能已经猜到今天写什么啦是滴我们今天来写写用空间换时间的线性排序
说之前我们先来回顾一下之前的排序算法最好的时间复杂度为 O(nlogn) ,且都基于元素之间的比较来进行排序
我们来说一下非基于元素比较的排序算法且时间复杂度为 On时间复杂度是线性的所以我们称其为线性排序算法
其优势在于在对一定范围内的整数排序时它的复杂度为 Ο(n+k)此时的 k 则代表整数的范围快于任何一种比较类排序算法不过也是需要牺牲一些空间来换取时间
下面我们先来看看什么是计数排序这个计数的含义是什么
我们假设某一分店共有 10 名员工
工龄分别为 1235022459
那么我们将其存在一个长度为 10 的数组里但是我们注意我们数组此时存的并不是元素值而是元素的个数见下图
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210327184632.ho3d12nf3q8.png)
此时我们这里统计次数的数组根据最大值来决定上面的例子中最大值为 9 则长度为 9 + 1 = 10暂且先这样理解后面会对其优化
我们继续以上图的例子来说明在该数组中**索引代表的为元素值**也就是上面例子中的工龄数组的值代表的则是元素个数也就是不同工龄出现的次数
即工龄为 0 的员工有 1 工龄为 1 的员工有 1 工龄为 2 的员工有 3
然后我们根据出现次数将其依次取出看看是什么效果
0122234559
我们发现此时元素则变成了有序的但是这并不是排序只是简单的按照统计数组的下标输出了元素值并没有真正的给原始数组进行排序
这样操作之后我们不知道工龄属于哪个员工
见下图
![微信截图_20210327202256](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210327202256.7g3nka7n0p40.png)
虽然喵哥和杰哥工龄相同如果我们按照上面的操作输出之后我们不能知道工龄为 4 的两个员工哪个是喵哥哪个是杰哥
所以我们需要借助其他方法来对元素进行排序
大家还记不记得我们之前说过的前缀和下面我们通过上面统计次数的数组求出其前缀和数组
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328131226.3x42hsrnna80.png)
因为我们是通过统计次数的数组得到了前缀和数组那么我们来分析一下 presum 数组里面值的含义
例如我们的 presum[2] = 5 ,代表的则是原数组小于等于 2 的值共有 5 presum[4] = 7 代表小于等于 4 的元素共有 7
是不是感觉计数排序的含义要慢慢显现出来啦
其实到这里我们已经可以理解的差不多了还差最后一步
此时我们要从后往前遍历原始数组然后将遍历到的元素放到临时数组的合适位置并修改 presum 数组的值遍历结束后则达到了排序的目的
![微信截图_20210328132549](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328132549.4w6kovlhtsa0.png)
我们从后往前遍历nums[9] = 9,则我们拿该值去 presum 数组中查找发现 presum[nums[9]] = presum[9] = 10 大家还记得我们 presum 数组里面每个值得含义不我们此时 presum[9] = 10,则代表在数组中小于等于的数共有 10 则我们要将他排在临时数组的第 10 个位置也就是 temp[9] = 9
我们还需要干什么呢我们想一下我们已经把 9 放入到 temp 数组里了已经对其排好序了那么我们的 presum 数组则不应该再统计他了则将相应的位置减 1 即可也就是 presum[9] = 10 - 1 = 9;
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328133100.5h5w473oi2s0.png)
下面我们继续遍历 5 然后同样执行上诉步骤
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328133401.23fulpjowbnk.png)
我们继续查询 presum 数组发现 presum[5] = 9,则说明小于等于 5 的数共有 9 我们将其放入到 temp 数组的第 9 个位置也就是
temp[8] = 5然后再将 presum[5] 1
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328133726.3s8wgrlcpzm0.png)
是不是到这里就理解了计数排序的大致思路啦
这个排序的过程像不像查字典呢通过查询 presum 数组得出自己应该排在临时数组的第几位然后再修改下字典直到遍历结束
那么我们先来用动画模拟一下我们这个 bug 版的计数排序加深理解
我们得到 presum 数组的过程在动画中省略直接模拟排序过程
![计数排序](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/计数排序.6y4quuwtxgw0.gif)
但是到现在就完了吗显然没有我们思考下这个情况
假如我们的数字为 9093949192 如果我们根据上面方法设置 presum 数组的长度那我们则需要设置数组长度为 95因为最大值是 94这样显然是不合理的会浪费掉很多空间
还有就是当我们需要对负数进行排序时同样会出现问题因为我们求次数的时候是根据 nums[index] 的值来填充 presum 数组的所以当 nums[index] 为负数时填充 presum 数组时则会报错而且此时通过最大值来定义数组长度也不合理
所以我们需要采取别的方法来定义数组长度
下面我们来说一下偏移量的概念
例如 9093949192我们 可以通过 max min 的值来设置数组长度即 94 - 90 + 1 = 5 偏移量则为 min 也就是 90
见下图
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328153724.61tvwfwjmmo0.png)
这样我们填充 presum 数组时就不会出现浪费空间的情况了负数出现负数的情况当然也可以继续看
例如-1-3021
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/微信截图_20210328154337.qtnhiuaixzk.png)
一样可以哦了到这里我们就搞定了计数排序下面我们来看一哈代码吧
Java Code:
```java
class Solution {
public int[] sortArray(int[] nums) {
int len = nums.length;
if (nums.length < 1) {
return nums;
}
//求出最大最小值
int max = nums[0];
int min = nums[0];
for (int x : nums) {
if (max < x) max = x;
if (min > x) min = x;
}
//设置 presum 数组长度,然后求出我们的前缀和数组,
//这里我们可以把求次数数组和前缀和数组用一个数组处理
int[] presum = new int[max-min+1];
for (int x : nums) {
presum[x-min]++;
}
for (int i = 1; i < presum.length; ++i) {
presum[i] = presum[i-1]+presum[i];
}
//临时数组
int[] temp = new int[len];
//遍历数组,开始排序,注意偏移量
for (int i = len-1; i >= 0; --i) {
//查找 presum 字典,然后将其放到临时数组,注意偏移度
int index = presum[nums[i]-min]-1;
temp[index] = nums[i];
//相应位置减一
presum[nums[i]-min]--;
}
//copy回原数组
System.arraycopy(temp,0,nums,0,len);
return nums;
}
}
```
Python Code:
```python
from typing import List
class Solution:
def sortArray(self,nums: List[int])->List[int]:
leng = len(nums)
if leng < 1:
return nums
# 求出最大最小值
max = nums[0]
min = nums[0]
for x in nums:
if max < x:
max = x
if min > x:
min = x
# 设置 presum 数组长度,然后求出我们的前缀和数组
# 这里我们可以把求次数数组和前缀和数组用一个数组处理
presum = [0] * (max - min + 1)
for x in nums:
presum[x - min] += 1
for i in range(1, len(presum)):
presum[i] = presum[i - 1] + presum[i]
# 临时数组
temp = [0] * leng
# 遍历数组开始排序注意偏移量
for i in range(leng - 1, -1, -1):
# 查找 presum 字典然后将其放到临时数组注意偏移度
index = presum[nums[i] - min] - 1
temp[index] = nums[i]
# 相应位置减一
presum[nums[i] - min] -= 1
# copy回原数组
nums = temp
return nums
```
好啦这个排序算法我们已经搞定了下面我们来扒一扒它
**计数排序时间复杂度分析**
我们的总体运算量为 n+n+k+n 总体运算是 3n + k 所以时间复杂度为 O(N+K)
**计数排序空间复杂度分析**
我们用到了辅助数组空间复杂度为 On
**计数排序稳定性分析**
稳定性在我们最后存入临时数组时有体现我们当时让其放入临时数组的合适位置并减一所以某元素前面的相同元素在临时数组仍然在其前面所以计数排序是稳定的排序算法
虽然计数排序效率不错但是用到的并不多
- 这是因为其当数组元素的范围太大时并不适合计数排序不仅浪费时间效率还会大大降低
- 当待排序的元素非整数时,也不适用,大家思考一下这是为什么呢?
好啦,今天的文章就到这啦,我们下期再见,拜了个拜.