#c

Set与Map容器专题

从基础到竞赛的关联容器应用技巧

#c
@鞠大龙
魅客科创中心

CONTENTS

目录

1. Set与Map基础概念

1.1 Set和Map的定义和特性

  • Set(集合)的定义:Set是一种存储唯一元素的容器,自动去重,元素自动排序

    • 遵循唯一性原则,不允许重复元素
    • 插入操作自动去重,查找操作快速高效
    • 元素按照特定顺序存储(Sorted类型)或无序存储(Hash类型)
  • Map(映射)的定义:Map是一种键值对存储结构,键唯一,值可以重复

    • 遵循键唯一性原则,每个键对应一个值
    • 通过键快速访问对应的值
    • 支持高效的插入、删除和查找操作
  • Set和Map的应用场景
    • 去重统计和唯一性检查
    • 快速查找和映射关系建立
    • 缓存系统和索引构建
    • 竞赛中的各种算法优化

1.2 两种实现方式的对比

Sorted类型(有序容器)

  • std::set / std::map
  • 基于红黑树实现
  • 元素自动排序存储
  • 时间复杂度:
  • 支持范围查询和有序遍历

适用场景

  • 需要有序性的数据
  • 范围查询操作
  • 获取最小/最大元素
  • 稳定的性能要求

Hash类型(无序容器)

  • std::unordered_set / std::unordered_map
  • 基于哈希表实现
  • 元素无序存储
  • 平均时间复杂度:
  • 快速访问,不支持有序操作

适用场景

  • 大量数据的快速查找
  • 频繁的插入删除操作
  • 只需要判断存在性
  • 对顺序没有要求

1.3 为什么需要Set和Map?

  • 传统数组的局限性

    • 查找效率低:
    • 需要手动维护顺序
    • 无法快速判断元素是否存在
    • 去重操作复杂
  • Set和Map的优势

    • 快速查找:
    • 自动去重和排序
    • 简化代码逻辑
    • 支持复杂的数据关系
  • 竞赛中的重要性
    • 算法优化的关键工具
    • 简化复杂问题的解决方案
    • 提高代码的可读性和维护性

2. Sorted类型详解

2.1 std::set基础操作

基本操作示例

#include <set>
#include <iostream>
using namespace std;

int main() {
    set<int> s;
    
    // 插入元素
    s.insert(3);
    s.insert(1);
    s.insert(4);
    s.insert(1);  // 重复元素,不会插入
    
    // 遍历(自动排序)
    for (int x : s) {
        cout << x << " ";  // 输出:1 3 4
    }
    
    // 查找元素
    if (s.find(3) != s.end()) {
        cout << "找到3";
    }
    
    return 0;
}

常用操作总结

  • 插入insert(element)
  • 删除erase(element)
  • 查找find(element)
  • 大小size()
  • 判空empty()
  • 清空clear()

高级操作

  • 范围查询lower_bound(), upper_bound()
  • 边界元素begin(), rbegin()
  • 元素计数count(element)

2.2 std::map基础操作

基本操作示例

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<string, int> m;
    
    // 插入键值对
    m["Alice"] = 95;
    m["Bob"] = 88;
    m["Charlie"] = 92;
    
    // 遍历(按键排序)
    for (auto& p : m) {
        cout << p.first << ": " 
             << p.second << endl;
        // 输出:Alice: 95, Bob: 88, Charlie: 92
    }
    
    // 查找键
    if (m.find("Alice") != m.end()) {
        cout << "Alice的成绩:" << m["Alice"] << endl;
    }
    
    return 0;
}

常用操作总结

  • 插入insert({key, value})map[key] = value
  • 访问map[key]map.at(key)
  • 删除erase(key)
  • 查找find(key)
  • 大小size()
  • 判空empty()

高级特性

  • 自动排序:按键的字典序排序
  • 范围查询:支持按键的范围操作
  • 迭代器:支持正向和反向迭代

2.3 红黑树特性分析

  • 有序性特性

    • 元素自动按升序排列
    • 支持范围查询:lower_bound(), upper_bound()
    • 可以获取最小/最大元素:begin(), rbegin()
    • 支持有序遍历和范围操作
  • 稳定性特性

    • 插入、删除、查找的时间复杂度稳定
    • 不受数据分布影响
    • 保证性能
    • 适合对性能稳定性要求高的场景
  • 内存效率
    • 红黑树需要额外的指针空间
    • 每个节点需要存储颜色信息和父子指针
    • 相比哈希表有更高的内存开销

3. Hash类型详解

3.1 std::unordered_set基础操作

基本操作示例

#include <unordered_set>
#include <iostream>
using namespace std;

int main() {
    unordered_set<int> us;
    
    // 插入元素
    us.insert(3);
    us.insert(1);
    us.insert(4);
    us.insert(1);  // 重复元素,不会插入
    
    // 遍历(无序)
    for (int x : us) {
        cout << x << " ";  // 顺序不确定
    }
    
    // 查找元素(平均O(1))
    if (us.find(3) != us.end()) {
        cout << "找到3";
    }
    
    return 0;
}

常用操作总结

  • 插入insert(element)
  • 删除erase(element)
  • 查找find(element)
  • 大小size()
  • 判空empty()
  • 清空clear()

性能特点

  • 平均时间复杂度
  • 最坏情况(哈希冲突严重时)
  • 内存使用:相对较低
  • 遍历顺序:不确定

适用场景

  • 大量数据的快速查找
  • 频繁的插入删除操作
  • 只需要判断存在性

3.2 std::unordered_map基础操作

基本操作示例

#include <unordered_map>
#include <iostream>
using namespace std;

int main() {
    unordered_map<string, int> um;
    
    // 插入键值对
    um["Alice"] = 95;
    um["Bob"] = 88;
    um["Charlie"] = 92;
    
    // 遍历(无序)
    for (auto& p : um) {
        cout << p.first << ": " 
             << p.second << endl;
        // 顺序不确定
    }
    
    // 查找键(平均O(1))
    if (um.find("Alice") != um.end()) {
        cout << "Alice的成绩:" << um["Alice"] << endl;
    }
    
    return 0;
}

常用操作总结

  • 插入insert({key, value})um[key] = value
  • 访问um[key]um.at(key)
  • 删除erase(key)
  • 查找find(key)
  • 大小size()
  • 判空empty()

哈希表特性

  • 快速访问:平均时间复杂度
  • 无序性:元素存储顺序不确定
  • 负载因子:影响哈希表的性能
  • 重新哈希:当负载因子过高时自动进行

3.3 哈希冲突处理机制

  • 哈希冲突的概念

    • 不同键值经过哈希函数计算得到相同的哈希值
    • 是哈希表不可避免的问题
    • 影响哈希表的性能
  • 常见的冲突解决方法

    • 链地址法:每个桶使用链表存储冲突元素
    • 开放地址法:在哈希表中寻找下一个空位置
    • 再哈希法:使用第二个哈希函数重新计算
  • C++中的实现
// 自定义哈希函数示例
struct MyHash {
    size_t operator()(const string& s) const {
        return hash<string>()(s);
    }
};

unordered_set<string, MyHash> custom_set;
  • 性能影响因素
    • 哈希函数的质量
    • 负载因子的大小
    • 数据分布的特性
    • 冲突解决策略的效率

4. 应用场景对比

4.1 性能对比分析

操作 Sorted类型 Hash类型 选择建议
插入 平均 大数据量选Hash
删除 平均 频繁操作选Hash
查找 平均 快速查找选Hash
遍历 两者相当
最小/最大 不支持 需要有序选Sorted
范围查询 支持 不支持 需要范围选Sorted

4.2 选择原则总结

选择Sorted类型的场景

  • 需要维护有序数据集合
  • 需要进行范围查询操作
  • 需要获取最小/最大元素
  • 需要稳定的性能表现
  • 数据量相对较小

具体应用示例

  • 排行榜维护和更新
  • 区间统计和范围操作
  • 有序去重和数据整理
  • 需要遍历有序数据的场景

选择Hash类型的场景

  • 大量数据的快速查找需求
  • 频繁的插入删除操作
  • 只需要判断元素存在性
  • 对数据顺序没有要求
  • 大数据量下的性能优化

具体应用示例

  • 缓存系统和快速查询
  • 词频统计和去重操作
  • 图论算法中的节点标记
  • 需要时间复杂度的场景

4.3 实际竞赛中的选择策略

  • CESE竞赛特点

    • 数据规模中等,对性能要求适中
    • 经常需要有序操作和范围查询
    • 推荐:优先使用Sorted类型
  • USACO竞赛特点

    • 数据规模较大,性能要求高
    • 经常需要快速查找和统计
    • 推荐:根据具体题目选择,大数据量用Hash
  • CSP-J/S竞赛特点
    • 题目难度适中,数据规模可控
    • 注重基础算法和数据结构
    • 推荐:掌握两种类型,根据题意选择

5. 经典问题解析

5.1 CESE题目:去重统计

问题描述
给定n个整数,统计去重后有多少个不同的数。

输入格式
第一行一个整数n,第二行n个整数

输出格式
不同整数的个数

解题思路
使用set自动去重,然后输出size

#include <iostream>
#include <set>
using namespace std;

int main() {
    int n, x;
    set<int> s;
    
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> x;
        s.insert(x);
    }
    
    cout << s.size() << endl;
    return 0;
}

5.2 USACO题目:词频统计

问题描述
统计一篇文章中每个单词出现的次数。

输入格式
多行文本,以EOF结束

输出格式
每个单词及其出现次数,按字典序输出

解题思路
使用map存储单词到次数的映射

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> wordCount;
    string word;
    
    while (cin >> word) {
        wordCount[word]++;
    }
    
    for (auto& p : wordCount) {
        cout << p.first << ": " << p.second << endl;
    }
    
    return 0;
}

5.3 POJ题目:最近公共祖先查询

问题描述
多次查询两个节点的最近公共祖先。

解题思路
使用map存储每个节点的父节点信息

#include <iostream>
#include <map>
#include <set>
using namespace std;

map<int, int> parent;  // 子节点->父节点映射

int findLCA(int a, int b) {
    set<int> path;
    
    // 记录a到根节点的路径
    while (a != 0) {
        path.insert(a);
        a = parent[a];
    }
    
    // 找到b路径上与a路径的交点
    while (b != 0) {
        if (path.find(b) != path.end()) {
            return b;
        }
        b = parent[b];
    }
    
    return 0;
}

5.4 HDU题目:缓存优化

问题描述
实现LRU缓存机制,支持get和put操作。

解题思路
使用unordered_map快速查找,结合双向链表维护访问顺序

#include <unordered_map>
#include <list>
using namespace std;

class LRUCache {
private:
    int capacity;
    list<pair<int, int>> cache;
    unordered_map<int, list<pair<int, int>>::iterator> map;
    
public:
    LRUCache(int capacity) : capacity(capacity) {}
    
    int get(int key) {
        if (map.find(key) == map.end()) return -1;
        
        // 移动到链表头部
        cache.splice(cache.begin(), cache, map[key]);
        return map[key]->second;
    }
    
    void put(int key, int value) {
        if (map.find(key) != map.end()) {
            // 更新值并移动到头部
            map[key]->second = value;
            cache.splice(cache.begin(), cache, map[key]);
        } else {
            // 插入新元素
            if (cache.size() == capacity) {
                // 删除尾部元素
                map.erase(cache.back().first);
                cache.pop_back();
            }
            cache.emplace_front(key, value);
            map[key] = cache.begin();
        }
    }
};

5.5 CSP-J/S题目:表达式求值

问题描述
实现支持变量的表达式求值。

解题思路
使用map存储变量名到值的映射

#include <iostream>
#include <map>
#include <string>
#include <sstream>
using namespace std;

map<string, int> variables;

int evaluate(const string& expr) {
    istringstream iss(expr);
    string token;
    int result = 0;
    char op = '+';
    
    while (iss >> token) {
        if (token == "+" || token == "-" || token == "*" || token == "/") {
            op = token[0];
        } else {
            int value;
            if (isdigit(token[0])) {
                value = stoi(token);
            } else {
                value = variables[token];
            }
            
            switch (op) {
                case '+': result += value; break;
                case '-': result -= value; break;
                case '*': result *= value; break;
                case '/': result /= value; break;
            }
        }
    }
    
    return result;
}

6. 练习题推荐

6.1 基础练习题

题目1:数组去重

  • 要求:使用set实现数组去重功能
  • 输入:包含重复元素的数组
  • 输出:去重后的有序数组
  • 难度:★☆☆☆☆

题目2:词频统计

  • 要求:使用map统计文本中单词频率
  • 输入:一段英文文本
  • 输出:单词及其出现次数(按字典序)
  • 难度:★☆☆☆☆

题目3:缓存模拟

  • 要求:使用unordered_map实现简单缓存
  • 功能:支持get和put操作
  • 限制:缓存大小固定
  • 难度:★★☆☆☆

6.2 进阶练习题

题目4:最近公共祖先

  • 要求:结合map和set实现LCA查询
  • 输入:树结构和查询对
  • 输出:每对节点的LCA
  • 难度:★★★☆☆

题目5:表达式求值

  • 要求:使用map支持变量表达式
  • 功能:支持变量赋值和表达式计算
  • 扩展:支持括号和优先级
  • 难度:★★★☆☆

题目6:LRU缓存

  • 要求:结合unordered_map和链表实现
  • 功能:完整的LRU缓存机制
  • 性能:所有操作O(1)时间复杂度
  • 难度:★★★★☆

7. 总结与提高

7.1 关键知识点回顾

Sorted类型核心要点

  • 基于红黑树实现,元素有序存储
  • 所有操作时间复杂度稳定为
  • 支持范围查询和有序遍历
  • 适合需要有序性的场景

Hash类型核心要点

  • 基于哈希表实现,访问快速
  • 平均时间复杂度为
  • 元素无序存储,不支持有序操作
  • 适合大数据量的快速访问

选择原则总结

  1. 需要有序性 → 选择Sorted类型
  2. 需要快速访问 → 选择Hash类型
  3. 数据量小 → 两者差异不大
  4. 数据量大 → 优先考虑Hash类型
  5. 需要范围查询 → 只能选择Sorted类型

7.2 学习建议和提高路径

基础知识掌握

  • 熟练掌握两种容器的基本操作
  • 理解底层实现原理
  • 掌握时间复杂度分析
  • 练习常见的应用场景

竞赛能力提升

  • 分析题目需求,正确选择容器
  • 掌握性能优化技巧
  • 学习高级特性和用法
  • 积累实战经验

实践项目推荐

  • 实现简单的数据库索引
  • 开发缓存系统组件
  • 完成算法竞赛题目
  • 参与开源项目贡献

进阶学习方向

  • 学习更复杂的数据结构
  • 掌握多容器组合使用
  • 了解并发容器和线程安全
  • 学习自定义哈希函数

7.3 参考资料和资源

在线文档资源

  • C++ Reference: cppreference.com
  • C++ STL官方文档
  • 算法竞赛专题网站

竞赛平台推荐

  • CESE竞赛平台
  • USACO训练平台
  • POJ在线评测
  • HDU OJ
  • CSP-J/S官方平台
Set与Map容器专题

#c

这个讲义使用了Awesome Marp主题的蓝色风格。 内容针对竞赛选手设计,注重基础概念和实际应用。