Effective STL 笔记若干-有关STL使用需要注意的方面

本文详细介绍了如何在C++ STL中选择合适的算法来删除元素、排序和查找,包括不同容器类型的适用算法及注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如何删除元素:

  • 去除一个容器中有特定值的所有对象:

    如果容器是vector、string或deque,使用erase-remove惯用法。

    c.erase(remove(c.begin(), c.end(), 1963),c.end());    //删除所有1963    

     

    如果容器是list,使用list::remove。

    如果容器是标准关联容器,使用它的erase成员函数。

  • 去除一个容器中满足一个特定判定式的所有对象:

    如果容器是vector、string或deque,使用erase-remove_if惯用法。

    c.erase(remove_if(c.begin(), c.end(), badValue),c.end());  //删除所有使bool badValue(T t)返回真的元素

     

    如果容器是list,使用list::remove_if。

    如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。(因为删除使所有指向被删除元素的迭代器失效,所以必须后置递增)

    AssocContainer<int> c;        // c现在是一种 标准关联容器
    AssocContainer<int> goodValues;    // 用于容纳不删除的值的临时容器
    //从c拷贝不删除的值到goodValues
    remove_copy_if(c.begin(), c.end(),inserter(goodValues,goodValues.end()),badValue);
    c.swap(goodValues);                

    AssocContainer<int> c;
    ...
    for (AssocContainer<int>::iterator i = c.begin();i != c.end();/*nothing*/ ){
        
    if (badValue(*i)) c.erase(i++);        
        
    else ++i;
    }
                        

     

  • 在循环内做某些事情(除了删除对象之外):

    如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器。

    for (SeqContainer<int>::iterator i = c.begin(); i != c.end();){
        
    if (badValue(*i)){
            
    //做你要做的事情,如Log
            i = c.erase(i);       // 通过把erase的返回值赋给i来保持i有效
        }

        
    else  ++i;
    }

     

    如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。(这个和刚才第二种情况中写循环的方法没什么区别)

    呃。。你说list?在这里用序列容器或关联容器的方法都行。

 

如何选择排序算法

  • 如果你需要在vector、string、deque或数组上进行完全排序,你可以使用sort或stable_sort。
  • 如果你有一个vector、string、deque或数组,你只需要排序前n个元素,应该用partial_sort。
    bool qualityCompare(const Widget& lhs, const Widget& rhs);  // 返回lhs的质量是不是比rhs的质量好
    // 把最好的20个元素(按顺序)放在widgets的前端
    partial_sort(widgets.begin(),widgets.begin() + 20,widgets.end(),qualityCompare);

  • 如果你有一个vector、string、deque或数组,你需要鉴别出第n个元素或你需要鉴别出最前的n个元素,而不用知道它们的顺序,nth_element是你应该注意和调用的。
    // 把最好的20个元素 放在widgets前端,而不在意它们之间的顺序
    nth_element(widgets.begin(),widgets.begin() + 19,widgets.end(),qualityCompare);            

  • 如果你需要把标准序列容器的元素或数组分隔为满足和不满足某个标准,你大概就要找partition或stable_partition。
    // 把所有满足hasAcceptableQuality的widgets移动到widgets前端,
    // 并且返回一个指向第一个不满足的widget的迭代器
    vector<Widget>::iterator goodEnd =partition(widgets.begin(),widgets.end(),hasAcceptableQuality);

  • 如果你的数据是在list中,你可以直接使用partition和stable_partition,你可以使用list的sort来代替sort和stable_sort。如果你需要partial_sort或nth_element提供的效果,你就必须间接完成这个任务。

算法sort、stable_sort、partial_sort和nth_element需要随机访问迭代器,所以它们可能只能用于vector、string、deque和数组。对标准关联容器排序元素没有意义,因为这样的容器使用它们的比较函数来在任何时候保持有序。唯一我们可能会但不能使用sort、stable_sort、partial_sort或nth_element的容器是list,list通过提供sort成员函数做了一些补偿。(有趣的是,list::sort提供了稳定排序。)所以,如果你想要排序一个list,你可以,但如果你想要对list中的对象进行partial_sort或nth_element,你必须间接完成。一个间接的方法是把元素拷贝到一个支持随机访问迭代器的容器中,然后对它应用需要的算法。另一个方法是建立一个list::iterator的容器,对那个容器使用算法,然后通过迭代器访问list元素。第三种方法是使用有序的迭代器容器的信息来迭代地把list的元素接合到你想让它们所处的位置。正如你所见,有很多选择。

partition和stable_partition与sort、stable_sort、partial_sort和nth_element不同,它们只需要双向迭代器。因此你可以在任何标准序列迭代器上使用partition和stable_partition。

“但性能怎么样?”,你想知道。这是极好的问题。一般来说,做更多工作的算法比做得少的要花更长时间,而必须稳定排序的算法比忽略稳定性的算法要花更长时间。我们可以把我们在本条款讨论的算法排序如下,需要更少资源(时间和空间)的算法列在需要更多的前面:

1. partition

4. partial_sort

2. stable_partition

5. sort

3. nth_element

6. stable_sort

 

如何选择查找算法

下面的表格道出了一切。(你想要详细的说明?噢,那还是直接阅读Effective STL item45比较好。)

你想知道的使用的算法使用的成员函数
在无序区间在有序区间在set或map上在multiset或multimap上
期望值是否存在?findbinary_searchcountfind
期望值是否存在?如果有,第一个等于这个值的对象在哪里?findequal_rangefindfind或lower_bound(参见下面)
第一个不在期望值之前的对象在哪里?find_iflower_boundlower_boundlower_bound
第一个在期望值之后的对象在哪里?find_ifupper_boundupper_boundupper_bound
有多少对象等于期望值?countequal_range,然后distancecountcount
等于期望值的所有对象在哪里?find(迭代)equal_rangeequal_rangeequal_range

上表总结了要怎么操作有序区间,equal_range的出现频率可能令人吃惊。当搜索时,这个频率因为等价检测的重要性而上升了。对于lower_bound和upper_bound,它很容易在相等检测中退却,但对于equal_range,只检测等价是很自然的。在第二行有序区间,equal_range打败了find还因为一个理由:equal_range花费对数时间,而find花费线性时间。

对于multiset和multimap,当你在搜索第一个等于特定值的对象的那一行,这个表列出了find和lower_bound两个算法作为候选。 已对于这个任务find是通常的选择,而且你可能已经注意到在set和map那一列里,这项只有find。但是对于multi容器,如果不只有一个值存在,find并不保证能识别出容器里的等于给定值的第一个元素;它只识别这些元素中的一个。如果你真的需要找到等于给定值的第一个元素,你应该使用lower_bound,而且你必须手动的对第二部分做等价检测,Effective STL条款19的内容可以帮你确认你已经找到了你要找的值。(你可以用equal_range来避免作手动等价检测,但是调用equal_range的花费比调用lower_bound多得多。)

在count、find、binary_search、lower_bound、upper_bound和equal_range中做出选择很简单。当你调用时,选择算法还是成员函数可以给你需要的行为和性能,而且是最少的工作。按照这个建议做(或参考那个表格),你就不会再有困惑。

 

需要有序区间的算法

这里有一个只能操作有序数据的算法的表:

binary_searchlower_bound
upper_boundequal_range
set_unionset_intersection
set_differenceset_symmetric_difference
mergeinplace_merge
includes 

另外,下面的算法一般用于有序区间,虽然它们不要求:

uniqueunique_copy
unique和unique_copy甚至在无序区间上也提供了定义良好的行为。但看看标准是怎么描述unique的行为的(斜体字是雷区):

从每个相等元素的连续组中去除第一个以外所有的元素。

换句话说,如果你要unique从一个区间去除所有重复值(也就是,让区间中所有值“唯一”),你必须先确保所有重复值一个接着一个。猜到什么了?那是排序完成的东西之一。实际上,unique一般用于从区间中去除所有重复值,所以你几乎总是要确保你传递给unique(或unique_copy)的区间是有序的。(Unix开发者会发现STL的unique和Unix的uniq之间有惊人的相似,我想这个相似决不是巧合。)

顺便说说,unique从一个区间除去元素的方式和remove一样,也就是说它只是区分出不除去的元素。如果你不知道那是什么意思,请立刻转向Effective STL条款3233。强调理解remove和类似remove的算法(包括unique)行为的重要性永远没有过分的时候。光有基本的理解还不够。如果你不知道它们做了什么,你会陷入困境。

 

永远让比较函数对相等的值返回false

(噢,让我先说一句,这里的比较函数不是判等函数。。。)

简言之,请实现"<"或者">",而不是"<="或者">="。

除非你的比较函数总是为相等的值返回false,你将会打破所有的标准关联型容器,不管它们是否允许存储复本。

从技术上说,用于排序关联容器的比较函数必须在它们所比较的对象上定义一个“严格的弱序化(strict weak ordering)”。(传给sort等算法的比较函数也有同样的限制)。如果你对严格的弱序化含义的细节感兴趣,可在很多全面的STL参考书中找到,比如Josuttis的《The C++ Standard Library》,Austern的《Generic Programming and the STL》,和SGI STL的网站。 我从未发现这个细节如此重要,但一个对严格的弱序化的要求直接指向了这个条款。那个要求就是任何一个定义了严格的弱序化的函数都必须在传入相同的值的两个拷贝时返回false。

 

警惕C++的奇怪解析

假设你有一个int的文件,你想要把那些int拷贝到一个list中。这看起来像是一个合理的方式:

ifstream dataFile("ints.dat");
// 警告!这完成的并不是像你想象的那样
list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());

这里的想法是传一对istream_iterator给list的区间构造函数,因此把int从文件拷贝到list中。

这段代码可以编译,但在运行时,它什么都没做。它不会从文件中读出任何数据。它甚至不会建立一个list。那是因为第二句并不声明list,而且它也不调用构造函数。其实它做的是……

打起精神,这声明了一个函数data,它的返回类型是list<int>。这个函数data带有两个参数:

  • 第一个参数叫做dataFile。它的类型是istream_iterator<int>。dataFile左右的括号是多余的而且被忽略。
  • 第二个参数没有名字。它的类型是指向一个没有参数而且返回istream_iterator<int>的函数的指针。

奇怪吗?但这符合C++里的一条通用规则——几乎任何东西都可能被分析成函数声明。如果你用C++编程有一段时间了,你应该会遇到另一个这条规则的表象。有多少次你会看见这个错误?

class Widget {...};    // 假设Widget有默认构造函数
Widget w();        // 嗯哦……

这并没有声明一个叫做w的Widget,它声明了一个叫作w的没有参数且返回Widget的函数。学会识别这个失言(faux pas)是成为C++程序员的一个真正的通过仪式。

所有这些都很有趣(以它自己的扭曲方式),但它没有帮我们说出我们想要说的,也就是应该用一个文件的内容来初始化一个list<int>对象。现在我们知道了我们必须战胜的解析,那就很容易表示了。用括号包围一个实参的声明是不合法的,但用括号包围一个函数调用的观点是合法的,所以通过增加一对括号,我们强迫编译器以我们的方式看事情:

list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>());    

不幸的是,目前并非所有编译器都知道它。一个更好的解决办法是在数据声明中从时髦地使用匿名istream_iterator对象后退一步,仅仅给那些迭代器名字。以下代码到哪里都能工作:
ifstream dataFile("ints.dat");
istream_iterator
<int> dataBegin(dataFile);
istream_iterator
<int> dataEnd;
list
<int> data(dataBegin, dataEnd);


今天就到这里。更多的内容,还是直接看Effective STL比较好...^_^

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值