排序之快速排序详解

一、算法介绍

快速排序(Quick Sort):它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,分别对这丙部分继续进行快速排序,直至整个序列有序。

任取一个元素 (如第一个) 为中心
所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;
对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个

①每一趟的子表的形成是采用从两头向中间交替式逼近法;

②由于每趟中对各子表的操作都相似,可采用递归算法。

二、基本步骤

设置两个指针i,j,首先在序列里面选出一个枢纽temp出来,将j指向的数字和temp比较,如果比temp大,则减1,如果比temp小,应该把当前j指向的位置上面的数值和

三、算法分析

最好:划分后,左侧右侧子序列的长度相同,

最坏:从小到大排好序,递归树成为单支树,每次划分只得到一个比上一次少一个对象的子序列,必须经过 n-1 趟才能把所有对象定位,而且第 i 趟需要经过 n-i 次关键码比较才能找到第 i 个对象的安放位置

若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况

时间效率:O(nlog2n) —每趟确定的元素呈指数增加
空间效率:O(log2n)—递归要用到栈空间
稳 定 性: 不稳定 —可选任一元素为支点

1.如何选枢纽

由上述描述可以知道,快速排序是以枢纽的点进行来回交换,所以快速排序的排序趟数和初始的序列有关系。
所以选择快速排序的枢纽点是非常重要的,因为关系到排序的效率。

取前或后法:序列中的第一个或最后一个元素作为基准,如果输入序列(上文中的数组)是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,时间复杂度为Θ(n^2)。而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为枢纽元是非常糟糕的

随机选取基准
这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n2)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。

三数取中法:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。

2.如何证明时间复杂度

1、最优情况

在最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,其递归树的深度就为 [log2n]+1( [x] 表示不大于 x 的最大整数),即仅需递归 log2n 次,需要时间为T(n)的话,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去,就有了下面的不等式推断:
这说明,在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。

2.最坏情况

然后再来看最糟糕情况下的快排,当待排序的序列为正序或逆序排列时,且每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为n(n-1)/2,最终其时间复杂度为O(n^2)。

3.平均时间复杂度

直接设对规模的数组排序需要的时间期望为, 期望其实就是平均复杂度换个说法.
空表的时候不用排, 所以初值条件就是 T(0) = 0 .所谓快排就是随便取出一个数,一般是第一个数,然后小于等于他的放左边, 大于他的的排右边.比如左边 k 个那接下来还要排: T(n - k) + T (k - 1) 的时间.然后 k 多少那是不确定的, 遍历 1~ n , 出现概率都是相等的. 另外分割操作本身也要时间 P(n) , 操作花费是线性时间 P(n) = cn , 这也要加进去, 所以一共是:

四、完整代码示例

public class QuickSort {

    //任取一个元素 (如第一个) 为中心
    //所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;
    //对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个
    //一趟排序过程后我们返回枢纽的位置
    int partition(int A[], int left, int right) {
        //选择枢纽元素
        int p = A[left];
        while (left < right) {
            //如果尾指针位置的数比枢纽数要大,移动尾指针的位置,否则就把所指示的值给首指针的位置
            while (left < right && A[right] >= p) {
                --right;
            }
            A[left] = A[right];
            //如果首指针位置的数比枢纽数要小,移动首指针的位置,否则就把所指示的值给尾指针的位置
            while (left < right && A[left] <= p) {
                ++left;
            }
            A[right] = A[left];
        }
        //此时的首尾指针已经相等,把枢纽的值赋给首尾指针相等的位置即可
        A[left] = p;
        return left;
    }

    //快速排序的递归
    void Quick(int A[], int left, int right) {
        //定义一个枢纽的位置
        int pnode;
        if (left < right) {
            pnode = partition(A, left, right);
            Quick(A, left, pnode - 1);
            Quick(A, pnode + 1, right);
        }
    }

    public static void main(String[] args) {

    }

参考文章

https://www.jianshu.com/p/c8b1384238f7

https://www.cnblogs.com/chengxiao/p/6262208.html

https://blog.csdn.net/oohaha_123/article/details/26558363

https://www.zhihu.com/question/22393997/answer/406278523

https://www.cnblogs.com/onepixel/articles/7674659.html


欢迎关注个人技术公众号:Coder辰砂

posted @ 2018-12-10 22:07 辰砂tj 阅读(...) 评论(...) 编辑 收藏