复杂度分析
算法
我们可以认为解决任何一个问题可以分为多个处理点,该处理点需要寻找向前的多个处理点或者向后的多个处理点。
而在回溯中,我们可能为该点先选定某个状态,然后直接按照该状态递归进入下一层,在所有后向的处理点处理完毕之后,将该状态以及该状态相关的
搜索
回溯时,如果我么希望找出全部解,则每个迭代函数的返回值为空。如果仅希望找到一个解,那么迭代函数返回值为
贪心策略是我们常用的算法策略之一,譬如在最小生成树的
动态规划问题核心在于确定变量与状态转化公式。典型的变量譬如两个字符串比较中的截取的字串长度
而在
动态规划类题目每种情况也是仅有一个可行解,因此可以利用分治
总结而言,一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的:
- 每个阶段只有一个状态
-> 递推; - 每个阶段的最优状态都是由上一个阶段的最优状态得到的
-> 贪心; - 每个阶段的最优状态是由之前所有阶段的状态的组合得到的
-> 搜索; - 每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的
-> 动态规划。
课程大纲暂定如下,后续会有微调,具体以实际上课内容为准:
1、
2、链表、递归、栈
链表环、共同结点等的判定
直方图最大矩形面积
3、字符串
循环位移问题
详解最长公共子序列
4、数组
5、树
二叉搜索树删除
二叉树的遍历
6、图
7、查找排序
无
8、贪心和动态规划
从机器学习的角度统一贪心法和动态规划
贪心的应用:任务安排问题、甘特图
走棋盘
9、概率组合数论统计
现实中的概率问题:以麻将为例
用动态规划推导组合公式
圆内
10、
时间复杂度
时间复杂度用来检验某个算法处理一定量的数据要花多长时间。为了描述这个复杂度,主要有相关的三种符号记法:
- 第一种叫
Big O notation ,它给出了运行时间的”渐进上界“,也就是算法在最坏情况下运行时间的上限。它的定义如下:对于f(n) 和g(n) ,若存在常数c 和N0 ,使得对于所有n > N0 ,都有|f(n)| < c * g(n) ,则称f(n) 为O(g(n)) 。 - 第三种叫做
Big Ω notation ,它给出了运行时间的“渐进下界”,也就是算法在最坏情况下运行时间的下限。它的定义如下:对于f(n) 和g(n) ,若存在常数c 和N0 ,使得对于所有n > N0 ,都有|f(n)| > c * g(n) ,则称f(n) 为Ω(g(n)) 。 - 第三种叫
Big Θ notation ,它确定了运行时间的”渐进确界“。定义如下:对于f(n) 和g(n) ,若存在常数c 和N0 ,对于所有n> N0 ,都有|f(n)| = c * g(n) ,则称f(n) 为Θ 为Θ(g(n)) 。
在平常的算法分析中最常用到的是
图中可以看到不同类型的复杂度的演变过程,我用了对数尺来建这个图。具体点儿说,数据量以很快的速度从
- 绿:
O(1) 或者叫常数阶复杂度,保持为常数( 要不人家就不会叫常数阶复杂度了) 。 - 红:
O(log(n)) 对数阶复杂度,即使在十亿级数据量时也很低。 - 粉:最糟糕的复杂度是
O(n^2) ,平方阶复杂度,运算数快速膨胀。 - 黑和蓝:另外两种复杂度
( 的运算数也是) 快速增长。
O
Common Algorithms Complexity: 常见算法复杂度
Data Structure Operations
Array Sorting Algorithms
Graph Operations
Heap Operations
分析方法
一个算法的运行时间也就是执行所有程序语句的耗时总和。然而在实际的分析中,我们并不需要考虑所有程序语句的运行时间,我们应该做的是集中注意力于最耗时 的部分,也就是执行频率最高而且最耗时的操作。也就是说,在对一个程序的时间复杂度进行分析前,我们要先确定这个程序中哪些语句的执行占用的它的大部分执 行时间,而那些尽管耗时大但只执行常数次
public static int count(int[] a) {
int N = a.length;
int cnt = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
for (int k = j + 1; k < N; k++) {
if (a[i] + a[j] + a[k] == 0) {
cnt++;
}
}
}
}
return cnt;
}
首先我们看到以上代码的第
- 寻找执行次数多的语句作为决定运行时间的
[ 关键操作]; - 分析关键操作的执行次数。
倍率实验
下面我们来介绍一下算法
我们用
~f(N) 表示所有随着N 的增大除以f(N) 的结果趋于1 的函数。用g(N)~f(N) 表示g(N) / f(N) 随着N 的增大趋近于1 。通常我们用到的近似方式都是g(N) ~ a * f(N) 。我们将f(N) 称为g(N) 的增长数量级。
我们还是拿
现在,我们来正式介绍倍率实验

public class DoublingTest {
public static double timeTrial(int N) {
int MAX = 1000000;
int[] a = new int[N];
for (int i = 0; i < N; i++) {
a[i] = StdRandom.uniform(-MAX, MAX);
}
long startTime = System.currentTimeMillis();
int count = ThreeSum.count(a);
long endTime = System.currentTimeMillis();
double time =(endTime - startTime) / 1000.0;
return time;
}
public static void main(String[] args) {
for (int N = 250; true; N += N) {
double time = timeTrial(N);
StdOut.printf("%7d %5.1f\n", N, time);
}
}
}

以上代码会以
250 0.0
500 0.1
1000 0.6
2000 4.3
4000 30.6
上面的输出之所以和理论值有所出入是因为实际运行环境是复杂多变的,因而会产生许多偏差,尽可能减小这种偏差的方式就是多次运行以上程序并取平均值。有了上面这个热身的小程序做铺垫,接下来我们就可以正式介绍这个“可以简单有效地预测任意程序执行性能并判断其运行时间的大致增长数量级”的方法了,实际上它的工作基于以上的
- 开发一个
[ 输入生成器] 来产生实际情况下的各种可能的输入。 - 反复运行下面的
DoublingRatio 程序,直至time/prev 的值趋近于极限2^b ,则该算法的增长数量级约为N^b(b 为常数) 。
运行倍率程序,我们可以得到如下输出:

250 0.0 2.0
500 0.1 5.5
1000 0.5 5.4
2000 3.7 7.0
4000 27.4 7.4
8000 218.0 8.0

我们可以看到,
若
T(N) ~ a _ N^b _ lgN ,那么T(2N) / T(N) ~2^b 。
以上定理的证明很简单,只需要计算
Example
前面我们介绍了算法分析的一些姿势,那么现在我们就来学以致用,一起来解决几道一线互联网企业有关于算法分析的面试
【腾讯】下面算法的时间复杂度是
____ int foo(int n) {
if (n <= 1) {
return 1;
}
return n * foo(n - 1);
}
看到这道题要我们分析算法时间复杂度后,我们要做的第一步便是确定关键操作,这里的关键操作显然是
【京东】以下函数的时间复杂度为
____ void recursive(int n, int m, int o) {
if (n <= 0) {
printf("%d, %d\n", m, o);
} else {
recursive(n - 1, m + 1, o);
recursive(n - 1, m, o + 1);
}
}
这道题明显要比上道题难一些,那么让我们来按部就班的解决它。首先,它的关键操作时
【京东】如下程序的时间复杂度为
____( 其中m > 1 ,e > 0)x = m;
y = 1;
while (x - y > e) {
x = (x + y) / 2;
y = m / x;
}
print(x);
以上算法的关键操作即
【搜狗】假设某算法的计算时间可用递推关系式
T(n) = 2T(n/2) + n ,T(1) = 1 表示,则该算法的时间复杂度为____
根据题目给的递推关系式,我们可以进一步得到:T(n) = 2(2T(n/4) + n/2) + n = … 将递推式进一步展开,我们可以得到该算法的时间复杂度为
算法复杂度
时间复杂度
一个算法执行所耗费的真实时间称为时间频度,从 理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就 可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时 间频度。记为
最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
在最坏情况下的时间复杂度为
如果算法的执行时间不随着问题规模
当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度
算法的时间复杂度不仅仅依赖于问题的规模,还与输入实例的初始状态有关。
在数值
递归算法时间复杂度推导
一般来说,在递归算法中的时间复杂度可以表示为
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组
$$ T(n)≤2T(n/2) +n,T(1)=0\ T(n)≤2(2T(n/4)+n/2) +n=4T(n/4)+2n\ T(n)≤4(2T(n/8)+n/4) +2n=8T(n/8)+3n\ ……\ T(n)≤nT(1)+(log2n)×n= O(nlogn) \ $$
空间复杂度
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。
对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,求一个 较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计 一个算法