前面关于一篇冒泡,选择,和插入排序的文章小爆了一下,真是让我受宠若惊,这是文章链接,大家感兴趣可以一起看一下,最后,博主现在也是一名大学生,让我们一起努力!排序算法1(冒泡,选择,插入)_@我好菜啊的博客-CSDN博客
之前我们学习的几种排序算法中,第一篇时时间复杂度为O(n^2)的冒泡排序,选择排序和插入排序,还有时间复杂度O(nlogn)的快速排序和归并排序,其中这两个排序的主要思想还是分治法,这篇主要介绍时间复杂度为O(n)的桶排序,计数排序和基数排序。
因为桶排序,计数排序和基数排序的时间复杂度都为O(n),他们是线性的,所以又称线性排序,之所以他们能够做到时间复杂度为线性,主要是因为他们都不是基于比较的排序算法,也就是说排序的过程中不涉及元素之间的比较。
桶排序
桶排序的核心处理思想是先定义几个有序的“桶”,将要排序的数据分到这几个桶,对每个“桶”里的数据进行单独的排序,再把桶里的数据按照顺序依次取出,组成的序列就是有序的。
看一张图
现在来看看他的时间复杂度。
假设要排序的元素一共有n个,把他们分到m个桶中,所以每个桶中的元素为k = n/m。然后对每个桶里面的元素使用快速排序算法来进行排序,这是一个桶内的时间复杂度为O(klog(k)),所以m个桶的时间复杂度为O(mklog(k)),又因为k = n/m,所以整个桶排序的时间复杂度为O(nlog(n/m))。当桶的个数m无限趋近于n的时候,桶排序的时间复杂度趋近于O(n)。
这个时候可能会有疑问,桶排序的性能看起来非常出色,能不能去代替快速排序呢?
答案肯定是不可以的,因为桶排序对排序的数据有一定的要求。
第一:待排序数据容易划分成m个桶,并且桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完成之后,各个桶之技安的数据不需要进行排序了。
第二:数据在各个桶之间是相对均匀的。也有特殊的情况,如果数据经过划分之后,有的桶的数据非常多,有的桶的数据非常少,那么桶内数据排序的时间复杂度就不是常量级的了。再更极端的情况下,数据再一个桶里面进行排序,那么他的时间复杂度会退化为O(nlog(n));
下面来说说桶排序的适用场景。
事实上,桶排序比较适用在外部排序中,外部排序就是数据存储在外部磁盘上,数据量比较大,但是内存有限,无法将数据全部加载到内存中处理。
看一个例子
假设有10GB的订单数据,我们希望按照金额进行排序,但是机器的内存有限,只有几百MB,没办法把数据全部加载待内存中进行处理。这个时候我们就可以借助桶排序。
处理步骤:
先扫描一下文件,查看订单金额所处的数据范围,如果扫描后得到的订单最小为1元,最大为10万元,我们将所有的订单根据金额划分的100个桶中,第一个桶存放的数据为1到1000,第二给数据存放的为1001到2000,依次类推。每个桶对应一个文件,并且按照金额大小进行编号。在理想的情况下,全部数据均匀分布到这一百个桶(文件),每个桶(文件)大概存放100MB的数据,我们就可以将这100个文件依次放入内存中,用快速排序算法进行排序,之后重新写回文件。
等所有文件排序好后,之后按照文件的编号依次读取每个文件的数据,并写入一个新的大文件,这个大文件的数据就是排序好的数据。
不过还是有特殊情况的,那就是划分之后的数据并不均匀,有的桶中存放的数据较多,有的就会较少。针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到200 元,201 元到 300 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订 单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为
下面来看看代码。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
class Test1{
public void bucketSort(int[] arr){
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
//找到数组元素的临界值
for(int i = 0;i < arr.length;i++){
max = Math.max(max,arr[i]);
min = Math.min(min,arr[i]);
}
//桶数
int bucketNum = (max - min) / arr.length + 1;
//泛型表示桶中装入一组数组数据
//创建桶
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0;i < bucketNum;i++){
bucketArr.add(new ArrayList<>());
}
//将每个元素放入桶中
for(int i = 0;i < arr.length;i++){
//找到适合的桶
int num = (arr[i] - min) / arr.length;
bucketArr.get(num).add(arr[i]);
}
//对每个桶元素进行排序
for(int i = 0;i < bucketArr.size();i++){
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
}
计数排序
计数排序是桶排序的一种特殊情况,当要排序的n个数据所处的范围并不大时,如最大是k,我们就可以把数据划分成k个桶。每个桶内的数据都是相等的,省略掉了排序的时间。
计数排序的思想很简单,与桶排序类似,只是桶的大小不一样。下面来看看计数排序的实现方法,先看一个例子。
有八个考生,分数范围是0到5分。这八个考生的成绩放在数组A[8]中,成绩分别是:2,5,3,0,2,3,0,3.考生的成绩从0到5分,我们可以使用大小为6的数组C[6]表示桶,他的下标对应分数,不过里面存储的内容为分数相同学生的个数。我们只需要遍历一遍记录考生分数的数组A[8],就可以得到C[6]的值。看一个图。
从下图可以看出,得到三分的考生只有三人,因此,他们在排序之后的有序数组R[8]中,会保存在下标为4,5,6的位置。
那么随之而来的又有一个问题,如何计算出每个分数的考生在有序数组中对应的存储位置呢?
处理的方法很巧妙,就是将C[6]数组顺序累加求和,C[k]里存储小于或等于分数k的考生个数
接下来是重点!!!
然后需要讨论的是数组A和数组C填充数组R
从后往前依次扫描数组A[2,5,3,0,2,3,0,3],当扫描到3的时候,我们从数组C中下标为3中存储的元素,如上图所示,元素为7。7所代表的意思是,到目前为止,分数小于或等于3的考生有7个,也就是说,3是数组R中第七个元素,也就是下标为6的地方。当我们把元素放到C[6]的地方的时候,小于或等于3的元素就剩下了六个,此时数组C[3]要对应的减去1,变成6.
依次类推,当扫描到第二个分数为3的时候,就会把3放入数组R中第六个元素的位置。当扫描完整个A数组时,也就完成了排序。
是不是还没看懂,再看一个图。
然后来看看代码
public void countingSort(int[] a,int n){
if(n <= 1) return;
//查找数组中数据的范围,也就是最大的数
int max = a[0];
for(int i = 1;i < a.length;i++){
if(a[i] > max){
max = a[i];
}
}
//申请一个计数数组,数组大小为max+1
int[] c = new int[max + 1];
//默认值全为0
for(int i = 0;i <= max;i++){
c[i] = 0;
}
//计算每个元素的个数,放入计数数组C中
for(int i = 0;i < n;i++){
c[a[i]]++; //大家仔细琢磨一下很巧妙
}
//将c数组中的元素依次累加
for(int i = 1;i <= max;i++){
c[i] = c[i - 1] + c[i];
}
//临时数组r,存储排序之后的结果
//注意从后往前扫描
int[] r = new int[n];
for(int i = n - 1;i >= 0;i--){
int index = c[a[i]] - 1;//下标值
r[index] = a[i]; //赋给临时数组r
c[a[i]]--;//原本的值需要自减
}
//将结果赋给a,可以用增强for
for(int i = 0;i < n;i++){
a[i] = r[i];
}
}
还有前面遗留的一点小问题,那就是为什么从后往前扫描数组A,是因为为了保证排序算法的稳定性,也就是排序前后,值相同的元素前后顺序不变。
计数排序的时间复杂度为O(n + k),k表示要排序的数据范围,如果k远小于n,那么时间复杂度就可以写为O(n)。也就是说,计数排序用于数据范围不大的场景中,还有就是计数排序只能给非负整数排序。如果待排序的数据为其他类型,那么我们需要在不改变相对大小的情况下,转换为非负整数。
基数排序
如果我们有十万个手机号码,然后希望将这些号码进行从小到大的排序,因为手机号码有11位,范围太大,此时其他的排序算法就不是很适合,这个适合就可以使用基数排序。
不难发现有这样一个规律,如果要比较a,b两个号码扽大小,如果前面几位数中,已经比b大了,那么之后的元素就不用比较了。所有我们想到了一个解题思路:先按照最后一位来进行排序,之后倒数第二位,以此类推,经过11次就已经排序成功了。
看一个图,字母代替号码
不过需要注意的是,按照每位来排序手机号码的排序方法必须是稳定的,如果是非稳定的,后一次排序并不会兼顾前一次排序之后的数据的顺序,那么这个算法肯定就是错误的。
package sort;
public class RadixSort {
private static void radixSort(int[] array,int d)
{
int n=1;//代表位数对应的数:1,10,100...
int k=0;//保存每一位排序后的结果用于下一位的排序输入
int length=array.length;
int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
int[] order=new int[length];//用于保存每个桶里有多少个数字
while(n<d)
{
for(int num:array) //将数组array里的每个数字放在相应的桶里
{
int digit=(num/n)%10;
bucket[digit][order[digit]]=num;
order[digit]++;
}
for(int i=0;i<length;i++)//将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
{
if(order[i]!=0)//这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
{
for(int j=0;j<order[i];j++)
{
array[k]=bucket[i][j];
k++;
}
}
order[i]=0;//将桶里计数器置0,用于下一次位排序
}
n*=10;
k=0;//将k置0,用于下一轮保存位排序结果
}
}
public static void main(String[] args)
{
int[] A=new int[]{73,22, 93, 43, 55, 14, 28, 65, 39, 81};
radixSort(A, 100);
for(int num:A)
{
System.out.println(num);
}
}
}