std :: remove和std :: remove_if设计的稳定性是否失败?

时间:2012-12-11 10:33:02

标签: c++ stl complexity-theory

最近(来自一篇SO评论)我了解到std::removestd:remove_if是稳定的。我错误地认为这是一个糟糕的设计选择,因为它阻止了某些优化?

想象一下,删除1M std::vector的第一个和第五个元素。由于稳定性,我们无法使用swap实现remove。相反,我们必须改变所有剩余的元素:(

如果我们不受稳定性的限制,我们可以(对于RA和BD iter)实际上有2个iters,一个从前面,第二个从后面,然后使用swap来将待移除的项目结束。我相信聪明的人可能会做得更好。我的问题一般,而不是我正在谈论的具体优化。

编辑:请注意,C ++会公布零开销原则,还有std::sortstd::stable_sort排序算法。

EDIT2: 优化将类似于以下内容:

remove_if

  • bad_iter从头开始查找谓词返回true的那些元素。
  • good_iter从最后查看谓词返回false的元素。

当两者都找到了预期时,他​​们会交换他们的元素。终止时间为good_iter <= bad_iter

如果它有帮助,可以把它想象成快速排序算法中的一个,但是我们不将它们与特殊元素进行比较,而是使用上面的谓词。

EDIT3:我玩了并试图找到最坏的情况(remove_if的最坏情况 - 注意谓词很少是真的)并且我得到了这个:

#include <vector>
#include <string>
#include <iostream>
#include <map>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <memory>
using namespace std;
int main()
{  
    vector<string> vsp;
    int n;
    cin >> n;
    for (int i =0; i < n; ++i)
    {   string s = "123456";
        s.push_back('a' + (rand() %26));
        vsp.push_back(s);
    }
    auto vsp2 = vsp;
    auto remove_start = std::chrono::high_resolution_clock::now();
    auto it=remove_if(begin(vsp),end(vsp), [](const string& s){ return s < "123456b";});
    vsp.erase(it,vsp.end());
    cout << vsp.size() << endl;
    auto remove_end = std::chrono::high_resolution_clock::now();
    cout << "erase-remove: " << chrono::duration_cast<std::chrono::milliseconds>(remove_end-remove_start).count() << " milliseconds\n";

    auto partition_start = std::chrono::high_resolution_clock::now();
    auto it2=partition(begin(vsp2),end(vsp2), [](const string& s){ return s >= "123456b";});
    vsp2.erase(it2,vsp2.end());
    cout << vsp2.size() << endl;
    auto partition_end = std::chrono::high_resolution_clock::now();
    cout << "partition-remove: " << chrono::duration_cast<std::chrono::milliseconds>(partition_end-partition_start).count() << " milliseconds\n";
}



C:\STL\MinGW>g++ test_int.cpp -O2 && a.exe
12345678
11870995
erase-remove: 1426 milliseconds
11870995
partition-remove: 658 milliseconds

对于其他用法,分区更快,相同或更慢。让我困惑的颜色。 :d

3 个答案:

答案 0 :(得分:12)

我假设您询问stable_remove的假设定义是remove当前是什么,remove要实施,但实施者认为最好给出正确的值以任何顺序。期望实施者能够在与stable_remove完全相同的情况下进行改进。

实际上,库无法轻松进行此优化。这取决于数据,但您不想花太多时间来确定在决定如何删除每个元素之前将删除多少元素。例如,你可以做一个额外的传递来计算它们,但是有很多情况下额外的传递是低效的。仅仅因为在某些情况下不稳定的移除比稳定更快并不一定意味着在两者之间进行选择的自适应算法是一个不错的选择。

我认为removesort之间的区别在于,排序已知是一个复杂的问题,有许多不同的解决方案和权衡和调整。所有“简单”排序算法平均平均。大多数标准算法非常简单,remove是其中之一,但sort不是。因此,我认为将stable_removeremove定义为单独的标准函数并不是很有意义。

编辑:使用我的调整进行编辑(类似于std::partition,但不需要保留右侧的值)对我来说似乎很合理。它需要一个双向迭代器,但标准中有一些先例可以在不同的迭代器类别上运行不同的算法,例如std::distance。因此,标准可以定义unstable_remove只有需要一个前向迭代器,但是如果它获得了一个bidi迭代器就会做你的事情。该标准可能不会列出算法,但它可能有一个短语,如“如果迭代器是双向的,最多min(k, n-k)移动k是删除的元素数”,这将实际上迫使它。但请注意,该标准目前尚未说出remove_if有多少动作,所以我认为将其固定下来并不是优先考虑的事。

当然,没有什么能阻止您实施自己的unstable_remove

如果我们接受标准不需要指定不稳定的删除,则问题可归结为它所定义的函数是否应该被称为stable_remove,预期未来remove对于bidi迭代器来说行为不同,并且对于前向迭代器可能表现不同,如果用于执行不稳定删除的一些聪明的启发式变得已经足够已知值得标准函数。我不这样说:如果标准功能的名称不完全正规,那不是灾难。从STL的remove_if中删除稳定性的保证可能是非常具有破坏性的。然后问题就变成了,“为什么STL没有把它称为stable_remove_if”,除了所有答案中的所有要点之外,我只能回答它,STL设计过程比标准化过程。

stable_remove还会打开一些关于其他标准函数的蠕虫,这些函数可能理论上具有不稳定版本。对于一个特别愚蠢的例子应该copy被称为stable_copy,以防万一存在一个实现,它在复制时明显更快地反转元素的顺序?应该copy调用copy_forward,以便实施可以选择copy_backward调用copy_forwardcopy哪个更快?委员会的部分工作是在某处画一条线。

我认为现实的标准是明智的,单独定义stable_removeremove_with_some_other_constraints是明智的,但remove_in_some_unspecified_way只是没有给出相同的优化机会sort_in_some_unspecified_way做的。 Introsort是在1997年发明的,就像C ++正在标准化一样,但我不认为remove周围的研究工作与sort完全相同。我可能错了,优化remove可能是下一个重要的事情,如果是这样,那么委员会就错过了一招。

答案 1 :(得分:3)

std::remove被指定用于前向迭代器。

从开始到结束使用一对迭代器的方法会增加对迭代器的要求,从而降低函数的效用或者违反/恶化渐近复杂度保证。

答案 2 :(得分:1)

回答我自己的问题&gt; 3年后:) 是的,这是一个“失败”。

有一个提案D0041R0会添加unstable_remove。 有人可能会说,只是因为有一个建议添加std :: unstable_remove,这并不意味着std :: remove是一个错误,但我不同意。 :)