CSP-J 初赛(以满分为目标):第十五课《结构体、排序与自定义比较——从“给数字排队”到“给一群人排队”》


第十五课 结构体、排序与自定义比较

------从"给数字排队"到"给一群人排队"


一、本课学习目标

学完这一课,大家应该能够回答:

  1. 什么是结构体?

  2. 为什么需要结构体?

  3. struct 里面可以放什么?

  4. .-> 有什么区别?

  5. 为什么结构体不能像普通数字一样直接比较?

  6. sort() 到底在比较什么?

  7. 什么是"排序规则"?

  8. 如何实现"先按成绩高低,再按姓名排序"?

  9. operator < 到底是什么意思?

  10. CSP-J程序阅读中如何手算结构体排序?


二、先给同学们讲一个故事

假设老师手里有三个学生:

复制代码
小明   95分   10岁
小红   98分   11岁
小刚   90分   10岁

如果只用变量:

复制代码
int score1 = 95;
int score2 = 98;
int score3 = 90;

只能保存成绩。

但是一个学生实际上有很多信息:

复制代码
姓名
年龄
成绩
学号
班级

怎么办?

我们可以把这些信息打包在一起

这就是:

结构体 struct


三、结构体就是"信息大礼包"

例如:

复制代码
struct Student
{
    string name;
    int age;
    int score;
};

这句话的意思是:

我们设计了一种新的数据类型,叫 Student

一个 Student 里面有三个成员:

复制代码
Student
 ├── name
 ├── age
 └── score

四、创建结构体变量

定义:

复制代码
struct Student
{
    string name;
    int age;
    int score;
};

然后:

复制代码
Student a;

现在:

复制代码
a
├── name
├── age
└── score

给它赋值:

复制代码
a.name = "小明";
a.age = 10;
a.score = 95;

五、.到底是什么?

这是结构体最重要的符号之一:

复制代码
a.name
a.age
a.score

. 可以理解成:

"从这个结构体里面,找到它的某个成员。"

例如:

复制代码
a.score

就是:

找到 ascore

给大家一个非常简单的口诀:

对象用点找成员。


六、把结构体想成一个抽屉柜

例如:

复制代码
a
┌───────────────┐
│ name          │
│ 小明           │
├───────────────┤
│ age           │
│ 10            │
├───────────────┤
│ score         │
│ 95            │
└───────────────┘

那么:

复制代码
a.name

就是打开第一个抽屉。

复制代码
a.score

就是打开第三个抽屉。


七、结构体数组

如果有100个学生:

复制代码
Student a[100];

那么:

复制代码
a[0]
a[1]
a[2]
...
a[99]

每一个都是一个完整的 Student

例如:

复制代码
a[0].name = "小明";
a[0].score = 95;

a[1].name = "小红";
a[1].score = 98;

这时候可以把它想象成:

复制代码
a[0] → 小明  10岁  95
a[1] → 小红  11岁  98
a[2] → 小刚  10岁  90

这就是:

"数组 + 结构体"

非常重要。


八、为什么结构体数组特别有用?

以前:

复制代码
int score[100];
string name[100];
int age[100];

三个数组必须保证:

复制代码
score[i]
name[i]
age[i]

永远对应同一个人。

很容易乱。

使用结构体:

复制代码
Student a[100];

就变成:

复制代码
a[i]
 ├── name
 ├── age
 └── score

一个学生的信息永远绑在一起。

所以:

结构体解决的是"多个相关信息如何打包"。


九、举个的结构体例子

复制代码
struct stu{
    char name[100];
    double score;
};

然后定义对象:

复制代码
stu s;

再:

复制代码
strcpy(s.name,"ZhangSan");
s.score = 99.99;

最后输出:

复制代码
cout << s.name << " " << s.score;

这体现了:

复制代码
定义结构体
 ↓
定义一个对象
 ↓
对象里面有多个成员变量

十、结构体指针

例如:

复制代码
Student a;
Student *p = &a;

此时:

复制代码
p
 ↓
a

p里面保存的是 a 的地址。


十一、.->

如果是普通结构体变量:

复制代码
Student a;
a.score;

使用:

复制代码
.

如果是结构体指针:

复制代码
Student *p = &a;

使用:

复制代码
->

所以:

复制代码
p->score

等价于:

复制代码
(*p).score

我们给出这一组对应关系:

复制代码
stu1->name
stu1->score

以及:

复制代码
(*stu1).name
(*stu1).score

十二、给大家一个超级简单的口诀

变量用点,指针用箭头。

即:

复制代码
a.score

普通变量

而:

复制代码
p->score

指针


十三、现在进入本课的重点:排序

假设:

复制代码
小明 95
小红 98
小刚 90

如果按照成绩从高到低:

复制代码
小红 98
小明 95
小刚 90

如果只有数字:

复制代码
sort(a, a+n);

很好理解。

因为:

复制代码
90 < 95 < 98

十四、但结构体怎么办?

现在:

复制代码
Student a[3];

里面是:

复制代码
小明 95
小红 98
小刚 90

如果直接:

复制代码
sort(a, a+3);

问题来了:

C++凭什么知道"哪个学生应该排在前面"?

因为一个学生不是一个数字。


十五、这就是"排序规则"

我们必须告诉计算机:

两个Student比较时,到底比较什么?

例如:

复制代码
比较成绩

那么:

复制代码
score大的排前面

或者:

复制代码
score小的排前面

甚至:

复制代码
成绩高的优先
如果成绩一样,年龄小的优先
如果年龄也一样,姓名字典序小的优先

这就是:

自定义排序规则


十六、第一种写法:比较函数

最适合小学生理解的是:

复制代码
bool cmp(Student a, Student b)
{
    return a.score > b.score;
}

然后:

复制代码
sort(a, a+n, cmp);

意思:

如果 a.score > b.score,那么 a 应该排在 b 前面。


十七、cmp到底在干什么?

这是孩子最容易困惑的地方。

不要把:

复制代码
return a.score > b.score;

理解成:

"比较之后返回成绩"。

它实际上是在回答一个问题:

"a应该排在b前面吗?"

如果:

复制代码
a = 小明 95
b = 小红 98

那么:

复制代码
a.score > b.score

是:

复制代码
95 > 98

结果:

复制代码
false

意思:

小明不应该排在小红前面。


十八、换一个比较

如果:

复制代码
a = 小红 98
b = 小明 95

那么:

复制代码
a.score > b.score

是:

复制代码
98 > 95

结果:

复制代码
true

意思:

小红应该排在小明前面。

所以:

复制代码
bool cmp(Student a, Student b)
{
    return a.score > b.score;
}

就是:

成绩从大到小排序。


十九、为什么升序是 <

如果:

复制代码
bool cmp(Student a, Student b)
{
    return a.score < b.score;
}

那么意思变成:

成绩小的应该排在前面。

因此:

复制代码
< → 升序
> → 降序

但是一定要强调:

这不是死记"<就是升序"。

真正应该记住:

cmp(a,b)回答的是"a是否应该排在b前面?"


二十、这是本课最重要的一句话

请让孩子记住:

sort不是在问"谁大",而是在问"谁排前面"。

例如:

复制代码
return a.score > b.score;

并不是:

"a比b大吗?"

而是:

"a的成绩比b高,所以a应该排在b前面吗?"

这一个理解非常重要。


二十一、多关键字排序

这是CSP-J非常值得掌握的内容。

例如:

复制代码
成绩越高越前
成绩相同,学号越小越前

写:

复制代码
bool cmp(Student a, Student b)
{
    if(a.score != b.score)
        return a.score > b.score;

    return a.id < b.id;
}

二十二、为什么要写两个条件?

例如:

复制代码
小明 95  3
小红 98  5
小刚 95  1

第一关键字:

复制代码
score

先按照成绩:

复制代码
98
95
95

于是:

复制代码
小红
小明
小刚

小明和小刚成绩一样。

怎么办?

继续比较:

复制代码
id

因为:

复制代码
1 < 3

所以:

复制代码
小红
小刚
小明

二十三、多关键字排序的通用模板

大家可以记成:

复制代码
bool cmp(Student a, Student b)
{
    if(第一关键字不同)
        return 第一关键字的排序规则;

    return 第二关键字的排序规则;
}

如果有三个关键字:

复制代码
bool cmp(Student a, Student b)
{
    if(a.score != b.score)
        return a.score > b.score;

    if(a.age != b.age)
        return a.age < b.age;

    return a.name < b.name;
}

就是:

复制代码
第一关键字
 ↓
相同?
 ↓
第二关键字
 ↓
还相同?
 ↓
第三关键字

二十四、给同学们一个"比赛排名"的方法

假设比赛排名规则:

复制代码
第一:
分数高

第二:
分数一样时,用时短

第三:
分数、用时都一样时,编号小

那么:

复制代码
bool cmp(Player a, Player b)
{
    if(a.score != b.score)
        return a.score > b.score;

    if(a.time != b.time)
        return a.time < b.time;

    return a.id < b.id;
}

这其实就是:

"先比第一项,平手再比第二项,再平手再比第三项。"


二十五、这和字典排序很像

字符串:

复制代码
abc
abd

比较:

复制代码
第一字符
 ↓
相同
 ↓
第二字符
 ↓
相同
 ↓
第三字符

结构体多关键字排序也是:

复制代码
第一关键字
 ↓
相同
 ↓
第二关键字
 ↓
相同
 ↓
第三关键字

所以大家会发现:

排序其实就是一层一层地比较。


二十六、operator<是什么?

现在进入稍微提高一点的内容。

有时候我们会看到:

复制代码
struct Student
{
    int score;

    bool operator < (const Student &b) const
    {
        return score < b.score;
    }
};

孩子第一反应通常是:

老师,这么长,到底在干什么?

其实它就是在告诉C++:

"两个Student使用 < 时,应该怎么比较。"


二十七、普通数字的 <

例如:

复制代码
3 < 5

C++知道:

复制代码
true

因为整数本身就有大小关系。

但是:

复制代码
Student a,b;
a < b;

C++不知道:

Student到底怎么比较?

于是我们自己定义:

复制代码
bool operator < (...)

二十八、把它理解成"教C++一条新规则"

原来C++知道:

复制代码
3 < 5

现在我们告诉它:

复制代码
Student a < Student b

是什么意思。

例如:

复制代码
bool operator < (const Student &b) const
{
    return score < b.score;
}

就是告诉C++:

两个学生比较大小时,看成绩。

于是:

复制代码
sort(a,a+n);

就可以工作了。


二十九、为什么名字叫operator<

因为:

复制代码
operator
+
<

合起来:

复制代码
operator<

就是:

"小于号运算符的自定义版本"。

同理C++还可以定义:

复制代码
operator+
operator==
operator>

等等。

本课对于CSP-J初赛:

理解它"重新定义比较规则"的思想即可。

不必再深入探究运算符重载的底层实现。


三十、结构体排序程序阅读题

现在开始真正进入CSP-J模式。

看程序:

复制代码
#include <bits/stdc++.h>
using namespace std;

struct Student
{
    string name;
    int score;
};

bool cmp(Student a, Student b)
{
    return a.score > b.score;
}

int main()
{
    Student a[3] =
    {
        {"A", 80},
        {"B", 95},
        {"C", 90}
    };

    sort(a, a+3, cmp);

    for(int i=0;i<3;i++)
        cout << a[i].name << " ";
}

问:

输出什么?


三十一、第一步:先写原始数据

复制代码
A 80
B 95
C 90

三十二、第二步:读cmp

复制代码
return a.score > b.score;

说明:

成绩高的在前面。


三十三、第三步:排序

复制代码
95 → B
90 → C
80 → A

所以:

复制代码
B C A

三十四、第四步:看输出

复制代码
cout << a[i].name << " ";

所以:

复制代码
B C A

这就是完整程序阅读过程。


三十五、再来一道多关键字题

复制代码
struct Student
{
    int score;
    int id;
};

bool cmp(Student a, Student b)
{
    if(a.score != b.score)
        return a.score > b.score;

    return a.id < b.id;
}

数据:

复制代码
A:90 3
B:95 2
C:90 1
D:95 5

三十六、先看第一关键字

成绩:

复制代码
B 95
D 95
A 90
C 90

三十七、成绩相同怎么办?

95分:

复制代码
B id=2
D id=5

因为:

复制代码
2 < 5

所以:

复制代码
B
D

90分:

复制代码
A id=3
C id=1

因为:

复制代码
1 < 3

所以:

复制代码
C
A

最终:

复制代码
B D C A

三十八、初赛容易错的地方

错误1:看到 > 就认为是"降序"

这个说法不够准确。

真正应该看:

复制代码
return a.score > b.score;

它回答:

a应该排在b前面吗?

所以这里才得到:

复制代码
分数高的排前

错误2:忘记"相同再比较"

看到:

复制代码
if(a.score != b.score)
    return a.score > b.score;

return a.id < b.id;

一定要翻译成:

复制代码
先成绩
 ↓
成绩相同
 ↓
再编号

错误3:把结构体当成一个数字

例如:

复制代码
Student a,b;

不能简单认为:

复制代码
a>b

到底是什么意思?

必须找到:

复制代码
cmp

或者:

复制代码
operator<

看它定义的规则。


三十九、sort()本身不负责决定规则

这是非常值得孩子理解的一个思想。

复制代码
sort(a,a+n,cmp);

可以分成:

复制代码
sort
 ↓
负责"排序"

cmp
 ↓
负责"告诉它谁在前面"

也就是说:

sort是裁判,cmp是比赛规则。

这是一个非常好的类比。


四十、如果没有cmp呢?

例如:

复制代码
sort(a,a+n);

它会使用默认的"小于比较"。

如果结构体自己定义了:

复制代码
operator<

那么就可以使用。

所以:

复制代码
sort(a,a+n,cmp)

和:

复制代码
sort(a,a+n)

可以理解为:

第一种

复制代码
sort
+
我们专门提供的比较规则

第二种

复制代码
sort
+
结构体自己定义的<规则

四十一、结构体排序与普通排序的知识升级

大家已经会:

复制代码
sort(a,a+n);

排序:

复制代码
3 1 5 2

现在继续升级成:

复制代码
sort(Student数组)

排序:

复制代码
姓名
成绩
年龄
学号
时间

这意味着:

从"给数据排序"升级到"给对象排序"。

这是很重要的编程思维。


四十二、本课和时间复杂度连接起来

复制代码
冒泡排序 O(n²)
选择排序 O(n²)
快速排序 O(n log n)

所以看到:

复制代码
sort(a,a+n);

初赛中除了问:

排完是什么?

还可能问:

时间复杂度大约是多少?

对于C++标准库 sort,我们可以把它理解为:

复制代码
O(n log n)

因此:

复制代码
冒泡:
O(n²)

sort:
O(n log n)

n 很大时,差别非常明显。


四十三、为什么排序会出现在很多算法里?

这其实是下一阶段算法思想的重要入口。

例如:

复制代码
原数据
 ↓
排序
 ↓
变得有规律
 ↓
更容易处理

比如:

复制代码
7 2 9 4 1

排序:

复制代码
1 2 4 7 9

排序以后:

  • 找最大值更方便

  • 找最小值更方便

  • 找第K大更方便

  • 二分查找可以开始使用

  • 贪心算法经常需要排序

  • 区间问题经常需要排序

所以:

排序不仅仅是"把数字排整齐"。

它是很多算法的前置工具。


四十四、本课要掌握的程序阅读方法

以后看到:

复制代码
struct ...

第一步:

把每个成员写出来。

看到:

复制代码
Student a[5];

第二步:

把它理解成5个完整的Student。

看到:

复制代码
a[i].score

第三步:

找到第i个学生的成绩。

看到:

复制代码
sort(...)

第四步:

找比较规则。

看到:

复制代码
cmp(...)

第五步:

把cmp翻译成"谁应该排前面"。

看到:

复制代码
if(...)

第六步:

先比较第一关键字,平手再比较第二关键字。


四十五、综合案例

CSP-J初赛类型的题。

复制代码
#include <bits/stdc++.h>
using namespace std;

struct Student
{
    string name;
    int score;
    int id;
};

bool cmp(Student a, Student b)
{
    if(a.score != b.score)
        return a.score > b.score;

    return a.id < b.id;
}

int main()
{
    Student a[4] =
    {
        {"Tom",90,3},
        {"Jack",95,4},
        {"Lucy",90,1},
        {"Bob",95,2}
    };

    sort(a,a+4,cmp);

    for(int i=0;i<4;i++)
        cout << a[i].name << " ";
}

四十六、不要直接看答案

先画表:

姓名 分数 学号
Tom 90 3
Jack 95 4
Lucy 90 1
Bob 95 2

比较规则:

复制代码
第一关键字:score 大的在前
第二关键字:id 小的在前

于是:

第一组:95分

复制代码
Jack  id=4
Bob   id=2

学号小的:

复制代码
Bob
Jack

第二组:90分

复制代码
Tom   id=3
Lucy  id=1

学号小的:

复制代码
Lucy
Tom

所以答案:

复制代码
Bob Jack Lucy Tom

四十七、总结一张"排序地图"

复制代码
                 sort
                  │
          ┌───────┴────────┐
          ↓                ↓
      普通数组           结构体数组
          │                │
       3 1 5 2             ↓
          │              cmp
          ↓                │
      默认比较             ↓
                      谁应该排前面?
                           │
                    ┌──────┼──────┐
                    ↓      ↓      ↓
                  第一    第二    第三
                 关键字   关键字   关键字
                    │
                    ↓
                  排序

四十八、本课"必背10句话"

1. struct可以把多个相关信息打包在一起。
2. 一个结构体变量里面可以有多个成员。
3. 普通结构体变量使用.访问成员。
4. 结构体指针使用->访问成员。
5. p->x等价于(*p).x
6. 结构体数组就是很多个完整的结构体排成一队。
7. sort()负责排序,比较规则决定谁排前面。
8. cmp(a,b)回答的是"a应该排在b前面吗?"
9. 多关键字排序就是"第一关键字相同,再比较第二关键字"。
10. 看到operator<,要想到"这个结构体自己定义了小于号的比较规则"。


四十九、课后练习

本课重点训练程序阅读能力

1、基础题

给:

复制代码
struct Student
{
    string name;
    int score;
};

让大家讲解:

复制代码
a.name
a.score
a[0].name
a[1].score

分别表示什么。


2、提高题

给:

复制代码
bool cmp(Student a, Student b)
{
    return a.score < b.score;
}

让大家讲解:

成绩是从大到小还是从小到大?


3、综合题

给:

复制代码
if(a.score != b.score)
    return a.score > b.score;

return a.id < b.id;

让大家讲解:

复制代码
第一关键字是什么?
第二关键字是什么?
分别升序还是降序?

4、最后一题

给一组5个学生:

复制代码
姓名、成绩、学号

先手工排出最终结果,再书写完整程序。


相关推荐
HugoStudio_SWAN1 小时前
C++ CMD 互动动画:按键触发爆炸效果
开发语言·c++·学习·程序人生
水饺编程1 小时前
第5章,[Win32 章节] :Polygon 函数和多边形填充模式
c语言·c++·windows·visual studio
wuminyu1 小时前
Java FFM处理网络读写事件源码剖析
java·linux·c语言·jvm·c++
一木 之林2 小时前
C/C++ 面向对象编程(OOP)(进阶)
java·c语言·c++
千谦阙听3 小时前
C++篇:类和对象(上)
开发语言·c++
_Narcissus_4 小时前
分治&递归
数据结构·c++·笔记·算法·leetcode·递归·分治
牛油果子哥q4 小时前
C++ Socket精讲:TCP/UDP套接字、客户端服务端实现、字节序、粘包拆包、简单HTTP服务、网络踩坑与面试全解
网络·c++·tcp/ip
程与留5 小时前
12_常用控件速查(下):QSpinBox、QSlider、QProgressBar、QCheckBox
c++·qt
OPEN-F5 小时前
C++进阶教程:运算符重载与类型转换
java·c++·算法