Java之快速排序详解&三数取中优化

原文章来自我的语雀知识库,点击跳转

快速排序的具体实现

    // 快速排序,使得array[st,ed]是有序的
    public static void quickSort(int[] array,int st,int ed){
        if (st >= ed){
            return;
        }
        // 以array[st]为基准,使用双指针不停交换array[st->ed]之间的元素
        // 直到找到位置sortedPos,将基准放在这个位置上
        // 其左边的元素都小于基准,其右边的元素都大于基准
        int sortedPos = findSortedPosition(array,st,ed);
        // 分治,处理sortedPos前后两个子数组
        // 由于sortedPos后面的子数组的所有元素大于前一个子数组中的所有元素
        // 所以分别使两个子数组变得有序之后,就达到了全局有序
        quickSort(array, st, sortedPos-1);
        quickSort(array, sortedPos+1, ed);
    }

    // 寻找一个基准array[pivot],使用双指针不停交换array[st->ed]之间的元素
    // 双指针相遇时,将基准与array[left]交换
    // 使得基准之前的所有元素小于基准,其后的所有元素大于基准
    public static int findSortedPosition(int[] array,int st,int ed){
        // 维护[st,left]之间的元素都小于或等于array[pivot]
        // 维护[right,ed]之间的元素都大于或等于array[pivot]
        int left = st;
        int right = ed;
        int pivot = st;
        while (left != right){
            while (left != right && array[right] >= array[pivot]){
                right--;
            }
            while (left != right && array[left] <= array[pivot]){
                left++;
            }
            if (left != right){
                swap(array, left, right);
            }
        }
        swap(array, pivot, right);
        return left;
    }

如何理解findSortedPosition这个函数?我用一个表格来解释这个过程。
标为红色的是基准,标为蓝色的是左指针维护的区域,标为绿色的是右指针维护的区域。
右指针的维护的区域为:区域内的元素都大于基准
左指针维护的区域为:区域内的元素都小于基准
我们不难得出,左指针和右指针移动的规律:由于要维护上述两个区域的性质,左指针遇到非法元素(比基准大的元素)会停下来,右指针遇到非法元素(比基准小的元素)会停下来。而且左、右指针相遇时也会停下来。
我们让右指针先移动,最终右指针遇到左指针时,左右指针都会停止在同一格上。
这时候,他们所在的这一格的数值,究竟比基准大还是比基准小?由于最后一轮中,右指针是先移动的,无论它是遇到了非法元素,还是遇到左指针停了下来,最后一轮右指针所指的一定是非法元素——比基准小的元素!
这时候,我们将基准元素与array[right]交换,就得到了最终结果——基准元素左边的元素都比它小,基准元素右边的元素都比它大。

49715372初始时,left=st,right=ed
49715372先移动右指针,第一个元素就是非法元素(比基准元素小的元素)
49715372再移动左指针,第一个元素就是非法元素(比基准元素大的元素)
42715379交换左右指针所指的元素,保证左、右区域的合法性
42715379先移动右指针,右指针遇到非法元素3停下来了
42715379再移动左指针,左指针遇到非法元素7停下来了
42315779交换左右指针所指的元素,保证左、右区域的合法性
42315779先移动右指针,右指针遇到非法元素1停下来了
42315779左右指针相遇,而且右指针指向比基准元素4小的元素
12345779将基准元素与array[right]交换,最终基准元素左侧的元素都小于基准元素,右侧的元素都大于基准元素。

如果让左指针先移动会怎么样?基于上述分析,我们知道左指针一定会向右移动并最终指向非法元素——比基准元素大的元素,这时候将基准与其交换,坏了,array中的第一个元素比基准元素大了。

平均时间复杂度O(nlogn)
最坏时间复杂度O(n^2)
稳定度不稳定:举个例子,[5,9,9,2],第一次划分的时候,首先9与2交换,
就破坏了两个9的相对位置
空间复杂度O(nlogn):快排并没有开辟空间,但是使用了递归,递归会开辟栈帧,
递归算法的空间复杂度 = 每次递归的空间复杂度*递归深度
适用场景n大的时候好
排序十万个随机数耗时15ms
排序一千个随机数耗时1ms
排序一百万个随机数耗时94ms

快速排序的时间复杂度分析——为什么平均时间复杂度是O(nlogn)?什么情况下退化成O(n^2)?

  1. 用注释分析一下平均时间复杂度为什么是O(nlogn)
    public static void quickSort(int[] array,int st,int ed){
        if (st >= ed){
            return;
        }
        // 划分的时间复杂度是O(n),因为需要比较每个元素,可能要交换一些元素
        int sortedPos = findSortedPosition(array,st,ed);
        // 采用分治的思路,每调用一次quickSort进行分治,实际上数据规模是成倍减小的。
        // 拿最好的情况来说,sortedPos正好是array正中间的位置
        // 那么这部分最好的时间复杂度是O(log2n)
        // 总而言之,治理嵌套了划分,两部分的时间复杂度相乘得到O(nlogn)
        quickSort(array, st, sortedPos-1);
        quickSort(array, sortedPos+1, ed);
    }
  1. 那什么时候退化成O(n^2)呢?答案是array已经排好序、或者逆序排序的情况下。递归二叉树画出来应该是一棵斜树。

因为在排好序的情况下,由于基准元素是第一个元素,那么经过一轮时间复杂度为O(n)的比较之后,发现基准元素依然只能在原地不动。而且这种划分还会进行n次,为什么要进行n次呢?原本明明是logn啊,这是因为每次划分,左边部分元素数量为0,右边部分的元素数量为n-1,每调用一次quickSort数据规模只减了1,所以"治理"的时间复杂度也退化成O(n)了,两部分相乘,最坏情况下的时间复杂度就是O(n^2)。
逆序的情况跟排好序的情况类似。

如何避免最坏情况的发生?换句话说,怎么优化快速排序?

目前一种比较好的优化方法是三数取中。具体来说,在进行划分的时候,我们取array[st]、array[ed]、array[mid]这三个值的中间元素作为基准,其中mid = st + (ed-st)/2;这样做有什么好处呢?考虑一下最好情况,我们希望基准正好是array[st->ed]这部分的中间值,取这三个数再取中间值实际上是一种贪心的思路,希望取到的值既不是最大也不是最小,可以避免最坏情况。
增加了三数取中优化的代码如下:

    public static void quickSort(int[] array,int st,int ed){
        if (st >= ed){
            return;
        }
        int sortedPos = findSortedPosition(array,st,ed);
        quickSort(array, st, sortedPos-1);
        quickSort(array, sortedPos+1, ed);
    }

    public static int findSortedPosition(int[] array,int st,int ed){
        // 三数取中
        makeLowMid(array, st, ed);
        int left = st;
        int right = ed;
        int pivot = st;
        while (left != right){
            while (left != right && array[right] >= array[pivot]){
                right--;
            }
            while (left != right && array[left] <= array[pivot]){
                left++;
            }
            if (left != right){
                swap(array, left, right);
            }
        }
        swap(array, pivot, right);
        return left;
    }

    // 保证array[low]是array[low]、array[mid]、array[high]里的中间值
    public static void makeLowMid(int[] array,int low, int high){
        int mid = low + ((high-low)>>1);
        // 保证array[high]大于array[mid]
        if (array[mid] > array[high]){
            swap(array, mid, high);
        }
        // 保证array[high]是三个数中最大的
        if (array[low] > array[high]){
            swap(array, low, high);
        }
        // 保证array[mid] < array[low]
        if(array[mid] > array[low]){
            swap(array, low, mid);
        }
        // 这样一来,array[high]是最大值,array[low]是中间值,array[mid]是最小值
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值