一年中的第几天如何处理闰年和月份前缀和?(LeetCode 1154)
简化版
给定日期 yyyy-mm-dd,先解析年、月、日,再把当前月份之前的天数累加,加上日。若是闰年且月份大于 2,需要额外加 1 天。闰年规则:能被 400 整除,或能被 4 整除但不能被 100 整除。
详细版
int dayOfYear(String date) {
int year = Integer.parseInt(date.substring(0, 4));
int month = Integer.parseInt(date.substring(5, 7));
int day = Integer.parseInt(date.substring(8, 10));
int[] days = {31,28,31,30,31,30,31,31,30,31,30,31};
int ans = day;
for (int i = 0; i < month - 1; i++) {
ans += days[i];
}
if (month > 2 && isLeap(year)) ans++;
return ans;
}
boolean isLeap(int y) {
return y % 400 == 0 || (y % 4 == 0 && y % 100 != 0);
}
月份是 1 基,数组是 0 基,循环到 month - 1 前一个月为止。
完整版教学
一、题目本质是月份前缀和
一年中的第几天 = 当前月之前所有月份天数 + 当前日。
2019-02-10
一月 31 天 + 10 = 41
所以先准备每个月天数数组,再累加前缀。
二、月份数组如何设计
非闰年月份天数:
| 月份 | 天数 |
|---|---|
| 1 | 31 |
| 2 | 28 |
| 3 | 31 |
| 4 | 30 |
| 5 | 31 |
| 6 | 30 |
| 7 | 31 |
| 8 | 31 |
| 9 | 30 |
| 10 | 31 |
| 11 | 30 |
| 12 | 31 |
用数组时,下标 0 对应 1 月,下标 month-2 对应当前月前一个月。
易错点:月份是 1 基,数组是 0 基;循环边界错一位,结果就会整月偏移。
三、闰年规则
闰年不是简单“能被 4 整除”。完整规则是:
能被 400 整除:闰年
能被 100 整除但不能被 400 整除:不是闰年
能被 4 整除但不能被 100 整除:闰年
例如 2000 是闰年,1900 不是闰年,2020 是闰年。
四、为什么月份大于 2 才加 1
闰年多出来的 1 天在 2 月 29 日。只有日期已经过了 2 月,才会影响一年中的第几天。
2020-02-10 不加 1
2020-03-01 要加 1
所以条件是 month > 2 && isLeap(year)。
五、解析日期字符串
题目通常给固定格式 yyyy-mm-dd,可以用固定下标截取:
year = date.substring(0, 4)
month = date.substring(5, 7)
day = date.substring(8, 10)
如果工程输入格式不固定,应使用日期库或更严格的 parser;算法题里按固定格式即可。
六、复杂度与优化
最多累加 11 个月,时间 O(1),空间也是 O(1)。如果要处理大量日期,可以预先准备普通年和闰年的月份前缀数组。
prefixNormal[month] = 普通年该月之前天数
prefixLeap[month] = 闰年该月之前天数
这样每次查询也是常数时间。
七、常见误区与追问
- 误区:把所有能被 4 整除的年份都当闰年。 世纪年要能被 400 整除才是闰年。
- 误区:闰年时任何月份都加 1。 只有 3 月及之后才受 2 月 29 日影响。
- 误区:月份数组下标和月份数字混用。 数组 0 对应 1 月。
- 追问:1900 是闰年吗? 不是,能被 100 整除但不能被 400 整除。
- 追问:2000 是闰年吗? 是,能被 400 整除。
- 追问:复杂度是多少? 固定 12 个月范围,时间和空间都是
O(1)。
八、加强记忆
日期题别怕,先拆成年月日,再做月份前缀和。闰年口诀:四年一闰,百年不闰,四百年再闰。最后只在 month > 2 时给闰年多加一天。