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

作者:咆哮的马甲 
出处: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的设计要节省一些。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值