快速排序浅析

快速排序及其边界问题浅析

快速排序是一种采用分治思想的排序方法,在大部分时候拥有相当高的效率。下面介绍其思路,并给出几个常用代码模板和边界问题分析。

快速排序的思想

快速排序是一种采用分治思想的排序方法。在每一步递归当中,它会选中一枚元素作为 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=l1, 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 ab,则 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..a1]<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..a1]<pivot)(ab) ⇒ \Rarr q [ 1.. b − 1 ] < p i v o t q[1..b - 1]< pivot q[1..b1]<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,r1],证明如下:
若最终 b < l b < l b<l, 则由 a ≤ b a \le b ab a < l a < l a<l,但经过第一轮循环(必定会发生)有 a ≥ l a \ge l al,矛盾!故 b ≥ l b\ge l bl
若最终 b ≥ r b \ge r br,又最终 b ≤ r b \le r br,故而 b = r b=r b=r.故在这个情况下整个过程只经过一次外部循环
终止时 a ≥ b a \ge b ab;考虑 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=ra,而终止时 a ≥ b a \ge b ab,则 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..r1]<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 br1, 从而 b ∈ [ l , r − 1 ]   □ b\in [l, r-1]\ \square b[l,r1] 

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. 数组从第二个开始严格递减)。其优点是指针并不会交错,边界处的问题非常清晰。


  1. 一点证明:
    { ⌈ 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=kn1+1x+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=22l+r=2l+r+1 ↩︎

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值