计数排序
前言:在博客写这些文章的目的用于记录所学,怕以后忘了,如果哪里写的不对欢迎指正,谢谢!!
学习目标:掌握计数排序算法的原理和思想
一、前提知识
排序算法概念、时间复杂度。可前往此网址 排序算法学习01_算法基础介绍阅读
二、计数排序介绍
计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序不是比较排序, 它利用数组下标来排序元素。
三、计数排序工作原理
计数排序利用数组下标来排序元素。通过把要排序序列中的值,依次放入一个新的数组中。如何放置?根据序列中值的大小放入数组对应的索引上,但此时并不真正实现放入元素,而是让对应索引位置上元素+1,进行计数。
排序时,则依次查看每个索引位置上元素值是否大于0(有个数的话),则把对应索引放入原序列。。即可得到一个排序好的序列
画个图理解吧(图片下面文字是大于0,我写错了~~~)
四、计数排序设计思路
主要思考以下几点
- 新数组长度如何选择
- 序列中的值如何依次“放入”新数组,并实现新数组对应索引位置上的元素+1
- 放置完毕,如何依次取出新数组的内容,放置到原序列,达到排序效果
- 关于第一点
- 从计数排序的工作原理可得知:序列的值是根据新数组(以下称为计数数组)的索引来放置的
- 那么计数数组长度的选择,则要大于序列中的最大值,才可以“存储”所有值
- 从计数排序的工作原理可得知:序列的值是根据新数组(以下称为计数数组)的索引来放置的
- 关于第二点
- 我们知道原序列的值对应着计数数组的索引,那么我们即可通过这样一种巧妙的组合达成这样的效果
- 例countArr[ arr[ 1 ] ],假设原序列arr[1],元素为5,那么就刚好可以对计数数组下标为5的地方进行改动
- 那要做什么改动呢?就是对该位置上的元素+1,进行计数,即conutArr[ arr[ 1 ] ]++,表明该位置此时有一个元素5
- 我们知道原序列的值对应着计数数组的索引,那么我们即可通过这样一种巧妙的组合达成这样的效果
- 关于第三点
- 这就简单了,依次遍历计数数组,看看每个计数数组索引上元素有没有大于0,那么把索引赋值到原序列,记得循环哦,有可能数量不只一个
五、代码实现
package com.migu.sortingAlgorithm;
import java.util.Arrays;
/**
* 计数排序算法
*/
public class CountSort {
public static void main(String[] args) {
int arr[] = {3,2,10,5,1,7,9,4,4,3,12,0};
countSort(arr);
System.out.println(Arrays.toString(arr)); // 调用工具类
}
public static void countSort(int[] arr) {
// 找出数组中的最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]; // 找最大值,用于建立数组长度
}
}
// 初始化计数数组,长度记得再+1
int[] countArr = new int[max + 1];
// 计数,遍历原序列,并计数到新数组
for (int i = 0; i < arr.length; i++) {
countArr[arr[i]]++;
}
// 排序
int index = 0; // 作为指针辅助原序列排序
//依次遍历计数数组,如果索引上有元素大于0,则代表有数据
for (int i = 0; i < countArr.length; i++) {
while (countArr[i] > 0) {
arr[index++] = i;
countArr[i]--;
}
}
}
}
六、计数排序优化
认真的童孩可能发现,以上计数排序具有一定局限性
- 如果当取值为负数时,那么将没有索引与之对应,则无法排序
- 如果一个序列中的最小值很大,比如这样 { 101,108,104,103 },那么如果依然采用上面方法,将会导致牺牲掉计数数组前面100个空间
基于以上两点,可以采用一种叫偏移量的东西解决
-
取偏移量,需取序列中元素最小的值作为偏移量,也就是元素101
-
此时数组长度length就为 max- offset +1 = 8,再加1,也就是数组长度为9,索引为0~8,序列里值减去偏移量正好可以放进该计数数组里
-
创建计数数组之后,如何“放置”元素呢?
-
每次放置之前减去偏移量,取出到原序列再加上即可
-
// 计数,遍历原序列,并计数到新数组 for (int i = 0; i < arr.length; i++) { countArr[arr[i]-offset]++; } //-------取出----------- while (countArr[i] > 0) { arr[index++] = i+offset; countArr[i]--; }
-
自始至终都是对索引来操作,计数数组的元素只用来计数用的
-
-
对于存在负数的序列,反之则加上偏移量即可,但还会有一些bug,就不细讲了
细心的童孩还可发现,我取的数列都是相近的,没有偏离很多,这也是计数排序的一些局限性,如果取的值相差太大,那么必然导致一些空间会牺牲掉
最后,还存在一个问题,就是排序算法很重要的一点:稳定性
采用以上方法并无法得知此算法是稳定的,那么我们需要进行二次改造
七、稳定计数排序设计思路
稳定的算法应该是什么样?比如存在这样一个序列{1,3,4,4,2,5,3}
排序后:{1,2,3,3,4,4,5},稳定排序时原序列第一个4,依然要为排序后的第一个4
思路转换到计数排序,首先我们依然先得到计数数组,我们的计数数组是如何接收同样数据的呢?在对应索引位置,再对元素+1进行计数。那么哪个计数值对应着原序列哪个元素,我们需要好好细究下,来看看看这张图
计数数组索引为3这个位置,这个元素2是不是计数着原序列第二个3,2减去1之后,那个1才对应着原序列第一个3
懂了之后,我们就要对这个计数数组进行二次改造
改造目的:让计数数组索引上的计数值,可以一 一对应着原序列元素的位置
- 第一步:老样子得到计数数组,然后进行变形,从第二位开始,每一位的元素等于与前面一位元素之和
- 变形后的计数数组,元素不再是用来计数,而是表示一个位置
- 第二步:(从上图可发现)
- 变形后的计数数组的索引5的位置,元素为7。对应到原序列为元素5不恰巧排序后处在会原序列的第七个位置嘛
- 变形后的计数数组的索引4的位置,元素为6。对应到原序列为元素4不恰巧排序后处在会原序列的第六个位置嘛
- 接下来看索引3,元素为4。对应到原序列为元素3处在排序后原序列的第四个位置
- 那第五个位置哪去了,这是因为原序列有两个元素5,前面那个5当然就要处在第五个位置
- 所以呢?我们该如何让这个原序列中元素5不丢失,那就需要从后往前遍历变形后的这些内容
- 第三步:如何遍历变形后的计数数组并排序
- 知道了上面规律后,我们就来找联系关系
- 首先我们需要再创建一个数组:用于排序的数组 sortedArr[ ],该数组用于接收排序好的内容,并依次赋值给原序列
- 则sortedArr[ countArr[ arr[ i ] ] - 1 ] = arr[ i ],有大伙看出门道吗
- 变形后的countArr[ ]数组此时的元素实际代表的是原序列元素排序所在位置,而该位置上的索引正好是原序列的元素。也就是我们只要构建这种关系,即可得到一个排序好的结果
- 那为什么要减1的操作呢?这是因为变形后的计数数组,元素表示的位置是从1开始算的
- 接着还要对 countArr[ arr[ i ] ]- -,对上一个位置的值进行处理绑定
八、稳定计数排序代码实现
public static void stableCountSort(int[] arr){
// 找出数组中的最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]; // 找最大值,用于建立数组长度
}
}
// 初始化计数数组,长度记得再+1
int[] countArr = new int[max + 1];
// 计数,遍历原序列,并计数到新数组
for (int i = 0; i < arr.length; i++) {
countArr[arr[i]]++;
}
// 变形
for (int i = 1; i < countArr.length; ++i) {
countArr[i] = countArr[i-1] + countArr[i];
}
// 用于接收排序结果,并赋值给原数组
int[] sortedArr = new int[arr.length];
// 根据规律进行赋值即可到排序效果
for (int i = arr.length - 1; i >= 0; --i) {
sortedArr[countArr[arr[i]]-1] = arr[i];
countArr[arr[i]]--; // 操作上个位置
}
//将排序的结果赋值到原数组
for (int i = 0; i < arr.length; ++i) {
arr[i] = sortedArr[i];
}
}
九、时间复杂度
计数排序的时间复杂度为 O(n + k ),k 指的是原序列中最大最小元素差,说的简单点,计数排序算法的时间复杂度约等于 O(n),快于任何比较型的排序算法。
空间复杂度只考虑创建的计数数组大小那就是O(k)了
十、总结
计数排序虽然强大,但只适用于计算整数,且当差值过大时,并不适用
关于稳定计数排序和偏移量结合使用问题,博主是个小菜鸡,想不明白。还有该篇哪里讲的有问题,欢迎指正!!然后很开心你能看到这里,加油打工人