【问题标题】:Best way to erase vector of ranges from std::vector从 std::vector 擦除范围向量的最佳方法
【发布时间】:2016-12-12 12:04:47
【问题描述】:

在我的一个项目中,有必要从std::vector<double> values 中删除某些元素。我必须删除的索引作为间隔向量给出。例如 {1,3} 意味着,我必须从 values 中删除从 1 到 3 的索引。

我可以假设给定的区间是互斥的。

下面显示的代码说明了所需的行为应该是什么样的。

#include <iostream>
#include <vector>

int main(int argc, char** args) {
    // Intervals of indices I have to remove from values
    std::vector<std::pair<int, int>> intervals = { {1,3},{7,9},{13,13} }; 

    // Vector of arbitrary values. 
    std::vector<double> values = {4.2,6.4,2.3,3.4,9.1,2.3,0.6,1.2,0.3,0.4,6.4,3.6,1.4,2.5,7.5 }
    removeIntervals(values, intervals);
    // intervals should contain 4.2,9.1,2.3,0.6,6.4,3.6,1.4,7.5
}

实现这一目标所需的最短代码量可能是多少?

到目前为止我最好的解决方案是:

 void removeIntervals(std::vector<double>& values, const std::vector < std::pair<int, int>>& intervals) {
    std::vector<bool> flags(values.size(), true);
    std::vector<double> ret;
    for (auto interval : intervals) {
        std:fill(flags.begin() + interval.first, flags.begin()+interval.second+1, false);
    }
    for (auto i = 0; i < values.size(); i++) {
        if (flags[i]) ret.push_back(values[i]);
    }
    values = ret;
 }

我可以假设,我的间隔是不重叠和连续的。看来,归结为从后到前执行擦除。

void removeIntervals2(std::vector<double>& values, const std::vector < std::pair<int, int>>& intervals) {
    auto revIntervals = intervals;
    std::reverse(revIntervals.begin(), revIntervals.end());
    for (auto interval : revIntervals) {
        values.erase(std::begin(values) + interval.first, std::begin(values) + interval.second + 1);
    }
}

【问题讨论】:

  • 看看std::remove_if。它实际上应该非常简单
  • 在示例场景中,如果您删除 {1, 3} 和 {7, 9},您的向量将有 8 个元素,因此无法删除 {13, 13}。也许您的问题描述需要更新?
  • 你能断言你的间隔不重叠吗?并且它们总是按递增顺序排列?
  • @UKMonkey 我可以保证间隔不重叠。在我的情况下,我可以假设它们是按递增顺序排列的。也许可以调用连续擦除,但是从后面开始?
  • 要求最少的代码量是(1)定义不明确和(2)无意义的。最好要求高效的代码或类似的。

标签: c++ algorithm stl stdvector std-pair


【解决方案1】:

由于您可以假设区间不重叠并且是递增顺序,因此解决方案是从后面开始(以便索引不会改变)并依次删除每个范围:

因此,对于您要求的最少代码量:

for (auto& it = intervals.rbegin(); it != intervals.rend(); ++it) {
  values.erase(values.begin() + it->first, std::next(values.begin() + it->second));

这样做的缺点是这将涉及大量的向量改组。实际上,您想要做的是将向量末尾的最后一个未交换的项目与要删除的项目交换,然后在完成后调整大小以切断末端;但这需要更多代码。

【讨论】:

  • 非常感谢。这似乎是我一直在寻找的最短的解决方案。太简单! :-)
  • erase() 擦除一系列元素 [first,last)。所以你的代码会因为包容性而失败。例如:如果是{13, 13},则不会有任何删除。我已经使用std::next() 编辑了你的答案,以便最后得到真实的答案。
  • 您的代码而言,这可能是最短的,但就整体代码或生成的代码而言不一定是最短的。此外,它肯定不是最有效的解决方案,因为它需要大量复制/移动元素。
  • @Walter 如我的回答中所述 - 有一种更好的算法可以在一般情况下减少洗牌,但根据要删除的内容,所需的洗牌次数可能最少,并且改进是过早的优化.
【解决方案2】:

这个问题很重要,因为在第一次调用vector::erase() 之后,所有索引/迭代器在第一个擦除之后的元素都将失效,包括要删除的进一步间隔。

因此,使用vector::erase() 必须按照要删除的元素的降序进行。

另一个不便源于使用int 索引而不是区间边界的迭代器。最后,vector::erase() 复制(矿石移动)所有元素超过最后移除的元素以填补空白。这保留了值的顺序,但在多个间隔的情况下会导致过度复制(移动)。

一种更有效的方法是只交换要删除的元素,最后缩小向量的大小。

【讨论】:

  • 使用反向迭代器还是很简单的。
  • 使用 reverse_iterator 很简单。 “琐碎”与问题有关,与解决方案无关。另请注意,如果效率很重要,那么您的解决方案就很糟糕。
【解决方案3】:

您肯定想要的是一个不仅代码短而且效率高的解决方案,最大限度地减少值向量中的副本和移位。

我肯定会选择您解决方案的第一部分,即删除要保留或删除的位置。

std::vector<bool> flags(values.size(), true);
for (auto interval : intervals) {
    std:fill(flags.begin() + interval.first, flags.begin()+interval.second+1, false);
}

对于第二部分,最短且最有效的是erase/remove_if 成语:

 values.erase(std::remove_if(begin(values), end(values),
    [&](const auto& v) { return !flags[&v - &(*values.begin())];}),
  values.end());

这里的效率是因为remove_if会首先标记需要移除的元素,然后它会通过首先放置元素来压缩向量并返回元素的位置要删除的第一个元素。最后,erase 将缩小向量。从算法的角度来看,这个解决方案可能是最优的。它应该为大型向量付出代价。

【讨论】:

    【解决方案4】:

    我想我会发布一个更容错的答案。如果您的间隔大于输入数组,例如如果intervals 包含{15, 15},这仍然可以正常工作。此外,这比 UKMonkey's solution 更快,因为它一次性完成所有工作:

    我注意到这段代码是实现定义的,并且只适用于Clang and Visual Studio 2015 Update 3:

    values.resize(distance(begin(values), remove_if(begin(values), end(values), [i = 0U, it = cbegin(intervals), end = cend(intervals)](const auto&) mutable { return it != end && ++i > it->first && (i <= it->second || (++it, true)); })));
    

    Live Example

    您可以在 for-loop 中完成同样的事情:

    size_t write = 0U;
    auto it = cbegin(intervals);
    
    for (size_t read = 0U; read < size(values); ++read) {
        if (it == cend(intervals) || read < it->first) {
            values[write++] = values[read];
        } else if (read == it->second) {
            ++it;
        }
    }
    
    values.resize(write);
    

    Live Example

    如果你迷上了“实现这一目标所需的最短代码量”,你也可以在for-loop 中使用来自 lambda 的我邪恶的,

    for (size_t read = 0U; read < size(values); ++read) if (it == cend(intervals) || read < it->first || (read == it->second && (++it, false))) values[write++] = values[read];
    

    【讨论】:

    • 这个解决方案对我来说似乎坏了。 (1) vector::erase 接受迭代器参数,而不是 int 索引; (2) 在第一次调用vector::erase() 之后,最后一个被擦除的迭代器之后的所有迭代器都将失效,从而中断对vector::erase() 的进一步调用(包括UB)。
    • @Walter 好电话。你是对的。我已经更新了我的答案。
    【解决方案5】:

    嗯,到目前为止的答案都很糟糕——要么制作全新的向量,要么需要 O(N^2) 时间——所以我会添加这个。

    不是删除你不想保留的元素,而是每次都移动其余的元素,而是将你确实想要保留的元素移动到正确的位置,然后截断向量.

    O(N) 时间,没有多余的空间:

    void removeIntervals(std::vector<double>& values, const std::vector < std::pair<int, int>>& intervals) {
        if (intervals.size()<=0)
            return;
    
        //keep the part before the first interval
        auto dest = values.begin()+intervals[0].first;
    
        for (size_t i=0; i<intervals.size(); ++i) {
    
            //copy the part to keep after each interval
            auto s = values.cbegin()+intervals[i].second+1;
            auto e = (i+i >= intervals.size() ?
                      values.cend() : 
                      values.cbegin()+intervals[i+1].first);
            while(s<e) {
                *dest++=*s++;
            }
        }
        values.erase(dest,values.end());
     }
    

    【讨论】:

      【解决方案6】:

      作为 Matt Timmermans 回答的补充:这不是问题,但如果您只想在 C++17 中保留区间中的值,您可以编写:

      void remove_if_not_in_interval(std::vector<double>& result, const std::vector<std::pair<int,int> >& intervals)
          {
            if (intervals.size() == 0)
              result.clear();
      
            auto dest = result.begin();
            for (auto [first, last] : intervals)
              {
                while(first!=last+1)
                  {
                    *dest++ = *(result.begin() + first++);
                  }
              }
      
            result.erase(dest,result.end());
          }
      

      【讨论】:

        猜你喜欢
        • 2011-07-17
        • 2011-05-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-20
        • 2017-05-18
        • 1970-01-01
        • 2018-12-05
        相关资源
        最近更新 更多