快速排序简介

` 对于包含n个数的输入数组来说,快速排序是一种最坏情况时间复杂度为O(n2n^2n2)的排序算法。虽然最坏情况时间复杂度是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度是O(n lgn),而且O(nlgn)中隐含的常数因子非常小。并且,它还能够进行原址排序。

提示:以下是本篇文章正文内容,是作者本人在自主学习过程中的学习思考,也相当于是作为学习笔记把自己的一些思考和感悟记录下来。其中的各种算法也都是借鉴于不同的地方。本篇文章一些介绍描述性的文字参考自《算法导论》。大部分对代码的分析可以不用在意,这是自己的一些拙见与多想的地方。

一、快速排序的描述?

快速排序是使用了分治的思想。比如对一个典型的子数组 A[p…r] 进行快速排序的三个步骤:
分解:数组 A[p…r] 被划分为两个(可能为空)子数组 A[p…q-1] 和 A[q+1…r],使得 A[p…q-1] 中的每一个元素都小于等于A[q],而 A[q] 也小于等于A[q+1…r] 中的每一个元素。其中,计算下标q 也是划分过程的一部分。
解决:通过递归调用快速排序。对子数组 A[p…q-1] 和 A[q+1…r] 进行排序。
合并:因为子数组都是原址进行排序的,所以不需要进行合并操作,数组 A[p…r] 已经有序。

二、几种不同的实现方法

1.从头遍历法(借鉴算法导论)

代码如下(示例):

//数组的划分
int Partition(int A[],int l,int r){int x=A[r];                       //标记1int i=l-1;                        //标记2for(int j=l;j<=r;j++){if(A[j]<=x){                  //标记3i++;swap(A[j],A[i]);}}return i;                         //标记4
}
//递归调用数组划分函数完成对数组的排序
void QuickSort(int A[],int l,int r){if(l>=r)return;int loc = Partition(A,l,r);QuickSort(A,l,loc-1);QuickSort(A,loc+1,r);
}

下面将对上述代码从实现的角度进行分析,并且就上面所做的标记进一步思考这么做原因。
(1) 实例分析
对于一个包含5个数据的数组使用此种方法来进行排序,第一次进行划分的具体步骤如下所示。
(a)2 8 7 1 3 (b)2 8 7 1 3 (c)2 8 7 1 3 (d)2 8 7 1 3 (e)2 1 7 8 3 (f)2 1 3 8 7

上述过程a~f 便是对数组初始数组进行排序的一个过程及元素在过程中的中间状态。其中,初始的x便是我们所选定的枢轴元素,以它为基准进行划分。黑色加粗的元素表示已经确定的小于等于枢轴的元素,正常且非加粗的字体表示大于枢轴的元素,标记(黄底)元素表示暂时还未确定大小的元素。(a) 初始待划分的数组,此时i=-1,j=0,x=3。(b) 2与自身进行交换,并放入了元素值较小的那一部分。此时i=0,j=0。(c)、(d) 将7和8添加到元素值较大的那一部分。此时i=0,j=2。(e)1和8进行交换,数值较小的部分规模增加。此时,i=1,j=3。(f)3和7进行交换,数值较小的部分规模增加。此时i=2,j=4。循环结束,枢轴元素3 位于两个部分之间。

在一次划分过程中,i,j 将数组分为了三部分。A[l…i] 中元素都小于等于x,A[i+1…j] 中元素都大于x,A[j+1…r] 中元素不确定它们与x 的大小关系。每当对当前元素进行比较之后再决定是否将其加入较小的元素集合中,或者是不做任何操作,继续往后遍历。

(2) 标记1的思考
从代码的角度来说所选取的枢轴元素是数组最右的一个元素,那我们可以选择别的元素来作为枢轴元素吗?比如左侧元素,或者中间某一个元素。

答案是不可以的。如果选取最右元素之外的其他元素来作为枢轴元素的话,当此元素为最大的值时,就不会对这个序列造成任何的改动,也就是划分起不到任何效果。比如对 9 8 7 进行的一次划分,当选取9作为枢轴元素时,第一次划分之后的序列仍然是9 8 7,返回 i=2,但是9 还在A[0] 处,因此就会出现枢轴元素的右边存在小于等于枢轴元素,并且x 的最终位置与返回位置不符的情况。

那将 标记3处的“<= ”换为“<”是否就可行了呢?详细分析见(4)。

但是如果非要使用数组中间的某个元素作为枢轴元素的话,可以先将该元素和A[r] 交换之后再 按照上述算法进行排序,依然还是成立的。

(3)标记2 的说明
通过前面的分析可以知道 i 所指示的位置是所有小于等于枢轴元素的最后一个位置,因此在开始时还没有进行判断,所有元素的大小还是不确定的,故先将 i-1。

(4)标记3的思考
在从前向后的遍历中对每个 “<=” 枢轴元素的数加入到前面较小的区域中。那如果把“<=” 换为“<” 算法是否还是可行呢?

试想一下这样一种情况:初始的元素序列为 6 5 3,调用QuickSort(A,0,2),当选取3 为枢轴元素时,由于所有的元素都是大于等于3的,即不存在小于3 的元素,所以第一次划分不会对数组改变,即 返回 i= -1。那么接下来就会调用QuickSort(A,0,-2) 和QuickSort(A,0,2),出现了和初始参数相同 的递归调用,因此会陷入无限递归中,导致栈溢出。

(5)标记4的思考
前面在介绍快速排序的基本实现思路的时候,提到过在划分的过程中要对每次的划分的最终位置进行计算,此处返回的 i 就是枢轴元素在此次划分之后所在的最终位置。那为什么返回的是 i ?

前面的实力分析中提到在一次划分技术后 A[l…i] 中所有元素都是小于等于枢轴元素的,按说枢轴元素在其中的某一个不确定的位置,怎么会是 i 呢?其实在枢轴元素就是A[r],那么在对A[r-1] 比较之后,A[l…i]中都小于等于A[r],A[i+1] 一定大于A[r],然后再对A[r] 进行比较,满足小于等于x,那么就会交换A[i+1]和A[r],注意交换的A[r] 的值也是等于枢轴元素值的,所以最后遍历完成时的 i 便是枢轴元素的最终位置,返回该位置没有任何问题。

(6)缺点
从大体上一看不出来此算法有太大的问题,对于每次划分都是遍历一次数组。但是如果当数组所有元素都是相等的值时,此时算法的复杂度就会变为O(n2n^2n2),在数据很大的情况下,不能提供很好的性能。
原因就是从头向后进行遍历,当元素值相同时,每次返回的枢轴元素位置都是A[r],也就是说,不能很好的将数组相等的元素值划分开,最佳的情况就是返回(l+r)/2。接下来所介绍的两种方法将弥补这一缺点。

2.前后置换法

代码如下(示例):

void QuickSort(int A[],int l,int r){if(l>=r)return;int x=A[l];                       //标记1int i=l-1,j=r+1;                  //标记2while(i<j){while(A[++i]<x);while(A[--j]>x);if(i<j)swap(A[i],A[j]);}QuickSort(A,l,j);                 //标记3QuickSort(A,j+1,r);
}

(1)实例分析
对如下数据调用该算法所进行的第一次划分状态如下所示。
(a)3 2 1 5 3 (b)3 2 1 5 3(c)3 2 1 5 3 (d)3 2 1 5 3

上面状态中除了(a)是初始状态之外,粗体表示此时使内循环退出时 i 所指向的数字,斜体表示此时使内循环退出时 j 所指向的数字,标记数字表示已经判断完成不需再移动的数字。(a)为初始待排序的数组,i= -1,j=5,枢轴元素为3。(b)互换两个3的位置,此时 i = 0 ,j = 4。(c)内循环结束时,i 指向5位置,j 指向1 位置。(d)外循环也结束,这就是第一轮互换的结果。

可以看到使用这种方法在进行实际的排序时,中间的一轮划分可能使得数组元素不会发生变化,并且主轴元素也不一定在其最终的位置,这与前面所说的每一次划分完成都能确定枢轴元素位置的说法有时候矛盾。但是这却不影响继续对元素的判断继续排序。要注意到,虽然枢轴元素的最终位置是没有确定的,但是可以发现一个规律,那就是 A[l…j] 中的元素都小于等于枢轴元素x,A[j+1…r] 中的元素都大于等于枢轴元素x。又使用的是递归进行调用划分函数的,当子数组中只有一个元素时,那么就一定确定其是有序的,即在依次递归划分的过程中就已经排好序了。

还有需要注意的一点是,在递归排序时并没有舍弃一个中间元素,那是因为没有元素的最终位置被确定。

在对一个子数组进行划分的过程中,A[l…i] 中元素都 小于等于x,A[j…r] 中元素都大于等于 x,A[i…j]中元素是未确定大小。而在结束一轮划分时,j 要么是比 i 小1,要么是 j 就等于 i 。

(2)标记1的思考
此处选择的枢轴元素是A[l],那么可以是其他的元素吗?

答案是可以的,选取A[l],A[(l+r)/2] 等作为枢轴元素都是可以。原因就在于这种方法只是在判定过程中对等于枢轴元素的值也做了互换,保证了对枢轴元素的处理,但是并不要求最终确定 枢轴元素的位置。因此选取哪个元素都是可以实现对数组的排序的。

(3)标记2 的思考
此处将 l 提前-1,将 r 提前+1的值分别赋给 i 和 j 目的是什么?

目的就是为了简化代码,使得每次使得内循环退出的 i 和 j 的值都是对应于正好不符合条件元素位置。
如果代码如下所示:

void QuickSort(int A[],int l,int r){if(l>=r)return;int x=A[l];                      int i=l,j=r;                      //修改1while(i<j){while(A[i]<x)i++;             //修改2while(A[j]>x)j--;if(i<j)swap(A[i],A[j]);}QuickSort(A,l,j);                QuickSort(A,j+1,r);
}

那么对于(1)中实例就会出现死循环的现象。i = 0,使得A[i]<x 一直不成立;j=4,使得A[j]>x 也一直不成立,导致 i 和 j 的值一直不会改变 ,程序陷入死循环。这与一开始设想的互换完两个元素之后,i 自动加1,j 自动减1有所相悖。所以说,此处的代码能使 i 和 j 能正常变化。

(4)标记3的思考
在递归调用QuickSort() 函数时使用的分界变量是 j 和 j +1。如果选取 j、j-1或者 i、i-1能否可行呢?

首先,如果选取 j、j-1 和 i、i+1肯定是不行的。因为根据代码中的判定规则在外层循环结束时 i>=j,并且还满足A[l…j] 都是小于等于 x 的,A[j+1…r] 都是大于等于 x 的,如果选取 j、j-1 就有可能会在A[j+1…r] 都大于等于 x 的元素加入一个小于x 的元素A[j];如果选择 i、i+1 就有可能会在A[l…j] 都小于等于 x 的元素中加入一个大于 x 的元素 A[i]。可以通过分析 上面例子 3 2 1 5 3 第一次划分结束时 i=3、j=2来带入。
其次,j、j+1 和 i-1、i 的选取是具有相对性的,这个相对性是和每次划分所选定元素的位置所决定的。如果每次划分都有可能选择A[l] 作为枢轴元素,则就选择 j、j+1;如果每次划分都有可能选择A[r] 作为枢轴元素,则就选择 i-1、i 。因为首先这两种选择都是不会破坏已经划分好的性质的;然后就是要保证对任何序列都不会出现重复的调用,否则就无限递归。

假设 一个序列 1 2 3 已经有序,初始调用为QuickSorrt(A,0,2),如果选择A[l] 作为枢轴元素,那么在第一次划分结束时 i =0、j =0,若选取 i-1、i 为基准进行递归划分的话,就有QuickSort(A,0,-1) 和QuickSort(A,0,2) 被递归调用,调用了和初始调用具有同样参数的函数,且序列没有改变,因此就导致了无限递归,直至栈满;如果选择A[r] 作为枢轴元素,则在第一次划分结束时 i = 2、j =2,若选择 j、j+1 为基准,就有QuickSort(A,0,2) 和QuickSort(A,2,3) 被调用,同理,还是会出现无限递归的情况。当选取一个中间元素作为枢轴元素时,无论如何 i 都会大于 l 且 j 都会小于 r 的,此时使用 j、j+1 和 i-1、i 均可,因为此时不会出现调用与初始递归相同的参数出现。

在实际应用中,可能对于已经有序的数列为了每次划分都尽可能平均会选择A[(l+r)/2] 作为枢轴元素,此时如果只有两个元素时其枢轴元素实际相当于 A[l],因此这时应选择 j、j+1 作为划分基准。

(5)补充
这个算法基本性能是最好的快速排序算法了,只要选取的枢轴元素适当能满足对各种不同情况的快速排序。并且解决了第一个算法不能很好的排序所有元素 都相等的情况,即在元素都相等的情况下,能较好的划分出两个子数组的长度。此外,还能随意的选择枢轴元素,比较灵活。

最明显的一个特点就是,在一次划分结束后,枢轴元素的最终位置并不一定能确定,只是划分了两个区域。

3.前后查找法

代码如下(示例):

int Partition(int A[],int l,int r){int x=A[l];                     //标记1while(l<r){while(l<r&&A[r]>=x)r--;     //标记2A[l]=A[r];while(l<r&&A[l]<=x)l++;A[r]=A[l];}A[r]=x;                           //标记3return r;
}void QuickSort(int A[],int l,int r){if(l>=r) return;int pos= Partition(A,l,r);QuickSort(A,l,pos-1);QuickSort(A,pos+1,r);
}

(1)实例分析
(a)6 5 1 9 8 3 (b)3 5 1 9 8 3 (c)3 5 1 9 8 9 (d)3 5 1 6 8 9

上述(a)是待排序列初始状态,初始时选定枢轴元素为 6,i=0、j=5。(b)从后向前找到第一个小于6 的数字放到A[i] 中。(c)从前向后寻找第一个大于6 的数字放到 A[j] 中。(d)退出外部循环时,l=r,将枢轴元素放在A[r] 中即可,r 便是 x 的最终位置。

其实,在划分的某一时刻 待排序列的数组满足这样一种情况,假设用 i、j 分别代表 l、r 的变化,即 A[l…i] 中都小于等于 x,A[j…r] 中的元素都大于等于 x,A[i…j] 中都是未确定 与 x 大小的元素。

(2)标记1处的思考
此处选择的是数组最左边的元素作为枢轴元素,那么可以选取其他元素作为枢轴元素吗?答案是不可以的。因为此处选定A[l] 作为枢轴元素时有深意的 ,这样先从后向前找到第一个小于 x 的值赋给A[l],此时A[r] 空闲了。也就是说,先把 枢轴元素记录下来,那么它的位置就空出来了,就能存放一个比它小的数;然后再从前向后找到第一个大于 x 的值赋给A[l]。这样依次寻找第二个、第三个小于/大于 x 的值赋给相应位置即可。

如果选择数组中间某一位置为 x,那么在第一次赋值时就会出现问题,把没有记录下来可能独一无二的A[l] 的值覆盖掉了,导致数据丢失。如果非要选取 p(l<p<r) 位置作为枢轴元素的话,那么要先交换 A[l] 和A[p] 的值,然后再进行划分就可以了。

(3)标记2 的 思考
此处的循环中在比较A[p] 与 x 大小关系的同时还要比较 i 和 j 的大小,这真的有必要吗?那它紧接着的一行就不加也没必要比较 i 和 j 的 大小了吗?

首先,在循环处比较A[p] 与 x 大小的同时比较 i 和 j 的大小是有必要的。因为如果所选定的枢轴元素A[l] 是最小的,在不加 l < r 的限定条件下,r 就会一直减少,直至为 -1,访问地址 A[-1] 出错。所以要加上 l<r 的限制条件。

其次,由于上面循环的限制,在退出循环后 i 要么 j,满足赋值条件进行赋值;要么 i 等于 j,自身对自身赋值,不会出现异常,所以无须再加 i<j 的限制,因为不会出现 i > j 的情况发生。

(4)标记3 的思考
此处是将最后退出循环时 r 的值作为枢轴元素的最终位置,此时所有元素都已经比较完成,只剩早先记录下的 x 还未恢复,所以将其放置在 A[r] 即可。那么 为什么 r 就是 x 的最终位置呢?为什么不是 l 呢?
首先,根据在划分过中的数组的各部分的性质可知 A[l…i] 都小于等于 x,A[j…r] 都大于等于 x,当 i 与 j 相邻时,即 j=i+1 时,此时不论接下来 i++,还是 j-- 都会导致 i=j 的发生,那么在每个循环的条件判断中就不成立,退出循环了,也就是说,循环结束时,必有 i=j。

其次,任意时刻都有一个位置A[i] 或A[j] 是多余的,要被赋予新的值,那么在退出循环时,i=j,即A[i] 原本的值已经多余,所以A[i] 就是 提前记录下的枢轴元素的最终位置。

(5)改进
其实这种算法已经能很好的适合各种情况了,但是如果是所有元素值都相等的情况,就会出现每次划分所返回的 x 的最终位置都是 l,但是理想情况下应该是返回 (l+r)/2 出现这种问题的原因就是对等于
x 的元素每次处理的时候选择忽略,导致不会对它们进行操作,因此还可以对此算法的划分函数进行改进。如下代码所示。

int Partition(int A[],int l,int r){int x=A[l];while(l<r){while(l<r&&A[r]>x)r--;     //标记1if(l<r)                    //标记2A[l++]=A[r];           //标记3while(l<r&&A[l]<x)l++;if(l<r)A[r--]=A[l];}A[l]=x;return l;
}

其中标记1的循环中将判定条件中的“=”去掉了以实现对等于 枢轴元素 x 值进行操作。标记 2 再进行一次判断的必要是因为A[l++] = A[r] 会在 l=r 时使 l 加一,不便于之后对最终位置的确定。标记3 使用A[l++] = A[r] 是为了防止存在多个等于 x 的值造成的位置 l 和 r 滞留。在最后划分结束时 l=r。

总结

综合上面的三种快速排序算法,第二、三种可以较好的适应于各种情况,第一种算法则不太适应于元素值都相等的情况。并且第二种方法在一次划分结束时,作为基准的枢轴元素并不会处在其最终的位置,而是在多次递归中确定了最终位置。

本文是我在学习过程对各种疑问的一种记录与想法,可能有的地方过于冗杂,希望不要过于纠结于此。大可以不必阅读代码下面的分析,只关乎代码则可直接使用即可。欢迎大家提出更好的建议和方法,第一次发文,意在分享与交流,如有错误之处,欢迎指正。

快速排序的几种实现方法相关推荐

  1. 快速排序的两种实现方法(c语言版本)

    经过调研发现,对任意无序整数数组,快速排序有两种实现方法,这里简单阐述下思路: 思路一:随意选择一个基准元,一般选择数组的起始元或末尾元,Weiss这本书上特意搞了个算法来选择基准元,--,总之就是基 ...

  2. 快速排序的5种优化方法

    三种快速排序以及快速排序的优化 1.快速排序的基本思想: 快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小.之后分别对这两部分记录继续进行排 ...

  3. 快速排序的三种实现方法

    文章目录 1.快速排序的思想 2.基本实现 2.1第一种排序 2.2第二种排序 2.3第三种排序(挖坑法) 2.4代码测试 3.代码优化 4.快速排序小结 1.快速排序的思想 快速排序的过程可以分为以 ...

  4. 快速排序的两种实现方法(js)

    快速排序的基本思想:通过一趟排序,将待排记录分割成独立的两部分,其中一部分记录的关键字均比另外一部分记录的关键字小,则可分别对着两部分记录继续进行排序,以达到整个序列有序的目的.----------- ...

  5. 郑州尚学堂:JAVA常用4种排序方法

    JAVA中在运用数组进行排序功能时,一般有四种方法:快速排序法.冒泡法.选择排序法.插入排序法. 当然 程序中最简单的使用就是:快速排序和冒泡排序,插入排序的使用更具有技巧性,选择排序则过于复杂,冗杂 ...

  6. 索骥馆-编程语言之《算法引论-一种创造性方法》扫描版[PDF]

    内容介绍: 本书是国际算法大师乌迪·曼博(UdiManber)博士撰写的一本享有盛誉的著作.全书共分12章:第1章到第4章为介绍性内容,涉及数学归纳法.算法分析.数据结构等内容:第5章提出了与归纳证明 ...

  7. 快速排序的三种实现以及应用场景

    好了,废话不多说,直接上干货. 1.快速排序的概念: 快速排序(Quicksort)是对冒泡排序的一种改进. 快速排序由C. A. R. Horno在1962年提出.它的基本思想是:通过一趟排序将要排 ...

  8. 快速排序基准位置的选取方法和快速排序优化

    目录 一. 快速排序基准位置的选取方法 1.固定位置法 2.随机选取基准法 3.三分取基准 二.优化: 1.当待排序数组当中数据比较少的时候,用直插 2.聚集相同元素法(基准一样的元素),减少递归的次 ...

  9. 数组的四种排序方法介绍

    最近在学习和练习一些算法方面的知识,发现在java中运用数组进行排序一般就四种方法:快速排序法.冒泡法.选择排序法.插入排序法.我们只要掌握这四种排序方法,基本上就能解决所有的排序问题.所以我接下来就 ...

最新文章

  1. Mask-RCNN技术解析
  2. PostgreSQL(三)pgpool管理PostgreSQL集群下主机宕机后的主从切换
  3. 浏览器设置了打开会显示特定网页为什么还是显示2345_Edge没能取代Chrome成为我的主力浏览器...
  4. Flink 在爱奇艺广告业务的实践
  5. 华为的型号命名规则_华为Mate 40 Pro+ 外观曝光 正面双挖孔 背部奥利奥
  6. 【2019浙江省赛 - J】Welcome Party(并查集,bfs,优先队列,建图)
  7. java实现的简单程序登录界面
  8. js获取ip地址_(原创)Node.JS实战31:大名鼎鼎的Express!
  9. wemall微信商城云平台 快速创建您的微信商城
  10. iOS之路10-给xib设置约束注意
  11. MFC_Combo_Box_用法(下拉框)
  12. CSS中自适应屏幕分辨率
  13. POJ2155 Matrix 二维线段树
  14. Adobe Premiere Pro (PR) 视频剪辑相关应用
  15. IT培训分享:11种热门编程语言的主要用途
  16. 怎样在matlab中使用多项式,MATLAB中的多项式运算
  17. Cookie或Token实现网站自动登录
  18. Win7下配置php运行环境
  19. 说一下HashMap的实现原理?
  20. Android AndroidManifest 文件详解

热门文章

  1. 2013 年度最新的 20 大热门开源软件
  2. 苹果vs谷歌:与微软的战争教会了苹果什么
  3. 【无机纳米材料科研制图——3ds Max 0107】3dmax高尔基体、细胞膜建模
  4. HIT-计算机系统大作业2022-程序人生-Hello‘s P2P
  5. 这20个电路图,硬件工程师随时可能用得上
  6. Atcoder Beginner Contest 271 A~E 题题解
  7. 5G网关中的“瑞士军刀”--动态讲解飞凌FCU2303的智能应用
  8. 【转载】Gartner names Microsoft a Leader in 2019 Endpoint Protection Platforms Magic Quadrant
  9. Xmind - win10系统安装 Xmind
  10. 微信公众号消息推送流