【问题标题】:How to delete items from a std::vector given a list of indices如何在给定索引列表的情况下从 std::vector 中删除项目
【发布时间】:2023-03-21 17:31:01
【问题描述】:

我有一个项目向量 items,以及一个应该从 items 中删除的索引向量:

std::vector<T> items;
std::vector<size_t> indicesToDelete;

items.push_back(a);
items.push_back(b);
items.push_back(c);
items.push_back(d);
items.push_back(e);

indicesToDelete.push_back(3);
indicesToDelete.push_back(0);
indicesToDelete.push_back(1);

// given these 2 data structures, I want to remove items so it contains
// only c and e (deleting indices 3, 0, and 1)
// ???

知道每次删除都会影响indicesToDelete 中的所有其他索引,执行删除的最佳方法是什么?

几个想法是:

  1. items复制到一个新的向量,一次一项,如果索引在indicesToDelete中则跳过
  2. 迭代items 并为每次删除,递减indicesToDelete 中具有更大索引的所有项目。
  3. 首先对indicesToDelete 进行排序,然后对indicesToDelete 进行迭代,每次删除都会增加一个indexCorrection,该indexCorrection 会从后续索引中减去。

似乎我在想这样一个看似微不足道的任务。有更好的想法吗?


编辑这是解决方案,基本上是 #1 的变体,但使用迭代器来定义要复制到结果的块。

template<typename T>
inline std::vector<T> erase_indices(const std::vector<T>& data, std::vector<size_t>& indicesToDelete/* can't assume copy elision, don't pass-by-value */)
{
    if(indicesToDelete.empty())
        return data;

    std::vector<T> ret;
    ret.reserve(data.size() - indicesToDelete.size());

    std::sort(indicesToDelete.begin(), indicesToDelete.end());

    // new we can assume there is at least 1 element to delete. copy blocks at a time.
    std::vector<T>::const_iterator itBlockBegin = data.begin();
    for(std::vector<size_t>::const_iterator it = indicesToDelete.begin(); it != indicesToDelete.end(); ++ it)
    {
        std::vector<T>::const_iterator itBlockEnd = data.begin() + *it;
        if(itBlockBegin != itBlockEnd)
        {
            std::copy(itBlockBegin, itBlockEnd, std::back_inserter(ret));
        }
        itBlockBegin = itBlockEnd + 1;
    }

    // copy last block.
    if(itBlockBegin != data.end())
    {
        std::copy(itBlockBegin, data.end(), std::back_inserter(ret));
    }

    return ret;
}

【问题讨论】:

  • #3 应该是“先对indeciesToDelete排序,然后按相反的顺序删除。那就不需要更正了。虽然,它仍然是缓慢的答案。
  • 项目是否只有少量元素。或者它可以是巨大的吗?复制 T 的成本是多少?销毁 T 的成本是多少? T 是否可移动(如 C++11 中的可移动)?
  • T 是一个小结构,包含一对 std::strings 和一些整数。通常我们会删除少量元素。我将使用下面发布的反向排序解决方案。谢谢大家。
  • 一定会喜欢你选择列表而不是矢量的时代
  • 你关心保持向量中元素的顺序吗?

标签: c++ algorithm


【解决方案1】:

我会选择 1/3,即:对索引向量排序,在数据向量中创建两个迭代器,一个用于读取,一个用于写入。将写入迭代器初始化为要删除的第一个元素,并将读取迭代器初始化为超出该元素的一个。然后在循环的每个步骤中,将迭代器递增到下一个值(写入)和下一个不被跳过的值(读取)并复制/移动元素。在循环结束时调用erase 丢弃最后写入位置之外的元素。

顺便说一句,这是在 STL 的 remove/remove_if 算法中实现的方法,不同之处在于您将条件保存在单独的有序向量中。

【讨论】:

  • 我已经实现了您的解决方案并将代码添加到问题中。干杯先生。
  • 一个更简单的算法是对 indicesToDelete 进行排序,并以相反的顺序从向量中删除元素。您不需要更新任何索引,因为所有后续索引都会降低。请注意,标准的erase 函数效率不高。如果不必保留顺序,请将一个元素与最后一个元素交换,并改为使用resize
  • @Angelorf:答案中的方法比您的方法更有效,两种方法都存在对索引进行排序的成本,每个 valid 元素最多复制一次我的方法,而在最坏的情况下,这将是您的线性次数。对erase 的调用并非效率低下。当调用完成时,它的行为就像 resize 到较小的尺寸 (erase(it,end())),检查中可能存在小的常数因子差异,如果您对 erase 感觉强烈,您可以将其替换为resize(it-begin()) 达到同样的效果。 [...]
  • [...] erase(it,end())resize 之间的差异仍将远小于从末端多次移动元素所产生的额外成本。成本将与方法相似,不同之处在于这是稳定的。
  • @DavidRodríguez-dribeas 输入向量大小为 N 且要移除 M 个元素,复制的总时间复杂度为 O(M)。单个元素可能被复制/移动 M 次这一事实可能会误导您认为最坏情况下的复制移动总数将是 O(M²),但事实并非如此。但是,在我们只保留最后一个元素的情况下,我的算法执行 M-1 个副本,而您的算法只执行一个副本——这是您算法的一个参数。然而,我的算法可以用更少的代码来执行。它易于理解和实施。
【解决方案2】:

这是我对这个问题的解决方案,它保持原始“项目”的顺序:

  1. 创建一个“矢量掩码”并用“假”值初始化(填充)它。
  2. 将要删除的所有索引的掩码值更改为“true”。
  3. 遍历“mask”的所有成员,并从向量“items”和“mask”中删除具有“true”值的元素。

这是代码示例:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<unsigned int> items(12);
    vector<unsigned int> indicesToDelete(3);
    indicesToDelete[0] = 3;
    indicesToDelete[1] = 0;
    indicesToDelete[2] = 1;
    for(int i=0; i<12; i++) items[i] = i;

    for(int i=0; i<items.size(); i++)
      cout << "items[" << i << "] = " << items[i] << endl;

    // removing indeces
    vector<bool> mask(items.size());
    vector<bool>::iterator mask_it;
    vector<unsigned int>::iterator items_it;
    for(size_t i = 0; i < mask.size(); i++)
      mask[i] = false;
    for(size_t i = 0; i < indicesToDelete.size(); i++)
      mask[indicesToDelete[i]] = true;        

    mask_it = mask.begin();
    items_it = items.begin();
    while(mask_it != mask.end()){
      if(*mask_it){
        items_it = items.erase(items_it);
        mask_it = mask.erase(mask_it);
      }
      else{
        mask_it++;
        items_it++;
      }
    }

    for(int i=0; i<items.size(); i++)
      cout << "items[" << i << "] = " << items[i] << endl;

    return 0;
}

这不是用于大型数据集的快速实现。 “erase()”方法在消除元素后需要时间重新排列向量。

【讨论】:

    【解决方案3】:

    由于讨论已在某种程度上转变为与性能相关的问题,因此我编写了以下代码。它使用remove_ifvector::erase,它们应该移动元素最少的次数。有一点开销,但对于大的情况,这应该不错。

    但是,如果你不关心元素的相对顺序,那么这不会那么快。

    #include <algorithm>
    #include <iostream>
    #include <string>
    #include <vector>
    #include <set>
    
    using std::vector;
    using std::string;
    using std::remove_if;
    using std::cout;
    using std::endl;
    using std::set;
    
    struct predicate {
        public:
            predicate(const vector<string>::iterator & begin, const vector<size_t> & indices) {
                m_begin = begin;
                m_indices.insert(indices.begin(), indices.end());
            }
    
            bool operator()(string & value) {
                const int index = distance(&m_begin[0], &value);
                set<size_t>::iterator target = m_indices.find(index);
                return target != m_indices.end();
            }
    
        private:
            vector<string>::iterator m_begin;
            set<size_t> m_indices;
    };
    
    int main() {
        vector<string> items;
        items.push_back("zeroth");
        items.push_back("first");
        items.push_back("second");
        items.push_back("third");
        items.push_back("fourth");
        items.push_back("fifth");
    
        vector<size_t> indicesToDelete;
        indicesToDelete.push_back(3);
        indicesToDelete.push_back(0);
        indicesToDelete.push_back(1);
    
        vector<string>::iterator pos = remove_if(items.begin(), items.end(), predicate(items.begin(), indicesToDelete));
        items.erase(pos, items.end());
    
        for (int i=0; i< items.size(); ++i)
            cout << items[i] << endl;
    }
    

    这个的输出是:

    second
    fourth
    fifth
    

    仍然可以减少一些性能开销。在 remove_if (至少在 gcc 上)中,谓词按向量中每个元素的值复制。这意味着我们可能每次都在集合 m_indices 上执行复制构造函数。如果编译器无法摆脱这一点,那么我建议将索引作为一个集合传递,并将其存储为 const 引用。

    我们可以这样做:

    struct predicate {
        public:
            predicate(const vector<string>::iterator & begin, const set<size_t> & indices) : m_begin(begin), m_indices(indices) {
            }
    
            bool operator()(string & value) {
                const int index = distance(&m_begin[0], &value);
                set<size_t>::iterator target = m_indices.find(index);
                return target != m_indices.end();
            }
    
        private:
            const vector<string>::iterator & m_begin;
            const set<size_t> & m_indices;
    };
    
    int main() {
        vector<string> items;
        items.push_back("zeroth");
        items.push_back("first");
        items.push_back("second");
        items.push_back("third");
        items.push_back("fourth");
        items.push_back("fifth");
    
        set<size_t> indicesToDelete;
        indicesToDelete.insert(3);
        indicesToDelete.insert(0);
        indicesToDelete.insert(1);
    
        vector<string>::iterator pos = remove_if(items.begin(), items.end(), predicate(items.begin(), indicesToDelete));
        items.erase(pos, items.end());
    
        for (int i=0; i< items.size(); ++i)
            cout << items[i] << endl;
    }
    

    【讨论】:

      【解决方案4】:

      这取决于您要删除的号码。

      如果您要删除许多项目,将未删除的项目复制到新向量然后用新向量替换旧向量(在对indicesToDelete 进行排序之后)可能是有意义的。这样,您将避免在每次删除后压缩向量,这是一个 O(n) 操作,可能会使整个过程 O(n^2)。

      如果您要删除一些项目,可能会以反向索引顺序进行删除(假设索引已排序),那么您不需要在项目被删除时调整它们。

      【讨论】:

        【解决方案5】:

        基本上,问题的关键是记住,如果您删除索引 i 处的对象,并且不使用墓碑占位符,那么向量必须复制 all i 之后的对象。这适用于您建议的所有可能性,#1 除外。无论您删除多少,复制到新列表都会生成一份副本,这使得它是最快的答案。
        正如 David Rodríguez 所说,对要删除的索引列表进行排序可以进行一些小的优化,但只有在删除超过 10-20 个时才值得(请先配置文件)。

        【讨论】:

          【解决方案6】:

          它甚至可能是选项 4:

          如果您要从大量中删除一些项目,并且知道永远不会有高密度的已删除项目:

          用“墓碑”值替换应删除的索引处的每个项目,表明这些索引处没有任何有效内容,并确保无论何时访问项目,都检查墓碑。

          【讨论】:

          • +1 开箱即用! tombstones 值在多线程情况下非常好,例如,您希望尽可能将更改保持在本地。但请注意,并非所有要求都可以容纳墓碑,值得注意的是,现在将“真实”索引映射到带有幻像元素的向量中的索引的成本很高。
          【解决方案7】:

          std::sort()indicesToDelete 按降序排列,然后在正常的for 循环中从items 中删除。那就不用调整指数了。

          【讨论】:

          • 想法 #1 的速度要快得多。
          • 想法#1,结合第二个向量的排序,使得线性算法 O( M + N ),在两个向量的大小上,实际上是 O(N) 在大小上要修改的向量。
          • 不是立即删除,如果操作后vector的顺序不重要,将索引处的元素与向量的末尾交换,然后在最后,擦除最后n 元素,其中nindicesToDelete 向量中的元素数。
          猜你喜欢
          • 2014-05-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-26
          • 2011-07-26
          • 1970-01-01
          相关资源
          最近更新 更多