如何确定算法的内存和时间复杂度?

Posted

技术标签:

【中文标题】如何确定算法的内存和时间复杂度?【英文标题】:How to determine memory and time complexity of an algorithm? 【发布时间】:2014-01-14 15:17:20 【问题描述】:

我不擅长确定时间和记忆的复杂性,如果有人可以帮助我,我将不胜感激。

我有一个算法,我不确定它的时间和内存复杂度是多少。

Function sample(k)
   IF k < 2
       Return 0
   Return 1 + sample(k/2)

它的时间和内存复杂度是多少?为什么?

谢谢

【问题讨论】:

这属于Theoretical Computer Science Stack Exchange @EzequielMuns 不,真的没有。这与research-level question 相差甚远。 【参考方案1】:

确定时间和内存复杂度相当于计算在运行算法时使用了这两种资源的多少,并查看这些数量如何随着输入大小而变化(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)

(希望在每次递归调用时将初始参数值pk 的值区分开来以避免混淆)

主要需要注意的是,因为有n = log(k)递归调用,所以有n这样的堆栈帧。每个栈帧的大小都是固定的,因此空间复杂度为O(log(k))

【讨论】:

请注意,使用尾递归的风格,空间部分可以很容易地消除,留下 O(logk) 时间和 O(1) 空间。 @EzequielMuns 感谢您的回答。我仍然不太清楚为什么空间复杂度是 log(k)。如果您能进一步解释,我将不胜感激。 @user3138212 查看扩展答案:)【参考方案2】:

您实际上是在查看 log_2(k),即以 2 为底的对数。要更改底数,您必须乘以一个常数。而且由于我们无论如何都要乘以常数,所以 O(log(n))、O(ln(n)) 和 O(log_2(n)) 都是相同的。

那么为什么上面的方法有以2为底的对数复杂度呢?每次通话时,您都将 k 分成两半。如果你倒退,你每次调用都会乘以 2。乘以 2 是 2^n,而 log_2(n) 正好是它的倒数。

如果你画一棵二叉树也许会有所帮助:一棵有 n 个节点的树的高度为 log_2(n),一棵高度为 n 的树有 2^n 个节点。

【讨论】:

以上是关于如何确定算法的内存和时间复杂度?的主要内容,如果未能解决你的问题,请参考以下文章

复杂度分析(上):如何分析统计算法的执行效率和资源消耗

[数据结构] 时间复杂度与空间复杂度

二分查找,教你如何提高搜索效率

算法时间复杂度

排序算法时间复杂度为O(n²)的排序算法

算法的时间和空间复杂度