排序算法之三——快速排序

 像合并排序一样,快速排序也是基于分治策略。下面是对一个典型子数组A[p..r]快速排序的分治过程的三个步骤:

分解:将数组A[p..r]分解成两个(可能为空)子数组A[p..q-1]和A[q+1..r],使得A[p..q-1]中所有的元素都小于等于A[q],而A[q+1..r]中的所有元素都大于A[q]。下标q也在该过程中计算得到。(本过程有两个步骤:确定中枢轴和中枢轴最终的位置q,使得q之前的元素都小于等于中枢轴,q之后的元素都大于中枢轴,——因此q为中枢轴的最终位置);

解决:通过递归调用快速排序,对子数组A[p..q-1]和A[q+1..r]排序;

合并:因为两个子数组是就地排序的,将它们的合并不需要额外操作,整个数组A[p..r]已排序。

下面的过程实现了快速排序:

QUICKSORT(A, p, r)
    if p < r
        then q = PARTIITON(A, p, r)
        QUICKSORT(A, p, q-1)
        QUICKSORT(A, q+1, r)

 

为排序一个完整的数组A,最初的调用是QUICKSORT(A, 1, length(A)),这里假定数组的下标从1开始。

快速排序的关键是数组划分过程——PARTITION,它对应分治中分解步骤。下面展示PARTITION的几种不同实现方式。(有时,为了行文方便,将PARTITION内联到QUICKSORT过程内。)

一:以左边的元素为中枢轴,从左到右扫描

代码如下:

PARTIITON1 (A, p, r)
    q = p
    for i = [p+1, r]
        /*循-环不变式为a:A[p+1..q]   < A[p]
        &&             A[q+1..i-1] >= A[p]  */
        if(A[i] < A[p])
            swap(++q, i)
    swap(p, q)
    //A[p..q-1] < A[q] <= A[q+1..r]
循环不变式如下所示:
循环终止时得到:
交换A[p]和A[q]得到:
至此,数组以第一个元素为中枢轴,分为两个子数组A[p..q-1]和A[q+1..r],而中枢轴的最终位置确定为q。
注:《算法导论》中采用最右端的元素为中枢轴,从左往右扫描。
二,以最左边的元素为中枢轴,从右到左扫描
数组划分过程如下:
PARTIITON2 (A, p, r)
    q = r + 1
    for (i = u; p <= i; i--)
        /*循环不变式为:A[i+1..q-1]   < A[p]
        &&             A[q..r] >= A[p]  */
        if(t <= A[i])
            swap(--q, i)
    //A[p..q-1] < A[q] <= A[q+1..r]
 
循环不变式如下所示:
与方式一对比:这里从右到左扫描,且在循环不变式的过程中交换A[p]到它最终的位置。
将t作为岗哨我们可以在循环中去掉一个判断,代码如下:
PARTIITON3 (A, p, r)

q = i = r + 1

do

   while A[--i] < t

       //空循环,遇到大于等于t的元素则停止

       ;

   //将大于t的元素放到A[q..r]中

   swap(--q, i)

while r < q

//A[p..q-1] < A[q] <= A[q+1..r]

 
三、从两侧逼近
考虑这样一种异常情况:数组由n个相等的元素构成。这种情况下,插入排序表现很好——每个元素都在它最终的位置上,故只需要O(n)的时间;而PARTIITON1表现糟糕——需要运行n-1次PARTIITON过程,每次PARTIITON都只能剥离第一个元素,且要花费O(n)的时间,故一共需要O(n2)的时间。
我们可以通过两侧逼近,来避免这种情况。循环不变式如下所示:
下标i和j初始化为数组的两个边界极点。主循环中包含两个循环:第一个循环,i从左往右移动过比t小的元素,遇到比t大的元素则停止。第二个循环,j从右往左移动过比t大的元素,遇到比t小的元素则停止。主循环测试i、j是否已经交叉了,如果没交叉则交换i、j,继续循环。
代码如下:
void QUICKSORT4(A, p, r)
    if (r <= p)
        return
    t = A[p]
    i = p
    j = r + 1
    while true
        do
            i++
        while i <= r && A[i] < t

        do
            j--
        while t < A[j]

        //有交叉则退出主循环
        if i > j
            break
        //交换i,j使得循环不变式满足
        swap(i, j)
    //j指示的位置为t最终的位置
    swap(p, j)
    //A[p..j-1] < A[j] <= A[j+1..r]

    QUICKSORT4(A, p, j-1)
    QUICKSORT4(A, j+1, r)

 

从上面的代码可以看出:当数组有n个相等的元素时,数据划分部分进行了多次的交换,虽然交换次数增多了,但j的位置尽可能的靠近中间位置。不会再出现一个子数组包含0个元素,另一个子数组包含n-1个元素的情况(PARTIITON1会造成这种情况),时间复杂度接近O(nlgn)。

四、随机挑选中枢轴
目前为止,我们都是拿第一个元素作为中枢轴。考虑这种情况:数组已经排序了,如果仍然拿第一个元素作中枢轴,数组划分将围绕最小的元素、次小的元素,如此直到最后一个元素,时间复杂度接近O(n2)。如果我们随机挑选一个元素作为中枢轴,情况就会好转。我们可以通过交换A[p]与A[p..r]中的某个随机元素(代码其余部分不变)来达到以随机元素为中枢轴的目的。交换的代码示意如下:
 
swap(p, randint(p, r))
 
五、“聪明”的快速排序
在规模较小的数组上进行快速排序的效率是很低的(只有当数组规模较大时,快速排序的优势才体现出来),远不如插入排序的效率。为此,Bob Sedgewick发明了一种聪明的快速排序方法——当数组规模较小时,什么都不做。我们通过改变代码中的第一条语句来实现这个想法,如下:
if r – p < cutoff
	return
 
这里,cutoff是一个小整数,经验值为50。
当程序结束时,数组虽然没被完全排序,但是数组被分成很多规模较小的块,块内元素的顺序是随机的,但块与块之间的是有序的——块中元素都比左边块中的元素大,比右边块中的元素小。整个数组是几乎有序的,再调用插入排序就可以将整个数组排序。对整个数组的排序代码如下:
 QUICKSORT5(A, 1, n)
 //调用快速排序之后数组Á被分解成许多有序À的Ì小块,再调用插入排序使整个数组有序
 InsertSort3()
InsertSort3的实现请参考链接点击打开链接点击打开链接
当然,也可以在每个小块内部调用插入排序来使得小块内部元素有序。结合以上情况,得到QUICKSORT5,代码如下:
//改进后的插入排序
void INSERTSORT(A, p, r)
    for i = [p + 1, r]
        t = A[i]
            for (j = i; j > p && A[j - 1] > t; j--)
                A[j] = A[j -1]
        A[j] = t

//产生[p,r]内的随机下标
int RANDINT(p, r)
    return p + rand() % (r - p + 1)

//快速排序
void QUICKSORT5(A, p, r)
    if (r - p < cutoff)
        //也可以把插入排序移到快速排序之外,在那里插入排序从数组的第一个元素开始插入排序
        INSERTSORT(A, p, r)
        return
    swap(p, RANDINT(p, r))
    t = A[p]
    i = p
    j = r + 1
    while true
        do
            i++
        while i <= r && A[i] < t

        do
            j--
        while t < A[j]

        //有交叉则退出主循环
        if i > j
            break
        //交换i,j使得循环不变式满足
        swap(i, j)
    //j指示的位置为t最终的位置
    swap(p, j)
    //A[p..j-1] < A[j] <= A[j+1..r]

    QUICKSORT5(A, p, j-1)
    QUICKSORT5(A, j+1, r)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值