Effective STL 笔记-第3章 关联容器

3 关联容器

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

find 对”相同“的定义是相等,是以 operator== 为基础的。但 x 和 y 有相等的值并不一定意味着它们的所有数据成员都有相等的值。

set::insert 对“相同”的定义是等价,是以 operator< 为基础的。

考虑 set<Widget> s 和 两个对象 w1 和 w2,如果下面的表达式为真,则 w1 和 w2 对于 operator< 具有相等的值:

! (w1 < w2) && ! (w2 < w1);

如果两个值中的任何一个(按照一定的排序准则)都不在另一个的前面,那么这两个值(按照这一准则)就是等价的。

标准关联容器总是保持排列顺序的,所以每个容器必须有一个比较函数(默认为 less)来决定保持怎样的顺序。等价的定义正是通过该比较函数实现的。

使用单一的比较函数可以避免一大堆问题。

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

set<string*> ssp;

是如下代码的缩写。

set<string*, less<string*> > ssp;

如果你想让 string* 指针在集合中按字符串的值排序,那么不能使用默认的比较函数子类(function class)less<string*>。你必须自己编写函数子类(不能是函数,set 需要的是一个类型,并在内部用它创建一个函数 )。

struct StringPtrLess:
	public binary_function<const string*,
							const string*,
							bool>{
	bool operator() (const string *ps1, const string *ps2) const
    {
        return *ps1 < *ps2
    }
};                             

第 21 条:总是让比较函数在等值情况下返回 false。

相等的值从来就不会有前后顺序关系,所以,对于相等的值,比较函数应当始终返回 false。

equal_range:指定一个包含等价值的区间。

任何一个定义了”严格的弱序化“的函数必须对相同值得两个拷贝返回 false。

第 22 条:切勿直接修改 set 或 multiset 中的键。

对于一个 map<K, V> 或 map<K, V> 类型的对象,其中的元素类型是 pair<const K, V>,因为键的类型是 const K,所以它不能修改。

set / multiset 中的值不是 const,所以对这些值进行修改的代码可以通过编译。

类型转换:

if (i != se.end())
{
    const_cast<Employee>(*i).setTitle("Corporate Deitiy");
}

注意,不能使用 static_cast ,因为类型转换的结果是一个临时的匿名对象,它是 *i 的一个拷贝,setTitle 被作用在这个临时对象上,而不是 *i 上!

执行一次强制类型转换就意味着临时关掉了类型系统的安全性。

小结:

  • 如果你不关心程序的可移植性,而你想改变 set 或 multiset 中元素的值,并且你的 STL 实现允许,则请继续做下去。只是注意不要改变元素中的键部分,即元素中能够影响容器有序性的部分。
  • 如果你重视可移植性,就要确保 set 和 multiset 中的元素不能被修改。至少不能未经过强制类型转换就修改。

安全、可移植的方法:

Employee selectedID;
...
EmpIDSet::iterator i =
    se.find(selectedID);						// 第1步:找到待修改的元素
if (i != se.end()) {
    Employee e (*i);							// 第2步:拷贝该元素
    e.setTitle("Corporate Deily");				// 第3步:修改拷贝
    se.erase(i++);								// 第4步:删除该元素
    											// 递增迭代器以保持它的有效性
    se.insert(i, e)								// 第5步:插入该元素,它的位置和原来相同。	
};				

第 23 条:考虑用排序的 vector 替代关联容器。

标准的关联容器通常被实现为平衡二叉树。适合插入、删除、查找的混合操作,提供对数时间的查找能力。但比较浪费内存空间(父指针,左儿子指针,右儿子指针)。如果节点散布在全部地址空间,将会导致更多的页缺失。

散列容器:提供常数时间的查找能力。

使用数据结构的一般过程:

  1. 设置阶段:创建一个新的数据结构,并插入大量元素。在这个阶段,几乎所有的操作都是插入和删除操作,很少或几乎没有查找操作。
  2. 查找阶段:查询该数据结构以找到特定的信息。在这个阶段,几乎所有的操作都是查找操作,很少或几乎没有插入和删除操作。
  3. 重组阶段:改变该数据结构的内容,或许是删除所有的当前数据,再插入新的数据。在行为上,这个阶段与第1阶段类似。但这个阶段结束以后,应用程序又回到了第2阶段。

使用 vector 替代标准关联容器:

  • 排序的 vector 中存储数据可能比在标准关联容器中存储同样的数据要耗费更少的内存。
  • 考虑到页面错误的因素,通过二分搜索法来查找一个排序的 vector 可能比查找一个标准关联容器要更快一点。
  • 存储在 vector 中的数据必须是 pair<K, V> ,因为排序时它的元素的值将通过赋值操作被移动。
  • 对 vector 做排序时,必须为 pair 写一个自定义的比较类型。(P85)

第 24 条:当效率至关重要时,请在 map::operator[] 与 map::insert 之间谨慎做出选择。

map 的 operator[] 函数与众不同,它与 vector、deque 和 string 的 operator[] 函数无关,与用于数组的内置 operator[] 也没有关系。它的设计目的时为了提供添加和更新(add or update)的功能。

m[k] = v 检查键 k 是否已经在 map 中了。如果没有,它就被加入,并以 v 作为相应的值。如果 k 已经在映射表中了,则与之关联的值被更新为 v。

k 不在映射表时,operator[] 使用值类型的默认构造函数创建一个新的对象,然后返回指向这个新对象的引用。这样做效率会很低,可以换为 insert 操作,节省了 3 个函数调用(创建默认构造的对象,析构该临时对象,调用赋值操作符)。

总结:当向映射表中添加元素时,要优先选用 insert 而不是 operator[];当更新已经在映射表中的元素的值时,要优先选择 operator[]。

P90:efficientAddOrUpdate

第 25 条:熟悉非标准的散列容器。

非标准的散列容器有 hash_map,hash_set,hash_multimap,hash_multiset

SGI 的散列容器:

template<typename T,
		typename HashFunction = hash<T>,
		typename CompareFunction = equal_to<T>,
		typename Allocator = allocator<T> >
class hash_set;            

注意,与标准关联容器不同, SGI 的散列容器使用 equal_to 作为默认的比较函数,通过测试两个对象是否相等而不是等价来决定容器中的两个对象是否相等。

SGI 的实现把表的元素放在一个单向链表中,而 Dinkumware 的实现则使用了双向链表。

posted @ 2021-08-19 15:41  CoolGin  阅读(42)  评论(0编辑  收藏  举报