CC++ & Algorithm

pair对组:像双人搭档一样的数据组合

困难2
语言版本:通用
概述:学习C++中的pair对组,它可以把两个不同或相同类型的值捆绑在一起,就像双人自行车上的搭档,让数据管理更简单。

成对数据的好搭档:认识C++ pair对组

你有没有遇到过这样的场景:老师让你记录每位同学的姓名和语文成绩,或者你玩游戏时要存储每个角色的生命值和攻击力,又或者你和朋友一起参加双人跳绳,需要记录两个人的名字和总得分?这些数据天然就是“成对”出现的。在C++中,有一个非常简单又强大的工具叫做 pair(对组),它能把两个数据捆绑在一起,就像双人自行车上的搭档一样,永远不分家。

1. 生活中的双人搭档——pair的直观理解

想象一下,你有一个好朋友叫“小红”,你们经常一起参加学校的双人比赛。无论是跳绳比赛、朗诵比赛还是科学实验,你们俩总是“绑定”在一起:你的名字和他(她)的名字、你们的得分、你们的排名……这些数据总是成对出现。在编程世界中,也有一种数据结构专门用来把两个数据“绑”在一起,它的名字叫 pair(对组)。

比如,你的名字是“小明”,你的好朋友叫“小红”,你们在数学竞赛中分别得了95分和90分。如果要把“小明-95”和“小红-90”这两组数据存储下来,或者要记录“小明和小红”这对组合的整体得分,pair就是最合适的工具。它就像一个双人背包,两个口袋可以装两种东西,但是它们永远在一起。

更具体地说,pair就像是一个简易的“双人座位”:第一个座位(first)放第一个数据,第二个座位(second)放第二个数据。你可以把任何两个东西放进去,不管它们是不是同一种类型。

2. pair的原理与使用方法——快速上手

pair 是C++标准模板库(STL)中的一个 工具组件,定义在 <utility> 头文件中。它非常简单:就是一个包含两个元素的“结构体”,这两个元素分别叫做 firstsecond。你可以把 firstsecond 想象成双人自行车的两个座位:第一个座位放第一样东西,第二个座位放第二样东西。

核心特点

  • 可以存储两个不同类型或相同类型的值。
  • 有默认的构造函数,也可以直接初始化。
  • 支持比较操作(先比较first,再比较second),常用于排序。
  • 可以和容器(如vector、map)配合使用。

2.1 创建pair的几种方法

就像你可以用不同方式交朋友一样,创建pair也有很多种方法:

#include <iostream>
#include <utility>   // 使用pair必须包含此头文件

using namespace std;

int main() {
    // 方法1: 声明并直接用花括号初始化(C++11开始支持)
    pair<string, int> student1 = {"Alice", 95};  // 学生的名字和成绩

    // 方法2: 使用make_pair函数(自动推导类型)
    pair<string, int> student2 = make_pair("Bob", 88);  // 第二个学生

    // 方法3: 先声明,再分别赋值
    pair<string, int> student3;      // 先声明一个空对组
    student3.first = "Cathy";        // 给第一个座位放名字
    student3.second = 92;            // 给第二个座位放分数

    // 输出看看
    cout << "学生1: " << student1.first << " - " << student1.second << "分" << endl;
    cout << "学生2: " << student2.first << " - " << student2.second << "分" << endl;
    cout << "学生3: " << student3.first << " - " << student3.second << "分" << endl;

    return 0;
}

2.2 访问pair中的元素

访问pair中的元素非常直接:用 .first 得到第一个元素,用 .second 得到第二个元素。注意不是用下标 [0][1],这是新手最容易搞错的地方。

pair<int, double> score_info = {5, 87.5};  // 5号同学,得分87.5
cout << "学号: " << score_info.first << ", 分数: " << score_info.second << endl;

2.3 pair的比较操作

pair支持 ==, !=, <, >, <=, >= 这些比较运算符。比较规则是“字典序”:先比较 first,如果相等再比较 second。这个特性非常有用,比如当你把多个pair放到一个容器中并排序时,默认就会先按第一个元素排,再按第二个元素排。

pair<int, int> p1 = {1, 5};
pair<int, int> p2 = {1, 3};
pair<int, int> p3 = {2, 0};

cout << boolalpha;  // 让true/false显示为文字
cout << (p1 < p2) << endl;  // false,因为first相等(1==1),但second: 5 > 3,所以p1 > p2
cout << (p1 < p3) << endl;  // true,因为1 < 2,不需要比较second

重要提醒:如果两个pair的first不同,比较结果完全由first决定,根本不会看second。只有first相等时才会比较second

2.4 pair与容器的配合

pair最常用的场景之一就是作为容器(比如vector、map)的元素。例如,你可以用 vector<pair<string, int>> 来存储一组“姓名-分数”数据。map的每个元素本身就是一个pair(key-value对)。

#include <vector>
#include <algorithm>  // 用于sort
// ... 其他头文件

vector<pair<string, int>> class_list;  // 班级名单:每个元素是一个学生的姓名和分数对
class_list.push_back({"Alice", 95});   // 添加学生
class_list.push_back({"Bob", 88});

2.5 C++17新语法:结构化绑定

从C++17开始,你可以用结构化绑定(structured binding)直接把pair的两个元素解包成独立变量,让代码更简洁:

pair<int, string> my_pair = {42, "hello"};
auto [num, msg] = my_pair;  // num得到42,msg得到"hello"
cout << num << " " << msg << endl;  // 输出: 42 hello

但作为初学者,先熟练掌握 .first.second 也很重要,因为很多旧代码还在用传统方式。

3. 新手最容易犯的五个错误

错误1:忘记包含头文件

pair定义在 <utility> 中,但很多STL容器(比如 <map><vector><algorithm>)在内部已经间接包含了它。不过为了清晰,最好显式包含 #include <utility>。如果漏掉,编译器可能会报“pair未定义”错误。

错误2:用下标访问pair

有人会误写成 pair_var[0]pair_var[1],但pair不是数组!正确方式是 pair_var.firstpair_var.second

pair<int, double> p = {1, 2.5};
cout << p[0];      // ❌ 错误!pair没有下标操作
cout << p.first;   // ✅ 正确

错误3:混淆pair和map

map的每个元素确实是一个pair(key-value对),但map本身是一个关联容器,而pair只是一个简单的对象。不能说“map就是pair”。map中的元素是 pair<const Key, Value> 类型。

错误4:排序时忘记自定义比较规则

如果你用 sortvector<pair<...>> 排序,默认是按first升序,再按second升序。但如果你希望按second排序,就必须编写自己的比较函数(或lambda)。很多新手直接使用默认排序,发现结果不符合预期。

// 错误:默认排序先比较first(名字),但你可能想按分数(second)排序
sort(class_list.begin(), class_list.end());

// 正确:自定义lambda按分数升序
sort(class_list.begin(), class_list.end(),
     [](const pair<string,int>& a, const pair<string,int>& b) {
         return a.second < b.second;
     });

错误5:在结构体/类中忽略pair的拷贝构造

pair本身支持拷贝,但如果你存储了动态分配的资源(如指针)到pair中,需要小心浅拷贝问题。不过初学者一般只存储基本类型或string,风险较小。

4. 完整示例:管理一个班级的考试成绩

下面是一个完整的程序,演示了从创建pair到存入vector、排序、查找、输出的全过程。每一行都加了中文注释,帮助你理解。

#include <iostream>
#include <vector>       // 使用vector容器
#include <algorithm>    // 使用sort和find_if
#include <utility>      // 使用pair
#include <string>       // 使用string

using namespace std;

int main() {
    // 1. 创建几个学生(pair:姓名,分数)
    pair<string, int> stu1 = {"Alice", 95};   // 第一个学生
    pair<string, int> stu2 = {"Bob", 88};     // 第二个学生
    pair<string, int> stu3 = {"Cathy", 92};   // 第三个学生

    // 2. 把学生放进一个vector(班级名单)
    vector<pair<string, int>> class_list;     // 定义一个存放pair的vector
    class_list.push_back(stu1);               // 添加stu1
    class_list.push_back(stu2);               // 添加stu2
    class_list.push_back(stu3);               // 添加stu3
    class_list.push_back(make_pair("David", 78)); // 直接创建并添加第四个

    // 3. 遍历并打印原始名单
    cout << "=== 原始班级名单 ===" << endl;
    for (const auto& p : class_list) {        // const auto&避免拷贝,提高效率
        cout << p.first << " : " << p.second << "分" << endl;
    }

    // 4. 按分数(second)升序排序
    sort(class_list.begin(), class_list.end(),   // 排序范围
         [](const pair<string,int>& a, const pair<string,int>& b) {
             return a.second < b.second;         // 按分数从小到大
         });

    cout << "\n=== 按分数升序排序后 ===" << endl;
    for (const auto& p : class_list) {
        cout << p.first << " : " << p.second << "分" << endl;
    }

    // 5. 查找分数为92的同学
    int target_score = 92;                       // 要查找的目标分数
    auto it = find_if(class_list.begin(), class_list.end(),  // 按条件查找
                      [target_score](const pair<string,int>& p) {
                          return p.second == target_score;
                      });
    if (it != class_list.end()) {
        cout << "\n找到分数为 " << target_score << " 的同学: " << it->first << endl;
    } else {
        cout << "\n没有找到分数为 " << target_score << " 的同学。" << endl;
    }

    // 6. 使用结构化绑定(C++17)再次遍历
    cout << "\n=== 使用结构化绑定的现代写法 ===" << endl;
    for (const auto& [name, score] : class_list) {  // 直接解包成name和score
        cout << name << " -> " << score << "分" << endl;
    }

    return 0;
}

代码运行结果(可能因输入顺序不同略有差异):

=== 原始班级名单 ===
Alice : 95分
Bob : 88分
Cathy : 92分
David : 78分

=== 按分数升序排序后 ===
David : 78分
Bob : 88分
Cathy : 92分
Alice : 95分

找到分数为 92 的同学: Cathy

=== 使用结构化绑定的现代写法 ===
David -> 78分
Bob -> 88分
Cathy -> 92分
Alice -> 95分

5. Python中的等效实现:元组与namedtuple

Python标准库中没有直接叫 pair 的类,但我们可以用 元组(tuple) 来达到同样的效果。元组就是不可变的序列,也可以包含两个或多个元素,而且Python的元组支持解包,使用起来更方便。此外,也可以用 collections.namedtuple 命名元组,使代码更具可读性。

# Python 使用元组实现 pair 功能
from typing import List, Tuple
from operator import itemgetter

# 1. 创建 pair 的几种方式(用元组)
student1 = ("Alice", 95)       # 直接写括号,第一个元素是名字,第二个是分数
student2 = tuple(["Bob", 88])  # 用tuple函数将列表转为元组
student3 = ("Cathy", 92)

# 访问元素:下标从0开始,t[0]相当于first,t[1]相当于second
print(f"学生1: {student1[0]} - {student1[1]}分")

# 2. 存入列表
class_list: List[Tuple[str, int]] = []  # 定义一个空列表,元素类型是(str,int)元组
class_list.append(student1)               # 添加第一个学生
class_list.append(student2)               # 添加第二个学生
class_list.append(student3)               # 添加第三个学生
class_list.append(("David", 78))          # 直接添加新元组

# 3. 遍历并打印
print("\n全班名单(姓名-分数):")
for p in class_list:
    print(f"{p[0]} : {p[1]}分")

# 4. 按分数排序(升序)
# 使用 sorted 并指定 key=itemgetter(1) 按第二个元素排序
sorted_list = sorted(class_list, key=itemgetter(1))
print("\n按分数升序排序后:")
for name, score in sorted_list:   # 直接解包元组,name=p[0], score=p[1]
    print(f"{name} : {score}分")

# 5. 查找某个成绩是否存在
target = 92
# 用列表推导式查找分数等于target的同学
result = [name for name, score in sorted_list if score == target]
if result:
    print(f"\n找到分数为 {target} 的同学: {result[0]}")
else:
    print(f"\n没有找到分数为 {target} 的同学。")

# 6. 使用 namedtuple(可选,增加可读性)
from collections import namedtuple
Student = namedtuple('Student', ['name', 'score'])  # 创建一个Student类型
s1 = Student('Alice', 95)
s2 = Student('Bob', 88)
# 访问直接使用属性名,不再用下标
print(f"\nnamedtuple示例: {s1.name} - {s1.score}分")

# 可以将namedtuple放入列表操作
named_list = [s1, s2, Student('Cathy', 92)]
named_list.sort(key=lambda x: x.score)   # 按分数排序
for s in named_list:
    print(f"{s.name} : {s.score}分")

Python解释

  • 元组 (name, score) 就是一个不可变的pair。
  • 使用 itemgetter(1) 可以方便地按第二个元素排序。
  • 列表推导式 [name for ... if ...] 完成查找。
  • namedtuple 是Python标准库中的一个高级工具,允许像访问成员变量一样访问元组元素,代码更易读。

6. 总结与思考

C++ pair 要点回顾

  • pair是一个模板类,需要包含头文件 <utility>,但很多STL容器(如map)已经间接包含了它。
  • 访问成员用 .first.second,注意不是下标。
  • pair支持比较操作(==, < 等),比较规则是先比first,再比second,这在排序时非常有用。
  • 可以用 make_pair 简化创建,也支持花括号初始化(C++11起)。
  • 在C++17及以上,可以使用结构化绑定 auto [a,b] = p; 解包。

Python 等效总结

  • 元组 (a,b) 是Python中最简单的pair。
  • 下标从0开始:t[0] 对应first,t[1] 对应second。
  • 元组是不可变的,如果需要修改内容,可以用列表 [a,b] 代替。
  • 当需要更语义化的访问时,推荐使用 namedtuple

注意事项

  • 在C++中,当pair作为容器元素且需要排序时,自定义比较函数要小心:如果缺省比较,则按字典序(先first后second)。
  • Python的元组排序也是按字典序,所以如果第一个元素相同,才会比较第二个。这与C++的pair默认排序完全一致。
  • 不要混淆“pair”和“map”:map的每个元素就是一个 pair,但 map本身是一个容器,而 pair只是一个存储两个值的对象。
  • pair常用于函数返回多个值(比如返回一个错误码和结果),或者作为 map 的键值对。

小练习

  1. 写一个程序,输入5个学生的姓名和语文成绩,用pair存储并输出最高分和最低分的学生信息。
  2. 思考:如果希望按成绩降序排序,C++和Python分别怎么做?(提示:C++改为 return a.second > b.second,Python用 reverse=True 或取负值)
  3. 尝试用pair来存储一个二维坐标点(x, y),并计算两点之间的距离。

通过pair的学习,你已经掌握了“成对数据”的打包方法。下一个知识点我们将学习更强大的“三合一”工具:tuple元组(C++的std::tuple和Python的tuple),它可以存储任意多个不同类型的数据。

例题精讲

1单选题

在C++中,关于pair对组的定义,以下哪种方式是正确的?

Apair<int, string> p = (1, "hello");
Bpair<int, string> p = make_pair(1, "hello");
Cpair<int, string> p = {1, "hello"};
Dpair<int, string> p = new pair(1, "hello");
2判断题

pair对组中的两个元素类型必须相同。

3填空题
以下代码创建了一个pair对象并输出其第一个元素,请填空:

#include <iostream>
#include <utility>
using namespace std;
int main() {
    pair<string, int> student = ___({"Tom", 18});
    cout << student.first;
    return 0;
}
4单选题

已知pair<int, double> p = {3, 2.5};,下列哪个表达式可以正确访问p中的第二个元素?

Ap[1]
Bp.get<1>()
Cp.second
Dp.tail
5填空题
下面的代码使用pair向map容器中插入一个键值对,请将空白处补充完整:

#include <iostream>
#include <map>
#include <utility>
using namespace std;
int main() {
    map<string, int> score;
    score.___("Alice", 95);
    cout << score["Alice"];
    return 0;
}