桶排序思想(计数排序、基数排序)

计数排序

描述: 计数排序运用了桶排序思想,是种非比较排序,具体步骤是:

  • 假设有长度为n的待排序列,取值范围min~max,首先创建和待排序列范围等长(max-min+1)的统计数组,从前到后遍历序列,每有一个元素 x x x,数组对应下标( x x x-min)处值+1,这样遍历完序列后,统计数组下标+偏移量min=代表的真实数值,该下标位置处的值代表这个元素出现的次数。
  • 对统计数组从前到后遍历,除首元素外任一位置值=该处值+前一位置值,目的是为了得到将原数组拷贝到新数组时每个元素应该放入的位置。
  • 从后往前遍历原数组,根据上一步所指位置将元素放入到新数组,同时统计数组对应位置处值-1,目的是下次遇到相同的元素应该放在刚刚放入位置的前面一位。

java代码

import java.util.Arrays;

public class CountSort {
	public static int[] countSort(int[] arr) {
		// 找出待排数组最大值和最小值
		int max = arr[0];
		int min = arr[0];
		for (int i = 1; i < arr.length; i++) {
			if (arr[i] > max)
				max = arr[i];
			if (arr[i] < min)
				min = arr[i];
		}
		
		// 创建统计数组并统计出对应元素个数
		int[] countArr = new int[max - min + 1];
		for (int i = 0; i < arr.length; i++) {
			countArr[arr[i] - min]++;
		}
		
		// 实现稳定排序的关键,统计数组每个位置代表的数值不再是相应元素个数,而是该元素和它前面所有小于它的元素个数之和
		for (int i = 1; i < countArr.length; i++) {
			countArr[i] = countArr[i] + countArr[i - 1];
		}
		
		// 创建和原数组等长的辅助数组,将原数组从后往前遍历每个元素插入辅助数组正确位置
		int[] sortedArr = new int[arr.length];
		for (int i = arr.length - 1; i >= 0; i--) {
			// arr[i]-min=统计数组下标index,countArr[index]-1为arr[i]在新排序数组中的正确位置
			sortedArr[--countArr[arr[i] - min]] = arr[i];
		}
		return sortedArr;
	}
	
	// 测试
	public static void main(String[] args) {
		int[] arr = new int[1000];
		// 随机生成1000个[0,10]整数
		for (int i = 0; i < 1000; i++) {
			arr[i] = (int) (Math.random() * 11);
		}
		System.out.println(Arrays.toString(countSort(arr)));
	}
}

算法分析

  • 时间复杂度 O ( n + k ) Ο(n+k) O(n+k),其中k是统计数组长度。因为整个过程中对原数组进行了3趟遍历操作,对统计数组进行了1趟遍历。一般来说统计数组相对原数组长度可以忽略,即使和原数组等长也只是增加了 O ( n ) Ο(n) O(n)的系数并没有数量级上的增长,所以也可认为时间复杂度 O ( n ) Ο(n) O(n)
  • 空间复杂度 O ( n + k ) Ο(n+k) O(n+k),排序过程需要一个和原数组等长的辅助数组和一个长度为k的统计数组。
  • 排序稳定
  • 两个局限性:1.当序列最大最小值之间差距过大时不适用,比如100个数范围从1~10000,那么就要创建一个长度10001的统计数组;2.当序列元素不是整数的时候不能创建对应的统计数组,也就不能进行计数排序了。

其实如果只是要求给一组整数排序而不必考虑稳定性的时候,就不需要对统计数组从前到后遍历相加求和,也不需要创建和原数组等长的辅助数组,根据统计数组下标与统计的数值之间的关系可直接对原数组赋值,减少了时间和空间开销。

基数排序

描述: 基数排序是桶排序思想的一种,属于非比较排序,本质上是一种多关键字排序,根据对不同权重关键字的优先排序顺序不同,有最高位优先(MSD)和最低位优先(LSD)两种。比如对一副扑克牌排序,已知面值A<2<3<…<Q<K,花色梅花<方块<红桃<黑桃,其中花色的地位高于面值:

  • 最高位优先:先按不同花色分成有次序的4堆,每堆中再按面值大小排序。其实体现的还是一种分治思想,将所有牌分到四个桶,每个桶中各自排序。
  • 最低位优先:先按面值大小分成13堆,然后将13堆牌按面值次序收集到一起,再按不同花色依次从13堆中抽排分成4堆,再次按花色大小收集后完成排序。这是一种分配与收集交替进行的思想。

下面以最低位优先为例假如对一组整数[73, 22, 93, 43, 55, 14, 28, 65, 39, 81]排序,首先根据个位数的数值,在遍历数组元素时将它们分配至编号0到9的桶子中:

43
9365
81227314552839
0123456789

接下来将这些桶子中的数值重新串接起来,得到[81, 22, 73, 93, 43, 14, 55, 65, 28, 39],接着再进行一次分配,这次是根据十位数来分配:

28
142239435565738193
0123456789

重新串起得到[14, 22, 28, 39, 43, 55, 65, 73, 81, 93],这时候整个数组已经排序完毕。如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

java代码

import java.util.Arrays;

public class RadixSort {
	// 提供给外部调用
	public static void radixSort(int[] arr) {
		int[] count = new int[10]; 						// 统计数组,因为排序数字是十进制,所以基数0~9
		int[] temp = new int[arr.length]; 				// 辅助数组
		int len = getLen(arr); 							// 求数组中最大数字位数
		int div; 										// 每次循环统计中,(arr[i]/div)%10分别代表个、十、百位上的数字
		for (int i = 0; i < len; i++) {
			div = (int) Math.pow(10, i);
			rSort(arr, temp, count, div);
		}
	}

	private static void rSort(int[] arr, int[] temp, int[] count, int div) {
		for (int i = 0; i < arr.length; i++)
			count[(arr[i] / div) % 10]++;
			
		// 下面这段代码作用和计数统计中相同,同样也保证了排序稳定
		for (int i = 1; i < count.length; i++)
			count[i] = count[i] + count[i - 1];		
				
		for (int i = arr.length - 1; i >= 0; i--)
			temp[--count[(arr[i] / div) % 10]] = arr[i];
			
		System.arraycopy(temp, 0, arr, 0, arr.length); 		// temp数组复制给arr
		Arrays.fill(count, 0); 								// 统计数组全部置0
	}

	private static int getLen(int[] arr) {
		int max = arr[0];
		for (int i = 1; i < arr.length; i++) {
			if (arr[i] > max)
				max = arr[i];
		}
		return Integer.toString(max).length();
	}

	// 测试
	public static void main(String[] args) {
		int[] arr = new int[10000];
		for (int i = 0; i < arr.length; i++)
			arr[i] = (int) (Math.random() * 1000);
		radixSort(arr);
		System.out.println(Arrays.toString(arr));
	}
}

算法分析

  • 时间复杂度:设初始n个元素,每个元素含有d个关键字,每个关键字大小范围是k,从上面代码可以看出每趟针对不同关键字排序中,只是对统计数组和原数组进行了几趟遍历赋值操作,时间复杂度在 O ( n + k ) Ο(n+k) O(n+k)数量级,d个关键字需要进行d趟这样的操作,所以总的时间复杂度 O ( d ( n + k ) ) Ο(d(n+k)) O(d(n+k))
  • 空间复杂度 O ( n + k ) Ο(n+k) O(n+k),整个过程只用到了一个长度和关键字范围大小相等的统计数组和一个与原数组等长的辅助数组。
  • 排序稳定,因为21行注释下的两个循环语句用到了和计数排序一样的定位思想,保证相同关键字排序前后位置不发生相对改变。
  • 上面是用顺序结构实现的,也能用于链式结构。
  • LSD的基数排序适用于位数小的序列,如果位数多的话使用MSD的效率会比较好。MSD的方式与LSD相反,是由最高位数为基底开始进行分配,但分配之后并不立马收集,而是在每个桶中建立子桶以第二高位数作为基底再次分配,如此重复直到以最低位作为基底分配完成,再自下往上依次收集完成排序。整个过程可用递归完成。
  • 基数排序更适合用于对时间和字符串等整体权值未知的数据进行排序。

本文参考了很多百度百科的内容,但百度百科上所给代码是用二维数组对原数组元素进行分配存储,其中第一维表示基数范围,第二维类似于桶直接存储,因为元素不确定所以第二维长度要以排序元素总数创建,当初始元素很多时太浪费空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值