快速排序及其边界问题浅析
快速排序是一种采用分治思想的排序方法,在大部分时候拥有相当高的效率。下面介绍其思路,并给出几个常用代码模板和边界问题分析。
快速排序的思想
快速排序是一种采用分治思想的排序方法。在每一步递归当中,它会选中一枚元素作为 p i v o t pivot pivot(主元)。对于当前在递归栈中被操作的数组范围内的所有数,小于等于 p i v o t pivot pivot的元素都将分布在此范围左侧,大于等于 p i v o t pivot pivot的数组都将分布在此范围右侧。随后对左右两个区间再次重复操作。
b a s i c c a s e basic\ case basic case: 范围内没有数或者只有一个数,此时结束递归。
快速排序模板、算法正确性及边界问题
来自闫总的两个模板:
I.
void quick_sort(int q[], int l, int r){
if (l >= r){return;}
int pivot = q[(r + l) >> 1] //>>是右移运算符,相当于/2但是比前者比后者快(因为前者直接对位进行操作)
//-1 and +1 because the first and the last element need to be checked.
int a = l - 1, b = r + 1;
while (a < b){
do {a++;}while(q[a] < pivot);
do {b--}while(q[b] > pivot);
/* a might be greater than or equal to b when the two loops above end.
So the check is a must.
Otherwise, q[b] would be less than the pivot and q[a] would be greater than the pivot*/
if (a < b) {swap(q[a], q[b])}
// it is okay if you write (a <= b).
}
quick_sort(q, l, b), quick_sort(q, b + 1, r);
}
(1)此算法并不会卡在 q [ a ] ( q [ b ] ) ≠ p i v o t q[a] (q[b])\not=pivot q[a](q[b])=pivot 的情况不动,因为每次都是先加1或减1再判断( d o w h i l e 的 好 处 do\ while的好处 do while的好处)。但是写成 q [ a ] ( q [ b ] ) ≥ ( ≤ ) p i v o t q[a](q[b])\ge(\le)pivot q[a](q[b])≥(≤)pivot会寄——举个例子,假设元素全相等,然后还 > 0 >0 >0,指针 a a a就会疯狂往后跑。
(2)使用循环不变式证明算法正确性:
待证明问题:当每次最外层
w
h
i
l
e
while
while一轮循环结束时,
(
q
[
1..
a
]
≤
x
)
∧
(
q
[
b
.
.
r
]
≥
x
)
)
(q[1..a]\le x)\land(q[b..r]\ge x) )
(q[1..a]≤x)∧(q[b..r]≥x))
初始化:在循环开始之前, a = l − 1 , b = r + 1 a=l-1,\ b=r+1 a=l−1, b=r+1,不变式显然成立。
保持:若一轮循环开始前不变式成立,执行循环:
//这个循环保证q[l..a - 1] < pivot, q[a] >= pivot.
do {a++;}while(q[a] < pivot);
//这个循环保证q[b + 1..r] > pivot, q[b] <= pivot.
do {b--}while(q[b] > pivot);
if (a < b) {swap(q[a], q[b])}
c a s e 1 case\ 1 case 1: 若在两次内部循环之后,有 a < b a < b a<b,则通过 s w a p ( q [ a ] , q [ b ] ) swap(q[a],q[b]) swap(q[a],q[b]),有 ( q [ 1.. a ] ≤ p i v o t ) ∧ ( q [ b . . r ] ) ≥ p i v o t ) (q[1..a]\le pivot)\land (q[b..r])\ge pivot) (q[1..a]≤pivot)∧(q[b..r])≥pivot),保持循环不变式
c a s e 2 case\ 2 case 2: 若 a ≥ b a \ge b a≥b,则 i f if if语句不会执行(此时对应最后一轮循环),此时有:
{ q [ 1.. a − 1 ] < p i v o t q [ b + 1.. r ] > p i v o t q [ b ] ≤ p i v o t q [ a ] ≥ p i v o t \begin{cases} q[1..a - 1]< pivot \cr q[b + 1..r]> pivot \cr q[b] \le pivot \cr q[a] \ge pivot \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧q[1..a−1]<pivotq[b+1..r]>pivotq[b]≤pivotq[a]≥pivot
显然在这个情况下,只要 q [ a ] > p i v o t q[a] > pivot q[a]>pivot 或者 q [ b ] < p i v o t q[b] < pivot q[b]<pivot ,循环不变式就不再保持了。故对于最后一轮循环,其开始之前循环不变式成立,但其结束之后循环不变式可能不再保持。
终止:虽然说循环不变式可能只持续到倒数第二轮循环结束,但是其依旧能“为我们提供一个有用的性质,且该性质有助于证明该算法是正确的”。证明如下:
(
q
[
1..
a
−
1
]
<
p
i
v
o
t
)
∧
(
a
≥
b
)
(q[1..a - 1]< pivot) \land (a \ge b)
(q[1..a−1]<pivot)∧(a≥b)
⇒
\Rarr
⇒
q
[
1..
b
−
1
]
<
p
i
v
o
t
q[1..b - 1]< pivot
q[1..b−1]<pivot,
q
[
b
]
≤
p
i
v
o
t
q[b] \le pivot
q[b]≤pivot
⇒
\Rarr
⇒
q
[
1..
b
]
≤
p
i
v
o
t
□
q[1..b]\le pivot \ \square
q[1..b]≤pivot □
(3)不能写成quick_sort(q, l, b - 1), quick_sort(q, b, r)
! 一方面,这是因为可能
q
[
b
]
<
p
i
v
o
t
q[b] < pivot
q[b]<pivot, 不满足递归条件。另一方面,若
p
i
v
o
t
pivot
pivot取到了
q
[
l
]
q[l]
q[l](注意,即使pivot写成
q
[
(
l
+
r
)
>
>
1
]
q[(l+r)>>1]
q[(l+r)>>1]也是有可能取到的,比方说当范围内只有两个数的情况下就会取到)且之后的数全面小于
q
[
l
]
q[l]
q[l],则
b
b
b会一直走到
l
l
l停下,造成0/n划分从而无限循环。
(4) p i v o t pivot pivot不能取 q [ r ] q[r] q[r]。e.g.若之前的所有数都小于 q [ r ] q[r] q[r],则会造成 b = r b = r b=r,从而造成无限循环。
(4)以上这种模板绝不会造成0/n划分,因为
b
b
b的取值范围是
[
l
,
r
−
1
]
[l, r -1]
[l,r−1],证明如下:
若最终
b
<
l
b < l
b<l, 则由
a
≤
b
a \le b
a≤b 有
a
<
l
a < l
a<l,但经过第一轮循环(必定会发生)有
a
≥
l
a \ge l
a≥l,矛盾!故
b
≥
l
b\ge l
b≥l;
若最终
b
≥
r
b \ge r
b≥r,又最终
b
≤
r
b \le r
b≤r,故而
b
=
r
b=r
b=r.故在这个情况下整个过程只经过一次外部循环。
终止时
a
≥
b
a \ge b
a≥b;考虑
a
a
a的上界,当数组中除了
p
i
v
o
t
pivot
pivot本身以外所有数都小于
p
i
v
o
t
pivot
pivot时,
a
a
a可取到上界
r
r
r,可见
b
=
r
≥
a
b = r \ge a
b=r≥a,而终止时
a
≥
b
a \ge b
a≥b,则
a
=
b
=
r
a = b=r
a=b=r。
故
q
[
l
.
.
r
−
1
]
<
p
i
v
o
t
q[l..r-1] < pivot
q[l..r−1]<pivot,这与
p
i
v
o
t
=
q
[
(
l
+
r
)
>
>
1
]
pivot = q[(l+r)>>1]
pivot=q[(l+r)>>1]矛盾。(对于任意非
b
a
s
i
c
c
a
s
e
basic\ case
basic case的递归,有
l
+
r
2
<
r
\dfrac{l+r}{2} <r
2l+r<r)
故
b
≤
r
−
1
b\le r-1
b≤r−1, 从而
b
∈
[
l
,
r
−
1
]
□
b\in [l, r-1]\ \square
b∈[l,r−1] □
II.
void quick_sort(int q[], int l, int r){
if (l >= r){return;}
int pivot = q[(l + r + 1) >> 1]
int a = l - 1, b = r + 1;
while (a < b){
do {a++} while(q[a] < pivot);
do {b--} while(q[b] > pivot);
if (a < b){swap(q[a], q[b];}
}
quick_sort(q, l, a - 1), quick_sort(q, a, r);
}
这是用
a
a
a做边界的模板。
(1)注意
p
i
v
o
t
pivot
pivot处是向上取整1,因为向下取整会取到
q
[
l
]
q[l]
q[l],从而无限循环。
(2)不能写成quick_sort(q, l, a), quick_sort(q, a + 1, r);
分析同上一个模板。
III. 一种没那么多比事的模板:
def quick_sort2(userinput: list, left_index: int, right_index: int):
if left_index >= right_index:
return None
pivot = userinput[left_index]
# define a pointer.
swap = left_index
for i in range(left_index + 1, right_index + 1):
if userinput[i] <= pivot:
swap += 1
userinput[swap], userinput[i] = userinput[i], userinput[swap]
userinput[swap], userinput[left_index] = userinput[left_index], userinput[swap]
quick_sort2(userinput, left_index, swap - 1)
quick_sort2(userinput, swap + 1, right_index)
这是一种单向循环实现的快速排序,在有些时候会效率低下(e.g. 数组从第二个开始严格递减)。其优点是指针并不会交错,边界处的问题非常清晰。
一点证明:
{ ⌈ n k ⌉ = ⌊ n − 1 k ⌋ + 1 ⌊ x ⌋ + n = ⌊ x + n ⌋ \begin{cases} \lceil \dfrac{n}{k} \rceil= \lfloor \dfrac{n-1}{k}\rfloor +1 \cr \lfloor x\rfloor +n= \lfloor x+n \rfloor \end{cases} ⎩⎨⎧⌈kn⌉=⌊kn−1⌋+1⌊x⌋+n=⌊x+n⌋ 取 n = l + r , k = 2 ⇒ ⌈ l + r 2 ⌉ = ⌊ l + r + 1 2 ⌋ n= l+r,k=2 \Rarr \lceil \dfrac{l+r}{2}\rceil=\lfloor\dfrac{l+r+1}{2}\rfloor n=l+r,k=2⇒⌈2l+r⌉=⌊2l+r+1⌋ ↩︎