资讯动态

KY18“今年的第几天”:闰年判断与日期计算入门必刷

发布时间:2026/10/13 12:25:38 来源:尧图企业网站定制
这道题我印象太深了KY18“今年的第几天”几乎是我刷题路上第一道真正让我觉得“咦写代码还能这么绕”的题目。题目本身不长输入一个年月日输出它是这一年的第几天看起来像是给小学生出的算术题但真上手写十个人里有六七个要栽在闰年上。这道题非常适合初学编程、准备考研机试或者刚接触在线评测系统OJ的同学拿来练手它没有复杂的算法但对基本功——循环、分支、数组、边界处理——的考察非常扎实。这篇文章我把这道题从题目拆解到三种语言实现再到踩坑记录完整地讲一遍。你如果能把这道题吃透之后遇到任何日期类的题目比如求两个日期相隔几天、判断星期几起点都会比其他人稳一大截。1. 题目拆解一句话里藏着多少考点1.1 题目描述与输入输出格式KY18这道题在题库里的标准描述大致是这样的输入一个日期格式为年、月、日输出该日期是当年的第几天。比如输入2000 1 1输出1输入2000 12 31输出366因为2000年是闰年。关键点在于题目绝不只测一两个用例OJ上通常会给出一组输入要求程序一次性处理完所有数据。很多第一次刷OJ的同学会在这里犯迷糊题目说“输入日期”到底是一个还是多个答案藏在“多组测试数据”这句话里。KY18遵循常见OJ套路会连续读入多行日期每行一组直到文件结束EOF。这意味着你没法只写一次scanf(%d %d %d, y, m, d)然后直接输出你得让它循环起来。输入的分隔符也有讲究。有的版本用空格分隔有的版本用连字符如2000-1-1还有的干脆只有一行输入。如果你按空格写死遇上连字符就全乱了。所以拿到题目第一件事不是开写而是看输入描述的细节这个习惯从这道题开始养成后面能少走很多弯路。1.2 核心考点拆解闰年判断、天数表、累加逻辑这道题说白了考三样东西。第一是闰年判断这是最核心的考点后面单开一节细说。第二是月份天数表你得把12个月的天数记清楚并且知道只有2月在闰年会从28变成29。第三是一个简单的累加逻辑把前m-1个月的天数加起来再加上d。把这三样拆开看没有一样是难的但合在一起就能筛掉不少人。我见过有人用12个if硬写也见过有人把天数写成312830...的长式子这些都能过但代码丑不说还容易错。学会用数组存天数表用循环累加才是这道题想让你练的东西。这里有个思维转变值得注意很多初学者拿到需求第一反应是“直接算”但计算机擅长的恰恰是“重复劳动”。既然每个月天数是有规律的为什么不把它们存进一张表然后循环加过去这种“数据驱动代码”的思路是日期类题目给所有人的第一课。1.3 为什么日期题总是被当作“入门必刷”日期题在OJ题库里的地位很特殊几乎每个平台都有类似题目KY18只是其中一个编号。它被当作入门必刷不是因为简单而是因为“简单中带坑”。闰年规则、月份天数、边界条件任何一个细节没想清楚测试就挂。更重要的是日期题能真实反映一个人的代码习惯。比如你有没有把数组初始化放在循环外面你有没有考虑到上次循环对数组内容的修改会影响下一次这些看似不起眼的点恰恰是后续复杂题目里会无限放大的问题。我身边的同学里能把这道题一遍写对的人后面学排序、学链表时明显更从容因为细节意识就是从这种小题目里练出来的。2. 关键原理闰年规则和天数表不能想当然2.1 闰年规则的完整表述少一条都不行闰年的完整规则是能被4整除但不能被100整除的年份是闰年或者能被400整除的年份也是闰年。写成代码就是bool isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); }这句话看着简单但我在实际带人刷题时发现超过一半的人第一次写的是y % 4 0后面的条款全丢了。为什么会丢因为大脑觉得“四年一闰”就够了完全忘了“四百年再闰”这个尾巴。为什么规则这么绕这里需要一点点天文常识。地球绕太阳一圈真正的时间是365天5小时48分46秒左右不是整整365天。公历用“四年一闰”来补但这样每四百年又会多出大约3天所以又规定整百的年份必须能被400整除才算闰年。于是1900年不是闰年2000年是闰年。这道题如果出个1900 3 1的用例只写y % 4 0的人就直接错了。我自己的记忆方法是把它当两个独立条件看第一条“能整除4但不能整除100”第二条“能整除400”。两条是或的关系。你可以把判断逻辑单独抽成一个函数而不是在main函数里堆if这样代码结构清楚后面改也好改。2.2 月份天数表这道题唯一的“参考答案”平年的月份天数基本是常识1月31、2月28、3月31、4月30、5月31、6月30、7月31、8月31、9月30、10月31、11月30、12月31。用数组表示就是int days[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};如果当年是闰年就把days[1]改成29这个操作非常直接。这里有个小提醒数组下标和月份之间差一位下标0对应1月下标1对应2月写循环的时候是i m - 1不是i m。这个“差一错误”是编程里最经典的坑几乎每个人都会踩一次。你可能会问能不能不查表用条件判断算每个月天数比如“4、6、9、11月是30天2月单独处理其他全是31天”。可以但代码可读性差很多而且容易漏。试想一下你拿到一个具体日期比如2024年11月23日你要在脑子里算它是第几天是不是也会先念叨“1月31、2月29今年闰年、3月31、4月30...”数组表就是把人类的这个思考过程原封不动搬进代码直观、不容易错、别人也好读。查表法是日期计算里最朴素但最可靠的武器没有之一。3. 三种常见实现及关键写法3.1 Python版切片一行完成累加Python写这道题最清爽列表切片加sum可以直接搞定累加部分。核心逻辑我放在下面def is_leap(year): return (year % 4 0 and year % 100 ! 0) or (year % 400 0) import sys for line in sys.stdin: line line.strip() if not line: continue y, m, d map(int, line.split()) month_days [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] if is_leap(y): month_days[1] 29 print(sum(month_days[:m - 1]) d)这里我用的是sys.stdin逐行读取而不是input()因为在OJ上输入可能很多逐行处理更稳。sum(month_days[:m - 1])的意思是把前m-1个月的天数相加注意切片终点是m-1不是m否则就把当前月整个算进去了。Python的便利性在这里体现得淋漓尽致但有个隐患需要注意如果你把month_days这个列表定义在for循环外面又在循环里改month_days[1] 29那下一次循环时它可能还是29导致非闰年被算错。我在第4章会专门展开这个问题这属于“看着没错但实际是个坑”的典型。3.2 C/C版scanf循环与EOF的正确姿势C语言版是刷OJ的标配也是最需要小心的版本。先看完整代码#include cstdio bool isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } int main() { int y, m, d; while (scanf(%d %d %d, y, m, d) ! EOF) { int days[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(y)) { days[1] 29; } int ans 0; for (int i 0; i m - 1; i) { ans days[i]; } ans d; printf(%d\n, ans); } return 0; }关键点在第一行的while (scanf(...) ! EOF)。scanf的返回值是成功读入的参数个数如果读到文件末尾它会返回EOF一般是-1。所以这个while循环的意思是只要还能读进三个整数就一直处理。这是OJ多组输入的标准写法建议当成固定模板记下来。另外注意我把days数组定义在了while循环内部。这意味着每组数据进来时数组都是全新初始化的不会被上次的闰年状态影响。如果你把它定义在main函数外面或者循环外面第二组数据来时days[1]可能还是29那就出事了。这个问题在Java版里特别容易出现C因为可以在循环内重新定义数组反而安全一些。3.3 Java版最容易踩数组状态污染的版本Java版的写法整体和C类似但因为Java不允许在循环内随便重新定义一个局部数组后还能保持简洁很多同学会把天数数组定义成成员变量或main里的局部变量于是问题就来了import java.util.Scanner; public class Main { static boolean isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } public static void main(String[] args) { Scanner sc new Scanner(System.in); int[] days {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; while (sc.hasNextInt()) { int y sc.nextInt(); int m sc.nextInt(); int d sc.nextInt(); if (isLeap(y)) { days[1] 29; } else { days[1] 28; } int ans 0; for (int i 0; i m - 1; i) { ans days[i]; } ans d; System.out.println(ans); } sc.close(); } }注意我做了什么每次循环里在if (isLeap(y))的else分支中显式把days[1]重置为28。如果你只写if (isLeap(y)) { days[1] 29; }第一组输入是闰年第二组输入是平年你就会拿29去算平年结果直接错误。这个问题在现实中出现频率极高核心原因就是“数组是可变对象它的状态会跨循环保留”。如果你不想每次重置可以在循环体内部用new int[]{...}重建数组但Java里这样写反而啰嗦。最优雅的做法是把days数组定义为“平年天数表”永远不改它判断闰年后在计算总和时单独加一个修正值。这个思路其实就是前缀和优化下一章我会展开。3.4 三种语言实现对比语言代码行数主要风险点推荐指数Python12行左右列表状态污染、循环读取方式新手最友好C/C17行左右EOF写法、数组初始化位置刷OJ首选Java20行左右数组状态污染、Scanner性能面试手写可用从刷OJ的角度我更推荐先用C把这题写明白因为它逼你理解EOF、数组、循环这些底层的细节。Python更像“用高级工具快速解决”适合用来验证思路。Java是面试场景的常客手写时要特别注意数组重置。三种语言都实现一遍花不了半小时但效果比只盯着一个语言猛写强得多。4. 实操中的坑与排查经验4.1 输入格式的坑空格、连字符和EOF这道题在牛客这类平台上标准输入是空格分隔但在一些变种题目里可能用-分隔。如果你用scanf(%d-%d-%d, y, m, d)可以处理连字符但处理不了空格反过来也是一样。最稳妥的办法是看清题目的输入样例如果样例是2000-1-1就按连字符解析如果是2000 1 1就按空格解析。不要试图写一个万能解析器输入格式是题目说死的部分你只需要匹配它。EOF的坑则更隐蔽。如果你写的是while (scanf(%d%d%d, y, m, d) 3)效果等价于判断是否读满了三个数这也可以。但如果你写成while (true)然后期待输入组数固定就可能死循环或越界。多组输入必须有一个终止条件EOF就是这个条件。掌握EOF的语义其实是理解“流式输入”的第一步后面处理文件、处理网络数据流还会反复遇到。4.2 多组数据下的数组状态污染代码运行不报错但结果错这是我在帮人debug时遇到过最多的问题。症状非常迷惑单测一组数据是对的连续跑两组就错而且错得毫无规律。原因就是我前面提到的天数数组在第一次循环里被改成闰年版本第二次循环没有重置。这个问题为什么难发现因为程序不会崩溃输出也有模有样只是某些用例对、某些用例错。排查思路是检查所有修改过数组内容的代码路径确认每个分支都有配套的恢复逻辑把数组定义放到循环内部让每次循环都得到全新数组使用不可变的天数表平年表再用额外变量修正闰年影响。第三个思路最干净。你可以定义平年表days[12]永远不变计算时判断“如果是闰年且月份大于2则总天数加1”。为什么是“月份大于2”因为2月本身的天数变化发生在月底只有当你把2月完整过完、进入3月及以后闰年才多出来的那一天才会计入总天数。这个细节是很多人的盲区也是面试官喜欢追问的点。4.3 常见错误速查表错误写法后果正确做法y % 4 0判断闰年1900年被误判为闰年完整判断 (y%40 y%100!0)循环写i m多加了本月完整天数应为i m - 1定义天数数组在循环外且不重置闰年状态污染后续数据循环内重建数组或用不可变表加修正忘记处理多组输入只输出第一组结果用while (scanf(...) ! EOF)或while (sc.hasNextInt())输入用空格解析但题目是连字符解析失败或读到错误字符按题目样例选择解析方式闰年修正无条件加11月的日期也被错误加1加1前判断m 2这张表我建议截图保存。它不只是针对KY18这道题的里面每一条在后续刷任何日期题时都可能遇到。尤其是“闰年修正要判断月份是否大于2”这条很多人第一次看到都不服觉得自己懂了结果在“求两个日期相隔天数”的题目里又摔一跤。4.4 一组完整的边界测试用例写完代码不是结束测完用例才算完。这道题的测试用例要覆盖以下六类情况输入2000 1 1 输出1 # 闰年第一天 输入2000 2 29 输出60 # 闰年2月29日31 29 输入2000 12 31 输出366 # 闰年最后一天 输入1900 2 28 输出59 # 非闰年2月28日31 28 输入2023 12 31 输出365 # 平年最后一天 输入2024 3 1 输出61 # 闰年3月1日31 29 1这六组用例全过你的代码基本就稳了。注意这里特意选了2000和1900就是专门测世纪年的闰年判断选了2月29日和3月1日是测月份边界和修正逻辑选了12月31日是测全年总天数。测试用例的设计本身也是一种能力能想到覆盖哪些边界点比会写代码更能体现功底。5. 进阶从“会做”到“懂日期计算”5.1 前缀和优化一次性算出前n个月的天数如果题目只是做一次查询循环累加一点问题没有。但如果查询次数上万比如给你一万个日期每个都从1月循环到m月时间开销就上来了。这时候可以用前缀和数组把“前n个月的天数总和”提前算好days_before [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] def day_of_year(y, m, d): ans days_before[m - 1] d if m 2 and is_leap(y): ans 1 return ansdays_before这个数组的含义是下标i对应的值表示“前i个月总共有多少天”。比如days_before[1]是31表示前1个月1月过了31天days_before[2]是59表示前2个月1月加2月过了59天平年。这样每次查询只需要O(1)时间不需要循环累加。这个优化思路的本质是空间换时间把已知的、重复的、不会变化的计算结果提前存好。你会发现后面学动态规划时前缀和思想还会反复出现。所以这道题不只是一个日期题它是一扇窗能看到优化最基本的样子。5.2 真实工程里直接调库但你要知道库里发生了什么刷题归刷题真实项目里基本不会自己手搓日期计算直接用现成库。Python的datetime一句就搞定from datetime import datetime print(datetime(2024, 3, 1).timetuple().tm_yday) # 61Java则可以用YearMonth、LocalDate等类。但我想强调一个观点调库不丢人不懂库在干什么才危险。如果你自己写不对闰年规则你连datetime为什么会输出61都解释不了。一旦某个日期数据出了偏差你连排查的方向都没有。所以刷题阶段老老实实手写一遍是为了给将来的工程能力打底。这也是为什么很多面试官喜欢从日期题开始问先让你手写再问“你知道库里是怎么实现的吗”。答得出底层逻辑的人和只会调API的人一轮问答就分出来了。5.3 衍生题求两个日期间的间隔天数KY18再往前走一步就是经典衍生题“给定两个日期求之间相隔多少天”。思路其实很简单把两个日期都转换成“从某个固定的公元纪年起点到该日期的总天数”然后相减取绝对值。具体的转换方法就是给day_of_year加一个年份维度def days_from_epoch(y, m, d): days_before [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] total y * 365 total (y - 1) // 4 - (y - 1) // 100 (y - 1) // 400 total days_before[m - 1] d if m 2 and is_leap(y): total 1 return total中间那行(y - 1) // 4 - (y - 1) // 100 (y - 1) // 400是在计算公元元年到y年之前一共经历了多少个闰年日。理解这一行的关键还是闰年规则每4年一个闰日但每100年去掉一个每400年再加回来。如果你能把这个公式看懂你对闰年规则的理解就已经超过很多工作两三年的程序员了。这个衍生题非常推荐作为下一个刷题目标因为它把“第几天”里学到的所有东西都复用了还增加了年份跨度的处理。做会它日期类的题基本就可以毕业了。5.4 刷题之外的体会我个人的习惯是做一道题不只是为了过而是把它当成一个“知识点胶囊”。KY18这颗胶囊里装着差一错误、数组可变性、EOF语义、闰年边界、前缀和思想。任何一个知识点拿出来都能在后面某道更难的题里派上用场。后面我再看到“这不就是KY18吗”的变种题——比如日历布局、时间戳换算、生日年龄计算——都会先回到这道题的代码模板上改一改心里特别有底。所以我真心建议你把这道题的三版代码都亲手敲一遍核对上面那张测试用例表然后试着改成前缀和写法算是把这一套组合拳打完。等你完成这些再回头看刚拿到题目时那种“简单但心里发虚”的感觉应该就完全消失了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑