时空复杂度分析与主定理
引言与约定
本文从实用主义出发,试图介绍时空复杂度分析这一 oier 必备的技能并略作推广,希望能整合出一篇全面、易懂、实用的复杂度分析指南。
读者应该注意,复杂度分析基于理论,并不能保证你的程序一定会按照你所预期的效率执行,请务必注意常数因子的影响。
约定 \(n\) 表示问题规模。
约定 \(T(n)\) 表示程序的运行时间。
约定 \(\log n = \log_2n\)
时间复杂度
一、时间复杂度定义与渐进符号
如果要估计一个程序运行效率,最直接的方法是用一个计数器直接计算运行次数,但是这种方法并不通用并且难以应对超大规模的数据,我们更需要一种相对方便的方法,为此我们可以舍弃一些精确度。所以我们就通过时间复杂度来评估一个算法随数据规模增大所需运行时间的增长趋势。
下面给出三个常见常用的渐近符号:
符号 | 含义 |
---|---|
\(\Theta\):渐进紧确界符号 | \(T(n)=\Theta(n)\) |
\(O\):渐进上界符号 | \(T(n) \leq O(n)\) |
\(\Omega\):渐进上界符号 | \(T(n) \geq \Omega(n)\) |
\(OI\) 中一般只估计时间复杂度上界即可,常见的复杂度关系如下:
\(O(wys)<O(1)<O(\log n)<O(\sqrt n)<O(n)<O(n \log n)<O(n^2)<O(n^3)<O(k^n)<O(n!)\)
在使用渐进符号的时候,忽略低次项及最高阶项的系数,只考虑最高阶项本身,比如一个算法 \(T(n) = 3n^2+2n+1\),那么它的时间复杂度就是 \(O(n^2)\)。
下面我们讨论一些复杂度的计算方法。
二、主定理
初步理解:
考虑这样一个递归式:
这里 \(a\) 表示子问题数量,\(\frac{n}{b}\) 表示子问题规模,\(f(n)\) 表示计算子问题所需的时间,主定理给出了如上递归式渐进紧确界的计算方法,这里不加证明的给出结论:
- 如果存在 \(\varepsilon > 0\) 使得 \(f(n) = O(n^{\log_ba-\varepsilon})\),则 \(T(n) = \Theta(n^{log_ba})\)
- 如果存在 \(k \ge 0\) ,使得 \(f(n) = \Theta(n^{\log_ba}\log^k n)\),则 \(T(n) = \Theta(n^{\log_ba}\log^{k+1} n)\)
- 如果存在 \(\varepsilon > 0\),使得 \(f(n) = \Omega(n^{\log_ba+\varepsilon})\),且满足存在一个常数 \(c \lt 1\) 和足够大的 \(n\) 有 \(af(\frac{n}{b})\le cf(n)\),则 \(T(n) = \Theta(f(n))\)
如果觉得上面的定理比较抽象的话,这里还有一份不严谨但胜在直观的理解:
- \(n^{\log_ba} > f(n) \Rightarrow T(n) = O(n^{\log_ba})\)
- \(f(n) = n^{\log_ba}\log^kn \Rightarrow T(n) = O(n^{\log_ba}\log^{k+1}n)\)
- \(n^{\log_ba} < f(n) \Rightarrow T(n) = O(f(n))\)
从直观上理解就是比较了 \(n^{\log_ba}\) 和 \(f(n)\) 对程序的影响来决定复杂度。
此外还有一些需要注意的限制:如果要应用第一种情况的话,要满足多项式意义下的 \(n^{\log_ba}>f(n)\);如果要应用第三种情况的话,还要满足正则条件 \(af(\frac{n}{b})\le cf(n)\)。
多项式意义下 \(f(x) > g(x) \Leftrightarrow \exist{\varepsilon},f(x)>g(x)*n^{\varepsilon}\)
小练习:
- 试计算二分查找的复杂度
首先写出递归式 \(T(n) = T(\frac{n}{2}) + O(1),a=1,b=2\),因为存在 \(k = 0\) 使得 \(n^{\log_21}\log^0 n = O(1)\),满足情况二,所以 \(T(n) = O(\log n)\)
-
试计算递归式 \(T(n) = T(\frac{n}{2}) + n\)
取 \(a=1,b=2\) ,因为存在 \(\varepsilon = 1,n^{log_21+\varepsilon} = n\),满足情况三,所以 \(T(n) = O(n)\)
-
试计算递归式 \(T(n) = 2T(\frac{n}{2}) + 1\)
取 \(a = 2,b = 2\),因为存在 \(\varepsilon = 1,n^{\log_2 2 - \varepsilon} = 1\),满足情况一,所以 \(T(n) = O(n)\)
-
试计算递归式 \(T(n) = 2T(\sqrt n) + \log n\)
这里递归式不是标准的形式,所以考虑简单的代数推导一下:
令 \(m = \log n\),\(T(2^m)=T(2^{\frac{m}{2}}) + m\),令 \(S(m) = T(2^m) = T(n)\),\(S(m) = 2S(\frac{m}{2})+m\)。
这里满足形式二 ,\(S(m) = m \log m\),所以 \(T(n) = \log n \log \log n\)
空间复杂度
一、c++ 中常用数据类型的空间占用
打个表:
数据类型(unsigned/signed) | 空间占用 | 256MB下最大变量级别 |
---|---|---|
char | 1byte | 2e8 |
short int | 2byte | 1e8 |
int | 4byte | 6e7 |
long long | 8byte | 3e7 |
double | 8byte | 3e7 |
float | 4byte | 6e7 |
bool | 1byte | 1e8 |
仅供参考,具体大小视环境而定。
二、如何估计空间占用(静态)
算是一个小技巧,就写在这里了。
bool flag1;
//declared here
bool flag2;
std::cout<< (&flag2 - &flag1) / 1024 / 1024;
原理就是静态变量的内存占用是连续的,用最后一个变量的地址减去第一个变量的地址就是占用的 byte 数,最后单位是 MB。
参考文献&&鸣谢
参考了包括公式、定义、例题等内容。
时空复杂度分析及master定理 - Chanis 的博客 - 洛谷博客 (luogu.com.cn)
从主方法到Akra-Bazzi定理 - 知乎 (zhihu.com)
《算法艺术与信息学竞赛》----刘汝佳、黄亮
《算法导论》----Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein。