常见排序算法汇总与分析(下)(基数排序与计数排序)

*爱你&永不变心* 提交于 2020-03-21 05:07:36

转载请注明出处:http://blog.csdn.net/fightlei/article/details/52586814


本篇汇总的算法将不再是基于比较的排序算法,因此会突破这类算法的时间复杂度下界O(nlog2n)。如果有朋友对前面的内容感兴趣,可以先去看看常见排序算法汇总与分析(中)(选择排序与归并排序)

我们先来总结基数排序算法,该算法在排序过程中不进行比较,而是通过“分配”和“收集”两个过程来实现的。


基数排序

【基本思想】

首先设立r个队列,对列编号分别为0~r-1,r为待排序列中元素的基数(例如10进制数,则r=10),然后按照下面的规则对元素进行分配收集

1,先按最低有效位的值,把n个元素分配到上述的r个队列中,然后从小到大将个队列中的元素依次收集起来
2,再按次低有效位的值把刚收集起来的关键字分配到r个队列中,重复收集工作
3,重复地进行上述分配和收集,直到最高有效位。(也就是说,如果位数为d,则需要重复进行d次,d由所有元素中最长的一个元素的位数计量)

为什么这样就可以完成排序呢?

以从小到大排序为例

首先当按照最低有效位完成分配和收集后,此时得到的序列,是根据元素最低有效位的值从小到大排列的。

当按照次低有效位进行第二次分配和收集后,得到的序列,是先根据元素的次低有效位的值从小到大排列,然后再根据最低有效位的值从小到大排列。

以此类推,当按照最高有效位进行最后一次分配和收集后,得到的序列,是先根据元素的最高有效位的值从小到大排列,再根据次高有效位排列,。。。,再根据次低有效位,再根据最低有效位。自然就完成了每个元素的从小到大排列。

【空间复杂度】O(n+r)

【时间复杂度】

平均情况:O(d(n+r))

因为每一趟分配的时间开销是O(n),收集的开销是O(r),因此执行d趟的时间开销为O(d(n+r)),通常d,r为常数

最好情况:O(d(n+r))

最坏情况:O(d(n+r))

【稳定性】稳定

【优点】

稳定排序;时间复杂度可以突破基于关键字比较排序法的下界O(n)

【缺点】

需要额外的辅助空间

【算法实现】

	/**
	 * 基数排序
	 * @param arr
	 */
	public static void radixSort(int[] arr) {
		//获得待排序列元素中的最大位数
		int maxBit = getMaxBit(arr);
		//对每一位分别进行分配和收集
		for (int bit = 1; bit <= maxBit; bit++) {
			//分配
			List<List<Integer>> buf = distribute(arr, bit);
			//收集
			collect(arr, buf);
		}
	}
	private static void collect(int[] arr, List<List<Integer>> buf) {
		int i = 0;
		//收集,依次将每个队列中的元素读出
		for (List<Integer> temp : buf) {
			for (int ele : temp) {
				arr[i++] = ele;
			}
		}
	}
	private static int getMaxBit(int[] arr) {
		int maxBit = 0;
		int len = 0;
		for (int ele : arr) {
			//利用字符串的length()方法获得元素位数
			len = (ele + "").length();
			if (len > maxBit) {
				maxBit = len;
			}
		}
		return maxBit;
	}
	//分配
	private static List<List<Integer>> distribute(int[] arr, int bit) {
		List<List<Integer>> buf = new ArrayList<>();
		//构建r个队列
		for (int i = 0; i < 10; i++) {
			buf.add(new ArrayList<>());
		}
		for (int ele : arr) {
			int value = getValueByBit(ele, bit);
			//根据每个元素指定位数上的值,将元素存放到对应的队列上
			buf.get(value).add(ele);
		}
		return buf;
	}
	//得到指定位数上的值
	private static int getValueByBit(int ele, int bit) {
		//没有该位,则返回0
		int value = 0;
		String temp = ele + "";
		if (temp.length() >= bit) {
			value = (int) (temp.charAt(temp.length() - bit) - '0');
		}
		return value;
	}

【本算法解读】

算法针对的是十进制数,所以r=10

算法首先获取到待排序列元素中的最大位数。然后按照基数排序的思想,对元素的每一位进行分配和收集。

分配distribute()方法:首先构建了r=10个队列,对应编号即是0~9。根据给定的位数,得到待排序列中每个元素在该位上的值,若元素没有该位,则返回0。根据每位上的值,将该元素放入相应的队列。完成一次分配。

收集collect()方法:依次从r个队列中读出其中的元素值,并将其存入原始序列中,即完成了一次收集。

【举个栗子】

对于待排序列413,10,8,28

首先获取到待排序列的最大位数,即为3。

然后根据每一位的值进行分配和收集,分配过程如下图:


由于是10进制数,所以构造10个队列,0~9。

首先从最低有效位开始,每个元素在该位上对应的值分别是3,0,8,8,根据该值,将对应元素放入对应的队列。分配结束后,依次从每个队列上读取元素值存入原始序列。可以看到一次分配以后,元素已经按照最低有效位从小到大排列。继续位数逐渐增大,直到最高有效位,重复上述操作即可完成排序。


计数排序

【基本思想】

计数排序是基于非比较排序,主要用于对于一定范围内的整数进行排序。采用的是空间换时间的方法。

针对待排序列中的每一个元素x,得到序列中小于x的元素个数,有了这一信息可以直接把x放到最终的输出序列的正确位置上。 计数排序之所以能做到线性时间复杂度是因为使用了索引的思想。

计数排序对输入的数据有附加的限制条件:
1、输入的线性表的元素属于有限偏序集S;
2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。
对于下面我即将给出的算法,它的限制条件是待排序列中的元素是有限个正整数(包括0,由于将元素值作为数组下标),最大值用k=O(n)表示,元素个数用n表示。它利用元素的实际值来确定它们在输出序列中的位置

【空间复杂度】O(n+k)

【时间复杂度】

平均情况:O(n+k)

最好情况:O(n+k)

最坏情况:O(n+k)

【稳定性】稳定

【优点】

稳定,在k值较小时突破了基于比较的排序算法下界

【缺点】

存在前提条件,需要大量额外空间,k值较大时效率很低

【算法实现】

	/**
	 * 计数排序
	 * @param arr
	 */
	public static void countSort(int[] arr) {
		int max = getMax(arr);
		int count[] = new int[max + 1];  //此时数组中元素值均被初始化为0
		//在以元素值为
		for (int ele : arr) {
			count[ele]++;
		}
		int k = 0;
		for (int i = 0; i <= max; i++) {
			for (int j = 0; j < count[i]; j++) {
				arr[k++] = i;
			}
		}
	}
    //获得待排元素中的最大值
	private static int getMax(int[] arr) {
		int max = 0;
		for (int ele : arr) {
			if (ele > max) {
				max = ele;
			}
		}
		return max;
	}

【本算法解读】

可以看到算法首先获得待排序列元素中的最大值,然后构建最大值+1的长度的计数数组。遍历待排序列的每个元素,并在以元素值为下标的数组值中加1。然后遍历计数数组,若对应下标的位置上值大于0(等于几就表示有几个元素),则表示存在有元素且其值为下标的大小。将该元素添加到原始序列中。由于下标是从小到大的,所以对应得到的序列也是从小到大排列的。

【举个栗子】

对于待排序列4,0,2,8,2

首先获取最大值即为0,然后构建8+1长度的计数数组。初始化时,计数数组中的值均为0.如下图所示:


将对应的元素值放在对应的下标里,例如元素4,放在了下标为4的位置里,计数数组的值加1,表示其中有一个元素值为下标的元素。待排序列中有两个2,所以下标为2的计数数组值是2,表示其中有两个元素值为下标的元素。

最后过遍历计数数组,如,遍历到0下标位置,数组值为1,表示有一个元素,元素值是下标0,添加到原始序列中。遍历到2下标位置,数组值为2,表示有两个元素,元素值均为下标2,将两个元素依次添加到原始序列中。以此类推,最终得到有序序列0,2,2,4,8。

那么,到这里我们这个常用排序算法的汇总就顺利完成了。本汇总分析了常用排序算法中的,交换排序(冒泡排序,快速排序),插入排序(直接插入排序,希尔排序),选择排序(直接选择排序,堆排序),归并排序,基数排序,计数排序。应该说是这个总结是暂时告一段落了,后面遇到一些好的算法,还会继续与大家分享。

为了方便大家,我已将本系列中用到的所有算法源码(java实现)上传至CSDN,需要的朋友可以点此下载


如有纰漏,敬请海涵。







易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!