为什么std :: list :: reverse具有O(n)复杂度?


192

为什么std::listC ++标准库中类的反向函数具有线性运行时?我认为对于双向链表,反向函数应该是O(1)。

反转双向链接列表应该只涉及切换头和尾指针。


18
我不明白为什么人们反对这个问题。这是一个完全合理的问题。反转双向链表应花费O(1)时间。
好奇的

46
不幸的是,有些人将“问题是好的”与“问题有好的想法”的概念混淆了。我喜欢这样的问题,其中基本上“我的理解似乎与普遍接受的做法有所不同,请帮助解决这一冲突”,因为扩大您的思维方式可以帮助您解决更多的问题!似乎其他人采取了“在99.9999%的情况下浪费处理,甚至不用考虑”的方法。如果有什么安慰的话,我的选票就便宜得多了!
corsiKa '16

4
是的,这个问题因其质量而受到过分的否决。可能与谁赞成布林迪的答案相同。公平地说,对于每个人都在高中学习的标准链接列表暗示或人们使用的许多实现,“反转双向链接列表应该只涉及切换头和尾指针”通常是不正确的。因此,人们在很多时候对问题或答案的直觉反应会推动赞成/反对决定。如果您在那句话中更清楚或省略了它,我想您的选票将会减少。
克里斯·贝克

3
还是让我来承担举证责任,@ Curious:我在这里鞭打了一个双重链表实现:ideone.com/c1HebO。您能指出如何期望Reverse函数在O(1)中实现吗?
CompuChip

2
@CompuChip:实际上,可能取决于实现方式。您不需要额外的布尔值即可知道要使用哪个指针:只需使用一个不指向您的指针即可...顺便说一句,使用XOR链接列表很可能是自动的。是的,这取决于列表的实现方式,并且可以澄清OP语句。
Matthieu M.

Answers:


187

假设reverseO(1)。(再次假设)可能有一个布尔列表成员,它指示链接列表的方向当前与创建列表的原始方向相同还是相反。

不幸的是,这将降低其他任何操作的性能(尽管不更改渐近运行时)。在每个操作中,都需要查询一个布尔值,以考虑是否遵循链接的“下一个”或“上一个”指针。

由于这大概被认为是相对少见的操作,因此该标准(该标准不规定实现,仅要求复杂性)规定该复杂度可以是线性的。这允许“下一个”指针始终明确地指示相同的方向,从而加快了常见情况的操作。


29
@MooseBoys:我不同意你的比喻。所不同的是,在列表中的情况下,实现可能提供reverseO(1)复杂性,而不会影响任何其他操作的大O,通过使用此布尔标志伎俩。但是,实际上,即使在技术上为O(1),在每个操作中都需要一个额外的分支,因此代价很高。相比之下,您无法创建一个列表结构,其中sortO(1)并且所有其他操作的成本相同。问题的关键在于,表面上,O(1)如果您只关心大O,就可以免费获得反向收益,那么为什么他们不这样做呢?
克里斯·贝克

9
如果使用异或链表,则反转将变为恒定时间。不过,迭代器会更大,增加/减少迭代器的计算开销会稍微更大。尽管对于任何类型的链表,不可避免的内存访问都可能使它相形见war 。
Deduplicator

3
@IlyaPopov:每个节点实际上都需要吗?用户从不询问列表节点本身的任何问题,仅询问主体列表主体。因此,对于用户调用的任何方法而言,访问布尔值都很容易。您可以制定以下规则:如果列表反转,则迭代器无效,例如,和/或使用迭代器存储布尔值的副本。因此,我认为它不一定会影响大O。我承认,我没有逐行通过规范。:)
克里斯·贝克

4
@凯文:嗯,什么?无论如何,您不能直接对两个指针进行异或运算,您需要先将它们转换为整数(显然是类型std::uintptr_t然后您可以对它们进行异或运算。)
Deduplicator

3
@Kevin,您绝对可以在C ++中创建XOR链接列表,实际上,它是此类事情的后代语言。没什么好说的,您不必使用std::uintptr_t,您可以强制转换为char数组,然后对组件进行XOR。它会比较慢,但100%可移植。可能您可以使它在这两种实现之间进行选择,并且在uintptr_t缺少时仅将第二种用作后备。如果在此答案中有描述,则需要一些说明:stackoverflow.com/questions/14243971/…–
克里斯·贝克

61

可能Ø(1)如果该列表将存储允许调换“的含义的标志prev”和“ next”指针每个节点都有。如果反转列表是经常的操作,那么实际上添加这样的列表可能会有用,并且我不知道任何理由为何当前标准禁止实施它。但是,拥有这样的标志会使列表的普通遍历更加昂贵(如果仅通过一个恒定的因素),因为

current = current->next;

operator++列表迭代器的中,您将获得

if (reversed)
  current = current->prev;
else
  current = current->next;

这不是您决定轻松添加的内容。考虑到列表遍历的次数通常比翻转遍历的次数多,因此标准强制采用此技术是非常不明智的。因此,允许反向运算具有线性复杂度。不过请注意,即牛逼 Ô(1)⇒ 牛逼Øñ),因此,如前所述,实现您“优化”技术将被允许。

如果您来自Java或类似的背景,您可能想知道为什么迭代器每次都要检查标志。难道我们不是有两个不同的迭代器类型,无论从公共基类派生,并有std::list::beginstd::list::rbegin多态返回相应的迭代器?可能的话,这会使整个情况变得更糟,因为推进迭代器现在将是间接(很难内联)的函数调用。在Java中,您还是要按常规方式支付此价格,但是再次重申,这是许多人在性能至关重要时使用C ++的原因之一。

正如本杰明·林德利Benjamin Lindley)在评论中所指出的那样,由于reverse不允许迭代器无效,因此标准允许的唯一方法似乎是将指向指针的指针存储在迭代器内部,这会导致双重间接内存访问。


7
@galinette:std::list::reverse不会使迭代器无效。
本杰明·林德利

1
@galinette对不起,我将您之前的评论误读为您编写时的“每个迭代器标志”,而不是“每个节点标志”。当然,每个节点的标志将适得其反,因为您必须再次进行线性遍历才能将它们全部翻转。
5gon12eder

2
@ 5gon12eder:您可以以极低的成本消除分支:将nextprev指针存储在数组中,并将方向存储为a 01。若要向前迭代,请遵循pointers[direction]并进行反向迭代pointers[1-direction](反之亦然)。这仍然会增加一点点开销,但可能少于分支。
杰里·科芬

4
您可能无法在迭代器中存储指向列表的指针。 swap()指定为恒定时间,并且不会使任何迭代器无效。
塔维安·巴恩斯

1
@TavianBarnes该死!好吧,然后是三重间接寻址……(我是说,实际上不是三重间接寻址。您必须将标志存储在动态分配的对象中,但是迭代器中的指针当然可以直接指向该对象,而不是间接访问列表。)
5gon12eder 2016-2-25

37

当然,因为所有支持双向迭代器的容器都具有rbegin()和rend()的概念,所以这个问题没有意义吗?

构建一个代理来反转迭代器并通过该代理访问容器很简单。

这种非运算的确是O(1)。

如:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

预期输出:

mat
the
on
sat
cat
the

鉴于此,在我看来,标准委员会没有花时间要求对容器进行O(1)逆序排序,因为这是没有必要的,并且标准库很大程度上是基于仅强制要求必需的内容而建立的,避免重复。

只是我的2c。


18

因为它必须遍历每个节点(n总计)并更新其数据(更新步骤的确是O(1))。这使整个操作成为可能O(n*1) = O(n)


27
因为您也需要更新每个项目之间的链接。取出一张纸并将其拉出,而不要投票。
布林迪

11
为什么要那么确定呢?你在浪费我们的时间。
布林迪

28
@Curious双链表的节点具有方向感。从那里的原因。

11
@Blindy一个好的答案应该是完整的。因此,“拿出一张纸并将其抽出”不应成为良好答案的必要组成部分。不好的答案会被否决。
RM

7
@Shoe:他们一定要吗?请研究XOR-链表等。
重复数据删除器

2

它还为每个节点交换上一个和下一个指针。这就是为什么需要线性的原因。尽管可以在O(1)中完成,但是使用此LL的函数是否也将有关LL的信息作为输入,例如它是正常访问还是反向访问。


1

仅是算法解释。假设您有一个包含元素的数组,那么您需要将其反转。基本思想是在每个元素上进行迭代,以将第一个位置上的元素更改为最后一个位置,将第二个位置上的元素更改为倒数第二个位置,依此类推。当您到达数组的中间时,将更改所有元素,因此将在(n / 2)次迭代中被更改为O(n)。


1

之所以为O(n)只是因为它需要以相反的顺序复制列表。每个单独的项目操作均为O(1),但在整个列表中有n个。

当然,在设置新列表的空间以及之后更改指针等过程中涉及到一些常量时间操作。一旦包含一阶n因子,O标记就不会考虑单个常量。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.