七大排序之快速排序

发布于:2023-01-04 ⋅ 阅读:(268) ⋅ 点赞:(0)


快速排序

快速排序的思想

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

快速排序时间复杂度为O(nlogn),是一个不稳定的排序算法

图解如下:

在这里插入图片描述

基准值的选择

  1. 选择边上(左或者右):在接近有序的数组上,快排会退化为O(n^2),左右两个子区间严重不平衡,二叉树退化为链表。
  2. 随机选择:随机在当前数组中选取一个树作为基准值。
  3. 几树取中,一般都是三数取中。

第二种和第三种都能避免在递归过程中,在接近有序的数组上,快速排序的分区严重不平衡的问题。

代码

public static void quickSort(int[] arr){
        quickSortInternal(arr,0,arr.length-1);
    }
    private static void quickSortInternal(int[] arr, int l, int r) {
        //优化1:小区间上使用插入排序,减少递归的次数
        if (r-l<=15){
            insertionSort(arr, l, r);
            return;
        }
        //先获取分区点,分区点左侧全是小于该元素的区间,分区点右侧全是大于该元素的区间
        int p=partition(arr,l,r);
        quickSortInternal(arr, l, p-1);
        quickSortInternal(arr, p+1, r);
    }
    private static int partition(int[] arr, int l, int r) {
        //优化2:随机在当前数组中选一个数
        //能避免递归过程中,在接近有序的数组上,快速排序的分区严重不平衡的问题
        int randomIndex=random.nextInt(l,r);
        swap(arr,l,randomIndex);
        int v=arr[l];

        //1.先选一个基准点
        //int v=arr[l];

        //分区点:
        //arr[l+1...j] < v
        //arr[j+1...i) >= v
        //i表示当前正在扫描的元素
        int j=l;
        for (int i = l+1; i <=r ; i++) {
            //如果arr[i]小于v,就将大于v的第一个元素和正在扫描的元素进行交换
            if (arr[i]<v){
                swap(arr,i,j+1);
                j++;
            }
        }
        //将基准值和最后一个小于v的元素进行交换,基准值就到了最终位置
        swap(arr,j,l);
        return j;
    }

二路快排

为什么引入二路快排?

当待排序的集合中出现大量重复元素时,就会导致某个分区的元素过多(相等的元素全在一个区间中),造成递归过程中,递归树严重不平衡,快排退化为O(n^2)。二路快排就是将相等的元素均分到左右两个子区间,解决了递归树严重不平衡的问题。

图解如下:

在这里插入图片描述
代码如下:

private static final ThreadLocalRandom random=ThreadLocalRandom.current();
    public static void quickSort(int[] arr){
        quickSortInternal(arr,0,arr.length-1);
    }
    private static void quickSortInternal(int[] arr, int l, int r) {
        //优化1:小区间上使用插入排序,减少递归次数
        if (r-l<=15){
            insertionSort(arr,l,r);
            return;
        }
        //获取分区点
        int p=partition(arr,l,r);
        quickSortInternal(arr,l,p-1);
        quickSortInternal(arr,p+1,r);
    }
    private static int partition(int[] arr,int l,int r){
        //优化2:随机选取基准值
        int randomIndex=random.nextInt(l,r);
        swap(arr,l,randomIndex);
        int v=arr[l];
        int i=l+1;  //[l+1..i)<=v
        int j=r;    //(j..r]>=v
        while (i<=j){
            //i从前向后扫描,碰到第一个大于等于v的元素停止
            while (i<=j && arr[i]<v){
                i++;
            }
            //j从后向前扫描,碰到第一个小于等于v的元素停止
            while (i<=j && arr[j]>v){
                j--;
            }
            if (i>=j){
                break;
            }
            //如果此时i<j,交换i和j的元素
            swap(arr,i,j);
            i++;
            j--;
        }
        //此时j指向小于等于v的区间的最后一个元素
        swap(arr,l,j);
        return j;
    }
    private static void insertionSort(int[] arr,int l,int r){
        for (int i = l+1; i <=r ; i++) {
            for (int j = i; j >0 ; j--) {
                if (arr[j]>=arr[j-1]){
                    break;
                }else{
                    swap(arr,j,j-1);
                }
            }
        }
    }
    private static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

三路快排

三路快排思想:在一次分区函数的操作中,将所有相等的元素都放在最终位置,只需要在小于v和大于v的子区间上进行快排,所有相等的元素就不再处理了。

图解如下:

三路快排
代码如下:

private static final ThreadLocalRandom random=ThreadLocalRandom.current();
    public static void quickSort(int[] arr){
        quickSortInternal(arr,0,arr.length-1);
    }
    private static void quickSortInternal(int[] arr, int l, int r) {
        //小区间使用插入排序
        if (r-l<=15){
            insertionSort(arr,l,r);
            return;
        }
        //随机选取基准值
        int randomIndex=random.nextInt(l,r);
        swap(arr,l,randomIndex);
        int v=arr[l];
        int lt=l;  //arr[l+1..lt]<v   lt代表小于v的最后一个元素
        int i=lt+1;  //arr[lt+1..i)==v  i代表正在扫描的元素
        int gt=r+1;  //arr[gt..r]>v     gt指向第一个大于v的元素
        //i表示当前扫描元素
        while (i<gt){
            if (arr[i]<v){
                swap(arr,lt+1,i);
                lt++;
                i++;
            }else if (arr[i]==v){
                i++;
            }else{
                //当前扫描元素大于v时,将gt-1与i交换,i不加加,因为换过来的gt-1还没处理
                swap(arr,i,gt-1);
                gt--;
            }
        }
        swap(arr,l,lt);
        quickSortInternal(arr, l, lt-1);
        quickSortInternal(arr, gt, r);
    }
    public static void insertionSort(int[] arr,int l,int r){
        for (int i = l+1; i <=r ; i++) {
            for (int j = i; j >0 ; j--) {
                if (arr[j]>=arr[j-1]){
                    break;
                }else{
                    swap(arr,j,j-1);
                }
            }
        }
    }
    public static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

总结

  • 当待排序的集合重复元素并不多时,随机化快排已经可以解决问题,甚至性能比二路快排和三路快排还要好;
  • 当待排序集合中有大量重复元素时,使用二路快排或三路快排优化重复元素的处理;
  • 当待排序集合是一个接近有序的集合,分区点的选择就不能单纯选择最左侧或最右侧,可以使用随机化选择或三数取中。