【问题标题】:std::forward_list and std::forward_list::push_backstd::forward_list 和 std::forward_list::push_back
【发布时间】:2012-01-05 12:25:16
【问题描述】:

我想使用std::forward_list

因为:

转发列表是一个支持快速插入和删除的容器 来自容器中任何地方的元素

但是没有 *std::forward_list::push_back* 实现。

是否有一种高性能的方式来添加对单或无理由的支持?

【问题讨论】:

  • 为什么需要它?您想双向增加您的列表吗?你不能同样轻松地使用push_front() 吗?
  • 请注意,双向std::list 还支持“从容器中的任何位置快速插入和删除元素”,并具有push_back。成本是每个条目的额外指针。内存这么紧,用不着吗?
  • 我想继续排序
  • @AlexanderGuiness:如果您想对其进行排序,set 或 multiset 可能是更好的选择?这保持了排序顺序,代价是插入和删除速度较慢(尽管插入单个元素会比将其插入列表然后重新排序更快)。
  • @AlexanderGuiness:单链表对于它支持的操作会稍微快一些(因为每个操作必须更新一半的指针),但两者具有相同的复杂度顺序。如果您想支持快速的push_back,它很可能会更慢。

标签: c++ c++11 std forward-list


【解决方案1】:

我建议反对std::forward_list,就像我建议反对std::list 在几乎所有情况下一样。就个人而言,我从来没有在我的代码中发现链表是最好的数据结构。

在 C++ 中,您的默认首选数据集合应为 std::vector。如果这是您真正需要的,它会为您提供高效的push_back。如果您只查看该操作的抽象 big-O 复杂性测量,它在技术上不会为您提供从中间有效的删除和插入。然而,在现实世界中,std::vector 仍然即使在中间插入和删除。

例如,Bjarne Stroustrup 创建了一个包含 100,000 个元素的测试 std::list 与 std::vector。他会搜索每个元素并删除它。然后他会找一个插入点插入到中间。他本可以对 std::vector 使用二分搜索,但没有让比较“更公平”。

结果显示std::vector 取得了巨大的胜利,即使在std::list 应该很强的情况下也是如此。简单地遍历std::list 需要更长的时间,因为所有对象在内存中相距多远。 std::list 对缓存不友好,这可能是现代处理器最重要的事情。

The complete talk by Bjarne Stroustrup

Thorough explanation of the effects, with benchmarks at multiple sizes

请注意,此处的第二个链接提供了一些您可能想要使用std::list 的情况,例如当元素的大小很大时。但是,我遇到过许多元素按特定顺序排列的情况,需要删除一些。

这些元素比任何内置类型都大,但并不大,在 32 位计算机上可能每个 20-30 字节)。元素的数量足够大,以至于我的整个数据结构只有几百 MiB。数据收集是一组基于当前已知信息理论上可以有效的值。该算法迭代所有元素并根据新信息删除不再有效的元素,每次遍历可能会删除大约 80% 的剩余元素。

我的第一个实现是一个简单的std::vector 方法,我在遍历时删除了无效元素。这适用于小型测试数据集,但是当我尝试做真实的事情时,它太慢而无用。我切换到std::list 作为容器,但使用了相同的算法,我看到了显着的性能改进。但是,它仍然太慢而无法使用。成功的改变是切换回std::vector,但我没有删除坏的元素,而是创建了一个新的std::vector,我发现任何好的元素都放入了std::vector,然后在函数结束时,我会简单地丢弃旧的std::vector 并使用新的,这给了我比std::list 更快的速度,就像std::list 给我的原始std::vector 一样多实现,而且这已经足够快了。

【讨论】:

  • 或者.. 在更新的每个元素上维护一个“死”标志,而不是删除元素,仅在死元素数量达到一定限制时压缩向量。当然不适用于向量中的原始类型,但非常有趣的答案,ta。
  • 很好的咆哮,但我根本不明白这是如何回答这个问题的。在很多情况下单链表就可以了。事实上,我认为幕后的任何实现基本上都使用单链帧列表来表示单个线程中的运行时堆栈(帧在内存中可能是连续的,但大小不等,因此不能像在向量中那样直接寻址)。通常,如果您只需要一个简单的堆栈或队列结构,您可以使用列表轻松实现它(如果是队列,则需要一个指向其末尾的附加指针)。
  • @MarcvanLeeuwen 它解决了问题而不是回答问题。问题是他们想要一个用于插入和删除的高性能数据结构。 std::forward_list 很少是实现如此广泛目标的数据结构。至于您的具体示例,std::forward_list 也无济于事(除非我误解了,我很可能是)。如果帧大小不相等,则意味着动态分配。指针链表几乎从来都不是比指针向量更好的数据结构。
  • 我发现单链表的最佳用途是原子添加/删除项目。可以使用 cmpxchg 更新指针。虽然我不相信 std::forward_list 这样做,但遗憾的是。
  • 这种遍历容器、仅将要保留的内容复制到新容器中的技术是现代 Java 垃圾收集器的工作方式。
【解决方案2】:

std::forward_list 支持快速插入和删除,但不能遍历到末尾。要实现.push_back,您首先需要到达列表的末尾,这是 O(N) 并且一点也不快,这可能是它没有实现的原因。

 

你可以通过递增.before_begin N 次找到指向最后一个元素的迭代器

auto before_end = slist.before_begin();
for (auto& _ : slist)
  ++ before_end;

然后使用.insert_after或.emplace_after插入元素:

slist.insert_after(before_end, 1234);

【讨论】:

  • 当然,forward_list 可以通过存储一个指向其末尾的额外迭代器来修改。这样,push_back 可以在 O(1) 时间内完成。
  • 所以没有任何理由实施push_back?
  • @KennyTM 它仍然缺乏双向性。
  • @KennyTM:不,std::list 是双向链接的。我的意思是在头部结构中存储一个指向末尾的额外指针,这正是 Mike Seymour 所建议的。
  • 或者写auto before_end = std::next(slist.before_begin(), std::distance(slist.begin(), slist.end()));
【解决方案3】:

std::forward_list 的重点是成为 std::list 的超精简版本,因此它不会将迭代器存储到最后一个元素。如果你需要一个,你必须自己维护它,像这样:

forward_list<int> a;

auto it = a.before_begin();

for(int i = 0; i < 10; ++i)
    it = a.insert_after(it, i);

【讨论】:

    【解决方案4】:

    没有push_back,因为列表不跟踪列表的后面,只有前面。

    您可以在列表周围编写一个包装器,该包装器维护一个指向最后一个元素的迭代器,并根据列表是否为空,使用insert_after 或push_front 实现push_back。如果您想支持更复杂的操作(例如sort 和splice_after),这将变得相当复杂。

    或者,如果您不需要 push_back 快速,可以直接在线性时间内完成。

    除非内存非常紧张,否则最好的解决方案是使用list。这与forward_list具有相同的性能特征,并且是双向的,支持push_back以及push_front;成本是每个元素一个额外的指针。

    【讨论】:

    • +1,只是我的想法。 sort 不会那么难,顺便说一句。只需对底层forward_list 进行排序并找到新的结尾。由于排序是 O(n lg n),所以它支配最终查找。
    • @larsmans:确实如此。不过,在恒定时间内splice 可能会更棘手(如果您正在拼接一个不跟踪其结尾的普通 forward_list)。
    • 我认为不可能同时拥有恒定时间 size 和恒定时间 splice,这就是为什么 std::list 上的操作复杂性存在差异的原因。 “一个或另一个,但不是两者兼而有之”。这也是为什么有人抱怨 std::list 在 C++11 中为 size 获得恒定时间保证的原因。
    【解决方案5】:

    不幸的是,我无法添加评论(声誉低下),但我只想提一下,forward_list 和列表的优点之一是插入删除操作不会使迭代器无效。我有一个应用程序,其中元素的集合在迭代和处理单个元素时不断增长。没有迭代器失效允许我实现段扫描(列表的开头作为段的开始,最后一个段的开始作为结束)。

    【讨论】:

      猜你喜欢
      • 2012-08-01
      • 2020-07-18
      • 2017-07-07
      • 1970-01-01
      • 2012-07-17
      • 2017-12-11
      • 2014-08-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多