“恒定”复杂性究竟意味着什么?时间?副本/动作的数量?

时间:2011-12-25 20:57:16

标签: c++ complexity-theory time-complexity

我可以想到C ++中的三个操作可以在某种意义上描述为具有“恒定”的复杂性。我已经看到了一些关于这意味着什么的辩论(*),在我看来,我们可以说“所有这些操作都是不变的,但有些操作比其他操作更稳定”: - ) < / p>

编辑2 :如果您已经认为自己知道答案,请在匆匆进入之前阅读此问题的一些辩论:What data structure, exactly, are deques in C++?很多人,得分很高,正在争论什么是“常数”的意思。我的问题并不像你想象的那么明显!)

编辑:我不需要'复杂'意味着什么。我想要一个明确的答案,也许是C ++标准的引用,它告诉我们完全什么应该是常量。处理器滴答,现实世界时间或其他什么?在其他线程上,有些人认为时间与什么完全无关是C ++标准所要求的。)

标准中的这些复杂性保证是否适用于操作的运行时?或者他们只是指定所包含对象可能发生的(最大)份数/移动次数?什么'摊销'是什么意思?

1。鉴于(非空)vector<int> v,以下内容在运行时显然是常量:

swap(v.front(), v.back());

(虽然一个学究者可能会指出它取决于数据是在缓存中还是换出来的等等!)。

2。鉴于list<int> l,执行push_back非常简单。正好分配了一个新项目,并且链接列表中的一些指针被洗牌。每个push_front涉及一个分配,总是具有相同的内存量,因此这显然是相当“恒定”的。但是,当然,进行分配的时间可能非常多变。内存管理可能需要花费大量时间才能找到合适的可用内存。

3。但在push_back上执行vector<int>更加难以预测。大多数情况下,它会非常快,但它会不时地为所有数据重新分配空间并将每个元素复制到新位置。因此,它在运行时方面的可预测性低于单个list::push_front,但它仍然被称为常量(摊销)。 平均,向向量添加大量数据将带来与添加量无关的复杂性,这就是为什么它被称为“摊销常量”时间。 (我是对的吗?)

最后,我问int以避免使用其他类型的复杂性。例如,vector< vector<int> >可能有点复杂,因为向量(向量)的每个元素可能具有不同的大小,例如,交换两个元素并不像在案例中那样恒定 1。以上。但理想情况下,我们可以回答所有vector<T>,而不仅仅是T=int

(*)有关辩论的示例,请参阅对这些答案的评论:What data structure, exactly, are deques in C++?

6 个答案:

答案 0 :(得分:10)

复杂性总是相对于特定变量或变量集来说明。因此,当标准谈到恒定时间插入时,它谈论的是相对于列表中项目数的恒定时间。也就是说,O(1)插入意味着列表中当前项目的数量不会影响插入的整体复杂性。该列表中可能包含500或50000000个项目,插入操作的复杂性将相同。

例如,std::list有O(1)插入和删除;插入的复杂性不受列表中元素数量的影响。但是,内存分配器的复杂性很可能取决于已经分配的内容的数量。但由于O(1)正在谈论列表中的项目数量,因此不包括这一点。它也不应该,因为那时我们将测量内存分配器的复杂性,而不是数据结构。

简而言之:这是一个不同的衡量标准。

  

这意味着我们可以随心所欲地实现我们的算法,包括在任何实用意义上时间不是真正恒定的算法,而是我们尊重所包含对象上“操作”的数量。

相对于实现,未指定复杂性。它是相对于算法指定的。上下文可以切换并不重要,因为运行时间是什么复杂性。

如上所述,您可以使用与删除相关的内存分配器O {log(n))来实现std::list(其中n是分配数)。但是,删除列表中的元素仍然相对于列表中的项目数为O(1)。

不要将复杂性与整体性能混为一谈。复杂性的目的是为不同变量的算法提供一般度量。希望代码快速运行的程序员的目的是找到一种合理的算法实现,该算法符合实现该性能所需的复杂性。

复杂性是一种评估算法效率的工具。复杂性意味着你不再思考。

  

'摊销'究竟是什么意思?

Amortized表示:

如果某些内容是“按时间摊销”,那么当您在同一数据结构上无限次重复操作时,复杂性限制为X.

因此,std::vector在后​​方有“摊销的常数时间”插入。因此,如果我们接受对象并对其执行无限多次插入,复杂度的渐近极限将与“恒定时间”插入没有区别。

在外行人看来,这意味着操作可能有时是非常数,但是它将是非常数的次数总是会减少。从长期的插入来看,它是恒定的时间。

答案 1 :(得分:3)

据我了解,常量复杂度意味着操作是O(1):您可以提前告诉操作有多少基本操作(读/写,汇编指令等)将采取。此绑定是目标对象的所有可能状态的公共边界。这里有一个问题:在多线程环境中,您无法预测线程切换,因此您只能在实时操作系统中对经过的操作时间进行一些推理。

关于摊销的常数复杂性,这甚至更弱。总结here的答案,可以保证平均你的操作是一个常数。这意味着,N后续操作的基本操作数为O(N)。这意味着基本操作的数量约为O(1),但允许一些罕见的跳跃。例如,向向量的尾部添加项通常是不变的,但有时需要额外的繁重操作;在这里分摊常数时间意味着附加操作不会经常发生并且花费可预测的时间量,因此N操作的总时间仍为O(N)。当然,同样的问题也适用于此。

所以,回答你的问题:

  1. 标准的复杂性保证实际上仅适用于执行操作所需的机器代码指令的数量,并不意味着运行时以某种方式受到限制。 (实际上,直到最近C ++还没有一个语言指定的线程概念,所以从那时起C ++标准的观点来看,程序是在专用的C ++机器上执行的。)
  2. 摊销是“平均一个常数”,这通常发生在几乎总是有限的运行时间的情况下,有一些非常罕见的偏差。
  3. 编辑:
    你可以查看例如C ++标准第23.1节:

      

    本条款中的所有复杂性要求仅根据所包含的操作数量来说明   对象。

答案 2 :(得分:3)

如果说某个操作具有“常数复杂性”,首先它通常指的是时间复杂度。我可以参考空间复杂性,但如果是这样,那将会正常明确指定。

现在,操作的复杂性指的是当操作中处理的项目数量增加时,执行操作的时间将增加多少。对于恒定复杂度操作,无论是处理零项还是一千万项,该函数都将花费相同的时间。

所以:

  1. swap()是常量复杂性,因为向量中有多少项无关紧要,操作将花费相同的时间。

  2. 推回列表。是不变的复杂因素,因为即使分配新项目可能花费一些可变的时间,分配时间也不会增加,因为列表有1000万个元素(至少在算法意义上不是这样 - 当然如果空闲内存变得更多分配可能需要更多时间,但从算法的角度来看,内存数量会无限大。

  3. 传真上的
  4. push_back()被称为“摊销”常数,因为在正常情况下不需要重新分配,操作将花费的时间与在多少元素中的数量无关已经向量 - 相对于1000万长度向量,将新元素添加到零长度向量的时间相同。但是,如果需要重新分配向量,则需要发生现有元素的副本,并且 不是常量操作 - 它是线性操作。但是假设这个向量被设计成使得那些重新分配不经常发生,以至于在amortized个操作过程中它们可以push_back()

  5. ,但对向量执行push_back更加难以预测。大多数情况下,它会非常快,但它会不时地为所有数据重新分配空间并将每个元素复制到新位置。因此,它在运行时方面比单个list :: push_front更难以预测,但它仍然被称为常量(摊销)。平均而言,向向量添加大量数据将带来与添加量无关的复杂性,这就是为什么它被称为“摊销常数”时间。 (我是对的吗?)

答案 3 :(得分:1)

O(1)的复杂性 - 常数(时间复杂度),意味着完成算法的时间与问题大小无关。

因此,在散列结构中查找值是O(1)因为执行它所需的时间与它包含的值有多少无关。但是,链接列表也不是这样,因为我们必须扫描值(当我们增加元素数量时,它们的数量会发生变化)才能找到我们的值。

在第3种情况下,当它复制每个元素时,它不是而不是和O(1)操作,而是O(N)操作(但大多数时候它是O(1),所以它通常是不变的)。摊销将此考虑在内,注意到算法通常在O(1)时间内完成,很少遇到O(N)情况。

答案 4 :(得分:1)

常量复杂性只意味着操作运行所花费的时间与输入的大小无关。它没有告诉你运行时成本。分摊常数复杂性意味着如果执行一系列操作,每个单独操作的平均时间将与问题实例的大小无关。

答案 5 :(得分:0)

关于问题复杂性尚未提及的一点是,通常存在一个隐含的假设,即机器的字大小足以使解决方案成为可能,但不会更大。例如,如果给出一个在1..N范围内的N个数组的数组,则假定机器的字大小足以处理1..N范围内的值,但不能保证会有1 ... N.任何“备用”位可用。这很重要,因为虽然保持阵列所需的位数当然是O(NlgN),但是需要O(NlgN)总空间的算法与需要精确O(1)+的算法之间存在根本区别。 N * lg(N)[四舍五入]空格。