根据向量大小在 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 的索引的主要内容,如果未能解决你的问题,请参考以下文章

C ++擦除向量末尾的一部分而不重新分配

C++ - 在 FOR 循环中从向量获取大小

如何擦除双循环中的向量元素

迭代 std::list 时擦除

向量:: 擦除分段错误

C ++根据成员函数从向量中擦除对象[重复]