LeetCode 1154. 一年中的第几天
题目描述
题意分析
给一个形如
YYYY-MM-DD的日期字符串,返回这一天是当年的第几天。一月一日算第1天。格式是完全固定的:年份四位、月份两位、日期两位、分隔符是横线,位数不足会补前导零。这个信号很关键——它意味着每个字段的起止下标是常量,不需要按分隔符切分,也不用担心
2019-1-9这种不规则写法。题目还保证日期本身合法,年份范围在
1900到2019之间。合法性有保证,所以不用校验「二月三十日」这类输入;而年份范围恰好跨过1900和2000两个特殊年份,正好能考出对世纪年规则的理解。边界要单独想清楚三种:月份是一月时不需要累加任何整月;日期不跨过二月时,是不是闰年根本不影响答案;日期在二月之后且当年是闰年时,必须多算一天。
解法:月份天数累加
核心思路
一个想当然的思路是先算出「从元旦到这一天的天数差」,可要真去做日期减法,反而要重新处理进位和月长,绕了一大圈。瓶颈在于把问题想复杂了:既然起点固定是当年一月一日,答案其实就是一个前缀和。
把年内的日子按月切段来看,答案自然分成两块:完整走过的那些月份的天数总和,加上当月已经过去的天数。后者就是
day本身(因为一号算第一天),前者是1到month - 1月的月长之和。月长里只有二月是变量,其余十一个月都是常量。所以整个问题被压缩成一件事:判断这一年是不是闰年。公历的闰年规则是「四年一闰,百年不闰,四百年再闰」,写成条件就是「能被
400整除,或者能被4整除但不能被100整除」。不变量是:循环累加到第
i轮时,answer恒等于「当年前i个月的总天数」加上day。 初始i = 0时answer就是day,符合定义;每轮把第i + 1个月的月长加进去,不变量保持;循环在i达到month - 1时停止,此时累加的正好是当前月之前的所有整月,answer即为答案。
解题步骤
- 手写一个
parseNumber从固定下标区间截取数字。年份取[0, 4)、月份取[5, 7)、日期取[8, 10),跳过下标4和7上的横线。用「乘十加个位」逐位累积,而不是调用现成的字符串转数字接口,是因为面试里这一步本身就是考点。- 建一张十二个月的天数表,二月先按平年的
28天填。用数组而不是一串条件判断,是为了让后续累加变成一个干净的循环。- 判断闰年,若成立就把表里二月改成
29。改表比在累加时加特判更好,因为它把「闰年」这个信息一次性收敛到数据里,后面的逻辑完全不必再关心年份。- 令
answer = day作为起点,这一步已经把当月的天数计入。- 循环
i从0到month - 2,把days[i]累加进answer。上界写成month - 1(不含)是因为数组下标比月份编号小一,而当前月本身不能整月计入。- 返回
answer。以
date = "2019-02-10"走一遍:解析得year = 2019、month = 2、day = 10。闰年判断:2019 % 400是19不为零;2019 % 4是3不为零,所以整个条件为假,二月保持28天。answer初始为10。循环上界是month - 1 = 1,只跑i = 0一轮,加上一月的31天,answer变成41。返回41。再以date = "2004-03-01"走一遍:解析得year = 2004、month = 3、day = 1。闰年判断:2004 % 400是4不为零,但2004 % 4为零且2004 % 100是4不为零,条件成立,days[1]改成29。answer初始为1;循环跑两轮,i = 0加一月的31得32,i = 1加二月的29得61。返回61。最后以date = "1900-03-01"检验世纪年:1900 % 400是300不为零;1900 % 4为零但1900 % 100也为零,所以后半个条件为假,1900不是闰年,二月仍是28天,答案是31 + 28 + 1 = 60,比2004年同一天少一天。
代码实现
// 一年中某天的序号可以拆成两部分:当前月之前所有月份的天数总和,再加上当前日期。
class Solution {
public int dayOfYear(String date) {
int year = parseNumber(date, 0, 4);
int month = parseNumber(date, 5, 7);
int day = parseNumber(date, 8, 10);
int[] days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (isLeapYear(year)) {
days[1] = 29;
}
int answer = day;
for (int i = 0; i < month - 1; i++) {
answer += days[i];
}
return answer;
}
private int parseNumber(String date, int start, int end) {
int value = 0;
for (int i = start; i < end; i++) {
value = value * 10 + date.charAt(i) - '0';
}
return value;
}
private boolean isLeapYear(int year) {
return year % 400 == 0 || year % 4 == 0 && year % 100 != 0;
}
}
// 一年中某天的序号可以拆成两部分:当前月之前所有月份的天数总和,再加上当前日期。
func dayOfYear(date string) int {
year := parseNumber(date, 0, 4)
month := parseNumber(date, 5, 7)
day := parseNumber(date, 8, 10)
days := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
if isLeapYear(year) {
days[1] = 29
}
answer := day
for i := 0; i < month-1; i++ {
answer += days[i]
}
return answer
}
func parseNumber(date string, start int, end int) int {
value := 0
for i := start; i < end; i++ {
value = value*10 + int(date[i]-'0')
}
return value
}
func isLeapYear(year int) bool {
return year%400 == 0 || year%4 == 0 && year%100 != 0
}
复杂度分析
- 时间复杂度:$O(1)$,输入长度恒为
10,解析三个字段各扫固定位数,累加循环最多跑11轮,全部与任何可变规模无关。- 空间复杂度:$O(1)$,只有一张长度为
12的常量月长表和几个整型变量,都不随输入变化。
关键点总结
- 「第几天」这类问题的通用形态是前缀和:把周期切成段,答案等于完整段的总和加上当前段内的偏移。想清楚这一点,就不必去做真正的日期减法。
- 把可变因素收敛进数据而不是散落在控制流里。这里把闰年的影响一次性写进月长表,后续累加逻辑就完全不必再感知年份,代码分支数直接减半。
- 格式完全固定的字符串应该按下标直接取,而不是先切分再转换。少一层中间结构就少一层出错空间。
- 闰年规则必须完整写出三条,尤其是「百年不闰、四百年再闰」这一层。只写
year % 4 == 0在1900这类年份上就会错,而这题的年份范围恰好包含1900。- 面试视角:这题真正的考点是手写解析和闰年判断,直接调用日期库会让考察点全部落空。要主动写出
parseNumber而不是用现成接口,并说明为什么。- 面试视角:常见追问是「怎么反过来,从第几天还原成日期」。答案是从月长表里依次扣减,直到不够扣为止,剩下的就是日;能顺带说明闰年表要先改好,会显得考虑周全。
易错点总结
- 错误写法:闰年只判断
year % 4 == 0。用例"1900-03-01"→1900能被4整除但它是世纪年且不能被400整除,实际不是闰年;该写法会把二月算成29天,返回61而正确答案是60。- 错误写法:闰年写成
year % 100 != 0 || year % 400 == 0却漏掉year % 4 == 0。用例"2019-03-01"→2019不能被100整除,条件直接为真,被误判成闰年,返回61而正确答案是60。- 错误写法:累加循环写成
for (int i = 0; i < month; i++)。用例"2019-01-09"→ 把当前月的整月天数也加了进去,返回9 + 31 = 40,而正确答案是9。- 错误写法:
answer初始化为0后只累加整月天数,忘记加上day。用例"2019-02-10"→ 返回31,少了当月已过的十天。- 错误写法:解析月份时取下标区间
[4, 7)。用例"2019-02-10"→ 把横线也读进去,'-' - '0'得到负数,月份被算成一个错误值,后续循环次数完全不对甚至越界。- 错误写法:假设日期字段没有前导零,用「按横线切分再取第二段的首字符」之类的方式解析。用例
"2019-02-10"→ 月份读成0,循环一次都不跑,返回10而正确答案是41。- 错误写法:把月长表写成从下标
1开始的十三个元素,却仍然用days[i]配合i从0起步。用例"2019-03-05"→ 首元素是占位值,累加结果整体错位,返回值比正确的64少或多一个月的天数。- 错误写法:判定闰年后直接给
answer加一,而不区分月份。用例"2004-01-15"→ 一月根本没跨过二月,闰年与否毫不相干,却被多算一天,返回16而正确答案是15。- 错误写法:Java 里写
date.charAt(i) - '0'时忘记运算符优先级,把表达式写成value * 10 + date.charAt(i) - '0'之外的形式,例如value * 10 + (date.charAt(i) - '0') * 10。用例"2019-02-10"→ 每一位都被额外放大十倍,解析出的年月日全部失真。- 错误写法:把月长表声明为静态常量并在闰年时就地改成
29,却不在下次调用前还原。用例先跑"2004-03-01"再跑"2019-03-01"→ 第二次调用沿用了上一次留下的29,返回61而正确答案是60。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 1360. 日期之间隔几天 | 简单 | 跨年份求差值,需把日期统一折算成绝对天数 |
| 1344. 时钟指针的夹角 | 中等 | 同样是周期换算,重点在分针带动时针的连续位移 |
| 8. 字符串转换整数 (atoi) | 中等 | 格式不固定,需处理空白、符号与溢出截断 |
| 93. 复原 IP 地址 | 中等 | 分隔位置未知,要靠回溯枚举并校验每段合法性 |
| 468. 验证IP地址 | 中等 | 重点在前导零、段数、进制等格式规则的完整覆盖 |
| 989. 数组形式的整数加法 | 简单 | 逐位处理并维护进位,考察手写数字运算的细心度 |