最近(来自一篇SO评论)我了解到std::remove
和std:remove_if
是稳定的。我错误地认为这是一个糟糕的设计选择,因为它阻止了某些优化?
想象一下,删除1M std::vector
的第一个和第五个元素。由于稳定性,我们无法使用swap实现remove
。相反,我们必须改变所有剩余的元素:(
如果我们不受稳定性的限制,我们可以(对于RA和BD iter)实际上有2个iters,一个从前面,第二个从后面,然后使用swap来将待移除的项目结束。我相信聪明的人可能会做得更好。我的问题一般,而不是我正在谈论的具体优化。
编辑:请注意,C ++会公布零开销原则,还有std::sort
和std::stable_sort
排序算法。
EDIT2: 优化将类似于以下内容:
remove_if
:
当两者都找到了预期时,他们会交换他们的元素。终止时间为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
答案 0 :(得分:12)
我假设您询问stable_remove
的假设定义是remove
当前是什么,remove
要实施,但实施者认为最好给出正确的值以任何顺序。期望实施者能够在与stable_remove
完全相同的情况下进行改进。
实际上,库无法轻松进行此优化。这取决于数据,但您不想花太多时间来确定在决定如何删除每个元素之前将删除多少元素。例如,你可以做一个额外的传递来计算它们,但是有很多情况下额外的传递是低效的。仅仅因为在某些情况下不稳定的移除比稳定更快并不一定意味着在两者之间进行选择的自适应算法是一个不错的选择。
我认为remove
和sort
之间的区别在于,排序已知是一个复杂的问题,有许多不同的解决方案和权衡和调整。所有“简单”排序算法平均平均。大多数标准算法非常简单,remove
是其中之一,但sort
不是。因此,我认为将stable_remove
和remove
定义为单独的标准函数并不是很有意义。
编辑:使用我的调整进行编辑(类似于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_forward
和copy
哪个更快?委员会的部分工作是在某处画一条线。
我认为现实的标准是明智的,单独定义stable_remove
和remove_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是一个错误,但我不同意。 :)