Effective STL-3 关联容器

part3 关联容器

与序列容器区别

(1) 自动排序

(2) 比较是按照等价(equivalence)而不是相等(equality)

(3) set/map 不允许有重复

key points

(1) 等价的概念

(2) 比较函数的限制

(3) 指针关联容器, 要自定义比较函数

(4) 键的常量性(constness)官方含义和实际含义

(5) 关联容器提高效率的建议

item19: 理解相等(equality)和等价(equivalence)的区别

1 STL中经常需要比较2个对象, 判断它们的值是否相同

2个代表性的函数算法find()和成员函数set::insert()对"相同"的定义不同: 相等和等价, 分别基于 operator== 和 operator<

关联容器所有成员函数(set::insert 和set::find()等)对相同的定义相同

相等并不一定意味所有数据成员都相等, 通常只关注某个字段

等价以"在已排序区间中对象值的相对排列顺序"为基础

2 为什么标准关联容器是基于等价而不是相等

例: 不区分大小写的 set<string, CIStringCompare[, equal_to<string> ] >, 决定排列顺序/是否等价的比较函数 忽略 字符串中字符大小写

    item35: 实现了这样的函数 ciStringCompare, 不区分大小写的比较
    
    但 set 需要一个`比较函数类型/functor`, 其 operator() 调用 ciStringCompare
    struct CIStringCompare
        : public binary_function <string, string, bool> // item40: 提供可配接能力
    {
        bool
        operator()(const string& lhs, const string& rhs)
        {
            return ciStringCompare(lhs, rhs); // item35, 忽略大小写的字符串比较函数
        }
    };

标准关联容器自动排序, 所以必须用1个比较函数(默认为std::less)决定 如何排序/排列顺序

(1) 若基于相等 判2个元素是否"相同", 则关联容器需要2个比较函数: 一个用于 排序(判/决定 2个值 的排列顺序/是否等价), 另一个用于 判相同(判/决定 2个值是否 相同/允许插入 )

-> 对non-multi容器(set/map)带来非常直观的混乱或错误

1)混乱: 成员函数实现可能基于相等, 这与正确的容器行为违背

2)错误: 破坏non-multi容器(set/map)keyPart值的唯一性(非重性); 会破坏multi容器(multiset/multimap)的排序规则

[1]
    set<string, CIStringCompare, equal_to<string> > set2CF;
                    |                   |
                    |                   |
                决定 排列顺序       决定 (key)值是否相同 => 决定是否允许插入 
                
    set2CF.insert("Abc");
    set2CF.insert("abc");
    
    CIStringCompare
        "Abc" 与 "abc" 等价(无排列顺序) => 后者不应该被插入, 以保持容器的排序性
        
    equal_to<string>
        "Abc" 与 "abc" 不相等 => 不相同 => 后者可以插入

    1) 假设实现保持了排序性, 后者没插入

        成员函数 理应基于排序性/等价
            => if(set2CF.find("abc") != set2CF.end() ) // find 成功 
            
        但`成员函数find可能基于 相等`
            => if(set2CF.find("abc") != set2CF.end() ) // find 失败 
            
        => 混乱
        
    2) 假设实现允许后者插入 => `破坏`non-multi容器(set/map)key值的唯一性(非重性)` 
    
[2] 上述换为 multiset 

    equal_to<string>
        "Abc" 与 "abc" 不相等 => 不相同 => 后insert的"abc"插入
    
    CIStringCompare
        "Abc" 与 "abc" 等价 => 排列顺序(谁在前)不限定/不确定 
            => `不能以确定的顺序遍历容器的元素`
            假定 "abc" 在 "Abc" 前 
        
    `成员函数find可能基于 相等`
        => if(multiSet2CF.find("abc") != multiSet2CF.end() ) // find 成功

(2) 若基于等价 判2个元素是否"相同", 则关联容器只需1个比较函数: 既用于 排序(判2个值 的排列顺序/是否等价), 又用于 判相同(判/决定 2个值是否 相同/允许插入 )

    set<string, CIStringCompare> cisSet;
    
    cisSet.insert("Abc"); 
    cisSet.insert("abc"); // "abc" 与 "Abc" 等价 => 相同 => "abc" 不被插入
    
    if(cisSet.find("abc") != cisSet.end() ) // set::find 基于等价 => find 成功
    
    if( find(cisSet.begin(), cisSet.end(), "abc" ) 
        != cisSet.end() ) // 算法 find() 基于相等 => find 失败 

=> 优先选择成员函数(如 std::find) 而不是相应的非成员函数/算法(如 find)

3 等价的含义: 两个值(按指定的比较函数/排序准则)任一个都不在另一个的前面, 则这两个值(按该比较函数/排序准则)等价

默认 operator<

    !(w1 < w2) && !(w2 < w1) // w1 < w2 不为真, 且 w2 < w1 不为真

client 如何知道2个对象是否等价? 答: 用关联容器的成员函数key_comp

默认 less == key_comp

    // 在容器c的排列顺序中, w1在w2之前不为真, w2在w1之前也不为真 
    !c.key_comp()(w1, w2) && !c.key_comp()(w2, w1) // key_comp()返回1个函数对象实例

w1不在w2之前 => c.key_comp()(w1, w2)为false => !c.key_comp()(w1, w2)为true

4 关联容器的比较函数

[1] 目的是判2个元素的key值(对set/map, 也是value值/只是key值)是否相同

[2] 决定排列顺序/排序准则

5 基于等价的默认比较函数

    less<T>
        用 operator <
        
    template <class T> 
    struct less : binary_function <T,T,bool> 
    {
        bool operator() (const T& x, const T& y) const 
        { return x < y; }
    };

6 非排序关联容器 非标准的hashtable, 2种设计: 基于相等/基于等价

item20: 为指针/智能指针/迭代器 (行为像指针) 关联容器指定/自定义比较类型

总结

(1) 指针关联容器, 想按指针所指对象的值 排序, 必须自定义比较函数functor: 以 string* 为参数, 按指针所指的string值排序

(2) 智能指针/迭代器 关联容器, 本节的方法同样适用

1*iter 是指向string的指针 => set 按指针的值 排序

    set<string*> pStrSet;

    // <=>
    set<string*, less<string*> > pStrSet;
    
    // <=>
    set<string*, less<string*>, allocator<string*> > pStrSet; // 这里不考虑分配器 
    set<string*> pStrSet;
    pStrSet.insert("abc");
    pStrSet.insert("bcd");
    
    for(set<string*>::const_iterator iter = pStrSet.begin();
        iter != pStrSet.end();
        ++iter)
        cout << *iter << endl;

2 自定义 string指针关联容器 比较函数functor

    struct StringPtrDereferenceLess
        : public binary_function<const string*, const string*, bool> // item40
    {
        bool
        operator()(const string* lhs, const string* rhs) const 
        {
            return *lhs < *rhs;
        }
    };
    typedef set<string*, StringPtrDereferenceLess> StringPtrSet;
    StringPtrSet stringPtrSet;
    // ...

(1) 手工循环

    for(StringPtrSet::const_iterator iter = stringPtrSet.begin();
        iter != stringPtrSet.end();
        ++iter)
        cout << **iter << endl; // **iter 才是 string* 所指的 string 对象

(2) 明显隐含循环的算法

    void print(const string* ps)
    {
        cout << *ps << endl;
    }
    for_each(stringPtrSet.begin(), stringPtrSet.end(), print);

(3) 算法的操作参数泛化解除(指针)引用的functor, 并改用明显隐含循环的 算法 transform 配合 输出流迭代器 ostream_iterator 联用

    struct Dereference
    {
        template<typename T>
        const T& 
        operator()(const T* ptr) const 
        {
            return *ptr;
        }
    };
    
    // 指针容器中元素(指针)经 transform 第4(操作)参数转换(解引用)一下变为指针所指(string)对象
    // => ostream_iterator 模板形参为所要打印的对象类型 string
    transform(stringPtrSet.begin(), stringPtrSet.end(),
              ostream_iterator<string>(std::cout, "\n"), // 输出区间迭代器
              Dereference() );

Note: copy 不能直接用, 否则, 打印出的是容器中元素值, 即指针值(string*) -> 用 transform 的第4(操作)参数转换(解引用)一下, 变为指针所指(string)对象

    // ostream_iterator 模板形参为所要打印的对象类型 string*
    copy(stringPtrSet.begin(), stringPtrSet.end(),
         ostream_iterator<string*>(cout, "\n") ); 

Note: 输出流迭代器 ostream_iterator 需要知道所要打印的对象类型

    template <class T,          // T: 所要输出的对象类型
              class charT=char, 
              class traits=char_traits<charT> > 
    class ostream_iterator;
    
    // Ctor: 第2参数为分隔符
    ostream_iterator (ostream_type& s 
                      [,const char_type* delimiter]); 

3 2的泛化: 自定义 指针关联容器 比较函数functor

    // 不考虑配接性 
    
    struct PtrDereferenceLess
    {
        template<typename T>
        bool
        operator()(const T* lhs, const T* rhs) const 
        {
            return *lhs < *rhs;
        }
    };
    
    // <=> 不必用 const PtrType lhs, const PtrType rhs
    struct PtrDereferenceLess
    {
        template<typename PtrType>
        bool
        operator()(PtrType lhs, PtrType rhs) const 
        {
            return *lhs < *rhs;
        }
    };
    typedef set<string*, PtrDereferenceLess> StringPtrSet;

item21: 总是让比较函数在 等keyPart值(也应该等价)情况下返回false

原因: 等价 未必(即 =/>) keyPart值相等; 等keyPart值 必然(即=>) 等价 (否则, 会破坏non-multi容器keyPart值的唯一性; 破坏multi容器的排序规则, 进而弄错成员函数(如 equal_range)的判断结果(本应基于等keyPart值的元素等价) ) => 此时, 比较函数应返回false, 表示两者 没有先后 排列顺序

1 例: less_equal(即 <= ) 作 set 的比较类型 -> 判断出 2个等值元素不等价

=> 不相同(允许第2个值插入 => 破坏set容器key值的唯一性) & 有排列顺序(不确定行为)

对2个值而言, 等价既决定是否有 排列顺序, 又决定是否 相同: 等价时 无排列顺序 且 相同

    set<int, less_equal<int> > s;
    s.insert(10);
    s.insert(10);
    // 两个等值(10)元素: 10_L / 10_R, 判是否等价 
    !(10_L <= 10_R) && !(10_R <= 10_L) 
        
    // 即 
    false && false == false => 不等价 <=> 不相同

2 如何快速确定比较函数是否有错: 若对等值(==)返回true, 则比较函数有错

错: !( 左 < 右 ) <=> >=

对: 右 < 左

    struct StringPtrDereferenceGreator
        : public binary_function<const string*, const string*, bool> 
    {
        bool
        operator()(const string* lhs, const string* rhs) const 
        {
            return !(*lhs < *rhs); // 旧的测试条件取反 -> 错 
        }
    };
    struct StringPtrDereferenceGreator
        : public binary_function<const string*, const string*, bool> 
    {
        bool
        operator()(const string* lhs, const string* rhs) const 
        {
            return *rhs < *lhs; 
        }
    };

3 对 multiset/multimap, 虽然可以包含重复(等价)值, 但也应该保证等key值的元素等价

否则, 会 破坏容器的排序规则,进而 弄错 成员函数(如 equal_range)的判断结果(本应基于 等keyPart值的元素等价)

例: less_equal(即 <= ) 作 multiset/multimap 的比较类型

equal_range 得1对迭代器, 定义的区间包含等价值, 不会如我们期望的那样 包含2个等keyPart值的元素(10_L/10_R 不等价)`

    multiset<int, std::less_equal<int> > s;
    s.insert(10);
    s.insert(10);
    
    // std::pair<std::multiset<int>::iterator, std::multiset<int>::iterator> 
    auto bounds = equal_range(s.begin(), s.end(), 10)
    
    std::cout << "bounds at pos " << (bounds.first - v.begin() );
    std::cout << " and " << (bounds.second - v.begin() ) << '\n';

4 对关联容器排序的比较函数必须为它们所比较的对象 定义一个 "严格的弱序化"(strict weak ordering), 必须在 等keyPart值下返回false

item22: 切勿直接修改 set或multiset中keyPart的值

1 对标准关联容器, keyPart的值是否可以被直接修改

keyPart: 对set/multiset是T类型元素的keyPart, 对map/multimap就是key

(1) 对 map/multimap而言, 第1模板形参类型为K时, 键(key)的类型是const K(常量类型) =>

编译器保证 keyPart不能被直接修改

    map/multimap<K, V>
        元素类型是 pair<const K, V>
        键(key)的类型是 常量类型(const K)

(2) 对 set/multiset而言, 第1模板形参类型(是 元素类型)为T时, 键(key)/元素的类型是T,keyPart的类型T中决定容器排列顺序(保证容器行为正确)的1个部分 =>

keyPart的值被直接修改(编译器允许)时, 会破坏容器(的排序性, 进而破坏容器的正确行为) => 必须避免;

但当key(键)/元素non-KeyPart值 被直接修改(编译器允许)时, 能保证容器正确性

    set/multiset<T>

同样的逻辑是否可以用于map/multimap中的keyPart?

答: 理论上可以, 但C++标准不允许

例: 修改set/multiset元素值而不修改keyPart值的例子

    class Employee
    {
    private:
        int id;             // keyPart
        std::string title; // 头衔, nonkeyPart => 被修改是合理的
    };
    
    struct IDLess
        : public binary_function<Employee, Employee, bool>
    {
        bool
        operator()(const Employee& lhs, const Employee& rhs )
        { 
            return lhs.getId() < rhs.getId(); 
        }
    };

    typedef set<Employee, IDLess> EmpIdSet;
    EmpIdSet empIdSet;

2 是否可以间接修改关联容器key的nonKeyPart的值: 可以

(1)对set/multiset, 正确且可移植的做法

const_cast<non-const T 引用>, 即 const_cast<T &> 强转掉 key值本身(而不能是copy)的常量性(constness)

set/multiset
    const_cast<T &>
    auto iter = empIdSet.find(targetElem);
    if(iter != empIdSet.end() )
        const_cast<T &>(*iter).setTitle("Leader");

(2)对map/multimap, 因为元素 pair<const K, V> 的第一部分是const, 上述做法将导致未定义结果

map/multimap
    const_cast<K &>

(3) 对4种关联容器均安全可移植的方法

[1] 找到 想修改的元素, 记录返回的迭代器作为后续insert的hint(提示), 以将插入的效率从对数时间提高到常数时间

    找到: 最优办法见 item45)

[2] 构造1份 non-const 拷贝
[3] 修改该拷贝的 nonKeyPart
[4] 删除元素, 通常用erase
[5] 用带hint的insert, 插入修改后copy

keyPart 不变 => 顺序相同 => 插入的位置与原被删除的位置`相同或紧邻

    相同: set/mutiset
    紧邻: map/multimap
    EmpIdSet empIdSet;
    Employee targetElem;
    // ...
    
    EmpIdSet::iterator iter = empIdSet.find(targetElem);
    if(iter != empIdSet.end() )
    {
        Employee eCopy(*iter);
        eCopy.setTitle("leader");
        se.erase(iter++);
        se.insert(iter, eCopy); // 插入的位置与原来相同
    }

3 尽管 set/multiset的元素不是const, 但STL实现有办法防止它们被修改; 对此, C++标准没有统一要求

方法: 迭代器解引用后返回const引用

set<T>::iterator 的 operator* 返回 const T&

4 试图直接修改set/multiset元素的代码不可移植

如果你重视可移植性, 就不要这么做

item23: 考虑用排序的vector替代关联容器

3类容器适用场景

查找速度
    hash容器  > 排序的vector > 标准关联容器

1 hash容器

适用场景: 无序

2 排序的vector

(1) 适用场景: 同一类操作([1] 插/删 [2] 查找 [3] 改值 <=> 删+插 ) 集中于同一阶段 => 3个主要阶段

    [1] 设置: 插/删

    [2] 查找

    [3] 重组 改值 <=> 删+插

(2) 时空性能 可能比关联容器好, 原因:

[1] 排序的vector可用二分查找算法 binary_search/lower_bound/equal_range 等(item34/item45), 保证了与关联容器相近的对数查找时间

[2] 为什么比关联容器时空性能好 ?

1] 每个元素的size小, 无需prev/next/parent指针

2] 局部性: 连续内存 => 内存页错误 少, 且内存集中 => 二分查找更快

(3) 排序的vector替换map/multimap时, 必须去掉 map/multimap元素pair<const K, V>的第1元素的const, 换成pair<K, V>

原因: 排序时, vector元素通过赋值被移动 => pair的2个部分都必须是可被赋值

(4) 需要2个比较函数: 本质都基于key/K类型, 但接口不同

[1] 排序比较函数: 2个参数都是 pair

[2] 查找比较函数: 2个参数1个是 pair, 1个是 K 类型

    typedef pair<string, int> strIntPair;
    
    class DataCmp
    {
    public:
        bool 
        operator()(const strIntPair& lhs, 
                   const strIntPair& ) const // 排序用
        {
            return keyLess(lhs.first, rhs.first);
        }
        
        bool 
        operator()(const strIntPair& lhs, 
                   const strIntPair::first_type& k) const // 查找用, 形式1
        {
            return keyLess(lhs.first, k);
        }
        
        bool 
        operator()(const strIntPair::first_type&, 
                   const strIntPair& rhs) const // 查找用, 形式2
        {
            return keyLess(k, rhs.first);
        }
    private:
        bool 
        keyLess(const strIntPair::first_type& kLhs, 
                const strIntPair::first_type& kRhs) const // 公共比较函数
        {
            return kLhs < kRhs;
        }
    };

3 标准关联容器

(1) 适用场景: 没法预测下一个操作是什么

(2) 性能好的原因: STL实现对插/删/查找混合操作做了优化

item24: 效率至关重要时, 请在map::operator[]与map::insert之间谨慎选择

1 map的下标函数 operator[]与众不同, 设计目的是为了提供 "添加(key不存在) 或 更新(key存在)"

    m[k] = v;
    
    // <=>
    m.operator[](k) = v;

检查 键k是否已经在map中, 若否, 则添加 元素pair(k, v); 若是, 则更新 k关联的value为v

具体实现: 2步
[1] operator[]返回引用, 指向k关联的value, k不在map/multimap中时, 构造value类型的默认对象返回
[2] v 被赋值引用所指的对象

1 添加时, insert比下标operator[]高效: 下标operator[] 多了临时对象的构造、析构和v的赋值

(1) 用 operator[] 添加 

    m[k] = v;
    
    <=>
    typedef map<K, V> KVMap;
    
    // step1
    pair<KVMap::iterator, bool> hintPair = 
        m.insert(KVMap::value_type(k, V() ) );
    
    // step2
    hintPair.first->second = v; // Note: hintPair.first 即 pair<K, V> 的 iter

KVMap::value_type 是 pair<K, V>

(2) 用 insert 添加

    m.insert(KVMap::value_type(k, v) );

2 更新, 形势好反: 下标操作不需要构造/析构 pair<K, V>及其中的 K 和 V

(1) 用 operator[] 更新

    m[k] = v; // 用 operator[] 更新
(2) 用 insert() 更新

    m.insert(KVMap::value_type(k, v) ).first->second = v; // 用 insert() 更新

    Note: m.insert(KVMap::value_type(k, v) ) 应该就够了, .first->second = v; 多余

3 STL最好提供(但没有)1个函数, 实现高效添加和更新

efficientAddOrUpdate(m, k, v)

思想: m.lower_bound(k) 返回的迭代器:

[1] 所指pair的key与k是/否等价 => k 是/否在m中 => 更新/insert

[2] 表明 可插入k的 第1个位置 => 等价区间的 前闭端

    template<typename MapType,
            typename K,
            typename V>
        typename MapType::iterator
    efficientAddOrUpdate(MapType& m, const K& k, const V& v)
    {
        // 可插入 k的 第1个位置 => 等价区间的 前闭端
        typename MapType::iterator lb = 
            m.lower_bound(k); // => !(m.key_comp()(lb->first, k) )
            
        if(lb != m,end() && 
            !(m.key_comp()(k, lb->first) ) ) // 与上一条语句结果接结合 
        {                                    // => lb 指向的pair的key 与 k等价 => k 在 m中 
            lb->second = v;                  // 与 map::operator[] 效果相似, 但更高效
            return lb;
        }
        else                                 // 与 map::insert( MPair(k, v) ) 效果相似, 返回值不同
        {
            typedef typename MapType::value_type MPair;
            return m.insert(lb, MPair(k, v) );
        }
    }

4 map::insert 3中形式3种返回值: pair/iterator/void

        pair<iterator,bool> 
    insert (const value_type& val);

        iterator 
    insert (iterator position, 
            const value_type& val);

    template <class InputIterator>  
        void 
    insert (InputIterator first, InputIterator last);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容