快速排序中从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的顺序,保证了i
和j
最后汇合在一个小于等于a[l]
的值,这也就保证了swap(a[i], a[l])
后的整体有序性。所以我们接下来分析,为什么每次交换前,i
和j
总能交汇在小于等于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]
情况一
那么,对于先j
后i
的代码,每次外层循环可以分以下情况讨论,如下图所示:
-
直接从
j
走到i
([4,5,6,7]),则有i == j, a[i] <= a[l]
;所以交换前有a[i] <= a[l]
(小于是因为第3
条,等于是在第一次循环开始时)。 -
j
走一部分,i
走到j
([4,1,2,6,7]),则有i == j, a[j] < a[l]
;所以交换前有a[i] < a[l]
。 -
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]
的数。
情况二
但是,对于先i
后j
的代码,每次外层循环可以分以下情况讨论,如下图所示:
- 直接从
i
走到j
([4,1,2,6]),则有i == j,a[i] ? a[l]
;所以交换前有a[i] ? a[l]
(关系不明)。 i
走一部分,j
走到i
([4,1,5,6,7]),则有i == j, a[i] > a[l]
;所以交换前有a[i] > a[l]
。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]
为轴时,以先i
后j
的方式是不行的。
思考
那么先i
后j
的方式是不是就完全没有可行办法呢?
也不是,在情况一中,我们是通过1,2,3
条确定了某个变量的范围,那么在先i
后j
的方式中能不能确定某个变量的范围呢?
我们需要三个条件共同确定一个条件,显然情况二中2
的a[i] > a[l]
是与情况二中3
的a[i] < a[l]
冲突的。但是情况二中2
的i
与j
是相等的呀,即我们可以通过情况二2,3
确定一个条件:a[j] > a[l]
。但是怎么通过第一条得出a[j]
与a[l]
的关系呢(情况二中1
的i
与j
也是相等的)?参考情况一,根据轴来确定,只要选取a[r]
为轴,即可满足初始时a[j] == a[r]
,且将本段中的l
无痛换成r
即可得出a[j] >= a[r]
。
所以在先i
后j
的写法中,只需要将a[r]
作为轴即可。即当取a[r]
为轴且i
先行时,i
与j
总会交汇在一个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
}