之前学的7种排序:冒泡、选择、插入、希尔、快速、归并、堆排序都是基于比较的排序
最低的平均时间复杂度为O(nlogn)
而接下来要学习的计数排序、基数排序、桶排序,都不是基于比较的排序
它们是典型的用空间换时间,在某些时候,平均时间复杂度要低于O(nlogn)
计数排序(Counting Sort)
计数排序是对整数的排序
计数排序是对一定范围内的整数进行排序
计数排序的核心思想
统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引
其大致思路是:
- 找出待排序的数组中的最大值
- 建立一个数组,数组的个数是最大值的值,数组中的值是待排序数组出现的次数
- 遍历新数组,如果数组元素大于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 将所有非空桶的元素合并成有序序列