基数排序为什么要从个位开始_基数排序(并非桶排序) MSD LSD java

参考:https://www.bilibili.com/video/av38482830?from=search&seid=12473129328906304471

基数排序详解以及java实现 - Leo-Yang - 博客园 这里直接拿此文的代码进行讲解,便于学习。

基数排序:

基本思想:分配+收集

在这里表示抱歉,由于第一次学的时候是参考前面视频进行学习的,后来看算法导论,发现视频有误。。基数排序和桶排序完全就不是一个东西。

思想:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后按序号将非空的连接。(我第一反应是想到了jdk的hashmap的桶,哈哈,其实有些像)

数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个,十,百...进行排序。

我理解后这样表达一波:

分配:就是把数据按照规则分发到各个桶中,比如 123,就会有三次分配,第一次分配从个位开始,把123分到第四个桶上(第一个桶是0),分配之后就是回收,也就是在此次分配中进行排序,比如123和28,3在第四个桶中,8在第9个桶中,所以回收之后就是28在123的后面,回收的结果就是重新修改这个数组,进行个位上的排序。然后,就是第二次分配,123中取出2,28中取出2,他们取出来的十位是一样的,所以在同一个桶中。然后依次类推下去。

我第一遍理解的时候,没理解过来,感觉不对啊,这样单纯排序就行了?

后面我才发现,原来是我忽略了基数排序最重要的部分,那就是每次排序的相互依赖性。比方说,第一次排序出来,28和18在同一个桶中,所以在数组中相邻,在第二次排序后,就会出现28在18的后面,也就是分出大小了。再比如,45和54,第一次排序45在54后面,第二次排序,54就在45后面了。感觉你可以理解为权重,高位的权重最大,后面的依次降低,当最高权重相同时,就看第二高的权值,从这个角度来说,其实第一次排序拍最高位更好理解一些,但都是一样的,看你的算法怎么设计了。从高的往低的排是叫MSD(most significant digital),从低的往高的排叫LSD(least significant digital)。这里只演示lsd。

可能看完上面的解释大家还不太懂,那么我就结合程序画图来讲解java是怎么实现的。代码直接引用上面链接里的,注重学习:下面的代码有误,后面进行解释并给出我认为正确的代码,大家可以留个悬念,但是思路是非常值得我们学习的。

public class RadixSort {
	private static void radixSort(int[] array, int d) {
		int n = 1;// 代表位数对应的数:1,10,100...
		int k = 0;// 保存每一位排序后的结果用于下一位的排序输入
		int length = array.length;
		int[][] bucket = new int[10][length];// 排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
		int[] order = new int[length];// 用于保存每个桶里有多少个数字
		while (n < d) {
			for (int num : array) // 将数组array里的每个数字放在相应的桶里
			{
				int digit = (num / n) % 10;// n分别为1,10,100,除以之后剩余的就是对应的个位之前,十位之前,百位之前。取余十是单独取出这一位,不把更高位的也加在里面。
				bucket[digit][order[digit]] = num;
				order[digit]++;
			}
			for (int i = 0; i < length; i++)// 将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
			{
				if (order[i] != 0)// 这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
				{
					for (int j = 0; j < order[i]; j++) {
						array[k] = bucket[i][j];
						k++;
					}
				}
				order[i] = 0;// 将桶里计数器置0,用于下一次位排序
			}
			n *= 10;
			k = 0;// 将k置0,用于下一轮保存位排序结果
		}

	}

	public static void main(String[] args) {
		int[] A = new int[] { 73, 22, 93, 43, 55, 14, 28, 65, 39, 81 };
		radixSort(A, 100);
		for (int num : A) {
			System.out.print(num + " ");
		}
	}

}

大家可以试着运行一下,然后不理解的就debug一下,当然是能不看文章理解了就行。

然后我们来一步一步分析它的流程:

先来看第一次分配:

6b882b87a1c5b9d6229571f924c7a9a2.png

大家清楚地了解里面的各个属性对应图中的元素才是最重要的。

                       for (int num : array) // 将数组array里的每个数字放在相应的桶里
			{
				int digit = (num / n) % 10;// n分别为1,10,100,除以之后剩余的就是对应的个位之前,十位之前,百位之前。取余十是单独取出这一位,不把更高位的也加在里面。
				bucket[digit][order[digit]] = num;
				order[digit]++;
			}
			for (int i = 0; i < length; i++)// 将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
			{
				if (order[i] != 0)// 这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
				{
					for (int j = 0; j < order[i]; j++) {
						array[k] = bucket[i][j];
						k++;
					}
				}
				order[i] = 0;// 将桶里计数器置0,用于下一次位排序
			}

这里对应的是第一个for循环为分配,第二个for循环为收集。

然后出来的就是81,22,73,93,43,14,55,65,28,39 .

第二遍又按照此顺序放入。只是此时是按十位放入,这个表达式是关键:

digit = (num / n) % 10;

取出对应的位数。n为1就取出第一位,n为10就取出十位。

然后分配的图如下:

a376efead70fd9678d1db58232514b7c.png

然后大家发现了什么吗?比如就22和28?

之前由于第一次分配的原因,28在22的后面,而不是在同一个桶中。然后这就成了第二次分配的依赖,此时桶2中的第一个元素是22,而不是28. 收集的时候,是从下标小的开始往下标大的收集,也就是说新生成的数组里22是在28前面的。即使是在同一个桶里,但也是有序的,这也就是我们体验这个基数排序的关键所在。 在第一次分配的时候,我们并没有任何依据,所以只能按最原始的顺序决定同一桶内的顺序(即无序),可是第二次排序就不一样了,有了第一次个位排序的基础后,同一桶内是按照下标由小到大是按照个位排序好了的。然后完全就可以类推下去了。

所以,你可以这样理解,第一次分配收集后,前面位数相同,个位位数不同的相对顺序已经是排好了的。就比如单纯的 2,4;或者121,122;所以此后,顺序的决定权就交给了十位。依次类推,就可以把基数排序完全理解了。

而如果是从msd算法角度看,其实更好理解:

原始数据: 73, 22, 93, 43, 55, 14, 28, 65, 39, 81

第一步: 14,{22,28},39,43,55,65,73,81,93

此时我们可以想成10个集合,第一步后集合之间的顺序已经确定不变了,所以集合的相对位置不会改变了。

第二步:14,22,28,39,43,55,65,73,81,93

第二步进行集合内部的排序即可。也就是确定22和28的相对位置。而这个22和28又不是另外用一个排序算法了排序的,而是存入桶中,然后28在22的后面,那么收集的时候28就在22的后面。 然后大家仔细想想这种算法怎么实现?感觉比起lsd算法来稍微麻烦了些。因为它要遍历每一个桶中的数据,然后再来进行一次MSD算法,也就是递归。两位数的话算起来比较简单,可是多位数的话就要有多次递归了,递归深度就比较深。但是就我们从外面理解起来就很简单,就是小集合再分小集合嘛。而LSD就是一个简单的循环就ok了。【排序算法】基数排序:LSD 与 MSD 这里有个MSD的链接,看起来不错,大家可以参考一下。

然后我们来分析LSD的一个算法的基本属性:

首先是稳定性:如果两个数相同,那么这两个数就会被分配到一个桶里,在前面的始终是下标下的,所以不存在交换两个相同的数之说,所以LSD是稳定的。

时间复杂度: 首先我们要看它进行多少次大循环,也就是多少位。设位数为k。然后你们猜我发现了啥?我发现链接的代码写错了,然后就那个例子而言,误打误撞输出了正确结果,然后我又看了评论区的讨论,感觉有点搞笑啊。我一开始太关注思路了,没去具体关注它的实现细节。让我来结合链接2的评论区进行简单讲解:

int length = array.length;

int[] order = new int[length];// 用于保存每个桶里有多少个数字

这个博主其实写错了,大家根据我前面的图也可以了解,order[i]里面存储的是第i个桶里的元素个数,里面的值肯定不会大于输入数组的长度。但是order数组的长度是等于桶的个数的,也就是说在这里肯定为10,即0-9。但是并不是非常肯定的,因为比如你要进行年月日的生日排序的时候,你肯定是年分一个大桶,月分一个大桶,日分一个大桶,然后具体的日又分为30个小桶,月分为12个小桶。

然后另一处错误的地方是

for (int i = 0; i < length; i++)// 将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果

这里其实博主注释得很明确了,它是要遍历每个桶,和输入的数组元素个数无关,所以这里应该定义一个常量或者作为参数传入一个桶的个数的,也就是10.所以即为

for (int i = 0; i < 10; i++)

其实 int[][] bucket = new int[10][length]这里的10也就是桶的个数了。。

然后再回到我们时间复杂度的计算上:

设大循环为K,即数字位数,分配的for循环为输入数组的元素个数,也就是我们常说的数据量n,而收集的for循环,即遍历的桶的个数,我们设为m,所以时间复杂度为 k*(n+m)

所以按照大O表示来简化,是O(n),即线性阶的时间复杂度。相比于我们常说的排序算法的时间复杂度都是比较小的。

空间复杂度: 需要m个桶,收集的时候需要一个n的数组,所以为O(m+n)。这个是第一个视频说的,我不是很理解,我觉得是我使用的辅助空间的大小,即一个order数组,和一个bucket数组,长度分别为m,m*n ,所以也就是m*n,也就是O(n).

下面图片截图与第一个链接:意在说明基数排序相比于其他排序在某些时候还是非常具有优越性的。

b2f984cb0c50091f8b6235b037ffeec8.png

这种算法不是基于比较的,而是基于分配和收集所以这种算法在合适的情况下非常快,效率很高。它不适合关键字的取值范围不确定的,或者非常大的情况,这样就很难算

感觉这两个算法刚开始接触会觉得比较麻烦,然后好好研究理解后,慢慢尝试着多写两遍代码就很好掌握了。

欢迎交流讨论。

下面给出我认为正确的LSD代码:可能还有其他问题,欢迎指出错误。

ublic class RadixSort {
	private static final int BARRELNUM = 10;

	private static void radixSort(int[] array, int d) {
		int n = 1;// 代表位数对应的数:1,10,100...
		int k = 0;// 保存每一位排序后的结果用于下一位的排序输入
		int length = array.length;
		int[][] bucket = new int[BARRELNUM][length];// 排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
		int[] order = new int[BARRELNUM];// 用于保存每个桶里有多少个数字
		while (n < d) {
			for (int num : array) // 将数组array里的每个数字放在相应的桶里
			{
				int digit = (num / n) % 10;// n分别为1,10,100,除以之后剩余的就是对应的个位之前,十位之前,百位之前。取余十是单独取出这一位,不把更高位的也加在里面。
				bucket[digit][order[digit]] = num;
				order[digit]++;
			}
			for (int i = 0; i < BARRELNUM; i++)// 将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
			{
				if (order[i] != 0)// 这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
				{
					for (int j = 0; j < order[i]; j++) {
						array[k] = bucket[i][j];
						k++;
					}
				}
				order[i] = 0;// 将桶里计数器置0,用于下一次位排序
			}
			n *= 10;
			k = 0;// 将k置0,用于下一轮保存位排序结果
		}

	}

	public static void main(String[] args) {
		int[] A = new int[] { 73, 22, 93, 43, 55, 14, 28, 65, 39, 81 };
		radixSort(A, 100);
		for (int num : A) {
			System.out.print(num + " ");
		}
	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值