分治算法
1、数学归纳法
在学习分治算法之前,我们先来了解一下该技术的源头——数学归纳法。
数学归纳法(
M
a
t
h
e
m
a
t
i
c
a
l
Mathematical
Mathematical
I
n
d
u
c
t
i
o
n
Induction
Induction,
M
I
MI
MI),是一种数学证明方法,通常被用于证明某个给定命题在整个(或者局部)自然数范围内成立。通常,数学归纳法的证明过程如下:
- 证明 n = 1 n=1 n=1 时命题成立;
- 假设 n = m n=m n=m 时命题成立(归纳假设);
- 根据归纳假设推导出 n = m + 1 n=m+1 n=m+1 时命题也成立(m代表任意自然数)
方法原理:
首先证明在某个起点值时命题成立,然后证明从一个值到下一个值的过程有效。当这两点都已经证明,那么任意值都可以通过反复使用这个方法推导出来。这种思想实际上几乎是所有递归算法的根据,假设现在有一个带有参数
n
n
n 的问题,我们需要用归纳法设计出一个能够解决问题的有效算法。此时,如果我们知道如何求解带有参数小于
n
n
n (如
n
−
1
,
n
/
2
n-1,\space n/2
n−1, n/2 等)的相同问题(上述证明过程中的归纳假设),那么我们设计算法的主要思路就是如何将已知解法扩展到带有参数
n
n
n 的情况。
2、分治算法原理
分治算法,顾名思义,就是“分而治之”,即将一个问题划分成若干子问题,并分别递归地解决每一个子问题,然后把这些子问题的解组合起来,构成原问题的最终解。因此,分治算法的核心操作概括为“分而治之”还有所不足,应当是:
分而治之,再作组合
现在,先根据一个简单的小例子来消化一下上述原理:
————————————
e
x
a
m
p
l
e
_
0
example\_0
example_0——————————————
已知
A
[
1
…
n
]
A[1\dots n]
A[1…n] 是一整数数组,要求同时查找并返回数组元素的最大值和最小值(为了简化问题,这里假定
n
n
n 是
2
2
2 的整数幂)。我们先来看一个朴素算法:
- 输入:整数数组 A [ 1 … n ] A[1\dots n] A[1…n], n n n 为 2 2 2 的整数幂
- 输出:一个数对 ( x , y ) (x,\space y) (x, y),其中 x x x 为数组元素的最小值, y y y 为最大值
- 1. x ← A [ 1 ] , y ← A [ 1 ] \hspace{1cm}1.\space x\leftarrow A[1],\space y\leftarrow A[1] 1. x←A[1], y←A[1]
- 2. f o r i ← 2 t o n \hspace{1cm}2.\space for\space i\leftarrow2\space\space to\space\space n 2. for i←2 to n
- 3. i f A [ i ] < x t h e n x ← A [ i ] \hspace{1cm}3.\space\hspace{1cm}if\space A[i]<x\space\space then\space\space x\leftarrow A[i] 3. if A[i]<x then x←A[i]
- 4. i f A [ i ] > y t h e n y ← A [ i ] \hspace{1cm}4.\space\hspace{1cm}if\space A[i]>y\space\space then\space\space y\leftarrow A[i] 4. if A[i]>y then y←A[i]
- 5. e n d f o r \hspace{1cm}5.\space end\space\space for 5. end for
- 6. r e t u r n ( x , y ) \hspace{1cm}6.\space return\space(x,\space y) 6. return (x, y)
上述算法的思路十分明显,将数组直接遍历一边,然后一边比较,一边更新最大值和最小值。显然,该算法执行的元素比较次数为
2
n
−
2
2n-2
2n−2。现在再来看看利用分治策略设计的算法:
m
i
n
_
m
a
x
(
A
[
1
…
n
]
,
l
o
w
,
h
i
g
h
)
min\_max(A[1\dots n],\space low,\space high)
min_max(A[1…n], low, high)
- 输入:整数数组 A [ 1 … n ] A[1\dots n] A[1…n], n n n 为 2 2 2 的整数幂
- 输出:一个数对 ( x , y ) (x,\space y) (x, y),其中 x x x 为数组元素的最小值, y y y 为最大值
- 1. m i n _ m a x ( A , 1 , n ) \hspace{1cm}1.\space min\_max(A,\space1,\space n) 1. min_max(A, 1, n)
- 过程: m i n _ m a x ( A , l o w , h i g h ) min\_max(A,\space low,\space high) min_max(A, low, high)
- 1. i f h i g h − l o w = 1 t h e n \hspace{1cm}1.\space if\space\space high-low=1\space\space then 1. if high−low=1 then
- 2. i f A [ l o w ] < A [ h i g h ] t h e n r e t u r n ( A [ l o w ] , A [ h i g h ] ) \hspace{1cm}2.\space\hspace{1cm}if\space\space A[low]<A[high]\space\space then\space\space return\space(A[low],\space A[high]) 2. if A[low]<A[high] then return (A[low], A[high])
- KaTeX parse error: Expected 'EOF', got '}' at position 85: …,\space A[low])}̲
- 4. e n d i f \hspace{1cm}4.\space\hspace{1cm}end\space\space if 4. end if
- 5. e l s e \hspace{1cm}5.\space else 5. else
- 6. m i d ← ⌊ ( l o w + h i g h ) / 2 ⌋ \hspace{1cm}6.\space\hspace{1cm}mid\leftarrow\lfloor(low+high)/2\rfloor 6. mid←⌊(low+high)/2⌋
- 7. ( x 1 , y 1 ) ← m i n _ m a x ( A , l o w , m i d ) \hspace{1cm}7.\space\hspace{1cm}(x_1,\space y_1)\leftarrow min\_max(A,\space low,\space mid) 7. (x1, y1)←min_max(A, low, mid)
- 8. ( x 2 , y 2 ) ← m i n _ m a x ( A , m i d + 1 , h i g h ) \hspace{1cm}8.\space\hspace{1cm}(x_2,\space y_2)\leftarrow min\_max(A,\space mid+1,\space high) 8. (x2, y2)←min_max(A, mid+1, high)
- 9. x ← min { x 1 , x 2 } \hspace{1cm}9.\space\hspace{1cm}x\leftarrow\min\{x_1,\space x_2\} 9. x←min{x1, x2}
- 10. y ← max { y 1 , y 2 } \hspace{0.85cm}10.\space\hspace{1cm}y\leftarrow\max\{y_1,\space y_2\} 10. y←max{y1, y2}
- 11. r e t u r n ( x , y ) \hspace{0.85cm}11.\space\hspace{1cm}return\space(x,\space y) 11. return (x, y)
- 12. e n d i f \hspace{0.85cm}12.\space end\space\space if 12. end if
下面我们对应分治算法的原理来看一下
m
i
n
_
m
a
x
(
A
[
1
…
n
]
,
l
o
w
,
h
i
g
h
)
min\_max(A[1\dots n],\space low,\space high)
min_max(A[1…n], low, high):
“分而治之”:将数组分割成两半,
A
[
1
…
n
2
]
A[1\dots\frac{n}{2}]
A[1…2n] 和
A
[
n
2
+
1
…
n
]
A[\frac{n}{2}+1\dots n]
A[2n+1…n],在每一半中找到最大值和最小值。该部分另一个重要之处在于什么时候停止“分”,当然是分到我们能够解决的子问题时就可以停止了,在上述例子中,就是当数组中仅有两个元素,我们能直接比较出最大值和最小值的时候。
“再作组合”:那么,我们怎么将两个子问题的解合并构成原问题的解呢?很简单,两个子问题各得到一对最大值和最小值,然后我们直接返回两个最小值中更小的那个和两个最大值中更大的那个即可。下面我们来看看该算法执行的元素比较次数,这里将其设为
C
(
n
)
C(n)
C(n),根据算法的思路,易得如下递推关系式:
C
(
n
)
=
{
1
i
f
n
=
2
2
C
(
n
2
)
+
2
i
f
n
>
2
C(n)=\begin{cases}1\hspace{1cm}if\space\space n=2\\\\2C(\frac{n}{2})+2\hspace{1cm}if\space\space n>2\end{cases}
C(n)=⎩⎪⎨⎪⎧1if n=22C(2n)+2if n>2
直接展开得
C
(
n
)
=
2
C
(
n
2
)
+
2
C(n)=2C(\frac{n}{2})+2
C(n)=2C(2n)+2
=
2
[
2
C
(
n
4
)
+
2
]
+
2
\hspace{0.85cm}=2\big[2C(\frac{n}{4})+2\big]+2
=2[2C(4n)+2]+2
=
4
C
(
n
4
)
+
4
+
2
\hspace{0.85cm}=4C(\frac{n}{4})+4+2
=4C(4n)+4+2
=
4
[
2
C
(
n
8
)
+
2
]
+
4
+
2
\hspace{0.85cm}=4\big[2C(\frac{n}{8})+2\big]+4+2
=4[2C(8n)+2]+4+2
=
8
C
(
n
8
)
+
8
+
4
+
2
\hspace{0.85cm}=8C(\frac{n}{8})+8+4+2
=8C(8n)+8+4+2
=
⋯
=
2
k
−
1
C
(
n
2
k
−
1
)
+
2
k
−
1
+
2
k
−
2
+
⋯
+
2
2
+
2
,
(
k
=
log
n
)
\hspace{0.85cm}=\cdots=2^{k-1}C(\frac{n}{2^{k-1}})+2^{k-1}+2^{k-2}+\cdots+2^2+2,(k=\log n)
=⋯=2k−1C(2k−1n)+2k−1+2k−2+⋯+22+2,(k=logn)
=
2
k
−
1
C
(
2
)
+
∑
j
=
1
k
−
1
2
j
\hspace{0.85cm}=2^{k-1}C(2)+\sum_{j=1}^{k-1}2^j
=2k−1C(2)+∑j=1k−12j
=
3
2
n
−
2
\hspace{0.85cm}=\frac{3}{2}n-2
=23n−2
显然快于之前的朴素算法。
————————————————————————————————
看到这里,相信大家对于分治策略设计的算法已经有了初步的了解。我们再来看一个例子:
————————————
e
x
a
m
p
l
e
_
1
example\_1
example_1——————————————
之前我曾经写过一篇关于二分查找的博客,里面仅仅给出了二分查找最常见的算法思路——将给定元素
x
x
x 与一个已排序数组
A
[
l
o
w
…
h
i
g
h
]
A[low\dots high]
A[low…high] 的中间元素进行比较,若
x
<
A
[
m
i
d
]
,
(
m
i
d
=
⌊
(
l
o
w
+
h
i
g
h
)
/
2
⌋
)
x<A[mid],(mid=\lfloor(low+high)/2\rfloor)
x<A[mid],(mid=⌊(low+high)/2⌋),则不再考虑
A
[
m
i
d
…
h
i
g
h
]
A[mid\dots high]
A[mid…high] 的部分,然后对
A
[
l
o
w
…
m
i
d
−
1
]
A[low\dots mid-1]
A[low…mid−1] 部分重复执行上述操作,直到查找到目标元素为止 ,相应的,若有
x
>
A
[
m
i
d
]
x>A[mid]
x>A[mid],则对
A
[
m
i
d
+
1
…
h
i
g
h
]
A[mid+1\dots high]
A[mid+1…high] 部分重复执行上述操作。
那么,我们这里可以借助递归算法,设计出实现二分查找的另一种算法:
- 输入:按非降序排列的数组 A [ 1 … n ] A[1\dots n] A[1…n] 和待查找元素 x x x
- 输出:若 x = A [ i ] x=A[i] x=A[i],则返回 i i i,否则直接返回 0 0 0
- 1. b i n a r y _ s e a r c h ( A , 1 , n ) \hspace{1cm}1.\space binary\_search(A,1,n) 1. binary_search(A,1,n)
- 过程: b i n a r y _ s e a r c h ( A , l o w , h i g h ) binary\_search(A,low,high) binary_search(A,low,high)
- 1. i f l o w > h i g h t h e n r e t u r n 0 \hspace{1cm}1.\space if\space\space low>high\space\space then\space\space return\space0 1. if low>high then return 0
- 2. e l s e \hspace{1cm}2.\space else 2. else
- 3. m i d ← ⌊ ( l o w + h i g h ) / 2 ⌋ \hspace{1cm}3.\space\hspace{1cm}mid\leftarrow\lfloor(low+high)/2\rfloor 3. mid←⌊(low+high)/2⌋
- 4. i f x = A [ m i d ] t h e n r e t u r n m i d \hspace{1cm}4.\space\hspace{1cm}if\space\space x=A[mid]\space\space then\space\space return\space mid 4. if x=A[mid] then return mid
- 5. e l s e i f x < A [ m i d ] t h e n r e t u r n b i n a r y _ s e a r c h ( A , l o w , m i d − 1 ) \hspace{1cm}5.\space\hspace{1cm}else\space if\space\space x<A[mid]\space\space then\space\space return\space binary\_search(A,low,mid-1) 5. else if x<A[mid] then return binary_search(A,low,mid−1)
- 6. e l s e r e t u r n b i n a r y _ s e a r c h ( A , m i d + 1 , h i g h ) \hspace{1cm}6.\space\hspace{1cm}else\space\space return\space binary\_search(A,mid+1,high) 6. else return binary_search(A,mid+1,high)
- 7. e n d i f \hspace{1cm}7.\space end\space if 7. end if
递归二分查找算法复杂度分析:
a. 若
n
=
0
n=0
n=0,此时数组为空,则不执行任何元素的比较操作;
b. 若
n
=
1
n=1
n=1,进入
e
l
s
e
else
else 语句块,若数组内唯一的元素不等于指定元素,即
x
≠
A
[
m
i
d
]
x\neq A[mid]
x=A[mid],算法对空数组进行递归操作;
c. 若
n
>
1
n>1
n>1,运气好的话,有
x
=
A
[
m
i
d
]
x=A[mid]
x=A[mid],算法仅仅执行一次元素比较,就直接返回正确结果,否则在执行一次元素比较后,还要继续递归调用数组的前/后一半。这里假设
C
(
n
)
C(n)
C(n) 为该算法面对最坏情况执行的元素比较次数,易得递推式:
C
(
n
)
⩽
{
1
i
f
n
=
1
1
+
C
(
⌊
n
/
2
⌋
)
i
f
n
⩾
2
C(n)\leqslant\begin{cases}1\hspace{1cm}if\space\space n=1\\\\1+C(\lfloor n/2\rfloor)\hspace{1cm}if\space\space n\geqslant2\end{cases}
C(n)⩽⎩⎪⎨⎪⎧1if n=11+C(⌊n/2⌋)if n⩾2
已知
∃
k
∈
Z
,
k
⩾
2
,
s
.
t
.
2
k
−
1
⩽
n
<
2
k
\exists\space k\in Z,k\geqslant2,\space s.t.\space 2^{k-1}\leqslant n<2^k
∃ k∈Z,k⩾2, s.t. 2k−1⩽n<2k,下面直接将上述递推式展开,可知:
C
(
n
)
⩽
1
+
C
(
⌊
n
/
2
⌋
)
C(n)\leqslant1+C(\lfloor n/2\rfloor)
C(n)⩽1+C(⌊n/2⌋)
⩽
2
+
C
(
⌊
n
/
4
⌋
)
\hspace{0.85cm}\leqslant2+C(\lfloor n/4\rfloor)
⩽2+C(⌊n/4⌋)
⩽
⋯
⩽
(
k
−
1
)
+
C
(
⌊
n
/
x
k
−
1
⌋
)
\hspace{0.85cm}\leqslant\cdots\leqslant(k-1)+C(\lfloor n/x^{k-1}\rfloor)
⩽⋯⩽(k−1)+C(⌊n/xk−1⌋)
=
(
k
−
1
)
+
1
=
k
\hspace{0.85cm}=(k-1)+1=k
=(k−1)+1=k
将不等式
2
k
−
1
⩽
n
<
2
k
2^{k-1}\leqslant n<2^k
2k−1⩽n<2k 同取对数并加上
1
1
1,可得
k
⩽
log
n
+
1
⩽
k
+
1
k\leqslant\log n+1\leqslant k+1
k⩽logn+1⩽k+1,
⇒
k
=
⌊
log
n
⌋
+
1
\Rightarrow k=\lfloor\log n\rfloor+1
⇒k=⌊logn⌋+1
⇒
C
(
n
)
⩽
⌊
log
n
⌋
+
1
\Rightarrow C(n)\leqslant\lfloor\log n\rfloor+1
⇒C(n)⩽⌊logn⌋+1
————————————————————————————————
根据上面的两个例子,相信大家对于分治算法的”分而治之“已经有了一定程度的了解,但是,并没有明确体会到“再作组合”对于分治算法的重要意义和具体操作,下面我们再结合一个简单的分治算法的例子来探究一下”分治“思想的本质。
————————————
e
x
a
m
p
l
e
_
2
example\_2
example_2——————————————
合并排序:
m
e
r
g
e
_
s
o
r
t
merge\_sort
merge_sort
- 输入:待排序数组 A [ 1 … n ] A[1\dots n] A[1…n]
- 输出:按非降序排列的数组 A [ 1 … n ] A[1\dots n] A[1…n]
- 1. m e r g e _ s o r t ( A , 1 , n ) \hspace{1cm}1.\space merge\_sort(A,1,n) 1. merge_sort(A,1,n)
- 过程: m e r g e _ s o r t ( A , l o w , h i g h ) merge\_sort(A,low,high) merge_sort(A,low,high)
- 1. i f l o w < h i g h t h e n \hspace{1cm}1.\space if\space\space low<high\space\space then 1. if low<high then
- 2. m i d ← ⌊ ( l o w + h i g h ) / 2 ⌋ \hspace{1cm}2.\space\hspace{1cm}mid\leftarrow\lfloor(low+high)/2\rfloor 2. mid←⌊(low+high)/2⌋
- 3. m e r g e _ s o r t ( A , l o w , m i d ) \hspace{1cm}3.\space\hspace{1cm}merge\_sort(A,low,mid) 3. merge_sort(A,low,mid)
- 4. m e r g e _ s o r t ( A , m i d + 1 , h i g h ) \hspace{1cm}4.\space\hspace{1cm}merge\_sort(A,mid+1,high) 4. merge_sort(A,mid+1,high)
- 5. m e r g e ( A , l o w , m i d , h i g h ) \hspace{1cm}5.\space\hspace{1cm}merge(A,low,mid,high) 5. merge(A,low,mid,high)
- 6. e n d i f \hspace{1cm}6.\space end\space if 6. end if
这里我们就直接从明面上接触到了“再作组合”的概念——
m
e
r
g
e
merge
merge,这一算法主要用于合并两个已排序的表。现在假设有一个数组
A
[
1
…
m
]
,
p
,
q
,
r
A[1\dots m],p,q,r
A[1…m],p,q,r 为其三个索引,并有
1
⩽
p
⩽
q
<
r
⩽
m
1\leqslant p\leqslant q<r\leqslant m
1⩽p⩽q<r⩽m,两个子数组
A
[
p
…
q
]
A[p\dots q]
A[p…q] 和
A
[
q
+
1
…
r
]
A[q+1\dots r]
A[q+1…r] 都按升序排列,合并这两个数组的过程,就是重新安排
A
A
A 中元素的位置,使得子数组
A
[
p
…
r
]
A[p\dots r]
A[p…r] 也按升序排列。算法具体思路如下:
使用两个指针
s
,
t
s,t
s,t,在初始状态下,
s
s
s 指向
A
[
p
]
A[p]
A[p],
t
t
t 指向
A
[
q
+
1
]
A[q+1]
A[q+1],再利用一个空数组
B
[
p
…
r
]
B[p\dots r]
B[p…r] 作为暂存器。每一次比较元素
A
[
s
]
A[s]
A[s] 和
A
[
t
]
A[t]
A[t],将小的元素添加到暂存器中,若相同则添加
A
[
s
]
A[s]
A[s],然后再更新指针:若
A
[
s
]
⩽
A
[
t
]
A[s]\leqslant A[t]
A[s]⩽A[t],则
s
s
s+=
1
1
1,否则
t
t
t+=
1
1
1。当条件
s
=
q
+
1
s=q+1
s=q+1 或者
t
=
r
+
1
t=r+1
t=r+1 满足时停止,若满足条件一,则把数组
A
[
t
…
r
]
A[t\dots r]
A[t…r] 中剩余的元素添加至
B
B
B 中;若满足条件二,则把数组
A
[
s
…
q
]
A[s\dots q]
A[s…q] 中剩余的元素添加至
B
B
B 中,最后再将暂存器数组
B
[
p
…
r
]
B[p\dots r]
B[p…r] 复制回
A
[
p
…
r
]
A[p\dots r]
A[p…r]。实际上,该思路的核心就是同时遍历两个子数组,在比较之后将每个元素一一装到暂存器中,下面看伪代码:
- 输入:数组 A [ 1 … m ] A[1\dots m] A[1…m] 和其三个索引 p , q , r , 1 ⩽ p ⩽ q < r ⩽ m p,q,r,1\leqslant p\leqslant q<r\leqslant m p,q,r,1⩽p⩽q<r⩽m,两个子数组 A [ p … q ] A[p\dots q] A[p…q] 和 A [ q + 1 … r ] A[q+1\dots r] A[q+1…r] 各自按升序排列。
- 输出:将子数组 A [ p … q ] A[p\dots q] A[p…q] 和 A [ q + 1 … r ] A[q+1\dots r] A[q+1…r] 合并为 A [ p … r ] A[p\dots r] A[p…r]
- 1. c o m m e n t : B [ p … r ] \hspace{1cm}1.\space comment:B[p\dots r] 1. comment:B[p…r] 是一个辅助数组(暂存器)
- 2. s ← p ; t ← q + 1 ; k ← p \hspace{1cm}2.\space s\leftarrow p\space;\space t\leftarrow q+1\space;\space k\leftarrow p 2. s←p ; t←q+1 ; k←p
- 3. w h i l e s ⩽ q a n d t ⩽ r \hspace{1cm}3.\space while\space\space s\leqslant q\space\space and\space\space t\leqslant r 3. while s⩽q and t⩽r
- 4. i f A [ s ] ⩽ A [ t ] t h e n \hspace{1cm}4.\hspace{1cm}if\space\space A[s]\leqslant A[t]\space\space then 4.if A[s]⩽A[t] then
- 5. B [ k ] ← A [ s ] \hspace{1cm}5.\hspace{2cm}B[k]\leftarrow A[s] 5.B[k]←A[s]
- 6. s ← s + 1 \hspace{1cm}6.\hspace{2cm}s\leftarrow s+1 6.s←s+1
- 7. e l s e \hspace{1cm}7.\hspace{1cm}else 7.else
- 8. B [ k ] ← A [ t ] \hspace{1cm}8.\hspace{2cm}B[k]\leftarrow A[t] 8.B[k]←A[t]
- 9. t ← t + 1 \hspace{1cm}9.\hspace{2cm}t\leftarrow t+1 9.t←t+1
- 10. e n d i f \hspace{0.85cm}10.\hspace{1cm}end\space if 10.end if
- 11. k ← k + 1 \hspace{0.85cm}11.\space\hspace{1cm}k\leftarrow k+1 11. k←k+1
- 12. e n d w h i l e \hspace{0.85cm}12.\space end\space while 12. end while
- 13. i f s = q + 1 t h e n B [ k … r ] ← A [ t … r ] \hspace{0.85cm}13.\space if\space\space s=q+1\space\space then\space\space B[k\dots r]\leftarrow A[t\dots r] 13. if s=q+1 then B[k…r]←A[t…r]
- 14. e l s e B [ k … r ] ← A [ s … q ] \hspace{0.85cm}14.\space else\space\space B[k\dots r]\leftarrow A[s\dots q] 14. else B[k…r]←A[s…q]
- 15. e n d i f \hspace{0.85cm}15.\space end\space if 15. end if
- 16. A [ p … r ] ← B [ p … r ] \hspace{0.85cm}16.\space A[p\dots r]\leftarrow B[p\dots r] 16. A[p…r]←B[p…r]
下面我们来看一下合并排序算法的操作过程:
假设有待排序数组
A
[
1
…
8
]
=
{
9
,
4
,
5
,
2
,
1
,
7
,
4
,
6
}
A[1\dots8]=\{9,4,5,2,1,7,4,6\}
A[1…8]={9,4,5,2,1,7,4,6},现在开始合并排序
m
e
r
g
e
_
s
o
r
t
(
A
,
1
,
8
)
merge\_sort(A,1,8)
merge_sort(A,1,8),调用该算法会引起一个隐含二叉树所表示的一系列递归调用,树的每个结点由上下两个数组组成,上面的数组是调用算法的输入,下面的数组则是调用算法的输出,换句话说,顶端数组是“分”后的待排序数组,底端数组是“治”后“再作组合”的数组。二叉树的每个边用表示控制流的两个相向的边取代,边上的标号指明这些递归调用发生的次序,显然,这个调用链顺序为:访问根、左子树和右子树,但是计算顺序略有不同:左子树、右子树、根。为了实现该算法,我们需要用一个栈来存储每次调用过程生成的局部数据。下面是图示:
算法分析:
显然,上述算法中的基本运算是元素比较,换句话说,我们可以认为运行时间与由算法执行的元素比较次数是成正比的。因此现在的主要工作,就是计算出合并排序算法对数组
A
[
1
…
n
]
A[1\dots n]
A[1…n] 排序时,总计执行的元素比较次数
C
(
n
)
C(n)
C(n)。首先,为了简便计算,我们不失一般性地假设
n
=
2
k
,
k
∈
N
n=2^k,k\in N
n=2k,k∈N。
再回到伪代码,
当
n
=
1
n=1
n=1 时,算法不执行任何元素比较操作,即
C
(
1
)
=
0
C(1)=0
C(1)=0;
当
n
⩾
2
n\geqslant2
n⩾2 时,算法执行了步骤
2
∼
5
2\sim5
2∼5,步骤
3
3
3 和
4
4
4 执行的元素比较操作都是
C
(
n
2
)
C(\frac{n}{2})
C(2n) 次,根据合并操作算法的定义,易知合并两个子数组需要执行元素比较操作
2
n
∼
n
−
1
\frac{2}{n}\sim n-1
n2∼n−1 次。因此得到如下两个递推关系式:
-
最好情况: C ( n ) = { 0 i f n = 1 2 C ( n 2 ) + n 2 i f n ⩾ 2 C(n)=\begin{cases}0\hspace{1cm}if\space\space n=1\\\\2C(\frac{n}{2})+\frac{n}{2}\hspace{1cm}if\space\space n\geqslant2\end{cases} C(n)=⎩⎪⎨⎪⎧0if n=12C(2n)+2nif n⩾2,易得 C ( n ) = n log n 2 C(n)=\frac{n\log n}{2} C(n)=2nlogn
-
最坏情况: C ( n ) = { 0 i f n = 1 2 C ( n 2 ) + n − 1 i f n ⩾ 2 C(n)=\begin{cases}0\hspace{1cm}if\space\space n=1\\\\2C(\frac{n}{2})+n-1\hspace{1cm}if\space\space n\geqslant2\end{cases} C(n)=⎩⎪⎨⎪⎧0if n=12C(2n)+n−1if n⩾2,展开得
C ( n ) = 2 C ( n 2 ) + n − 1 C(n)=2C(\frac{n}{2})+n-1 C(n)=2C(2n)+n−1
= 2 ( 2 C ( n 2 2 ) + n 2 − 1 ) + n − 1 \hspace{0.85cm}=2(2C(\frac{n}{2^2})+\frac{n}{2}-1)+n-1 =2(2C(22n)+2n−1)+n−1
= 2 2 C ( n 2 2 ) + n − 2 + n − 1 \hspace{0.85cm}=2^2C(\frac{n}{2^2})+n-2+n-1 =22C(22n)+n−2+n−1
= ⋯ = 2 k C ( n 2 k ) + k n − 2 k − 1 − 2 k − 2 − ⋯ − 2 − 1 \hspace{0.85cm}=\cdots=2^kC(\frac{n}{2^k})+kn-2^{k-1}-2^{k-2}-\cdots-2-1 =⋯=2kC(2kn)+kn−2k−1−2k−2−⋯−2−1
= 2 k C ( 1 ) + k n − ∑ i = 0 k − 1 2 i \hspace{0.85cm}=2^kC(1)+kn-\sum_{i=0}^{k-1}2^i =2kC(1)+kn−∑i=0k−12i
= 0 + k n − ( 2 k − 1 ) \hspace{0.85cm}=0+kn-(2^k-1) =0+kn−(2k−1)
= n log n − n + 1 \hspace{0.85cm}=n\log n-n+1 =nlogn−n+1
综上,若
n
n
n 是任意正整数,则有合并排序算法执行的元素比较次数为
C
(
n
)
=
{
0
i
f
n
=
1
C
(
⌊
n
/
2
⌋
)
+
C
(
⌈
n
/
2
⌉
)
+
b
n
i
f
n
⩾
2
C(n)=\begin{cases}0\hspace{1cm}if\space\space n=1\\\\ C(\lfloor n/2\rfloor)+C(\lceil n/2\rceil)+bn\hspace{1cm}if\space\space n\geqslant2\end{cases}
C(n)=⎩⎪⎨⎪⎧0if n=1C(⌊n/2⌋)+C(⌈n/2⌉)+bnif n⩾2,其中
b
b
b 为非负常数,易知解为
C
(
n
)
=
Θ
(
n
log
n
)
C(n)=\Theta(n\log n)
C(n)=Θ(nlogn),
进一步得到,算法的实际复杂度为
T
(
n
)
=
n
log
n
T(n)=n\log n
T(n)=nlogn。
————————————————————————————————
3、分治范式
一般来说,分治范式由以下步骤组成:
-
(
1
)
(1)
(1) 划分步骤
在该步骤中,算法的输入被分为 p ⩾ 1 p\geqslant1 p⩾1 个部分,每个子实例的规模必须严格小于原始实例的规模 n n n。
在绝大多数情况下, p p p 都等于 2 2 2,例如归并排序算法 m e r g e _ s o r t merge\_sort merge_sort。有时候 p p p 也会等于 1 1 1,此时,往往是将一部分输入直接丢弃,再对余下部分作递归,这种思路实际上等价于将算法输入划分为两个部分,但是其中一部分不考虑。
注意:这里的 p p p 也可以是与 log n , n ε ( 0 < ε < 1 ) \log n,\space n^{\varepsilon}(0<\varepsilon<1) logn, nε(0<ε<1) 一样的数量级,思维万万不可局限于部分整数。另外,划分步骤在几乎所有的分治算法中都是不变的——把输入数据分成 p p p 部分,并且转到治理步骤。在许多分治算法中,它需要的时间为 O ( n ) O(n) O(n),甚至仅仅是 O ( 1 ) O(1) O(1)。 -
(
2
)
(2)
(2) 治理步骤
如果问题的规模大于某个预定的阈值 n 0 n_0 n0,该步骤直接执行 p p p 个递归调用即可。
阈值是由算法的数学分析导出的,我们可以在不影响算法的时间复杂度的前提下,将任意常数量增加到阈值上。例如,在归并排序算法 m e r g e _ s o r t merge\_sort merge_sort 中,我们有阈值 n 0 = 1 n_0=1 n0=1,那么就算我们将其设置为任意正常数,都不会对其时间复杂度产生影响。这是因为根据定义,时间复杂度是用来衡量 n → ∞ n\to\infty n→∞ 时算法的有效性。 -
(
3
)
(3)
(3) 组合步骤
该步骤主要解决组合 p p p 个递归调用的解,进而得到期望的输出值的问题。在归并排序算法 m e r g e _ s o r t merge\_sort merge_sort 中,这一步包含合并两个排序序列,这两个序列是借助合并算法 m e r g e merge merge 进行两次递归调用得到的。
在分治算法中,组合步骤可以由合并、排序、搜索、找最值、矩阵运算等组成。
注意:组合步骤对于执行一个实际的分治算法是非常关键的,因为算法的效率很大程度上就取决于我们如何设计这一步骤。
综上,我们可以得到分治算法的一般流程:
- ( 1 ) (1) (1) 若实例 I I I 的规模较小,则直接进行求解以获取答案,否则进入第 2 2 2 步。这一点看起来是一句废话,但是在现实生活中,我们不会总是遇到大规模问题,若一味追求借助分治算法解决问题,有时候会弄巧成拙;
- ( 2 ) (2) (2) 把实例 I I I 划分为 p p p 个大小几乎相同的子实例 I 1 , I 2 , … , I p I_1,I_2,\dots,I_p I1,I2,…,Ip;
- ( 3 ) (3) (3) 对于每一个子实例 I j , 1 ⩽ j ⩽ p I_j,1\leqslant j\leqslant p Ij,1⩽j⩽p,递归调用算法,并得到 p p p 个部分解;
- ( 4 ) (4) (4) 组合 p p p 个部分解的结果,进而求出并返回原实例 I I I 的解。
注意:子实例与原始实例性质完全一样,子实例之间可以彼此独立地求解,并且递归停止时子实例可以直接求解。我们将上面的一般流程概括为代码语言,即
- 分治算法: D i v i d e _ a n d _ C o n q u e r ( I ) Divide\_and\_Conquer(I) Divide_and_Conquer(I)
- 1. i f ∣ I ∣ ⩽ c t h e n S ( I ) 1.\space if\space\space|I|\leqslant c\space\space then\space\space S(I) 1. if ∣I∣⩽c then S(I)
- 2. d i v i d e I i n t o I 1 , I 2 , ⋯ , I p 2.\space divide\space\space I\space\space into\space\space I_1,I_2,\cdots,I_p 2. divide I into I1,I2,⋯,Ip
- 3. f o r j ← 1 t o p 3.\space for\space\space j\leftarrow 1\space to\space p 3. for j←1 to p
- 4. y j ← D i v i d e _ a n d _ C o n q u e r ( I j ) 4.\space\hspace{1cm}y_j\leftarrow Divide\_and\_Conquer(I_j) 4. yj←Divide_and_Conquer(Ij)
- 5. r e t u r n m e r g e ( y 1 , y 2 , … , y p ) 5.\space return\space\space merge(y_1,y_2,\dots,y_p) 5. return merge(y1,y2,…,yp)
4、分治算法的典型算法例
(
1
)
(1)
(1) 排序算法(归并排序和快速排序都运用了分治算法的思想)
上文中已经简单了解过了归并排序,这里我们再来看一下快速排序:
已知快速排序算法主要借助的就是“分治”的思想。那么,如何“分”,就是该算法的重要前提:
划分算法:设
A
[
1
…
n
]
A[1\dots n]
A[1…n] 是一个实数数组,要求将其整理为非降序排列。
我们先来设置分割点
r
e
f
ref
ref,一般来说都会默认为数组的第一个元素,即
r
e
f
=
A
[
1
]
ref=A[1]
ref=A[1]。下面就需要调整数组顺序,使得小于或等于
r
e
f
ref
ref 的元素都排在它前面,所有大于
r
e
f
ref
ref 的元素都排在它的后面。
例如有
e
x
=
{
5
,
3
,
9
,
2
,
7
,
1
,
8
}
ex=\{5,3,9,2,7,1,8\}
ex={5,3,9,2,7,1,8},再执行完划分算法后就要得到
e
x
=
{
1
,
3
,
2
,
5
,
7
,
9
,
8
}
ex=\{1,3,2,5,7,9,8\}
ex={1,3,2,5,7,9,8}。但是,这又是如何得到的呢?实际上有很多策略都可以实现划分,但是上述的例子中,采取的是如下算法:
- 输入:数组 A [ l o w , h i g h ] A[low,\space high] A[low, high]
- 输出:新数组 A A A 和 r e f = A [ l o w ] ref=A[low] ref=A[low] 的在新数组中的位置
- 1. i ← l o w \hspace{1cm}1.\space i\leftarrow low 1. i←low
- 2. r e f ← A [ l o w ] \hspace{1cm}2.\space ref\leftarrow A[low] 2. ref←A[low]
- 3. f o r j ← l o w + 1 t o h i g h \hspace{1cm}3.\space for\space\space j\leftarrow low+1\space\space to\space\space high 3. for j←low+1 to high
- 4. i f A [ j ] ⩽ r e f t h e n \hspace{1cm}4.\space\hspace{1cm}if\space\space A[j]\leqslant ref\space\space then 4. if A[j]⩽ref then
- 5. i ← i + 1 \hspace{1cm}5.\space\hspace{2cm}i\leftarrow i+1 5. i←i+1
- 6. i f i ≠ j t h e n s w a p ( A [ i ] , A [ j ] ) \hspace{1cm}6.\space\hspace{2cm}if\space\space i\neq j\space\space then\space\space swap(A[i],A[j]) 6. if i=j then swap(A[i],A[j])
- 7. e n d i f \hspace{1cm}7.\space\hspace{1cm}end\space if 7. end if
- 8. e n d f o r \hspace{1cm}8.\space end\space for 8. end for
- 9. s w a p ( A [ l o w ] , A [ i ] ) \hspace{1cm}9.\space swap(A[low],A[i]) 9. swap(A[low],A[i])
- 10. w ← i \hspace{0.85cm}10.\space w\leftarrow i 10. w←i
- 11. r e t u r n A a n d w \hspace{0.85cm}11.\space return\space A\space and\space w 11. return A and w
上述算法在执行过程中主要利用 i , j i,j i,j 指针,先将其设置为 i = l o w , j = l o w + 1 i=low,j=low+1 i=low,j=low+1,然后借助 f o r for for 循环遍历数组, f o r for for 循环执行结束后得到:
- A [ l o w ] = r e f A[low]=ref A[low]=ref
- A [ k ] ⩽ r e f , l o w ⩽ k ⩽ i A[k]\leqslant ref,\space low\leqslant k\leqslant i A[k]⩽ref, low⩽k⩽i
- A [ k ] > r e f , i < k ⩽ j A[k]>ref,\space i<k\leqslant j A[k]>ref, i<k⩽j
最后再将 A [ i ] A[i] A[i] 与 r e f ref ref 交换,这样就得到了我们期待的新数组。下面我们根据一个例子来具体看一下划分算法的执行步骤:
初始值情况 | { 5 , 7 , 1 , 6 , 4 , 8 , 3 , 2 } \{5,7,1,6,4,8,3,2\} {5,7,1,6,4,8,3,2} 此时有 l o w = 1 , h i g h = 8 , r e f = 5 low=1,high=8,ref=5 low=1,high=8,ref=5 |
---|---|
( 1 ) (1) (1) | i = l o w , A [ i ] = 5 ; j = l o w + 1 , A [ j ] = 7 i=low,A[i]=5;\space j=low+1,A[j]=7 i=low,A[i]=5; j=low+1,A[j]=7 |
( 2 ) (2) (2) | j = 3 j=3 j=3 时 A [ 3 ] = 1 ⩽ r e f = 5 ⇒ i = 2 ≠ j ⇒ { 5 , 1 , 7 , 6 , 4 , 8 , 3 , 2 } A[3]=1\leqslant ref=5\Rightarrow i=2\neq j\Rightarrow\{5,1,7,6,4,8,3,2\} A[3]=1⩽ref=5⇒i=2=j⇒{5,1,7,6,4,8,3,2} |
( 3 ) (3) (3) | j = 5 j=5 j=5 时 A [ 5 ] = 4 ⩽ r e f = 5 ⇒ i = 3 ≠ j ⇒ { 5 , 1 , 4 , 6 , 7 , 8 , 3 , 2 } A[5]=4\leqslant ref=5\Rightarrow i=3\neq j\Rightarrow\{5,1,4,6,7,8,3,2\} A[5]=4⩽ref=5⇒i=3=j⇒{5,1,4,6,7,8,3,2} |
( 4 ) (4) (4) | j = 7 j=7 j=7 时 A [ 7 ] = 3 ⩽ r e f = 5 ⇒ i = 4 ≠ j ⇒ { 5 , 1 , 4 , 3 , 7 , 8 , 6 , 2 } A[7]=3\leqslant ref=5\Rightarrow i=4\neq j\Rightarrow\{5,1,4,3,7,8,6,2\} A[7]=3⩽ref=5⇒i=4=j⇒{5,1,4,3,7,8,6,2} |
( 5 ) (5) (5) | j = 8 j=8 j=8 时 A [ 8 ] = 2 ⩽ r e f = 5 ⇒ i = 5 ≠ j ⇒ { 5 , 1 , 4 , 3 , 2 , 8 , 6 , 7 } A[8]=2\leqslant ref=5\Rightarrow i=5\neq j\Rightarrow\{5,1,4,3,2,8,6,7\} A[8]=2⩽ref=5⇒i=5=j⇒{5,1,4,3,2,8,6,7} |
( 6 ) (6) (6) | j = 8 = h i g h ⇒ j=8=high\Rightarrow j=8=high⇒ f o r for for 循环停止 |
( 7 ) (7) (7) | s w a p ( A [ l o w ] , A [ i ] ) ⇒ { 2 , 1 , 4 , 3 , 5 , 8 , 6 , 7 } swap(A[low],A[i])\Rightarrow\{2,1,4,3,5,8,6,7\} swap(A[low],A[i])⇒{2,1,4,3,5,8,6,7} |
( 8 ) (8) (8) | 算法执行完毕 |
最后注意到上述算法仅仅用到一个额外变量来存储它的局部变量,因此该划分算法的空间复杂度为 Θ ( 1 ) \Theta(1) Θ(1)。下面,我们就可以结合划分算法,借助递归调用完成快速排序的算法设计:
—————————-—— q u i c k _ s o r t quick\_sort quick_sort——————————————
- 输入:待排序数组 A [ 1 … n ] A[1\dots n] A[1…n] 和数组起始下标索引
- 输出:非降序排列数组 A [ 1 … n ] A[1\dots n] A[1…n]
- q u i c k _ s o r t ( A [ 1 … n ] , 1 , n ) quick\_sort(A[1\dots n],1,n) quick_sort(A[1…n],1,n)
- 过程: q u i c k _ s o r t ( A , l o w , h i g h ) quick\_sort(A,low,high) quick_sort(A,low,high)
- 1. i f l o w < h i g h t h e n \hspace{1cm}1.\space if\space\space low<high\space\space then 1. if low<high then
- 2. i ← l o w \hspace{1cm}2.\space\hspace{1cm} i\leftarrow low 2. i←low
- 3. r e f ← A [ l o w ] \hspace{1cm}3.\space\hspace{1cm} ref\leftarrow A[low] 3. ref←A[low]
- 4. f o r j ← l o w + 1 t o h i g h \hspace{1cm}4.\space\hspace{1cm} for\space\space j\leftarrow low+1\space\space to\space\space high 4. for j←low+1 to high
- 5. i f A [ j ] ⩽ r e f t h e n \hspace{1cm}5.\space\hspace{2cm}if\space\space A[j]\leqslant ref\space\space then 5. if A[j]⩽ref then
- 6. i ← i + 1 \hspace{1cm}6.\space\hspace{3cm}i\leftarrow i+1 6. i←i+1
- 7. i f i ≠ j t h e n s w a p ( A [ i ] , A [ j ] ) \hspace{1cm}7.\space\hspace{3cm}if\space\space i\neq j\space\space then\space\space swap(A[i],A[j]) 7. if i=j then swap(A[i],A[j])
- 8. e n d i f \hspace{1cm}8.\space\hspace{2cm}end\space if 8. end if
- 9. e n d f o r \hspace{1cm}9.\space\hspace{1cm} end\space for 9. end for
- 10. s w a p ( A [ l o w ] , A [ i ] ) \hspace{0.85cm}10.\space\hspace{1cm} swap(A[low],A[i]) 10. swap(A[low],A[i])
- 11. w ← i \hspace{0.85cm}11.\space\hspace{1cm} w\leftarrow i 11. w←i
- 12. q u i c k _ s o r t ( A , l o w , w − 1 ) \hspace{0.85cm}12.\space\hspace{1cm} quick\_sort(A,low,w-1) 12. quick_sort(A,low,w−1)
- 13. q u i c k _ s o r t ( A , w + 1 , h i g h ) \hspace{0.85cm}13.\space\hspace{1cm} quick\_sort(A,w+1,high) 13. quick_sort(A,w+1,high)
- 14. e n d i f \hspace{0.85cm}14.\space end\space if 14. end if
————————————————————————————————
下面再来看一下快速排序算法时间复杂度的计算。
-
a. 最坏情况
对于算法 q u i c k _ s o r t ( A , l o w , h i g h ) quick\_sort(A,low,high) quick_sort(A,low,high) 的每次调用, r e f = A [ l o w ] ref=A[low] ref=A[low] 都是数组中的最小值元素,那么仅仅存在一个有效的递归调用,而另一个调用就将作用在空数组上,即算法是由调用 q u i c k _ s o r t ( A , 1 , n ) quick\_sort(A,1,n) quick_sort(A,1,n) 开始,下一步的两个递归调用分别是 q u i c k _ s o r t ( A , 1 , 0 ) quick\_sort(A,1,0) quick_sort(A,1,0) 和 q u i c k _ s o r t ( A , 2 , n ) quick\_sort(A,2,n) quick_sort(A,2,n),其中,第一个就是无效调用。
因此,若输入的待排序数组已经是非降序排列的,最坏情况就出现了。简单来说,下面对于 q u i c k _ s o r t ( A , l o w , h i g h ) quick\_sort(A,low,high) quick_sort(A,low,high) 过程的 n n n 次调用将发生 q u i c k _ s o r t ( A , 1 , n ) , q u i c k _ s o r t ( A , 2 , n ) , … , q u i c k _ s o r t ( A , n , n ) quick\_sort(A,1,n),\space quick\_sort(A,2,n),\dots,\space quick\_sort(A,n,n) quick_sort(A,1,n), quick_sort(A,2,n),…, quick_sort(A,n,n)。
由于对于输入大小为 j j j 的元素,划分算法执行的元素比较次数为 j − 1 j-1 j−1,因此在最坏情况下,算法 q u i c k _ s o r t ( A , l o w , h i g h ) quick\_sort(A,low,high) quick_sort(A,low,high) 执行的元素总比较次数是
( n − 1 ) + ( n − 2 ) + ⋯ + 1 + 0 = n ( n − 1 ) 2 = Θ ( n 2 ) (n-1)+(n-2)+\cdots+1+0=\frac{n(n-1)}{2}=\Theta(n^2) (n−1)+(n−2)+⋯+1+0=2n(n−1)=Θ(n2)
但是我们要注意一点,如果我们总是在线性时间内选择中项作为分割点 r e f ref ref,最坏情况下的运行时间可以被改进为 Θ ( n log n ) \Theta(n\log n) Θ(nlogn),主要是因为这么划分元素是高度平衡的,而在这种情况下,两个递归调用具有几乎相同数目的元素,进而得到计算比较次数的递推式:
C ( n ) = { 0 i f n = 1 2 C ( n 2 ) + Θ ( n ) i f n > 1 C(n)=\begin{cases}0\hspace{1cm}if\space\space n=1\\\\2C(\frac{n}{2})+\Theta(n)\hspace{1cm}if\space\space n>1\end{cases} C(n)=⎩⎪⎨⎪⎧0if n=12C(2n)+Θ(n)if n>1
最终求得 C ( n ) = Θ ( n log n ) C(n)=\Theta(n\log n) C(n)=Θ(nlogn),但是找出中项的算法对时间复杂度的影响不可控,因而不值得与 q u i c k _ s o r t ( A , l o w , h i g h ) quick\_sort(A,low,high) quick_sort(A,low,high) 算法配合使用。 -
b. 平均情况
我们必须承认最坏情况会在算法执行时发生,但是它同时也属于极端情况,即在实际情况下是很少发生的,因此用最坏情况下的算法运行时间来界定时间复杂度很不合理。下面来看看平均情况:
为了便于理解,我们假定输入数据是互不相同的(这是不失一般性的,因为算法的性能与输入数据值是无关的,有关系的是它们的相对次序),不如更进一步,我们直接假设进行排序的元素是前 n n n 个正整数 1 , 2 , … , n 1,2,\dots,n 1,2,…,n。
在分析算法的平均性能的过程中,我们必须高度关注输入数据的概率分布,这里采用简化分析的方法,就假定元素的每个排列的出现是等可能的,即对于 1 , 2 , … , n 1,2,\dots,n 1,2,…,n 的 n ! n! n! 种排列情况来说,每一个排列的出现是等可能的,这一点主要是为了确保数组中的每个数被选作 r e f ref ref 的可能性是一样的,即数组 A A A 中任意元素被选为分割点的概率都是 1 n \frac{1}{n} n1。设 C ( n ) C(n) C(n) 为对于大小为 n n n 的输入数据, q u i c k _ s o r t ( A , l o w , h i g h ) quick\_sort(A,low,high) quick_sort(A,low,high) 算法所执行的平均比较次数,结合上述假设和算法的具体设计,得到
C ( n ) = ( n − 1 ) + 1 n ∑ w = 1 n ( C ( w − 1 ) + C ( n − w ) ) C(n)=(n-1)+\frac{1}{n}\sum_{w=1}^n\big(C(w-1)+C(n-w)\big) C(n)=(n−1)+n1∑w=1n(C(w−1)+C(n−w))
∵ ∑ w = 1 n C ( n − w ) = C ( n − 1 ) + C ( n − 2 ) + ⋯ + C ( 0 ) = ∑ w = 1 n C ( w − 1 ) \because\sum_{w=1}^nC(n-w)=C(n-1)+C(n-2)+\cdots+C(0)=\sum_{w=1}^nC(w-1) ∵∑w=1nC(n−w)=C(n−1)+C(n−2)+⋯+C(0)=∑w=1nC(w−1)
∴ C ( n ) = ( n − 1 ) + 2 n ∑ w = 1 n C ( w − 1 ) \therefore C(n)=(n-1)+\frac{2}{n}\sum_{w=1}^nC(w-1) ∴C(n)=(n−1)+n2∑w=1nC(w−1)
⇒ n C ( n ) = n ( n − 1 ) + 2 ∑ w = 1 n C ( w − 1 ) \Rightarrow nC(n)=n(n-1)+2\sum_{w=1}^nC(w-1) ⇒nC(n)=n(n−1)+2∑w=1nC(w−1)
⇒ ( n − 1 ) C ( n − 1 ) = ( n − 1 ) ( n − 2 ) + 2 ∑ w = 1 n − 1 C ( w − 1 ) \Rightarrow (n-1)C(n-1)=(n-1)(n-2)+2\sum_{w=1}^{n-1}C(w-1) ⇒(n−1)C(n−1)=(n−1)(n−2)+2∑w=1n−1C(w−1)
⇒ C ( n ) n + 1 = C ( n − 1 ) n + 2 ( n − 1 ) n ( n + 1 ) = D ( n ) \Rightarrow\frac{C(n)}{n+1}=\frac{C(n-1)}{n}+\frac{2(n-1)}{n(n+1)}=D(n) ⇒n+1C(n)=nC(n−1)+n(n+1)2(n−1)=D(n)
⇒ D ( n ) = D ( n − 1 ) + 2 ( n − 1 ) n ( n + 1 ) a n d D ( 1 ) = 0 \Rightarrow D(n)=D(n-1)+\frac{2(n-1)}{n(n+1)}\space\space and\space\space D(1)=0 ⇒D(n)=D(n−1)+n(n+1)2(n−1) and D(1)=0
⇒ D ( n ) = 2 ∑ j = 1 n j − 1 j ( j + 1 ) \Rightarrow D(n)=2\sum_{j=1}^n\frac{j-1}{j(j+1)} ⇒D(n)=2∑j=1nj(j+1)j−1
= 2 ∑ j = 1 n 2 j + 1 − 2 ∑ j = 1 n 1 j \hspace{1.32cm}=2\sum_{j=1}^n\frac{2}{j+1}-2\sum_{j=1}^n\frac{1}{j} =2∑j=1nj+12−2∑j=1nj1
= 4 ∑ j = 2 n + 1 1 j − 2 ∑ j = 1 n 1 j \hspace{1.32cm}=4\sum_{j=2}^{n+1}\frac{1}{j}-2\sum_{j=1}^n\frac{1}{j} =4∑j=2n+1j1−2∑j=1nj1
= 2 ∑ j = 1 n 1 j − 4 n n + 1 \hspace{1.32cm}=2\sum_{j=1}^n\frac{1}{j}-\frac{4n}{n+1} =2∑j=1nj1−n+14n
= 2 ln n − Θ ( 1 ) \hspace{1.32cm}=2\ln n-\Theta(1) =2lnn−Θ(1)
= 2 log e log n − Θ ( 1 ) \hspace{1.32cm}=\frac{2}{\log e}\log n-\Theta(1) =loge2logn−Θ(1)
≈ 1.44 log n \hspace{1.32cm}\approx1.44\log n ≈1.44logn
⇒ C ( n ) = ( n + 1 ) D ( n ) ≈ 1.44 n log n \Rightarrow C(n)=(n+1)D(n)\approx1.44n\log n ⇒C(n)=(n+1)D(n)≈1.44nlogn
综上,我们可以得到 q u i c k _ s o r t quick\_sort quick_sort 算法的时间复杂度为 Θ ( n log n ) \Theta(n\log n) Θ(nlogn)。
( 2 ) (2) (2) 矩阵乘法问题,即设 A , B A,B A,B 是两个 n × n n\times n n×n 的矩阵,求 C = A × B C=A\times B C=A×B
- 我们先来看一下最传统的直接相乘法:
矩阵 C C C 可以简单表示为 C = [ c i j ] i = 1 , 2 , … , n ; j = 1 , 2 , … , n C=[c_{ij}]_{i=1,2,\dots,n;j=1,2,\dots,n} C=[cij]i=1,2,…,n;j=1,2,…,n,根据矩阵乘法的定义,易知 c i j = ∑ k = 1 n a i k b k j c_{ij}=\sum_{k=1}^na_{ik}b_{kj} cij=∑k=1naikbkj,这样我们就能直接求出矩阵 C C C,但是,直接的想法是以复杂的运算为代价的。我们假设每做一次标量乘法耗时 m m m,每做一次标量加法耗时 a a a,那么,直接相乘算法的时间复杂度就是
T ( n ) = n 2 ∗ n ∗ m + n 2 ∗ ( n − 1 ) ∗ a = Θ ( n 3 ) T(n)=n^2*n*m+n^2*(n-1)*a=\Theta(n^3) T(n)=n2∗n∗m+n2∗(n−1)∗a=Θ(n3) - 显然,当数据规模达到一定的规模,这是难以接受的。因此,我们希望利用分治策略设计出一个效率更高的算法——分块矩阵法。顾名思义,这种方法是将大矩阵进行分块处理,分割成一个一个小矩阵,再按照矩阵乘法法则展开计算,即有
A
=
(
A
11
A
12
A
21
A
22
)
,
B
=
(
B
11
B
12
B
21
B
22
)
,
C
=
(
C
11
C
12
C
21
C
22
)
A=\left(\begin{matrix}A_{11}&A_{12}\\\\ A_{21}&A_{22}\end{matrix}\right),\space B=\left(\begin{matrix}B_{11}&B_{12}\\\\ B_{21}&B_{22}\end{matrix}\right),\space C=\left(\begin{matrix}C_{11}&C_{12}\\\\ C_{21}&C_{22}\end{matrix}\right)
A=⎝⎛A11A21A12A22⎠⎞, B=⎝⎛B11B21B12B22⎠⎞, C=⎝⎛C11C21C12C22⎠⎞
∴ C 11 = A 11 B 11 + A 12 B 21 \therefore\space C_{11}=A_{11}B_{11}+A_{12}B_{21} ∴ C11=A11B11+A12B21
C 12 = A 11 B 12 + A 12 B 22 \hspace{0.4cm}C_{12}=A_{11}B_{12}+A_{12}B_{22} C12=A11B12+A12B22
C 21 = A 21 B 11 + A 22 B 21 \hspace{0.4cm}C_{21}=A_{21}B_{11}+A_{22}B_{21} C21=A21B11+A22B21
C 22 = A 21 B 12 + A 22 B 22 \hspace{0.4cm}C_{22}=A_{21}B_{12}+A_{22}B_{22} C22=A21B12+A22B22
⇒ C = ( A 11 B 11 + A 12 B 21 A 11 B 12 + A 12 B 22 A 21 B 11 + A 22 B 21 A 21 B 12 + A 22 B 22 ) \Rightarrow C=\left(\begin{matrix}A_{11}B_{11}+A_{12}B_{21}&A_{11}B_{12}+A_{12}B_{22}\\\\A_{21}B_{11}+A_{22}B_{21}&A_{21}B_{12}+A_{22}B_{22}\end{matrix}\right) ⇒C=⎝⎛A11B11+A12B21A21B11+A22B21A11B12+A12B22A21B12+A22B22⎠⎞
根据上式,我们可以看出需要 8 8 8 次 n 2 × n 2 \frac{n}{2}\times\frac{n}{2} 2n×2n 矩阵乘法和 4 4 4 次 n 2 × n 2 \frac{n}{2}\times\frac{n}{2} 2n×2n 矩阵加法,这里设 a , m a,m a,m 分别表示数量加法和乘法的耗时,因此易知:
T ( n ) = { m i f n = 1 8 T ( n 2 ) + 4 ( n 2 ) 2 a i f n ⩾ 2 T(n)=\begin{cases}m\hspace{1cm}if\space\space n=1\\\\8T(\frac{n}{2})+4(\frac{n}{2})^2a\hspace{1cm}if\space\space n\geqslant2\end{cases} T(n)=⎩⎪⎨⎪⎧mif n=18T(2n)+4(2n)2aif n⩾2
⇒ T ( n ) = ( m + a 2 2 8 − 2 2 ) n 3 − ( a 2 2 8 − 2 2 ) n 2 = m n 3 + a n 3 − a n 2 = Θ ( n 3 ) \Rightarrow T(n)=(m+\frac{a2^2}{8-2^2})n^3-(\frac{a2^2}{8-2^2})n^2=mn^3+an^3-an^2=\Theta(n^3) ⇒T(n)=(m+8−22a22)n3−(8−22a22)n2=mn3+an3−an2=Θ(n3)
算法的耗时并没有改进,相反,耗费的时间甚至比传统的计算算法还要多(主要是耗在递归上了)。但这并不意味着分治策略是无效的,不过我们需要扭转一下思维定式。 -
S
t
r
a
s
s
e
n
Strassen
Strassen 算法,它的核心思路就是先利用分治策略分割矩阵,然后再通过增加加减法的次数来减少乘法次数。
算法引入 M 1 = ( A 11 + A 22 ) ( B 21 + B 22 ) M_1=(A_{11}+A_{22})(B_{21}+B_{22}) M1=(A11+A22)(B21+B22)
M 2 = ( A 21 + A 22 ) B 11 \hspace{1.25cm}M_2=(A_{21}+A_{22})B_{11} M2=(A21+A22)B11
M 3 = A 11 ( B 12 − B 22 ) \hspace{1.25cm}M_3=A_{11}(B_{12}-B_{22}) M3=A11(B12−B22)
M 4 = A 22 ( B 21 − B 11 ) \hspace{1.25cm}M_4=A_{22}(B_{21}-B_{11}) M4=A22(B21−B11)
M 5 = ( A 11 + A 12 ) B 22 \hspace{1.25cm}M_5=(A_{11}+A_{12})B_{22} M5=(A11+A12)B22
M 6 = ( A 21 − A 11 ) ( B 11 + B 12 ) \hspace{1.25cm}M_6=(A_{21}-A_{11})(B_{11}+B_{12}) M6=(A21−A11)(B11+B12)
M 7 = ( A 12 − A 22 ) ( B 21 + B 22 ) \hspace{1.25cm}M_7=(A_{12}-A_{22})(B_{21}+B_{22}) M7=(A12−A22)(B21+B22)
则有 C 11 = M 1 + M 4 − M 5 + M 7 \hspace{0.6cm}C_{11}=M_1+M_4-M_5+M_7 C11=M1+M4−M5+M7
C 12 = M 3 + M 5 \hspace{1.25cm}C_{12}=M_3+M_5 C12=M3+M5
C 21 = M 2 + M 4 \hspace{1.25cm}C_{21}=M_2+M_4 C21=M2+M4
C 22 = M 1 + M 3 − M 2 + M 6 \hspace{1.25cm}C_{22}=M_1+M_3-M_2+M_6 C22=M1+M3−M2+M6,这么做并没有改变结果
⇒ C = ( M 1 + M 4 − M 5 + M 7 M 3 + M 5 M 2 + M 4 M 1 + M 3 − M 2 + M 6 ) \Rightarrow C=\left(\begin{matrix}M_1+M_4-M_5+M_7&M_3+M_5\\\\M_2+M_4&M_1+M_3-M_2+M_6\end{matrix}\right) ⇒C=⎝⎛M1+M4−M5+M7M2+M4M3+M5M1+M3−M2+M6⎠⎞
进而得到 T ( n ) = { m i f n = 1 7 T ( n 2 ) + 18 ( n 2 ) 2 a i f n ⩾ 2 T(n)=\begin{cases}m\hspace{1cm}if\space\space n=1\\\\7T(\frac{n}{2})+18(\frac{n}{2})^2a\hspace{1cm}if\space\space n\geqslant2\end{cases} T(n)=⎩⎪⎨⎪⎧mif n=17T(2n)+18(2n)2aif n⩾2
⇒ T ( n ) = ( m + 9 a 2 2 2 7 − 2 2 ) n log 7 − ( 9 a 2 2 2 7 − 2 2 ) n 2 \Rightarrow T(n)=(m+\frac{\frac{9a}{2}2^2}{7-2^2})n^{\log7}-(\frac{\frac{9a}{2}2^2}{7-2^2})n^2 ⇒T(n)=(m+7−2229a22)nlog7−(7−2229a22)n2
= m n log 7 + 6 a n log 7 − 6 a n 2 \hspace{1.25cm}=mn^{\log7}+6an^{\log7}-6an^2 =mnlog7+6anlog7−6an2
= Θ ( n log 7 ) = Θ ( n 2.81 ) \hspace{1.25cm}=\Theta(n^{\log7})=\Theta(n^{2.81}) =Θ(nlog7)=Θ(n2.81)
我们也可以根据 M a s t e r T h e o r e m Master\space Theorem Master Theorem 的结论进行估算:
T ( n ) = Θ ( n log b a ) = Θ ( n log 2 7 ) = Θ ( n 2.81 ) T(n)=\Theta(n^{\log_b^a})=\Theta(n^{\log_2^7})=\Theta(n^{2.81}) T(n)=Θ(nlogba)=Θ(nlog27)=Θ(n2.81)
耗时缩短,传统算法的效率得到了改进。
(
3
)
(3)
(3) 大整数相乘
通常,我们在分析算法的计算复杂度时,都会将加法和乘法运算当作基本运算来处理,即将执行一次加法或乘法运算耗费的计算时间默认为一个常数,常数值的大小主要取决于计算机硬件的处理速度。而我们也知道,常数时间对于算法时间复杂度的影响是很有限的,往往会忽略不计。
但是,这一默认行为仅仅在参加运算的整数处于一定范围内是才是合理的(这个整数范围主要取决于计算机硬件对整数的表示),换句话说,在某些情况下我们需要处理很大的整数,它无法在计算机硬件能够直接表示的整数范围内进行处理。此时,我们就必须使用软件的方法来实现大整数的算术运算,而该操作的耗费时间,就不是一个常数了。我们都知道计算机处理数字是一般都会使用二进制,这里就举一个二进制整数相乘的例子:
假设有两个
n
b
i
t
n\space bit
n bit 的二进制整数
x
,
y
x,y
x,y,需要计算
x
y
xy
xy。
- 传统乘法运算:相信大家在小学的时候都学习过十进制乘法的计算法则(逐位相乘),二进制也是一样的,这里我们可以将位( b i t bit bit)的乘法或加法运算视作基本运算,那么逐位相乘算法的时间复杂度为 T ( n ) = Θ ( n 2 ) T(n)=\Theta(n^2) T(n)=Θ(n2)。
- 分治策略改写:将二进制整数分为两部分,即有
x
=
a
⋅
2
n
2
+
b
,
y
=
c
⋅
2
n
2
+
d
x=a\cdot 2^{\frac{n}{2}}+b,\space y=c\cdot2^{\frac{n}{2}}+d
x=a⋅22n+b, y=c⋅22n+d,这一操作一开始可能不易理解,我们举个例子,现在有
x
=
(
1101
)
2
=
13
x=(1101)_2=13
x=(1101)2=13,那么可以得到
a
=
(
11
)
2
=
3
,
b
=
(
01
)
2
=
1
,
x
=
a
⋅
2
n
2
+
b
=
3
⋅
2
2
+
1
=
13
a=(11)_2=3,b=(01)_2=1,x=a\cdot2^{\frac{n}{2}}+b=3\cdot2^2+1=13
a=(11)2=3,b=(01)2=1,x=a⋅22n+b=3⋅22+1=13。
因此, x y = ( a ⋅ 2 n 2 + b ) ( c ⋅ 2 n 2 + d ) = a c ⋅ 2 n + ( a d + c b ) ⋅ 2 n 2 + b d xy=(a\cdot2^{\frac{n}{2}}+b)(c\cdot2^{\frac{n}{2}}+d)=ac\cdot2^n+(ad+cb)\cdot2^{\frac{n}{2}}+bd xy=(a⋅22n+b)(c⋅22n+d)=ac⋅2n+(ad+cb)⋅22n+bd,下面我们来看一下该算法的时间复杂度:
( 1 ) (1) (1) 4 4 4 次 n 2 b i t \frac{n}{2}\space bit 2n bit 位数的乘法( a c , a d , c b , b d ac,ad,cb,bd ac,ad,cb,bd) ⇒ 4 T ( n 2 ) \Rightarrow4T(\frac{n}{2}) ⇒4T(2n)
( 2 ) (2) (2) a c ac ac 左移 n n n 位( a c ⋅ 2 n ac\cdot2^n ac⋅2n,这里涉及到二进制移位操作实现乘法) ⇒ Θ ( n ) \Rightarrow\Theta(n) ⇒Θ(n)
( 3 ) (3) (3) 求解 a d + c b ad+cb ad+cb 时执行了 n b i t n\space bit n bit 加法 ⇒ Θ ( n ) \Rightarrow\Theta(n) ⇒Θ(n)
( 4 ) (4) (4) a d + c b ad+cb ad+cb 左移 n 2 \frac{n}{2} 2n 位( ( a d + c b ) ⋅ 2 n 2 (ad+cb)\cdot2^{\frac{n}{2}} (ad+cb)⋅22n) ⇒ Θ ( n ) \Rightarrow\Theta(n) ⇒Θ(n)
( 5 ) (5) (5) 两个加法操作( a c ⋅ 2 n + ( a d + c b ) ⋅ 2 n 2 + b d ac\cdot2^n+(ad+cb)\cdot2^{\frac{n}{2}}+bd ac⋅2n+(ad+cb)⋅22n+bd) ⇒ Θ ( n ) \Rightarrow\Theta(n) ⇒Θ(n)
⇒ T ( n ) = { a i f n = 1 4 T ( n 2 ) + Θ ( n ) i f n > 1 ⇒ T ( n ) = Θ ( n 2 ) \Rightarrow T(n)=\begin{cases}a\hspace{1cm}if\space\space n=1\\\\4T(\frac{n}{2})+\Theta(n)\hspace{1cm}if\space\space n>1\end{cases}\Rightarrow T(n)=\Theta(n^2) ⇒T(n)=⎩⎪⎨⎪⎧aif n=14T(2n)+Θ(n)if n>1⇒T(n)=Θ(n2)
没有显著提高效率,那么我们来看一下如何在分治策略的基础上降低时间复杂度 -
∵
a
d
+
c
b
=
(
a
+
b
)
(
c
+
d
)
−
a
c
−
b
d
\because ad+cb=(a+b)(c+d)-ac-bd
∵ad+cb=(a+b)(c+d)−ac−bd
∴ x y = ( a ⋅ 2 n 2 + b ) ( c ⋅ 2 n 2 + d ) \therefore xy=(a\cdot2^{\frac{n}{2}}+b)(c\cdot2^{\frac{n}{2}}+d) ∴xy=(a⋅22n+b)(c⋅22n+d)
= a c ⋅ 2 n + ( a d + c b ) ⋅ 2 n 2 + b d \hspace{0.8cm}=ac\cdot2^n+(ad+cb)\cdot2^{\frac{n}{2}}+bd =ac⋅2n+(ad+cb)⋅22n+bd
= a c ⋅ 2 n + ( ( a + b ) ( c + d ) − a c − b d ) ⋅ 2 n 2 + b d \hspace{0.8cm}=ac\cdot2^n+((a+b)(c+d)-ac-bd)\cdot2^{\frac{n}{2}}+bd =ac⋅2n+((a+b)(c+d)−ac−bd)⋅22n+bd
∴ T ( n ) = { a i f n = 1 3 T ( n 2 ) + Θ ( n ) i f n > 1 ⇒ T ( n ) = Θ ( n log 3 ) = Θ ( n 1.59 ) \therefore T(n)=\begin{cases}a\hspace{1cm}if\space\space n=1\\\\3T(\frac{n}{2})+\Theta(n)\hspace{1cm}if\space\space n>1\end{cases}\Rightarrow T(n)=\Theta(n^{\log3})=\Theta(n^{1.59}) ∴T(n)=⎩⎪⎨⎪⎧aif n=13T(2n)+Θ(n)if n>1⇒T(n)=Θ(nlog3)=Θ(n1.59)
算法效率有了显著的提高。
(
4
)
(4)
(4)
H
a
n
o
i
Hanoi
Hanoi 塔问题
设
a
,
b
,
c
a,b,c
a,b,c 是
3
3
3 个塔座。开始时,在塔座
a
a
a 上有一叠圆盘(总计
n
n
n 个),这些圆盘自下而上、由大到小地叠在一起,各圆盘从小到大编号为
1
,
2
,
3
,
…
,
n
1,2,3,\dots,n
1,2,3,…,n。
现在要求将塔座
a
a
a 上的这一叠圆盘移到塔座
c
c
c 上,并且按照原顺序堆叠。同时,在移动圆盘时必须遵循以下规则:
- ( 1 ) (1) (1) 每次只能移动一个圆盘
- ( 2 ) (2) (2) 无论何时都不允许将较大的圆盘压在较小的圆盘上
- ( 3 ) (3) (3) 在满足上述规则的前提下,可以将圆盘移至 a , b , c a,b,c a,b,c 中任意一个塔座上
分析该问题,我们得到以下基本思路:
- ( 1 ) (1) (1) 以 c c c 为中介,从 a a a 将 1 1 1 至 n − 1 n-1 n−1 号圆盘移至 b b b
- ( 2 ) (2) (2) 将 a a a 中剩下的第 n n n 号圆盘移至 c c c
- ( 3 ) (3) (3) 以 a a a 为中介,从 b b b 将 1 1 1 至 n − 1 n-1 n−1 号圆盘移至 c c c
最终得到算法思路:
- 输入:圆盘个数 n n n, 3 3 3 个塔座 a , b , c a,b,c a,b,c
- 输出:将塔座 a a a 中的所有圆盘遵循移动规则移至塔座 c c c
- 1. h a n o i ( n , a , b , c ) \hspace{1cm}1.\space hanoi(n,a,b,c) 1. hanoi(n,a,b,c)
- 过程: v o i d h a n o i ( i n t n , i n t a , i n t b , i n t c ) void\space\space hanoi(int\space n,\space int\space a,\space int\space b,\space int\space c) void hanoi(int n, int a, int b, int c)
- 1. i f n > 0 \hspace{1cm}1.\space if\space\space n>0 1. if n>0
- 2. h a n o i ( n − 1 , a , b , c ) { a → b , c \hspace{1cm}2.\space\hspace{1cm}hanoi(n-1,a,b,c)\space\{a\rightarrow b,c 2. hanoi(n−1,a,b,c) {a→b,c 做辅助 } \} }
- 3. m o v e ( a , c ) { a → c } \hspace{1cm}3.\space\hspace{1cm}move(a,c)\space\{a\rightarrow c\} 3. move(a,c) {a→c}
- 4. h a n o i ( n − 1 , b , c , a ) { b → c , a \hspace{1cm}4.\space\hspace{1cm}hanoi(n-1,b,c,a)\space\{b\rightarrow c,a 4. hanoi(n−1,b,c,a) {b→c,a 做辅助 } \} }
- 5. e n d i f \hspace{1cm}5.\space end\space if 5. end if
(
5
)
(5)
(5) 寻找中项和第
k
k
k 小元素
给定一排序完毕的数组
A
[
1
⋯
n
]
A[1\cdots n]
A[1⋯n],我们按照字面义将“中项”定义为数组中间的元素:若
n
n
n 为奇数,则中项就是数组中的第
n
+
1
2
\frac{n+1}{2}
2n+1 个元素;若
n
n
n 为偶数,则存在两个中间元素,即第
n
2
\frac{n}{2}
2n 个元素和第
n
2
+
1
\frac{n}{2}+1
2n+1 个元素,此时,我们人为地选取第
n
2
\frac{n}{2}
2n 个元素作为中项。
综上,我们得到中项的基本数学定义——第
⌈
n
/
2
⌉
\lceil n/2\rceil
⌈n/2⌉ 个元素。
因此,我们可以根据中项的定义得到查找中项的直接方法:先排序,再返回第
⌈
n
/
2
⌉
\lceil n/2\rceil
⌈n/2⌉ 个元素值。然而根据这一方法,实际上我们可以查找任意数组中的第
k
k
k 小元素,寻找中项仅仅只是其中的一个特例而已(
k
=
⌈
n
/
2
⌉
k=\lceil n/2\rceil
k=⌈n/2⌉)。显然,上面提到的直接查找算法的时间复杂度为
Ω
(
n
log
n
)
\Omega(n\log n)
Ω(nlogn),我们希望通过分治策略改进算法效率,那么可以先参考二分查找——以中间元素为基准分割数组,然后通过判断抛弃部分元素,不断减小问题规模。据此,我们可以初步给出以下思路:
- 若数组 A [ 1 … n ] A[1\dots n] A[1…n] 中的元素个数小于一个阈值,那么我们可以采用直接查找的方式找到第 k k k 小元素。这一阈值的含义就是说,当数组规模小于阈值时,直接查找的时间复杂度是完全可以接受的,甚至比优化之后效率更高。
- 若数组
A
[
1
…
n
]
A[1\dots n]
A[1…n] 中的元素个数大于该阈值,就将
n
n
n 个元素分成
⌊
n
/
5
⌋
\lfloor n/5\rfloor
⌊n/5⌋ 组。每组
5
5
5 个元素,注意,如果
n
n
n 不是
5
5
5 的倍数,则暂时排除剩余的元素,但这并不意味着排除第
k
k
k 小元素在这些丢弃元素中的可能性。
对每组元素依次排序,并取出他们的中项(即第 3 3 3 个元素),因此,我们可以得到 ⌊ n / 5 ⌋ \lfloor n/5\rfloor ⌊n/5⌋ 个中项组成的新数组,我们将新数组的中项记作 m m mm mm。然后我们可以根据 m m mm mm 将数组 A [ 1 … n ] A[1\dots n] A[1…n] 分为三个子数组: A 1 = { a ∣ a < m m } , A 2 = { a ∣ a = m m } , A 3 = { a ∣ a > m m } A_1=\{a|a<mm\},\space A_2=\{a|a=mm\},\space A_3=\{a|a>mm\} A1={a∣a<mm}, A2={a∣a=mm}, A3={a∣a>mm}。然后再判断第 k k k 小元素可能在哪个子数组中出现:若在 A 2 A_2 A2 中出现,则已经找到;否则,对 A 1 / A 3 A_1/A_3 A1/A3 进行递归。
根据上述思路,得到算法 s e l e c t ( A [ 1 … n ] , l o w , h i g h , k ) select(A[1\dots n],low,high,k) select(A[1…n],low,high,k)
- 输入:数组 A [ 1 … n ] A[1\dots n] A[1…n] 和整数 k k k
- 输出:数组 A A A 中的第 k k k 小元素
- 1. s e l e c t ( A , 1 , n , k ) \hspace{1cm}1.\space select(A,1,n,k) 1. select(A,1,n,k)
- 过程: s e l e c t ( A , l o w , h i g h , k ) select(A,low,high,k) select(A,low,high,k)
- 1. p ← h i g h − l o w + 1 \hspace{1cm}1.\space p\leftarrow high-low+1 1. p←high−low+1
- 2. i f p < \hspace{1cm}2.\space if\space\space p< 2. if p< 阈值 t h e n \space then\space then 对 A A A 进行排序,并 r e t u r n A [ k ] return\space\space A[k] return A[k]
- 3. q ← ⌊ p / 5 ⌋ \hspace{1cm}3.\space q\leftarrow\lfloor p/5\rfloor 3. q←⌊p/5⌋,将数组 A A A 分成 q q q 组,每组 5 5 5 个元素。若 5 5 5 不整除 p p p,则排除剩余的元素
- 4. \hspace{1cm}4.\space 4. 对 q q q 组数组进行分别排序,将各自中项 合并为新数组 M M M
- 5. m m ← s e l e c t ( M , 1 , q , ⌈ q / 2 ⌉ ) \hspace{1cm}5.\space mm\leftarrow select(M,1,q,\lceil q/2\rceil) 5. mm←select(M,1,q,⌈q/2⌉)
- 6. \hspace{1cm}6.\space 6. 将 A [ l o w … h i g h ] A[low\dots high] A[low…high] 分为 3 3 3 组:
- A 1 = { a ∣ a < m m } \hspace{2cm}A_1=\{a|a<mm\} A1={a∣a<mm}
- A 2 = { a ∣ a = m m } \hspace{2cm}A_2=\{a|a=mm\} A2={a∣a=mm}
- A 3 = { a ∣ a > m m } \hspace{2cm}A_3=\{a|a>mm\} A3={a∣a>mm}
- 7. c a s e \hspace{1cm}7.\space case 7. case
- ∣ A 1 ∣ ⩾ k : r e t u r n s e l e c t ( A 1 , 1 , ∣ A 1 ∣ , k ) \hspace{2cm}|A_1|\geqslant k:return\space\space select(A_1,1,|A_1|,k) ∣A1∣⩾k:return select(A1,1,∣A1∣,k)
- ∣ A 1 ∣ + ∣ A 2 ∣ ⩾ k : r e t u r n m m \hspace{2cm}|A_1|+|A_2|\geqslant k:return\space\space mm ∣A1∣+∣A2∣⩾k:return mm
- ∣ A 1 ∣ + ∣ A 2 ∣ < k : r e t u r n s e l e c t ( A 3 , 1 , ∣ A 3 ∣ , k − ∣ A 1 ∣ − ∣ A 2 ∣ ) \hspace{2cm}|A_1|+|A_2|<k:return\space\space select(A_3,1,|A_3|,k-|A_1|-|A_2|) ∣A1∣+∣A2∣<k:return select(A3,1,∣A3∣,k−∣A1∣−∣A2∣)
- 8. e n d c a s e \hspace{1cm}8.\space end\space case 8. end case
时间复杂度分析:
已知
s
e
l
e
c
t
select
select 算法先将数组中的元素划分为各组,每组含
5
5
5 个元素,现在将每组从低到顶按升序排列,绘制算法示意图。
由图可知,
W
W
W 区域中的所有元素都不大于
m
m
mm
mm,
X
X
X 区域中的所有元素都不小于
m
m
mm
mm。现在设
A
1
′
=
{
x
∣
x
⩽
m
m
}
,
A
3
′
=
{
x
∣
x
⩾
m
m
}
A_1'=\{x|x\leqslant mm\},\space A_3'=\{x|x\geqslant mm\}
A1′={x∣x⩽mm}, A3′={x∣x⩾mm},此外,根据算法定义易知
A
1
=
{
x
∣
x
<
m
m
}
,
A
3
=
{
x
∣
x
>
m
m
}
A_1=\{x|x<mm\},\space A_3=\{x|x>mm\}
A1={x∣x<mm}, A3={x∣x>mm}。因此,结合算法示意图可以得到
∣
A
1
′
∣
⩾
3
⌈
⌊
n
/
5
⌋
2
⌉
⩾
3
2
⌊
n
/
5
⌋
|A_1'|\geqslant3\lceil\frac{\lfloor n/5\rfloor}{2}\rceil\geqslant\frac{3}{2}\lfloor n/5\rfloor
∣A1′∣⩾3⌈2⌊n/5⌋⌉⩾23⌊n/5⌋
⇒
∣
A
3
∣
⩽
n
−
3
2
⌊
n
/
5
⌋
⩽
n
−
3
2
(
n
−
4
5
)
=
n
−
0.3
n
+
1.2
=
0.7
n
+
1.2
\Rightarrow|A_3|\leqslant n-\frac{3}{2}\lfloor n/5\rfloor\leqslant n-\frac{3}{2}(\frac{n-4}{5})=n-0.3n+1.2=0.7n+1.2
⇒∣A3∣⩽n−23⌊n/5⌋⩽n−23(5n−4)=n−0.3n+1.2=0.7n+1.2
⇒
∣
A
3
′
∣
⩾
3
2
⌊
n
/
5
⌋
,
∣
A
1
∣
⩽
0.7
n
+
1.2
\Rightarrow|A_3'|\geqslant\frac{3}{2}\lfloor n/5\rfloor,\space|A_1|\leqslant0.7n+1.2
⇒∣A3′∣⩾23⌊n/5⌋, ∣A1∣⩽0.7n+1.2
根据上式,我们可以得到集合
A
1
,
A
3
A_1,A_3
A1,A3 中元素数量的上界值,即小于
m
m
mm
mm 的元素的数量和大于
m
m
mm
mm 的元素的数量大致上都不能多于
0.7
n
0.7n
0.7n。估算
s
e
l
e
c
t
select
select 算法时间复杂度需要依次观察每一步的耗费:
s
t
e
p
1
step\space 1
step 1:
Θ
(
1
)
\Theta(1)
Θ(1)
s
t
e
p
2
step\space 2
step 2:
Θ
(
1
)
\Theta(1)
Θ(1)
s
t
e
p
3
step\space 3
step 3:
Θ
(
n
)
\Theta(n)
Θ(n)
s
t
e
p
4
step\space 4
step 4:
Θ
(
n
)
\Theta(n)
Θ(n)
s
t
e
p
5
step\space 5
step 5:
Θ
(
⌊
n
/
5
⌋
)
\Theta(\lfloor n/5\rfloor)
Θ(⌊n/5⌋)
s
t
e
p
6
step\space 6
step 6:
Θ
(
n
)
\Theta(n)
Θ(n)
s
t
e
p
7
step\space 7
step 7:
Θ
(
0.7
n
+
1.2
)
\Theta(0.7n+1.2)
Θ(0.7n+1.2),现在希望进一步规整化这一表示,为此假设
0.7
n
+
1.2
⩽
⌊
0.75
n
⌋
0.7n+1.2\leqslant\lfloor0.75n\rfloor
0.7n+1.2⩽⌊0.75n⌋。因此,若
0.7
n
+
1.2
⩽
0.75
n
−
1
0.7n+1.2\leqslant0.75n-1
0.7n+1.2⩽0.75n−1,即当
n
⩾
44
n\geqslant44
n⩾44 时上述不等式将会成立,因此在
s
e
l
e
c
t
select
select 算法中我们一般将阈值设置为
44
44
44。根据上面的分析,我们可以得到关于时间复杂度的递推关系式:
T
(
n
)
=
{
c
i
f
n
<
44
T
(
⌊
n
/
5
⌋
)
+
T
(
⌊
3
n
/
4
⌋
)
+
c
n
i
f
n
⩾
44
T(n)=\begin{cases}c\hspace{1cm}if\space\space n<44\\\\ T(\lfloor n/5\rfloor)+T(\lfloor3n/4\rfloor)+cn\hspace{1cm}if\space\space n\geqslant44\end{cases}
T(n)=⎩⎪⎨⎪⎧cif n<44T(⌊n/5⌋)+T(⌊3n/4⌋)+cnif n⩾44,
c
c
c 是一个足够大的常数。
(
6
)
(6)
(6) 最接近点对问题
设
S
S
S 是平面上
n
n
n 个点的集合,而最接近点对问题就是在
S
S
S 中找到一个点对
p
p
p 和
q
q
q 的问题,使其相互距离最短。即考虑在
S
S
S 中找到
p
1
=
(
x
1
,
y
1
)
,
p
2
=
(
x
2
,
y
2
)
p_1=(x_1,y_1),\space p_2=(x_2,y_2)
p1=(x1,y1), p2=(x2,y2),使两点之间的距离
d
(
p
1
,
p
2
)
=
(
x
1
−
x
2
)
2
+
(
y
1
−
y
2
)
2
d(p_1,p_2)=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}
d(p1,p2)=(x1−x2)2+(y1−y2)2 在所有
S
S
S 中点对间最小。
最直接的传统算法就是简单检验所有可能的
n
(
n
−
1
)
2
\frac{n(n-1)}{2}
2n(n−1) 个距离并返回最短间距的点对。但是在这里我们将利用分治策略,设计一个时间复杂度为
Θ
(
n
log
n
)
\Theta(n\log n)
Θ(nlogn) 的算法来解决最接近点对问题(注意是返回点对之间的距离,而不是找到具有最短距离的点对,这两者的难度是截然不同的)。下面我们来简单看一下算法思路:
- 以 x x x 坐标升序的标准对点集 S S S 中所有的点进行排序
- 用垂直线 L L L 将点集 S S S 大致划分为两个子集 S l , S r S_l,S_r Sl,Sr,使 ∣ S l ∣ = ⌊ ∣ S ∣ / 2 ⌋ , ∣ S r ∣ = ⌈ ∣ S ∣ / 2 ⌉ |S_l|=\lfloor|S|/2\rfloor,\space|S_r|=\lceil|S|/2\rceil ∣Sl∣=⌊∣S∣/2⌋, ∣Sr∣=⌈∣S∣/2⌉。设垂直线 L L L 是经过 x x x 坐标为 S [ ⌊ n / 2 ⌋ ] S[\lfloor n/2\rfloor] S[⌊n/2⌋] 的垂直线,那么, S l S_l Sl 中所有的点都分布在 L L L 的左侧, S r S_r Sr 中所有的点都分布在 L L L 的右侧
- 对上述操作进行递归,求出两个子集 S l , S r S_l,S_r Sl,Sr 的最小间距 δ l \delta_l δl 和 δ r \delta_r δr
- 求出 S l S_l Sl 中的点与 S r S_r Sr 中的点之间的最小间距 δ ′ \delta' δ′
- 返回 min ( δ l , δ r , δ ′ ) \min(\delta_l,\delta_r,\delta') min(δl,δr,δ′)
我们知道绝大多数分治算法的组合步骤耗费最多,上述算法也是如此,核心工作就在于如何优化
δ
′
\delta'
δ′ 的求解操作,已知利用朴素算法计算
S
l
S_l
Sl 中每个点和
S
r
S_r
Sr 中每个点之间的距离需要
Ω
(
n
2
)
\Omega(n^2)
Ω(n2)。
设
δ
=
min
{
δ
l
,
δ
r
}
\delta=\min\{\delta_l,\delta_r\}
δ=min{δl,δr},若最接近点对由
S
l
S_l
Sl 中的点
p
l
p_l
pl 和
S
r
S_r
Sr 中的点
p
r
p_r
pr 组成,则
p
l
,
p
r
p_l,p_r
pl,pr 一定在划分线
L
L
L 的距离
δ
\delta
δ 内,换句话说,如果令
S
l
′
S_l'
Sl′ 和
S
r
′
S_r'
Sr′ 分别表示
S
l
S_l
Sl 和
S
r
S_r
Sr 中距离
L
L
L 小于
δ
\delta
δ 的点的集合,则
p
l
p_l
pl 一定在
S
l
′
S_l'
Sl′ 中,
p
r
p_r
pr 一定在
S
r
′
S_r'
Sr′ 中。尽管在最坏情况下,比较
S
l
′
S_l'
Sl′ 中的每一个点和
S
r
′
S_r'
Sr′ 中的每一个点的距离需要耗时
Ω
(
n
2
)
\Omega(n^2)
Ω(n2),此时
S
l
′
=
S
l
,
S
r
′
=
S
r
S_l'=S_l,\space S_r'=S_r
Sl′=Sl, Sr′=Sr。但是,这些比较操作并非都需要真正执行,实际上我们只需要比较
S
l
S_l
Sl 中每个点
p
p
p 和距离
p
p
p 在
δ
\delta
δ 内的那些点。假设
δ
′
⩽
δ
\delta'\leqslant\delta
δ′⩽δ,则存在
p
l
∈
S
l
′
,
p
r
∈
S
r
′
p_l\in S_l',\space p_r\in S_r'
pl∈Sl′, pr∈Sr′,有
d
(
p
l
,
p
r
)
=
δ
′
d(p_l,p_r)=\delta'
d(pl,pr)=δ′,进而知道
p
l
,
p
r
p_l,p_r
pl,pr 之间的垂直距离不超过
δ
\delta
δ。此外,由于
p
l
∈
S
l
′
,
p
r
∈
S
r
′
p_l\in S_l',\space p_r\in S_r'
pl∈Sl′, pr∈Sr′,易知这两点都在以
L
L
L 为中心的
δ
×
2
δ
\delta\times2\delta
δ×2δ 矩形区内或者边界上。设
T
T
T 是两个垂直带(距离
L
L
L
δ
\delta
δ 的两条直线)内点的集合,若在
δ
×
2
δ
\delta\times2\delta
δ×2δ 矩形区内,任意两点间的距离一定不超过
δ
\delta
δ,则这个矩形最多可以容纳
8
8
8 个点(可以结合鸽巢原理证明):其中最多
4
4
4 个点属于点集
S
l
S_l
Sl,
4
4
4 个点属于点集
S
r
S_r
Sr。当
S
l
S_l
Sl 中的一个点与
S
r
S_r
Sr 中的一个点在矩形的顶与
L
L
L 的交点处重合,并且
S
l
S_l
Sl 中的一个点与
S
r
S_r
Sr 中的一个点在矩形的底与
L
L
L 的交点处重合,则得到最大数。因此,
T
T
T 中的每个点最多需要和
T
T
T 中的
7
7
7 个点进行比较。
综上,我们可以得到如下算法:
- 输入:平面上 n n n 个点的集合 S S S
- 输出: S S S 中两点的最小距离
- 1. \hspace{1cm}1.\space 1. 以 x x x 坐标升序的准则对 S S S 中的点进行排序
- 2. Y ← \hspace{1cm}2.\space Y\leftarrow 2. Y←以 y y y 坐标升序的准则对 S S S 中的点进行排序
- 3. δ ← c p ( 1 , n ) \hspace{1cm}3.\space\delta\leftarrow cp(1,n) 3. δ←cp(1,n)
- 过程: c p ( l o w , h i g h ) cp(low,high) cp(low,high)
- 1. i f h i g h − l o w + 1 ⩽ 3 t h e n \hspace{1cm}1.\space if\space\space high-low+1\leqslant3\space\space then\space\space 1. if high−low+1⩽3 then 直接计算 δ \delta δ
- 2. e l s e \hspace{1cm}2.\space else 2. else
- 3. m i d ← ⌊ ( l o w + h i g h ) / 2 ⌋ \hspace{1cm}3.\space\hspace{1cm}mid\leftarrow\lfloor(low+high)/2\rfloor 3. mid←⌊(low+high)/2⌋
- 4. x 0 ← x ( S [ m i d ] ) \hspace{1cm}4.\space\hspace{1cm}x_0\leftarrow x(S[mid]) 4. x0←x(S[mid])
- 5. δ l ← c p ( l o w , m i d ) \hspace{1cm}5.\space\hspace{1cm}\delta_l\leftarrow cp(low,mid) 5. δl←cp(low,mid)
- 6. δ r ← c p ( m i d + 1 , h i g h ) \hspace{1cm}6.\space\hspace{1cm}\delta_r\leftarrow cp(mid+1,high) 6. δr←cp(mid+1,high)
- 7. δ ← min { δ l , δ r } \hspace{1cm}7.\space\hspace{1cm}\delta\leftarrow \min\{\delta_l,\delta_r\} 7. δ←min{δl,δr}
- 8. k ← 0 \hspace{1cm}8.\space\hspace{1cm}k\leftarrow0 8. k←0
- 9. f o r i ← 1 t o n { \hspace{1cm}9.\space\hspace{1cm}for\space\space i\leftarrow1\space\space to\space\space n\space\space\{ 9. for i←1 to n {从 Y Y Y 中抽取 T } T\} T}
- 10. i f ∣ x ( Y [ i ] ) − x 0 ∣ ⩽ δ t h e n \hspace{0.85cm}10.\space\hspace{2cm}if\space\space|\space x(Y[i])-x_0\space|\leqslant\delta\space\space then 10. if ∣ x(Y[i])−x0 ∣⩽δ then
- 11. k ← k + 1 \hspace{0.85cm}11.\space\hspace{3cm}k\leftarrow k+1 11. k←k+1
- 12. T [ k ] ← Y [ i ] \hspace{0.85cm}12.\space\hspace{3cm}T[k]\leftarrow Y[i] 12. T[k]←Y[i]
- 13. e n d i f \hspace{0.85cm}13.\space\hspace{2cm}end\space if 13. end if
- 14. e n d f o r { k \hspace{0.85cm}14.\space\hspace{1cm}end\space for\space\space\{ k 14. end for {k 是 T T T 的大小 } \} }
- 15. δ ′ ← 2 δ { \hspace{0.85cm}15.\space\hspace{1cm}\delta'\leftarrow2\delta\space\space\{ 15. δ′←2δ {将 δ ′ \delta' δ′ 初始化为大于 δ \delta δ 的值 } \} }
- 16. f o r i ← 1 t o k − 1 { \hspace{0.85cm}16.\space\hspace{1cm}for\space\space i\leftarrow1\space\space to\space\space k-1\space\space\{ 16. for i←1 to k−1 {计算 δ ′ } \delta'\} δ′}
- 17. f o r j ← i + 1 t o min { i + 7 , k } \hspace{0.85cm}17.\space\hspace{2cm}for\space\space j\leftarrow i+1\space\space to\space\space\min\{i+7,k\} 17. for j←i+1 to min{i+7,k}
- 18. i f d ( T [ i ] , T [ j ] ) < δ ′ t h e n δ ′ ← d ( T [ i ] , T [ j ] ) \hspace{0.85cm}18.\space\hspace{3cm}if\space\space d(T[i],T[j])<\delta'\space\space then\space\space\delta'\leftarrow d(T[i],T[j]) 18. if d(T[i],T[j])<δ′ then δ′←d(T[i],T[j])
- 19. e n d f o r \hspace{0.85cm}19.\space\hspace{2cm}end\space for 19. end for
- 20. e n d f o r \hspace{0.85cm}20.\space\hspace{1cm}end\space for 20. end for
- 21. δ ← min { δ , δ ′ } \hspace{0.85cm}21.\space\hspace{1cm}\delta\leftarrow\min\{\delta,\delta'\} 21. δ←min{δ,δ′}
- 22. e n d i f \hspace{0.85cm}22.\space end\space if 22. end if
- 23. r e t u r n δ \hspace{0.85cm}23.\space return\space\delta 23. return δ
时间复杂度分析:
易知递推关系式为
T
(
n
)
=
{
1
i
f
n
=
2
3
i
f
n
=
3
2
T
(
n
2
)
+
Θ
(
n
)
i
f
n
>
3
T(n)=\begin{cases}1\hspace{1cm}if\space\space n=2\\\\3\hspace{1cm}if\space\space n=3\\\\2T(\frac{n}{2})+\Theta(n)\hspace{1cm}if\space\space n>3\end{cases}
T(n)=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧1if n=23if n=32T(2n)+Θ(n)if n>3
⇒
T
(
n
)
=
Θ
(
n
log
n
)
\Rightarrow T(n)=\Theta(n\log n)
⇒T(n)=Θ(nlogn)