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

Cur*_*ous 189 c++ stl linked-list c++11

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

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

Ami*_*ory 183

假设,reverse可能是O(1).那里(再次假设)可能是一个布尔列表成员,指示链表的方向当前是否与创建列表的原始方向相同或相反.

不幸的是,这会降低基本上任何其他操作的性能(尽管不会改变渐近运行时).在每个操作中,需要咨询布尔值以考虑是否遵循链接的"下一个"或"上一个"指针.

由于这可能被认为是一种相对不常见的操作,因此标准(不规定实现,仅复杂性)指定复杂性可以是线性的.这允许"下一个"指针始终明确地表示相同的方向,从而加速了常见操作.

  • @MooseBoys:我不同意你的比喻.不同之处在于,在列表的情况下,通过使用这个布尔标记技巧,实现可以提供"反向"和"O(1)"复杂度**,而不会影响任何其他操作的重要性**.但是,实际上,即使技术上是O(1),每次操作中的额外分支也是昂贵的.相比之下,你不能建立一个列表结构,其中`sort`是O(1)而所有其他操作都是相同的成本.问题的关键在于,看起来,如果你只关心大O,你可以免费获得"O(1)"反向,所以他们为什么不那样做. (28认同)
  • 如果您使用了XOR链接列表,则反转将成为恒定时间.迭代器虽然会更大,但递增/递减它的计算成本会略高一些.对于*任何*类型的链表,这可能与不可避免的内存访问相形见绌. (8认同)
  • @Kevin:嗯,什么?你不能直接xor两个指针,你需要先将它们转换为整数(显然是类型`std :: uintptr_t`.*然后*你可以xor它们. (4认同)
  • @IlyaPopov:每个节点是否真的需要这个?用户从不询问列表节点本身的任何问题,只询问主列表主体.因此,对于用户调用的任何方法,访问布尔值都很容易.例如,如果列表被反转,您可以制定迭代器无效的规则,和/或将布尔值的副本存储到迭代器中.所以我认为它不会影响大O,必然.我承认,我并没有逐行完成规范.:) (3认同)
  • @Kevin,你绝对可以在C++中创建一个XOR链接列表,它实际上就是这种东西的海报 - 儿童语言.没有什么说你必须使用`std :: uintptr_t`,你可以转换为`char`数组,然后对组件进行异或.它会慢一点,但100%便携.可能你可以让它在这两个实现之间进行选择,如果缺少`uintptr_t`,只能使用第二个作为后备.如果在这个答案中描述了一些:http://stackoverflow.com/questions/14243971/can-an-xor-linked-list-be-implemented-in-c-without-causing-undefined-behavior (3认同)
  • @MooseBoys我认为你在这里所写的内容 - 我完全同意 - 正是这一点.你可以假设对容器进行最常见的操作,例如迭代,并说它不是*O(1)*,而是*O(n log(n))*,因为它可能需要,比方说,排序,而排序可能是*O(1)*由于"懒惰"排序.我想定义标准的艺术决定了这是否有意义. (2认同)
  • @MooingDuck:不,见5.7/7:"对于加法或减法,如果表达式P或Q具有类型"指向cv T的指针",其中T与cv-unqualified数组元素类型不同,则行为是未定义的.两个不同的对象可以存储在两种不同类型的存储器中,其中减法只是没有意义. (2认同)

5go*_*der 60

可能Ø(1)如果该列表将存储允许调换"的含义的标志prev"和" next"指针每个节点都有.如果反转列表将是一个频繁的操作,这样的添加可能实际上是有用的,我不知道为什么实现它将被当前标准禁止.但是,拥有这样一个标志会使列表的普通遍历更加昂贵(如果只是一个常数因素),而不是

current = current->next;
Run Code Online (Sandbox Code Playgroud)

operator++列表迭代器中,你会得到

if (reversed)
  current = current->prev;
else
  current = current->next;
Run Code Online (Sandbox Code Playgroud)

这不是你决定轻易添加的东西.鉴于列表通常比反转更频繁地遍历,标准强制执行此技术是非常不明智的.因此,允许反向操作具有线性复杂性.不过请注意,即牛逼Ô(1)⇒ 牛逼Ø(ñ),因此,如前所述,实现您"优化"技术将被允许.

如果你来自Java或类似的背景,你可能想知道为什么迭代器每次都必须检查标志.难道我们不是有两个不同的迭代器类型,无论从公共基类派生,并有std::list::beginstd::list::rbegin多态返回相应的迭代器?虽然可能,这会使整个事情变得更糟,因为推进迭代器将是一个间接(难以内联)函数调用.在Java中,你无论如何都会定期支付这个价格,但这又是许多人在性能至关重要时为C++提供的原因之一.

正如Benjamin Lindley在评论中所指出的,由于reverse不允许使迭代器无效,标准允许的唯一方法似乎是将指针存储回迭代器内的列表,这会导致双间接内存访问.

  • @galinette:`std :: list :: reverse`不会使迭代器失效. (7认同)
  • 您可能无法在迭代器中存储指向列表的指针.`swap()`被指定为常量时间而不是使任何迭代器无效. (4认同)
  • @ 5gon12eder:你可以以非常低廉的成本消除分支:将`next`和`prev`指针存储在一个数组中,并将方向存储为`0`或`1`.要向前迭代,你要遵循`指针[direction]`并反向迭代`指针[1-direction]`(反之亦然).这仍然会增加一点点开销,但*可能*小于分支. (2认同)

Ric*_*ges 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;
}
Run Code Online (Sandbox Code Playgroud)

预期产量:

mat
the
on
sat
cat
the
Run Code Online (Sandbox Code Playgroud)

鉴于此,在我看来,该标准委员会并没有花时间去责成O(1)反向排序的容器,因为它不是必需的,标准库在很大程度上是建立在强制的原则唯一就是绝对必要的,同时避免重复.

只是我的2c.


Bli*_*ndy 18

因为它必须遍历每个节点(n总计)并更新其数据(更新步骤确实如此O(1)).这使得整个操作O(n*1) = O(n).

  • @Curious双链表的节点有方向感.从那里的原因. (28认同)
  • 因为您还需要更新每个项目之间的链接.取出一张纸然后将其拉出而不是低调. (27认同)
  • 为什么问你是否如此确定呢?你在浪费我们的时间. (11认同)
  • @Blindy一个好的答案应该是完整的."取出一张纸并将其画出来"因此不应该成为一个好答案的必要组成部分.答案不是很好的答案可能会受到影响. (11认同)
  • @Shoe:他们有吗?请研究XOR链接列表等. (7认同)
  • @ Snowhawk04:没有.请研究XOR链接列表,这不是它的工作原理.关键是当你迭代时,你的"迭代器"实际上是一对指向相邻节点的指针.不需要历史记录,XOR链表绝对不是队列. (3认同)
  • @ChrisBeck:我认为在这样的iterator-is-a-pair XOR中没有使迭代器失效的情况下很难[插入](http://en.cppreference.com/w/cpp/container/list/insert)项目 - 列表设置. (3认同)