排序算法稳定性及意义
排序算法中有具有稳定性和非稳定两种。
对上面的数组进行排序,可以看到原数组中相同的数字也有一定的次序。什么是排序算法的稳定性?拿本示例来说明,如果一个排序算法对数组进行排序后,还能保持原数组的相同数字的次序那么这个排序算法就具有稳定性:
如上图,该数组排序之后,相同的数字仍保持了和原数组中相同数字的次序,那么这个算法就是具有稳定性的排序算法。如果排序之后,相同数字无法保证和原数组相同数字的那种次序,那么该算法就不具备稳定性。
冒泡排序的稳定性分析
冒泡排序是每次将最大的数字排到最后面,在设计冒泡排序的swap操作时,如果两数相等,那么不发生交换,这样冒泡排序就可以保证稳定性。所以冒泡排序是稳定性排序。
选择排序的稳定性分析
选择排序是找出最小数字的索引然后将minIndex处的数字依次同index == 0 处的数字进行交换,直至数列有序。试想对[7,7,1,4]这个数组进行排序,很显然第一步应该是1和第一个7发生交换,但是交换完毕后,第一个7就跑到了原数组第二个7的后面,所以,选择排序并不具备排序的稳定性。
插入排序的稳定性分析
插入排序即扑克牌排序,很显然,在依次插入数字的过程中,我们只要设定,当新插入的数字和前面的数字相等时,停止swap,就可以保证排序的稳定,所以插入排序是一个具备稳定性的排序算法。
归并排序的稳定性分析
归并排序是对两部分sort好的数据进行一个merge的过程,在merge过程中,如果左边sort好的指针p1指向的数字和右边sort好的指针p2指向的数字相等时,只需要让左边sort好的p1指向的数字进入新的mergeArr数组中,并让p1++即可,merge过程可以保证新的mergeArr相对于原数组保证了相同数字的次序性,所以归并排序是一个具有稳定性的排序。
快速排序的稳定性分析
快速排序是荷兰国旗问题的partition以及将这个过程重复递归的实现,partition无法保证稳定性,可以举例实验一下[7,7,1,4,9,7],对该数组进行partition以后,7的次序已经被打乱了,所以快速排序并不具有稳定性。
堆排序的稳定性分析
堆排序主要是由heapInsert以及heapify来实现的,还是拿[7,7,1,4,9,7]进行实验,在heapInsert实现最大堆的过程中,已经扰乱了7的次序,所以堆排序也不具备稳定性。
排序稳定性的意义
看这个例子:
要求,对各班进行成绩的排序,如果我使用的是不具备稳定性的排序算法,首先我要对班级进行排序,然后再依次对各个班级的成绩进行排序。如果我使用的是一个具有稳定性的排序算法,那么只需要对所有的data,先进行班级的排序,再对成绩进行排序,反之亦可。因为如果对班级先排好序,在进行成绩的排序时,班级的次序是不变的,先进行成绩上的排序,再进行班级的排序时,成绩上的次序也是不变的。在软件开发的领域中,有的时候,业务上面需要我们保证排序的稳定性。所以,排序稳定性有着重要的意义。
工程中的排序算法
工程中,我们并不需要手写排序算法,可以直接使用例如java.util.Arrays
类库提供的sort方法进行排序,那么工程中,类库提供给我们的算法是如何排序的呢?是使用了哪种排序呢?
首先,工程中类库提供的排序都是经过高度集成与优化的,时间复杂度O(NlogN)的算法都涉及到递归的操作,工程中是不允许出现递归的,这些递归操作都被改写成了非递归的操作。同时它也集成了多种排序。
在对数据进行排序前,首先Arrays.sort方法会先判断数据的长度,如果数据在一定范围内,会直接使用插入排序,因为插入排序虽然是一个时间复杂为O(N²)的排序算法,但是如果数据量并不是很大,那么相对来讲很快,并且插入排序的特点就是如果一个数据近似有序,那么时间复杂度也近似O(N)。如果数据量较大,那么Arrays.sort方法会再判断排序的data是基础类型还是一个自己定义的对象,如果是基础类型,那么会使用快排,因为基础类型要保持稳定性是无意义的,如果是对象例如自己定义的类Student,那么就会使用归并排序,保证data的稳定性。无论是归并排序还是快速排序,当数据量被分解到一定范围时,会再次转换为插入排序
归并排序及快排的补充点
- 归并排序的额外空间复杂度O(N)实际上可以转化为O(1),搜索并参照归并排序内部缓存法。
- 快速排序实际上可以做到稳定性,搜索并参照"01 stable sort"
比较器
比较器使用示例程序如下:
import java.util.Arrays;
import java.util.Comparator;
public class Student {
public String name;
public int classes;
public int grades;
public Student(String name,int classes,int grades){
this.name = name;
this.classes = classes;
this.grades = grades;
}
// 将班级按照升序排序
public static class ClassesAscendingComparator implements Comparator<Student>{
@Override
public int compare(Student o1,Student o2){
return o1.classes - o2.classes;
}
}
// 将成绩按照升序排序
public static class GradesAscendingComparator implements Comparator<Student>{
@Override
public int compare(Student o1,Student o2){
return o1.grades - o2.grades;
}
}
// print
public static void printStudents(Student[] students){
for(Student student:students){
System.out.println("班级:"+student.classes+"姓名:"+student.name+"成绩:"+student.grades);
}
System.out.println("=======================");
}
public static void main(String[] args){
Student s1 = new Student("王二毛",1,97);
Student s2 = new Student("李小虎",3,89);
Student s3 = new Student("李狗",2,88);
Student[] students = new Student[]{s1,s2,s3};
printStudents(students);
Arrays.sort(students,new ClassesAscendingComparator());
printStudents(students);
Arrays.sort(students,new GradesAscendingComparator());
printStudents(students);
}
}
结果如下:
班级:1姓名:王二毛成绩:97
班级:3姓名:李小虎成绩:89
班级:2姓名:李狗成绩:88
=======================
班级:1姓名:王二毛成绩:97
班级:2姓名:李狗成绩:88
班级:3姓名:李小虎成绩:89
=======================
班级:2姓名:李狗成绩:88
班级:3姓名:李小虎成绩:89
班级:1姓名:王二毛成绩:97
=======================
不仅仅是数组结构,其他的数据结构也具备排序方法,比较器则是告诉这些方法,应该按照哪一种标准进行排序。
计数排序
桶排序是一种非基于比较的排序思想。
计数排序是桶排序的一种实现,对多少范围的数字进行排序,则就要准备多少个桶,将数字对号放入桶内,然后再遍历桶,即可完成排序,计数排序的代码如下:
// 本程序并未考虑待排序的数组中含负数,小数等情况
public class BucketSort {
public static int[] initBucket(int[] arr){
int maxNum = Integer.MIN_VALUE;
for(int i = 0;i < arr.length;i++){
maxNum = Math.max(arr[i],maxNum);
}
return new int[maxNum+1];
}
public static void bucketSort(int[] arr){
if(arr == null || arr.length == 1)
return;
int[] bucket = initBucket(arr);
for(int i = 0;i < arr.length;i++){
bucket[arr[i]]++;
}
for(int i = 0,j = 0;i < bucket.length;i++){
if(bucket[i] != 0){
while(bucket[i]>0){
arr[j] = i;
j++;
bucket[i]--;
}
}
}
}
}
本程序并未考虑负数的情况,即使有负数程序也可以做出对应的改进,但是如果数组中出现了小数,非基于比较的桶排序则无法解决。那么计数排序是一种稳定性的排序算法么?是的,本程序不是基于稳定性设计的计数排序,但是仍然可以改进,改进成稳定性的计数排序程序如下:
// 具有稳定性的计数排序
public class BucketSort2 {
public static int[] initBucket(int[] arr){
int maxNum = Integer.MIN_VALUE;
for(int i = 0;i < arr.length;i++){
maxNum = Math.max(arr[i],maxNum);
}
return new int[maxNum+1];
}
public static void bucketSort(int[] arr){
if(arr == null || arr.length == 1)
return;
int[] bucket = initBucket(arr);
for(int i = 0;i < arr.length;i++){
bucket[arr[i]]++;
}
// 稳定性改进
for(int i = 1;i < bucket.length;i++){
bucket[i] = bucket[i-1]+bucket[i];
}
int[] helpArr = new int[arr.length];
for(int i = arr.length-1;i >=0;i--){
helpArr[bucket[arr[i]] - 1] = arr[i];
bucket[arr[i]]--;
}
for(int i = 0;i < arr.length;i++){
arr[i] = helpArr[i];
}
}
}
思路是怎样的呢?下图是非稳定的计数排序思路,bucket的索引代表的arr的具体数值,而bucket数组中存储的数字代表的是arr数组中bucket[index]出现的次数。
而改进的代码思路是这样的:
for(int i = 1;i < bucket.length;i++){
bucket[i] = bucket[i-1]+bucket[i];
}
bucket数组 bucket[index]不再表示index出现了多少个,而是表示包括index前面还有多少个数字。
将bucket设计为这样的功能后,再声明一个和arr长度一样的空数组。
然后,对arr进行从后往前的遍历,遍历第一个数字arr[arr.length-1]即7时,看看bucket[arr[arr.length-1]]存储的值为6,代表排好序后,7这个数字应该排在第六位,我们将7存储在helpArr中即,helpArr[6-1] = 7。同时,因为7在helpArr中已经排好序了,所以我们要将bucket[arr[arr.length-1]]存储的数字进行减一。我们将helpArr数组填满后,再将helpArr数组复制到原数组中,这样我们就完成了整个排序的过程。因为我们从后向前去遍历,所以保证了原数组的次序性,所以改进后的计数排序是具有稳定性的。
基数排序
如果知道了计数排序,那么基数排序也很好理解。基数排序准备了十个桶,然后对原数组的数字的个位,十位,百位,千位...依次进行入桶排序的操作。如果不理解,可以随意参照一个动态演示进行理解。基数排序的代码如下所示:
public class RadixSort {
public static int getMaxNumLength(int[] arr){
int max = Integer.MIN_VALUE;
for(int i = 0;i < arr.length;i++){
max = Math.max(arr[i],max);
}
int len = 0;
while(max != 0){
max = max/10;
len++;
}
return len;
}
// 获取num第d位上的数字
public static int getBit(int num,int d){
return (int)((num % Math.pow(10,d))/Math.pow(10,(d-1)));
}
public static void radixSort(int[] arr){
int maxNumLen = getMaxNumLength(arr);
int[] bucket = new int[10];
int[] helpArr = new int[arr.length];
for(int i = 1;i <= maxNumLen;i++){
for(int j = 0;j < bucket.length;j++){
bucket[j] = 0;
}
for(int j = 0;j < arr.length;j++){
bucket[getBit(arr[j],i)]++;
}
for(int j = 1;j<bucket.length;j++){
bucket[j] = bucket[j-1] + bucket[j];
}
for(int j = arr.length-1;j>=0;j--){
helpArr[bucket[getBit(arr[j],i)]-1] = arr[j];
bucket[getBit(arr[j],i)]--;
}
for(int j = 0;j < arr.length;j++){
arr[j] = helpArr[j];
}
}
}
}
基数排序相对于计数排序而言,优化了桶的数量。如果数字特别大的情况下,比如数组中最大的数字是798,那么计数 排序就需要准备799个桶,而基数排序只需要准备10个桶,然后循环3次计数排序的过程即可。
桶排序问题
给定一个数组,求如果排序之后,相邻两数的最大差值
要求:时间复杂度为O(N) 且,要求不能使用非基于比较的排序(桶排序)
这道题,要求时间复杂度为O(N)。基于比较的排序,时间复杂度最好也只有O(NlogN),虽然桶排序的时间复杂度为O(N),但是本题限制了使用桶排序。但是虽然禁止使用桶排,不过可以巧用桶排序的思想。对于一个待排序的数组arr来说,我们准备arr.length+1个桶。思路如下:
举例:现有数组[37,49,88,99,0,17,26,72,23],数组的length为9,我们需要准备10个桶,数组中最大的数字为99,最小的数字为0,按照范围,数组中的数字被分配到桶里,不过桶承载3个信息,1:hasNum表示是否该桶是否为空,2:max表示桶中的最大值,min表示桶中的最小值。结果如下:
我们思考一下,相邻两数最大差值是什么?
会不会来自同一个桶?
不会,因为N个数字,我们准备N+1个桶,即使一个桶只盛放一个数字那么也还剩下一个空桶。只要剩一个空桶,相邻两数最大差值一定不来自一个桶。为什么原因如下:
一个桶数字最大的差值肯定要小于空桶两侧的相邻差值。本示例中,来自不同的桶的相邻数为49,60,它们的差值也要比相同桶中的最大相邻差值情况9要大,所以相邻两数最大的差值绝对不可能来自同一个桶。
所以,我们只需要设定一个变量为maxGap,计算出相邻两桶(空桶不算相邻桶)的min-max,将最大值赋予maxGap即为我们所要的答案,虽然我们应用了桶的概念,但是实际上根本不需要进行排序。代码如下:
public class MaxGap2 {
// 最大数字无法使用whichBucket判断,当num为max时,结果应当为 whichBucket(max,len,max,min) - 1
public static int whichBucket(int num,int len,int max,int min){
return (int)((num - min) * len / (max - min));
}
public static int maxGap(int[] arr){
if(arr == null || arr.length <2)
return 0;
int minNum = Integer.MAX_VALUE;
int maxNum = Integer.MIN_VALUE;
int[] bucket = new int[arr.length+1];
for(int i = 0;i < arr.length;i++){
minNum = Math.min(minNum,arr[i]);
maxNum = Math.max(maxNum,arr[i]);
}
if(maxNum == minNum)
return 0;
boolean[] hasNum = new boolean[arr.length+1];
int[] maxBucketNum = new int[arr.length+1];
int[] minBucketNum = new int[arr.length+1];
for(int i = 0;i < arr.length;i++){
int bucketIndex = whichBucket(arr[i],arr.length+1,maxNum,minNum);
if(arr[i] == maxNum){
bucketIndex--;
}
if(!hasNum[bucketIndex]){
maxBucketNum[bucketIndex] = arr[i];
minBucketNum[bucketIndex] = arr[i];
hasNum[bucketIndex] = true;
}else{
maxBucketNum[bucketIndex] = arr[i] > maxBucketNum[bucketIndex] ? arr[i] : maxBucketNum[bucketIndex];
minBucketNum[bucketIndex] = arr[i] < minBucketNum[bucketIndex] ? arr[i] : minBucketNum[bucketIndex];
}
}
int maxBefore = maxBucketNum[0];
int maxGap = Integer.MIN_VALUE;
for(int i = 1;i < arr.length+1;i++){
if(hasNum[i]){
maxGap = Math.max((minBucketNum[i] - maxBefore),maxGap);
maxBefore = maxBucketNum[i];
}
}
return maxGap;
}
}