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

Why does std::list::reverse have O(n) complexity?

本文关键字:复杂性 具有 std list reverse 为什么      更新时间:2023-10-16

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

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

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

不幸的是,这基本上会降低任何其他操作的性能(尽管不会更改渐近运行时)。在每个操作中,都需要参考布尔值来考虑是遵循链接的"下一个"还是"上一个"指针。

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

如果列表存储一个允许交换每个节点具有的"prev"和"next"指针含义的标志,则可能是O(1)。如果反转列表是一项频繁的操作,那么这样的添加实际上可能是有用的,我不知道当前标准禁止实施它的任何原因。但是,拥有这样的标志会使列表的普通遍历更加昂贵(如果只是通过常量因子),因为

current = current->next;

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

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

这不是您决定轻松添加的东西。鉴于列表通常遍历的频率远高于反向遍历的频率,因此标准强制使用这种技术是非常不明智的。因此,允许反向操作具有线性复杂度。但是,请注意,t ∈ O(1) ⇒ tOn),因此,如前所述,在技术上实现您的"优化"是允许的。

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

正如 Benjamin Lindley 在评论中指出的那样,由于不允许reverse使迭代器失效,因此标准允许的唯一方法似乎是在迭代器中存储一个指向列表的指针,这会导致双重间接内存访问。

当然,由于所有支持双向迭代器的容器都有 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。

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

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

只是一个算法解释。假设你有一个包含元素的数组,然后你需要反转它。基本思想是迭代每个元素,更改元素上的元素第一个位置到最后一个位置,第二个位置上的元素到倒数第二个位置,依此类推。当你到达数组的中间时,你会改变所有元素,因此在(n/2)迭代中,这被认为是O(n)。

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

当然,在为新列表设置空间以及之后更改指针等时,也涉及一些常时操作。 O 表示法在包含一阶 n 因子后不会考虑单个常量。