(10)快速排序

1. 算法描述

快速排序(quick-sort)与前面介绍的归并排序(merge-sort)一样,使用了分治思想

下面是对一个一般的子数组A[p~r]进行快速排序的分治步骤:

  • 分解:数组A[p~r]被划分为两个子数组A[p...q]和A[q+1...r],使得A[q]大于等于A[p...q]中的每个元素,且小于等于A[q+1...r]中的每个元素。
  • 解决:对子数组A[p...q]和A[q+1...r]递归的调用快速排序。
  • 合并:因为子数组都是原址排序的,所以不需要合并操作,此时的A数组已经是排好序的。

下面给出伪代码:
(10)快速排序_第1张图片
快速排序伪代码1.png

可以看出,算法的关键是partiton方法的实现。下面给出它的算法实现:

(10)快速排序_第2张图片
快速排序伪代码2.png

直接看可能觉得很晕,我们结合实例看看它是如何工作的:


(10)快速排序_第3张图片
快速排序的示例.png

1、解释一下上图的变量的意思:

  • i 记录了数组小于A[r]的下标位置,
  • j代表轮训下标的位置,
  • p代表数组的下界
  • r是数组的上界

2、在i和j移动的过程中,数组被分成了三个部分:

  • 并且浅灰部分的元素均比A[r]小,
  • 黑色部分的元素均比A[r]大
  • 白色部分表示还没有轮训的数据,表示即将要排序的数据

下面给出java的实现代码:

/**
 * @Project: 10.dataStructure
 * @description: 快速排序
 * @author: sunkang
 * @create: 2018-08-19 23:19
 * @ModificationHistory who      when       What
 **/
public class QuickSort {

    /**
     * 利用分治策略的思想
     * 分解: 数组arr[low...high]被划分为 arr[low,q-1] 和 arr[q+1,high],使得arr[low,q-1] 中的每一个元素都小于
     * 等于arr[q],而arr[q+1,high]都大于arr[q]
     * 解决: 通过递归调用快速排序,对子数组 arr[low,q-1] 和arr[q+1,high] 进行排序
     * 合并: 因为子数组都是原址排序的,所以不需要合并操作:数组A[p...r]已经有序
     * @param arr
     * @param low
     * @param high
     */
    public void quickSort(Integer[] arr,int low,int high){
        if(low 
2. 快速排序的性能

快速排序的运行时间是跟划分密切相关的,因为划分影响着子问题的规模。

(1) 最坏情况划分

当每次划分把问题分解为一个规模为n-1的问题和一个规模为0的问题时,快速排序将产生最坏的情况(以后给出这个结论的证明,目前可以想象的出)。由于划分操作的时间复杂度为θ(n);当对一个长度为0的数组进行递归操作时,会直接返回,时间为T(0) = θ(1)。于是算法总运行时间的递归式为:

T(n) = T(n-1) + T(0) + θ(n) = T(n-1) + θ(n) w

可以解得,T(n) = θ(n²)。

由此可见,在划分都是最大程度不平均的情况下,快速排序算法的运行时间并不比插入排序好,甚至在某些情况下(比如数组本身已按大小排好序),不如插入排序。

(2) 最好情况划分

当每次划分都是最平均的时候(即问题规模被划分为[n/2]和【n/2】-1时),快速排序性能很好,总运行时间的递归式为:

T(n) = 2T(n/2) + θ(n)

可以解得,T(n) = θ(nlg n)。

(3) 平均划分

快速排序算法的平均运行时间,更接近于最好情况划分时间而非最坏情况划分时间。理解这一点的关键就是理解划分的平均性是如何反映到描述运行时间的递归式上的。

我们举个例子,对于一个9:1的划分,乍一看,这种划分是很不平均的。此时的运行时间递归式为:

T(n) = T(9n/10) + T(n/10) + cn,

我们可以用如下递归树来更加形象地描述运行时间:

(10)快速排序_第4张图片
快速排序的平均划分.png

递归会在深度为log10/9n = θ(lg n )处终止,因此,快速排序的总代价为O(nlgn)。可见,在直观上看起来非常不平均的划分,其运行时间是接近最好情况划分的时间的。事实上,对于任何一种常数比例的划分,其运行时间总是O(nlgn)。

3. 快速排序的随机化版本

以上的讨论其实都做了一个前提的声明,输入数据的所有排列都是等概率的。但是事实上这个条件并不一定总是成立。正如以前介绍的,有时候我们再在算法中引入随机性,可以使得算法对所以的输入都有较好的期望性能。很多人都选择随机化版本的快速排序作为大数据输入情况下的排序算法。

我们可以使用对数组的所有元素进行随机化排序的方式引入随机性。但为了简便,我们这里采用一种叫做随机抽样(random sampling)的随机化技术。

与以上始终采用A[r]作为“基准”的方法不同的是,随机抽样是从子数组A[p~r]中随机的抽取一个元素,把它作为“基准”,并与A[r]交换。其他的过程与上面介绍的一致。

下面是随机化版本的算法描述:


(10)快速排序_第5张图片
随机化快速排序伪代码1.png

(10)快速排序_第6张图片
随机化快速排序伪代码2.png

下面是Java的实现代码

/**
 * @Project: 10.dataStructure
 * @description:
 * @author: sunkang
 * @create: 2018-08-21 08:58
 * @ModificationHistory who      when       What
 **/
public class RandomQuickSort {

    /**
     * 利用分治策略的思想
     * 分解: 数组arr[low...high]被划分为 arr[low,q-1] 和 arr[q+1,high],使得arr[low,q-1] 中的每一个元素都小于
     * 等于arr[q],而arr[q+1,high]都大于arr[q]
     * 解决: 通过递归调用快速排序,对子数组 arr[low,q-1] 和arr[q+1,high] 进行排序
     * 合并: 因为子数组都是原址排序的,所以不需要合并操作:数组A[p...r]已经有序
     * @param arr
     * @param low
     * @param high
     */
    public void quickSort(Integer[] arr,int low,int high){
        if(low 

总结:
随机化的快速排序效果要好与不加随机化的,这样减少了最坏的情况的发生,随机化的快速排序算法的最坏情况的运行时间是θ(n²),一般来说随机化加入了随机化,平均情况下的运行时间为O(nlgn),这里就不证明了

你可能感兴趣的:((10)快速排序)