快速排序中从j往前遍历的原因分析

快速排序中从j往前遍历的原因分析

上一篇博文中分析了归并排序、快速排序和快速选择算法的过程已经复杂度,其中有一个点让我产生了思考。在快速排序中,以a[l]为轴,内层两个循环先移动j指针,再移动i指针;而交换则会导致结果错误?

// 轴为 a[l]
while(i < j) {
    while(i < j && a[j] >= a[l]) j--;  // 不能与下一行交换顺序 
    while(i < j && a[i] <= a[l]) i++;  
    swap(a[i], a[j]);    // 交换这两个不在正确区域的数
}
swap(a[i], a[l]);  // 将轴放在正确的位置上

引入

我们知道,上述快速排序算法每次执行完交换轴的操作后,会将区间分为三部分:小于等于轴的部分、轴、大于等于轴的部分。如下图所示,经过交换轴后,数组被分为了绿色(小于等于轴)、红色(轴)、蓝色(大于等于轴)部分。这很合理很正常,他本该如此。

在这里插入图片描述

但其实可能你忽略了一个条件,能够这么交换的前提是:a[i]是绿色(即a[i]是小于等于a[l]的)。如果不满足这个条件,结果将会变成下图,显然不是期望的结果:

在这里插入图片描述

你可能会想,对哦,好像确实是这样,但我怎么没遇到也没想过呢?因为你代码是对的😆,他达成了这一前提。如果你使用的是开头的代码,那么他会给你上上图的结果;如果你将代码内层两个循环的顺序交换,那么他将出现上图的结果。例如下图所示,数组[4,2,1,6,7,8],在不同顺序情况下的不同结果:

在这里插入图片描述

分析

通过上述内容,我们知道了,先j后i的顺序,保证了ij最后汇合在一个小于等于a[l]的值,这也就保证了swap(a[i], a[l])后的整体有序性。所以我们接下来分析,为什么每次交换前,ij总能交汇在小于等于a[l]的位置。(弄清楚后就可以灵活变化了,而不是只会写模板,改动不了一点)

// 轴为 a[l]
while(i < j) {
    while(i < j && a[j] >= a[l]) j--;  // 不能与下一行交换顺序 
    while(i < j && a[i] <= a[l]) i++;  
    swap(a[i], a[j]);    // 交换这两个不在正确区域的数
}
swap(a[i], a[l]);  // 将轴放在正确的位置上

我们首先可以得出几条前提信息:

  • a[l]轴,则初始时a[i] == a[l]a[j] ? a[l]a[j]a[l]的关系不明)
  • 内层循环j走过的部分[j + 1, r]有:a[j + 1, r] >= a[l]
  • 内层循环i走过的部分[l, r - 1]有:a[l, i - 1] <= a[l]
  • 内层循环j没走到i(即走了一部分,没走完),则有a[j] < a[l];同理内层循环i没走到j,则有a[i] > a[l]

情况一

那么,对于先ji的代码,每次外层循环可以分以下情况讨论,如下图所示:

  1. 直接从j走到i([4,5,6,7]),则有i == j, a[i] <= a[l];所以交换前有a[i] <= a[l](小于是因为第3条,等于是在第一次循环开始时)。

  2. j走一部分,i走到j([4,1,2,6,7]),则有i == j, a[j] < a[l];所以交换前有a[i] < a[l]

  3. j走一部分,i走一部分([4,6,2,7]),则有i != j, a[i] > a[l], a[j] < a[l];所以交换前有a[i] > a[l] && a[j] < a[l],交换后有a[i] < a[l] && a[j] > a[l]

    交换后又是外层循环新的开始,且比开局的条件(a[i] == a[l] && a[j] ? a[l])更好,因为他确认了a[j]的范围(a[i] < a[l] && a[j] > a[l])。

3条是中间情况。通过第1,2条可知,每当i == j时(交汇),一定有a[i] <= a[l]。原因就在于当取a[l]为轴且j先行时,i一定会停在一个小于等于a[l]的数。

情况二

但是,对于先ij的代码,每次外层循环可以分以下情况讨论,如下图所示:

  1. 直接从i走到j([4,1,2,6]),则有i == j,a[i] ? a[l];所以交换前有a[i] ? a[l]关系不明)。
  2. i走一部分,j走到i([4,1,5,6,7]),则有i == j, a[i] > a[l];所以交换前有a[i] > a[l]
  3. i走一部分,j走一部分([4,6,2,7]),则有i != j, a[i] > a[l], a[j] < a[l];所以交换前有a[i] > a[l] && a[j] < a[l],交换后有a[i] < a[l] && a[j] > a[l]

与情况一相似,但是通过1,2条可知,每当i == j时(交汇时),a[i]a[l]的大小情况是不明的,不能直接交换轴。所在以a[l]为轴时,以先ij的方式是不行的。

思考

那么先ij的方式是不是就完全没有可行办法呢?

也不是,在情况一中,我们是通过1,2,3条确定了某个变量的范围,那么在先ij的方式中能不能确定某个变量的范围呢?

我们需要三个条件共同确定一个条件,显然情况二中2a[i] > a[l]是与情况二中3a[i] < a[l]冲突的。但是情况二中2ij是相等的呀,即我们可以通过情况二2,3确定一个条件:a[j] > a[l]。但是怎么通过第一条得出a[j]a[l]的关系呢(情况二中1ij也是相等的)?参考情况一,根据轴来确定,只要选取a[r]为轴,即可满足初始时a[j] == a[r],且将本段中的l无痛换成r即可得出a[j] >= a[r]

所以在先ij的写法中,只需要将a[r]作为轴即可。即当取a[r]为轴且i先行时,ij总会交汇在一个a[i/j] >= a[r]的位置。原因就在于,j一定会停在一个大于等于a[r]的数。

在这里插入图片描述

修改核心代码如下:

while(i < j) {
    while(i < j && a[i] <= a[r]) i++;    // 先i后j
    while(i < j && a[j] >= a[r]) j--;

    swap(a[i], a[j]);
}
swap(a[i], a[r]);   // 将本行的以及两个内循环中的l改成r
}
  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值