快速排序详解

本文探讨了快速排序算法的性能特点,尤其是最坏情况下的O(n²)和平均情况下的O(nlogn),强调了其在处理随机数列时的优势。文中详细介绍了Lomuto分区和Hoare分区的实现方式,以及如何选择合适的基准点以优化排序性能。
摘要由CSDN通过智能技术生成

性能

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

特点

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了

实现

单边循环(lomuto分区)要点

  • ·选择最右侧元素作为基准点
  • ·j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
    • 。交换时机: j找到小的,且与i不相等
    • 。当 i 找到>=基准点元素后,不应自增,
  • ·最后基准点与i交换,i 即为基准点最终索引
public class QuikSortLomuto {
    public static void sort(int[] a){
        quick(a,0,a.length - 1);
    }

    private static void quick(int[] a, int left, int right) {
        if (left >= right){
            return;
        }
        // p 代表基准点元素最终索引
        int p = partition(a,left,right);
        quick(a,left,p - 1);
        quick(a,p + 1,right);
    }

    private static int partition(int[] a, int left, int right) {
        // prv 是基准点元素
        int prv = a[right];
        int i= left,j = left;
        //快慢指针
        // i 找比基准点大的,j找比基准点小的
        while (j < right){
            //  交换时机,j找到小的,且与i不相等(即i、j指向同一元素),
            if (a[j] < prv){//j没找到比prv大的元素
                if (i != j){
                    swap(a,i,j);
                }
                i++;//因为 j 找到了比基准点小的了,确定 i 的位置
            }
            j++; // j必须不断往前走, i 可能停下
        }
        swap(a,i,right);
        return i;

    }

    private static void swap(int[] a,int i,int j){
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void main(String[] args) {
//        int[] a ={9,3,7,2,8,5,1,4};
        int[] a = {4, 2, 1, 3, 2, 4};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }
选择最左侧侧元素作为基准点 
private static int partition(int[] arr, int left, int right) {
        int prev  = arr[left];
        int index = left+1;//每次的假设大分区的左边界
        //保证最后分区的正确性
        for (int i = index; i <= right; i++) {
            if (arr[i] < prev){
                swap(arr, i, index);
                index++;
            }
        }
        swap(arr,left,index-1);
        return index-1;
    }

双边循环( QuikSortHoare)

  • ·选择最左侧元素作为基准点
  • · j找比基准点小的,i找比基准点大的,一旦找到,二者进行交换
    • 。 i从左向右
    • 。j从右向左
  • ·最后基准点与i交换,i即为基准点最终索引
代码
public static void sort(int[] a) {
        quick(a, 0, a.length - 1);
    }

    private static void quick(int[] a, int left, int right) {
        if (left >= right) {
            return;
        }
        // p 代表基准点元素最终索引
        int p = partition(a, left, right);
        quick(a, left, p - 1);
        quick(a, p + 1, right);
    }

    private static int partition(int[] a, int left, int right) {
//        ThreadLocalRandom.current().nextInt(10);
        int index = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a,index,left);
        // [0, 10)
        //希望[4,6],right - left + 1计算排序区间内的元素个数
        // right - left + 1 =3 ->[0,2]+4=[4,6]
        // prv 是基准点元素,与
        int pv = a[left];
        int il = left;
        int ir = right;
        while (il < ir) {
            //注意:最终 il == ir 的,且il 和 ir 保持不动
            // ,ir找比基准点小的,且单边寻找不能过界
            while (il < ir && a[ir] > pv) {
                ir--;
            }
            // il 找比基准点大的
            while (il < ir && a[il] <= pv) {
                il++;
            }
            //3 可以交换了
            swap(a, il, ir);

        }
        swap(a, left, il);

        return il;

    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void main(String[] args) {
        int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }
双边快排注意事项
  1. 为啥内层循环加 i < j 条件

内层i(j)相当于单独遍历,一定要保证不能越界

  1. 为啥要先处理 j,再处理 i

    逻辑错误,相遇时换基准点会发生错误错误,所谓错误,体现在循环结束上,在i l == rj 时,(谁先开始找就停在谁那)先找大的那么是先 il停下来,此时 a[il] 大于基准点,最后在整个分区循环结束完的交换中,必然将大的元素交到前面去。

    也不是不可以,需要将基准点选择、最后 il == rj 时交换改变。即谁先找,基准点从对面选择和它交换;但基准点一般于后者进行交换

  2. 随机元素作为基准点元素

    对于顺序反转,效果不好,极度不平衡,且性能大大降低O(nlong(n)) ----> O(n^2)

    理想的基准点是:恰好是中间元素

    最终折中随机

最优版实现

public static void sort(int[] a) {
        quick(a, 0, a.length - 1);
    }

    private static void quick(int[] a, int left, int right) {
        if (left >= right) {
            return;
        }
        // p 代表基准点元素最终索引
        int p = partition(a, left, right);
        quick(a, left, p - 1);
        quick(a, p + 1, right);
    }

    


    /*
            循环内:
                i 从 left + 1 开始,从左向右找大的或相等的
                j 从 right 开始,从右向左找小的或相等的
                    交换, i++, j--

            循环外 j 和 基准点交换, j  即为分区位置 -——>分区均衡
     */
    private static int partition(int[] a, int left, int right) {
        int pv = a[left];
        int i = left + 1;
        int j = right;
        //为什么要去=号,因为while循环是找j作为小区间的有边界进行所以初始化后须进入判断
        while (i <= j) {
            while (i <= j && a[i] < pv) {
                i++;
            }
            while (i <= j && a[j] > pv) {
                j--;
            }
            //符合条件时还得是否越界
            if ( i <= j){
                swap(a, i, j);
                i++;
                j--;
            }

        }
        swap(a, j, left);
        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void main(String[] args) {
//        int[] a = {4, 2, 1, 3, 2, 4};// 最外层循环 = 要加
//        int[] a = {2,1,3,1};// 内层循环 = 要加
       int[] a ={2,1,3,2};// 内层if 要加
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值