确定时间和内存复杂度相当于计算在运行算法时使用了这两种资源的多少,并查看这些数量如何随着输入大小而变化(k在这种情况下)变化。
时间将取决于每条指令被评估多少次,而空间将取决于所涉及的数据结构需要多大才能计算解决方案。
在这个特定场景中,您正在查看递归算法,因此基本上这涉及计算 1) 进行了多少递归调用,以及 2) 为每个调用完成了多少工作。
由于每次调用时输入减半,调用序列将如下所示:
sample(k) )
sample(k/2) )
sample(k/4) )
... ) - n = number of calls
sample(4) )
sample(2) )
sample(1) )
以这种方式将每个递归调用减半将导致对数次调用。
n = log(k)
在每次调用时,我们都会在调用堆栈中存储 常量 个变量,并进行常量工作(操作)。这是因为变量的数量和每次调用中的比较/加法/除法的数量 不会随着k 的增大而增大。
总时间复杂度是调用次数乘以每次调用完成的工作量,因此
time complexity = A*log(k) + B
对于一些常数 A 和 B,它们分别反映了进行递归调用和进行比较/除法的实际时间成本。同样:
space complexity = C*log(k) + D
对于合适的常数 C 和 D 分别用于递归和变量存储的空间成本。
现在在这种分析中,我们主要关心渐近复杂度,也就是说,我们并不真正关心常量,因为它们反映了运行算法的机器的详细信息,我们真的想知道曲线(随着 k 变大)。如果您遵循使用 Big-Oh 表示法编写复杂性的规则,您将得到结果:
space complexity = time complexity = O(log(k))
编辑:内存复杂性细节
正如我之前所说,内存复杂度取决于计算解决方案需要多大的数据结构,所以你可能会问:这个函数中没有使用数据结构,那么 log(k) 在哪里记忆去哪儿了?
简短回答:您必须存储log(k) 参数k 的不同值,每个递归调用一个。
详细答案:这里有一个隐式数据结构被函数调用机制(我们通过递归利用)使用,它的名字是call stack。每次调用sample(k) 时,都会创建一个新的堆栈帧并将一些值压入堆栈:参数k 的本地值、返回地址和其他与实现相关的内容。这样,每个递归调用在堆栈上形成一个“层”,存储其本地信息。整个画面最终看起来像这样:
----------- < top of stack
| k = 1 |
| ... | < stack frame for sample(1)
|---------|
| k = 2 |
| ... | < stack frame for sample(2)
|---------|
...
|---------|
| k = p/2 |
| ... | < stack frame for sample(p/2)
|---------|
| k = p |
| ... | < stack frame for sample(p)
|---------|
| | < stack frame for main() or whatever
initially called sample(p)
(we don't count this one)
(希望在每次递归调用时将初始参数值p 与k 的值区分开来以避免混淆)
主要需要注意的是,因为有n = log(k)递归调用,所以有n这样的堆栈帧。每个栈帧的大小都是固定的,因此空间复杂度为O(log(k))。