算法专项(1)——快速排序

简介

快速排序(Quicksort)可简称”快排”,属于交换排序中的典型代表,是对同属于交换排序的冒泡排序的改进,体现了分治的思想。

原理详解

首先应该明确,既然属于交换排序,那排序过程的实质是:对给定的待排序列,不断交换调整其中的元素的位置,让每个元素处在正确的位置上,最终达到有序。在明确这点后,再看其基本思想:

基本思想:首先从待排序列中选定一个关键字作为一个枢轴,通过其他关键字与枢轴的比较,将除枢轴外的其余关键字划分成位于枢轴前后的两个子序列,位于枢轴前的子序列中的所有关键字均不大于枢轴,位于枢轴后的子序列中的关键字均不小于枢轴。此时,枢轴作为这两个子序列的分割点,它已经处在正确的位置上。再按同样的方法对这两个子序列分别进行快速排序,依次递归。最终使得原来的待排序列整体有序。

由基本思想可见,快速排序的过程中要做两件事:
(1).首先,将给定的无序序列划分成两部分;
(2).然后,对划分出的两个子序列分别进行快速排序。
对子序列进行快速排序的操作很简单,调用函数即可。所以,快速排序的核心在于划分子序列
至此,给出一个示例代码,以方便理解前述和后续的内容,将对照代码继续解析。

代码片段1
该段代码定义了QuickSort()方法,其中调用的partition()定义在下面的代码片段2中,所干的事情就是前边提到的(2)。附有注释,简单易懂,就不多说。

    /**
     * 将无序序列sequence位于s和t之间的元素排序
     * 
     * @param sequence
     *            待排序列
     * @param s
     *            序列的起点
     * @param t
     *            序列的终点
     */
    public void QuickSort(int[] sequence, int s, int t) {
        int pivotloc = s; // 枢轴的位置
        // 当t>s时,才有必要进行排序
        if (t > s) {
            // 将待排序列划分成两个子序列:[s, pivotloc - 1]和[pivotloc + 1, t]
            pivotloc = partition(sequence, s, t); 
            // 分别对两个子序列进行快速排序
            QuickSort(sequence, s, pivotloc - 1); 
            QuickSort(sequence, pivotloc + 1, t);
        }
    }

代码片段2
该段代码定义了partition()方法,所干的事情就是前边提到的(1),即划分成子序列,代码本身简洁精炼,为便于理解加多了几行注释使得长度增加,不必有过多的心理压力。

    /**
     * 将给定的序列sequence中位于low和high之间的元素划分成两个子序列,并返回枢轴所在的位置
     * 
     * @param sequence
     *            待划分的序列
     * @param low
     *            参与划分的元素的下界
     * @param high
     *            参与划分的元素的上界
     * @return int 本次划分后枢轴所在的位置
     */
    private int partition(int[] sequence, int low, int high) {
        int piovt = sequence[low]; // 定义枢轴变量并赋初值,同时也是备份了sequence[low]的值
        while (low < high) {
            // 从high位置开始向左逐个扫描,直到遇到第一个比枢轴小的元素就停止
            // 然后将这个元素放到low对应的位置上
            while (low < high && sequence[high] >= piovt) {
                high--;
            }
            sequence[low] = sequence[high];

            // 接着
            // 从low位置开始向右逐个扫描,直到遇到第一个比枢轴大的元素就停止
            // 然后将这个元素放到high对应的位置上
            while (low < high && sequence[low] <= piovt) {
                low++;
            }
            sequence[high] = sequence[low];
        }

        // 直到low==high时,上面的while循环才会结束
        // 此时,low==high,即为枢轴的位置
        sequence[low] = piovt; // 将枢轴的值存放到它的位置上
        return low; // 返回枢轴的位置
    }

划分子序列
现结合上面的代码片段2,说说划分子序列的具体过程:
Step1.从high位置开始向左(即位标减小的方向)逐个扫描,直到遇到一个比枢轴小的关键字停止,然后将这个值放到low位置上。
Step2.然后从low位置上开始向右(即位标增大的方向)逐个扫描,直到遇到一个比枢轴大的关键字停止,然后将这个值放到high位置上。
这两步的操作正体现出交换排序 的含义:先从右边开始找一个比较小的,然后从左边开始找个大的,然后将小的放左边,大的放右边,这就完成了“交换”,不过应当注意,这个交换不是真正的一对一交换

然后循环执行如上两步,直到一直向左走的high和向右走的low相遇就停止。相遇则说明在相遇位置的右边没有比枢轴更大的,在左边没有比枢轴更小的,那当前的位置就是这个枢轴应该待的地方,即正确的位置,所以,枢轴就位了。

于是,以枢轴为分割点,原来的序列被分为两个子序列。只要知道分割点的位置,就能知道划分出的子序列的起止位标,故只需返回枢轴的位置即可。

算法性能

  • 时间复杂度
    • 平均时间复杂度——O(nlogn)
    • 最坏情况下的时间复杂度——O(n*n)
  • 空间复杂度
    • 平均空间复杂度——O(logn)
    • 最坏情况下的空间复杂度——O(n)
  • 稳定性——不稳定

深入思考

至此,原理已经详解完了,但是其中有几个值得深入思考的问题,如有兴趣,可以继续一探究竟。
1.如何实现从大到小排序?
细心点就会发现,上述的示例代码实现的是从小到大排序,那要从大到小排序又该怎么写?
其实很简单,回顾下上面示例代码的原理,是选了一个关键字作为枢轴,相当于确定了一个标准,比枢轴小的丢左边,比枢轴大的丢右边,因此,将其反过来,将小的丢右边,大的丢左边就OK啦!因此,修改的部分很小,如下:

    // 在partition()方法中做如下修改即可
    // 将
    while (low < high && sequence[high] >= piovt) 
    // 改为
    while (low < high && sequence[high] <= piovt) 
    ----------
    // 将
    while (low < high && sequence[low] <= piovt) 
    // 改为
    while (low < high && sequence[low] >= piovt)

2.枢轴随便选一个都可以?
是的,枢轴随便选一个就可以!根据该算法的原理,枢轴只是关键字调整位置的一个标准,任意一个元素都可以作为枢轴。

但是,选枢轴的策略不同,会影响到partition方法的具体实现。例如,可以每次选序列的第三个元素作为枢轴,但是,当遇到长度小于3的序列时,就要改变下策略,如此就需要条件判断,徒增更多的麻烦。如上所示,将第一个元素或者最后一个元素选为枢轴,在编码实现起来最简单!

3.为什么不是真正的一对一交换?
常见的一对一交换是形如这样的:

    int a = 25, b = 36, tmp = 0;
    tmp = a;
    a = b;
    b = tmp;

而仔细研究下上面的代码,是先将low0的值备份到piovt,把low0的位置腾出来,然后移动high0到high1,将high1的值放到low0的位置,然后移动low0到新的位置low1,并将这个值放到high1,接着,high1继续向左移动成为high2,并将high2的值放到low1,如此类推。

代码下载

博主已将示例代码的工程打包上传,名为快速排序示例代码(JAVA版),点击此处下载。

博主水平有限,加之时间仓促,错漏难免,望不吝赐教!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值