[算法学习]线性时间排序:计数排序、基数排序和桶排序

        在算法导论中第八章专门列出了线性时间的排序算法,常规的基于比较的排序算法的上限是O(nlogn),本章介绍了三个算法,计数排序,基数排序,桶排序。

        第一次接触是计数排序是leetcode上面有一道计算论文什么因子的题,基数排序和桶排序在上学期算法课上老师讲过,当时一知半解,一直以为他们两个是同一个算法。最近自习看完了这一张才发现这两个算法是完全不一样的,看了一些博客也有很多的把这两个算法混淆,我觉得这样是不好的,写出来的博客要负责任,不然容易误导新手,我在开始就走了很多弯路,所以想在此总结一下自己的理解。


一、计数排序

1、书上的算法

        计数排序假设n个输入元素中的每一个都是在0到k区间内的一个整数,其中k为某个整数。详细的原理算法导论上面讲的很明白,在这里直接给出我的代码。

	public static int[] countingSort_extra(int nums[], int k){
		int n=nums.length;
		int[] re=new int[n];
		int[] C=new int[k+1];
		for(int i=0; i<n; i++){
			C[nums[i]]++;
		}
		for(int i=1; i<C.length; i++){
			C[i]+=C[i-1];
		}
		for(int i=n-1; i>=0; i--){//必须从后向前遍历,才能保证稳定
			re[C[nums[i]]-1]=nums[i];
			C[nums[i]]--;
		}		
		return re;
	}



        书上面说的很明白,计数排序主要有两个特点,一个是不基于比较,可以在线性时间内完成。还有就是稳定的排序,主要是作为下面讲到的基数排序的基础。

        有一点就是书后面的思考题问道第10行(上面代码中的11行)的循环可不可以从前向后遍历,根据我的理解,如果从前向后遍历,算法会运行正确,但是不能确保排序的稳定性,所以如果作为基数排序的基础,必须要从后向前遍历以确保稳定性。


2、空间上的改进

        上面的方法名之所以叫countingSort_extra是因为排序后的数组存在另一个数组中,所以一共需要两个额外的数组空间,如果改进一下,排序后直接写回原数组,可以节省空间,下面是我的代码


        这个算法也可以实现线性时间的排序,而且空间节省很多,但同样,他也是不稳定的,所以,他和上面说的从前向后遍历一样,只能实现计数排序,但是不稳定,不能用在基数排序中。

	public static void countingSort(int[] nums, int k){
		int n=nums.length;
		int[] B=new int[k+1];
		int count=0;
		for(int i=0; i<n; i++){
			B[nums[i]]++;
		}
		for(int i=0; i<=k; i++){
			while(B[i]-->0){
				nums[count++]=i;
			}
		}
	}


二、基数排序

        给定n个d位数,其中每个数位都有k个可能的取值。如果radixSort使用的稳定排序方法耗时O(n+k),那么它就可以在O(d(n+k))时间内将这样数排好序。

他与桶排序是完全不一样的,在此说一下几个用到的关键参数。

        d:位数,它决定了内层排序的次数。比如329是一个三位数,d=3,基数排序的原理就是从低到高(注意一定要从低到高)按位比较,所以整个算法会从个位十位百位排序,而每次排序用到的就是上面的计数排序。

        k:可能的取值,与上面计数排序的k意义相同,就是可能的取值区间,一般对数字排序的肯定都是0-9的10个取值。

        n:就是元素的个数,即nums.length。因为要稳定排序,所以肯定需要额外的n长度数组来保存,在下面我的代码中,我建立了一个temp数组,两个数组依次从一个写到另一个中,这样就不用每次都回写了。

	private static int[] c;
	//计算num倒数第n位的数字
	private static int valInBit(int num, int n){
		int temp=1;
		while(n>0){
			temp*=10;
			n--;
		}
		return num%temp/(temp/10);
	}
	
	//按照数组的倒数第n位数字进行排序
	private static void countingSort(int[] nums, int[] re, int n){
		Arrays.fill(c, 0);
		for(int i=0; i<nums.length; i++){
			c[valInBit(nums[i], n)]++;
		}
		for(int i=1; i<c.length; i++){
			c[i]+=c[i-1];
		}
		for(int i=nums.length-1; i>=0; i--){
			int val=valInBit(nums[i], n);
			int index=c[val];
			re[index-1]=nums[i];
			c[val]--;
		}
		System.out.println("step"+n+": "+Arrays.toString(re));
	}
	
	/*
	 * 给定n个d位数,其中每个数位有k个可能的取值
	 */
	public static void radixSort(int[] nums, int d, int k){
		c=new int[k];
		int[] temp=new int[nums.length];
		for(int i=1; i<=d; i++){
			if((i&1)==1){
				countingSort(nums, temp, i);
			}else{
				countingSort(temp, nums, i);
			}
		}	
		
		if((d&1)!=1){
			nums=temp;
		}
		
	} 
	public static void main(String[] args) {
		int[] nums={329,457,657,839,436,720,355};
		radixSort(nums, 3, 10);
		System.out.println(Arrays.toString(nums));
	}



控制台的输出如下

step1: [720, 355, 436, 457, 657, 329, 839]
step2: [720, 329, 436, 839, 355, 457, 657]
step3: [329, 355, 436, 457, 657, 720, 839]
[720, 329, 436, 839, 355, 457, 657]


三、桶排序

       桶排序将[0,1)区间划分为n个相同大小的子区间,或称为桶。然后将n个输入数分别放到各个桶中。为了得到输出结果,我们先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。

        以上是算法导论中的说法,说实话这节我并没有看太明白,尤其是后面证明时间代价是O(n)。

我个人的理解,桶排序的关键在于桶的划分,有点类似于设计哈希函数,使得各个元素均分布在各个桶中,然后对每个桶中的元素进行排序,最后再统计到一起。

        下面是书中的一个例子,对10个元素划分10个桶,然后按第一位小数对应各个桶中。



        下面是我的代码,用ArrayList来代替了图中的链表。

	private static int[] c;
	//计算num倒数第n位的数字
	private static int valInBit(int num, int n){
		int temp=1;
		while(n>0){
			temp*=10;
			n--;
		}
		return num%temp/(temp/10);
	}
	
	//按照数组的倒数第n位数字进行排序
	private static void countingSort(int[] nums, int[] re, int n){
		Arrays.fill(c, 0);
		for(int i=0; i<nums.length; i++){
			c[valInBit(nums[i], n)]++;
		}
		for(int i=1; i<c.length; i++){
			c[i]+=c[i-1];
		}
		for(int i=nums.length-1; i>=0; i--){
			int val=valInBit(nums[i], n);
			int index=c[val];
			re[index-1]=nums[i];
			c[val]--;
		}
		System.out.println("step"+n+": "+Arrays.toString(re));
	}
	
	/*
	 * 给定n个d位数,其中每个数位有k个可能的取值
	 */
	public static void radixSort(int[] nums, int d, int k){
		c=new int[k];
		int[] temp=new int[nums.length];
		for(int i=1; i<=d; i++){
			if((i&1)==1){
				countingSort(nums, temp, i);
			}else{
				countingSort(temp, nums, i);
			}
		}	
		
		if((d&1)!=1){
			nums=temp;
		}
		
	} 
	public static void main(String[] args) {
		int[] nums={329,457,657,839,436,720,355};
		radixSort(nums, 3, 10);
		System.out.println(Arrays.toString(nums));
	}

控制台输出如下

[0.12, 0.17]

[0.21, 0.23, 0.26]

[0.39]

[0.68]

[0.72, 0.78]

[0.94]

[0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]


四、总结

        以上是我根据书上的内容结合自己的理解写的,代码全部是自己敲的并且经过少量测试,个人水平有限不敢保证完全没有错误,主要用于自己总结加深理解,如有错误欢迎指正大家一起学习进步。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值