题目叫"计算天数",分值15分,序号是实验7-2-4,看起来平平无奇。但就是这道平平无奇的题,我见过不少同学在它面前反复栽跟头。不是不会做——"输入年、月、日,输出这是一年中的第几天",闭着眼睛都能听懂要求——可一提交到在线评测系统,迎面就是红红的错误反馈。自己本地跑又好好的,到底哪里错了?
答案往往藏在一些不起眼的细节里:闰年怎么判断、二月到底按28天还是29天算、月份表的下标该从0开始还是从1开始,以及评测系统在边界日期上埋的那些测试点。这篇文章就把这道题从审题到满分完整过一遍,把算法思路、完整代码、常见坑位和自测方法一次讲清楚。正在和这道题较劲的同学可以直接照抄,已经过了的也可以借这个机会把日期类问题的套路查漏补缺。
1. 题目拆解:15分到底在考哪几个点
1.1 从题号看考核定位
"实验7-2-4"这个编号在多数课程实验体系里是有讲究的。实验7说明你已经学完了分支结构和循环结构,可能刚接触数组或函数;2-4说明这是第2个实验模块的第4道小题。换句话说,命题人默认你会写for循环、会处理数组、会做基本的分支判断。这道题不是单纯考语法,而是考你能否把几个简单知识点组合起来,解决一个完整的小问题。
我见过有同学一上来就写四五十行代码,switch-case从1写到12,里面再嵌套一堆if判断闰年,最后把自己绕晕了。真正会写代码的人看到这道题,第一反应是先拆解成五件事:读入三个整数、判断闰年、累加前month-1个月的天数、加上当月日期、输出结果。就这五件事,15分的题绝对不值得超过30行代码。
1.2 输入输出格式里的隐藏要求
在线评测系统和平时自己跑控制台最大的区别是:它不关心你的程序内部写得有多漂亮,只关心给定输入后输出是否一分不差。
以最常见的题目原型为例,输入格式是一行三个正整数,空格分隔,分别代表年、月、日;输出格式是一个整数,表示该日是该年的第几天。这里有几个容易被忽略的细节。
输入分隔符上,scanf("%d%d%d")对空格分隔和换行分隔都能接受,但如果你照猫画虎写成scanf("%d,%d,%d"),而评测数据是空格分隔,就会读到错位。还有个小细节,很多评测平台会对输出做严格比对,printf("%d\n", day_of_year)末尾的换行是标准写法,别漏。
年份范围一般给的是四位正整数,不会有超过int的情况。但要注意,如果题目描述里写了"年份在1到10^9之间",那就要用long long,这种变体在面试题里更常见,后面会展开。
1.3 评分点拆解:一份参考答案藏着的评分逻辑
虽然我们看不到评测系统内部具体挂了哪些测试点,但从15分的分值可以推测,它多半是按测试点给分:常规年份、闰年2月、12月31日、平年2月28日、1月1日这类边界值,每个点对应若干分。只要有一个测试点过不了,就会扣掉对应的分。
这给我们的启示是:别只测题目给的样例。样例在大多数情况下是"标准中间值",比如2021年3月1日这种,不会暴露闰年错误,也不会暴露2月边界错误。评测系统里的测试点一定包含最容易被忽略的边界——尤其是2月的最后一天和闰年2月29日。所以做题之前先想清楚"评测系统会在哪些日期上卡你",比单纯把代码写对更关键。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法路线图:闰年判断与天数表设计
2.1 闰年的完整判断规则,以及为什么容易记混
闰年的规则本身不复杂:能被4整除但不能被100整除的年份,或者能被400整除的年份,是闰年。写成C语言就是一行:
c复制int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
但很多同学的记忆是"四年一闰",于是条件写成了year%4==0。这个写法忽略了一个关键事实:整百年份必须能被400整除才是闰年。比如1900年能被4整除,但它是平年;2000年能被400整除,所以才是闰年。如果用year%4==0判断,1900年会被错误地当作闰年,2月就会多算一天,整个程序的输出在1900年全错。
这里我提供一个记忆技巧:你可以把规则记成"能被4整除,但要排除整百年;整百年要能被400整除"。反向记忆是"不是闰年的情况:能被4整除的年份里,那些能被100整除但不能被400整除的"。拿1900、2000、2100三个年份多写几遍,印象会非常深刻。
2.2 天数表:用一个数组存下12个月
计算天数最朴素的做法是逐个if判断月份,但那样代码又长又容易漏。更推荐的做法是用一个int数组,把平年每月的天数存进去:
c复制int months[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
下标0对应1月,下标11对应12月。这样,要求"前month-1个月的累加天数",只需要一个for循环遍历months[0]到months[month-2]。
这里有个约定俗成的细节:2月先按28天存,如果这一年是闰年,并且month大于2,最后再补上多出来的那1天。这样做的好处是,无论是不是闰年,前几个月的累加逻辑完全一致,闰年只是一个额外的修正项,不容易把逻辑写乱。
2.3 计算流程:先累加整月,再补上当月日期
完整计算流程可以分为三步。
第一步,判断闰年,得到一个标志变量is_leap(1是闰年,0不是)。第二步,从1月累加到month-1月,得到这些整月总天数。第三步,如果是闰年且month>2,总天数加1;然后再加上day,输出。
这里最关键的一步是"闰年且month>2"这个条件。为什么月份必须大于2?因为2月多出的那一天是加在2月最后一天的,只要当前日期还没过完2月,比如2月1日,那今年前面的整月天数里就不该有2月,自然也不该把闰日算进去。很多错误代码把条件写成is_leap就直接加1,导致2020年2月1日算出来比正确答案多1天。
这么说可能还有点抽象,举两个具体例子对照一下。2020年3月1日,闰年,1月31天加2月29天再加1天,结果是61天,此时需要把闰日算进去,因为3月1日已经越过2月了。2020年2月1日,闰年,但1月31天加2月1天就结束了,结果是32天,这时候绝不能加闰日,因为2月还没过完。所以month > 2这个判断不是随便写的,它是逻辑上必然的要求。
3. 满分代码的一次成型:C语言实现的完整思路
3.1 开写前的三个决定
动手前我习惯先把三个问题定下来:用不用数组?用数组。用不用函数?可以不用,但分一个函数出来更清爽。闰年标志位怎么命名?用int is_leap,一眼能看懂。
为什么用数组而不是switch-case?因为从1月累加到month-1月这件事,在数组里就是一次循环,而用switch-case你得重复写每个月的天数,或者写一长串累加语句,代码量翻倍且容易漏。面试里如果遇到"不许用数组"的附加条件,再用switch-case也不迟。
3.2 完整代码与逐段解读
下面给一个可以直接交上去的版本,C语言:
c复制#include <stdio.h>
int main() {
int year, month, day;
scanf("%d%d%d", &year, &month, &day);
int months[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
int total = 0;
for (int i = 0; i < month - 1; i++) {
total += months[i];
}
if (is_leap && month > 2) {
total += 1;
}
total += day;
printf("%d\n", total);
return 0;
}
逐段解读一下。
第一段定义变量并用scanf读入三个整数。%d%d%d之间不写分隔符时,空格、换行都能作为分隔符,兼容性最好。第二段初始化months数组,注意数组长度是12,下标从0到11,索引月份时要减1。第三段is_leap的表达式把闰年完整规则压缩在一行里,括号把两组条件分开,可读性更好。
第四段是核心循环,累加前month-1个月的天数。循环变量i从0跑到month-2,正好对应months数组的0号到month-2号位置。第五段闰年修正,month > 2用来判断是否需要把闰年的多出一天算进去。第六段加上day并换行输出。
再补充一个更简短的查表写法,逻辑完全一样,但连循环都省了:
c复制#include <stdio.h>
int main() {
int year, month, day;
scanf("%d%d%d", &year, &month, &day);
int cum[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
int total = cum[month - 1] + day;
if (is_leap && month > 2) {
total++;
}
printf("%d\n", total);
return 0;
}
cum数组存的是"前i个月的总天数":cum[1]=31代表前1个月(1月)的总天数是31,cum[11]=334代表前11个月的总天数是334,12月31日在平年是365天,正好等于334+31。这个表的好处是省一个for循环,但需要你在考场上能快速推出来,而不是死记硬背。建议的做法是:按平年把每月天数逐项累加,得到0, 31, 31+28, 31+28+31……一路加下去。
3.3 Python版参考实现:同一套思路换个语言
如果学校的实验课允许选Python,代码会更短,逻辑也更容易读:
python复制def is_leap(year):
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
def day_of_year(year, month, day):
days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
total = sum(days[:month - 1])
if is_leap(year) and month > 2:
total += 1
return total + day
year, month, day = map(int, input().split())
print(day_of_year(year, month, day))
注意Python的切片sum(days[:month-1])正好对应C语言里累加前month-1个月,month=1时切片为空,sum为0,逻辑完全一致。
4. 高发错误清单:哪些写法会在评测机上当场翻车
4.1 错误一:闰年条件只写 year%4==0
这个错误的具体表现是:2020、2024这类普通闰年测出来全对,2021、2022这类平年也对,但一旦评测系统里放了1900年或者2100年,输出就比正确答案多1天。原因前面说过,整百年份还要额外判断能否被400整除。
我自己的检查办法是:写完闰年判断后,马上用三个特殊年份验证——2000(闰年)、1900(平年)、2100(平年)。如果这三个年份都对,闰年逻辑基本就稳了。别偷懒,不要觉得评测系统不会出这种诡异的年份,实际上整百年份恰恰是这类题目最常用的隐藏测试点。
4.2 错误二:把2月当成31天
这个错误多出现在手工写switch-case的场景。有些同学把每月天数抄错,2月写成30或31,或者把4月、6月、9月、11月的小月天数记错。要避免这个问题,最稳妥的方式是数组初始化时把12个天数写全,并且用注释标出月份:
c复制int months[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月11月12月
还有个更隐蔽的坑:有人在循环里把months[2](3月)当成2月来用。记住下标和月份的对应关系:months[0]是1月,months[1]是2月,months[2]是3月。月份减1才是下标,这个映射错位一次,整个累加结果就全乱了。
4.3 错误三:数组下标越界与循环边界
如果输入的是12月,循环要累加months[0]到months[10],循环条件是i < 11,也就是i < month - 1。很多同学这里写成i <= month,造成数组下标越界,读到一个垃圾值,算出来的结果可能在输入12月且日数较大时突然出错,让本地测试和评测结果出现诡异的差异。
循环边界建议大家用具体数字推一遍:month=3时,需要累加1月和2月,循环应该跑i=0和i=1两次,条件i<2,也就是i < month - 1。把边界条件写成i < month - 1,不要写成i < month,这个减1是整个循环最要紧的细节。
4.4 错误四:输入输出格式的细节
这类问题在在线评测系统上很常见,但不是算法问题。具体表现包括:
scanf写成scanf("%d, %d, %d", &year, &month, &day),评测数据里没逗号就会读不全,变量拿到垃圾值。- 输出少了换行符,部分平台会报格式错误。
- 把题目理解成"输出距离年末还有多少天",答非所问,白扣好几分。审题时注意"该日是该年的第几天"和"该年还剩多少天"完全是两个意思。
这些格式问题最好在提交前用肉眼检查一遍scanf和printf的格式串。题目要求输出一个整数就只输出一个整数,不要在行尾多加空格,也不要加"结果是""答案是"这类提示文字,在线评测系统只认标准输出格式。
5. 自测用例与时间边界:用穷举心态验证你的代码
5.1 一组覆盖关键分支的测试用例表
这里给一张经过筛选的测试用例表,覆盖平年、闰年、年初、年末、2月边界、整百年份。拿这几条先跑一遍,能过的话代码基本稳了。
| 输入 | 预计输出 | 说明 |
|---|---|---|
| 2021 1 1 | 1 | 平年第一天 |
| 2021 12 31 | 365 | 平年最后一天 |
| 2020 1 1 | 1 | 闰年第一天 |
| 2020 12 31 | 366 | 闰年最后一天 |
| 2021 2 28 | 59 | 平年2月最后一天 |
| 2020 2 29 | 60 | 闰年2月29日 |
| 2020 3 1 | 61 | 闰年2月后的第一天 |
| 2021 3 1 | 60 | 平年2月后的第一天 |
| 1900 2 28 | 59 | 整百年份,平年,关键用例 |
| 1900 3 1 | 60 | 1900年不是闰年的验证点 |
| 2000 2 29 | 60 | 整百年份,闰年 |
| 2000 12 31 | 366 | 整百年份,闰年加年末 |
5.2 手动推导预期输出的方法
表里的预期输出是怎么来的?以2020年2月29日为例:2020是闰年,1月31天,2月29天,所以前两个月是60天,当天就是第60天。以1900年3月1日为例:1900不是闰年,1月31天、2月28天,到3月1日就是31+28+1=60天。每一条都可以用"前几个整月的天数累加,再加上当月日期"推理出来,不需要死记结论。
如果某一条测出来和预期不一样,先别急着改代码,拿笔在纸上把累加过程写出来,看是闰年判断错、天数表错、循环边界错,还是最后的输出格式错。这个"先定位再动手"的排查顺序,能帮你省下大量盲目改代码的时间。
5.3 为什么每月1日和每月最后一天是最佳测试边界
日期类题目的隐藏坑几乎都出现在"月份切换"和"年份切换"这两个边界上。每月1日能验证上个月的天数是否算对;每月最后一天能验证当前月份的天数是否完整。把12个月的第1天和最后一天各测一遍,等于把年份内部的所有临界点都扫了一遍。再加上2月29日、12月31日这类跨年、跨闰的边界,理论上评测系统能踩的坑你都已经踩过了。
我在实际验证时还喜欢用一组"批量年份搜索法":写一个循环,把1900到2100年每一年的1月1日、2月28日、3月1日、12月31日都跑一遍,再和Python标准库算出的正确结果做对比。手写代码和标准库输出一起比对,发现不一致就立刻定位到具体年份和日期。这个思路其实就是把测试从"抽查"升级成"穷举",关键时刻能救命。
6. 变体延伸:同一道题在面试题里的N种问法
6.1 变体一:计算两个日期的间隔天数
这是"计算天数"最常见的升级版,面试里出现频率相当高。思路是定义函数days_from_epoch(year, month, day),计算某个日期距离某个固定原点的总天数,再用两个日期的差值相减。总天数的计算可以复用上面的逻辑,只要在day_of_year的结果上加上之前所有年份的总天数即可。
以公元1年1月1日为原点,计算方法可以拆成三步:
- 计算从公元1年到year-1年之间所有整年的天数总和。每年按365天算,再加上这些年份里闰年的数量。
- 用前面的
day_of_year逻辑算出这个日期在当年是第几天。 - 两步相加就得到总天数;两个日期的总天数相减,取绝对值,就是间隔天数。
其中"年份区间内的闰年数量"可以用整数除法公式快速算出来:
c复制int leap_count = (year - 1) / 4 - (year - 1) / 100 + (year - 1) / 400;
这个公式在很多日期类面试题里可以直接背下来,能省不少时间。
6.2 变体二:日期合法性校验
有些题目会反过来考:给定年月日,先判断是不是合法日期,再计算天数。合法性判断的要点在于2月的天数要动态决定:2月合法天数是闰年29天、平年28天。比如2021年2月29日不合法,2020年2月29日合法。
做法是先定义每个月的天数表,然后根据闰年修正2月天数,再判断day是否在1到该月天数之间。核心判断可以写成这样:
c复制int is_valid = (month >= 1 && month <= 12) && (day >= 1 && day <= months[month - 1]);
注意这里对2月要先做闰年修正,否则months[1]恒为28,2020年2月29日会被误判为不合法。
6.3 变体三:从"第几天"到"星期几"
还有一类高频面试题是"给定日期,求星期几"。最常用的方法是蔡勒公式,或者更朴素的思路:以某个已知星期几的日期为基准,用"相差天数模7"来计算。一旦你有了days_from_epoch函数,星期几不过就是总天数对7取模,再映射到星期几,只是要注意基准日期的星期对应关系。
这些变体在本质上都是在"天数表+闰年判断+累加"这套核心逻辑上做扩展。把这套东西吃透了,遇到任何日期题都不会慌,因为它们的骨架是同一副。
最后分享一个我个人特别推荐的习惯:凡是日期类题目,别只跑样例。拿到题先花两分钟想清楚评测系统的测试点长什么样,再花两分钟把用例表写出来,最后再动手写代码。这个习惯看起来绕了远路,实际上帮你躲开了大量的查错时间。这道"计算天数"只是15分的小题,但它在评测系统里考验的细节态度,会在你做大作业、写真实业务代码时持续带来回报。真实开发里的日期处理远比这道题复杂,时区、夏令时、各种反直觉的历法规则排着队等着你,那时候你再回头看这15分,会发现它其实是帮你建立"边界意识"的第一课。
