在应用中,我们通常不可避免地要对容器中的某些特定元素进行删除操作。这看起来并不是什么困难的问题。我们先写一个循环来迭代容器中的元素,如果迭代元素是要删除的元素,则删除之。代码如下所示:
vector<int> intContainer;
.....
for(vector<int>::iterator is = intContainer.begin();
it != intContainer.end();
++it)
{
if ( *it == 25)
intContainer.erase(it);
}
写出此代码的原意是将vector中值为25的元素删除,但不幸的是,这样做是错误的,这么做会带来诡异的未定义行为。因为当一个容器的一个元素被删除时,指向那个元素的所有迭代器将失效。当intContainer.erase(it)返回时,it已经失效。在for循环中对于失效的it执行自增操作,这是一件多么不靠谱的事情啊。既然这样行不通,那么我们可以求助于STL提供的remove算法。借助remove算法来达到删除元素的目的。
vector<int> intContainer;
.....
size_t before_size = intContainer.size();
remove(intContainer.begin(), intContainer.end(), 25);
size_t after_size = intContainer.size();
运行程序以后发现before_size和after_size是一样的,说明元素并没有被真正删除。写出以上程序,是处于对remove算法的不了解而致。STL中remove算法会将不该删除的元素前移,然后返回一个迭代器,该迭代器指向的是那个应该删除的元素,仅此而已。所以如果要真正删除这一元素,在调用remove之后还必须调用erase,这就是STL容器元素删除的"erase_remove"的惯用法。
vector<int> intContainer;
.....
intContainer.erase( remove(intContainer.begin(),
intContainer.end(), 25),
intContainer.end());
erase-remove的惯用法适用于连续内存容器,比如vector,deque和string,它也同样适用于list,但是并不是我们推荐的方法,因为使用list成员函数remove会更高效,代码如下:
list<int> list_int;
....
list_int.remove(25);
如果是关联容器呢?标准关联容器没有remove成员函数,使用STL算法的remove函数时编译同不过。所以上述remove形式对于标准关联容器并不适用。在这种情况下,解决办法就是调用erase:
map<int, int> mapContainer;
...
mapContainer.erase(25);
对于标准关联容器,这样的元素删除方式是简单有效的,时间复杂度为O(logn).
当我们需要删除的不是某一个元素,而是具备某一条件的元素的时候,我们只需要将remove替换成remove_if即可
bool Is2BeRemove(int value)
{
return value < 25;
}
vector<int> nVec;
list<int> nList;
....
nVec.erase(remove_if(nVec.begin(), nVec.end(), Is2BeRemove),
nVec.end());
nList.remove_if(Is2BeRemove);
总结如下
删除容器中具有特定值的元素:
如果容器是vector、string或者deque,使用erase-remove的惯用法。如果容器是list,使用list::remove。如果容器是标准关联容器,使用它的erase成员函数。
删除容器中满足某些条件的元素:
如果容器是vector、string或者deque,使用erase-remove_if的惯用法。如果容器是list,使用list::remove_if。如果容器是标准关联容器,使用remove_copy_if&swap 组合算法,或者自己设计个遍历删除算法。
重点提一下以下内容,学完aoj0121有感
STL的remove函数和list的remove成员函数
今天看书刚刚看的,就记录下来吧。这可能是老生常谈了,权且作为一个警醒的例子吧。
大家都知道STL有两个非常重要的组成部分,容器和算法。
算法就是一个个的函数,通过迭代器和容器关联在一起,完成一些工作。
算法和容器的分离为程序设计提供了很大的灵活性,但是也带来了一些负面效果,下面我讲的这个问题就是一个例子。
STL的算法里有一个remove函数,而list自身也有一个remove函数,功能都是一样的,移除某一个元素,那我们应该使用哪一个呢?
看一下下面这段程序
<span style="font-size:14px;">list<int> numbers;
for ( int number = 0; number <= 6; number ++ ) {
numbers.push_front(number);
numbers.push_back(number);
}
copy(numbers.begin(), numbers.end(),
ostream_iterator<int>(cout, " "));
cout << endl;
// remove algorithm will remove element but not erase the element from container
// it will return the logical desination of container
list<int>::iterator endOfNumbers = remove(numbers.begin(), numbers.end(), 3);
copy(numbers.begin(), numbers.end(),
ostream_iterator<int>(cout, " "));
cout << endl;</span>
输出是什么呢?
第一行肯定是6 5 4 3 2 1 0 0 1 2 3 4 5 6,那么第二行会输出什么?
如果是没有仔细看过STL的人肯定会认为remove(number.begin(), numbers.end(), 3)会移除所有值为3的元素。所以输出是:6 5 4 2 1 0 0 1 2 4 5 6。
但是,我们看一下它真正的输出:
6 5 4 2 1 0 0 1 2 4 5 6 5 6
你可能会非常惊讶,为什么最后会多出5和6两个数呢?
我们来讲一下remove算法的原理。
remove算法工作时并不是直接把元素删除,而是用后面的元素替代前面的元素,也即是说如果我对1234这个序列remove 2,返回的序列是 1344(3被复制到2的位置,4被复制到3的位置)。
这样上面的例子就好解释了,那两个3的元素并没有被移除,而是用后面的元素覆盖了前面的元素。多出的那两个数没有被移除掉而已。
那么我们应该如何真正完成移除呢?remove函数会返回一个迭代器,那个迭代器是这个序列的逻辑终点,也即是我代码里的endOfNumbers,它指向倒数第二个5上。
于是我们要利用list的erase函数完成元素移除
numbers.erase(endOfNumbers, numbers.end());
这样我们就完成了我们的工作,稍稍有点曲折……
其实我们可以把这两步放在一起,比如如果我想接着移除所有值为2的元素
numbers.erase(remove(numbers.begin(), numbers.end(), 2), numbers.end());
这样我们就可以一步到位了。
但是这样好么?
不好。
大家会发现,remove函数的原理是复制而不是指针的移动(因为函数操纵的是迭代器,而C++的迭代器没有定义删除操作),这样会带来一个问题:我们使用list是因为它的修改的效率非常高,改变一下指针就可以了。而这里我们复制了元素,如果在vector中,可能还是高效的,因为vector无论如何都要复制,而对于list就不是如此了,会极度降低我们的效率。
那我们怎么办呢?
答案是使用list自己的remove函数
numbers.remove(1);
我们可以这样删除所有值为1的元素。
也即是说,如果要删除list中的元素,我们应该使用list的remove成员函数,而不是remove算法!
小结
我们都知道,STL是一个效率、复用性、灵活性折衷的产物,其中效率至关重要,所以STL已经禁止了一些效率低的操作(比如list的随机访问),而鼓励你去使用其它的容器。
但是,在算法中,为了灵活性,STL还是会牺牲一些东西,比如我们这个例子。
个人觉得,STL作为C++标准库的一个组成部分,特点和C++本身一模一样,强大而复杂,有些地方难以理解,很多细节需要学习注意,我们要学会避免陷入某些陷阱之中,比如这个例子就是一个效率陷阱。
其它更多的陷阱是错误处理方面的,STL本身并没有规定过多的错误处理,大部分的错误处理都交给了我们,理由很简单:性能至上,如果一个东西自身没有错误检查,我们可以包装一个带错误检查的类;但是如果这个东西自身就带了错误检查,那么我们就没有任何方法提升它的效率了。这也是很多C和C++库的设计原则。
所以,很多时候,需要我们深入细节,然后再决定到底怎么做。因为C++就是如此:有很多路可以走,需要我们自己选择最好的一条路。