快速排序及优化

快排的基本思想:

1.从待排序区间选择一个数,作为基准值(pivot);
2.切分(Partition): 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
3.采用分治思想,对基准值左右两个小区间按照同样的方式处理,直到小区间的长度为小于等于1,代表数组已经有序;

1.普通快排

基准值:最左侧或最右侧值

public class QuickSort1 {
    public static void main(String[] args) {
        //测试
        Scanner in=new Scanner(System.in);
        String a=in.nextLine();
        String b=a.substring(1,a.length()-1);
        String[] str=b.split(",");
        int[] n=new int[str.length];
        for(int i=0;i<n.length;i++) {
            n[i] = Integer.parseInt(str[i]);
        }
        quickSort(n,0, n.length-1);
        for(int j=0;j<n.length;j++) {
            System.out.println(n[j]);
        }
    }

 
    public static void quickSort(int[] s,int left,int right){
        //3.快速排序
        //每次选定一个阈值,小于阈值的放左边,大于阈值的放右边。
        //阈值怎么选?怎么快速放?
        if(left>=right) return ;//终止条件
        int key=partition(s,left,right);//得到切分轴
        quickSort(s,left,key-1);//左侧区间递归
        quickSort(s,key+1,right);//右侧区间递归
    }

    public static int partition(int[] s,int left,int right){
        int pivot = s[left];//基准值
        if(left<right) {
            while (left < right) {
                while (s[right] >= pivot && left < right) {
                    right--;
                }
                if (left < right) {
                    s[left] = s[right];
                    left++;
                }
                while (s[left] < pivot && left < right) {
                    left++;
                }
                if (left < right) {
                    s[right] = s[left];
                    right--;
                }
            }
        }
        s[left] = pivot;//得到数组划分轴
        return left;
    }
}

 对于普通快排来说:

  在数组有序、逆序等基本有序情况下,时间复杂度是O(n^2):每次都选择了最左侧或者最右侧的值作为切分轴,相当于将除基准值之外的区间做n次递归(可以想象成只有右子树的树),递归树深度为n,每一层操作都需要遍历一次剩下的所有元素,这个操作时间复杂度为O(n),总时间复杂度为O(n^2)。

对于近乎有序数组,每次选择边上元素作为基准值,会造成切分严重不平衡:
例如举一个极端的例子:[1,2,3,4,5,6,7,9,8]数据量可以类比放大,此时如果我们选择左边第一个元素为基准值,每次处理只会移除一个元素,会导致整个排序过程会切分很多次,此时效率会非常差

  在数组乱序,理想的情况下,我们选取的分界点刚好就是这个区间的中位数。也就是说,在操作之后,正好将区间分成了满足数字个数相等的左右两个子区间(快排是按照值的大小划分,个数可能相等,可能不等)。此时基准值左右两个区间都可以递归,递归树的深度应该是1+2+4+...+2^k=n-->k=logn层。对于每一个区间,处理的时候,都需要遍历一次区间中的每一个元素。这也就意味着,快速排序和归并排序一样,每一层的总时间复杂度都是O(n),因为需要对每一个元素遍历一次。所以快速排序最好的时间复杂度为O(nlogn)。

在处理有序数据之前,我们可以先将当前数组的顺序进行打乱,然后再排序,这样可以提升一定的效率,但是处理极端的数据时效果肯定不是最好的,且代码实现变得更加复杂。
四种优化方式: 在基本快速排序的基础上进行优化:三数取中确定key值、数组较小时选用插入排序、聚集相等元素、尾递归优化等。 快速排序的优化主要是在基准的选择上和尽量减少partition的运行:   基准值选择:
    
1.随机选择法:每次在待排序区间里随机选择一个索引作为基准值。
    2.三分取中法:array[left], array[mid], array[right] 大小是中间的为基准值。   减少partition的方法:
    1.聚合相同元素:聚合相等的元素,这样下次就不用再切分这部分区间。
    2.数组较小时选择插入排序:直接利用插入排序对小区间处理,而不是将其切分到长度0或1,这样可以减少切分次数,提升效率。

2.随机快排(随机选择+插入)

基准值:随机选择

主要解决:基本有序的问题

 

import java.util.Scanner;

public class QuickSortRand {
    public static void quickSortRand(int[] s, int left, int right){
        //1.基准值选择:随机选择
        //2.小区间用插入排序
        if(left>=right){
            return;
        }
        int key=partition(s,left,right);//得到划分轴
        //小区间用插入排序
        //大区间继续递归
        if(key-left<=15){
            insertSort(s,left,key-1);
        }else{
            quickSortRand(s,left,key-1);
        }
        if(right-key<=15){
            insertSort(s,key+1,right);
        }else{
            quickSortRand(s,key+1,right);
        }
    }

    public static int partition(int[] s,int left,int right){
        //随机选择: 产生start - end 的随机数
        int randNum=(int)(Math.random()*(right-left)+left);
        //int pivot=s[randNum];//error
        //这里需要把randNum和left的位置换一下,才能直接用以下代码
        //因为pivot是空出来的一个坑,之前left空出坑,right才先走,现在randNum空出坑,最简单的方法就是randNum和left交换位置。
        swap(s,left,randNum);
        int pivot=s[left];
        while(left<right){
            while(s[right]>=pivot&&left<right){
                right--;
            }
            if(s[right]<pivot){
                s[left]=s[right];
                left++;
            }
            while(s[left]<pivot&&left<right){
                left++;
            }
            if(s[left]>pivot){
                s[right]=s[left];
                right--;
            }
        }
        s[left]=pivot;
        return left;
    }

    public static void swap(int[] s,int left,int num){
        int tmp=s[left];
        s[left]=s[num];
        s[num]=tmp;
    }

    private static void insertSort(int[] s, int left, int right) {
        if(left>=right){
            return;
        }
        //直接插入排序原理:当插入第i个元素时,前面的i-1个元素已经时有序的,
        //此时用第i个元素的值和前面i-1个元素进行比较,直到找到插入位置,插入即可,原来位置的元素顺序后移。
        for (int i=left+1;i<=right;i++) {//s[left]有序
            for (int j=i;j>left&&s[j]<s[j-1];j--) {//j>left:比到最左侧
                swap(s,j,j-1);
            }
        }
    }
    public static void main(String[] args) {
        //测试
        Scanner in =new Scanner(System.in);
        String a=in.next().toString();
        String b=a.substring(1,a.length()-1);
        String[] c=b.split(",");
        int[] t=new int[c.length];
        for(int i=0;i<c.length;i++){
            t[i]=Integer.parseInt(c[i]);
        }
        quickSortRand(t,0,t.length-1);
        for(int i=0;i<t.length;i++){
            System.out.println(t[i]);
        }
    }
}

 

3.三路快排(三数取中+聚合相同元素+小区间插入排序) 目前终极版

基准值:三数取中

主要解决:大量重复数据问题

三路快排将数组分割为三个部分:小于基准值、等于基准值、等于基准值。
在下一次递归时,只需要处理小于基准值和大于基准值两个区间。
相当于聚合了等于基准值的相同元素,会减少partition

tips:

等差数列和公式
    Sn=n(a1+an)/2=na1+n(n-1)/2 d
等比数列求和公式
    q≠1时 Sn=a1(1-q^n)/(1-q)=(a1-anq)/(1-q)
    q=1时Sn=na1
    (a1为首项,an为第n项,d为公差,q 为等比)

 

posted @ 2023-03-19 16:58  壹索007  阅读(51)  评论(0编辑  收藏  举报