
这道题我太熟了在一些OJ平台和笔试刷题列表里编号KY18的题目就叫“今年的第几天”。别看它名字简单就是个日期计算问题但每年都能挂掉一批人。不是你不会算天数而是边界条件、闰年判断、输入输出格式这些细节稍不留神就翻车。今天我把这道题从题干到底层逻辑完整拆一遍把容易踩的坑全部摆出来。1. 题目到底在问什么一个看似简单但全是细节的日期换算1.1 核心需求解析“KY18 今年的第几天”按最常见的OJ原题描述输入是一个形如“YYYY MM DD”的日期输出这个日期是当年的第几天。比如输入“2024 03 01”要输出61因为2024年是闰年1月31天加2月29天再加上3月的1天共61天。这里有几个隐含考点闰年判断。这是老生常谈但很多人把条件记错。月份天数映射。2月平年28天闰年29天其他月份有30天和31天之分。跨月的连续累加逻辑。需要把目标月份之前的所有月份天数全部加起来再加上当月日期。输入可能包含0或不合理的日期部分OJ会要求你处理非法输入。这道题适合的人群很广刚学C语言、Python或者Java的初学者准备机试和笔试的在校生以及想快速捡回基础算法手感的工作党。它的价值在于用最少的代码量把一个涉及条件判断、数组映射、循环累加的经典流程完整走一遍。1.2 为什么这类题值得认真做我见过不少人觉得这种题“没含量”顺手写个if就交上去了。实际上日期处理是工程里出现频次极高的逻辑。日志系统要算时间戳业务报表要算季度天数排课系统要判断假期优惠券要判断过期时间——底层全都绕不开“给定日期推算偏移量”这个操作。更重要的是这道题的AC率在不少OJ平台常年不高。原因不是难而是粗心。粗心这个毛病在真实开发里代价更大所以用这种短小而精悍的题目练手成本低收益扎实。2. 从需求到方案三种主流解法与选型理由2.1 方案一逐月累加查表法最直观的思路就是先建一个每月的天数表然后从1月加到目标月的前一个月最后加上目标月的日。#include stdio.h int isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } int main() { int y, m, d; int days[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; while (scanf(%d %d %d, y, m, d) ! EOF) { int sum 0; if (isLeap(y)) { days[2] 29; } else { days[2] 28; } for (int i 1; i m; i) { sum days[i]; } sum d; printf(%d\n, sum); } return 0; }这段代码的逻辑很清楚把闰年对2月的影响落实在days数组上再循环累加。查表法最大的好处是直观改起来方便不容易漏月份。2.2 方案二switch-case结构如果编程环境比较老或者题目不允许用数组虽然很少见可以用switch-case或者if-else逐月写。比方说从12月往回倒数用累加变量把前几个月的天数加上去。这个方案的缺点显而易见代码冗余容易写串行但它的好处是不依赖额外存储在某些嵌入式笔试场合会用到。2.3 方案三预计算前缀和 / 直接调用标准库有些语言自带日期函数比如Python里可以这样import datetime y, m, d map(int, input().split()) date datetime.date(y, m, d) year_start datetime.date(y, 1, 1) print((date - year_start).days 1)这种写法在工程里没问题日常开发当然要利用标准库。但在笔试场景里用标准库要分场合。如果面试官明确要求你实现逻辑那就不要依赖库函数。而且很多OJ判题机的环境隔离得很死某些库的行为在不同版本里会有差异宁可手写也别赌。从我个人的角度如果是刷题和笔试手写查表法是最稳妥的。原因有三第一代码量小出错概率低第二不依赖任何语言特性换语言能直接平移第三面试官能看到你的计算逻辑而不是觉得你在“绕过问题”。2.4 方案选型背后的核心原则选择查表法还有一层考虑是“可解释性”。在面试现场面试官很可能会追问“如果我把输入改成2023年12月31日你的代码跑几步”查表法每一步都有明确的循环变量你能对着代码讲清楚“现在加到第几个月了为什么这里要取i m而不是i m”。这种把思路外化的能力比闷头写一段高深代码更值钱。3. 闰年判断与月份天数这道题的两个命门3.1 闰年判断的准确写法公历闰年的规则是这样的能被4整除但不能被100整除的年份是闰年或者能被400整除的年份也是闰年。写成代码就是int isLeap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); }为什么“能被400整除”这个条件不能省略因为世纪年如1900、2000、2100虽然都能被4整除但按格里高利历1900年和2100年并不是闰年只有2000年这种能被400整除的才是。如果不加这个条件遇到2100年这样的输入就会多算一天。实际校验一下2024年、2028年能被4整除且不能被100整除是闰年1900年能被4整除也能被100整除但不能被400整除不是闰年2000年能被400整除是闰年。这套规则必须一次写对因为“今年的第几天”这个题目里闰年直接影响2月的天数进而影响3月及之后所有日期的计算结果。3.2 月份天数的存储方式处理月份天数时我建议用长度为13的数组下标1到12分别对应1月到12月下标0填0作为占位。这样写的好处是下标和月份一一对应不用额外做“下标减一”的转换。int days[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};这里还有一句口诀方便记忆1月3月5月7月8月10月12月是31天4月6月9月11月是30天2月平年28天闰年29天。别小看这个表很多人把8月写成30天把7月写成30天这种错误在笔试里特别常见。有个简单的记忆方法7月和8月连续两个大月因为8月是从凯撒时代就在那儿了这段历史不需要背但数字得记对。3.3 改变2月天数的时机一个容易忽略的点是无论输入的是几月都必须在计算前把days[2]调整好。如果写成这样for (int i 1; i m; i) { sum days[i]; } if (isLeap(y)) sum 1;这也能算出结果因为闰年额外加1天。但这样写有两个坏处第一如果月份小于等于2月比如输入“2024 02 10”这个循环会加上days[1]但不会加days[2]末尾加的1天就多余了最终会多算一天。第二逻辑分散不利于代码复用。所以正确的姿势是先把days[2]设置成28或29再进循环让每个月的天数都处在它该在的位置。4. 完整实现与测试用例设计从能跑到跑稳4.1 完整代码示例我用C语言写一版完整实现结构是“读取-计算-输出”的经典流程#include stdio.h int isLeap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } int dayOfYear(int year, int month, int day) { int days[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(year)) { days[2] 29; } int total 0; for (int i 1; i month; i) { total days[i]; } total day; return total; } int main() { int y, m, d; while (scanf(%d%d%d, y, m, d) ! EOF) { printf(%d\n, dayOfYear(y, m, d)); } return 0; }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 [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] if is_leap(year): days[2] 29 return sum(days[:month]) day while True: try: y, m, d map(int, input().split()) print(day_of_year(y, m, d)) except EOFError: break这里用了一个取巧的Python切片sum(days[:month])会把下标0到month-1的所有天数加起来。由于days[0]是0正好等于1月到month-1月的天数总和。这个写法的可读性不差而且代码短。4.2 测试用例设计你要能自证正确很多新手交代码前只测一个样例这是不够的。最少要测以下几类测试输入预期输出说明2024 01 011闰年第一天2024 12 31366闰年最后一天2023 12 31365平年最后一天2024 02 2960闰年2月末2023 02 2859平年2月末2024 03 0161闰年跨2月后的第一天1900 03 0160被100整除但不被400整除的年份2000 03 0161能被400整除的世纪闰年第7和第8个用例特别重要因为它们能验证你的闰年逻辑是否完整。如果只写了year % 4 0就判断为闰年1900会被错判成闰年输出61而不是60。这种错误藏在普通用例里根本发现不了但出题人特别爱在边界年份上挖坑。4.3 输入格式与循环读入的处理OJ题目通常有多个测试用例直到文件结束。C语言里用while (scanf(...) ! EOF)Python里用try/except EOFError或者使用sys.stdin.read()批量处理。注意一点不要图省事只读一次就退出因为很多题目不会明确告诉你测试数据的组数你需要循环读入到文件末尾。还有一个小技巧如果输入里含有非法字符或者格式不标准用scanf的返回值判断可以顺带过滤。但如果你只想安心求解在本地测试的时候可以先把输入重定向到文件然后再用管道喂给程序这样能避免手动重复输入。5. 常见错误与排查技巧这些坑我全都踩过5.1 少算或多算一个月的错误这是最常见的BUG。核心在于每次循环时“目标月”的数据要不要参与累加。原则是需要累加的是“目标月之前的所有完整月份”目标月只加天数不加上整个月的天数。比如输入“2024 03 10”正确过程是加1月的31天加2月的29天再加3月的10天得到70。如果循环写成了i m就会把3月的31天也加进去结果变成101整整多了31天。所以循环边界一定要卡在i month。5.2 闰年判断中“世纪年”的漏判不做year % 400 0这个条件是第二个高频错误。我见过很老练的人都在这上面翻车因为在2000年前后的年份随便测几个都不是很容易撞到临界点。建议在代码里注释掉这个判断之前先问自己这个程序能不能正确处理2100年如果答案是“会当成闰年”那你的判断条件是有缺陷的。5.3 2月天数修改位置不当前面提到过如果把闰年额外加一天放在循环之后会导致1月和2月的输入被多算。更隐蔽的情况是你在同一个程序里处理多个测试用例第一个用例修改了days[2]为29第二个用例忘记重置成28结果平年被当成闰年。所以每处理一个用例前都要让days[2]回到初始状态或者像上面给出的方案一样在函数内部重新定义数组这样天然避开了状态残留问题。5.4 月份和日期合法性的校验部分OJ的输入只保证整数格式不保证日期一定合法比如可能给出“2023 02 30”。如果题目没有明确要求校验你忽略掉也能过如果明确要求处理非法输入就需要在计算前先做一次检查if (month 1 || month 12) return -1; if (day 1 || day days[month]) return -1;这个检查并不复杂但能看出一个人的工程素养。在面试中主动进行参数校验是加分项因为真实系统里你永远无法信任调用方的输入。5.5 数据类型溢出日期题的年份一般不会太大C语言里int足够。但如果题目扩展成“距离公元某年的第几天”或者让你算“从1970年1月1日到输入日期的天数”累加的天数会达到百万级依然int能扛住但如果你再扩展成毫秒级时间戳int就绷不住了。习惯上把total声明成long long能省去很多不必要的焦虑。5.6 本地通过但OJ报错的排查思路我调试这类题目时有一套固定流程第一步确认是不是格式问题。检查输出有没有多余的空格、中文冒号、或者换行差异。第二步确认是不是多组输入问题。有些题目要求一次读多行你如果只处理了一行就会只通过部分测试点。第三步确认是不是边界问题。把闰年、平年、1月、12月、1号、月末全测一遍。第四步如果还是错把手算结果和程序输出逐一比对。别急着猜拿一个具体的输入在草稿纸上手算看程序错了多少、错在哪一步。我之前帮人排查过一个很诡异的案例程序在本地输出完全正确在OJ上却错了一个测试点。后来发现是题目要求的输出格式带了前缀“Case 1:”而他没注意题目描述里的前缀要求。这种问题跟算法无关纯粹是审题不仔细。所以刷题第一课永远是把题目描述读三遍。6. 进阶延伸如果题目变了你还能不能写对6.1 变体一给定天数和年份反推日期“KY18 今年的第几天”的正向逻辑是知道的越多越简单。但有些题目会反着来输入年份和当年的第几天要求输出对应的月和日。这时候就不能只做加法了需要逐月做减法void convert(int year, int total) { int days[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(year)) days[2] 29; int month 1; while (total days[month]) { total - days[month]; month; } printf(%d %d\n, month, total); }这个逻辑的本质是二分查找的前身也是一道非常经典的数组扫描题。会做正推的题不一定能马上写出反推所以我建议两个方向都练一练。6.2 变体二计算两个日期之间相隔多少天如果你把两个日期都转换成“相对公元元年1月1日的总天数”然后做差取绝对值问题就变得简单了。这里需要额外注意每一年贡献的天数是365还是366代码实现时核心就是复用闰年判断。int totalDays(int y, int m, int d) { int res 0; for (int i 1; i y; i) { res isLeap(i) ? 366 : 365; } res dayOfYear(y, m, d); return res; }这种方法虽然慢但思路清晰适合笔试。进阶优化可以提前用前缀和把年份的天数累积表存下来把时间压到O(1)但这道题不要求性能我很少在三题以内的笔试里主动做优化。6.3 变体三带字符串的日期格式有些题目输入是“2024-03-01”这种字符串而不是三个单独的整数。这种情况下解析字符串就是一个额外考点。Python里可以用split(-)C语言里可以用sscanf。如果你在笔试现场遇到这种输入别慌先提取数字再复用同一个计算内核这样不会让日期逻辑和字符串解析混在一起。我自己的经验是把日期计算的核心逻辑封装成一个函数不管输入长什么样最终都传成y、m、d三个整数进去这样既干净又不容易出错。7. 实操心得与最后分享这道题我前前后后带过不少同学写过印象最深的是有人问明明手算一下就出来的题为什么非要写程序其实这个问题正好点出了程序思维的核心——把重复劳动自动化。一个人手算100个日期能算到怀疑人生但程序可以在1秒内完成全部计算。而你的价值就是把这套计算规则变成机器能执行的逻辑。写这道题的时候我强烈建议你用纸笔走一遍完整的流程哪怕你已经知道答案。先写下“输入2024年3月1日”然后想清楚“我要把哪些天数加起来”再对照代码逐行走一遍。这样做只需要5分钟但它能让你彻底理解循环和作用域这两件事比盲目刷十道题更有用。最后分享一个刷题心得做完这道题顺手把闰年判断、月份天数表、循环累加这三个片段背下来。它们会在很长一段时间里反复出现在各种编程题中背下它们不是死记硬背而是帮助你建立条件反射看到日期题就知道这些组件大概长什么样看到数组题就知道可以用下标映射数据。这种“套路感”积累多了写代码的速度和自信度都会上一个台阶。