23. C++ 关联容器

23.1 关联容器介绍

关联容器内部的元素都是排好序的,有以下四种。

  • set:排好序的集合,不允许有相同元素。
  • multiset:排好序的集合,允许有相同元素。
  • map:每个元素都分为关键字和值两部分,容器中的元素是按关键字排序的。不允许有多个元素的关键字相同。
  • multimap:和 map 类似,差别在于元素的关键字可以相同。

关联容器使用注意事项:
1、不能修改 set 或 multiset 容器中元素的值(也不能修改 map 和 multimap 容器中元素的关键字)。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。
2、如果要修改 set 或 multiset 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素。
3、关联容器内部的元素或关键字之间比较大小可以用<运算符,也可以用自定义的比较器。因为有序,所以在关联容器上进行查找的速度较快。
4、使用关联容器的目的也就在于快速查找。当一个元素被插入关联容器时,该元素会和已有的元素进行比较,最终被插入一个合适的位置。
5、在关联容器中查找元素和插入元素的时间复杂度都是 O(log(n))。从 begin() 到 end() 遍历整个关联容器,就是从小到大遍历整个容器。
6、关联容器一般是用平衡二叉树实现的。

除了所有容器共有的成员函数外,关联容器还具有以下成员函数:

  • find:查找某个值。
  • lower_bound:查找某个下界。
  • upper_bound:查找某个上界。
  • equal_range:同时查找上界和下界。
  • count:计算等于某个值的元素个数。
  • insert:插人一个元素或一个区间。

23.2 STL multiset

multiset 是关联容器的一种,是排序好的集合(元素已经进行了排序),并且允许有相同的元素。

不能直接修改 multiset 容器中元素的值。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。因此,如果要修改 multiset 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素。

使用 multiset 必须包含头文件 <set>。multiset 类模板的定义如下:

template <class Key, class Pred = less<Key>, class B = allocator<Key> > class multiset {
    ...
};

该模板有三个类型参数:Key、Pred 和 B。类型参数可以有默认值,默认值就是某种类型。例如,Pred 类型参数的默认值就是 less<Key> 类型,B 的默认值就是 allocator<Key> 类型。第三个类型参数极少用到,一般都用默认值,因此这里不做介绍。

第一个类型参数说明 multiset 容器中的每个元素都是 Key 类型的。第二个类型参数 Pred 用于指明容器中元素的排序规则,在被实例化后,Pred 可以是函数对象类,也可以是函数指针类型。

multiset 内部在排序时定义了一个变量Pred op,根据表达式op(x, y)来比较两个元素 x、y 的大小。该表达式的值为 true,则说明 x 比 y 小。Pred 的默认值是 less<Key>,less 是 STL 中的函数对象类模板,其定义如下:

template <class_Tp>
struct less
{
    bool operator() (const _Tp &__x, const _Tp &__y) const
    { return __x < __y; }
};

这说明,在默认情况下,multiset 容器中的元素是用<运算符比较大小的。例如,假设 A 是一个类的名字,可以定义一个如下的容器对象:

multiset <A> s;

由于 multiset 的类型参数可以使用默认值,因此上面的语句等价于:

multiset < int, less<A>, allocator<A> > s;

模板类 multiset < A, less<A>, allocator<A> > 的 insert 成员函数可以用来插入一个元素。 插入过程中需要进行元素之间的比较,可以认为 insert 成员函数中定义了一个变量 less <A> op,用 op(x, y) 来比较元素 x、y 的大小。归根到底,还是用<运算符比较 x、y 的大小。 因此,<运算符必须经过适当重载,才可以向 multiset <A>容器中插人元素。

下面的程序 会编译出错:

#include <set>
using namespace std;
class A{};
int main(){
    multiset <A> a;
    a.insert( A() ); //编译出错,因为不能用“<”运算符比较两个A对象
}

multiset 常用的成员函数如表 1 所示。有的成员函数有不止一个版本,这里不一一 列出。


表 1 .png

multiset 及 set 中的 find 和 count 并不是用==运算符比较元素是否和待查找的值相等的。它们进行比较的原则是:如果x比y小和y比x小同时为假,就认为 x 和 y 相等。

下面通过一个例子说明 multiset 的用法。

#include <iostream>
#include <set>

using namespace std;

template <class T>
void Print(T first, T last)
{
    for (; first != last; ++first)
        cout << *first << " ";
    cout << endl;
}

class A
{
private:
    int n;
public:
    A(int n_) { n = n_; }
    friend bool operator < (const A & a1, const A & a2)
    { return a1.n < a2.n; }
    friend ostream & operator << (ostream & o, const A & a2)
    { o << a2.n; return o; }
    friend class MyLess;
};

class MyLess
{
public:
    bool operator() (const A & a1, const A & a2)  //按个位数比较大小
    { return (a1.n % 10) < (a2.n % 10); }
};

typedef multiset <A> MSET1;  //MSET1 用“<”运算符比较大小
typedef multiset <A, MyLess> MSET2;  //MSET2 用 MyLess::operator() 比较大小

int main()
{
    const int SIZE = 6;
    A a[SIZE] = { 4, 22, 19, 8, 33, 40 };

    MSET1 m1;
    m1.insert(a, a + SIZE);
    m1.insert(22);
    cout <<  m1.count(22) << endl;  //输出 2
    Print(m1.begin(), m1.end());  //输出 4 8 19 22 22 33 40

    MSET1::iterator pp = m1.find(19);
    if (pp != m1.end()){  //条件为真说明找到
        cout << "found" << endl;  //本行会被执行,输出 found
    }
    cout << *m1.lower_bound(22) << "," << *m1.upper_bound(22) << endl; //输出 22,33

    pp = m1.erase(m1.lower_bound(22), m1.upper_bound(22));
    //pp指向被删元素的下一个元素
    Print(m1.begin(), m1.end());  //输出 4 8 19 33 40
    cout << *pp << endl;  //输出 33

    MSET2 m2;  //m2中的元素按n的个位数从小到大排序
    m2.insert(a, a + SIZE);
    Print(m2.begin(), m2.end());  //输出 40 22 33 4 8 19

    return 0;
}

image.png

注意:
用 erase 成员函数删除迭代器 i 指向的元素后,迭代器 i 即告失效, 此时不能指望 ++i 后 i 会指向被删除元素的下一个元素;相反,++i 可能立即导致出错。如果想要得到被删除元素后面那个元素的迭代器,可以在删除前获取其迭代器并保存起来(这同样适用于 set、map、multimap 的 erase 成员函数)。事实上,如果得到了某关联容器的迭代器,则该迭代器并不会因为容器中元素的插入以及其他元素的删除而失效。只要该迭代器指向的元素没有被删除,就可以一直使用它。


23.3 C++ STL set

set 是关联容器的一种,是排序好的集合(元素已经进行了排序)。set 和 multiset 类似,它和 multiset 的差别在于 set 中不能有重复的元素。multiset 的成员函数 set 中也都有。

不能直接修改 set 容器中元素的值。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。因此,如果要修改 set 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素。

使用 set 必须包含头文件 <set>。set 的定义如下:

template < class Key, class Pred = less<Key>, class A = allocator<Key> > class set {...}

由于不能有重复元素,所以 set 中插入单个元素的 insert 成员函数与 multiset 中的有所不同,其原型如下:

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

如果 set 的 insert 成员函数的返回值是 pair 模板类对象 x,如果 x.second 为 true,则说明插入成功,此时 x.first 就是指向被插入元素的迭代器;如果 x.second 为 false,则说明要插入的元素已在容器中,此时 x.first 就是指向原有那个元素的迭代器。

关联容器的 equal_range 成员函数的返回值也是 pair 模板类对象,其原型如下:

pair<iterator, iterator> equal_range(const T & val);

返回值对象中的 first 就是 lower_bound 的值,second 就是 upper_bound 的值。

下面的程序演示了 set 的用法。

#include <iostream>
#include <set>

using namespace std;

int main()
{
    typedef set<int>::iterator IT;
    int a[5] = { 3,4,6,1,2 };
    set<int> st(a,a+5);    // st里是 1 2 3 4 6
    pair< IT,bool> result;
    result = st.insert(5); // st变成  1 2 3 4 5 6
    if(result.second){//插入成功则输出被插入元素
        cout << * result.first  << " inserted" << endl; //输出: 5 inserted
    }
    if(st.insert(5).second){
        cout << * result.first  << endl;
    }
    else{
        cout << * result.first << " already exists" << endl;
    }
    //输出 5 already exists
    pair<IT,IT> bounds = st.equal_range(4);
    cout << * bounds.first << "," << * bounds.second ;  //输出:4,5

    return 0;
}

image.png

23.4 C++ STL multimap和map

multimap 是关联容器的一种,multimap 的每个元素都分为关键字和值两部分,容器中的元素是按关键字排序的,并且允许有多个元素的关键字相同。

注意,不能直接修改 multimap 容器中的关键字。因为 multimap 中的元素是按照关键字排序的,当关键字被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。 multimap 中的元素都是 pair 模板类的对象。

map 是关联容器的一种,map 的每个元素都分为关键字和值两部分,容器中的元素是按关键字排序的,并且不允许有多个元素的关键字相同。

注意,不能直接修改 map 容器中的关键字。因为 map 中的元素是按照关键字排序的,当关键字被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。

这里我们统一介绍下map,基本理解了map,multimap也就可以很好的使用了。

要使用 map,必须包含头文件 <map>。map 的定义如下:

template < class Key, class T, class Pred = less<Key>, class A = allocator<T> >
class map{
    ...
    typedef pair< const Key, T > value_type;
    ...
};

map 和 multimap 十分类似,区别在于 map 容器中元素的关键字不能重复。multimap 有的成员函数,map 都有。此外,map 还有成员函数 operator[]:

T & operator[] (Key k);

该成员函数返回 first 值为 k 的元素的 second 部分的引用。如果容器中没有元素的 first 值等于 k,则自动添加一个 first 值为 k 的元素。如果该元素的 second 成员变量是一个对象,则用无参构造函数对其初始化。

下面的程序演示了 map 的用法。

#include <iostream>
#include <map>

using namespace std;

template <class T1,class T2>
ostream & operator <<(ostream & o,const pair<T1,T2> & p)
{ //将pair对象输出为 (first,second)形式
    o << "(" << p.first  << "," << p.second << ")";
    return o;
}

template<class T>
void Print(T first,T last)
{//打印区间[first,last)
    for( ; first != last; ++ first){
        cout <<  * first << " ";
    }
    cout << endl;
}

typedef map<int,double,greater<int> > MYMAP; //此容器关键字是整型,元素按关键字从大到小排序

int main()
{
    MYMAP mp;
    mp.insert(MYMAP::value_type(15,2.7));
    pair<MYMAP::iterator,bool> p = mp.insert(make_pair(15,99.3));
    if(!p.second){
        cout << * (p.first) << " already exists" << endl; //会输出
    }
    cout << mp.count(15) << endl; //输出 1
    mp.insert(make_pair(20,9.3));
    cout << mp[40] << endl;//如果没有关键字为40的元素,则插入一个

    Print(mp.begin(),mp.end());//输出:(40,0)(20,9.3)(15,2.7)
    mp[15] = 6.28; //把关键字为15的元素值改成6.28
    mp[17] = 3.14; //插入关键字为17的元素,并将其值设为3.14
    Print(mp.begin(),mp.end());

    return 0;
}

image.png

map应用例程:
1.问题描述:
输入一些单词(以“#”为结束标志),找出所有满足如下条件的单词:该单词不能通过字母的重排,得到输入文本中的另一个单词。在判断是否满足条件是不分大小写,但是在输出时应保留输入时的大小写,按字典序进行排列(所有大写字母在所有小写字母前面)。

样例输入:
ladder came tape soon leader acme RIDE lone Dreis peat
ScAlE orb eye Rides dealer NotE derail LaCeS drIed
noel dire Disk mace Rob dries

样例输出:
Disk
NotE
derail
drIed
eye
ladder
soon

思路:
1.如何判断单词是否能通过重排得到另一个单词?->只要两单词长度一样,字母出现次数一样(不区分大小写)即可->怎么判断字母出现次数一样?->将所有单词自己按照字典序排序字母,如果有两个strcmp一样即为可以通过重排
2.将每个单词标记为x,y->x为其标准化后的字符串(全部变为小写且排序完成),y为其标准化单词出现次数。如果y出现大于一次·,则不存入容器,最后输出容器即可!
3.利用map的映射关系以及set自动排序型以及忽略重复

#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
using namespace std;


map<string,int> cnt;
vector<string> word;


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

推荐阅读更多精彩内容