C++进阶(四)--set和map的介绍与使用

目录

1.序列式容器和关联式容器

2.set系列的使用

[2.1 set和multiset参考⽂档](#2.1 set和multiset参考⽂档)

[2.2 set的介绍](#2.2 set的介绍)

2.3.set的构造和迭代器

[2.4 set的增删查](#2.4 set的增删查)

键值对

set的增删查

find和erase的使用样例

[2.5 multiset和set的差别](#2.5 multiset和set的差别)

3.map系类的使用

[3.1 map和multimap参考⽂档](#3.1 map和multimap参考⽂档)

[3.2 map类的介绍](#3.2 map类的介绍)

[3.3 pair类型介绍](#3.3 pair类型介绍)

[3.4 map的构造](#3.4 map的构造)

[3.5 map的增删查](#3.5 map的增删查)

[3.6 map的数据修改](#3.6 map的数据修改)

3.7构造遍历及增删查使⽤样例

[3.8 map的迭代器和[]功能样例](#3.8 map的迭代器和[]功能样例)

[3.9 multimap和map的差异](#3.9 multimap和map的差异)

4.题目

[4.1 两个数组的交集](#4.1 两个数组的交集)

[4.2 环形链表II](#4.2 环形链表II)

[4.3 随机链表的复制](#4.3 随机链表的复制)

4.4前K个⾼频单词

[4.5 单词识别](#4.5 单词识别)


1.序列式容器和关联式容器

前⾯我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。

关联式容器也是用来存储数据的,其里面存储的是 <key, value> 结构的键值对,在数据检索时比序列式容器效率更高。关联式容器的元素存储是基于元素之间的关系(通常是根据键的排序关系),而不是像序列式容器那样仅依赖元素的插入顺序。由于这种关系的存在,如果随意交换元素的位置,可能会破坏其存储结构的内在逻辑。

关联式容器有 map/set 系列和 unordered_map/unordered_set 系列。本篇博客讲解的 map 和 set 底层是红黑树,红黑树是一颗平衡二叉搜索树。set 是用于 key 搜索场景的结构,map 是用于 key/value 搜索场景的结构。

2.set系列的使用

2.1 set和multiset参考⽂档

set - C++ Reference

2.2 set的介绍

set类的模板参数

cpp 复制代码
template < class T,                        // set::key_type/value_type
           class Compare = less<T>,        // set::key_compare/value_compare
           class Alloc = allocator<T>      // set::allocator_type
           > class set;

• T就是set底层关键字的类型

• set默认要求T⽀持⼩于⽐较,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现仿函数传给第⼆个模版参数

• set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第三个参 数。

• ⼀般情况下,我们都不需要传后两个模版参数。

1.存储特性:set是按照一定次序存储元素的容器,它会自动对元素进行排序。例如,对于存储整数的 set,元素会按照升序排列;对于存储自定义对象的 set,需要定义相应的比较函数(仿函数)。

2.元素的唯一性:在 set 中,元素的 value 既作为元素本身,也作为元素的键(可以理解为 value 就是 key,类型为 T),并且每个 value 必须是唯一的。这意味着相同元素不会重复存储,因此可以利用 set 来对元素集合进行去重操作。

  1. 元素的修改限制:set 中的元素不能在容器中修改,因为元素总是 const。这是因为 set 的底层是红黑树,修改元素的值可能会破坏红黑树的结构规则,导致树的不平衡或违反排序规则。

  2. 排序准则:在内部,set 中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。默认情况下,对于内置类型,使用 < 运算符进行比较;对于自定义类型,需要提供自定义的比较函数或重载 < 运算符。

2.3.set的构造和迭代器

set的构造我们关注以下⼏个接⼝即可。

set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。

代码示例:

cpp 复制代码
#include <iostream>
#include <set>
#include <vector>
using namespace std;

int main() {
    // 1. 无参默认构造
    set<int> set1;
    set1.insert(3);
    set1.insert(1);
    set1.insert(2);
    cout << "set1: ";
    for (const auto& elem : set1) {
        cout << elem << " ";
    }
    cout << endl;

    // 2. 迭代器区间构造
    vector<int> vec = { 5, 4, 6 };
    set<int> set2(vec.begin(), vec.end());
    cout << "set2: ";
    for (const auto& elem : set2) {
        cout << elem << " ";
    }
    cout << endl;

    // 3. 拷贝构造
    set<int> set3(set1);
    cout << "set3 (copy of set1): ";
    for (const auto& elem : set3) {
        cout << elem << " ";
    }
    cout << endl;

    // 5. 初始化列表构造
    set<int> set4 = { 7, 8, 9 };
    cout << "set4: ";
    for (const auto& elem : set4) {
        cout << elem << " ";
    }
    cout << endl;

    // 使用正向迭代器
    cout << "Forward iteration of set4: ";
    for (set<int>::iterator it = set4.begin(); it != set4.end(); ++it) 
    {
        cout << *it << " ";
    }
    cout << endl;

    // 使用反向迭代器
    cout << "Reverse iteration of set4: ";
    for (set<int>::reverse_iterator rit = set4.rbegin(); rit != set4.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;

    // 使用 auto 关键字简化迭代器使用
    cout << "Forward iteration of set4 (using auto): ";
    for (auto it = set4.begin(); it != set4.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;

    cout << "Reverse iteration of set4 (using auto): ";
    for (auto rit = set4.rbegin(); rit != set4.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;

    return 0;
}

输出:

2.4 set的增删查

这里需要先了解一下键值对,后面的pair会有详细讲解

键值对

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该词,在词典中就可以找到与其对应的中文含义。

SGI-STL中关于键值对的定义:

cpp 复制代码
template <class T1, class T2>
struct pair
{
    T1 first;
    T2 second;
    // 默认构造
    pair()
        :first(T1())
        ,second(T2())
    {}
    // 带参构造
    pair(const T1& a, const T2& b)
        :first(a)
        ,second(b)
    {}
};
set的增删查

了解以下基本接口就行,涉及到其他的可以查看手册,下面是比较常用的,直接阅读代码即可,可以自己复制代码测试。

讲解一下第一段代码中的键值对:

pair<set<int>::iterator, bool>:insert 方法的返回值是一个 pair 类型。这个 pair 的 first 成员是一个迭代器,指向新插入元素的位置(如果插入成功),或者指向已经存在的相同元素的位置(如果元素已经在 set 中)。pair 的 second 成员是一个 bool 类型,用于指示插入操作是否成功,true 表示插入成功,false 表示元素已经存在,插入失败。

cpp 复制代码
#include <iostream>
#include <set>
#include <vector>
#include <string>
using namespace std;

int main() {
    // 定义一个存储整数的 set 容器
    set<int> mySet;

    // 1. 单个数据插入,如果已经存在则插入失败
    pair<set<int>::iterator, bool> insertResult1 = mySet.insert(5);
    cout << "Inserting 5: " << (insertResult1.second ? "Success" : "Failure (already exists)") << endl;
    //再次插入,包失败的
    insertResult1 = mySet.insert(5);
    cout << "Inserting 5 again: " << (insertResult1.second ? "Success" : "Failure (already exists)") << endl;

    // 2. 列表插入,已经在容器中存在的值不会插入
    mySet.insert({ 1, 2, 3, 4 });

    cout << "After inserting {1, 2, 3, 4}: ";
    for (const auto& elem : mySet) {
        cout << elem << " ";
    }
    cout << endl;

    // 3. 迭代器区间插入,已经在容器中存在的值不会插入
    vector<int> vec = { 5,6, 7, 8 };
    mySet.insert(vec.begin(), vec.end());

    cout << "After inserting elements from vector: ";
    for (const auto& elem : mySet) {
        cout << elem << " ";
    }
    cout << endl;

    // 4. 查找 val,返回 val 所在的迭代器,没有找到返回 end()
    set<int>::iterator findIt = mySet.find(3);
    if (findIt != mySet.end()) {//distance是计算两个迭代器之间的距离,这里的3距离1有两个位置
        cout << "Found element 3 at position: " << distance(mySet.begin(), findIt) << endl;
    }
    else {
        cout << "Element 3 not found." << endl;
    }

    // 5. 查找 val,返回 val 的个数//由于当前set去重,所以这里只返回1 or 0
    set<int>::size_type count = mySet.count(4);
    cout << "Count of element 4: " << count << endl;

    // 6. 删除一个迭代器位置的值
    set<int>::const_iterator position = mySet.find(2);
    if (position != mySet.end()) {
        mySet.erase(position);
        cout << "Deleted element at position of 2." << endl;
    }

    cout << "After deleting element at position of 2: ";
    for (const auto& elem : mySet) {
        cout << elem << " ";
    }
    cout << endl;

    // 7. 删除 val,val 不存在返回 0,存在返回 1
    set<int>::size_type eraseResult = mySet.erase(8);//size_type类似于size_t,是专门为容器准备额类型
    cout << "Deleted " << eraseResult << " element(s) with value 8." << endl;

    cout << "After deleting element 8: ";
    for (const auto& elem : mySet) {
        cout << elem << " ";
    }
    cout << endl;

    // 8. 删除一段迭代器区间的值
    set<int>::const_iterator first = mySet.find(5);
    set<int>::const_iterator last = mySet.find(7);
    mySet.erase(first, last);//左闭右开-- 1 3 4 7

    cout << "After deleting elements from 5 (inclusive) to 7 (exclusive): ";
    for (const auto& elem : mySet) 
    {
        cout << elem << " ";
    }
    cout << endl;

    // 9. 返回大于等于 val 位置的迭代器
    set<int>::iterator lower = mySet.lower_bound(4);//三元运算符类型需要统一,所以需要转换为字符串
    cout << "Lower bound of 4: " << (lower == mySet.end() ? "end" : to_string(*lower)) << endl;//to_string将(*lower)转换为字符串

    // 10. 返回大于 val 位置的迭代器
    set<int>::iterator upper = mySet.upper_bound(4);
    cout << "Upper bound of 4: " << (upper == mySet.end() ? "end" : to_string(*upper)) << endl;
    mySet.insert({1,2,3,4,5,6,7,8,9});
    cout << "After insert new elements:";
    for (const auto& elem : mySet) {
        cout << elem << " ";
    }
    cout << endl;
    set<int>::iterator itlower = mySet.lower_bound(4);
    cout << "Lower bound of 4: " << (itlower == mySet.end() ? "end" : to_string(*itlower)) << endl;

    
    set<int>::iterator itupper = mySet.upper_bound(8);
    cout << "Upper bound of 4: " << (itupper == mySet.end() ? "end" : to_string(*itupper)) << endl;
    //使用lower_bound和upper_boun也可以实现迭代区间删除
    mySet.erase(itlower, itupper);
    cout << "删除一段区间:";//4~9同样是左闭右开
    for (const auto& elem : mySet) {
        cout << elem << " ";
    }
    return 0;
}
find和erase的使用样例
cpp 复制代码
#include <iostream>
#include <set>
#include <algorithm>  // 包含算法库,用于 find 函数

using namespace std;

int main() {
    set<int> s = { 4, 2, 7, 2, 8, 5, 9 };

    // 输出初始集合元素
    cout << "初始集合元素: ";
    for (const auto& e : s) {
        cout << e << " ";
    }
    cout << endl;

    // 删除最小值
    s.erase(s.begin());
    cout << "删除最小值后: ";
    for (const auto& e : s) {
        cout << e << " ";
    }
    cout << endl;

    // 直接删除元素 x
    int x;
    cout << "请输入要删除的值: ";
    cin >> x;
    size_t num = s.erase(x);
    if (num == 0) {
        cout << x << " 不存在!" << endl;
    }
    else {
        cout << "删除 " << x << " 后: ";
        for (const auto& e : s) {
            cout << e << " ";
        }
        cout << endl;
    }

    // 直接查找元素 x 并利用迭代器删除
    cout << "请输入另一个要删除的值: ";
    cin >> x;
    auto pos = s.find(x);
    if (pos != s.end()) {
        s.erase(pos);
        cout << "使用迭代器删除 " << x << " 后: ";
        for (const auto& e : s) {
            cout << e << " ";
        }
        cout << endl;
    }
    else {
        cout << x << " 不存在!" << endl;
    }

    // 算法库的查找 O(N)
    cout << "请输入要查找的值(使用算法库): ";
    cin >> x;
    auto pos1 = find(s.begin(), s.end(), x);
    if (pos1 != s.end()) {
        cout << x << " 找到了(使用算法库)." << endl;
    }
    else {
        cout << x << " 没找到(使用算法库)." << endl;
    }

    // set 自身实现的查找 O(logN)
    cout << "请输入要查找的值(使用 set 的 find): ";
    cin >> x;
    auto pos2 = s.find(x);
    if (pos2 != s.end()) {
        cout << x << " 找到了(使用 set 的 find)." << endl;
    }
    else {
        cout << x << " 没找到(使用 set 的 find)." << endl;
    }

    // 利用 count 间接实现快速查找
    cout << "请输入要查找的值(使用 count): ";
    cin >> x;
    if (s.count(x)) {
        cout << x << " 存在!" << endl;
    }
    else {
        cout << x << " 不存在!" << endl;
    }

    return 0;
}

2.5 multiset和set的差别

multiset的介绍及使用

multiset和set的使⽤基本完全类似,主要区别点在于multiset⽀持值冗余,那么 insert/find/count/erase都围绕着⽀持值冗余有所差异,具体参看下⾯的样例代码理解。

cpp 复制代码
#include <iostream>
#include <set>
using namespace std;

int main() {
    // 相比 set,multiset 是排序的,但是不去重
    multiset<int> s = {4, 2, 7, 2, 4, 8, 4, 5, 4, 9};

    // 输出 multiset 中的所有元素
    auto it = s.begin();
    while (it != s.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // 相比 set,x 可能会存在多个,find 查找中序的第一个
    int x;
    cout << "请输入要查找的值: ";
    cin >> x;
    auto pos = s.find(x);
    while (pos != s.end() && *pos == x) {
        cout << *pos << " ";
        ++pos;
    }
    cout << endl;

    // 输出 x 的实际个数
    cout << "值 " << x << " 的个数: " << s.count(x) << endl;

    // 相比 set,erase 给值时会删除所有的 x
    s.erase(x);
    cout << "删除所有 " << x << " 后的 multiset: ";
    for (const auto& e : s) {
        cout << e << " ";
    }
    cout << endl;

    return 0;
}

输出结果:

3.map系类的使用

3.1 map和multimap参考⽂档

map - C++ Reference

3.2 map类的介绍

模板参数:

cpp 复制代码
template < class Key,                                     // map::key_type
           class T,                                       // map::mapped_type
           class Compare = less<Key>,                     // map::key_compare
           class Alloc = allocator<pair<const Key,T> >    // map::allocator_type
           > class map;

key: 键值对中key的类型

T: 键值对中value的类型

Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)

Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器

map 是一种关联容器:存储键值对(key-value pairs),每个键在 map 中是唯一的,不能重复。

按照键(key)的次序存储元素,该次序由键的比较函数(通常是 less<Key> 或自定义的比较函数)决定。

元素存储结构:内部使用 value_type 来表示键值对,typedef pair<const key, T> value_type;,其中 key 是键的类型,T 是值的类型。键是 const,以保证键在 map 中不会被修改,避免破坏存储的有序性和映射关系。

排序规则:元素根据键的大小进行排序,对于自定义类型的键,需要提供自定义的比较函数或重载 operator< 运算符,以确保 map 能正确排序元素。

访问元素的方式:

operator[]:使用 operator[] 可以通过键访问对应的值,如 map[key]。如果键存在,会返回该键对应值的引用;如果键不存在,会插入一个新的键值对,键为 key,值为该值类型的默认值,并返回该新值的引用。

at() 方法:与 operator[] 类似,但如果键不存在会抛出 std::out_of_range 异常,而不是插入新元素。

find() 方法:查找键是否存在,返回指向元素的迭代器,如果键不存在,返回 end() 迭代器。

性能特点:查找、插入和删除操作的平均时间复杂度为 O (log n)`,因为其底层是平衡二叉搜索树(红黑树)。相比 unordered_map,map 的元素查找速度可能较慢,但 map 能提供有序的元素序列,适合需要顺序迭代元素的场景。

3.3 pair类型介绍

map底层的红⿊树节点中的数据,使⽤pair存储键值对数据

cpp 复制代码
template <class T1, class T2>
struct pair
{
    // map::key_type,通常是键的类型
    // map::mapped_type,通常是映射值的类型
    // map::key_compare,用于比较键的类型

    // 类型别名,方便使用
    typedef T1 first_type;
    typedef T2 second_type;

    // 存储第一个元素的数据成员
    T1 first;
    // 存储第二个元素的数据成员
    T2 second;

    // 默认构造函数,将 first 初始化为 T1 的默认值,second 初始化为 T2 的默认值
    pair() : first(T1()), second(T2())
    {}

    // 接受两个参数的构造函数,使用传入的参数初始化 first 和 second
    pair(const T1& a, const T2& b) : first(a), second(b)
    {}

    // 接受另一个 pair 对象的构造函数,从另一个 pair 对象复制元素
    template<class U, class V>
    pair(const pair<U, V>& pr) : first(pr.first), second(pr.second)
    {}
};

// 一个全局函数模板,用于方便地创建 pair 对象
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y)
{
    // 创建并返回一个 pair 对象,使用传入的参数 x 和 y
    return (pair<T1, T2>(x, y));
}
  • make_pair 是一个模板函数,用于创建并返回一个 pair 对象。
  • 它接受两个参数 xy,分别用于初始化 pairfirstsecond 成员。
  • 使用 make_pair 可以简化 pair 对象的创建过程,特别是当 pair 的类型较复杂时,可以避免显式指定类型。

示例:

cpp 复制代码
#include <iostream>
#include <utility> // 包含 pair 和 make_pair 的定义

using namespace std;

int main() {
    // 使用构造函数创建 pair
    pair<int, string> p1(10, "Alice");
    cout << "p1: " << p1.first << ", " << p1.second << endl;

    // 使用 make_pair 创建 pair
    pair<int, string> p2 = make_pair(20, "Bob");
    cout << "p2: " << p2.first << ", " << p2.second << endl;

    // 使用模板构造函数创建 pair
    pair<double, char> p3 = make_pair(3.14, 'C');
    cout << "p3: " << p3.first << ", " << p3.second << endl;

    return 0;
}

输出:

3.4 map的构造

map的构造我们关注以下⼏个接⼝即可。

map的⽀持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,map⽀持修改value数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜索树的结构。

cpp 复制代码
#include <iostream>
#include <map>
#include <string>
#include <list>
#include <iterator>

using namespace std;

// 定义一个仿函数(函数对象),用于比较键
struct MyCompare {
    bool operator()(const string& s1, const string& s2) const {
        if (s1.length() < s2.length()) {
            return true;
        }
        else if (s1.length() > s2.length()) {
            return false;
        }
        else {
            // 长度相同,按字典序比较
            return s1 < s2;
        }
    }
};

int main() {
    // (1) 无参默认构造
    map<string, int, MyCompare> myMap1;
    myMap1.insert({ "Alice", 10 });
    myMap1.insert({ "Bob", 20 });
    myMap1.insert({ "Charlie", 30 });

    cout << "myMap1 的元素:" << endl;
    for (const auto& pair : myMap1) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (2) 迭代器区间构造
    list<pair<string, int>> myList = { {"David", 40}, {"Eve", 50} };
    map<string, int, MyCompare> myMap2(myList.begin(), myList.end());

    cout << "\nmyMap2 的元素(来自列表):" << endl;
    for (const auto& pair : myMap2) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (3) 拷贝构造
    map<string, int, MyCompare> myMap3(myMap1);

    cout << "\nmyMap3 的元素(myMap1 的拷贝):" << endl;
    for (const auto& pair : myMap3) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (5) initializer_list 构造
    map<string, int, MyCompare> myMap4({ {"Frank", 60}, {"Grace", 70} });

    cout << "\nmyMap4 的元素(initializer_list):" << endl;
    for (const auto& pair : myMap4) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // 迭代器是一个双向迭代器
    cout << "\n使用迭代器遍历 myMap4:" << endl;
    for (auto it = myMap4.begin(); it != myMap4.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }

    // 正向迭代器
    cout << "\n使用 begin() 和 end() 遍历 myMap4:" << endl;
    for (auto it = myMap4.begin(); it != myMap4.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }

    // 反向迭代器
    cout << "\n使用 rbegin() 和 rend() 反向遍历 myMap4:" << endl;
    for (auto rit = myMap4.rbegin(); rit != myMap4.rend(); ++rit) {
        cout << rit->first << ": " << rit->second << endl;
    }

    return 0;
}

3.5 map的增删查

map的增删查关注以下⼏个接⼝即可:

map增接⼝,插⼊的pair键值对数据,跟set所有不同,但是查和删的接⼝只⽤关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代还可以修改value.

以下代码建议复制,修改数据自行测试:

(1)代码中的insert逻辑这里讲解一下:

map 的 insert 方法的内部逻辑:

当你调用 myMap.insert({key, value}); 时:

查找键是否存在:map 会使用其内部的比较函数(通常是 operator< 或自定义的比较函数)来检查键是否已经存在于 map 中。

键不存在的情况:如果键不存在,insert 方法会将新的键值对 {key, value} 插入到 map 中,并将 insert 操作的返回值 pair<iterator, bool> 中的 bool 部分设置为 true。同时,iterator 部分会指向新插入的元素。

键存在的情况:如果键已经存在,insert 方法不会插入新的键值对,因为 map 不允许重复的键。

此时,insert 操作的返回值 pair<iterator, bool> 中的 bool 部分会被设置为 false。iterator 部分会指向已存在的具有相同键的元素。

cpp 复制代码
#include <iostream>
#include <map>
#include <string>
#include <list>
#include <iterator>

using namespace std;

// 定义一个仿函数(函数对象),用于比较键
struct MyCompare {
    bool operator()(const string& s1, const string& s2) const {
                if (s1.length() < s2.length()) {
                    return true;
                }
                else if (s1.length() > s2.length()) {
                    return false;
                }
                else {
                    // 长度相同,按字典序比较
                    return s1 < s2;
                }
            }
};

int main() {
    // 创建一个 map,键为 string,值为 int
    map<string, int, MyCompare> myMap;

    // Member types
    // key_type     -> The first template parameter (Key)
    // mapped_type  -> The second template parameter (T)
    // value_type   -> pair<const key_type, mapped_type>

    // (1) 单个数据插入,如果 key 已经存在则插入失败
    pair<map<string, int, MyCompare>::iterator, bool> insertResult1 = myMap.insert({ "Alice", 10 });
    cout << "插入 {'Alice', 10} 结果: " << (insertResult1.second ? "成功" : "失败") << endl;

    pair<map<string, int, MyCompare>::iterator, bool> insertResult2 = myMap.insert({ "Alice", 20 });//键唯一,插入失败
    cout << "插入 {'Alice', 20} 结果: " << (insertResult2.second ? "成功" : "失败") << endl;

    // (2) 列表插入,已经在容器中存在的值不会插入
    myMap.insert({ {"Bob", 20}, {"Charlie", 30} });
    cout << "插入列表后的元素:" << endl;
    for (const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (3) 迭代器区间插入,已经在容器中存在的值不会插入
    list<pair<string, int>> myList = { {"David", 40}, {"Eve", 50} };
    myMap.insert(myList.begin(), myList.end());
    cout << "插入迭代器区间后的元素:" << endl;
    for (const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (4) 查找 k,返回 k 所在的迭代器,没有找到返回 end()
    string key = "Charlie";
    auto it = myMap.find(key);
    if (it != myMap.end()) {
        cout << "找到键 " << key << ",值为 " << it->second << endl;
    }
    else {
        cout << "键 " << key << " 未找到" << endl;
    }

    // (5) 查找 k,返回 k 的个数
    cout << "键 " << key << " 的个数: " << myMap.count(key) << endl;

    // (6) 删除一个迭代器位置的值
    myMap.erase(it);
    cout << "删除键 " << key << " 后的元素:" << endl;
    for (const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (7) 删除 k,k 存在返回 1,不存在返回 0
    key = "Bob";
    cout << "删除键 " << key << ",返回值: " << myMap.erase(key) << endl;

    // (8) 删除一段迭代器区间的值
    auto first = myMap.begin();
    auto last = myMap.end();
    --last; 
    myMap.erase(first, last);//左闭右开,当前last指向最后一个元素
    cout << "删除一段迭代器区间后的元素:" << endl;
    for (const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // (9) 返回大于等于 k 位置的迭代器
    key = "David";
    auto lower = myMap.lower_bound(key);
    cout << "大于等于键 " << key << " 的第一个元素: ";
    if (lower != myMap.end()) {
        cout << lower->first << ": " << lower->second << endl;
    }
    else {
        cout << "未找到" << endl;
    }

    // (10) 返回大于 k 位置的迭代器
    key = "David";
    auto upper = myMap.upper_bound(key);
    cout << "大于键 " << key << " 的第一个元素: ";
    if (upper != myMap.end()) {
        cout << upper->first << ": " << upper->second << endl;
    }
    else {
        cout << "未找到" << endl;
    }

    return 0;
}

3.6 map的数据修改

map⽀持修改mapped_type数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜 索树的结构。

map第⼀个⽀持修改的⽅式时通过迭代器,迭代器遍历时或者find返回key所在的iterator修改,map 还有⼀个⾮常重要的修改接⼝operator[],但是operator[]不仅仅⽀持修改,还⽀持插⼊数据和查找数据,所以他是⼀个多功能复合接⼝,需要注意从内部实现⻆度,map这⾥把我们传统说的value值,给的是T类型,typedef为 mapped_type。⽽value_type是红⿊树结点中存储的pair键值对值。⽇常使⽤我们还是习惯将这⾥的 T映射值叫做value。

operator[]原理:

这是手册里面的,解释一下:

键存在时:如果 k 与容器中某个元素的键匹配,该函数返回其映射值的引用。例如,假设有一个 map<string, int> myMap,其中已经存在键为 "apple" 的元素,那么 myMap["apple"] 将返回对应 int 值的引用,可直接对其进行修改等操作。

键不存在时:如果 k 不与容器中任何元素的键匹配,函数会插入一个具有该键的新元素,并返回其映射值的引用。需要注意的是,这总是会使容器大小增加 1,即使没有给该元素赋值(该元素使用其默认构造函数构造)。例如,对于一个空的 map<string, int> myMap,执行 myMap["banana"] = 0; 之前,myMap["banana"] 这一操作就会先插入键为 "banana" 的新元素,然后返回其 int 值的引用,再将 0 赋值给该引用所指向的元素。

调用 operator[] 函数等价于 (*((this->insert(make_pair(k,mapped_type()))).first)).second。这里 this 指向当前的 map 对象,insert(make_pair(k,mapped_type())) 尝试插入键为 k、值为 mapped_type 类型默认值的键值对,insert 函数返回一个 pair<iterator, bool>,其中 iterator 指向插入或已存在的元素,bool 表示是否插入成功。.first 获取迭代器,* 解引用迭代器得到键值对元素,.second 获取该元素的值(即映射值)。

cpp 复制代码
// 方式一:使用 operator[]
myMap[key] = 42;

// 方式二:使用等价形式(不推荐,仅为解释等价性)
(*((myMap.insert(make_pair(key, int()))).first)).second = 42;

示例代码:

注释应该写的蛮清楚的,有问题,评论区留言。

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

using namespace std;

// 定义一个仿函数(函数对象),用于比较键
struct MyCompare {
    bool operator()(const string& a, const string& b) const {
        return a.length() < b.length();
    }
};

int main() {
    // 创建一个 map,键为 string,值为 int,使用自定义比较函数
    map<string, int, MyCompare> myMap;

    // Member types
    // key_type     -> The first template parameter (Key)
    // mapped_type  -> The second template parameter (T)
    // value_type   -> pair<const key_type, mapped_type>

    // 查找 k,返回 k 所在的迭代器,没有找到返回 end()
    // 如果找到了,通过 iterator 可以修改 key 对应的 mapped_type 值
    string key = "Alice";
    auto it = myMap.find(key);
    if (it != myMap.end()) {
        cout << "找到键 " << key << ",值为 " << it->second << endl;
        it->second = 100; // 修改值
    }
    else {
        cout << "键 " << key << " 未找到" << endl;
    }

    // 插入一个 pair<key, T> 对象
    // 1. 如果 key 已经在 map 中,插入失败,则返回一个 pair<iterator, bool> 对象,返回 pair 对象 first 是 key 所在结点的迭代器,second 是 false
    // 2. 如果 key 不在 map 中,插入成功,则返回一个 pair<iterator, bool> 对象,返回 pair 对象 first 是新插入 key 所在结点的迭代器,second 是 true
    // 也就是说无论插入成功还是失败,返回 pair<iterator, bool> 对象的 first 都会指向 key 所在的迭代器
    pair<map<string, int, MyCompare>::iterator, bool> insertResult1 = myMap.insert({ "Bob", 20 });
    cout << "插入 {'Bob', 20} 结果: " << (insertResult1.second ? "成功" : "失败") << endl;

    // 再次插入相同的键
    pair<map<string, int, MyCompare>::iterator, bool> insertResult2 = myMap.insert({ "Bob", 30 });
    cout << "再次插入 {'Bob', 30} 结果: " << (insertResult2.second ? "成功" : "失败") << endl;

    // operator[] 的内部实现
    // 1. 如果 k 不在 map 中,insert 会插入 k 和 mapped_type 默认值,同时 [] 返回结点中存储 mapped_type 值的引用,那么我们可以通过引用修改映射值。所以 [] 具备了插入 + 修改功能
    // 2. 如果 k 在 map 中,insert 会插入失败,但是 insert 返回 pair 对象的 first 是指向 key 结点的迭代器,返回值同时 [] 返回结点中存储 mapped_type 值的引用,所以 [] 具备了查找 + 修改的功能
    key = "Charlie";
    myMap[key] = 40; // 如果键不存在,则插入键值对;如果键存在,则修改值
    cout << "插入或修改键 " << key << ",值为 " << myMap[key] << endl;

    // 输出 map 中的所有元素
    cout << "Map 中的元素:" << endl;
    for (const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    return 0;
}

3.7构造遍历及增删查使⽤样例

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

using namespace std;

int main() {
    // initializer_list 构造及迭代遍历
    map<string, string> dict = {
        {"left", "左边"}, {"right", "右边"},
        {"insert", "插入"}, {"string", "字符串"}
    };

    // 使用 auto 和迭代器遍历 map
    auto it = dict.begin();
    while (it != dict.end()) {
        // 使用 operator-> 访问 pair 的成员
        cout << it->first << ":" << it->second << endl;
        ++it;
    }
    cout << endl;

    // insert 插入 pair 对象的 4 种方式,对比之下,最后一种最方便
    pair<string, string> kv1("first", "第一个");
    dict.insert(kv1);
    dict.insert(pair<string, string>("second", "第二个"));
    dict.insert(make_pair("sort", "排序"));
    dict.insert({ "auto", "自动的" });

    // "left" 已经存在,插入失败
    dict.insert({ "left", "左边,剩余" });

    // 范围 for 遍历
    for (const auto& e : dict) {
        cout << e.first << ":" << e.second << endl;
    }
    cout << endl;

    string str;
    cout << "请输入单词" << endl;
    while (cin >> str)
    {
        auto ret = dict.find(str);
        if (ret != dict.end())
        {
            
            cout << "->" << ret->second << endl;
        }
        else
        {
            cout << "⽆此单词,请重新输⼊"<< endl;
        }
    }

    // erase 等接口跟 set 完全类似,这里就不演示讲解了

    return 0;
}

3.8 map的迭代器和[]功能样例

使用 finditerator 统计水果出现的次数

cpp 复制代码
#include <iostream>
#include <map>
#include <string>
#include <vector>

using namespace std;

int main() {
    // 利用 find 和 iterator 修改功能,统计水果出现的次数
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
    map<string, int> countMap;

    for (const auto& str : arr) {
        // 先查找水果在不在 map 中
        auto ret = countMap.find(str);
        if (ret == countMap.end()) {
            // 不在,说明水果第一次出现,则插入 {水果, 1}
            countMap.insert({ str, 1 });
        }
        else {
            // 在,则查找到的节点中水果对应的次数++
            ret->second++;
        }
    }

    // 输出统计结果
    for (const auto& e : countMap) {
        cout << e.first << ":" << e.second << endl;
    }
    cout << endl;

    return 0;
}

利用 [] 插入 + 修改功能,巧妙实现统计水果出现的次数

cpp 复制代码
#include <iostream>
#include <map>
#include <string>
#include <vector>

using namespace std;

int main() {
    // 利用 [] 插入 + 修改功能,巧妙实现统计水果出现的次数
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
    map<string, int> countMap;

    for (const auto& str : arr) {
        // [] 先查找水果在不在 map 中
        // 1. 不在,说明水果第一次出现,则插入 {水果, 0},同时返回次数的引用,++ 一下就变成 1 次了
        // 2. 在,则返回水果对应的次数++
        countMap[str]++;
    }

    // 输出统计结果
    for (const auto& e : countMap) {
        cout << e.first << ":" << e.second << endl;
    }
    cout << endl;

    return 0;
}

插入和修改操作

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

using namespace std;

int main() {
    map<string, string> dict;

    // 插入一个键值对
    dict.insert(make_pair("sort", "排序"));

    // key 不存在 -> 插入 {"insert", string()}
    dict["insert"];

    // 插入 + 修改
    dict["left"] = "左边";

    // 修改
    dict["left"] = "左边、剩余";

    // key 存在 -> 查找
  //  cout << dict["left"] << endl;
    for (auto& s : dict)
    {
        cout << s.first << ":" << s.second <<endl;
    }

    return 0;
}

3.9 multimap和map的差异

multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么 insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如 find时,有多个key,返回中序第⼀个。其次就是multimap不⽀持[],因为⽀持key冗余,[]就只能⽀持插⼊了,不能⽀持修改。

4.题目

来几道题目练练手,感受一下set和map的方便

4.1两个数组的交集

cpp 复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        set<int> s1 = {nums1.begin(),nums1.end()};
        set<int> s2 = {nums2.begin(),nums2.end()};
        // 因为set遍历是有序的,有序值,依次⽐较
        // ⼩的++,相等的就是交集
        auto it1 = s1.begin();
        auto it2 = s2.begin();
        vector<int> vt;
        while(it1!=s1.end()&&it2!=s2.end())
        {
            if(*it1>*it2)
            {
                it2++;
            }
            else if(*it1<*it2)
            {
                it1++;
            }
            else
            {
                vt.push_back(*it1);
                it1++;
                it2++;
            }
        }
        return vt;
    }
};

4.2 环形链表II

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        set<ListNode*> s;
        ListNode* cur = head;
        while(cur)
        {
            if(s.count(cur))
            {
                return cur;
            }
            else
            {
                s.insert(cur);
            }
            cur = cur->next;
        }
        return nullptr;
    }
};

4.3 随机链表的复制

这里如果没有学习过map,为了控制随机指针,我们将拷⻉结点链接在原节点的后⾯解决,后⾯拷⻉节点还得解下来链接,⾮常⿇烦。这⾥我们直接让{原结点,拷⻉结点}建⽴映射关系放到map中,控制随机指 针会⾮常简单⽅便,这⾥体现了map在解决⼀些问题时的价值,完全是降维打击。

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        map<Node*,Node*> NodeMap;
        Node* copyhead = nullptr,*copytail = nullptr;
        Node* cur = head;
        while(cur)
        {
            if(copyhead == nullptr)
            {
                copyhead = copytail = new Node(cur->val);
            }
            else
            {
                copytail->next = new Node(cur->val);
                copytail = copytail->next;
            }
            NodeMap[cur] = copytail;//成立键值对
            cur = cur->next;
        }

        //处理random
        cur = head;
        Node* copy = copyhead;
        while(cur)
        {
            if(cur->random==nullptr)
            {
                copy->random=nullptr;
            }
            else
            {
                //通过[]就能找到键值对应的val
                copy->random = NodeMap[cur->random];
            }
            cur = cur->next;
            copy = copy->next;
        }
         return copyhead;
    }
   
};

4.4前K个⾼频单词

cpp 复制代码
class Solution {
public:
    struct Compare{
        bool operator()(const pair<string,int>& s1,const pair<string,int>& s2) const
        {
            return s1.second>s2.second;
        }
    };
    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int> countMap;
        for(auto& e:words)
        {
            countMap[e]++;
        }
        vector<pair<string,int>> v(countMap.begin(),countMap.end());
        stable_sort(v.begin(),v.end(),Compare());//这里必须使用稳定的排序
        
        vector<string> strV;
        for(int i = 0;i<k;i++)
        {
            strV.push_back(v[i].first);
        }
        return strV;
    }
};

4.5 单词识别

cpp 复制代码
#include <algorithm>
#include <cctype>
#include <cwctype>
#include <iostream>
#include<map>
#include<string>
#include<vector>
#include<utility>
using namespace std;
bool compare(const pair<string,int>& s1,const pair<string, int>& s2)
{
    return s1.second>s2.second;
}
int main() {
    map<string,int> mapCount;
    string s;
    while(getline(cin,s))
    {
        int j = 0;
        for(int i = 0;i<s.size();i++)
        {
            if(s[i] == ' '||s[i]=='.')
            {
                string wd = s.substr(j,i-j);
                if(isupper(wd[0]))//单词的第一个字母是否大写
                    {
                        wd[0] = towlower(wd[0]);
                    }
                j = i+1;
                mapCount[wd]++;
            }
        }
        vector<pair<string,int>> v(mapCount.begin(),mapCount.end());
        sort(v.begin(),v.end(),compare);
        for(int i = 0;i<v.size();i++)
        {
            cout<<v[i].first<<":"<<v[i].second<<endl;
        }
        return 0;
    }
}
// 64 位输出请用 printf("%lld")

本篇博客到此结束,欢迎评论区留言

相关推荐
OTWOL23 分钟前
【C++编程入门基础(一)】
c++·算法
许苑向上26 分钟前
Java八股文(下)
java·开发语言
菜鸟一枚在这31 分钟前
深入解析设计模式之单例模式
开发语言·javascript·单例模式
独孤求败Ace35 分钟前
第44天:Web开发-JavaEE应用&反射机制&类加载器&利用链&成员变量&构造方法&抽象方法
java·开发语言
计算机-秋大田1 小时前
基于Spring Boot的农产品智慧物流系统设计与实现(LW+源码+讲解)
java·开发语言·spring boot·后端·spring·课程设计
matlabgoodboy1 小时前
Matlab代编电气仿真电力电子电机控制自动化新能源微电网储能能量
开发语言·matlab·自动化
镰圈量化1 小时前
当电脑上有几个python版本Vscode选择特定版本python
开发语言·vscode·python
宇寒风暖1 小时前
侯捷 C++ 课程学习笔记:内存管理与工具应用
c++·笔记·学习
背太阳的牧羊人1 小时前
RAG检索中使用一个 长上下文重排序器(Long Context Reorder) 对检索到的文档进行进一步的处理和排序,优化输出顺序
开发语言·人工智能·python·langchain·rag
ITPUB-微风2 小时前
美团MTSQL特性解析:技术深度与应用广度的完美结合
java·服务器·开发语言