为什么学了快排等优秀的排序还要学习基数排序?因为基数排序有其他让人意象不到的快处,末尾介绍。
故事
"你们是我教过最差的一届, 这都要期末考试了, 还考成这样, 从今天起每天一场模拟考试, 越到后面考试越是重要, 大家知道吗?"班主任猛的一拍桌子,整个峡谷为之震动。于是每天, 学生们要进行苦逼的考试, 还要进行排名,你说苦不苦。而且,胆敢有不参加考试的同学,当天考试成绩一律按照零分进行排名。但是有谁知道呢, 险恶的班主任竟然用这些考试成绩最为最后排名的一些依据: 最后期末考试成绩相同的排名, 按照最近的一次考试排名, 如果仍然有相同, 再按这次考试最近一次考试的成绩排名, 依次类推, 用来保证排名的公正性 。
(老师其实就相当于要去完成了一次广义的基数排序啦。)
基数排序
具体到某个数字,每个数字影响力最大的其实就是最高位的数字,比如“520”中的"5"。但是由于位数不同,只比较很多数字的最高位是无法得出大小关系的, 比如"9"和"80",“9"的最高位是"9”,“80"的最高位是"8”,“9”>“8”,但是"9"<“80”,就像不可能用别人的期末考试成绩和自己的月考来比,这毫无可比性啊!于是我们可以通过补零来挽救这种情况,“9”写成“09”就可以直接按位来比较的。(怪不得老师要强制把没来人的那场考试当零分来计算)
位数相同了以后(既然同学们每场试都有排名了),我们就可以进行数字的排序了(同学们的排名就有了)。就是按最高位排序,如果相同的话就接着比较下一位,依次类推得到总的排名。(“期末考试成绩相同的话,就按最近的平时成绩来”,老师:“不愧是我”)
如何实现
令老师头痛的是怎么快速的进行这样的排序呢?毕竟我们老师做什么都要快。而作为班上的顶级快枪手的你也一样,于是你向老师喃喃的说道为什么不从“小”的开始呢?
“对啊,从第一次考试开始往后接着排序并一直维持上一次的基本顺序,这样递推到最后就可以了,可是,那具体如何实现呢?”
“哎,就想看h书一样,万般描述都是虚无的,咱们还是看图吧!”
现在我们要排序如下的数组, 然后如图是最基本的实现效果
具体到实践操作中:
- 首先构建是十个桶,就是十个容器, 用来存储数字,如图
2. 一开始,按位将数字放入桶中,每个桶中的数字的共同的数字特征是最低位相同(位次依次类推),比如“542”的最低位是2,就放在2号桶中,看图:
-
然后按顺序取出。判断是否已经取到最高位,如果已经取到最高位则退出
-
此时再按下一位的数字,此时也就是十位来依次放入,如果有数字“2”不满十位, 就补零,理由如上面所讲,相当于回到步骤2:
当步骤2条件满足就跳出, 这个时候就已经排序好了
代码实现
到了最重要的代码实现环节了,而在实际代码中, 桶中存储的其实是桶中每个数字的最高排名,拿出时则是已经排序好了的,具体实现见代码和详细注释。
注:以下代码只适用于正整数,对于负数则需要开二倍数组
int a[] = {1,6,4231,353,34,53};
//v: 序列存储
vector<int> v(a,a+sizeof(a)/sizeof(a[0]));
//序列中最高位所在值
int m = *max_element(v.begin(),v.end());
//bucket: 桶 exp: 当前位数指示,比如去十位,则exp = 10
int bucket[10],exp = 1;
//res: 暂存排序好的序列
int res[1000];
while(exp<m){
//初始化桶,清空桶内原有数字
memset(bucket,0,sizeof(bucket));
//按位入桶, 获取每个桶内有多少个满足条件的数字
for(int i = 0;i<v.size();i++) bucket[v[i]/exp%10]++;
//通过累加bucket内的值,从而得到各个桶内数字的在一轮结果中的下标
for(int i = 1;i<10;i++) bucket[i] +=bucket[i-1];
//逆序遍历,得到第一轮排序好的数组,逆序是因为这样可以不打乱原来的排序
for(int i = v.size()-1;i>=0;i--) res[--bucket[v[i]/exp%10]] = v[i];
//将第一轮排序好的数组赋值给原数组
for(int i = 0;i<v.size();i++) v[i] = res[i];
//转到下一位
exp*=10;
}
这只是正整数的排序, 其实基数排序不仅仅可以使用在这一个方面,还能做许多其他排序做不到的事情,比如数据结构进阶中的后缀数组。
既然会了基数排序, 还不赶紧去学一下后缀数组?
相信大家到这里已经学会了基数排序的运用, 如果有需要改正或者需要补充的地方欢迎评论区留言。