数据结构与算法---排序---计数排序、基数排序、桶排序

之前学的7种排序:冒泡、选择、插入、希尔、快速、归并、堆排序都是基于比较的排序
最低的平均时间复杂度为O(nlogn)

而接下来要学习的计数排序、基数排序、桶排序,都不是基于比较的排序
它们是典型的用空间换时间,在某些时候,平均时间复杂度要低于O(nlogn)


计数排序(Counting Sort)

计数排序是对整数的排序
计数排序是对一定范围内的整数进行排序

计数排序的核心思想

统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

其大致思路是:

  1. 找出待排序的数组中的最大值
  2. 建立一个数组,数组的个数是最大值的值,数组中的值是待排序数组出现的次数
  3. 遍历新数组,如果数组元素大于0,则将该元素的下标赋值到原来数组上,并且新数组元素-1操作,原来数组后移一位

在这里插入图片描述
因此,不难写出代码:

public static void main(String[] args) {
		
		int[] array = {22, 2, 4, 8, 8, 8, 12, 14};
		
		countingSort(array);
		
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + " ");
		}
	}
	
	public static void countingSort(int[] array)
	{
		//找出待排序的最大值
		int max = array[0];
		for (int i = 1; i < array.length; i++) {
			if(array[i] > max)
			{
				max = array[i];
			}
		}
		
		//新建一个数组
		int[] countsArray = new int[max + 1];
		
		//数组中的值是待排序数组出现的次数
		for (int i = 0; i < array.length; i++) {
			countsArray[array[i]]++;
		}
		
		/**
		 * 遍历新数组
		 * 如果数组元素大于0,则将该元素的下标赋值到原来数组上,
		 * 并且新数组元素-1操作,原来数组后移一位
		 * */
		
		//原来数组的下标
		int index = 0;
		for (int i = 0; i < countsArray.length; i++) {
			while(countsArray[i]-- > 0)
			{
				array[index++] = i;
			}
		}
	}

但是,这个算法是有很大的问题的,具体表现在:

  • 无法对负整数进行排序
  • 极其浪费内存空间
    比如,1,2,10000,就要开出10000+1个长度的数组,严重浪费空间
  • 不稳定排序

因此,我们需要对该算法进行优化


优化

首先,针对空间优化,我们可以取最小值到最大值的区间去存放数据,这样,不需要从0开始
比如,7, 3, 5, 8, 6, 7, 4, 5。
按原来的写法,数组从0开始,最大值8结束,数组个数为max个
优化过后,数组从最小值为3开始,最大值为8结束,数组个数只需要max - min + 1,空间优化了min个

在这里插入图片描述

当然,数组还是从下标0开始,只是在数组下标0与元素之间有某种关联即可。

这种方法也解决了上面的不能存放负整数的缺陷。
之所以不能存放负整数,是因为上面的方法索引与值是对应的,就是值为5,放在索引为5的数组中。值为-5,数组没有索引为-5。因此不能存放负整数。

现在这种优化,是一个相对关系,比如:-2,0,1

元素 -2 -1 0 1
索引 0 1 2 3
次数 1 0 1 1

为了保证该算法是稳定排序,我们可以改变数组存储的元素
原来数组里面被存储的出现次数
现在我们可以存储成其前面所有次数 + 该值出现的次数
这一个值包含一个有效信息:该值在排序后的有序数组的位置信息

在这里插入图片描述

比如,counts数组中,索引为5的值为8;索引5的值8 减去 索引4的值7 等于 元素8出现的次数为1次
索引为2的值为4;索引2的值4 减去 索引1的值2 等于 元素5出现的次数为2次

而使用索引为5的值8 减去 元素8出现的次数 等于 8在有序数组的下标
索引为4的值7 减去 元素7出现的次数 等于 7在有序数组的下标
7 - 1 = 6
7 - 2 = 5

倒数第n个7的下标 = 7出现的次数 - n

换成官方点的语言:
假设array中的最小值是min
array中的元素k对应的counts索引是k - min

array中的元素k在有序序列中的索引
counts[k - min] - p(p代表着是倒数第几个k)

比如元素8在有序序列中的索引
counts[8 - 3] - 1,结果为7

倒数第1个元素7在有序序列中的索引
counts[7 - 3] - 1,结果为6

倒数第2个元素7在有序序列中的索引
counts[7 - 3] - 2,结果为5

其实,最主要最重要的就是,counts数组不再存储元素出现的次数,而是其前面所有次数 + 该值出现的次数

从右往左遍历,逐渐减,所以,之前在后面的元素,放在有序数组的后面。可以保证有序数组是稳定的。

public static void main(String[] args)
	{
		int[] array = {7, 5, 1, 3, 2, 4, 6, 10, 11, 13, 12, 15, 16, 14, 8, 9};
		countingSort(array);
		
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i]+ " ");
		}
	}
	
	public static void countingSort(int[] array)
	{
		//找出待排序的最大值、最小值
		int max = array[0];
		int min = array[0];
		for (int i = 1; i < array.length; i++) 
		{
			if(array[i] > max)
			{
				max = array[i];
			}
			
			if(array[i] < min)
			{
				min = array[i];
			}
		}
		
		//新建一个数组
		int[] countsArray = new int[max - min + 1];
		
		//数组中的值是待排序数组出现的次数
		for (int i = 0; i < array.length; i++) {
			countsArray[array[i] - min]++;
		}
		
		//累加次数
		for (int i = 1; i < countsArray.length; i++) {
			countsArray[i] +=  countsArray[i - 1];
		}
		
		//从后往前遍历,将遍历出的元素放在有序数组中合适的位置
		int[] sortArray = new int[array.length];
		for (int i = array.length - 1; i >= 0; i--) {
			int index = --countsArray[array[i] - min];
			sortArray[index] = array[i];
		}
		
		//将有序数组覆盖到array中
		for (int i = 0; i < sortArray.length; i++) {
			array[i] = sortArray[i];
		}
		
	}
复杂度分析

最好、最坏、平均时间复杂度为:O(n + k)
空间复杂度为:O(n + k)

k是整数的取值范围

优化后的计数排序属于稳定排序


基数排序(Radix Sort)

基数排序非常适合用于整数排序(尤其是非负整数)

执行流程:
依次对个位数、十位数、百位数、千位数、万位数…进行排序

在这里插入图片描述
个位数、十位数、百位数的取值范围都是0~9,因此,我们可以使用计数排序对它们进行排序

如何求一个数字的百位数、十位数、个位数?
比如,数字593
百位数:593/100 % 10 = 5;
十位数:593/10 % 10 = 9;
个位数:593/1 % 10 = 3;

public static void main(String[] args)
	{
		int[] array = {7, 5, 1, 3, 2, 4, 6, 10, 11, 13, 12, 15, 16, 14, 8, 9};
		RadixSort(array);
		
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i]+ " ");
		}
	}
	
	/**基数排序*/
	public static void RadixSort(int[] array)
	{
		//找出最大值
		int max = array[0];
		for (int i = 1; i < array.length; i++) 
		{
			if(array[i] > max)
			{
				max = array[i];
			}
		}
		
		for (int divider = 1; divider <= max; divider *= 10) {
			countingSort2(array, divider);
		}
	}
	
	/**计数排序(基数排序使用的改变版)*/
	public static void countingSort2(int[] array, int divider)
	{
		//新建一个数组
		int[] countsArray = new int[10];
		
		//数组中的值是待排序数组出现的次数
		for (int i = 0; i < array.length; i++) {
			countsArray[array[i] / divider % 10]++;
		}
		
		//累加次数
		for (int i = 1; i < countsArray.length; i++) {
			countsArray[i] +=  countsArray[i - 1];
		}
		
		//从后往前遍历,将遍历出的元素放在有序数组中合适的位置
		int[] sortArray = new int[array.length];
		for (int i = array.length - 1; i >= 0; i--) {
			int index = --countsArray[array[i] / divider % 10];
			sortArray[index] = array[i];
		}
		
		//将有序数组覆盖到array中
		for (int i = 0; i < sortArray.length; i++) {
			array[i] = sortArray[i];
		}
	}
复杂度分析

最好、最坏、平均时间复杂度为:O(d * (n + k)),d是最大值的位数,k是进制

空间复杂度:O(n + k),k是进制

属于稳定排序


桶排序(Bucket Sort)

了解

执行流程:
1 创建一定数量的桶(比如用数组、链表作为桶)
2 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
3 分别对每个桶进行单独排序
4 将所有非空桶的元素合并成有序序列

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值