
第十五课 结构体、排序与自定义比较
------从"给数字排队"到"给一群人排队"
一、本课学习目标
学完这一课,大家应该能够回答:
-
什么是结构体?
-
为什么需要结构体?
-
struct里面可以放什么? -
.和->有什么区别? -
为什么结构体不能像普通数字一样直接比较?
-
sort()到底在比较什么? -
什么是"排序规则"?
-
如何实现"先按成绩高低,再按姓名排序"?
-
operator <到底是什么意思? -
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
就是:
找到
a的score。
给大家一个非常简单的口诀:
对象用点找成员。
六、把结构体想成一个抽屉柜
例如:
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个学生:
姓名、成绩、学号
先手工排出最终结果,再书写完整程序。