【算法】5 传说中的快排是怎样的,附实现示例

简介: 版权声明:转载请联系本人,感谢配合!本站地址:http://blog.csdn.net/nomasp https://blog.csdn.net/NoMasp/article/details/46314763 什么是快速排序快速排序简介快速排序(英文名:Quicksort,有时候也叫做划分交换排序)是一个高效的排序算法,由Tony Hoare在1959年发明(1961年公布)。
版权声明:转载请联系本人,感谢配合!本站地址:http://blog.csdn.net/nomasp https://blog.csdn.net/NoMasp/article/details/46314763

什么是快速排序

快速排序简介

快速排序(英文名:Quicksort,有时候也叫做划分交换排序)是一个高效的排序算法,由Tony Hoare在1959年发明(1961年公布)。当情况良好时,它可以比主要竞争对手的归并排序和堆排序快上大约两三倍。这是一个分治算法,而且它就在原地排序

所谓原地排序,就是指在原来的数据区域内进行重排,就像插入排序一般。而归并排序就不一样,它需要额外的空间来进行归并排序操作。为了在线性时间与空间内归并,它不能在线性时间内实现就地排序,原地排序对它来说并不足够。而快速排序的优点就在于它是原地的,也就是说,它很节省内存

引用一张来自维基百科的能够非常清晰表示快速排序的示意图如下:

这里写图片描述

快速排序的分治思想

由于快速排序采用了分治算法,所以:

一、分解:本质上快速排序把数据划分成几份,所以快速排序通过选取一个关键数据,再根据它的大小,把原数组分成两个子数组:第一个数组里的数都比这个主元数据小或等于,而另一个数组里的数都比这个主元数据要大或等于。

这里写图片描述

二、解决:用递归来处理两个子数组的排序。 (也就是说,递归地求上面图示中左半部分,以及递归地求上面图示中右半部分。)

三、合并:因为子数组都是原址排序,所以不需要合并操作,通过上面两步后数组已经排好序了。

所以快速排序的主要思想是递归与划分

如何划分

当然最重要的是它的复杂度是线性的,也就是 Θ ( n ) 个划分的子程序。

Partition(A,p,q)   // A[p,..q] 
1   x=A[p]   // pivot=A[p] 主元 
2   r=p 
3   for i=p+1 to q
4       do if A[i]<=x
5          then r=r+1 
6             exch A[r]<->A[i] 
7   exch A[p]<->A[r] 
8   return r // i pivot 

这就是划分的伪代码,基本的结构就是一个for循环语句,中间加上了一个if条件语句,它实现了对子数组 A [ p . . . q ] 的原址排序。

这里写图片描述

刚开始时 i 等于 p j 等于 p + 1 。在这个循环中查找i下标的数据,如果它比 x 大,那就将其存放到“>=x”区域并将 j 加1后进行下一次循环。而如果它比 x 小,那就要做些动作来维持循环不变量了。将 i 的下标加1后将下标i对应的数据和下标j所对应的数据互换位置。然后再移动区域的界限并开始下一次循环。

那么这个算法在n个数据下的运行时间大约是 O ( n ) ,因为它几乎把每个数都比较了一遍,而每个步骤所需的时间都为 O ( 1 )

这里写图片描述

(倒数第二行的3和4的位置错了,应该是到最后一行的时候才交换过来)

上面这幅图详细的描述了Partition过程,每一行后也加了注释。

将递归的思想作用于划分上

有了上面这些准备工作,再加上分治的思想实现快速排序的伪代码也是很简单的。

Quicksort(A,p,q) 
1   if p<q 
2     then r=Partition(A,p,q)   
3          Quicksort(A,p,r-1) 
4          Quicksort(A,r+1,q) 

为了排序一个数组A的全部元素,初始调用时 Q u i c k s o r t ( A , 1 , A . l e n g t h )

实现示例

// Java
package com.nomasp;

/**
 * Created by nomasp on 16/9/24.
 */
public class QuickSort {

    private int[] array;

    public QuickSort(int[] array) {
        this.array = array;
    }

    private void swap(int x, int y) {
        int tmp = array[x];
        array[x] = array[y];
        array[y] = tmp;
    }

    private int partition(int start, int end) {
        int pivot = array[start];
        int r = start;
        for (int i = start + 1; i <= end; i++) {
            if (array[i] <= pivot) {
                r += 1;
                swap(r, i);
            }
        }
        swap(start, r);
        return r;
    }

    private void quickSort(int start, int end) {
        if (start < end) {
            int r = partition(start, end);
            quickSort(start, r - 1);
            quickSort(r + 1, end);
        }
    }

    public int[] sort() {
        quickSort(0, array.length - 1);
        return array;
    }

    public int[] sort(int start, int end) {
        quickSort(start, end);
        return array;
    }
}

快速排序的算法分析

相信通过前面的诸多实践,大家也发现了快速排序的运行时间依赖于Partition过程,也就是依赖于划分是否平衡,而归根结底这还是由于输入的元素决定的。

如果划分是平衡的,那么快速排序算法性能就和归并排序一样。

如果划分是不平衡的,那么快速排序的性能就接近于插入排序。

怎样是最坏的划分

1)输入的元素已经排序或逆向排序
2)每个划分的一边都没有元素

也就是说当划分产生的两个子问题分别包含了n-1个元素和0个元素时,快速排序的最坏情况就发生了。

T ( n ) = T ( 0 ) + T ( n 1 ) + \Theta(n) = Θ ( 1 ) + T ( n 1 ) + Θ ( n ) = Θ ( n 1 ) + Θ ( n ) = Θ ( n 2 )

这是一个等差级数,就和插入排序一样。它并不比插入排序快,因为当同样是输入元素已经逆向排好序时,插入算法的运行时间为 Θ ( n ) 。但快速排序仍旧是一个优秀的算法,这是因为在平均情况下它已经很高效。

我们为最坏情况画一个递归树。

这里写图片描述

这是一课高度不平衡的递归树,图中左边的那些 T ( 0 ) 的运行时间都为 Θ ( 1 ) ,而总共有n个。

所以算法的中运行时间为:

T ( n ) = Θ ( n ) + Θ ( n 2 ) = Θ ( n 2 )

最坏划分的算法分析

通过上面的图示我们知道了在最坏情况下快速排序的复杂度是 Θ ( n 2 ) ,但以图示的方式并不是一种严谨的证明方式,我们应该使用代入法来证明它。

当输入规模为n时,时间 T ( n ) 有如下递归式:

T ( n ) = m a x 0 r n 1 ( T ( r ) + T ( n r 1 ) ) + Θ ( n )

除去主元后,在Partition函数中生成的两个子问题的规模的和为n-1,所以r的规模才是0到n-1。

假设 T ( n ) c n 2 成立,其中c为常数这个大家都知道的。于是上面的递归式为:

T ( n ) m a x 0 r n 1 ( c r 2 + c ( n r 1 ) 2 ) + Θ ( n ) c m a x 0 r n 1 ( r 2 + ( n r 1 ) 2 ) + Θ ( n )

1)而 r 2 + ( n r 1 ) 2 对于r的二阶导数为正,所以在区间 0 r n 1 的右端点取得最大值。

于是有 m a x 0 r n 1 ( r 2 + ( n r 1 ) 2 ) ( n 1 ) 2 = n 2 2 n + 1 ,所以对于 T ( n ) 有:

T ( n ) c n 2 c ( 2 n 1 ) + Θ ( n )

最终因为我们可以选择一个足够大的 c ,来使得 c ( 2 n 1 ) 大于 Θ ( n ) ,所以有 T ( n ) = O ( n 2 )

2) r 2 + ( n r 1 ) 2 对于r的二阶导数为正,所以在区间 0 r n 1 的左端点取得最小值。

于是有 m a x 0 r n 1 ( r 2 + ( n r 1 ) 2 ) ( n 1 ) 2 = n 2 2 n + 1 ,所以对于 T ( n ) 有:

T ( n ) c n 2 c ( 2 n 1 ) + Θ ( n )

同样我们也可以选择一个足够小的 c ,来使得 c ( 2 n 1 ) 小于 Θ ( n ) ,所以有 T ( n ) = Ω ( n 2 )

综上这两点得到 T ( n ) = Θ ( n 2 )

怎样是最好的划分

当Partition将数组分为 n / 2 n / 2 两个部分时是最高效的。此时有:

T ( n ) = 2 T ( n / 2 ) + Θ ( n ) = Θ ( n l g n )

怎样是平衡的划分

快速排序的平均运行时间更接近于其最好情况,而非最坏情况。

此处有一个经典的示例,将数组按 1 9 的比例进行划分会怎样呢?这种划分看似很不平衡,但真的会因此而影响效率么?

其中此时的递归式是:

T ( n ) = T ( 1 10 n ) + T ( 9 10 n ) + Θ ( n )

这里依旧通过递归树来观察一番。

这里写图片描述

因为每次都减少十分之一,需要减多少次才能达到n呢,也恰好也是以10为底对数的定义。所以左侧的高度为 l o g 10 n 了,相应的右侧的高度为 l o g 10 9 n

所有那些叶子加在一起也只有 Θ ( n ) ,所以有:

T ( n ) c n l o g 10 9 n + Θ ( n )

其实 T ( n ) 的下界也渐近为 n l g n ,所以总时间为:

T ( n ) = Θ ( n l g n )

只要划分是常数比例的,算法的运行时间总是 O ( n l g n )

随机化快速排序

随机算法的思想

在前面分析快速排序的平均情况性能时,是建立在输入数据的所有排列都是等概率的条件下的,但在实际工程中往往不会总出现这种良好的情况。

【算法】3 由招聘问题看随机算法中我们介绍了随机算法,它使得对于所有的输入都有着较好的期望性能,因此随机化快速排序在有大量数据输入的情况下是一种更好的排序算法。

以下是随机化快速排序的好处:

1)其运行时间不依赖与输入序列的顺序

2)无需对输入序列的分布做任何假设

3)没有 一种特别的输入会引起最差的运行情况

4)最差的情况由随机数产生器决定

随机抽样技术

现在我们来使用一种叫做随机抽样(random sampling)的随机化技术,使用该技术就不再始终采用A[p]作为主元,而是从A[p…q]中随机选择一个元素作为主元。

为了达到这一目的,首先将 A [ p ] 与从 A [ p . . . q ] 中随机选出的一个元素交换。

通过对序列 p . . . q 的随机抽样,我们可以保证主元元素 x = A [ p ] 是等概率地从子数组的 q p + 1 个元素中选取的。

因为主元元素是随机选择的,我们可以期望在平均情况下对输入数组的划分是比较均衡的。所以对前面的两份伪代码做如下修改:

RANDOMIZED-PARTITION(A,p,q)
1   i=RANDOM(p,q)
2   exchange A[p] with A[i]
3   return PARTITION(A,p,q)
RANDOMIZED-QUICKSORT(A,p,q)
1   if p<q
2       r=RANDOMIZED-PARTITION(A,p,q)
3       RANDOMIZED-QUICKSORT(A,p,r-1)
4       RANDOMIZED-QUICKSORT(A,r+1,q)

有了随机抽样技术后再也不用担心快速排序遇到最坏划分的情况啦,所以说随机化快速排序的期望运行时间是 O ( n l g n )

实现示例

// Java
package com.nomasp;

import java.util.Random;

/**
 * Created by nomasp on 16/9/24.
 */
public class QuickSort {

    private int[] array;

    public QuickSort(int[] array) {
        this.array = array;
    }

    private void swap(int x, int y) {
        int tmp = array[x];
        array[x] = array[y];
        array[y] = tmp;
    }

    private int partition(int start, int end) {
        int pivot = array[start];
        int r = start;
        for (int i = start + 1; i <= end; i++) {
            if (array[i] <= pivot) {
                r += 1;
                swap(r, i);
            }
        }
        swap(start, r);
        return r;
    }

    private int randomizedPartition(int start, int end) {
        Random random = new Random();
        int i = start + random.nextInt(end - start);
        swap(start, i);
        return partition(start, end);
    }

    private void randomizedQuickSortCore(int start, int end) {
        if (start < end) {
            int r = randomizedPartition(start, end);
            randomizedQuickSortCore(start, r - 1);
            randomizedQuickSortCore(r + 1, end);
        }
    }

    public int[] quickSort() {
        randomizedQuickSortCore(0, array.length - 1);
        return array;
    }

    public int[] quickSort(int start, int end) {
        randomizedQuickSortCore(start, end);
        return array;
    }
}



感谢您的访问,希望对您有所帮助。 欢迎大家关注、收藏以及评论。


为使本文得到斧正和提问,转载请注明出处:
http://blog.csdn.net/nomasp


目录
相关文章
|
11月前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
283 64
|
移动开发 算法 前端开发
前端常用算法全解:特征梳理、复杂度比较、分类解读与示例展示
前端常用算法全解:特征梳理、复杂度比较、分类解读与示例展示
325 0
|
机器学习/深度学习 人工智能 算法
【人工智能】传统语音识别算法概述,应用场景,项目实践及案例分析,附带代码示例
传统语音识别算法是将语音信号转化为文本形式的技术,它主要基于模式识别理论和数学统计学方法。以下是传统语音识别算法的基本概述
777 2
|
机器学习/深度学习 运维 算法
深入探索机器学习中的支持向量机(SVM)算法:原理、应用与Python代码示例全面解析
【8月更文挑战第6天】在机器学习领域,支持向量机(SVM)犹如璀璨明珠。它是一种强大的监督学习算法,在分类、回归及异常检测中表现出色。SVM通过在高维空间寻找最大间隔超平面来分隔不同类别的数据,提升模型泛化能力。为处理非线性问题,引入了核函数将数据映射到高维空间。SVM在文本分类、图像识别等多个领域有广泛应用,展现出高度灵活性和适应性。
557 2
|
并行计算 算法 Python
Dantzig-Wolfe分解算法解释与Python代码示例
Dantzig-Wolfe分解算法解释与Python代码示例
|
机器学习/深度学习 算法 数据挖掘
Python机器学习10大经典算法的讲解和示例
为了展示10个经典的机器学习算法的最简例子,我将为每个算法编写一个小的示例代码。这些算法将包括线性回归、逻辑回归、K-最近邻(KNN)、支持向量机(SVM)、决策树、随机森林、朴素贝叶斯、K-均值聚类、主成分分析(PCA)、和梯度提升(Gradient Boosting)。我将使用常见的机器学习库,如 scikit-learn,numpy 和 pandas 来实现这些算法。
|
存储 算法 Java
Java查找算法概览:二分查找适用于有序数组,通过比较中间元素缩小搜索范围;哈希查找利用哈希函数快速定位,示例中使用HashMap存储键值对,支持多值关联。
【6月更文挑战第21天】Java查找算法概览:二分查找适用于有序数组,通过比较中间元素缩小搜索范围;哈希查找利用哈希函数快速定位,示例中使用HashMap存储键值对,支持多值关联。简单哈希表实现未涵盖冲突解决和删除操作。
190 1
|
机器学习/深度学习 算法 Python
python与朴素贝叶斯算法(附示例和代码)
朴素贝叶斯算法以其高效性和优良的分类性能,成为文本处理领域一项受欢迎的方法。提供的代码示例证明了其在Python语言中的易用性和实用性。尽管算法假设了特征之间的独立性,但在实际应用中,它仍然能够提供强大的分类能力。通过调整参数和优化模型,你可以进一步提升朴素贝叶斯分类器的性能。
436 0
|
算法 搜索推荐 数据可视化
【漫画算法】插入排序:插入宝石的传说
【漫画算法】插入排序:插入宝石的传说
|
存储 算法 测试技术
数据结构学习记录——树习题-Complete Binary Search Tree(题目描述、输入输出示例、数据结构的选择、核心算法、计算左子树的规模)
数据结构学习记录——树习题-Complete Binary Search Tree(题目描述、输入输出示例、数据结构的选择、核心算法、计算左子树的规模)
144 1

热门文章

最新文章