算法优化
参考 : 《数据结构与算法分析》. Mark Allen Weiss. 机械工业出版社
第二章:算法分析
- 算法( algorithm) 是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。
数学基础
四个定义
- 如果存在正常数
c
c
c 和
n
0
n_0
n0,使得当
N
≥
n
0
N\geq n_0
N≥n0 时
T
(
N
)
≤
c
f
(
N
)
T(N)\leq cf(N)
T(N)≤cf(N), 则记为
T
(
N
)
=
O
(
f
(
N
)
)
T(N) =O(f(N))
T(N)=O(f(N))
- 可以看成 T ( N ) T(N) T(N) 的增长率小于或等于 f ( N ) f(N) f(N) 的增长率; 时间复杂度常用 O ( ) O() O() 来衡量
- 如果存在正常数
c
c
c 和
n
0
n_0
n0, 使得当
N
≥
n
0
N\geq n_0
N≥n0 时
T
(
N
)
≥
c
g
(
N
)
T(N) \geq cg(N)
T(N)≥cg(N), 则记为
T
(
N
)
=
Ω
(
g
(
N
)
)
T(N) = \Omega (g(N))
T(N)=Ω(g(N))。
Ω
\Omega
Ω读音: Omega
- 可以看成 T ( N ) T(N) T(N) 的增长率大于或等于 g ( N ) g(N) g(N) 的增长率
-
T
(
N
)
=
θ
(
h
(
N
)
)
T(N) =\theta(h(N))
T(N)=θ(h(N)) 当且仅当
T
(
N
)
=
O
(
h
(
N
)
)
T(N)=O(h(N))
T(N)=O(h(N)) 和
T
(
N
)
=
Ω
(
h
(
N
)
)
T(N) =\Omega(h(N))
T(N)=Ω(h(N)) 。
θ
\theta
θ读音 : theta
- 可以看成 T ( N ) T(N) T(N) 的增长率等于 h ( N ) h(N) h(N)
- 如果对每一正常数
c
c
c 都存在常数
n
0
n_0
n0 , 使得当
N
>
n
0
N>n_0
N>n0 时
T
(
N
)
<
c
p
(
N
)
T(N) <cp(N)
T(N)<cp(N) , 则
T
(
N
)
=
o
(
p
(
N
)
)
T(N)=o(p(N))
T(N)=o(p(N))。有时也可以说, 如果
T
(
N
)
=
o
(
p
(
N
)
)
T(N) = o(p(N))
T(N)=o(p(N)) 且
T
(
N
)
≠
θ
(
p
(
N
)
)
T(N)\ne \theta (p(N))
T(N)=θ(p(N)), 则
(
p
(
N
)
)
(p(N))
(p(N))。 可以读成小o
- 可以看成 T ( N ) T(N) T(N) 的增长率小于 p ( N ) p(N) p(N) 的增长率; 与 O ( N ) O(N) O(N) 不同的是没有等于。
这些定义的目的是要在函数间建立一种相对的级别。给定两个函数, 通常存在一些点, 在这些点上一个函数的值小于另一个函数的值, 因此, 一般地宣称, 比如说 f ( N ) < g ( N ) f(N) <g(N) f(N)<g(N) , 是没有什么意义的。于是, 我们比较它们的相对增长率(relative rate of growth)。当将相对增长率应用到算法分析时,我们将会明白为什么它是重要的度量。
虽然对于较小的N值 1000 N 1000N 1000N 要比 N 2 N^2 N2 大, 但 N 2 N^2 N2 以更快的速度增长, 因此 N 2 N^2 N2 最终将是更大的函数。在这种情况下, N = 1000 N=1000 N=1000 是转折点。第一个定义是说, 最后总会存在某个点 n 0 n_0 n0 , 从它开始以后 c ⋅ f ( N ) c·f(N) c⋅f(N) 总是至少与 T ( N ) T(N) T(N) 一样大, 从而若忽略常数因子,则 f ( N ) f(N) f(N) 至少与 T ( N ) T(N) T(N) 一样大。在我们的例子中, T ( N ) = 1000 N T(N)=1 000N T(N)=1000N, f ( N ) = N 2 f(N)=N^2 f(N)=N2, n 0 = 1000 n_0=1000 n0=1000 而 c = 1 c=1 c=1 ,我们也可以让 n 0 = 10 n_0=10 n0=10 而 c = 100 c=100 c=100 因此,可以说 1000 N = 0 ( N 2 ) 1000N=0(N^2) 1000N=0(N2) (N平方级)。这种记法称为大O标记法。人们常常不说 “…级的”,而是说“大O…"。
法则 1 : 如果 T 1 ( N ) = O ( f ( N ) ) T_1(N) = O(f(N)) T1(N)=O(f(N)) 且 T 2 ( N ) = O ( g ( N ) ) T_2(N)=O(g(N)) T2(N)=O(g(N)), 那么
-
T
1
(
N
)
+
T
2
(
N
)
=
O
(
f
(
N
)
+
g
(
N
)
)
T_1(N) + T_2(N) = O(f(N)+g(N))
T1(N)+T2(N)=O(f(N)+g(N))
- 直观地和非正式地可以写成 m a x ( O ( f ( N ) ) , O ( g ( N ) ) ) max(O(f(N)) , O(g(N))) max(O(f(N)),O(g(N))), 即从中选出最大的作为结果
- T 1 ( N ) ∗ T 2 ( N ) = O ( f ( N ) ∗ g ( N ) ) T_1(N)*T_2(N) = O(f(N)*g(N)) T1(N)∗T2(N)=O(f(N)∗g(N))
法则 2 : 如果 T ( N ) T(N) T(N) 是一个 k k k 次多项式, 则 T ( N ) = θ ( N k ) T(N) = \theta (N^k) T(N)=θ(Nk)
法则 3 : 对于任意常数 k k k , l o g k N = O ( N ) log^k N=O(N) logkN=O(N)。 它告诉我们对数增长得非常缓慢
注意 : 将常数或低阶项放进大 O O O是非常坏的习惯。不要写成 T ( N ) = O ( 2 N 2 ) T(N) =O(2N^2) T(N)=O(2N2) 或 T ( N ) = O ( N 2 + N ) T(N) =O(N^2+N) T(N)=O(N2+N), 在这两种情形下, 正确的形式是 T ( N ) = O ( N 2 ) T(N) =O(N^2) T(N)=O(N2)。这就是说,在需要大 O O O表示的任何分析中, 各种简化都是可能发生的。低阶项一般可以被忽略, 而常数也可以弃掉。 此时, 要求的精度是很粗糙的。
典型的增长率
表格中从上往下函数的增长率在增大, 我们在优化算法的时候一般都是从下往上一级一级优化。 比如一般解法的时间复杂度为 O ( N ) O(N) O(N), 那能不能降到 O ( N 2 ) O(N^2) O(N2)呢? 当可以降到 O ( N 2 ) O(N^2) O(N2)的时候, 再去进一步考虑能否降到 O ( N l o g N ) O(NlogN) O(NlogN)或者 O ( N ) O(N) O(N)。
函数 | 名称 |
---|---|
c c c | 常数 |
l o g N logN logN | 对数 |
l o g 2 N log^2 N log2N | 对数平方的 |
N N N | 线性的 |
N l o g N NlogN NlogN | |
N 2 N^2 N2 | 二次的 |
N 3 N^3 N3 | 三次的 |
2 N 2^N 2N | 指数的 |
在计算机科学中,除非有特别的声明,否则所有的对数都是以2为底的。因此一般都省略2为底数的写法
比如 :当说排序时,普通排序算法的时间复杂度是 O ( N 2 ) O(N^{2}) O(N2) , 或者叫二次的。
问题的规模 :
不难发现当运行时间成指数级的, 随着输入的n增大, 问题的规模成指数级爆炸增长, 这样的运行时间是十分糟糕的,因此对算法的时间复杂度的优化是十分有必要的。
运行时间
-
T a v g ( N ) T_{avg}(N) Tavg(N) : 算法对于输入量N所花费的平均运行时间
-
T w o r s t ( N ) T_{worst}(N) Tworst(N) : 算法对于输入量N所花费的最坏情况的运行时间
-
显然 T a v g ( N ) ≤ T w o r s t ( N ) T_{avg}(N) \leq T_{worst}(N) Tavg(N)≤Tworst(N) ; 平均情形性能常常反映典型的行为, 而最坏情形的性能则代表对任何可能输入的性能的一种保证。
一般法则
法则一 : for循环
:一个for循环的运行时间至多是该for循环内部那些语句(包括测试)的运行时间乘以迭代的次数。
法则二 : 嵌套的for循环
: 从里向外分析这些循环。在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有的for循环的大小的乘积。
法则三 : 顺序语句
: 将各个语句的运行时间求和即可(这意味着,其中的最大值就是所得的运行时间)
法则四 : if/else语句
:
if(condition)
S1
else
S2
一个if /else
语句的运行时间从不超过判断的运行时间再加上 S1 和 S2 中运行时间长者的总的运行时间。
分析的基本策略是从内部(或最深层部分)向外展开工作的。如果有方法调用, 那么要首先分析这些调用。
最大子序列和问题
经典的算法优化问题
- 问题 : 给定(可能有负数)整数
A
1
,
A
2
,
.
.
.
,
A
N
A_1, A_2, ..., A_N
A1,A2,...,AN, 求
∑
k
=
i
j
A
k
\sum\limits_{k=i}^j A_k
k=i∑jAk 的最大值。(为了方便起见,设定当所有整数均为负数时, 则最大子序列和为0)。
- 比如 : 对于输入 − 2 , 11 , − 4 , 13 , − 5 , − 2 -2, 11, -4, 13, -5, -2 −2,11,−4,13,−5,−2, 最大子序列和为20 (从 A 2 到 A 4 A_2到A_4 A2到A4)
- 子序列是指不改变原序列的连续顺序, 从头部或者是尾部开始进行删减得到的序列
算法思想 :
做法一 : 穷举法 (也称暴力解法)
-
逐个遍历求和, 比较子序列和大小, 选择最大的一组子序列和
-
时间复杂度 T ( N ) = O ( N 3 ) T( N ) = O( N^3 ) T(N)=O(N3)
int MaxSubsequenceSum ( const int A[ ], int N )
{
int ThisSum, MaxSum, i, j, k;
/* 1*/ MaxSum = 0; /* initialize the maximum sum */
/* 2*/ for( i = 0; i < N; i++ ) /* start from A[i] */
/* 3*/ for( j = i; j < N; j++ ) { /* end at A[j] */
/* 4*/ ThisSum = 0;
/* 5*/ for( k = i; k <= j; k++ )
/* 6*/ ThisSum += A[ k ]; /* sum from A[i] to A[j] */
/* 7*/ if ( ThisSum > MaxSum )
/* 8*/ MaxSum = ThisSum; /* update max sum */
} /* end for-j and for-i */
/* 9*/ return MaxSum;
}
分析 :
运行时间为 O ( N 3 ) O( N^3 ) O(N3) , 完全取决于带注释的第5和第6行代码, 他们由三重嵌套for循环中的 O ( 1 ) O(1) O(1) 语句组成, 最外层循环大小为 N N N 。次外层循环大小为 N − i N-i N−i , 它可能要小,但也可能是 N N N。如果假设最坏的情况下, 这可能会使得最终的界有些大。最里层循环的大小为 j − i + 1 j-i+1 j−i+1 我们也要假设它的大小为 N N N。因此总数为 O ( 1 ⋅ N ⋅ N ⋅ N ) = O ( N 3 ) O(1·N·N·N) =O(N^3) O(1⋅N⋅N⋅N)=O(N3)。带注释的第1行总共的开销只是 0 ( 1 0(1 0(1), 而带注释的第7和8行也只不过总共开销 O ( N 2 ) O(N^2) O(N2), 因为它们只是两层循环内部的简单表达式。
但这样的计算不够精确。更加精确地分析应该是由 ∑ i = 0 N − 1 ∑ j = i N − 1 ∑ k = i j 1 \sum\limits_{i=0}^{N-1} \sum\limits_{j=i}^{N-1} \sum\limits_{k=i}^{j} 1 i=0∑N−1j=i∑N−1k=i∑j1 得到的结果, 对该式子从内到外求值。
演算一下 : ∑ i = 0 N − 1 ∑ j = i N − 1 ∑ k = i j 1 \sum\limits_{i=0}^{N-1} \sum\limits_{j=i}^{N-1} \sum\limits_{k=i}^{j} 1 i=0∑N−1j=i∑N−1k=i∑j1
- ∑ k = i j 1 = j − i + 1 \sum\limits_{k=i}^{j} 1= j-i+1 k=i∑j1=j−i+1 ; 因为从第i个数到第j个数中间一共有 ( j − i + 1 ) (j-i+1) (j−i+1) 个数相加, 即 ( j − i + 1 ) × 1 (j-i+1)\times1 (j−i+1)×1
- 将 ∑ k = i j 1 = j − i + 1 \sum\limits_{k=i}^{j} 1= j-i+1 k=i∑j1=j−i+1 代入到式子得到 ∑ j = i N − 1 ( j − i + 1 ) \sum\limits_{j=i}^{N-1}(j-i+1) j=i∑N−1(j−i+1), 可以把 ( − i + 1 ) (-i+1) (−i+1)看作常量a, 那么就有 ∑ j = i N − 1 ( j + a ) = ( i + a ) + ( i + 1 + a ) + ( i + 2 + a ) + . . . + ( N − 1 + a ) = i + ( N − 1 ) 2 ( N − 1 ) + a × ( N − 1 ) = ( N − i + 1 ) ( N − i ) 2 \sum\limits_{j=i}^{N-1}(j+a) = (i+a)+(i+1+a)+(i+2+a)+...+(N-1+a) = \frac{i+(N-1)}{2}(N-1) + a \times(N-1)=\frac{(N-i+1)(N-i)}{2} j=i∑N−1(j+a)=(i+a)+(i+1+a)+(i+2+a)+...+(N−1+a)=2i+(N−1)(N−1)+a×(N−1)=2(N−i+1)(N−i),
- 再将 ( N − i + 1 ) ( N − i ) 2 \frac{(N-i+1)(N-i)}{2} 2(N−i+1)(N−i)代入式子有 ∑ i = 0 N − 1 ( N − i + 1 ) ( N − i ) 2 \sum\limits_{i=0}^{N-1} \frac{(N-i+1)(N-i)}{2} i=0∑N−12(N−i+1)(N−i) , 因为 ∑ i = 0 N − 1 \sum\limits_{i=0}^{N-1} i=0∑N−1不方便计算, 所以化为 ∑ i = 1 N \sum\limits_{i=1}^{N} i=1∑N , 但是怎样能让他们的值不变呢, 细心的话就会发现, 他们都只是都加了1,这里可以把 N N N当做是常数, 原来的 i i i的范围是 [ 0 , N − 1 ] [0,N-1] [0,N−1], 现在变成 [ 1 , N ] [1,N] [1,N], 令 t = i + 1 t = i+1 t=i+1, 即 i = t − 1 i = t-1 i=t−1, 将 i = t − 1 i = t-1 i=t−1代入进去则有 ∑ i = 0 N − 1 ( N − i + 1 ) ( N − i ) 2 = ∑ t = 1 N ( N − ( t − 1 ) + 1 ) ( N − ( t − 1 ) ) 2 = ∑ t = 1 N ( N − t + 2 ) ( N − t + 1 ) ) 2 \sum\limits_{i=0}^{N-1} \frac{(N-i+1)(N-i)}{2} = \sum\limits_{t=1}^{N} \frac{(N-(t-1)+1)(N-(t-1))}{2} = \sum\limits_{t=1}^{N} \frac{(N-t+2)(N-t+1))}{2} i=0∑N−12(N−i+1)(N−i)=t=1∑N2(N−(t−1)+1)(N−(t−1))=t=1∑N2(N−t+2)(N−t+1)) 然后换元即可得到 ∑ i = 1 N ( N − i + 2 ) ( N − i + 1 ) ) 2 \sum\limits_{i=1}^{N} \frac{(N-i+2)(N-i+1))}{2} i=1∑N2(N−i+2)(N−i+1)), 验算结果和+1之前的一样。
- 将 ∑ i = 1 N ( N − i + 2 ) ( N − i + 1 ) ) 2 \sum\limits_{i=1}^{N} \frac{(N-i+2)(N-i+1))}{2} i=1∑N2(N−i+2)(N−i+1)) 中的常数系数 1 2 \frac{1}{2} 21 提取出来, 得到 1 2 ∑ i = 1 N ( N − i + 2 ) ( N − i + 1 ) \frac{1}{2} \sum\limits_{i=1}^{N} (N-i+2)(N-i+1) 21i=1∑N(N−i+2)(N−i+1) ; 因为 ( N − i + 2 ) ( N − i + 1 ) = N 2 + i 2 − ( 2 N + 3 ) i + 3 N + 2 (N-i+2)(N-i+1)=N^2+i^2-(2N+3)i+3N+2 (N−i+2)(N−i+1)=N2+i2−(2N+3)i+3N+2, N N N为常数,求和中可以提取出来, 所以就有 1 2 ∑ i = 1 N i 2 − ( N + 3 2 ) ∑ i = 1 N i + 1 2 ( N 2 + 3 N + 2 ) ∑ i = 1 N 1 \frac{1}{2}\sum\limits_{i=1}^{N}i^2 - (N+\frac{3}{2}) \sum\limits_{i=1}^{N}i+\frac{1}{2}(N^2+3N+2)\sum\limits_{i=1}^{N}1 21i=1∑Ni2−(N+23)i=1∑Ni+21(N2+3N+2)i=1∑N1, 又 ∑ i = 1 N i 2 = N ( N + 1 ) ( 2 N + 1 ) 6 \sum\limits_{i=1}^{N}i^2=\frac{N(N+1)(2N+1)}{6} i=1∑Ni2=6N(N+1)(2N+1)这个是公式1 就懒得推导了 , ∑ i = 1 N i = N ( N + 1 ) 2 \sum\limits_{i=1}^{N} i = \frac{N(N+1)}{2} i=1∑Ni=2N(N+1), 这个就不用证明了, 等差数列求和(都懂).
所以推导的结果为 :
∑ i = 0 N − 1 ∑ j = i N − 1 ∑ k = i j 1 = ∑ i = 0 N − 1 ∑ j = i N − 1 ( j − i + 1 ) = ∑ i = 0 N − 1 ( N − i + 1 ) ( N − i ) 2 = ∑ i = 1 N ( N − i + 1 ) ( N − i + 2 ) 2 = 1 2 ∑ i = 1 N i 2 − ( N + 3 2 ) ∑ i = 1 N i + 1 2 ( N 2 + 3 N + 2 ) ∑ i = 1 N 1 = 1 2 N ( N + 1 ) ( 2 N + 1 ) 6 − ( N + 3 2 ) N ( N + 1 ) 2 + N 2 + 3 N + 2 2 N = N 3 + 3 N 2 + 2 N 6 \begin {aligned} \sum\limits_{i=0}^{N-1} \sum\limits_{j=i}^{N-1} \sum\limits_{k=i}^{j} 1&= \sum\limits_{i=0}^{N-1} \sum\limits_{j=i}^{N-1}(j-i+1) \\ &=\sum\limits_{i=0}^{N-1} \frac{(N-i+1)(N-i)}{2} \\ &= \sum\limits_{i=1}^{N} \frac{(N-i+1)(N-i+2)}{2} \\ &=\frac{1}{2}\sum\limits_{i=1}^{N}i^2 - (N+\frac{3}{2}) \sum\limits_{i=1}^{N}i+\frac{1}{2}(N^2+3N+2)\sum\limits_{i=1}^{N}1 \\ &= \frac{1}{2} \frac{N(N+1)(2N+1)}{6} - (N+\frac{3}{2})\frac{N(N+1)}{2}+\frac{N^2+3N+2}{2} N \\ &= \frac{N^3+3N^2+2N}{6} \end {aligned} i=0∑N−1j=i∑N−1k=i∑j1=i=0∑N−1j=i∑N−1(j−i+1)=i=0∑N−12(N−i+1)(N−i)=i=1∑N2(N−i+1)(N−i+2)=21i=1∑Ni2−(N+23)i=1∑Ni+21(N2+3N+2)i=1∑N1=216N(N+1)(2N+1)−(N+23)2N(N+1)+2N2+3N+2N=6N3+3N2+2N
做法二
改进算法
-
我们可以通过消除一个for循环来避免三次for循环运行的时间
-
最里层循环可以省略掉
-
T ( N ) = O ( N 2 ) T( N ) = O( N^2 ) T(N)=O(N2)
int MaxSubsequenceSum ( const int A[ ], int N )
{
int ThisSum, MaxSum, i, j;
/* 1*/ MaxSum = 0; /* initialize the maximum sum */
/* 2*/ for( i = 0; i < N; i++ ) { /* start from A[ i ] */
/* 3*/ ThisSum = 0;
/* 4*/ for( j = i; j < N; j++ ) { /* end at A[ j ] */
/* 5*/ ThisSum += A[ j ]; /* sum from A[i] to A[j] */
/* 6*/ if ( ThisSum > MaxSum )
/* 7*/ MaxSum = ThisSum; /* update max sum */
} /* end for-j */
} /* end for-i */
/* 8*/ return MaxSum;
}
做法三
进一步改进
-
分治法, 用递归解决 : 将问题分成两个大致相等的子问题, 然后递归求解
-
T ( N ) = O ( N l o g N ) T( N ) = O( NlogN ) T(N)=O(NlogN)
private static int maxSumRec(int a[], int left, int right)
{
if(left==right)
if(a[left]>0)
return a[left];
else
return 0;
int center = (left+right)/2;
int maxLeftSum = maxSumRec(a, left, center); //左半部分递归调用, 选出最大值
int maxRightSum = maxSumRec(a, center+1, right); //右半部分递归调用, 选出最大值
int maxLeftBorderSum = 0, leftBorderSum = 0;
for(int i=center; i>=left; i--){
leftBorderSum += a[i];
if(leftBorderSum>maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}
int maxRightBorderSum = 0, rightBorderSum = 0;
for(int i=center+1; i<=right; i++){
rightBorderSum += a[i];
if(rightBorderSum>maxRightBorderSum)
maxRightBorderSum = rightBorderSum;
}
return max3(maxLeftSum, maxRightSum, maxLeftBorderSum+maxRightBorderSum);//选出三个数中最大的数
}
public static int max3(int num1, int num2, int num3){
// return (num1>Math.max(num2,num3))?num1:Math.max(num2,num3); //比较三个数的大小, 这里就懒得写太多, 直接用java中Math类的静态方法max(); 但这个只能比较两个数的大小, 所以又用了三目运算来比较大小
return Math.max(num1, Math.max(num2,num3)); //全都直接用Math类的max方法
}
public static int maxSubSum3(int[] a){
return maxSumRec(a, 0, a.length-1);
}
时间分析 :
令 T ( N ) T(N) T(N)是求解大小为 N N N的最大子序列和问题所花费的时间。如果 N = 1 N=1 N=1, 则执行程序第3行到第7行花费某个常数时间量, 我们设为一个时间单位。于是, T ( 1 ) = 1 T(1)=1 T(1)=1。否则, 程序必须运行两个递归调用,即在第14行和第25行之间的两个for循环, 以及某个小的簿记量, 如第9行和第13行。这两个for循环总共接触到数组 A A A, 从 A 0 A_0 A0到 A N − 1 A_{N-1} AN−1的每一个元素,而在循环内部的工作量是常量, 因此, 在第14到25行花费的时间为 O ( N ) O(N) O(N)。在第3行到第9行和27行上的程序的工作量都是常量, 从而与 O ( N ) O(N) O(N)相比可以忽略。其余就是第10、11行上的递归调用运行的工作。这两行求解大小为 N / 2 N/2 N/2的子序列问题 (假设 N N N是偶数)。因此, 这两行每行花费 T ( N / 2 ) T(N/2) T(N/2)个时间单元, 共花费 2 × T ( N / 2 ) 2\times T(N/2) 2×T(N/2)个时间单元。因此总的时间为 2 × T ( N / 2 ) + O ( N ) 2\times T(N/2)+O(N) 2×T(N/2)+O(N)
得到方程组 :
{ T ( 1 ) = 1 T ( N ) = 2 T ( N / 2 ) + O ( N ) \begin{cases} T(1) = 1 \\ \\ T(N) = 2T(N/2) + O(N) \end{cases} ⎩⎪⎨⎪⎧T(1)=1T(N)=2T(N/2)+O(N)
为了简化计算, 可以用 N N N代替 O ( N ) O(N) O(N), 所以 T ( N ) = 2 T ( N / 2 ) + N T(N) = 2T(N/2) + N T(N)=2T(N/2)+N, 因为 T ( 1 ) = 1 T(1) = 1 T(1)=1, 则可推出 T ( 2 ) = 2 T ( 1 ) + 2 = 4 = 2 × 2 , T ( 4 ) = 2 T ( 2 ) + 4 = 12 = 4 × 3 , T ( 8 ) = 2 T ( 4 ) + 8 = 32 = 8 × 4 , . . . . T(2)= 2T(1)+2=4=2\times2, T(4)=2T(2)+4=12=4\times3, T(8)=2T(4)+8=32=8\times 4, .... T(2)=2T(1)+2=4=2×2,T(4)=2T(2)+4=12=4×3,T(8)=2T(4)+8=32=8×4,.... 依次类推可推出 T ( N ) = N × ( k + 1 ) T(N)=N\times(k+1) T(N)=N×(k+1)可用数学归纳法证明, 假设 N = 2 k N=2^k N=2k (因为前面假设 N N N为偶数), 则 T ( N ) = N × ( k + 1 ) = N l o g N + N T(N)=N\times(k+1)=NlogN+N T(N)=N×(k+1)=NlogN+N
则有 : T ( N ) = 2 T ( N / 2 ) + N = N × ( k + 1 ) = N l o g N + N = O ( N l o g N ) \begin {aligned}T(N) &= 2T(N/2) + N \\ &= N \times (k+1) \\ &= NlogN+N \\ &=O(NlogN) \end {aligned} T(N)=2T(N/2)+N=N×(k+1)=NlogN+N=O(NlogN)
这个分析假设 N N N是偶数, 否则 N / 2 N/2 N/2就不确定了。通过该分析的递归性质可知, 实际上只有, 当 N N N是2的幂时结果才是合理的, 否则我们最终要得到大小不是偶数的子问题, 方程就是无效的了。当 N N N不是2的幂时, 我们多少需要更加复杂一些的分析, 但是大 O O O的结果是不变的。
做法四 :
将时间复杂度降为 O ( N ) O( N ) O(N)
动态规划的做法:
-
设前 n n n 项数字的最大子序列和为 f ( n ) f(n) f(n), 则:
f ( n ) = { 0 , n = 0 a n , f ( n − 1 ) ≤ 0 f ( n − 1 ) + a n , f ( n − 1 ) > 0 f(n)= \begin{cases} 0{\quad},{\quad} n=0 \\ a_n{\quad}, {\quad}f(n-1)\leq0 \\ f(n-1)+a_n {\quad}, {\quad}f(n-1) > 0 \end{cases} f(n)=⎩⎪⎨⎪⎧0,n=0an,f(n−1)≤0f(n−1)+an,f(n−1)>0
-
可以看出只有一层for循环, 所以时间复杂度为 O ( N ) O(N) O(N)
-
T ( N ) = O ( N ) T( N ) = O( N ) T(N)=O(N)
int MaxSubsequenceSum( const int A[ ], int N )
{
int ThisSum, MaxSum, j;
/* 1*/ ThisSum = MaxSum = 0;
/* 2*/ for ( j = 0; j < N; j++ ) {
/* 3*/ ThisSum += A[ j ];
/* 4*/ if ( ThisSum > MaxSum )
/* 5*/ MaxSum = ThisSum;
/* 6*/ else if ( ThisSum < 0 )
/* 7*/ ThisSum = 0;
} /* end for-j */
/* 8*/ return MaxSum;
}
二分查找
- 折半查找 (binary search)
- 折半查找:给定一个整数 X X X和整数 A 0 , A 1 , . . , A N − 1 A_0, A_1,.., A_{N-1} A0,A1,..,AN−1, 后者已经预先排序并在内存中, 求下标 i i i使得 A i = X A_i=X Ai=X, 如果 X X X不在数据中, 则返回 i = − 1 i=-1 i=−1 。
int BinarySearch ( const ElementType A[ ],
ElementType X, int N )
{
int Low, Mid, High;
/* 1*/ Low = 0; High = N - 1;
/* 2*/ while ( Low <= High ) {
/* 3*/ Mid = ( Low + High ) / 2;
/* 4*/ if ( A[ Mid ] < X )
/* 5*/ Low = Mid + 1;
else
/* 6*/ if ( A[ Mid ] > X )
/* 7*/ High = Mid - 1;
else
/* 8*/ return Mid; /* Found */
} /* end while */
/* 9*/ return NotFound; /* NotFound is defined as -1 */
}
欧几里得算法
- 在欧几里德算法中,一次迭代中余数并不按照一个常数因子递减。但可以证明,在两次迭代后,余数最多是先前值的一半,因此迭代次数至多是 2 l o g N = O ( l o g N ) 2logN = O( log N ) 2logN=O(logN)
- 定理 : 如果
M
>
N
M>N
M>N , 则
M
M
M mod
N
<
M
/
2
N<M/2
N<M/2 .
- 证明: 存在两种情形。如果 N < M / 2 N<M/2 N<M/2, 则由于余数小于 N N N, 故定理在这种情形下成立。另一种情形是 N > M / 2 N>M/2 N>M/2, 但是此时 M M M仅含有一个 N N N从而余数为 M − N < M / 2 M-N<M/2 M−N<M/2, 定理得证。
unsigned int Gcd ( unsigned int M, unsigned int N )
{
unsigned int Rem;
/* 1*/ while ( N > 0 ) {
/* 2*/ Rem = M % N;
/* 3*/ M = N;
/* 4*/ N = Rem;
} /* end while */
/* 5*/ return M;
}
幂运算
- 时间复杂度 : O ( N ) O(N) O(N)
long int Pow ( long int X, unsigned int N )
{
long int P = 1;
while ( N -- ) P *= X;
return P;
}
改进 :
- 时间复杂度 : 2 l o g N = O ( l o g N ) 2logN = O(logN) 2logN=O(logN)
long int Pow ( long int X, unsigned int N )
{
/* 1*/ if ( N == 0 )
/* 2*/ return 1;
/* 3*/ if ( N == 1 )
/* 4*/ return X;
/* 5*/ if ( IsEven( N ) )
/* 6*/ return Pow( X, N / 2 ) * Pow( X, N / 2 );
else
/* 7*/ return Pow( X, N / 2 ) * Pow( X, N / 2 ) * X;
}
证明如下: ↩︎