为什么C ++标准库中std::list
类的反向函数具有线性运行时?我认为对于双向链表,反向函数应该是O(1)。
反转双向链接列表应该只涉及切换头部和尾部指针。
答案 0 :(得分:182)
假设reverse
可能是 O(1)。那里(再次假设)可能是一个布尔列表成员,指示链表的方向当前是否与创建列表的原始方向相同或相反。
不幸的是,这会降低基本上任何其他操作的性能(尽管不会改变渐近运行时)。在每个操作中,需要咨询布尔值以考虑是否遵循链接的“下一个”或“上一个”指针。
由于这可能被认为是一种相对不常见的操作,因此标准(不指定实现,仅复杂性)指定复杂性可以是线性的。这允许“下一个”指针始终明确表示相同的方向,从而加速了常见操作。
答案 1 :(得分:59)
如果列表存储了一个允许交换“prev
”和“{{1的含义”的标志,那么可以 O (1) “每个节点都有指针。如果反转列表是一个频繁的操作,这样的添加可能实际上是有用的,我不知道为什么实现它将被当前标准禁止的任何原因。但是,拥有这样一个标志会使列表的普通遍历更加昂贵(如果只是一个常数因子),而不是
next
在列表迭代器的current = current->next;
中,你会得到
operator++
这不是您决定轻松添加的内容。鉴于列表通常比反转更频繁地遍历,标准授权这种技术是非常不明智的。因此,允许反向操作具有线性复杂性。但请注意, t ∈ O (1)⇒ t ∈ O ( n < / i>)所以,如前所述,在技术上实施“优化”是允许的。
如果你来自Java或类似的背景,你可能想知道为什么迭代器每次都必须检查标志。难道我们不能有两个不同的迭代器类型,它们都是从一个公共基类型派生的,并且if (reversed)
current = current->prev;
else
current = current->next;
和std::list::begin
多态返回适当的迭代器?虽然可能,这会使整个事情变得更糟,因为推进迭代器将是一个间接(难以内联)函数调用。在Java中,你无论如何都要定期支付这个价格,但是这又是很多人在性能至关重要时为C ++付出的原因之一。
正如评论中Benjamin Lindley指出的那样,由于std::list::rbegin
不允许使迭代器无效,标准允许的唯一方法似乎是将指针存储回迭代器内的列表这会导致双间接内存访问。
答案 2 :(得分:37)
当然,因为所有支持双向迭代器的容器都有rbegin()和rend()的概念,这个问题没有实际意义吗?
构建一个反转迭代器并通过它访问容器的代理是微不足道的。
这种非操作确实是O(1)。
如:
.map()
预期产出:
.map()
鉴于此,在我看来,标准委员会没有花时间强制要求O(1)容器的反向排序,因为它没有必要,标准库主要建立在强制要求的原则上。严格必要,同时避免重复。
只是我的2c。
答案 3 :(得分:18)
因为它必须遍历每个节点(总共n
)并更新其数据(更新步骤确实是O(1)
)。这使得整个操作O(n*1) = O(n)
。
答案 4 :(得分:2)
它还为每个节点交换上一个和下一个指针。这就是为什么它需要线性。虽然如果使用此LL的函数也将LL的信息作为输入,例如它是正常访问还是反向访问,它可以在O(1)中完成。
答案 5 :(得分:1)
只有算法解释。 想象一下,你有一个包含元素的数组,那么你需要将其反转。 基本思想是迭代每个元素更改元素 第一个位置到最后一个位置,第二个位置的元素到倒数第二个位置,依此类推。当你到达数组的中间时,你将改变所有元素,因此在(n / 2)次迭代中,这被认为是O(n)。
答案 6 :(得分:1)
它是O(n),因为它需要以相反的顺序复制列表。每个单独的项目操作都是O(1),但在整个列表中有n个。
当然,在设置新列表的空间和之后更改指针等方面需要进行一些常量操作。一旦包含一阶n因子,O表示法不会考虑单个常量。