代码改变世界

《Effective STL 读书笔记》 第五章 算法

2011-09-12 12:53  咆哮的马甲  阅读(501)  评论(2编辑  收藏  举报
作者:咆哮的马甲
出处:http://www.cnblogs.com/arthurliu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。 
转载请保持文档的完整性,严禁用于任何商业用途,否则保留追究法律责任的权利。

第三十条: 确保目标区间足够大
 

下面例子中,希望将一个容器中的内容添加到另一个容器的尾部

1 int transformogrify(int x); //将x值做一些处理,返回一个新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 ... //初始化values
8
9 transform(values.begin(),values.end(),results.end(),transformogrify);

由于results.end()返回的迭代器指向一段未初始化的内存,上面的代码在运行时会导致无效对象的赋值操作。

可以通过back_inserter或者front_inserter来实现在头尾插入另一个容器中的元素。因为front_inserter的实现是基于push_front操作(vector和string不支持push_front),所以通过front_inserter插入的元素与他们在原来容器中的顺序正好相反,这个时候可以使用reverse_iterator。

 1 int transformogrify(int x); //将x值做一些处理,返回一个新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 ... //初始化values
8
9 transform(values.begin(),values.end(),back_inserter(results),transformogrify);
10
11 int transformogrify(int x); //将x值做一些处理,返回一个新的值
12
13 deque<int> values;
14
15 deque<int> results;
16
17 ... //初始化values
18
19 transform(values.rbegin(),values.rend(),front_inserter(results),transformogrify);

  

另外可以使用inserter在results的任意位置插入元素

1 int transformogrify(int x); //将x值做一些处理,返回一个新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 ... //初始化values
8
9 transform(values.begin(),values.end(),inserter(results,results.begin()+results.size()/2),transformogrify); //插入中间 

书中提到“但是,如果该算法执行的是插入操作,则第五条中建议的方案(使用区间成员函数)并不适用”,不知是翻译的问题还是理解不到位,为什么插入操作不能用区间成员函数替换? 在我看来是因为区间成员函数并不支持自定义的函数对象,而这又跟插入操作有什么关系呢?莫非删除可以???

如果插入操作的目标容器是vector或string,可以通过reserve操作来避免不必要的容器内存重新分配。

 1 int transformogrify(int x); //将x值做一些处理,返回一个新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 //... //初始化values
8
9 results.reserve(values.size()+results.size()); //预留results和values的空间
10
11 transform(values.begin(),values.end(),back_inserter(results),transformogrify);

  

如果操作的结果不是插入而是替换目标容器中的元素,可以采用下面的两种方式

 1 int transformogrify(int x); //将x值做一些处理,返回一个新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 //... //初始化values
8
9 results.resize(values.size()); //想想对于results.size() > values.size() 和results.size() < values.size()两种情况
10
11 transform(values.begin(),values.end(),results.begin(),transformogrify);
12
13 int transformogrify(int x); //将x值做一些处理,返回一个新的值
14
15 vector<int> values;
16
17 vector<int> results;
18
19 //... //初始化values
20
21 results.clear(); //results.size()为,results.capacity()不变
22
23 results.reserve(values.size()); //相对于上一种方式,如果values.size()小于原来的results.size(),那么会空余出一些元素的内存。
24
25 transform(values.begin(),values.end(),results.begin(),transformogrify);



第三十一条:
了解各种与排序有关的选择
 

对vector、string、deque或数组中的元素执行一次完全排序,可以使用sort或stable_sort

 1 vector<int> values;
2
3 values.push_back(4);
4
5 values.push_back(1);
6
7 values.push_back(2);
8
9 values.push_back(5);
10
11 values.push_back(3);
12
13 sort(values.begin(),values.end()); // 1,2,3,4,5

  

对vector、string、deque或数组中的元素选出前n个进行并对这n个元素进行排序,可以使用partial_sort

1 partial_sort(values.begin(),values.begin()+2,values.end()); // 1,2,4,5,3 注意第二个参数是一个开区间

  

对vector、string、deque或数组中的元素,要求找到按顺序排在第n个位置上的元素,或者找到排名前n的数据,但并不需要对这n个数据进行排序,这时可以使用nth_element

1 nth_element(values.begin(),values.begin()+1,values.end()); // 1,2,3,4,5 注意第二个参数是一个闭区间

这个返回的结果跟我期望的有些差距,期望的返回值应该是1,2,4,5,3。VC10编译器


对于标准序列容器(这回包含了list),如果要将其中元素按照是否满足某种特定的条件区分开来,可以使用partition或stable_partition

1 vector<int>::iterator firstIteratorNotLessThan3 = partition(values.begin(),values.end(),lessThan3); //返回值为 2,1,4,5,3
2
3 vector<int>::iterator firstIteratorNotLessThan3 = stable_partition(values.begin(),values.end(),lessThan3); //返回值为 1,2,4,5,3

  

对于list而言,它的成员函数sort保证了可以stable的对list中元素进行排序。对于nth_element和partition操作,有三种替代方案:

  • 将list中的元素拷贝到提供随机访问迭代器的容器中,然后执行相应的算法
  • 创建一个list::iterator的容器,在对容器执行相应的算法
  • 利用一个包含迭代器的有序容器的信息,反复调用splice成员函数,将list中的成员调整到相应的位置。



第三十二条: 如果确实要删除元素,请确保在remove这一类算法以后调用erase
 

remove算法接受两个迭代器作为参数,这两个迭代器指定了需要进行操作的区间。Remove并不知道它所操作的容器,所以并不能真正的将容器中的元素删除掉。

 1 vector<int> values;
2
3 for(int i=0; i<10; i++)
4
5 {
6
7 values.push_back(i);
8
9 }
10
11 values[3] = values[5] = values[9] = 99;
12
13 remove(values.begin(),values.end(),99); // 0,1,2,4,6,7,8,7,8,99

  

从上面的代码可见,remove并没有删除所有值为99的元素,只不过是用后面元素的值覆盖了需要被remove的元素的值,并一一填补空下来的元素的空间,对于最后三个元素,并没有其他的元素去覆盖他们的值,所以仍然保留原值。

clip_image002


上图可以看出,remove只不过是用后面的值填补了空缺的值,但并没有将容器中的元素删除,所以在remove之后,要调用erase将不需要的元素删除掉。

1 values.erase(remove(values.begin(),values.end(),99),values.end()); // 0,1,2,4,6,7,8

  

类似于remove的算法还有remove_if和unique, 这些算法都没有真正的删除元素,习惯用法是将它们作为容器erase成员函数的第一个参数。

List是容器中的一个例外,它有remove和unique成员函数,而且可以从容器中直接删除不需要的元素。



第三十三条:
对于包含指针的容器使用remove这一类算法时要特别小心
 

 1 class Widget{
2 public:
3 ...
4 bool isCertified() const;
5 ...
6
7 };
8
9 vector<Widget*> v;
10
11 for(int i=0; i<10; i++)
12 {
13 v.push_back(new Widget());
14 }
15
16 v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());

  

上面的代码可能会造成内存泄漏

clip_image004


避免内存泄漏的方式有两种,第一种是先将需要被删除的元素的指针删除并设置为空,然后再删除容器中的空指针。第二种方式更为简单而且直观,就是使用智能指针。

方案1

 1 void delAndNullifyUncertified(Widget*& pWidget)
2 {
3 if(!pWidget->isCertified())
4 {
5 delete pWidget;
6 pWidget = 0;
7 }
8 }
9
10 vector<Widget*> v;
11
12 for(int i=0; i<10; i++)
13 {
14 v.push_back(new Widget());
15 }
16
17 for_each(v.begin(),v.end(),delAndNullifyUncertified);
18
19 v.erase(remove(v.begin(),v.end(),static_cast<Widget*>(0)),v.end());

  

方案2

 1 template<typename T>
2 class RCSP{...}; // Reference counting smart pointer
3
4 typedef RSCP<Widget> RSCPW;
5
6 vector<RSCPW> v;
7
8 for(int i=0; i<10; i++)
9 {
10 v.push_back(RSCPW(new Widget()));
11 }
12
13 v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());



第三十四条:
了解哪些算法要求使用排序的区间作为参数
 

  • 用于查找的算法binary_search, lower_bound, upper_bound和equal_range采用二分法查找数据,所以数据必须是事先排好序的。对于随机访问迭代器,这些算法可以保证对数时间的查找效率,对于双向迭代器,需要线性时间
  • set_union, set_intersection, set_difference和set_symmetric_difference提供了线性时间的集合操作。排序的元素是线性效率的前提。
  • merge和inplace_merge实现了合并和排序的联合操作。读入两个排序的区间,合并成一个新的排序区间。具有线性时间的性能。
  • includes,判断一个区间中的元素是否都在另一个区间之中。具有线性的时间性能。
  • unique和unique_copy不一定需要排序的区间,但一般来说只有针对排序的区间才能删除所有的重复数据,否则只是保留相邻的重复数据中的第一个。

针对一个区间的进行多次算法的操作,要保证这些算法的排序方式是一致的。(比如都是升序或都是降序)



第三十五条:
通过mismatchlexicographical_compare实现简单的忽略大小写的字符串比较
 

Mismatch的作用在于找出两个区间中第一个对应值不同的位置。 要实现忽略大小写的字符串比较,可以先找到两个字符串中第一个不同的字符,然后通过比较这两个字符的大小。

 1 int ciStringCompareImpl(const string& s1, const string& s2)
2 {
3 typedef pair<string::const_iterator, string::const_iterator> PSCI; //pair of string::const_iterator
4
5 PSCI p = mismatch(s1.begin(),s1.end(),s2.begin(),not2(ptr_fun(ciCharCompare)));
6
7 if(p.first == s1.end())
8 {
9 if(p.second == s2.end()) return 0;
10 else return -1;
11 }
12
13 return ciCharCompare(*p.first,*p.second);
14 }

  

Lexicograghical_compare是strcmp的一个泛化的版本,strcmp只能与字符数组一起工作,而lexicograghical_compare可以与任何类型的值区间一起工作。

1 bool charLess(char c1, char c2);
2
3 bool ciStringCompair(const string& s1, const string& s2)
4 {
5 return lexicographical_compare(s1.begin(),s1.end(),s2.begin(),s2.end(),charLess);
6 }



第三十六条:
理解copy_if算法的正确实现
 

标准的STL中并不存在copy_if算法,正确的copy_if算法的实现如下所示:

 1 template<typename InputIterator,
2 typename OutputIterator,
3 typename Predicate>
4 OutputIterator copy_if(InputIterator begin,
5 InputIterator end,
6 OutputIterator destBegin,
7 Predicate p)
8 {
9 while(begin != end)
10 {
11 if(p(*begin))
12 {
13 *destBegin++ = *begin;
14 ++begin;
15 }
16
17 return destBegin;
18 }
19 }



第三十七条:
使用accumulate或者for_each进行区间统计
 

accumulate有两种形式

第一种接受两个迭代器和一个初始值,返回结果是初始值与两个迭代器区间的元素的总和。

1 vector<int> v;
2 ...
3 accumulate(v.begin(),v.end(),0);

  

第二种方式加了一个统计函数,使得accumulate函数变得更加通用。

1 vector<string> v;
2 ...
3 accumulate(v.begin(),v.end(),static_cast<string::size_type>(0), StringLegthSum);

  

accumulate的一个限制是不能产生任何的副作用,这时,for_each就是一个很好的补充。For_each接受三个参数,两个迭代器确定的一个区间,以及统计函数。For_each的返回值是一个函数对象,必须通过调用函数对象中的方法才能够取得统计的值。

 1 struct Point
2 {
3 Point(double _x, double _y):x(_x),y(_y)
4 {
5 }
6
7 double x,y;
8 }
9
10 class PointAverge : public unary_function<Point,void>
11 {
12 public:
13 PointAverage(): sum_x(0.0), sum_y(0.0),sum(0)
14 {
15 }
16
17 void operator()(const Point& p) //可以产生副作用
18 {
19 sum++;
20 sum_x += p.x;
21 sum_y += p.y;
22 }
23
24 Point GetResult() //用于返回统计结果
25 {
26 return Point(sum_x/sum, sum_y/sum);
27 }
28
29 private:
30
31 double sum_x, sum_y;
32 nt sum;
33 }
34
35 vector<Point> v;
36 ...
37 Point result = for_each(v.begin(),v.end(),PointAverage()).GetResult();