首页新闻博问专区闪存班级 我的博客我的园子账号设置退出登录注册登录 个人公众号交流:bigsai bigsai 博客园首页新随笔联系订阅管理随笔 - 46 文章 - 0 评论 - 67 八大排序算法—16张图搞懂基数排序 原创公众号:bigsai 转载需联系笔者前言在排序算法中,大家可能对桶排序、计数排序、基数排序不太了解,不太清楚其算法的思想和流程,也可能看过会过但是很快就忘记了,但是不要紧,幸运的是你看到了本篇文章。本文将通俗易懂的给你讲解基数排序。基数排序,是一种原理简单,但实现复杂的排序。很多人在学习基数排序的时候可能会遇到以下两种情况而浅尝辄止:一看原理,这么简单,懂了懂了(顺便溜了)再一看代码,这啥啥啥啊?这些的肯定有问题(不看溜了)要想深入理解基数排序,必须搞懂基数排序各种形式(数字类型、等长字符类型、不等长字符)各自实现方法,了解其中的联系和区别,并且也要掌握空间优化的方法(非二维数组而仅用一维数组)。下面跟着我详细学习基数排序吧!基数排序原理首先百度百科看看基数排序的定义:基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,基数排序法的效率高于其它的稳定性排序法。基数排序也称为卡片排序,简而言之,基数排序的原理就是多次利用计数排序(计数排序是一种特殊的桶排序),但是和前面的普通桶排序和计数排序有所区别的是,基数排序并不是将一个整体分配到一个桶中,而是将自身拆分成一个个组成的元素,每个元素分别顺序分配放入桶中、顺序收集,当从前往后或者从后往前每个位置都进行过这样顺序的分配、收集后,就获得了一个有序的数列。在具体实现上如果从左往右那就是最高位优先(Most Significant Digit first)法,简称MSD法;如果从右往左那就是最低位优先(Least Significant Digit first)法,简称LSD法。但是不管从最高位开始还是从最低位开始要保证和相同位进行比较,你需要注意的是如果是int等数字类型需要保证从右往左(从低位到高位)保证对齐,如果是字符类型的话需要从左往右(从高位到低位)保证对齐。你可能会问为啥不直接将这个数或者这个数按照区间范围放到对应的桶中,一方面基数排序可能很多时候处理的是字符型的数据,不方便放入某个桶中,另一方面如果数字很大,不方便直接放入桶中。并且基数排序并不需要交换,也不需要比较,就是多次分配、收集得到结果。所以遇到这种情况我们基数排序思想很简单,就拿 934,241,3366,4399这几个数字进行基数排序的一趟过程来看,第一次会根据各位进行分配、收集:分配和收集都是有序的,第二次会根据十位进行分配、收集,此次是在第一次个位分配、收集基础上进行的,所以所有数字单看个位十位是有序的。而第三次就是对百位进行分配收集,此次完成之后百位及其以下是有序的。而最后一次的时候进行处理的时候,千位有的数字需要补零,这次完毕后后千位及以后都有序,即整个序列排序完成。想必看到这里基数排序的思想你也已经懂了吧,但是虽然懂你不一定能够写出代码来,继续看看下面的分析和实现。数字类型基数排序有很多时候也有很多时候对基数排序的讲解也是基于数字类型的,而数字类型这里就用int来实现,对于数字类型的基数排序你需要注意的有以下几点:无论是最高位优先法还是最低位优先法进行遍历需要保证数字各位、十位、百位等对齐,这里我使用最低位优先法从个位开始向上。数字类型的基数排序需要十个桶(0-9),你可以使用二维数组,第一维度长度为10表示十个数字,第二个维度为数组长度,用来存储数字(因为最坏情况可能当前位数字一样)。但这样无疑太浪费内存空间了,你可以使用List或者Queue替代,这里就用List了。具体实现要先找到最大值确定最高多少位,用来进行遍历时候确认。收集的时候借助一个自增参数遍历收集。每次收集完毕十个桶(bucket)需要清空待下次收集。实现的代码为:static void radixSort(int[] arr)//int 类型 从右往左
{
Listbucket[]=new ArrayList[10];
for(int i=0;i<10;i++)
{
bucket[i]=new ArrayList();
}
//找到最大值
int max=0;//假设都是正数
for(int i=0;i<arr.length;i++)
{
if(arr[i]>max)
max=arr[i];
}
int divideNum=1;//1 10 100 100……用来求对应位的数字
while (max>0) {//max 和num 控制
for(int num:arr)
{
bucket[(num/divideNum)%10].add(num);//分配 将对应位置的数字放到对应bucket中
}
divideNum*=10;
max/=10;
int idx=0;
//收集 重新捡起数据
for(Listlist:bucket)
{
for(int num:list)
{
arr[idx++]=num;
}
list.clear();//收集完需要清空留下次继续使用
}
}
}
等长字符串基数排序除了数字之外,等长字符串也是常常遇到的方式,其主要方法和数字类型差不多,这里也看不出策略上的不同。低位优先法或者高位优先法都可使用(这里依旧低位从右向左)。在实现细节方面,和前面的数字类型区别不是很大,但是因为字符串是等长的遍历更加方便容易。但需要额外注意的是:字符类型的桶bucket不是10个而是ASCII字符的个数(根据实际需要查看ASCII表)。其实就是利用char和int之间关系可以直接按照每个字符进行顺序存储。具体实现代码为:static void radixSort(String arr[],int len)//等长字符排序情况 长度为len
{
Listbuckets[]=new ArrayList[128];
for(int i=0;i<128;i++)
{
buckets[i]=new ArrayList();
}
for(int i=len-1;i>=0;i–)//每一位上进行操作
{
for(String str:arr)
{
buckets[str.charAt(i)].add(str);//分配
}
int idx=0;
for(Listlist:buckets)
{
for(String str:list)
{
arr[idx++]=str;//收集
}
list.clear();//收集完该bucket清空
}
}
}
非等长字符串基数排序等长的字符串进行基数排序时候很好遍历,那么非等长的时候该如何考虑呢?这种非等长不能像处理数字那样粗暴的计算当成0即可。字符串的大小是从前往后进行排列的(和长度没关系)。例如看下面字符串,“d”这个字符串即使很短但是在排序依然放在最后面。你知道该怎么处理吗?“abigsai”
“bigsai”
“bigsaisix”
“d”
如果高位优先,前面一旦比较过各个字符的桶(bucket)就要固定下来,也就是在进行右面下一个字符分配、收集的时候要标记空间,即下次进行分配收集的前面是‘a’字符的一组,‘b’字符一组,并且不能越界,实现起来很麻烦这里就不详细讲解了有兴趣的可以自行研究一下。而本篇实现的是低位优先。低位优先采用什么思路呢?很简单,跟我看图解。第一步,先将字符按照长度进行分配到一个桶(bucket)中,声明一个ListwordLen[maxlen+1];在遍历字符时候,以字符长度为下表index,将字符串顺序加入进去。其中maxlen为最长字符串长度,之所以要maxlen+1是因为需要使用maxlen下标(0-maxlen)。第二步,分配完成遍历收集到原数组中,这样原数组在长度上相对有序。这样就可以进行基数排序啦,当然,在开始的时候并不是全部都进行分配收集,而是根据长度慢慢递减,长度可以到达6位分配、收集,长度到达5的分配、收集……长度为1的进行分配、收集。这样进行一遭就很完美的进行完基数排序,因为我们借助根据长度收集的桶可以很容易知道当前长度开始的index在哪里。具体实现的代码为:static void radixSort(String arr[])//字符不等长的情况进行排序
{
//找到最长的那个
int maxlen=0;
for(String team:arr)
{
if(team.length()>maxlen)
maxlen=team.length();
}
//一个对长度分 一个对具体字符分,先用长度来找到
ListwordLen[]=new ArrayList[maxlen+1];//用长度先统计各个长度的单词
Listbucket[]=new ArrayList[128];//根据字符来划分
for(int i=0;i<wordLen.length;i++)
wordLen[i]=new ArrayList();
for(int i=0;i<bucket.length;i++)
bucket[i]=new ArrayList();
//先根据长度来一下
for(String team:arr)
{
wordLen[team.length()].add(team);
}
int index=0;//先进行一次(按照长度分)的桶排序使得数组长度初步有序
for(Listlist:wordLen)
{
for(String team:list)
{
arr[index++]=team;
}
}
//然后 先进行长的 从后往前进行
int startIndex=arr.length;
for(int len=maxlen;len>0;len–)//每次长度相同的要进行基数一次
{
int preIndex=startIndex;
startIndex-=wordLen[len].size();
for(int i=startIndex;i<arr.length;i++)
{
bucket[arr[i].charAt(len-1)].add(arr[i]);//利用字符桶重新装
}
//重新收集
index=startIndex;
for(Listlist:bucket)
{
for(String str:list)
{
arr[index++]=str;
}
list.clear();
}
}
}
空间优化(等长字符)基数排序上面无论是等长还是不等长,使用的空间其实都是跟二维相关的,我们能不能使用一维的空间去解决这个问题呢?当然能啊。在使用空间的整个思路是差不多的,但是这里为了让你能够理解我们在讲解的时候讲解等长字符串的情况。先回忆刚刚讲的等长字符串,就是从个位进行遍历,在遍历的时候将数据放到对应的桶里面,然后在进行收集的时候放回原数组。你能否发现什么规律?一个字符串收集的时候放的位置其实它只需要知道它前面有多少个就可以确定!并且当前位置字符如果相同那么就是根据arr中相对顺序来进行当前轮。所以我们可以尝试来动态维护这个int bucket[]。第一次进行只记录次数,第二次进行叠加表示比当前位置+1编号小的元素的个数。但是这样处理不太好知道比当前位置小的有多少,所以我们在分配的时候向下挪一位,这样bucket[i]就可以表示比当前位置小的元素的个数。我们在进行收集的时候需要再次遍历arr,但我们需要一个临时数组String value[]储存结果(因为arr没遍历完后面不能使用),而进行遍历的规则就是:遍历arr时候对应字符串str,该位字符对应bucket[str.charAt(i)]桶中数字就是要放到arr中的编号(多少个比它小的就放到第多少位),放置之后要对bucket当前位自增(因为下一个这个位置字符串要把这个str考虑进去)。这样到最后即可完成排序。第一趟遍历arr前两个字符串部分过程如下:第一趟中间两个字符串处理情况:第一趟最后两个字符串处理情况:就这样即可完成一趟操作,一趟完成记得将value的值赋值到arr中,当然有方法使用指针引用可以避免交换数据带来的时间影响,但这里为了使大家更加简单理解就直接复制过去。这样完成若干次,整个基数排序即可完成。具体实现的代码为:static void radixSortByArr(String arr[],int len)//固定长度的使用数组进行优化
{
int charLen=129;//多用一个
String value[]=new String[arr.length];
for(int index=len-1;index>=0;index--)//不同的位置
{
int bucket[]=new int[charLen];//储存character的桶
for(int i=0;i<arr.length;i++)//分配
{
bucket[(int)(arr[i].charAt(index)+1)]++;
}
for(int i=1;i<bucket.length;i++)//叠加 当前i位置表示比自己小的个数
{
bucket[i]+=bucket[i-1];
}
for(int i=0;i<arr.length;i++)
{
value[bucket[arr[i].charAt(index)]++]=arr[i];//中间的++因为当前位置填充了一个,下次再来同元素就要后移
}
System.arraycopy(value,0,arr,0,arr.length);//copy数组
}
}
至于不定长的,思路也差不多,这里就留给你优秀的你自己去思考啦。结语至于基数排序的算法分析,以定长的情况分析,假设有n数字(字符串),每个有k位,那么根据基数就要每一位都遍历就是K次,每次都是O(n)级别。所以差不多是O(n*k)级别,当然k远远小于n,可能有成千上万个数,但是每个数或者字符正常可没成千上万那么长。本次基数排序就全讲完啦,那么多张图我想你也应该懂了。最后我请你们两连事帮忙一下:点赞、关注一下支持, 您的肯定是我在平台创作的源源动力。微信搜索「bigsai」,关注我的公众号,不仅免费送你电子书,我还会第一时间在公众号分享知识技术。加我还可拉你进力扣打卡群一起打卡LeetCode。记得关注、咱们下次再见!好文要顶 关注我 收藏该文 bigsai
关注 - 1
粉丝 - 35 +加关注 0 0
« 上一篇: 面试官:缓存穿透、缓存雪崩和缓存击穿是什么? posted @ 2020-11-15 16:59 bigsai 阅读(136) 评论(0) 编辑 收藏
刷新评论刷新页面返回顶部
发表评论 【福利】注册AWS账号,立享12个月免费套餐 编辑预览 7693b08a-a8f6-49f3-f45a-08d88556cc23 Markdown 帮助自动补全 不改了退出 订阅评论 [Ctrl+Enter快捷键提交]
首页 新闻 博问 专区 闪存 班级 【推荐】News: 大型组态、工控、仿真、CADGIS 50万行VC++源码免费下载
【推荐】博客园 & 陌上花开HIMMR 给单身的程序员小哥哥助力脱单啦~
【推荐】了不起的开发者,挡不住的华为,园子里的品牌专区
【推荐】未知数的距离,毫秒间的传递,声网与你实时互动
【福利】AWS携手博客园为开发者送免费套餐与抵扣券
【推荐】 阿里云折扣价格返场,错过再等一年
相关博文:
· 16
· leetcode-16
· 作业16
· 16周作业
· 16次作业
» 更多推荐…最新 IT 新闻:
· 手残党福音:不会摘隐形眼镜?这个机器人可以帮你
· 我是如何被职场PUA毁掉的?
· 唯品会的去库存生意,还能玩多久?
· 美团“特价版”横空出世,王兴打响守城之战
· 马斯克终结美国载人航天的“寄俄篱下”
» 更多新闻…
公告 微信公众号:bigsai
回复进群即可加入LeetCode打卡计划,目前以后一百多位伙伴加入。 昵称: bigsai
园龄: 1年3个月
粉丝: 35
关注: 1 +加关注
< 2020年11月> 日一二三四五六 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 1 2 3 4 5 6 7 8 9 10 11 12
搜索 常用链接 我的随笔我的评论我的参与最新评论我的标签 随笔档案 2020年11月(3) 2020年10月(7) 2020年9月(2) 2020年8月(2) 2020年7月(4) 2020年6月(2) 2020年5月(2) 2020年4月(1) 2020年2月(1) 2020年1月(2) 2019年12月(1) 2019年10月(1) 2019年9月(4) 2019年8月(14) 最新评论1. Re:「排序算法」图解双轴快排@ToolGood 是的概率非常小。你说的这个确实是一个非常好优化思路。但快排最坏O(n2)也是一个非常重要的知识点,得提一下。…–bigsai2. Re:「排序算法」图解双轴快排文章中“ 如果运气肯不好遇到O(n)平方的,那确实就很被啦:” 这句话,我不认同,因为概率太小了。 因为通常快速排序法 最优化有三个地方,第一点就是在【开始值、结尾值、1/2位置的值】取中值做为比较值…–ToolGood3. Re:二叉树——前序遍历、中序遍历、后序遍历、层序遍历详解(递归非递归)如果没有错别字就更好了。 递归跑到最底层,然后又逐渐一层一层往上返。最后一层执行完,然后又跑到倒数第二层去执行,倒数第二层执行完,又跑到倒数第三层。。。一直到最上层。最上层执行完,递归也执行完了。…–super超人4. Re:JDBC+MySQL入门实战(实现CURD的例子)@bigsai 好的,谢谢!…–51testing软件测试5. Re:JDBC+MySQL入门实战(实现CURD的例子)@51testing软件测试 哈哈可以哒,著名原创公众号就行啦。如果公众号可以给您开白哈…–bigsai阅读排行榜 1. 拓扑排序详解与实现(58715) 2. 二叉树——前序遍历、中序遍历、后序遍历、层序遍历详解(递归非递归)(58644) 3. Dijkstra算法详细(单源最短路径算法)(16285) 4. 我用数据结构花了一夜给女朋友写了个h5走迷宫小游戏(5177) 5. 数据结构与算法—队列图文详解(3867) 评论排行榜 1. 我用数据结构花了一夜给女朋友写了个h5走迷宫小游戏(30) 2. 二叉树——前序遍历、中序遍历、后序遍历、层序遍历详解(递归非递归)(6) 3. 数据结构与算法—递归(阶乘、斐波那契、汉诺塔)(6) 4. 毕业季疫情下的校园生活是咋样的?(4) 5. 哪吒票房逼近30亿,从豆瓣短评简单分析人们对哪吒的态度(4) 推荐排行榜 1. 我用数据结构花了一夜给女朋友写了个h5走迷宫小游戏(21) 2. 拓扑排序详解与实现(14) 3. 二叉树——前序遍历、中序遍历、后序遍历、层序遍历详解(递归非递归)(10) 4. 毕业季疫情下的校园生活是咋样的?(5) 5. SpringBoot+MongoDB实现物流订单系统(3)
Copyright © 2020 bigsai
Powered by .NET 5.0.0 on Kubernetes 个人公众号交流:bigsai