【问题标题】:Erasing elements in std::vector by using indexes使用索引擦除 std::vector 中的元素
【发布时间】:2011-09-30 08:58:18
【问题描述】:

我有一个std::vector<int>,我需要删除给定索引处的所有元素(向量通常具有高维)。我想知道,考虑到应该保留原始向量的顺序,这是执行此类操作的最有效方法。

虽然,我找到了有关此问题的相关帖子,但其中一些需要删除single elementmultiple elements,其中remove-erase idiom 似乎是一个很好的解决方案。 但是,就我而言,我需要删除多个元素,并且由于我使用的是索引而不是直接值,因此无法应用 remove-erase idiom,对吗? 我的代码如下所示,我想知道在效率方面是否可以做得更好?

bool find_element(const vector<int> & vMyVect, int nElem){
    return (std::find(vMyVect.begin(), vMyVect.end(), nElem)!=vMyVect.end()) ? true : false;
}

void remove_elements(){

    srand ( time(NULL) );

    int nSize = 20;
    std::vector<int> vMyValues;
    for(int i = 0; i < nSize; ++i){
            vMyValues.push_back(i);
    }

    int nRandIdx;
    std::vector<int> vMyIndexes;
    for(int i = 0; i < 6; ++i){
        nRandIdx = rand() % nSize;
        vMyIndexes.push_back(nRandIdx);
    }

    std::vector<int> vMyResult;
    for(int i=0; i < (int)vMyValues.size(); i++){
        if(!find_element(vMyIndexes,i)){
            vMyResult.push_back(vMyValues[i]);
        }
    }
}

【问题讨论】:

  • 问题是,在删除第一个元素后索引将不再有效,与迭代器相同(您可以从具有vec.begin() + index 的索引中获取迭代器)。
  • @Georg,代码做了它应该做的。这个想法是删除给定position 处的element。在我的代码中,elementvMyValues 表示,positionvMyIndexes 表示。
  • 我认为我在阅读时遇到了与安迪相同的盲点...您当前的代码没有就地删除,因此不存在该问题 ;)
  • 如果效率是一个问题,并且如果您正在执行大量按位置删除(并且可能还插入),请考虑使用除std::vector 之外的其他容器@ --- sdt::list(链接-list 容器)或std::set(键是值本身的容器)

标签: c++ algorithm stl vector


【解决方案1】:

这是一种基于Andriy Tylychkoanswer 的算法,因此可以更轻松、更快速地使用答案,而无需将其分开。它还消除了在索引列表开头添加 -1 和末尾添加 items 的需要。还有一些调试代码以确保indices 有效(items 中的排序和有效索引)。

template <typename Items_it, typename Indices_it>
auto remove_indices(
    Items_it items_begin, Items_it items_end
  , Indices_it indices_begin, Indices_it indices_end
)
{
    static_assert(
      std::is_same_v<std::random_access_iterator_tag
        , typename std::iterator_traits<Items_it>::iterator_category>
      , "Can't remove items this way unless Items_it is a random access iterator");

    size_t indices_size = std::distance(indices_begin, indices_end);
    size_t items_size = std::distance(items_begin, items_end);
    if (indices_size == 0) {
        // Nothing to erase
        return items_end;
    }

    // Debug check to see if the indices are already sorted and are less than
    // size of items.
    assert(indices_begin[0] < items_size);
    assert(std::is_sorted(indices_begin, indices_end));

    auto last = items_begin;
    auto shift = [&last, &items_begin](size_t range_begin, size_t range_end) {
        std::copy(items_begin + range_begin, items_begin + range_end, last);
        last += range_end - range_begin;
    };

    size_t last_index = -1;
    for (size_t i = 0; i != indices_size; ++i) {
        shift(last_index + 1, indices_begin[i]);
        last_index = indices_begin[i];
    }
    shift(last_index + 1, items_size);
    return last;
}

这是一个使用示例:

template <typename T>
std::ostream& operator<<(std::ostream& os, std::vector<T>& v)
{
    for (auto i : v) {
        os << i << " ";
    }
    os << std::endl;
    return os;
}

int main()
{
    using std::begin;
    using std::end;
    std::vector<int> items = { 1, 3, 6, 8, 13, 17 };
    std::vector<int> indices = { 0, 1, 2, 3, 4 };

    std::cout << items;
    items.erase(
          remove_indices(begin(items), end(items), begin(indices), end(indices))
        , std::end(items)
    );
    std::cout << items;

    return 0;
}

输出:

1 3 6 8 13 17 
17 

所需的标题是:

#include <iterator>
#include <vector>
#include <iostream> // only needed for output
#include <cassert>
#include <type_traits>

Demo 可以在 godbolt.org 上找到。

【讨论】:

    【解决方案2】:

    擦除-删除给定索引处的多个元素

    更新:在收到@kory 的性能反馈后,我修改了算法,不再使用标记和移动/复制块中的元素(不是一个接一个)。

    注意事项:
    • 索引需要排序且唯一
    • 使用std::move(替换为std::copy for c++98):

    Github Live example

    代码:
    template <class ForwardIt, class SortUniqIndsFwdIt>
    inline ForwardIt remove_at(
        ForwardIt first,
        ForwardIt last,
        SortUniqIndsFwdIt ii_first,
        SortUniqIndsFwdIt ii_last)
    {
        if(ii_first == ii_last) // no indices-to-remove are given
            return last;
        typedef typename std::iterator_traits<ForwardIt>::difference_type diff_t;
        typedef typename std::iterator_traits<SortUniqIndsFwdIt>::value_type ind_t;
        ForwardIt destination = first + static_cast<diff_t>(*ii_first);
        while(ii_first != ii_last)
        {
            // advance to an index after a chunk of elements-to-keep
            for(ind_t cur = *ii_first++; ii_first != ii_last; ++ii_first)
            {
                const ind_t nxt = *ii_first;
                if(nxt - cur > 1)
                    break;
                cur = nxt;
            }
            // move the chunk of elements-to-keep to new destination
            const ForwardIt source_first =
                first + static_cast<diff_t>(*(ii_first - 1)) + 1;
            const ForwardIt source_last =
                ii_first != ii_last ? first + static_cast<diff_t>(*ii_first) : last;
            std::move(source_first, source_last, destination);
            // std::copy(source_first, source_last, destination) // c++98 version
            destination += source_last - source_first;
        }
        return destination;
    }
    
    用法示例:
    std::vector<int> v = /*...*/; // vector to remove elements from
    std::vector<int> ii = /*...*/; // indices of elements to be removed
    
    // prepare indices
    std::sort(ii.begin(), ii.end());
    ii.erase(std::unique(ii.begin(), ii.end()), ii.end());
    
    // remove elements at indices
    v.erase(remove_at(v.begin(), v.end(), ii.begin(), ii.end()), v.end());
    

    【讨论】:

    • 这运行得相当快,在我的数百万数据集上运行 0.47 秒,其他运行速度较慢大约 5-10 分钟,只有 Andriy Tylychko 的运行速度有一半时间更快。
    • @kory 我已经更新了算法来避免标记和移动/复制块中的元素(不是一个接一个)
    • 是的,这里一切正常,这是目前最快的整体算法,恭喜!
    【解决方案3】:

    您可以做的是将向量(实际上是任何非关联容器)一分为二 组,一组对应于要擦除的索引,另一组包含其余的。

    template<typename Cont, typename It>
    auto ToggleIndices(Cont &cont, It beg, It end) -> decltype(std::end(cont))
    {
        int helpIndx(0);
        return std::stable_partition(std::begin(cont), std::end(cont), 
            [&](typename Cont::value_type const& val) -> bool {
                return std::find(beg, end, helpIndx++) != end;
        });
    }
    

    然后您可以从(或最多)分割点删除以擦除(仅保留) 索引对应的元素

    std::vector<int> v;
    v.push_back(0);
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    
    int ar[] = { 2, 0, 4 };
    v.erase(ToggleIndices(v, std::begin(ar), std::end(ar)), v.end());
    
    • 如果不需要“仅按索引保留”操作,您可以使用 remove_if insted of stable_partition(O(n) 与 O(nlogn) 复杂度)
    • 要将 C 数组作为容器工作,lambda 函数应该是 [&](decltype(*(std::begin(cont))) const& val) -> bool { return std::find(beg, end, helpIndx++) != end; } 但是 .erase() 方法不再是一种选择

    【讨论】:

    • 这在我的测试中非常慢,运行大约需要 5 分钟,而 Andriy Tylychko 的答案需要 0.24 秒。
    【解决方案4】:

    如果您想确保每个元素只移动一次,您可以简单地遍历每个元素,将要保留的元素复制到新的第二个容器中,不要复制要删除的元素,然后删除旧容器并用新容器替换它:)

    【讨论】:

      【解决方案5】:

      为了避免多次移动相同的元素,我们可以通过删除索引之间的范围来移动它们

      // fill vMyIndexes, take care about duplicated values
      vMyIndexes.push_back(-1); // to handle range from 0 to the first index to remove
      vMyIndexes.push_back(vMyValues.size()); // to handle range from the last index to remove and to the end of values
      std::sort(vMyIndexes.begin(), vMyIndexes.end());
      std::vector<int>::iterator last = vMyValues.begin();
      for (size_t i = 1; i != vMyIndexes.size(); ++i) {
          size_t range_begin = vMyIndexes[i - 1] + 1;
          size_t range_end = vMyIndexes[i];
          std::copy(vMyValues.begin() + range_begin, vMyValues.begin() + range_end,   last);
          last += range_end - range_begin;
      }
      vMyValues.erase(last, vMyValues.end());
      

      附:修复了一个错误,感谢 Steve Jessop 耐心地尝试向我展示它

      【讨论】:

      • @Andy T,我不确定它是否在要删除的索引是连续的情况下正常工作,例如(13,14,15,16)
      • @Peter:应该,因为std::copy 将传递两个相等的迭代器,因此不会复制任何内容。不过,我担心第一次通过循环。我们复制了我们应该保留的 second 范围,从索引 vMyIndexes[0]+1vMyIndexes[1],所以我认为如果 vMyIndexes 中的第一个值不是,我们会在前面丢失一些值0. 可能我们应该在vMyIndexes 的开头加上-1,或者等价地在我们开始之前将vMyIndexes[0] 添加到last
      • @Steve:我把vMyValues.size()放在最后,和你提议的差不多
      • 实际上,我坚持-1的想法是无效的,因为根据std::copy的规则,last不允许在[vMyValues.begin() + range_begin, vMyValues.begin() + range_end)范围内。所以last不需要从头开始。
      • @Andy T:确实,你最终解决了问题,但一开始还没有解决等效问题。
      【解决方案6】:

      我认为它可能会更有效,如果您只是对索引进行排序,然后从向量中从最高到最低删除这些元素。删除列表中的最高索引不会使您要删除的较低索引无效,因为只有高于已删除索引的元素才会更改其索引。

      是否真的更有效将取决于排序的速度。关于这个解决方案的另一个优点是,您不需要值向量的副本,您可以直接在原始向量上工作。代码应如下所示:

      ... fill up the vectors ...
      
      sort (vMyIndexes.begin(), vMyIndexes.end());
      
      for(int i=vMyIndexes.size() - 1; i >= 0; i--){
          vMyValues.erase(vMyValues.begin() + vMyIndexes[i])
      }
      

      【讨论】:

      • +1 优雅的解决方案。我不确定它的效率,因为从最高到最低擦除你会多次移动尾部元素
      • @Andy T.:无论你如何擦除,你总是会在删除元素“之后”移动所有元素。通过从末尾擦除,您可以最大限度地减少每个“索引”要移动的元素数量,因此它是最有效的“就地”解决方案。
      • @Matthieu:对,顺序无所谓,但我不同意the most efficient in-place solution,请看我的帖子
      • 这个方法很慢。我不建议使用这个。
      • 是的,同意 tqjustc,与其他方法相比,这种方法非常非常慢,并且在处理大型数据集时变得非常重要。例如,这需要将近 8 分钟才能运行,而其他方法需要 0.03 秒才能运行。