在日常编程中,我们经常需要根据某个特定的标识来快速查找和检索数据。比如,根据学号查找学生信息、根据商品编号获取价格、或者判断某个用户名是否已经存在。这种基于键进行高效查找的需求,正是关联容器设计的初衷。
关联容器支持基于键的高效查找和检索操作。与顺序容器不同,关联容器中元素的位置不是由它们在容器中的物理位置决定,而是由元素的键值决定。这种设计使得我们能够快速定位和访问特定的元素,而不需要遍历整个容器。

C++标准库提供了八种关联容器,它们在三个维度上有所不同:
set类型只存储键,而map类型存储键值对,其中键作为索引,值表示与该索引关联的数据。有序关联容器使用比较函数来维护元素的顺序,这类容器包括map(关联数组,存储键值对)、set(键集合)、multimap(允许重复键的map)和multiset(允许重复键的set)。它们的头文件分别是map和set。
无序关联容器使用哈希函数来组织元素,包括unordered_map、unordered_set、unordered_multimap和unordered_multiset。这些容器定义在unordered_map和unordered_set头文件中。
虽然大多数程序员都熟悉向量和链表这样的数据结构,但很多人可能没有使用过关联数据结构。在深入了解这些容器的技术细节之前,让我们先通过实际例子来感受它们的用法和威力。
map容器本质上是一个键值对的集合。我们可以把它想象成一个高效的"字典",其中每个条目都有一个唯一的"词条"(键)和对应的"释义"(值)。这种数据结构通常被称为关联数组,它像普通数组一样支持下标访问,但下标不必是整数,而可以是任何类型的键。
让我们通过一个学生成绩管理的例子来理解map的使用。假设我们需要统计每门课程的选课人数:
map<string, size_t> courseCount;
string courseName;
while (cin >> courseName) {
++courseCount[courseName];
}
// 输出统计结果
for (const auto &course : courseCount) {
cout << course.first << ":" << course.second
<< "人选课" << endl;
}这个程序读取课程名称,并统计每门课程出现的次数。当我们使用courseCount[courseName]时,如果该课程名称还不在map中,下标运算符会自动创建一个新的条目,键为课程名称,值初始化为0。无论元素是否已存在,我们都可以对其值进行递增操作。
当我们遍历map时,每个元素都是一个pair对象。pair的first成员保存键(课程名称),second成员保存值(选课人数)。需要注意的是,有序容器会按照键的字典序输出结果。
set容器就是键的集合,它最适合用来判断某个值是否存在。比如在文本处理中,我们可能需要过滤掉一些常见的停用词。
让我们扩展前面的例子,假设我们要统计课程选课人数,但需要排除一些无效的课程代码:
map<string, size_t> courseCount;
set<string> invalidCodes = {"TMP", "TEST", "DEBUG", "TEMP",
"NULL", "EMPTY", "INVALID"};
string courseName;
while (cin >> courseName) {
// 只统计有效的课程代码
if (invalidCodes.find(courseName) == invalidCodes.end()) {
在这个改进的版本中,我们首先创建了一个包含无效课程代码的set。然后在统计之前,我们使用find方法检查当前的课程名称是否在无效代码集合中。find方法返回一个迭代器:如果找到了目标元素,迭代器指向该元素;如果没找到,则返回end()迭代器。只有当课程代码不在无效集合中时,我们才进行统计。
关联容器的定义方式与顺序容器类似,但有一些重要的区别。对于map类型,我们必须同时指定键和值的类型;对于set类型,我们只需要指定键的类型。
map<string, int> studentGrades; // 空的成绩映射
// 使用列表初始化
set<string> departments = {"计算机", "数学", "物理", "化学"};
// 初始化教师信息映射
map<string, string> teachers = {
{"张教授", "计算机系"},
{"李教授", "数学系"},
初始化map时,我们需要将每个键值对包装在花括号中,形成{key, value}的格式。键是第一个元素,值是第二个元素。
标准的map和set要求键必须唯一,但有时我们需要允许重复的键。比如在图书管理系统中,同一个作者可能写了多本书,这时我们就需要使用multimap:
vector<int> numbers;
for (int i = 0; i < 10; ++i) {
numbers.push_back(i % 5); // 创建包含重复值的序列
numbers.push_back(i % 5);
}
set<int> uniqueNumbers(numbers.cbegin(), numbers.cend
这个例子展示了set和multiset的区别:set自动去除重复元素,只保留唯一值;而multiset保留所有元素,包括重复的。
有序关联容器对键类型有特殊要求:键类型必须定义一种比较元素的方法。默认情况下,标准库使用键类型的<运算符来比较键。这意味着键类型必须支持严格弱序关系,这种关系具有以下特性:
两个键不能同时"小于"对方;如果键A小于键B,且键B小于键C,那么键A必须小于键C;如果两个键互不小于对方,我们称它们"等价"。
在实际应用中,大多数内置类型和标准库类型都满足这些要求。但当我们使用自定义类型作为键时,可能需要提供自定义的比较函数。
假设我们有一个表示学生的类,我们希望按照学号对学生进行排序:
struct Student {
string id;
string name;
int grade;
};
bool compareById(const Student &lhs, const Student &rhs) {
return lhs.id < rhs.id;
}
// 使用自定义比较函数的multiset
multiset<Student, decltype(compareById)*> studentSet(compareById);这里我们定义了一个比较函数compareById,然后在定义multiset时指定了这个函数的类型。我们使用decltype来获取函数类型,并加上*表示这是一个函数指针类型。
在深入学习关联容器的操作之前,我们需要了解pair类型。pair是一个模板类型,定义在utility头文件中,它可以保存两个数据成员。
pair<string, int> studentInfo; // 保存学生姓名和年龄
pair<string, vector<int>> courseScores; // 保存课程名和成绩列表
// 使用初始化列表创建pair
pair<string, string> authorBook{"鲁迅", "朝花夕拾"};pair的两个数据成员是公开的,分别命名为first和second。我们可以直接访问这些成员:
cout << "作者:" << authorBook.first << endl;
cout << "作品:" << authorBook.second << endl;标准库还提供了make_pair函数来创建pair对象:
auto bookInfo = make_pair("红楼梦", 1200); // 自动推断类型当我们需要从函数返回两个值时,pair特别有用:
pair<bool, string> validateUser(const string &username) {
if (username.empty()) {
return {false, "用户名不能为空"};
}
if (username.length() < 3) {
return {false, "用户名长度至少3个字符"};
}
return {true
关联容器的迭代器具有一些特殊性质。对于map类型,解引用迭代器得到的是一个pair对象,其中first成员是const键,second成员是对应的值:
map<string, int> examScores = {{"张三", 85}, {"李四", 92}, {"王五", 78}};
auto mapIter = examScores.begin();
cout << "学生:" << mapIter->first << endl; // 输出键
cout << "成绩:" << mapIter->second << endl; // 输出值
对于set类型,虽然定义了iterator和const_iterator类型,但两者都只提供只读访问。这是因为set中的键也是const的:
set<int> numbers = {1, 3, 5, 7, 9};
set<int>::iterator setIter = numbers.begin();
if (setIter != numbers.end()) {
// *setIter = 42; // 错误!set中的键是只读的
cout << *setIter << endl; // 正确:可以读取键
}当我们遍历关联容器时,元素按照键的升序返回:
map<string, int> scores = {{"物理", 88}, {"数学", 95}, {"化学", 82}};
for (const auto &subject : scores) {
cout << subject.first << ": " << subject.second << "分" << endl;
}
// 输出顺序:化学、数学、物理(按字典序)关联容器提供了多种添加元素的方法。insert成员函数是最通用的方法:
set<string> courses;
courses.insert("数据结构");
courses.insert("算法分析");
// 使用迭代器范围插入
vector<string> newCourses = {"操作系统", "数据库", "网络原理"};
courses.insert(newCourses.begin(), newCourses.end());
// 使用初始化列表插入
courses.insert({对于map类型,我们需要插入键值对。有多种方法可以创建这样的pair对象:
map<string, int> inventory;
// 使用花括号初始化(推荐)
inventory.insert({"苹果", 50});
// 使用make_pair
inventory.insert(make_pair("香蕉", 30));
// 显式构造pair
inventory.insert(pair<string, int>("橙子", 25));
// 使用value_type
inventory.检测插入结果
对于要求唯一键的容器,insert操作返回一个pair,告诉我们插入是否成功。返回的pair的first成员是指向具有给定键的元素的迭代器,second成员是一个布尔值,指示元素是否被插入:
map<string, size_t> wordCount;
string word = "hello";
auto result = wordCount.insert({word, 1});
if (!result.second) { // 如果插入失败(元素已存在)
++result.first->second; // 递增计数器
}这种模式在计数程序中很常见。我们尝试插入一个新的键值对,如果键已经存在,就增加对应的计数。
多重容器的插入
对于允许重复键的容器,insert总是成功插入元素,并返回指向新元素的迭代器:
multimap<string, string> authorBooks;
authorBooks.insert({"金庸", "射雕英雄传"});
authorBooks.insert({"金庸", "神雕侠侣"}); // 总是成功插入
authorBooks.insert({"金庸", "倚天屠龙记"});关联容器提供了几种删除元素的方法。我们可以通过迭代器删除单个元素或一个范围的元素:
map<string, int> scores = {{"数学", 95}, {"物理", 88}, {"化学", 92}};
// 删除指定位置的元素
auto iter = scores.find("物理");
if (iter != scores.end()) {
scores.erase(iter);
}
// 删除指定键的所有元素
size_t
对于唯一键容器,erase返回0或1,表示是否找到并删除了元素。对于多重键容器,返回值可能大于1:
multimap<string, string> phoneBook;
phoneBook.insert({"张三", "13800000001"});
phoneBook.insert({"张三", "13800000002"});
size_t removed = phoneBook.erase("张三");
cout << "删除了" << removed << "个电话号码" << endl; // 输出:删除了2个电话号码map和unordered_map支持下标操作,这是它们独有的特性。下标操作接受一个键,返回关联的值。但是要特别注意:如果键不存在,下标操作会自动插入一个新元素:
map<string, int> studentAges;
studentAges["张三"] = 20; // 插入新元素
studentAges["李四"] = 21;
cout << studentAges["张三"] << endl; // 输出:20
cout << studentAges["王五"] << endl; // 输出:0(自动插入并值初始化)
// 现在map中有三个元素了
cout << "学生总数:" << studentAges.由于下标操作可能插入新元素,我们只能对非const的map使用下标操作。下标操作返回的是mapped_type对象(即值类型),而解引用map迭代器得到的是value_type对象(即pair类型)。
如果我们只想检查某个键是否存在而不想插入新元素,应该使用find或count:
if (studentAges.find("赵六") != studentAges.end()) {
cout << "找到赵六的年龄:" << studentAges["赵六"] << endl;
} else {
cout << "赵六不在记录中" << endl;
}关联容器提供了多种查找元素的方法。find是最基本的查找操作,它返回指向找到元素的迭代器,如果没找到则返回end()迭代器:
set<string> validUsers = {"admin", "user1", "user2", "guest"};
string username = "user1";
auto found = validUsers.find(username);
if (found != validUsers.end()) {
cout << "用户" << username << "验证通过" << endl;
}
count方法返回具有给定键的元素数量。对于唯一键容器,结果只能是0或1;对于多重键容器,结果可能大于1:
cout << validUsers.count("admin") << endl; // 输出:1
cout << validUsers.count("hacker") << endl; // 输出:0在多重键容器中查找元素更加复杂,因为可能有多个元素具有相同的键。标准库提供了几种方法来处理这种情况。最直观的方法是结合使用find和count:
multimap<string, string> courseTeachers;
courseTeachers.insert({"数据结构", "张教授"});
courseTeachers.insert({"数据结构", "李教授"});
courseTeachers.insert({"算法分析", "王教授"});
string course = "数据结构";
auto teacherCount = courseTeachers.count(course);
auto iter = courseTeachers.
更高效的方法是使用lower_bound和upper_bound:
auto lowerIter = courseTeachers.lower_bound(course);
auto upperIter = courseTeachers.upper_bound(course);
for (auto iter = lowerIter; iter != upperIter; ++iter) {
cout << course << "由" << iter->second << "授课" << endl;
}lower_bound返回指向第一个不小于给定键的元素的迭代器,upper_bound返回指向第一个大于给定键的元素的迭代器。这两个迭代器构成一个范围,包含所有具有给定键的元素。
最直接的方法是使用equal_range,它返回一个pair,包含lower_bound和upper_bound的结果:
auto range = courseTeachers.equal_range(course);
for (auto iter = range.first; iter != range.second; ++iter) {
cout << course << "由" << iter->second << "授课" << endl;
}为了展示关联容器在实际编程中的应用,让我们构建一个文本转换程序。这个程序读取两个文件:第一个文件包含转换规则,每条规则指定一个单词及其替换内容;第二个文件包含要转换的文本。
假设我们的转换规则文件包含:
很好 excellent
不错 good
一般 okay
较差 poor
很差 terrible要转换的文本是:
这次考试成绩很好
那个项目做得不错
这个方案一般
服务质量较差程序应该输出:
这次考试成绩excellent
那个项目做得good
这个方案okay
服务质量poor我们的解决方案包含三个函数:transformText管理整体流程,buildTransformMap构建转换映射,applyTransform执行单词转换。
void transformText(ifstream &ruleFile, ifstream &inputFile) {
auto transformMap = buildTransformMap(ruleFile);
string line;
while (getline(inputFile, line)) {
istringstream lineStream(line);
string word;
bool firstWord = true;
while (lineStream >> word) {
if (firstWord) {
这个函数首先调用buildTransformMap构建转换规则映射,然后逐行读取输入文件。对于每一行,我们使用istringstream将其分解为单词,并对每个单词应用转换规则。
map<string, string> buildTransformMap(ifstream &ruleFile) {
map<string, string> transformMap;
string key, value;
while (ruleFile >> key && getline(ruleFile, value)) {
if (value.size() > 1) {
transformMap[key] = value.substr(1); // 跳过前导空格
这个函数读取规则文件,每条规则包含一个要转换的单词和对应的替换文本。我们使用>>读取第一个单词作为键,然后用getline读取该行的剩余部分作为值。由于getline不会跳过前导空格,我们需要使用substr(1)去掉键和值之间的空格。
const string& applyTransform(const string &word,
const map<string, string> &transformMap) {
auto foundIter = transformMap.find(word);
if (foundIter != transformMap.cend()) {
return foundIter->second;
} else {
return word;
}
转换函数接受一个单词和转换映射,返回转换后的结果。我们使用find查找单词是否在映射中存在,如果存在就返回对应的转换结果,否则返回原始单词。
C++11引入的无序关联容器使用哈希函数而不是比较操作来组织元素。当键类型没有明显的排序关系,或者当维护元素顺序的代价过高时,无序容器是理想的选择。
无序容器提供与有序容器相同的操作接口,这意味着我们可以很容易地在两者之间切换:
// 使用无序map重写词频统计程序
unordered_map<string, size_t> wordFreq;
string word;
while (cin >> word) {
++wordFreq[word];
}
for (const auto &entry : wordFreq) {
cout << entry.first << " 出现 " << entry.second << " 次" << endl;
}这个程序与使用有序map的版本几乎完全相同,唯一的区别是容器类型声明。但是输出的顺序会有所不同:有序容器按照键的字典序输出,而无序容器的输出顺序是不确定的。
虽然哈希容器在理论上具有更好的平均性能,但在实践中获得良好的性能往往需要仔细的性能测试和调优。通常,除非有明确的性能需求或键类型本身无法排序,否则使用有序容器往往更容易获得稳定的性能。
无序容器将元素组织在一系列桶中,每个桶包含零个或多个元素。容器使用哈希函数将元素映射到桶。为了访问元素,容器首先计算元素的哈希码,然后在对应的桶中搜索元素。
理想情况下,哈希函数会将每个特定值映射到唯一的桶。但是,允许不同的键映射到相同的桶。当一个桶包含多个元素时,需要顺序搜索来找到目标元素。
unordered_map<string, int> studentIds;
// 添加一些数据...
// 检查容器的哈希性能
cout << "桶的数量:" << studentIds.bucket_count() << endl;
cout << "负载因子:" << studentIds.load_factor() << endl;
cout << "最大负载因子:" << studentIds.max_load_factor() << endl;
// 如果负载因子过高,可以主动重新哈希
if (studentIds.load_factor()
对于内置类型和标准库类型(如string),标准库已经提供了哈希函数。但是对于自定义类型,我们需要提供自己的哈希函数和相等比较操作:
struct Student {
string id;
string name;
bool operator==(const Student &other) const {
return id == other.id;
}
};
// 自定义哈希函数
struct StudentHash {
size_t operator()(const Student &s) const {
或者,我们可以使用函数来定义哈希和相等操作:
size_t studentHasher(const Student &s) {
return hash<string>()(s.id);
}
bool studentEqual(const Student &lhs, const Student &rhs) {
return lhs.id == rhs.id;
}
unordered_multiset<Student, decltype(studentHasher)*, decltype
通过理解和掌握关联容器,我们能够编写更加高效和优雅的程序。这些容器不仅提供了快速的查找和检索能力,还通过其丰富的接口支持各种复杂的数据操作需求。在实际开发中,选择合适的关联容器类型往往能够显著提升程序的性能和可维护性。
编写一个程序,使用map存储学生的姓名和成绩,然后实现查找和显示所有学生的功能。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> scores; // 姓名 -> 成绩
// 添加学生成绩
scores["张三"] = 85;
scores["李四"] = 90;
scores["王五"] = 78;
编写一个程序,使用multimap存储学生的姓名和课程,一个学生可以选择多门课程。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
multimap<string, string> courses; // 姓名 -> 课程
// 添加选课信息
courses.insert({"张三", "数学"});
courses.insert({"张三", "英语"});
courses.insert({
编写一个程序,使用set存储一组不重复的数字,然后检查某个数字是否存在。
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> numbers;
// 添加数字(set自动去重)
numbers.insert(10);
numbers.insert(20);
numbers.insert(30);
numbers.insert(20); // 重复,不会被添加
numbers.
编写一个程序,统计一段文本中每个单词出现的次数,并输出统计结果。
#include <iostream>
#include <map>
#include <string>
#include <sstream>
using namespace std;
int main() {
string text = "hello world hello cpp world cpp";
map<string, int> wordCount;
// 将文本分割成单词
istringstream iss(text);
string word;
while (iss >> word) {
wordCount[word]
实现一个简单的电话簿程序,支持添加联系人、查找电话号码和显示所有联系人。
#include <iostream>
#include <map>
#include <string>
using namespace std;
class PhoneBook {
private:
map<string, string> contacts; // 姓名 -> 电话号码
public:
// 添加联系人
void addContact(const string& name, const string& phone) {
contacts[name] =
map是关联容器,存储键值对,根据键自动排序。使用find()方法查找元素,如果找到返回指向该元素的迭代器,否则返回end()。遍历map时,pair.first是键,pair.second是值。
multimap允许一个键对应多个值。使用equal_range()可以获取所有匹配某个键的元素范围,返回一对迭代器。insert()方法用于插入元素。当需要一对多关系时,multimap是很好的选择。
set存储不重复的元素,并自动排序。使用insert()添加元素,如果元素已存在则不会重复添加。find()方法查找元素,返回指向找到元素的迭代器,如果没找到返回end()。
使用map统计单词出现次数非常方便。wordCount[word]++这行代码会自动处理:如果单词不存在,会创建新条目并初始化为0,然后自增;如果已存在,直接自增。map的键自动排序,所以输出结果也是有序的。
这个例子展示了如何使用map实现一个简单的电话簿。map非常适合存储键值对关系,查找效率高(O(log n)),并且自动按键排序。在实际应用中,如果需要一个人对应多个电话号码,可以使用multimap或map<string, vector<string>>。