深入理解C++关联式容器:set、multiset、map和multimap详解

序列式容器 与 关联式容器

我们知道:

  • C++ 中,我们将 vector、list、queue 这种底层为线性序列的数据结构叫做 序列式容器,其存储的就是元素本身
  • 关联式容器键-值对的形式存储数据。每个键在容器中必须是唯一的,而值则与相应的键关联。

键-值对

<key,value> 结构,在C++标准库中,std::pair 是一个模板类,其存储的就是键值对。其定义如下:

namespace std {
    template <class T1, class T2>
    struct pair {
        typedef T1 first_type;
        typedef T2 second_type;

        T1 first;
        T2 second;

        pair();
        pair(const T1& x, const T2& y);
        template<class U, class V> pair(const pair<U, V>& p);
    };
}

一般键是唯一且不可重复的标识符,用于查找和访问对应的值。值可以是任何数据类型,包括整数、字符串、对象等


树形结构的关联式容器

C++ 中,标准库提供了 std::map 、std::set 、std::multimap、std::multimap 四个基于红黑树实现的关联式容器。下面依次介绍其性质与使用:


std::set

Cplusplus 官网set文档

性质

上面的cpp文档详细的介绍了set这个容器,下面我们对其性质进行总结概括:

  1. 有序性set 中的元素按照其key值自动被排序。默认情况下使用元素的 < 操作符(小于)比较,也可以通过自定义比较函数来指定排序规则。插入 / 删除 新元素时,set 会自动确保元素的有序性,并且不会插入重复的元素。

  2. 唯一性:对于set,元素的值(value)即是键(key),而且每个value必须是唯一的。

  3. 不可修改性:set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。

  4. 底层实现:通常基于红黑树实现,因此插入、删除和查找操作的平均时间复杂度都是 O(log n)。

  5. 迭代器支持:set 提供了正向迭代器,可以用于遍历集合中的元素。

Attention:

  • set在创建时为std::set<value> myset;,只存放value,其底层上存放的是<value, value> 的键值对。实际实现中,只存储了键,而值部分为空。
  • 我们知道set中的元素不可直接修改,为什么?
    • 由于 std::set 是一种基于红黑树实现的关联容器,它要求元素在容器中保持有序性和唯一性。因此,直接修改元素可能会破坏红黑树的结构,导致容器不再符合要求。

使用

模板参数列表

下面是C++标准库中的 set 模板参数列表

template<
    class Key, // Key(键)类型
    class Compare = std::less<Key>, // 比较函数类型
    class Allocator = std::allocator<Key> // 分配器类型
> class set;

分别解释:

  • Keystd::set 中存储的元素的类型。set 中的元素按照键值自动被排序,并且每个元素在中都是唯一的
  • Compare可选的比较函数类型,用于指定元素的比较规则。默认情况下,使用 < 操作符进行比较。
  • 可选的分配器类型用于分配内存和管理容器中的元素

应用

数据去重std::set会自动对元素进行排序并移除重复的元素

std::vector<int> numbers = {4, 1, 2, 2, 3, 4};
std::set<int> uniqueNumbers(numbers.begin(), numbers.end());
// 现在uniqueNumbers中包含的是去重后的元素{1, 2, 3, 4}

快速查找std::set内部基于红黑树实现,因此查找操作非常高效,时间复杂度为O(log n)

std::set<std::string> names = {"Alice", "Bob", "Charlie", "David"};
if (names.find("Bob") != names.end())  // 查找Bob
{
    std::cout << "Bob 在set中" << std::endl;
}

迭代器的使用可以通过迭代器来访问集合中的元素

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};

    // 使用迭代器遍历 set 中的所有元素
    for (auto it = mySet.begin(); it != mySet.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 反向迭代器
    for (auto rit = mySet.rbegin(); rit != mySet.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    // 使用迭代器查找特定元素
    int target = 5;
    auto findIt = mySet.find(target);
    if (findIt != mySet.end()) {
        std::cout << target << "找到了" << std::endl;
    } else {
        std::cout << target << "没找到" << std::endl;
    }

    return 0;
}

std::map

cplusplus 官网文档

性质

上面的cpp文档详细的介绍了map这个容器,下面我们对其性质进行总结概括:

  1. 有序性map 中的元素按照key的大小顺序进行排序,默认情况下采用升序方式。这使得在 map 中进行范围遍历时,能够以键的顺序访问元素。

  2. 唯一键map 中的键是唯一的,如果插入一个已经存在的键,则会覆盖原有的键对应的值。

  3. 查找效率:由于 map 基于红黑树实现,对于元素的查找、插入和删除操作具有较高的效率,时间复杂度为 O(log n)。

  4. 键-值对映射:map 提供了键和值之间的映射关系,通过键可以快速查找对应的值,这使得 map 适合用于需要快速查找和检索值的场景。

Attention:

  • map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。

模板参数列表

template <class Key, class T, class Compare = std::less<Key>, class Allocator = std::allocator<std::pair<const Key, T>>>
class map;

这里是对每个模板参数的解释:

  • Key键的类型,用于唯一标识每个元素。

  • T值的类型,与键相关联的数据类型。

  • Compare可选参数,用于比较键的函数对象类型,默认情况下采用 std::less<Key>,表示采用 < 操作符进行键的比较。

  • Allocator可选参数,内存分配器的类型,默认情况下采用 std::allocator<std::pair<const Key, T>>,表示使用标准分配器

应用

我们下面用map进行一些应用:字典、计数器

字典

#include <iostream>
#include <map>
#include <string>

int main() {
	// 创建字典
    std::map<std::string, std::string> dictionary;

    // 添加词条到字典
    dictionary["apple"] = "苹果";
    dictionary["banana"] = "香蕉";
    dictionary["cat"] = "猫";

    // 查找词条
    std::string word = "";
    getline(word);
    if (dictionary.find(word) != dictionary.end()) {
        std::cout << word << ": " << dictionary[word] << std::endl;
    } else {
        std::cout << "字典中未查到该词" << std::endl;
    }

    return 0;
}

上面的代码中,我们使用map<string, string> 来模拟字典,key,value分别对应一个单词的英文和中文含义。

计数器

#include <iostream>
#include <map>

int main() {
    std::map<int, int> counter;

    // 统计数字出现的次数
    int numbers[] = {5, 3, 7, 5, 3, 9, 5};
    for (int num : numbers) {
        counter[num]++;
    }

    // 输出数字及其出现的次数
    for (const auto& pair : counter) {
        std::cout << pair.first << " occurs " << pair.second << " times" << std::endl;
    }

    return 0;
}

计数器通过将value值设为int类型,可以统计值为key的元素的出现个数。


std::multiset

cpluscplus官网文档

性质

multiset 的 模板参数列表 与 set 是相同的,但在性质上有所差别。

这里我们先看 多重集(Multiset)和集合(Set)之间 的区别

  1. 元素重复性multiset 允许元素重复出现,而 set 中每个元素都是唯一的。

  2. 插入操作:在 set 中,插入已经存在的元素会被忽略,因为元素不能重复;而在 multiset 中,可以插入重复的元素,每个元素将会被单独计数

  3. 查找操作:在 set 中,由于元素唯一,查找特定元素的速度较快;而在 multiset 中,由于元素可能重复,查找特定元素需要遍历多个相同元素

  4. 应用场景:set 适合用于需要保持元素唯一性的场景,比如存储不重复的关键字;而 multiset 则适用于需要统计元素重复次数的场景,比如统计数据集中各个元素的出现频率。

使用

当有以下情况时,我们会使用 multiset 而不是set:

  1. 需要存储重复元素的情况
  2. 不需要维护元素的唯一性
  3. 需要快速查找和删除重复元素

下面的代码展示了,multiset的使用:

int main() {
    std::multiset<int> numbers;

    // 插入元素
    numbers.insert(10);
    numbers.insert(20);
    numbers.insert(30);
    numbers.insert(20);  // 插入重复的元素

    // 输出元素
    std::cout << "该multiset包含: ";
    for (const auto& num : numbers) {
        std::cout << " " << num;
    }
    std::cout << std::endl;

    // 统计特定元素的重复次数
    int target = 20;
    int count = numbers.count(target);
    printf("值为%d的元素共有%d个\n", target, count);

    // 删除指定元素
    numbers.erase(target);

    // 再次输出元素
    printf("删除元素%d后,multiset共包含:", target);
    for (const auto& num : numbers) {
        std::cout << " " << num;
    }
    std::cout << std::endl;

    return 0;
}

代码输出结果:

在这里插入图片描述


std::multimap

cpluscplus官网文档

性质

这里我们主要关心 multimap 与 map 的区别(性质 及 操作):

  • 键的唯一性map中的键是唯一的,每个键只能对应一个值。而multimap允许多个键有相同的值,一个键可以对应多个值

  • 插入顺序: map按照键的大小顺序进行排序,并保持该顺序。multimap不对键进行排序,插入的顺序被保留

  • 查找操作在map中,由于键的唯一性,使用find()函数查找一个键时,如果找到了就返回该键对应的值,如果没找到则返回end()迭代器。而在multimap中,find()函数返回指向第一个匹配的键-值对的迭代器,如果没有匹配的键,则返回end()迭代器。

  • 删除操作: 在map中,使用erase()函数删除一个键时,如果该键存在,则删除该键对应的键-值对,并将其从容器中移除。而在multimap中,erase()函数会删除所有与给定键匹配的键-值对

总的来说,multimap 提供了一种允许重复键并自动排序的数据结构,适用于需要按照键进行检索且允许重复键的情况下使用。

应用

我们利用multimap 一个key可以对应多个value的特性,下面的示例使用 multimap 构建了一个学生表:

#include <iostream>
#include <map>
#include <string>

int main() {
    std::multimap<std::string, std::string> studentCourses;

    // 添加学生和他们的课程
    studentCourses.insert(std::make_pair("Alice", "Math"));
    studentCourses.insert(std::make_pair("Alice", "Physics"));
    studentCourses.insert(std::make_pair("Bob", "Chemistry"));
    studentCourses.insert(std::make_pair("Bob", "Biology"));
    studentCourses.insert(std::make_pair("Bob", "Math"));

    // 输出每个学生选修的课程
    for (const auto& pair : studentCourses) {
        std::cout << pair.first << " takes " << pair.second << std::endl;
    }

    return 0;
}

输出如下:

在这里插入图片描述

结尾

上面介绍的关联式容器,在做OJ题时也会经常用上,在理解其使用场景后做题,可以很好的运用它们。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值