希尔排序时间复杂度证明

希尔排序,思想简单,代码容易,但内核丰富,时间复杂度证明富含数学知识,在此记录。

先来一份希尔排序算法的伪代码:

希尔排序的基础是插入排序,其正确性也由插入排序导出。我们只需要保证集合 HH 中最后一个元素为 11,则相当于最后执行了一次正常插入排序,即可保证整个序列有序。

插入排序的时间复杂度与上述伪代码中每个 jj 的 ii 的移动次数有关,而希尔排序的中心思想便是通过预先选定若干个 hh 执行 InsertionSort(h)\text{InsertionSort}(h),使得数组变得部分有序,让后面的 InsertionSort\text{InsertionSort} 函数 ii 指针的移动次数量级可被控制住,从而得到比插入排序更快的 o(n2)o(n^2) 的排序算法。

从上文分析可以看出,希尔排序的时间复杂度,与集合 HH 的选取密切相关。下面给出集合 HH 的两种经典选取方式,这两种选取方式均使得排序算法的复杂度降为 o(n2)o(n^2) 级别。

命题 11:若集合 H={2k−1∣k=⌊log⁡2n⌋,…,1}H=\{2^k-1\mid k=\lfloor\log_2 n\rfloor,\ldots,1 \},则希尔排序算法的时间复杂度为 O(n3/2)O(n^{3/2})。

命题 22:若集合 H={k=2p⋅3q∣p,q∈N,k≤n}H=\{k=2^p\cdot 3^q\mid p,q\in \mathbb N,k\le n\},则希尔排序算法的时间复杂度为 O(nlog⁡2n)O(n\log^2 n)。


为证明这两个命题,我们先给出一个重要的定理并证明它,这个定理反应了希尔排序的最主要特征。

定理 11(我喜欢称其为 “跳跃保序定理”):只要程序执行了一次 InsertionSort(h)\text{InsertionSort}(h),不管之后怎样调用 InsertionSort\text{InsertionSort} 函数,AA 数组怎样变换,下列性质均会被一直保持:

A[1],A[1+h],A[1+2h],…A[2],A[2+h],A[2+2h],…⋮A[h],A[h+h],A[h+2h],…A[1],A[1+h],A[1+2h],\ldots\\ A[2],A[2+h],A[2+2h],\ldots\\ \vdots\\ A[h],A[h+h],A[h+2h],\ldots

证明:

我们先证明一个引理:

引理 11:对于整数 n,mn,m、正整数 ll 与两个数组 X(x1,x2,…,xn+l),Y(y1,y2,…,ym+l)X(x_1,x_2,\ldots,x_{n+l}),Y(y_1,y_2,\ldots,y_{m+l}),满足如下要求:

y1≤xn+1,y2≤xn+2,…,yl≤xn+ly_1\le x_{n+1},y_2\le x_{n+2},\ldots,y_l\le x_{n+l}

则我们将两个数组分别升序排序后,上述要求依然成立。

证明:

设数组 XX 排序完为数组 X′(x1′,…,xn+l′)X'(x'_1,\ldots,x'_{n+l}),数组 YY 排序完为数组 Y′(y1′,…,ym+l′)Y'(y'_1,\ldots,y'_{m+l})。

对于任何 1≤i≤l1\le i\le l,xn+i′x'_{n+i} 小等于数组 X′X' 中的 l−il-i 个元素,也小等于数组 XX 中的 l−il-i 个元素(这是因为 XX 与 X′X' 的元素可重集合是相同的)。

那么在可重集合 {xn+1,…,xn+l}⊂X\{x_{n+1},\ldots,x_{n+l} \} \subset X 中,大等于 xn+i′x'_{n+i} 的元素个数不超过 l−il-i 个。

进而小于 xn+i′x'_{n+i} 的元素个数至少有 ii 个,取出其中的 ii 个,设它们为 xn+k1,xn+k2,…,xn+kix_{n+k_1},x_{n+k_2},\ldots,x_{n+k_i}。于是有:

yk1≤xn+k1≤xn+i′,yk2≤xn+k2≤xn+i′,…,yki≤xn+ki≤xn+i′y_{k_1}\le x_{n+k_1}\le x'_{n+i},y_{k_2}\le x_{n+k_2}\le x'_{n+i},\ldots,y_{k_i}\le x_{n+k_i}\le x'_{n+i}

所以 xn+i′x'_{n+i} 至少大等于 YY 也即 Y′Y' 中的 ii 个元素,那么自然有 yi′≤xn+i′ (1≤i≤l)y'_i\le x'_{n+i}\,(1\le i\le l)。

■\blacksquare

再回到原命题的证明:

我们实际上只需要证明调用完 InsertionSort(h)\text{InsertionSort}(h) 的紧接着下一次调用 InsertionSort(k)\text{InsertionSort}(k) 后,hh 个子列仍有序即可,之后容易用归纳法得出。下面只考虑下一个调用:

执行完 InsertionSort(h)\text{InsertionSort}(h) 后,如下组已经完成排序:

A[1],A[1+h],A[1+2h],…A[2],A[2+h],A[2+2h],…⋮A[h],A[h+h],A[h+2h],…A[1],A[1+h],A[1+2h],\ldots\\ A[2],A[2+h],A[2+2h],\ldots\\ \vdots\\ A[h],A[h+h],A[h+2h],\ldots

而之后执行 InsertionSort(k)\text{InsertionSort}(k),则会将如下组排序:

A[1],A[1+k],A[1+2k],…A[2],A[2+k],A[2+2k],…⋮A[k],A[k+k],A[k−1+2k],…A[1],A[1+k],A[1+2k],\ldots\\ A[2],A[2+k],A[2+2k],\ldots\\ \vdots\\ A[k],A[k+k],A[k-1+2k],\ldots

对于每个 i (1≤i≤min⁡(h,k))i\,(1\le i\le \min(h,k)),考虑如下两个组:

A[i],A[i+k],A[i+2k],……,A[i+h],A[i+h+k],A[i+h+2k],…A[i],A[i+k],A[i+2k],\ldots\\ \ldots,A[i+h],A[i+h+k],A[i+h+2k],\ldots

第二个组前面也加上 “…\ldots” 的原因是可能 i+h>ki+h> k 从而前面也有元素。

则第二个组就是引理 11 中的 XX 数组,第一个组就是 YY 数组,ll 就是第二个组从 i+hi+h 之后顶到末尾的长度,nn 是第二个组中前面那个 “…\ldots” 的长度,mm 是第一个组去掉前 ll 个后剩下的个数。

又因为有:

A[i]≤A[i+h],A[i+k]≤A[i+h+k],…A[i]\le A[i+h],A[i+k]\le A[i+h+k],\ldots

所以由引理 11 可得执行 InsertionSort(k)\text{InsertionSort}(k) 将两个组分别排序后,这个关系依然满足,即依然有 A[i]≤A[i+h] (1≤i≤min⁡(h,k))A[i]\le A[i+h]\,(1\le i\le \min(h,k))。

若有 i>min⁡(h,k)i> \min(h,k),容易发现取正整数 w (1≤w≤min⁡(h,k))w\,(1\le w\le \min(h,k)) 再加上若干个 kk 即可得到 ii,则之前的情况已经蕴含了此情况的证明。

综合以上论述便有:执行完 InsertionSort(k)\text{InsertionSort}(k) 依然有 A[i]≤A[i+h] (1≤i≤n−h)A[i]\le A[i+h]\,(1\le i\le n-h)。

得证。

□\Box

这个定理揭示了希尔排序在特定集合 HH 下可以优化复杂度的关键,因为在整个过程中,它可以一致保持前面的成果不被摧毁(即 hh 个子列分别有序),从而使后面的调用中,指针 ii 的移动次数大大减少。

接下来我们单拎出来一个数论引理进行证明。这个定理在 OI 界因小凯的疑惑一题而大为出名。而在希尔排序复杂度的证明中,它也使得跳跃保序定理得到了很大的扩展。

引理 22(我喜欢称其为 “小凯” 定理):若 a,ba,b 均为正整数且互素,则不在集合 {ax+by∣x,y∈N}\{ax+by\mid x,y\in \mathbb N \} 中的最大正整数为 ab−a−bab-a-b。

证明:

分两步证明:

  • 先证明方程 ax+by=ab−a−bax+by=ab-a-b 没有 x,yx,y 均为非负整数的解:

    若无非负整数的限制,容易得到两组解 (b−1,−1),(−1,a−1)(b-1,-1),(-1,a-1)。

    通过其通解形式 x=x0+tb,y=y0−tax=x_0+tb,y=y_0-ta,容易得到上面两组解是 “相邻” 的(因为 b−1−b=−1b-1-b=-1)。

    当 tt 递增时,xx 递增,yy 递减,所以如果方程有非负整数解,必然会夹在这两组解中间,但这两组解 “相邻”,中间没有别的解。

    故不可能有非负整数解。

  • 再证明对任意整数 c>ab−a−bc>ab-a-b,方程 ax+by=cax+by=c 有非负整数解:

    我们找一组解 (x0,y0)(x_0,y_0) 满足 0≤x0<b0\le x_0<b(由通解的表达式,这可以做到)。

    则有:

    by0=c−ax0≥c−a(b−1)>ab−a−b−ab+a=−bby_0=c-ax_0\ge c-a(b-1)>ab-a-b-ab+a=-b

    所以 b(y0+1)>0b(y_0+1)>0,又因为 b>0b>0,所以 y0+1>0y_0+1>0,所以 y0≥0y_0\ge 0。

    所以 (x0,y0)(x_0,y_0) 为一组非负整数解。

综上得证。

■\blacksquare

而下面这个定理则揭示了 “小凯” 定理是如何扩展跳跃保序定理的。

定理 22:如果 gcd⁡(ht+1,ht)=1\gcd(h_{t+1},h_t)=1,则程序先执行完 InsertionSort(ht+1)\text{InsertionSort}(h_{t+1}) 与 InsertionSort(ht)\text{InsertionSort}(h_t) 后,执行 InsertionSort(ht−1)\text{InsertionSort}(h_{t-1}) 的时间复杂度为 O(nht+1htht−1)O\left(\dfrac{nh_{t+1}h_t}{h_{t-1}} \right),且对于每个 jj,其 ii 的移动次数是 O(ht+1htht−1)O\left(\dfrac{h_{t+1}h_t}{h_{t-1}} \right) 级别的。

证明:

对于 j≤ht+1htj\le h_{t+1}h_t 的部分,ii 的移动次数显然是是 O(ht+1htht−1)O\left(\dfrac{h_{t+1}h_t}{h_{t-1}} \right) 级别的。

故以下假设 j>ht+1htj>h_{t+1}h_t。

对于任意的正整数 kk 满足 1≤k≤j−ht+1ht1\le k\le j-h_{t+1}h_t,注意到:

ht+1ht−ht+1−ht<ht+1ht≤j−k≤j−1h_{t+1}h_t-h_{t+1}-h_t<h_{t+1}h_t\le j-k\le j-1

又因为 gcd⁡(ht+1,ht)=1\gcd(h_{t+1},h_t)=1,故由引理 22,得存在非负整数 a,ba,b,使得:

aht+1+bht=j−kah_{t+1}+bh_t=j-k

即得:

k=j−aht+1−bhtk=j-ah_{t+1}-bh_t

由跳跃保序定理,得:

A[j−bht]≤A[j−(b−1)ht]≤…≤A[j−ht]≤A[j]A[j-bh_t]\le A[j-(b-1)h_t]\le \ldots\le A[j-h_t]\le A[j]

与

A[j−bht−aht+1]≤A[j−bht−(a−1)ht+1]≤…≤A[j−bht−ht+1]≤A[j−bht]A[j-bh_t-ah_{t+1}]\le A[j-bh_t-(a-1)h_{t+1}]\le \ldots\le A[j-bh_t-h_{t+1}]\le A[j-bh_t]

综合以上既有:A[k]=A[j−aht+1−bht]≤A[j]A[k]=A[j-ah_{t+1}-bh_t]\le A[j]。

所以对于任何 1≤k≤j−ht+1ht1\le k\le j-h_{t+1}h_t,有 A[k]≤A[j]A[k]\le A[j]。

在 Shell-Sort 伪代码中 ii 指针每次减 ht−1h_{t-1},减 O(ht+1htht−1)O\left(\dfrac{h_{t+1}h_t}{h_{t-1}} \right) 次,即可使得 i≤j−ht+1hti\le j-h_{t+1}h_t,进而有 A[i]≤A[j]A[i]\le A[j],不满足 while 循环的条件退出。

证明完对于每个 jj 的移动复杂度后,即可得到总的时间复杂度:

∑j=ht−1+1nO(ht+1htht−1)=O(nht+1htht−1)\sum_{j=h_{t-1}+1}^n{O\left(\frac{h_{t+1}h_t}{h_{t-1}} \right)}=O\left(\frac{nh_{t+1}h_t}{h_{t-1}}\right)

得证。

□\Box

认真观察定理 22 的证明过程,可以发现:跳跃保序定理可以进行 “线性组合”,即 AA 以 hh 为间隔有序,以 kk 为间隔亦有序,则以 hh 和 kk 的非负系数线性组合仍是有序的。而这种 “线性性” 即是由 “小凯” 定理保证的。


有了这两个定理,我们可以证明开头的两个命题。

先证明命题 11:

证明:

将 HH 写为序列的形式:

H(h1=1,h2=3,h3=7,…,h⌊log⁡2n⌋=2⌊log⁡2n⌋−1)H(h_1=1,h_2=3,h_3=7,\ldots,h_{\lfloor \log_2 n\rfloor}=2^{\lfloor \log_2 n\rfloor}-1)

Shell-Sort 执行顺序为:InsertionSort(h⌊log⁡2n⌋),InsertionSort(h⌊log⁡2n⌋−1),…,InsertionSort(h2),\text{InsertionSort}(h_{\lfloor \log_2 n\rfloor}),\text{InsertionSort}(h_{\lfloor \log_2 n\rfloor-1}),\ldots,\text{InsertionSort}(h_2),
InsertionSort(h1)\text{InsertionSort}(h_1).

分两部分去分析复杂度:

  • 对于前面的若干个满足 ht≥nh_t\ge \sqrt{n} 的 hth_t,显然有 InsertionSort(ht)\text{InsertionSort}(h_t) 的时间复杂度为 O(n2ht)O\left(\dfrac{n^2}{h_t} \right)。

    考虑对最接近 n\sqrt{n} 的项 hkh_k,有:

    O(n2ht)=O(n3/2)O\left(\dfrac{n^2}{h_t} \right)=O(n^{3/2})

    而对于 i>ki>k 的 hih_i,因为有 2hi<hi+12h_i<h_{i+1},所以可得:

    O(n2hi)=O(n3/2/2i−k) (i>k)O\left(\dfrac{n^2}{h_i} \right)=O(n^{3/2}/2^{i-k})\,(i>k)

    所以大等于 n\sqrt n 部分的总时间复杂度为:

    ∑i=k⌊log⁡2n⌋O(n3/2/2i−k)=O(n3/2)\sum_{i=k}^{\lfloor \log_2 n\rfloor}{O(n^{3/2}/2^{i-k})}=O(n^{3/2})

  • 对于后面剩下的满足 ht<nh_t< \sqrt{n} 的项,前两项的复杂度还是 O(n3/2)O(n^{3/2}),而对于后面的项 hth_{t},有定理 22 可得时间复杂度为:

    O(nht+2ht+1ht)=O(nht+2⋅ht+2/2ht+2/4)=O(nht+2)O\left(\frac{nh_{t+2}h_{t+1}}{h_t} \right)=O\left(\frac{nh_{t+2}\cdot h_{t+2}/2}{h_{t+2}/4} \right)=O(nh_{t+2})

    再次利用 2hi<hi+12h_i<h_{i+1} 性质可得此部分总时间复杂度为(下式中 kk 沿用了上一种情况中的含义):

    2O(n3/2)+∑i=1k−3O(nhi+1)=O(n3/2)+∑i=1k−3O(nhk−1/2k−i−3)=O(n3/2)+O(nhk−1)=O(n3/2)2O(n^{3/2})+\sum_{i=1}^{k-3}{O(nh_{i+1})}=O(n^{3/2})+\sum_{i=1}^{k-3}{O(nh_{k-1}/2^{k-i-3})}=O(n^{3/2})+O(nh_{k-1})=O(n^{3/2})

综上可得总时间复杂度即为 O(n3/2)O(n^{3/2})。

□\Box

再证明命题 22:

证明:

注意到一个事实:如果已经执行过了 InsertionSort(2)\text{InsertionSort}(2) 与 InsertionSort(3)\text{InsertionSort}(3),那么因为 2⋅3−2−3=12\cdot 3-2-3=1,所以由定理 22,每个元素只有与它相邻的前一个元素可能大于它,之前的元素全部都小于它。于是 ii 指针只需要最多两次就可以退出 while 循环。也就是说,此时再执行 InsertionSort(1)\text{InsertionSort}(1),复杂度降为 O(n)O(n)。

更进一步:如果已经执行过了 InsertionSort(4)\text{InsertionSort}(4) 与 InsertionSort(6)\text{InsertionSort}(6),我们考虑所有的下标为奇数的元素组成的子列与下标为偶数的元素组成的子列。则执行这两个函数相当于把这两个子列分别执行 InsertionSort(2)\text{InsertionSort}(2) 与 InsertionSort(3)\text{InsertionSort}(3)。那么也是一样,这时候再执行 InsertionSort(2)\text{InsertionSort}(2),相当于对两个子列分别执行 InsertionSort(1)\text{InsertionSort}(1),也只需要两个序列和级别,即 O(n)O(n) 的复杂度就可以将数组变为 22 间隔有序。

不断归纳,就可以得到:如果已经执行过了 InsertionSort(2h)\text{InsertionSort}(2h) 与 InsertionSort(3h)\text{InsertionSort}(3h),则执行 InsertionSort(h)\text{InsertionSort}(h) 的复杂度只有 O(n)O(n)。

接下来分为两部分分析复杂度:

  • 对于 ht>n/3h_t>n/3 的部分,则执行每个 InsertionSort(ht)\text{InsertionSort}(h_t) 的复杂度为 O(n2/ht)O(n^2/h_t)。

    而 n2/ht<3nn^2/h_t<3n,所以单词插入排序复杂度为 O(n)O(n)。

    而这一部分元素个数是 O(log⁡2n)O(\log^2 n) 级别的,所以这一部分时间复杂度为 O(nlog⁡2n)O(n\log^2 n)。

  • 对于 ht≤n/3h_t\le n/3 的部分,因为 3ht≤n3h_t\le n,所以这之前已经执行了 InsertionSort(2ht)\text{InsertionSort}(2h_t) 与 InsertionSort(3ht)\text{InsertionSort}(3h_t),于是执行 InsertionSort(ht)\text{InsertionSort}(h_t) 是 O(n)O(n)。
    还是一样的,这一部分元素个数也是 O(log⁡2n)O(\log^2 n) 级别的,所以这一部分时间复杂度为 O(nlog⁡2n)O(n\log^2 n)。

综上可得总时间复杂度即为 O(nlog⁡2n)O(n\log^2 n)。

□\Box