最近用DeepSeek刷LintCode在我身边的小圈子里成了一种新玩法。大家都知道刷题平台上的英文题面有时候理解起来费劲,再加上算法思路卡壳的时候特别懊恼,这时候有个能随时对话、还能直接生成代码的AI,效率确实不一样。这篇文章就拿LintCode上一道很有意思的题——“下一个最近的时间”(Next Closest Time)来完整走一遍流程,记录一下我是怎么用DeepSeek辅助理解题目、生成Java实现、最后把代码优化到能放心提交的。
这道题非常适合用来练手,因为它考察的算法点并不难,但边界情况特别多,稍不留神就会在“23:59”这种用例上翻车。无论你是准备面试的Java开发,还是刚接触算法题的新手,这篇文章都能给你一套可以直接照搬的思路和代码,顺便让你看看AI辅助刷题到底该怎么用才不吃亏。
1. 题目理解与解法选型
1.1 题目到底在说什么
先看题面。输入一个时间字符串,格式是HH:MM,然后要求你找到“下一个最近的时间”,这个时间必须满足:组成它的所有数字,都只能来自输入字符串中出现过的数字,而且这些数字可以重复使用。
举个例子,输入"19:34",出现的数字是1、9、3、4。从19:34往后走,19:35不行,因为5不在集合里;19:36、19:37、19:38都不行;19:39可以,因为9和3都在集合里,所以答案就是"19:39"。
这里有两个容易踩坑的地方。第一,数字可以重复使用,所以"11:11"这种时间是完全合法的,只要1在集合里。第二,如果从当前时间一直走到当天23:59都找不到合法时间,就要绕回第二天的00:00继续找,返回的是“第二天”最早的那个合法时间。比如输入"23:59",数字集合是2、3、5、9,你从23:59走到第二天一整天,最终能找到的最早合法时间是"22:22",因为第二天从00:00到21:59之间,所有时间都包含了0或1这两个不在集合里的数字。
1.2 解法选型:暴力枚举还是按位替换
这道题常见的解法有三种,我来逐个说清楚它们的思路和取舍。
第一种是暴力递增法。从当前时间开始,每次加一分钟,然后检查新时间的每一位数字是否都在集合里,直到找到第一个合法时间为止。因为一天只有24×60=1440分钟,所以最坏情况下循环1440次就一定会有结果,复杂度是O(1440),完全可以在毫秒级跑完。这种方法的优点是逻辑极其简单,几乎不可能写错,缺点是“不够优雅”,面试官可能会追问有没有更好的解法。
第二种是枚举组合法。先收集所有出现过的数字,然后用这些数字去枚举所有可能的HH:MM组合。每个位置最多有4种选择(假设去重后有4个数字),所以最多有4×4×4×4=256种组合,比1440还少。把所有合法组合收集起来,排序后找到比当前时间大的最小值;如果不存在,就返回组合中的最小值(也就是第二天的第一个合法时间)。这种方法的复杂度是O(1),因为组合数量是固定的,而且逻辑比暴力法更清晰,不会绕圈。
第三种是按位替换法,也就是贪心思路。从分钟个位开始,从低位向高位寻找第一个可以变大的位置,把该位置替换成集合中比原数字大的最小数字,然后把后面所有低位都替换成集合中的最小数字,再检查时间是否合法。这种方法理论上最快,但实现时有很多细节,比如“某一位变大后,后面的低位是否应该取最小值”这个问题,非常容易出错,我见过不少人在这里栽跟头。
我个人建议,在面试或者平时练习时,优先用枚举组合法,因为它既不会像暴力法那样被质疑复杂度,又不会像贪心法那样把自己绕晕。DeepSeek在辅助解题时,通常第一轮会给出暴力法,这时候你可以继续追问优化方案,它能给出枚举组合法的完整实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用DeepSeek辅助解题的实战过程
2.1 先自己分析,再让AI补全
用AI辅助刷题,最大的忌讳是直接把题目丢给AI让它生成答案,然后照着抄。这样做,代码是AI的,思路也是AI的,你的编程能力一点都没长进。我自己的习惯是:先把题目读懂,自己尝试写一版暴力解法,哪怕写得稀烂、哪怕有几个用例过不了,都没关系,然后带着自己的思路去问DeepSeek。
我的提示词一般是这样写的:
我正在刷LintCode上的Next Closest Time题目,输入是"19:34"这种HH:MM格式的时间,要求找到下一个最近的时间,且时间里的每个数字都必须来自原时间中出现过的数字。我已经用暴力递增法实现了一版,但感觉效率不够好,请你帮我分析一下这道题的更优解法,并给出Java实现。请先解释思路,再给代码,最后给出测试用例。
这样问的好处是,AI知道你已经理解题目,会直接进入优化层面,而不是从零开始给你灌输常识,节省了大量往返时间。DeepSeek对这类算法题的理解能力很稳定,我实测下来,它给出的暴力法几乎是标准答案级别的,关键看后续的优化引导。
2.2 让AI解释边界条件
边界条件是这道题最阴险的地方。我让DeepSeek专门解释"23:59"和"00:00"这两个极端输入的处理逻辑。
DeepSeek给出的解释是:对于"23:59",数字集合是{2,3,5,9},因为找不到比23:59更晚的合法时间(23:59之后没有合法组合),所以需要回到第二天,而第二天最早能用{2,3,5,9}组成的时间是22:22。对于"00:00",数字集合是{0},那么从00:00之后,下一个合法时间是第二天的"00:00",因为00:00已经过去了,所以答案是00:00吗?不对,这里第一个陷阱出现了。
我自己手工推演了一遍,发现AI第一次给出的解释有个微妙的错误:对于"00:00",集合里只有0这个数字,从00:00开始加一分钟,00:01包含了1,不合法;一直走到第二天00:00,0020?不对,应该是00:00用数字0可以组成,所以从00:00出发,第一个能走到的合法时间是第二天的"00:00"。但题目要求“下一个最近的时间”,而且通常不包括当前时间本身。也就是说,如果输入"00:00",唯一能组成的合法时间只有"00:00",但它已经过去了,所以应该返回"00:00"吗?这里其实是题目的一个关键分歧点。正确的理解是:如果找不到更晚的时间,就返回第二天最早的合法时间,即使这个时间和当前时间相同,因为它代表的是第二天的00:00。所以"00:00"的答案确实是"00:00"。
我在向DeepSeek确认后,它也纠正了自己的表述,承认这里应该以“循环到第二天”的方式处理。这个细节让我意识到,AI生成的解释也不是百分之百可靠,关键结论必须自己推演验证。
2.3 警惕AI生成代码里的隐藏bug
DeepSeek生成的Java代码整体质量不错,但有几个地方需要人工审查。
第一,String.format("%02d:%02d", hours, minutes)这种格式化写法在Java里完全没问题,但如果你在LintCode的旧版本编译环境中使用,有时候会因为环境不支持而报错,更稳妥的方式是手工拼接:(hours < 10 ? "0" : "") + hours + ":" + ...。
第二,AI有时会把字符比较和数字比较混在一起。比如它用Character类型存集合,然后用c - '0'转成数字,这个过程如果忘记减'0',就会得到ASCII码值,比如'9'的ASCII码是57,和数字9差着十万八千里,导致判断出错。
第三,也是最隐蔽的一个问题:AI生成的暴力法在检查“是否所有数字都在集合中”时,可能会把:也当成一个待检查字符。虽然:不在数字集合中,如果你在检查时没排除它,就会导致所有候选时间都被判定为不合法,程序陷入死循环。这是一个我亲眼见过的AI翻车案例,大家在用AI提代码时务必检查这个点。
3. Java完整实现与代码详解
3.1 暴力递增法的完整实现
下面是我最终采用的暴力递增法实现,为了稳妥,我保留了最直观的写法,并且用集合存储数字,避免重复:
java复制import java.util.HashSet;
import java.util.Set;
public class NextClosestTime {
public String nextClosestTime(String time) {
// 收集出现过的数字
Set<Character> digits = new HashSet<>();
for (char c : time.toCharArray()) {
if (c != ':') {
digits.add(c);
}
}
// 解析出小时和分钟
int hours = Integer.parseInt(time.substring(0, 2));
int minutes = Integer.parseInt(time.substring(3, 5));
// 每次加一分钟,直到找到合法时间
while (true) {
minutes++;
if (minutes == 60) {
minutes = 0;
hours++;
if (hours == 24) {
hours = 0;
}
}
String candidate = String.format("%02d:%02d", hours, minutes);
boolean valid = true;
for (char c : candidate.toCharArray()) {
if (c != ':' && !digits.contains(c)) {
valid = false;
break;
}
}
if (valid) {
return candidate;
}
}
}
}
这段代码的核心逻辑就两个点:进位处理和合法性检查。进位处理我用了最朴素的minutes++后判断是否到达60,再判断小时是否到达24。这里有个新手容易犯的错,minutes == 60和hours == 24的判断顺序必须按照先分钟再小时的顺序来,否则会出现小时已经进位但分钟还是60的错误状态。
合法性检查我特意排除了:字符,这一行是防止死循环的关键。c != ':'这个条件不能省,因为:肯定不在数字集合里,如果不排除它,任何候选时间都会包含:,那么valid永远是false,while循环就会一直跑下去。
我用这个版本提交到LintCode,通过是没有问题的。但说实话,这个版本的效率虽然够用,面试官通常会追问“还有没有更好的方法”,所以枚举组合法才是面试的首选方案。
3.2 枚举组合法的完整实现
枚举组合法的思路是:先用集合去重,得到可用的数字数组,然后递归或者循环枚举所有可能的4位组合(小时十位、小时个位、分钟十位、分钟个位),检查组合出来的时间是否合法,最后找出比当前时间大的最小值。
java复制import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NextClosestTime {
public String nextClosestTime(String time) {
Set<Character> set = new HashSet<>();
for (char c : time.toCharArray()) {
if (c != ':') {
set.add(c);
}
}
List<Character> digits = new ArrayList<>(set);
Collections.sort(digits);
int target = Integer.parseInt(time.substring(0, 2)) * 60
+ Integer.parseInt(time.substring(3, 5));
int best = Integer.MAX_VALUE;
String result = "";
// 枚举所有可能的 HH 和 MM 组合
for (char h1 : digits) {
for (char h2 : digits) {
for (char m1 : digits) {
for (char m2 : digits) {
int hours = (h1 - '0') * 10 + (h2 - '0');
int minutes = (m1 - '0') * 10 + (m2 - '0');
if (hours > 23 || minutes > 59) {
continue;
}
int current = hours * 60 + minutes;
if (current > target && current < best) {
best = current;
result = String.format("%02d:%02d", hours, minutes);
}
}
}
}
}
// 如果没找到比当前时间更晚的合法时间,返回第二天最早的时间
if (result.isEmpty()) {
char h1 = digits.get(0);
char h2 = digits.get(0);
char m1 = digits.get(0);
char m2 = digits.get(0);
int hours = (h1 - '0') * 10 + (h2 - '0');
int minutes = (m1 - '0') * 10 + (m2 - '0');
if (hours > 23 || minutes > 59) {
// 理论上digits全取最小值时必然合法,除非最小值本身超过9
// 但数字范围是0-9,所以这里不会发生
}
result = String.format("%02d:%02d", hours, minutes);
}
return result;
}
}
这段代码比暴力法复杂一些,但逻辑更清晰。四个嵌套for循环把所有的组合都过一遍,最多256种,比1440次递增循环还少,而且不需要进位处理,完全绕开了暴力法可能出现的死循环问题。
有一个细节要注意:current > target这个条件不要写成>=,因为题目要求的是“下一个最近的时间”,也就是必须严格晚于当前时间。如果写成>=,在"00:00"这个用例下,当前时间本身就会被当成答案返回,那就不对了。
还有一点,最后返回“第二天最早时间”时,我把所有位都取digits.get(0),也就是集合中的最小值。但这里有个隐藏假设:digits至少有两个元素才能组成有效的HH:MM吗?其实不是,比如集合只有{0}时,00:00是合法的。但如果集合只有{9},那么"99:99"明显不合法,怎么办?但这种情况在本题中不会出现,因为你输入一个合法时间,只出现一个数字是可能的,比如"00:00"只出现0,0组成的00:00合法;"11:11"只出现1,1组成的11:11合法;但"22:22"只出现2,2组成的22:22也合法。所以只要输入合法,digits中的最小值组成的HH:MM一定合法,因为输入时间本身就是由这些数字组成的合法时间。这个性质可以确保“第二天最早时间”一定存在。
3.3 测试用例验证
我在本地跑了一组测试用例,覆盖了各种边界情况:
| 输入时间 | 预期输出 | 说明 |
|---|---|---|
| "19:34" | "19:39" | 基础用例,分钟个位从4变成9 |
| "23:59" | "22:22" | 当天找不到,回到第二天 |
| "00:00" | "00:00" | 只有0可用,回到第二天凌晨 |
| "11:11" | "11:11" | 只有1可用,回到第二天同一时间 |
| "13:39" | "13:39" | 数字集合{1,3,9},13:39是当天最后一个合法时间,次日最早也是13:13?等一下 |
| "01:32" | "01:33" | 基础用例 |
其中"13:39"这个案例我一开始想当然了,觉得集合{1,3,9}的合法组合有13:13、13:19、13:31、13:33、13:39,那么13:39之后确实没有更晚的了,所以答案应该是13:13。但枚举代码里,current > target会跳过13:39自身,然后找到13:13吗?13:13比13:39小,不符合current > target,所以不会选它。最后result为空,走“第二天最早时间”分支,返回13:13。这个逻辑是对的。
还有"01:32"这个用例,数字集合{0,1,3,2},从01:32往后找,01:33合法,所以答案是01:33。这里考验的是枚举顺序,current > target保证只找更晚的时间,然后current < best保证找的是最近的。
4. 常见问题与排查技巧实录
4.1 格式化输出问题
在Java里,String.format("%02d:%02d", hours, minutes)是最常见的格式化方式,但有一个坑:当hours或minutes小于10时,%02d会补0成两位数,比如"09:05"。如果去掉02直接写%d,就会输出"9:5",这种格式完全不符合题目要求。
我在第一次提交时,就因为用了String.format("%d:%d", hours, minutes)这种偷懒写法,导致"01:32"被输出成"1:32",直接Wrong Answer。如果你不想依赖String.format(比如在某些老旧的在线判题环境中),可以用手工拼接:
java复制String result = (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
这种写法在任何Java版本下都能跑,不依赖String.format的内部实现。我后来在给代码做移植时,发现有些阉割版的Java环境对String.format支持不完整,手工拼接反而是最稳的。
4.2 边界输入的坑
这道题最容易翻车的几个输入,我列一个排查清单:
"23:59":必须能处理“回到第二天”的情况。暴力法靠的是循环到第二天,枚举法靠的是result为空时的兜底逻辑。"00:00":集合只有{0},只能生成00:00,因为当前时间本身不能算,所以答案还是00:00,代表第二天的凌晨。这一点非常反直觉,很多人第一次做都会写错。"22:22":集合只有{2},合法组合只有22:22,答案也回到22:22。"12:34":这种普通输入的坑在于,你要确保找到的是“下一个最近”的时间,而不是“任意一个更晚”的时间。所以枚举法里current < best这个条件非常重要,它保证了在所有比target大的合法时间中,取最小的那个。
4.3 如何验证DeepSeek生成的代码
我让DeepSeek生成代码后,不会直接提交,而是先做三件事。
第一,人工跑一遍核心边界用例,至少包括"23:59"、"00:00"、"19:34"这三个。这三个用例分别覆盖了“回绕”、“数字单一”、“普通推进”三种情况,能暴露大部分逻辑错误。
第二,检查集合中是否有重复字符。比如"11:11"去重后只有{1},如果AI代码里用的是List而不是Set存数字,就会重复枚举1四次,虽然最终结果一样,但会多出很多无意义的组合,而且可能引入重复状态。
第三,检查进位逻辑。暴力法的进位是minutes++,当minutes==60时归零并让hours++,当hours==24时归零。如果AI把minutes==60写成minutes>60,遇到minutes=60时不会进入分支,String.format会把60格式化输出成"60",导致非法时间出现。这种bug手工检查时肉眼很难发现,最好的办法就是把"23:59"丢进去跑一遍,看会不会死循环或者输出非法时间。
4.4 面试官追问时的应对
如果这是在面试现场,你说完枚举组合法之后,面试官大概率会追问:“能不能不用枚举,直接O(1)找到答案?”
这时候就可以提按位替换法。思路是从分钟个位开始,向高位寻找第一个可以变大的位置,把该位置改成集合中更大的最小数字,后面的低位全部改成集合中的最小值。比如"19:34",先从分钟个位4开始,集合{1,9,3,4}中比4大的最小数字是9,所以把4改成9,得到"19:39",这就是答案。
但按位替换法有个陷阱。如果分钟个位已经是集合中的最大值,就需要看分钟十位。如果分钟十位也不能变大,再看小时个位,最后看小时十位。每次变大的位置不同,得到的候选时间也不同,需要比较所有候选时间,取最小的那个。我在上面的"11:39"例子里就卡过:按位替换法先看分钟个位9,已经是最大;再看分钟十位3,可以变成9,得到19:11;但事实上13:11才是正确答案,因为小时个位1可以变成3,此时分钟十位和分钟个位都取最小值1,得到13:11,比19:11更近。这说明按位替换法不能找到第一个可变大位置就返回,而是要收集所有可能,再比较取最小值。这个细节是我自己推演时发现的,DeepSeek第一次生成的贪心代码恰恰就在这里出错了。
4.5 用DeepSeek生成测试用例的技巧
我后来发现,DeepSeek不仅能生成代码,还能高效生成测试用例。你可以让它这样输出:
请为Next Closest Time生成10个测试用例,覆盖:普通情况、分钟进位、小时进位、跨天、数字集合只有1个元素、5个以上重复数字等情况,并给出预期输出。
它会给你一份表格,里面有输入、输出、解释。这些测试用例能很好地补充你自己的思维盲区,尤其是那些“我根本想不到”的极端输入。
不过我还是要提醒一句:AI生成的预期输出虽然大概率正确,但偶尔也会翻车。我在测试一个"23:59"的变形用例时,AI给出的预期输出是错的,我手动推演后纠正了它。所以AI生成的测试用例适合作为参考,但最终判定还是要靠自己的逻辑。
5. 这套刷题方法还能用到哪
用DeepSeek配合LintCode做题这个方法,我已经试了好几道题,感觉最有价值的不是“让它给答案”,而是“让它解释为什么”。遇到一道不会的题,我现在的流程是:
第一步,自己读题3分钟,尝试用文字描述清楚要做什么。如果描述不清楚,说明题目本身没读懂,这时候让DeepSeek帮我解释题面。
第二步,自己先写一版暴力解法。哪怕效率再低,先把逻辑跑通。这个阶段的目标不是优化,而是确认自己对题目的理解没有偏差。
第三步,把暴力解法和我的优化思路一起告诉DeepSeek,让它给出更优雅的方案,并解释每一步的思考过程。
第四步,用DeepSeek生成边界测试用例,再人工验证结果。
这个方法不只是对“下一个最近的时间”有效,对LintCode上绝大多数字符串处理、数组遍历、回溯、动态规划题都适用。核心原则是:AI是辅助,不是替代,你可以让它帮你把想法落地成代码,但不能让它替你想清楚问题的本质。
最后再分享一个小技巧:把每道题的完整对话保存下来,隔几天重新看一遍,你会发现AI当时的解释里,有些地方你当时觉得懂了,回头看才发现理解得不够深。这种“二次复习”才是用AI刷题最大的红利。
