代码改变世界

《Effective STL 读书笔记》 第三章 关联容器

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


第十九条: 理解相等(equality)和等价(equivalence)的区别  

  • 相等的概念是基于operator==的,也就是取决于operator==的实现
  • 等价关系是基于元素在容器中的排列顺序的,如果两个元素谁也不能排列在另一个的前面,那么这两个元素是等价的。
标准关联容器需要保证内部元素的有序排列,所以标准容器的实现是基于等价的。标准关联容器的使用者要为所使用的容器指定一个比较函数(默认为less),用来决定元素的排列顺序。

非成员的函数(通常为STL算法)大部分是基于相等的。下列代码可能会返回不同的结果
 1 struct CIStringCompare:
2 public binary_function<string, string, bool> {
3 bool operator()(const string& lhs,
4 const string& rhs) const
5 {
6 int i = stricmp(lhs.c_str(),rhs.c_str());
7 if(i < 0)
8 return true;
9 else
10 return false;
11 }
12 };
14
15
16 set<string,CIStringCompare> s; //set的第二个参数是类型而不是函数
17 s.insert("A");
18
19 if(s.find("a") != s.end()) //true
20 {
21 cout<<"a";
22 }
23
24 if(find(s.begin(),s.end(),"a") != s.end()) //false
25 {
26 cout<<"a";
27 }



第二十条: 为包含指针的关联容器指定比较类型  

下面的程序通常不会得到用户期望的结果。
1 set<string*> s;
2 s.insert(new string("A"));
3 s.insert(new string("C"));
4 s.insert(new string("B"));
5
6 for(set<string*>::iterator i = s.begin(); i != s.end(); i++)
7 {
8 cout<<**i; //输出一定会是ABC么?
9 }

因为set中存储的是指针类型,而它也仅仅会对指针所处的位置大小进行排序,与指针所指向的内容无关。

当关联容器中存储指针或迭代器类型的时候,往往需要用户自定义一个比较函数来替换默认的比较函数。

 1 struct CustomedStringCompare:
2 public binary_function<string*, string*, bool> {
3 bool operator()(const string* lhs,
4 const string* rhs) const
5 {
6 return *lhs < *rhs;
7 }
8 };
9
10
11 set<string*,CustomedStringCompare> s;
12 s.insert(new string("A"));
13 s.insert(new string("C"));
14 s.insert(new string("B"));
15
16 for(set<string*, CustomedStringCompare>::iterator i = s.begin(); i != s.end(); i++)
17 {
18 cout<<**i; //ABC
19 }

  
可以更进一步的实现一个通用的解引用比较类型

1 struct DerefenceLess{
2 template<typename PtrType>
3 bool operator()(PtrType ptr1, PtrType ptr2) const
4 {
5 return *ptr1 < *ptr2;
6 }
7 };
8
9 set<string*,DerefenceLess> s;

  


 

如果用less_equal来实现关联容器中的比较函数,那么对于连续插入两个相等的元素则有
1 set<int,less_equal<int>> s;
2 s.insert(1);
3 s.insert(1);

因为关联容器是依据等价来实现的,所以判断两个1是否等价!

!(1<=1) && !(1<=1) // false 不等价

所以这两个1都被存储在set中,从而破坏了set中不能有重复数据的约定. 


比较函数的返回值表明元素按照该函数定义的顺序排列,一个值是否在另一个之前。相等的值不会有前后顺序,所以,对于相等的值,比较函数应该返回false。


对于multiset又如何呢?multiset应该可以存储两个相等的元素吧? 答案也是否定的。对于下面的操作:
1 multiset<int,less_equal> s;
2 s.insert(1);
3 s.insert(1);
4
5 pair<multiset<int,less_equal>::iterator,multiset<int,less_equal>::iterator> ret = s.equal_range(1);

  

返回的结果并不是所期望的两个1。因为equal_range的实现(lower_bound:第一个不小于参数值的元素(基于比较函数的小于), upper_bound:第一个大于参数值的元素)是基于等价的,而这两个1基于less_equal是不等价的,所以返回值中比不存在1。

事实上,上面的代码在执行时会产生错误。VC9编译器Debug环境会在第3行出错,Release环境会在之后用到ret的地方发生难以预测的错误。

  

第二十二条: 切勿直接修改set或multiset的键  
 

set、multiset、map、multimap都会按照一定的顺序存储其中的元素,但如果修改了其中用于排序的键值,则将会破坏容器的有序性。

对于map和multimap而言,其存储元素的类型为pair<const key, value>,修改map中的key值将不能通过编译(除非使用const_cast)。
对于set和multiset,其存储的键值并不是const的,在修改其中元素的时候,要小心不要修改到键值。
 1 class Employee
2 {
3 public:
4 int id;
5 string title;
6 };
7
8 struct compare:
9 public binary_function<Employee&, Employee&, bool> {
10 bool operator()(const Employee& lhs,
11 const Employee& rhs) const
12 {
13 return lhs.id < rhs.id;
14 }
15 };
16
17
18 set<Employee,compare> s;
19
20 Employee e1,e2;
21
22 e1.id = 2;
23 e1.title = "QA";
24
25 e2.id = 1;
26 e2.title = "Developer";
27
28 s.insert(e1);
29 s.insert(e2);
30
31 set<Employee,compare>::iterator i = s.begin();
32 i->title = "Manager"; //OK to update non-key value
33 i->id = 3; // 破坏了有序性

  
 有些STL的实现将set<T>::iterator的operator*返回一个const T&,用来保护容器中的值不被修改,在这种情况下,如果希望修改非键值,必须通过const_case。

1 set<Employee,compare>::iterator i = s.begin();
2 const_cast<Employee&>(*i).title = "Manager"; //OK
3 const_cast<Employee*>(&*i).title = "Arch"; //OK
4 const_cast<Employee>(*i).title = "Director"; // Bad 仅仅就修改了临时变量的值 set中的值没有发生改变

  
对于map和multimap而言,尽量不要修改键值,即使是通过const_cast的方式,因为STL的实现可能将键值放在只读的内存区域当中。

相对安全(而低效)的方式来修改关联容器中的元素

  1. 找到希望修改的元素。
  2. 将要被修改的元素做一份拷贝。(注意拷贝的Map的key值不要声明为const)
  3. 修改拷贝的值。
  4. 从容器中删除元素。(erase 见第九条)
  5. 插入拷贝的那个元素。如果位置不变或邻近,可以使用hint方式的insert从而将插入的效率从对数时间提高到常数时间。
 1 set<Employee,compare> s;
2
3 Employee e1,e2;
4
5 e1.id = 2;
6 e1.title = "QA";
7
8 e2.id = 1;
9 e2.title = "Developer";
10
11 s.insert(e1);
12 s.insert(e2);
13
14 set<Employee,compare>::iterator i = s.begin();
15 Employee e(*i);
16 e.title = "Manager";
17
18 s.erase(i++);
19 s.insert(i,e);

 

第二十三条: 考虑使用排序的vector替代关联容器  
 

哈希容器大部分情况下可以提供常数时间的查找效率,标准容器也可以达到对数时间的查找效率。

标准容器通常基于平衡二叉树实现, 这种实现对于插入、删除和查找的混合操作提供了优化。但是对于3步式的操作(首先进行插入操作,再进行查找操作,再修改元素或删除元素),排序的vector能够提供更好的性能。
因为相对于vector,关联容器需要更大的存储空间。在排序的vector中存储数据比在关联容器中存储数据消耗更少的内存,考虑到页面错误的因素,通过二分搜索进行查找,排序的vector效率更高一些。

如果使用排序的vector替换map,需要实现一个自定义的排序类型,该排序类型依照键值进行排序。
 

第二十四条: 当效率至关重要时,请在map:operator[]和map:insert之间谨慎作出选择 
 

从效率方面的考虑,当向map中添加元素时,应该使用insert,当需要修改一个元素的值的时候,需要使用operator[]

如果使用operator[]添加元素

1 class Widget{
2 };
3
4
5 map<int,Widget> m;
6 Widget w;
7
8 m[0] = w;
9 //Widget构造函数被调用两次 

对于第8行,如果m[0]没有对应的值,则会通过默认的构造函数生成一个widget对象,然后再用operator=将w的值赋给这个widget对象。 使用insert可以避免创建这个中间对象。

1 map<int,Widget> m;
2 Widget w;
3
4 m.insert(map<int,Widget>::value_type(0,w)); //没有调用构造函数

  
如果使用insert修改元素的值(当然,不会有人这样做)

 1 map<int,Widget> m;
2 Widget w(1);
3 m.insert(map<int,Widget>::value_type(0,w));
4
5 Widget w2(2);
6
7 m.insert(map<int,Widget>::value_type(0,w2)).first->second = w2; //构造了一个pair对象
8
9 // 上面这段代码比较晦涩
10 // map::insert(const value_type& x)的返回值为pair<iterator,bool>
11 // 当insert的值已经存在时,iterator指向这个已经存在的值,bool值为false。
12 // 反之,指向新插入的值,bool值为true。

使用operator[]则轻便且高效的多

1 map<int,Widget> m;
2 Widget w(1);
3 m.insert(map<int,Widget>::value_type(0,w));
4
5 Widget w2(2);
6
7 m[0] = w2;

 
一个通用的添加和修改map中元素的方法

 1 template<typename MapType,
2 typename KeyType,
3 typename ValueType>
4 typename MapType::iterator InsertOrUpdate(MapType& map,const KeyType& k, const ValueType& v) // 注意typename的用法 从属类型前一定要使用typename
5 {
6 typename MapType::iterator i = map.lower_bound(k); // 如果i!=map.end(),则i->first不小于k
7
8 if(i!=map.end() && !map.key_comp()(k,i->first)) // k不小于i->first 等价!
9 {
10 i->second = v;
11 return i;
12 }
13
14 else
15 {
16 return map.insert(i,pair<const KeyType, ValueType>(k,v));
17 }
18 };
19
20
21 map<int,Widget> m;
22 Widget w(1);
23
24 map<int,Widget>::iterator i = InsertOrUpdate<map<int,Widget>,int,Widget>(m,0,w);

 

第二十五条: 熟悉非标准的哈希容器
 

如果你和我一样对于hash容器仅仅停留在知道的层次,这篇文章是我看到的国内对于hash_map讲解的最为认真的文章,建议参考一下。

常见的hash容器的实现有SGI和Dinkumware,SGI的hashset的声明类似于

1 template<typename T,
2 typename HashFunction = hash<T>,
3 typename CompareFunction = equal_to<T>,
4 typename Allocator = allocator<T>>
5 class hashSet;

  
Dinkumware的hash_set声明

1 template<typename T,
2 typename CompareFunction>
3 class hash_compare;
4
5 template<typename T,
6 typename HashingInfo = hash_compare<T,less<T>>,
7 typename Allocator = allocator<T>>
8 class hash_set;

  
SGI使用传统的开放式哈希策略,由指向元素的单向链表的指针数组(桶)构成。Dinkumware同样使用开放式哈希策略,由指向元素的双向链表的迭代器数组(桶)组成。从内存的角度上讲,SGI的设计要节省一些。