根据向量大小在 for 循环内擦除 std::vector 的索引
Posted
技术标签:
【中文标题】根据向量大小在 for 循环内擦除 std::vector 的索引【英文标题】:Erasing indices of std::vector inside a for loop based on vector size 【发布时间】:2016-12-07 21:21:36 【问题描述】:我创建了一个函数来过滤掉 std::vector 中我不喜欢的元素,在本例中是一个 opencv 轮廓向量。下面的代码看起来可以工作,但它不起作用,我怀疑这是因为每当我删除索引时索引都会更改,但是我会继续进行下一次 i 值迭代。
void FilterContours( std::vector<std::vector<cv::Point>> contours )
for ( int i = 0; i < contours.size(); i++ )
//Remove contours smaller than 5 from vector - example
if ( contours[i].size() < 5 )
contours.erase(contours.begin() + i);
continue;
//Other filtering...
return;
所以问题是,这是否会按预期工作(我认为不会),如果没有,我如何使其按预期工作?我应该在擦除后添加 i -= 1 以保持正确的索引位置吗?
【问题讨论】:
不要这样擦除。只需使用 erase / remove_if 成语。 ^^ 但是如果你认为在你的 if 中添加i -= 1
更简单--i
会起作用,你为什么不试试呢?此外,虽然这不是问题,但请记住,如果您基于固定迭代器(如 std::vector::end
)进行循环,并且您在不更新结束迭代器的情况下修改向量的大小,则会导致 UB。
一些建议——如果你发现自己编写的循环看起来好像已经被别人写过一千次,那么很可能存在一个 STL 算法或一组算法函数做这项工作。在您的情况下,从序列容器中删除项目是之前已经完成数百万次的事情之一,因此我们有涵盖这种情况的算法 (std::remove_if)
优秀的反应家伙,擦除删除成语是我的最终解决方案,在性能上有相当显着的差异。 @Paul-- 你说得对,我认为 STL 算法中有一些东西,但我只是无法确定我需要的那个,并且知道我会很快得到一个非常直接的响应,而且我确实做到了。跨度>
【参考方案1】:
每次erase()
容器中的一个元素时,它的size()
会递减,其余元素的索引也会递减。但是您正在无条件地增加循环计数器,因此每次您擦除一个元素时,您跳过紧随其后的下一个元素!
此外,您正在通过值传递您的 vector
,因此您正在对 vector
的 副本 进行操作,调用者将不会看到原始 vector
的任何更改.
正确的方法是:
仅当元素未被擦除时,才在循环体内增加索引变量。当你删除一个元素时,让变量保持原样:
void FilterContours( std::vector<std::vector<cv::Point>> &contours )
int i = 0;
while ( i < contours.size() )
if ( contours[i].size() < 5 )
contours.erase(contours.begin() + i);
continue;
//Other filtering...
++i;
使用迭代器代替索引:
void FilterContours( std::vector<std::vector<cv::Point>> &contours )
auto it = contours.begin();
while ( it != contours.end() )
if ( it->size() < 5 )
it = contours.erase(it);
continue;
//Other filtering...
++it;
使用erase-remove 成语:
void FilterContours( std::vector<std::vector<cv::Point>> &contours )
contours.erase(
std:::remove_if(
contours.begin(),
contours.end(),
[](const std::vector<cv::Point> &v)
if (v.size() < 5) return true;
//Other filtering...
return false;
),
contours.end()
);
【讨论】:
您可能在第二个代码块中缺少erase
的第二个参数。
我尝试了一些解决方案,擦除删除习语提供了最佳性能。谢谢你的详细解答!【参考方案2】:
使用擦除删除成语:
contours.erase(
std::remove_if(contours.begin(), contours.end(), [](const std::vector<cv::Point>& v)
return v.size() < 5;
),
contours.end()
);
【讨论】:
【参考方案3】:一般来说,当你迭代移除时,最好向后迭代:
for ( int i = contours.size()-1; i >=0; --i)
这会起作用,但会导致代码变慢,因为在每次删除时,删除后的元素都会被复制/移回。出于这个原因,使用标准算法库提供的专用习语会更好、更快、更易读,这些习语通常是非常优化的。在这种情况下,您有 erase/remove_if
组合:
contours.erase(std::remove_if(contours.begin(), contours.end(), [](const auto& elem) return elem.size() < 5; ), contours.end() );
这里的一大优势是std::remove_if()
以比直观循环更智能的方式运行:它首先“标记”要删除的元素,然后将剩余的元素压缩在一起。这个过程是 O(N),而(直观的)循环是 O(N^2),对于大向量来说差别很大。
p.s.:FilterContours
函数的签名以通过引用获取向量:
void FilterContours( std::vector<std::vector<cv::Point>>& contours ) // <-- by reference
【讨论】:
【参考方案4】:你的FilterContours
应该带一个引用,否则对调用者没有任何影响。
void FilterContours(std::vector<std::vector<cv::Point>>& contours)
for (auto it = contours.begin(); it != contours.end(); )
if (it->size() < 5)
it = contours.erase(it);
else
++it;
编辑: 如果你想以相反的顺序做,你可以这样做:
void FilterContours_reverse(std::vector<std::vector<cv::Point>>& contours)
for (auto it = contours.rbegin(); it != contours.rend(); )
if (it->size() < 5)
contours.erase(std::next(it++).base());
else
++it;
【讨论】:
以上是关于根据向量大小在 for 循环内擦除 std::vector 的索引的主要内容,如果未能解决你的问题,请参考以下文章