排序算法稳定性及桶排序

排序算法稳定性及意义

排序算法中有具有稳定性和非稳定两种。


image.png

对上面的数组进行排序,可以看到原数组中相同的数字也有一定的次序。什么是排序算法的稳定性?拿本示例来说明,如果一个排序算法对数组进行排序后,还能保持原数组的相同数字的次序那么这个排序算法就具有稳定性:


稳定性排序.png

如上图,该数组排序之后,相同的数字仍保持了和原数组中相同数字的次序,那么这个算法就是具有稳定性的排序算法。如果排序之后,相同数字无法保证和原数组相同数字的那种次序,那么该算法就不具备稳定性。

冒泡排序的稳定性分析

冒泡排序是每次将最大的数字排到最后面,在设计冒泡排序的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的次序,所以堆排序也不具备稳定性。

排序稳定性的意义

看这个例子:


image.png

要求,对各班进行成绩的排序,如果我使用的是不具备稳定性的排序算法,首先我要对班级进行排序,然后再依次对各个班级的成绩进行排序。如果我使用的是一个具有稳定性的排序算法,那么只需要对所有的data,先进行班级的排序,再对成绩进行排序,反之亦可。因为如果对班级先排好序,在进行成绩的排序时,班级的次序是不变的,先进行成绩上的排序,再进行班级的排序时,成绩上的次序也是不变的。在软件开发的领域中,有的时候,业务上面需要我们保证排序的稳定性。所以,排序稳定性有着重要的意义。

工程中的排序算法

工程中,我们并不需要手写排序算法,可以直接使用例如java.util.Arrays类库提供的sort方法进行排序,那么工程中,类库提供给我们的算法是如何排序的呢?是使用了哪种排序呢?

首先,工程中类库提供的排序都是经过高度集成与优化的,时间复杂度O(NlogN)的算法都涉及到递归的操作,工程中是不允许出现递归的,这些递归操作都被改写成了非递归的操作。同时它也集成了多种排序。

在对数据进行排序前,首先Arrays.sort方法会先判断数据的长度,如果数据在一定范围内,会直接使用插入排序,因为插入排序虽然是一个时间复杂为O(N²)的排序算法,但是如果数据量并不是很大,那么相对来讲很快,并且插入排序的特点就是如果一个数据近似有序,那么时间复杂度也近似O(N)。如果数据量较大,那么Arrays.sort方法会再判断排序的data是基础类型还是一个自己定义的对象,如果是基础类型,那么会使用快排,因为基础类型要保持稳定性是无意义的,如果是对象例如自己定义的类Student,那么就会使用归并排序,保证data的稳定性。无论是归并排序还是快速排序,当数据量被分解到一定范围时,会再次转换为插入排序

归并排序及快排的补充点

  1. 归并排序的额外空间复杂度O(N)实际上可以转化为O(1),搜索并参照归并排序内部缓存法。
  2. 快速排序实际上可以做到稳定性,搜索并参照"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]出现的次数。


image.png

而改进的代码思路是这样的:

        for(int i = 1;i < bucket.length;i++){
            bucket[i] = bucket[i-1]+bucket[i];
        }
image.png

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表示桶中的最小值。结果如下:


image.png

我们思考一下,相邻两数最大差值是什么?

会不会来自同一个桶?
不会,因为N个数字,我们准备N+1个桶,即使一个桶只盛放一个数字那么也还剩下一个空桶。只要剩一个空桶,相邻两数最大差值一定不来自一个桶。为什么原因如下:


image.png

一个桶数字最大的差值肯定要小于空桶两侧的相邻差值。本示例中,来自不同的桶的相邻数为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;
    }
}