Java数据结构排序详解--冒泡排序、直接插入排序、折半查找、希尔排序、选择排序、归并排序、快排

冒泡、直接插入、折半查找、希尔、选择、归并、快排

我们先思考一个问题:如何衡量一个排序算法的优劣呢?

一般来讲我们可从三个方面来看:

1.算法的时间效率:

a.时间复杂度:最好、最坏、平均时间复杂度相结合判断。

b.时间复杂度系数、低阶、常数的影响(因只有元素数量足够多时才可忽略系数、低阶、常数的影响,否则不可忽略)。

c.比较交换次数。

2.算法的内存消耗:

通过空间复杂度衡量。

3.算法的稳定性(重要)

算法的稳定性是指若在待排序的集合中存在数值相等的元素,判断集合有序后数值相等元素相对位置是否改变,若未改变,则为稳定性排序。

算法稳定性图解:

在这里插入图片描述

另外介绍一下原地排序的概念:原地排序是特指空间复杂度为O(1)的排序算法。

冒泡排序

冒泡排序每次只会操作相邻两个元素,对其做大小比较,看是否满足于题目要求排序。每次都会保证有一个元素被移动到最终位置。

冒泡排序图解:

在这里插入图片描述

冒泡排序代码:

public class Test {
    public static void main(String[] args) {
        int[] data=new int[]{6,4,5,1,2,3};
        bubbleSort(data);
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    public static void bubbleSort(int[] data){
        int n=data.length;
        if(n<=1){//判断若数组为空或只有一个元素时直接返回即可
            return;
        }
        for(int i=0;i<n;i++){//控制循环次数
            for(int j=0;j<n-i-1;j++){//控制下次遍历元素个数(因已移动到最终位置的元素不需要遍历)
                if(data[j]>data[j+1]){
                    data[j]^=data[j+1];
                    data[j+1]^=data[j];
                    data[j]^=data[j+1];
                }
            }
        }
    }
}

因数组元素可能在冒泡几次后已经有序,故可提前退出。所以冒泡排序可进行优化,优化思路为设立一个标志点进行判断(若某次元素间无交换就说明数组已经有序)

优化代码如下:

public class Test {
    public static void main(String[] args) {
        int[] data=new int[]{6,4,5,1,2,3};
        bubbleSort(data);
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    public static void bubbleSort(int[] data){
        int n=data.length;
        if(n<=1){
            return;
        }
        for(int i=0;i<n;i++){
            boolean flag=false;//设立标志点默认为false
            for(int j=0;j<n-i-1;j++){
                if(data[j]>data[j+1]){
                    flag=true;//若有元素交换则标志点为true
                    data[j]^=data[j+1];
                    data[j+1]^=data[j];
                    data[j]^=data[j+1];
                }
            }
            if(!flag){//判断标志点,若标志点为false即可认为数组有序,直接跳出即可
                System.out.println("冒泡"+i+"次后数组有序");
                break;
            }
        }
    }
}

在这里插入图片描述

我们通过代码可以看出冒泡排序最好排序时间复杂度为O(n)(情况为数组正好与最终要求一致,只需遍历一遍检查即可),最坏时间复杂度为O(n2)(情况为数组顺序正好与最终要求完全相反例如原数组为降序最终要求为升序),平均时间复杂度为O(n2)(元素相当大时才可忽略系数、低阶、常数的影响)

空间时间复杂度为O(1)(因没有新开辟空间,只是在原有数组的基础上交换)。

冒泡排序为稳定性排序和原地排序。

插入排序

1.直接插入排序:基于有序数组元素内容的插入得来。

做题思路:首先需要找到元素插入位置,再将元素插入即可。

核心步骤:将数组分为已排序区间和待排序区间(默认第一个元素为有序的,后面其他元素为无序)。若按照升序排序,则需将待排序区间第一个元素与已排序区间最后一个元素进行大小比较,若大于,则不需移动成为已排序区间最后一个元素即可;若小于,则需要将待排序区间第一个元素(插入元素)挨个从后向前对比已排序区间元素,直至找到合适位置插入即可。

核心步骤图解:

在这里插入图片描述

插入排序源代码:

public class Test {
    public static void main(String[] args) {
        int[] data=new int[]{6,4,5,1,2,3};
        insertSort(data);
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    public static void insertSort(int[] data){//直接插入排序算法
        int n=data.length;
        if(n<=1){//判断若数组为空或只有一个元素时直接返回即可
            return;
        }
        for(int i=1;i<n;i++){//外层循环控制循环次数(下标从1开始是因为默认第一个元素有序,此循环区间为待排序区间)
            int  value=data[i];//插入元素
            //内层循环控制两元素对比次数(下标从已排序区间最后一个元素开始从后向前遍历)
            int j=i-1;
            for(;j>=0;j--){
                if(value<data[j]){
                    data[j+1]=data[j];//若插入元素小于已排序元素,则需要向前移动,即相当于已排序元素向后移动
                }else{//若插入元素大于已排序某元素直接跳出
                    break;
                }
            }
            data[j+1]=value;//将元素插入(此时j+1是因为上面循环退出时j--之后判断)
        }
    }
}

我们通过代码可以看出直接插入排序最好排序时间复杂度为O(n)(情况为数组正好与最终要求一致,只需遍历一遍检查即可),最坏时间复杂度为O(n2)(情况为数组顺序正好与最终要求完全相反例如原数组为降序最终要求为升序),平均时间复杂度为O(n2)(元素相当大时才可忽略系数、低阶、常数的影响)

空间时间复杂度为O(1)(因没有新开辟空间,只是在原有数组的基础上交换)。

直接插入排序为稳定性排序和原地排序。

那么冒泡排序和直接插入排序相比哪个应用更广、效率更高呢?

我们按照开头判断排序算法效率的方法来看,时间复杂度空间复杂度两者均相同,故我们可看元素之间的交换次数。我们通过分析可知冒泡排序每次均要进行两个相邻元素之间的对比交换,而直接插入排序交换次数明显较少。故直接插入排序算法应用更广、效率更高。

测试代码:

import java.util.Random;

public class Test {
    public static void main(String[] args) {
        int[] data=generateRandomArray(8000,1000,10000);
        bubbleSort(data);
        insertSort(data);
    }
    public static void insertSort(int[] data){
        long startTime=System.currentTimeMillis();
        int n=data.length;
        if(n<=1){
            return;
        }
        for(int i=1;i<n;i++){
            int  value=data[i];
            int j=i-1;
            for(;j>=0;j--){
                if(value<data[j]){
                    data[j+1]=data[j];
                }else{
                    break;
                }
            }
            data[j+1]=value;
        }
        long endTime=System.currentTimeMillis();
        long time=endTime-startTime;
        System.out.println("直接插入:"+time+"毫秒");
    }
    public static void bubbleSort(int[] data){
        long starttime=System.currentTimeMillis();
        int n=data.length;
        if(n<=1){
            return;
        }
        for(int i=0;i<n;i++){
            boolean flag=false;
            for(int j=0;j<n-i-1;j++){
                if(data[j]>data[j+1]){
                    flag=true;
                    data[j]^=data[j+1];
                    data[j+1]^=data[j];
                    data[j]^=data[j+1];
                }
            }
            if(!flag){
                break;
            }
        }
        long endtime=System.currentTimeMillis();
        long time=endtime-starttime;
        System.out.println("冒泡:"+time+"毫秒");
    }
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
    public static int[] dataCopy(){
        return 
    }
}

在这里插入图片描述

2.折半插入(二分查找):直接插入优化一

思路:

第一步:将原始数组划分为已排序区间和待排序区间,默认原始数组第一个元素为有序的,故待排序区间为原始数组第二个元素到最后一个元素。依次将待排序区间元素与已排序区间中间元素比较大小,若待排序元素大于中间元素则在中间元素后面继续使用二分查找(low=mid+1),反之在前面继续查找直到找到插入位置。(high=mid-1)。

第二步:判断元素插入位置是否在已排序区间最后一个元素至前,若是则需要先向后搬移元素再插入,若否则直接插入即可(此种情况为待插入元素大于已排序区间最后一个元素,即大于已排序区间所有元素,直接插入最后一个即可)。

折半查找图解:

在这里插入图片描述

折半查找源代码:

import java.util.Random;

public class Test {
    public static void main(String[] args) {
        int[] data=generateRandomArray(20,100,1000);
        sort(data);
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    public static void sort(int[] data){
        int n=data.length;
        if(n<=1){//判断数组是否为空数组或只有一个元素,若是直接返回
            return;
        }else {
            for(int i=1;i<n;i++){//遍历待排序区间元素
                int low=0;//已排序区间头下标
                int high=i-1;//已排序区间尾下标
                int value=data[i];//待插入元素
                int j=i-1;//判断待插入元素插入位置是否大于已排序区间最后一个元素位置,若是直接插入,否则需要向后搬移元素再插入
                while (low<=high){//控制循环次数
                    int mid=((low^high)+(low&high)/2);//求平均值不会溢出
            //为保证折半查找的稳定性,判断中间值与待插入元素大小关系时应如下(若相反则会破坏其稳定性)
                    if(data[mid]>value){//若中间值大于待插入元素,则应在中间值前面继续二分查找,中间值后面不考虑
                        high=mid-1;
                    }else {//若中间值小于等于待插入元素,则应在中间值后面继续二分查找,中间值前面不考虑
                        low=mid+1;
                    }
                }
                for(;j>=high+1;j--){//若插入位置在已排序区间内,则需要从后向前挨个搬移元素
                    data[j+1]=data[j];
                }
                data[j+1]=value;//最后再插入元素
            }
        }
    }
    //在一定范围内随机生成n个数字[rangL,rangR]
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
}

确保算法稳定性处代码图解:

在这里插入图片描述

如果判断条件写反就变成不稳定算法:

在这里插入图片描述

运行结果:

在这里插入图片描述

折半查找时间复杂度:O(log2n)

空间复杂度:O(1)

稳定性:是。

3.希尔排序:直接插入排序优化二

思路:以上直接插入排序、折半查找排序都是挨个查找交换,那么能不能跳跃式比较大小交换呢?希尔排序就是采用跳跃式比较交换方式。具体执行过程是自行定义一个step(就是你希望每次跳跃多大的距离,自行定义),将距离最近step的元素进行大小比较,若前面元素大于后面,则交换;然后不断缩短step的范围,到step=1时结束(step=1时相当于直接排序)。

希尔排序图解:

在这里插入图片描述

希尔排序代码:

import java.util.Random;

public class Test {
    public static void main(String[] args) {
        int[] data=generateRandomArray(20,100,1000);
//        int[] data=new int[]{2,8,6,3,1,7,5,4};
        sort(data);
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    public static void sort(int[] data){
        int n=data.length;
        if(n<=1){
            return;
        }else {
            int step=n/2;//自定义即可没有规定
            while(step>=1){
                for(int i=step;i<n;i++){//遍历step之后的元素
                    int value=data[i];
                    int j=i-step;
                    for(;j>=0;j-=step){//控制两元素比较大小次数
                        if(value<data[j]){//当待排序元素小于前面元素时,将前面元素搬移至后面i处
                            data[j+step]=data[j];
                        }else{
                            break;
                        }
                    }
                    data[j+step]=value;//插入元素
                }
                step/=2;//控制元素比较大小之间距离
            }
        }
    }
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
}

运行结果:

在这里插入图片描述

时间复杂度在O(n1.5)-O(n2)之间,因比较两元素之间长度为自定义。

空间复杂度O(1)。因没有开辟新空间只是在原有基础上进行元素交换。

稳定性:否。举例待排序数组为 2 8 8 2,step=2是,前面的8会移动到后面8的后面,相等元素的相对位置改变,故不是稳定性算法排序。

选择排序

思路:将原始数组分为已排序区间和待排序区间,但默认已排序空间为空,原始数组元素为待排序空间;每次查找到待排序空间中的最小值,放置已排序区间的末尾即可。

选择排序图解:

在这里插入图片描述

选择排序源代码:

import java.util.Random;

public class Test {
    public static void main(String[] args) {
        int[] data=generateRandomArray(20,100,1000);
//        int[] data=new int[]{2,8,6,3,2,7,5,4};
        sort(data);
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    public static void sort(int[] data){
        int n=data.length;
        if(n<=1){
            return;
        }else {
            for(int i=0;i<n-1;i++){//控制遍历次数
                int minIndex=i;//默认待排序区间最小元素为待排序区间第一个元素,统计其下标
                int j=i+1;
                for(;j<n;j++){//比较找出最小值
                    if(data[j]<data[minIndex]){
                        minIndex=j;//找到最小值的下标
                    }
                }
                //最小值与待排序区间第一个元素交换(即交换完就成为已排序区间的最后一个元素)
                //此处交换不可使用异或交换,若采用时假设第一次选择排序第一个元素恰好就是最小元素,此时自己跟自己异或等于0结果错误。
                int temp=data[i];
                data[i]=data[minIndex];
                data[minIndex]=temp;
            }
        }
    }
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
}

时间复杂度:最好最坏平均都是O(n)

空间复杂度:O(1),为原地排序

稳定性:否。例如待排序数组为5 8 5 2 9时,两个5之间的相对位置会发生改变,故不是稳定性排序。

我们可以发现,选择排序是跟直接插入排序有一些类似的,那么在同等条件下,选选择排序呢还是直接插入排序呢?

答案是选择直接插入排序,因两者时间复杂度、空间复杂度都相同,但直接插入排序为稳定性排序。

归并排序:采用分治思想

分治思想:将大事情化小,类似于递归。故采用分治思想可以解决的问题一定可以使用递归方法。

思路:找到数组第一个元素下标、最后一个元素下标、中间元素下标,利用递归方法将数组满满划分为更小的数组,直到当前第一个元素下标和最后一个元素下标重合时(即此时数组只有一个元素),比较相邻两个小数组元素大小,将其合并(利用建立临时数组),最后将临时数组中的元素拷贝回原数组即可。

归并排序图解:

在这里插入图片描述

归并排序源代码:

public class Test {
    public static void main(String[] args) {
        int[] data=new int[]{1,6,5,6,5,7,3,9,0};
        mergeSort(data);
    }
    public static void mergeSort(int[] data){
        int n=data.length;
        if(n<=1){
            return;
        }
        mergeInternal(data,0,n-1);//划分数组需要知道数组第一个元素下标、最后一个元素下标
        for(int i:data){
            System.out.print(i+" ");
        }
    }
    private static void mergeInternal(int[] data,int low,int high){//拆分数组
        if(low>=high){//递归出口,若数组拆分至只有一个元素时退出递归
            return;
        }
        int mid=(low+high)>>1;//中间元素下标
        //左边小数组
        mergeInternal(data,low,mid);//当初始数组有8个元素时,中间元素下标为3,第四个元素
        //右边小数组
        mergeInternal(data,mid+1,high);
        //合并
        merge(data,low,mid,high);
    }
    private static void merge(int[] data,int low,int mid,int high){//合并数组
        //将两个数组分别比较元素大小,小的放在临时数组前面,需要直到两个数组的首元素下标
        int i=low;//第一个数组首元素下标
        int j=mid+1;//第二个数组首元素下标
        int k=0;//临时数组下标
        int[] temp=new int[high-low+1];//临时数组长度为要合并两数组长度之和
        //此时两个数组中均有元素
        while (i<=mid&&j<=high){
            if(data[i]<=data[j]){//体现其稳定性
                //第一个数组中的相同位置元素最小
                temp[k++]=data[i++];
            }else{
                temp[k++]=data[j++];
            }
        }
        //判断当前还有哪个数组元素没有走完(可能出现情况为某个数组元素可能统一较小,故会先跑完)
        //假设第一个数组还没有走完
        int start=i;
        int end=mid;
        if(j<=high){//第二个数组还没有走完
            start=j;
            end=high;
        }
        //把剩余元素直接放置在temp数组即可
        while (start<=end){
            temp[k++]=data[start++];
        }
        //将临时空间中已经合并好的元素拷贝回原数组
        for(i=0;i<=high-low;i++){
            data[low+i]=temp[i];
        }
    }
}

归并排序时间复杂度:O(nlogn)

空间复杂度T(n)

稳定性:是

快速排序

思路:在原始数组中找到一个区分点,然后遍历数组将大于区分点的元素放置区分点右边,小于等于区分点的元素放置区分点左边,每当发现比基准值小的元素就放在基准值左边,大于等于时放在基准值右边。当结束一次遍历时,基准值元素一定在最终位置。按照基准值左边与右边待排序数组重复上述过程。

快排整体思路图解:

在这里插入图片描述

快排具体执行过程图解:

在这里插入图片描述

快速排序源代码:

import java.util.Random;

public class Inc {
    public static void main(String[] args) {
        int[] data=generateRandomArray(10000,10,20000);//随机生成一万个整数,在10-20000区间
        quickSort(data);
    }
    public static void quickSort(int[] array){//快排
        long start=System.currentTimeMillis();
        int n=array.length;
        if(n<=1){//判断待排序数组是否为空数组或只含有一个元素,若是则直接返回
            return;
        }
        quickSortInternal(array,0,n-1);
        long end=System.currentTimeMillis();
        System.out.println("基础快排耗时:"+(end-start)+"毫秒");//测试程序耗时
    }
    //快排递归
    private static void quickSortInternal(int[] array,int low,int high){
        if(low>=high){//递归推出条件
            return;
        }
        int q=partition(array,low,high);//确定分区点最终位置
        quickSortInternal(array,low,q-1);//左边区间(小于区分点元素)递归排序
        quickSortInternal(array,q+1,high);//右边区间(大于等于区分点元素)递归排序
    }

    /**
     * 对数组array[l...r]部分进行partition操作
     * 返回p,使得array[l+1...p-1]<array[p],array[p+1...r]>=array[p]</array[p],array[p+1...r]>
     * @param array 待排序数组
     * @param l 数组开始点
     * @param r 数组结束点
     * @return 分区点下标
     */
    private static int partition(int[] array,int l,int r){
        //默认比较元素为待排序数组的第一个元素
        int v=array[l];
        //[l+1...j]为<v的元素区间(刚开始为空区间,随着<v元素的加入区间加长)
        int j=l;
        //[j+1...i-1]为>=v的元素区间(刚开始为空区间,随着>=v元素的加入区间加长)
        int i=l+1;//i下标作用在于遍历除比较元素外的其他元素,即从第二个元素开始到结尾
        for(;i<=r;i++){//遍历数组
            if(array[i]<v){//当大于等于v时保持不动即可,<v时放置j下标区间
                swap(array,i,j+1);
                j++;
            }
        }
        //此时<v元素在前面,>=v元素在后面区间,将<v元素区间的最后一个元素与比较元素交换即可以达到最终效果(区分点元素到达最终位置,<v的元素在区分点左边区间,>=v的元素在区分点右边区间)
        swap(array,l,j);
        return j;//返回区分点下标
    }
    private static void swap(int[] array,int i,int j){//交换
        int temp=array[i];
        array[i]=array[j];
        array[j]=temp;
    }
    //随机生成数字
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
}

在这里插入图片描述

快排时间复杂度:O(nlogn)(左右分区极度均衡时),最坏时时间复杂度退化到O(n^2)(比如极端情况下,数组已经升序有序时,每次选择第一个元素为区分点导致左右两个区间极度不平衡,全部元素位于右边区间,此时就需要大约n次分区操作,每次分区我们大约要扫描n/2个元素)

**空间复杂度:O(1),为原地排序,**因快排优化了归并排序需要另外开辟空间的问题,解决占用内存多的问题,采用原地分区方法,实现原地排序算法。

稳定性:否。(二路快排会详细解释)

以上就是简单的快速排序过程,那么快排可以优化吗?

假设此时有个待排序数组为int[]=new int[]{1,2,3,4,5,6,7,8,9},若每次都默认选择数组第一个元素为比较元素时,就容易出现>=v与<v的区间元素个数的极度不均衡现像(因每次都默认选择第一个元素为区分点),那么此时运算大量数据时就会影响程序整体效率。

仔细思考一下,上述问题的出现是因为分区点的选择太过于固定,若每次能够随机选取区分点的话两区间元素极度不均衡的概率就会大大降低(若随机选取分区点,int[]=new int[]{1,2,3,4,5,6,7,8,9}此数组每次选到第一个元素的概率大约为1/81),从而提高程序效率。

待排数组几乎有序情况图解:

在这里插入图片描述

优化后快排源代码:

import java.util.Random;

public class Inc {
    public static void main(String[] args) {
        int[] data=generateRandomArray(10000,10,20000);
        quickSort(data);
    }
    public static void quickSort(int[] array){
        long start=System.currentTimeMillis();
        int n=array.length;
        if(n<=1){
            return;
        }
        quickSortInternal(array,0,n-1);
        long end=System.currentTimeMillis();
        System.out.println("优化快排耗时:"+(end-start)+"毫秒");
    }
    private static void quickSortInternal(int[] array,int low,int high){
        if(low>=high){
            return;
        }
        int q=partition2(array,low,high);
        quickSortInternal(array,low,q-1);
        quickSortInternal(array,q+1,high);
    }
    private static int partition2(int[] array,int l,int r){//每次快排随机生成分区点
        //Math.random()为产生一个[0,1)的随机数(double)
        int randomIndex=(int)(Math.random()*(r-l+1)+l);//随机数*数组长度+左下标然后强转确保区分点下标在数组合理下标范围内
        swap(array,randomIndex,l);//将第一个元素与随机区分点元素交换
        int v=array[l];//每次获得随机区分点元素
        int j=l;
        int i=l+1;
        for(;i<=r;i++){
            if(array[i]<v){
                swap(array,i,j+1);
                j++;
            }
        }
        swap(array,j,l);
        return j;
    }
    private static void swap(int[] array,int i,int j){
        int temp=array[i];
        array[i]=array[j];
        array[j]=temp;
    }
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
}

在这里插入图片描述

二路快排:适合待排序数组中有大量重复元素时使用

二路快排的出现主要是为了解决当待排序数组元素重复度过高时,单纯随机选取分区点已经没有意义,极大可能容易产生两区间极度不均衡的情况,影响程序性能。

二路快排思路:故我们可设立两个指针,分别让从前到后、从后到前遍历;从前到后遍历的元素遇到>=v的停下来,<v的i++即可,从后向前便利的元素遇到<=v的元素停下来,>v的j–即可;最终遍历出口为i>j,当i=j时还需要比较与区分点元素的大小。这样遍历一遍后可以保证左右两个区间元素个数的平衡(因与区分点元素相等的其他元素分布于左右区间,跟单路快排相等元素只存在于某一区间不同)。

二路快排思路图解:

在这里插入图片描述

二路快排源代码:

import java.util.Random;

public class Inc {
    public static void main(String[] args) {
        int[] data=generateRandomArray(10000,10,100);//待排序数组由一万个元素组成,在10-100之间随机选取,重复度很高
        quickSort(data);
    }
    public static void quickSort(int[] array){
        long start=System.currentTimeMillis();
        int n=array.length;
        if(n<=1){
            return;
        }
        quickSortInternal(array,0,n-1);
        long end=System.currentTimeMillis();
        System.out.println("二路快排耗时:"+(end-start)+"毫秒");
    }
    private static void quickSortInternal(int[] array,int low,int high){
        if(low>=high){
            return;
        }
        int q=partition3(array,low,high);
        quickSortInternal(array,low,q-1);
        quickSortInternal(array,q+1,high);
    }
    private static int partition3(int[] array,int l,int r){
        //每次快排随机选取区分点
        int randomIndex=(int)(Math.random()*(r-l+1)+l);
        swap(array,l,randomIndex);
        int v=array[l];
        int i=l+1;//[l+1...i-1]区间为<=v(刚开始区间长度为空,随着元素加入长度变长)
        int j=r;//[j...r]区间为>=v(刚开始区间长度为空,随着元素加入长度变长)
        while (true){//死循环
            //若左边区间下标合理且元素<v就一直i++即可,遇到>=v的元素时停止
            while (i<=r&&array[i]<v)i++;
            //若右边区间下标合理且元素>v就一直j--即可,遇到<=v的元素时停止
            while (j>=0&&array[j]>v)j--;
            if(i>j){//遍历终止条件
                break;
            }
            //此时i下标元素为大于等于v,j下标元素为小于等于v,此时i下标元素与j下标元素交换即可
            swap(array,i,j);
            //交换完成后接着遍历剩余元素,直到条件不满足时退出循环
            i++;
            j--;
        }
        //循环退出条件为i>j,即此时j下标为小于v区间的最后一个元素,只需要将区分点元素与其交换即可达到j下标元素左边为小于等于区分点元素的元素集合,右边区间为大于等于区分点元素的元素集合
        swap(array,l,j);
        return j;//返回区分点下标
    }
    private static void swap(int[] array,int i,int j){
        int temp=array[i];
        array[i]=array[j];
        array[j]=temp;
    }
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }

二路排序10000个元素耗时:

在这里插入图片描述

相同情况下直接快排耗时:

在这里插入图片描述

相同情况下优化快排(随机选取区分点)耗时:

在这里插入图片描述

三路排序:优化二路排序,增加了==v区间

思路:在二路排序的基础上,新增==v区间,在待排序数组存在大量元素重复度过高的情况下,可达到一次性确定所有与区分点元素相同的最终位置,大幅度减少遍历元素,提高程序性能。

思路图解:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

三路排序源代码:

import java.util.Random;

public class Inc {
    public static void main(String[] args) {
        int[] data=generateRandomArray(100000,1,10);//测试十万个元素,在1-100区间随机生成,重复度极高
        quickSort(data);
    }
    public static void quickSort(int[] array){
        long start=System.currentTimeMillis();
        int n=array.length;
        if(n<=1){
            return;
        }
        quickSortInternal3(array,0,n-1);
        long end=System.currentTimeMillis();
        System.out.println("三路排序耗时:"+(end-start)+"毫秒");
    }
    private static void quickSortInternal3(int[] array,int l,int r){//三路排序执行过程
        if(l>=r){//判断递归退出条件
            return;
        }
        //随机生成区分点
        int randomIndex=(int)(Math.random()*(r-l+1)+l);
        swap(array,l,randomIndex);
        int v=array[l];
        //[l+1...lt-1]为<v(刚开始为空)
        int lt=l;
        //[gt...r]为>v(刚开始为空)
        int gt=r+1;
        //[lt=1...i-1]为==v(刚开始为空)
        int i=l+1;
        while (i<gt){//遍历数组条件(因现存gt下标区间的都是遍历数组时换到里面的,故不用再次遍历)
            if(array[i]<v){//若小于v时,i下标元素与lt+1下标元素交换
                swap(array,i,lt+1);
                lt++;//<v区间长度加长
                i++;//遍历下一个元素,因遍历过的元素大于的已经交换到>v的区间,i下标元素与lt+1元素交换,lt+1元素必定为==v,故需i++
            }else if(array[i]>v){//若大于v时,i下标元素与gt-1下标元素交换,因刚开始要将此区间置空,故gt=r+1,此时空间不存在,gt指针从后向前遍历,故与gt-1下标元素交换
                swap(array,i,gt-1);
                gt--;//从后向前遍历,下标--表明区间长度加长,此时i下标不动时因为从后面区间换过来的元素还没有遍历过,故需要继续判断其大小
            }else{//若==v时,直接i++继续遍历下一个元素即可
                i++;
            }
        }
        //此时一次快排遍历结束,将区分点元素与<v区间的最后一个元素交换,达到左边区间为<v,右边区间为>v集合
        swap(array,l,lt);
        //递归<v区间
        quickSortInternal3(array,l,lt-1);
        //递归>v区间
        quickSortInternal3(array,gt,r);
        //此时与区分点元素数值相同的所有元素均到达最终位置
    }
    private static void swap(int[] array,int i,int j){
        int temp=array[i];
        array[i]=array[j];
        array[j]=temp;
    }
    public static int[] generateRandomArray(int n,int rangeL,int rangeR){
        if(rangeL>rangeR){
            throw new IndexOutOfBoundsException("越界异常");
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=new Integer(new Random().nextInt(rangeR-rangeL+1)+rangeL);
        }
        return arr;
    }
 }

测试程序耗时:

在这里插入图片描述

同种情况下二路排序耗时:

在这里插入图片描述

快排总结:

原始快排方法时间复杂度取决于区分点导致两边区间是否均衡,若趋于均衡时间复杂度为O(nlogn),若极端情况出现两边区间一边元素过多另一边过少或者没有,时间复杂度就会退化到O(n^2);

二路排序利用原地分区巧妙的解决了原始快排的两区间不均衡的问题,很大程度上优化了原始快排;

三路排序是在二路排序的基础上再次优化,新增==v区间,一次可确定多个元素到达最终位置,更大程度上优化了原始快排。

但三者相比,三路快排在待排序数组元素重复度过高时效率最高,但在待排序数组近乎无序时相对于二路排序较慢(因三个指针),但在可接受范围内,故建议使用三路快排。

快排与归并排序对比:

两者都采用分治思想,但归并排序是从下至上解决问题,即将待排序数组分解到单个元素时再合并数组,且开辟新空间非原地排序,在运算大量数据时占用空间;快排则是完美解决了归并排序的问题,采用原地分区思想,节省空间,故相同情况下采用快排。

猜你喜欢

转载自blog.csdn.net/weixin_42617262/article/details/88668481