Time complexity problem
某笔试题中的时间复杂度
平时老是懒得写博客,都快一年没写了…… 又到了找工作的季节,前几天看见一道笔试题就决定写篇文章分(吐)析(槽)一下。
题目描述
如下程序实现对指定的自然数求阶乘:
int func(int n)
{
if (n <= 1)
return 1;
return n * func(n - 1);
}
则其算法复杂度是:
- A.
- B.
- C.
- D.
这次还是把代码手工格式化了一下,不禁想吐槽那帮出题人和面试官: 学生把代码压行你们就直接骂,你们自己压行就合理合法了吗(掀桌)?
我们不去吐槽 func
被递归调用
与 的区别
我们首先复习一下 Big-
当且仅当存在常数
如果我们假设这题的答案是
根据
那么我们令
因此这题也能选
为什么我们在平时做题或者写题解的时候可以使用 Big-
下文假设选项都使用了 Big-
未定义行为可能导致出人意料的结果
我们之前已经无数次讨论未定义行为,稍有常识的人都能看出,
对于本题提供的代码,只要 int
为 32 位带符号整数类型的机器上,打开
gcc
的 -fsanitize=undefined
开关,编译并运行该程序:
test.c:15:11: runtime error: signed integer overflow: 13 * 479001600 cannot be represented in type 'int'
需要注意的是,未定义行为属于一种编程错误。那么,只有两种可能:
- 这题的题面是错的,应该改成“如下程序在
x86_64-Linux
平台实现对 以内的自然数求阶乘”。那么因为 是个常数,答案应该是 。 - 这题的代码是错的。那这显而易见地体现了贵司拙劣的代码水平,还招个 p 人啊。
就算我们不考虑代码错误的问题,未定义行为就是
“什么都可能发生,也可能什么都不发生”,那么这程序在
代码优化可能导致出人意料的结果
当然,一些项目使用 -fwrapv
保证带符号整数溢出具有确定(2-补码)的行为,
例如 Linux 内核。为了让这题看上去可做一点,我们假设使用了该选项。
那么如果我是一个 C 编译器,我会怎么分析这代码呢?补码算术相当于对
(这只是一个粗糙的界,实际上
于是,作为一个聪明的编译器,我决定把代码优化成
int func(int x)
{
static const int ans[] = {1,1,2,6,24,/*...*/};
unsigned t = x & 0xFFFFFFC0u;
return t ?t >> 31 : ans[x];
}
这样时间复杂度就变成了
从以上分析我们可以看出,在讨论算法时间复杂度的问题时, 根本不该 出现任何编程语言编写的代码。 这是因为编程语言只规定了代码产生的副作用 (side-effect) 或其行为, 只要能够保证行为一致,编译器可以把实际的目标代码优化到他妈都不认识, 导致时间复杂度变化。此时我们必须使用严谨的数学语言。
计算机字长与时间复杂度
如果这个题给出了伪代码(而不是 C 代码),并且声明了计算机字长足够大,
那么可以说时间复杂度是
首先我们需要使用斯特林近似,求出阶乘的计算机表示需要多少个字:
随便把对数变换一下,就能看出
计算
对它求和就能得到总的时间复杂度
连用两次分部积分法得到
去掉低阶项和常数,就是