【问题标题】:Difference between erase and remove擦除和删除的区别
【发布时间】:2010-10-22 09:48:29
【问题描述】:

我对 std::remove 算法的用法之间的区别有点困惑。具体来说,当我使用此算法时,我无法理解要删除的内容。我写了一个这样的小测试代码:

std::vector<int> a;
a.push_back(1);
a.push_back(2);

std::remove(a.begin(), a.end(), 1);


int s = a.size();

std::vector<int>::iterator iter = a.begin();
std::vector<int>::iterator endIter = a.end();

std::cout<<"Using iter...\n";
for(; iter != endIter; ++iter)
{
    std::cout<<*iter<<"\n";
}

std::cout<<"Using size...\n";
for(int i = 0; i < a.size(); ++i)
{
    std::cout<<a[i]<<"\n";
}

两种情况下的输出都是 2,2。

但是,如果我将擦除与删除一起使用,则如下所示:

a.erase(std::remove(a.begin(), a.end(), 1), a.end());

我得到的输出为 2。

所以我的问题是:

(1)。 std::remove 除了与擦除功能一起使用外,还有其他用途吗?

(2)。即使在执行 std::remove 之后,为什么 a.size() 返回 2 而不是 1?

我在 Scott Meyer 的 Effective STL 书中阅读了有关擦除删除习语的内容。但我仍然有这种困惑。

【问题讨论】:

  • 我发现这方面最难的部分是擦除和删除几乎是英文保存的意思,所以很容易忘记哪个是哪个

标签: c++ stl


【解决方案1】:

std::remove 有什么作用?

这是std::remove 的伪代码。花几秒钟看看它在做什么,然后阅读说明。

Iter remove(Iter start, Iter end, T val) {
    Iter destination = start;

    //loop through entire list
    while(start != end) { 
        //skip element(s) to be removed
        if (*start == val) { 
            start++; 
         }
         else //retain rest of the elements
             *destination++ = *start++;
     }

     //return the new end of the list
     return destination;
}

请注意,remove 只是将序列中的元素向上移动,覆盖您想要删除的值。所以你想要删除的值确实已经消失了,但是问题是什么?假设您有值为 {1, 2, 3, 4, 5} 的向量。在为 val = 3 调用 remove 之后,向量现在具有 {1, 2, 4, 5, 5}。也就是说,4 和 5 向上移动,因此 3 从向量中消失了,但 向量的大小 没有改变。此外,向量的末尾现在包含 5 的额外剩余副本。

vector::erase 有什么作用?

std::erase 获取您想要摆脱的范围的开始和结束。它不需要 您要删除的值,只需要范围的开始和结束。这是它如何工作的伪代码:

erase(Iter first, Iter last)
{
    //copy remaining elements from last
    while (last != end())
        *first++ = *last++;

   //truncate vector
   resize(first - begin());
}

所以擦除操作实际上改变了容器的大小并释放了内存。

remove-erase 习惯用法

std::removestd::erase 的组合允许您从容器中删除匹配的元素,这样如果元素被删除,容器实际上会被截断。操作方法如下:

//first do the remove
auto removed = std::remove(vec.begin(), vec.end(), val);

//now truncate the vector
vec.erase(removed, vec.end());

这被称为remove-erase idiom。为什么会这样设计?洞察力是查找元素的操作更加通用,并且独立于底层容器(仅依赖于迭代器)。但是擦除操作取决于容器如何存储内存(例如,您可能使用链表而不是动态数组)。因此 STL 期望容器在提供通用“删除”操作的同时进行自己的擦除,因此所有容器都不必实现该代码。在我看来,这个名字非常具有误导性,std::remove 应该被称为std::find_move

注意:以上代码是严格的伪代码。实际的 STL 实现更智能,例如使用std::move 而不是复制。

【讨论】:

    【解决方案2】:

    要在vector等容器中删除具有某些条件(等于某个值或其他条件,例如小于)的元素,它总是结合函数成员函数erasestd::removestd::remove_if

    在vector中,函数erase可以按位置删除元素,如:

    迭代器擦除(迭代器位置);

    迭代器擦除(先迭代器,后迭代器);

    但如果你想擦除某些条件的元素,你可以将它与std::removestd::remove_if结合起来。

    例如,您想擦除以下向量中的所有元素6

    std::vector<int> vec{6, 8, 10, 3, 4, 5, 6, 6, 6, 7, 8};
    // std::remove move elements and return iterator for vector erase funtion
    auto last = std::remove(vec.begin(), vec.end(), 6);
    for(int a:vec)
        cout<<a<<" ";
    cout<<endl;
    // 8 10 3 4 5 7 8 6 6 7 8 
    
    vec.erase(last, vec.end());
    for(int a:vec)
        cout<<a<<" ";
    cout<<endl;
    // 8 10 3 4 5 7 8 
    

    std::remove 如下所示,它不会擦除任何元素,它只是移动元素并返回迭代器。

    可能的实现:

    template< class ForwardIt, class T >
    ForwardIt remove(ForwardIt first, ForwardIt last, const T& value)
    {
        first = std::find(first, last, value);
        if (first != last)
            for(ForwardIt i = first; ++i != last; )
                if (!(*i == value))
                    *first++ = std::move(*i);
        return first;
    }
    

    结论:

    如果你想删除某些条件的元素,你基本上使用vector::iterator erase (iterator first, iterator last);

    首先获取范围开始:

    auto last = std::remove(vec.begin(), vec.end(), equal_condition_value);

    按范围擦除(始终使用 end())

    vec.erase(last, vec.end());

    引用:

    https://en.cppreference.com/w/cpp/algorithm/remove

    【讨论】:

    • 这是最清楚最全面的答案。此外,它直观地显示了迭代器,包括remove 返回的迭代器 显示了removeerase 之间的关系,迭代器方式。在阅读此答案之前,我不明白事情是如何运作的。
    【解决方案3】:

    我遇到了同样的问题,试图了解其中的区别。 到目前为止给出的解释都在钱上,但我只是在看到一个例子后才理解;

    #include <algorithm>
    #include <string>
    #include <iostream>
    #include <cctype>
    
    int main()
    {
        std::string str1 = "Text with some   spaces";
        std::string::iterator it = remove(str1.begin(), str1.end(), 't');
        std::cout << str1 << std::endl;// prints "Tex wih some   spaceses"
        for (str1.begin();it != str1.end(); ++it) 
        {
             std::cout << *it; //prints "es"
        }
    
    }
    

    如您所见,remove 仅将小写的 't' 移动到字符串的末尾,同时将新的迭代器返回到新字符串的末尾(新字符串是旧字符串直到被删除的位置元素被插入) 这就是为什么当您打印从“删除”获得的迭代器时

       "Text with some   spaces"
           ^   ^removes both 't', then shift all elements forward -1 //what we want to remove
       "Text with some   spaces"
                              ^ end of string                    -2 //original state of string
       "Tex with some   spacess"
                              ^end of string                     -3 //first 't' removed
       "Tex wih some   spaceses"
                              ^end of string                     -4 //second 't' removed
       "Tex wih some   spaceses"
                            ^new iterator that remove() returned -5 // the state of string after "remove" and without "erase"
    

    如果你将第 5 步获得的迭代器传递给“erase()”,它将知道从那里擦除到字符串的末尾,重新调整进程中的字符串大小

    【讨论】:

      【解决方案4】:

      我能想到的最简单的方法:

      erase() 是您可以对容器中的元素执行的操作。给定容器中的迭代器/索引,erase( it ) 会从容器中删除迭代器引用的内容。

      remove() 是您可以对范围执行的操作,它会重新排列该范围,但不会 删除范围内的任何内容。

      【讨论】:

        【解决方案5】:

        remove 并不是“真正”删除 任何事情,因为它做不到。

        为了“真正地”从容器中移除元素,您需要访问容器 API。删除仅适用于迭代器,而与这些迭代器指向的容器无关。因此,即使 remove 想要“实际删除”,它也不能。

        用以下未删除的元素删除覆盖“已删除”的元素,然后由调用者决定使用返回的新逻辑end 而不是原来的end

        在您的情况下,从vector a 中删除逻辑上删除的1,但大小仍为 2 本身。擦除实际上从向量中删除了元素。 [从向量new endold end]

        remove 的主要思想是它不能改变元素的数量,它只是根据标准从一个范围内删除元素。

        【讨论】:

          【解决方案6】:

          remove() 实际上并没有从容器中删除元素——它只是将未删除的元素向前分流到已删除元素的顶部。关键是要意识到remove() 不仅适用于容器,而且适用于任意前向迭代器对:这意味着它不能实际删除元素,因为任意迭代器对不一定具有删除元素的能力。

          例如,指向常规 C 数组开头和结尾的指针是前向迭代器,因此可以与 remove() 一起使用:

          int foo[100];
          
          ...
          
          remove(foo, foo + 100, 42);    // Remove all elements equal to 42
          

          这里很明显remove() 无法调整数组大小!

          【讨论】:

            【解决方案7】:

            std::remove 不会删除实际的对象,而是将它们推到容器的末尾。内存的实际删除和释放是通过擦除完成的。所以:

            (1)。 std::remove 除了与擦除功能一起使用外,还有其他用途吗?

            是的,它有助于将一对迭代器放入一个新序列,而不必担心适当的解除分配等问题。

            (2)。即使在执行 std::remove 之后,为什么 a.size() 返回 2 而不是 1?

            容器仍然保存这些对象,您只有一组新的迭代器可以使用。因此,大小仍然是以前的大小。

            【讨论】:

            • 嗯。实际上 std::remove() 不会将删除的元素移动到容器的末尾——容器的剩余位置将包含它们的原始值。 (它必须以这种方式工作以使用前向迭代器保留 O(n) 时间。)
            • j_random_hacker,嗯,实际上它看起来像在它完成工作之后,返回的迭代器之后的范围也不能包含任何“删除”元素 - 这是我尚未考虑的附加要求 - 所以这些要素并非完全不确定。所以我的答案是错误的,我把它删除了
            • 它说:“消除迭代器 i 在范围 [first, last) 中引用的所有元素,这些元素满足以下相应条件:*i == value”,我将其解释为该范围最后不再包含任何“已删除”的值。有趣,我不知道。但我不认为人们可以依赖这些元素与以前相同。如果该范围至少包含“已删除”值,则肯定不能。所以我认为 cplusplus.com 又错了 imo :D
            • 最后,我认为这只是措辞不佳,我不应该删除我的答案。我认为returned_iterator 之后的元素只是有一些不确定的值。叹息:(呵呵
            • @litb:我不认为 remove() 可以将剩余元素“归零”,因为不需要表达式“*i = 0”(其中 i 是前向迭代器)有效。
            猜你喜欢
            • 2015-04-22
            • 1970-01-01
            • 1970-01-01
            • 2012-08-18
            • 1970-01-01
            • 2019-02-20
            • 2013-10-18
            • 2012-07-16
            • 2014-05-10
            相关资源
            最近更新 更多