ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

力扣Hot100刷题效率低?亲手搭建Java本地调试模板全攻略

力扣Hot100刷题效率低?亲手搭建Java本地调试模板全攻略 先把话说在前面如果你刷力扣还在“在线编辑器里手写代码、提交、看报错、再改”的死循环里打转那我强烈建议你停下来花一个晚上搭一套自己的力扣Hot100 Java本地模板。这套东西做完之后刷题的速度、对代码的掌控感完全不是一个级别。我用这个模板带过好几轮新人也帮不少同事做过本地环境抢救这里把整个搭建思路、模板代码、刷题顺序、常见坑一次性讲透。整套方案以免费工具为主JDK、IDEA Community、Maven、JUnit 5都够用不花一分钱。1. 为什么要做一套本地模板1.1 在线OJ的痛点在线刷题平台的体验说实话一直没什么质的提升。最烦人的就是边界用例。力扣上很多题目你提交之后报错它只给你看失败的输入和输出但看不到完整的用例数据尤其像“字符串拼接超时”“整数溢出”这类问题在线编辑器里很难复现。还有一点平台自带的编辑器功能太弱了没有断点调试、没有代码折叠、没有多文件组织遇到一道需要自定义数据结构的题比如LRU缓存、并查集写起来相当憋屈。更关键的问题在于很多人刷题是在“背题”不是在“练题”。在线编辑器打开一道题看完题解照着敲一遍提交通过下一题。整个过程没有沉淀没有自己的测试用例没有对代码逐步演进的记录。三个月后回头看啥也没留下。1.2 本地化的回报本地模板解决的就是上面这些问题。断点调试可以直接看到每一步变量变化自定义边界用例可以随便造更重要的是可以建立一个属于自己的题解工程把Hot100里的每道题按数据结构、算法类型归类每个类里放一个独立的解法类配套测试用例随时可以回归。本地环境还有一个隐形好处刷题的同时顺便练了Java工程化。Maven怎么配、JUnit怎么用、依赖怎么管理、包怎么划分这些东西面试时也会问到。我把这套模板搭好之后很多同事的反馈是“好像突然会写工程代码了”其实就是因为天天在这个结构里写算法自然就熟了。本地模板还能并行复习Java基础。Hot100里的题目涉及HashMap、PriorityQueue、Deque、List、数组拷贝、字符串处理、泛型、Lambda这些都是Java面试最喜欢问的点。用本地模板刷题时顺手写几个Demo类验证底层实现逻辑比单独背八股文有效得多。2. 环境准备与工程结构2.1 JDK、IDEA与Maven的基础安装JDK版本我建议直接用JDK 17 LTS不要再用JDK 8了。原因很简单Hot100题目本身对Java版本要求不高但JDK 17有更好的语言特性record、switch表达式、文本块也接近当前主流公司的生产环境。用太久远的JDK 8本地跑得欢面试时问“你用的什么版本”反而尴尬。安装时记住两个核心点。第一JAVA_HOME环境变量必须指向JDK安装目录不是bin目录不是JRE目录。第二PATH里追加%JAVA_HOME%\binWindows或$JAVA_HOME/binmacOS/Linux。装完后命令行执行java -version确认看到类似openjdk version 17.0.x就算成功。IDEA直接用Community版就足够了不用折腾破解和激活Hot100刷题不需要企业版功能。Maven 3.8以上即可配置阿里云镜像这个步骤不要省不然第一次拉依赖能让你怀疑人生。至于IDEA里怎么关联JDK和Maven网上的教程很多这里不展开只提醒一句IDEA的Settings里要确认Project SDK选的是JDK 17Maven的Runner里JRE也要选对否则编译时经常报“invalid source release”。2.2 本地模板的目录划分与pom配置工程名我建议叫algorithm-hot100包名用com.leetcode之类都可以。目录结构直接按刷题维度来分不要按力扣题号分。按题号分的缺点是不同类型的题混在一起复习时没有主线。按算法类型分训练时目标感更强也方便日后快速定位。algorithm-hot100 |- pom.xml |- src/main/java/com/leetcode |- template 模板类新题直接复制 |- datastructure 自定数据结构ListNode、TreeNode等 |- arrayhash 数组与哈希表 |- twopointer 双指针与滑动窗口 |- dp 动态规划 |- backtrack 回溯 |- binarytree 二叉树 |- graph 图论与并查集 |- stackqueue 栈、队列、堆 |- sort 排序算法专项 |- concurrent 并发编程Demo面试准备用 |- javabase Java基础验证Demo面试八股配套 |- src/test/java/com/leetcode |- arrayhash 对应主代码的测试类 |- template |- ...pom.xml没什么花哨的核心就是引入JUnit 5和Lombok可选。Lombok能省掉手写getter/setter的功夫但有的人用不惯也可以不用。我建议加上因为面试手写代码时往往需要快速构造测试数据Data和Builder挺好用的。dependencies dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.2/version scopetest/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.32/version scopeprovided/scope /dependency /dependencies如果你没有安装Lombok插件IDEA里会看到注解爆红。这个是小问题装上插件、开启Annotation Processing就好后面常见问题里会细说。3. 模板代码设计3.1 主类模板每道题我建议写成一个独立的类不要堆在一个类里写几十个方法。类名直接用题目关键词的英文缩写比如两数之和就是TwoSum。每个类里包含三部分空实现的方法骨架、几个重要测试用例写在main方法里或单独测试类里、以及解题思路的注释。注释写清楚为什么这么做而不是写“定义了一个变量”。我是这样设计的package com.leetcode.arrayhash; import java.util.HashMap; import java.util.Map; /** * 1. 两数之和 * * 思路遍历数组用 Map 记录“当前值需要的另一半 - 当前值的下标”。 * 每次遍历先查 Map命中直接返回没命中就把当前值放进 Map。 * 时间复杂度 O(n)空间复杂度 O(n)。 * * 进阶思考如果数组有序能不能用双指针做如果要求原地怎么办 */ public class TwoSum { public int[] twoSum(int[] nums, int target) { MapInteger, Integer map new HashMap(); for (int i 0; i nums.length; i) { int need target - nums[i]; if (map.containsKey(need)) { return new int[]{map.get(need), i}; } map.put(nums[i], i); } return new int[0]; } public static void main(String[] args) { TwoSum solution new TwoSum(); // 基础用例 int[] nums1 {2, 7, 11, 15}; int[] result1 solution.twoSum(nums1, 9); System.out.println(print(result1)); // 期望输出 [0, 1] // 重复元素用例 int[] nums2 {3, 3}; int[] result2 solution.twoSum(nums2, 6); System.out.println(print(result2)); // 期望输出 [0, 1] // 负数与零 int[] nums3 {-3, 0, 4, 3}; int[] result3 solution.twoSum(nums3, 0); System.out.println(print(result3)); // 期望输出 [0, 3] } private static String print(int[] arr) { return Arrays.toString(arr); } }main方法里直接写测试用例对刷题来说是最快的。不要只用一个用例至少覆盖基础场景、重复元素、负数零、大数溢出如果涉及。本地调试时用IDEA的Debug模式跑main方法想看哪一步看哪一步。3.2 高频数据结构模板Hot100里很多题需要自建数据结构最典型的就是链表和二叉树。每次手写ListNode和TreeNode很烦而且不同题里定义还不一样容易出错。我在datastructure包里统一放了一份定义。package com.leetcode.datastructure; public class ListNode { public int val; public ListNode next; public ListNode() {} public ListNode(int val) { this.val val; } public ListNode(int val, ListNode next) { this.val val; this.next next; } // 根据数组快速构造链表方便本地测试 public static ListNode of(int... vals) { ListNode dummy new ListNode(); ListNode cur dummy; for (int val : vals) { cur.next new ListNode(val); cur cur.next; } return dummy.next; } Override public String toString() { StringBuilder sb new StringBuilder(); ListNode cur this; while (cur ! null) { sb.append(cur.val); if (cur.next ! null) { sb.append( - ); } cur cur.next; } return sb.toString(); } }TreeNode同理加一个根据层级数组构造树的方法注意处理null占位以及一个中序遍历/层序输出方法。这样在做翻转二叉树、验证二叉搜索树、二叉树最大路径这些题时验证数据特别方便。这里不贴完整代码了核心就是public static TreeNode of(Integer... vals)用队列按层构造遇到null就跳过左右子树指针。3.3 算法套路的本地模板示例Hot100里有些算法的套路非常固定我建议把模板先写好在template包里刷题时直接复制改参数。比如单调栈模板每日温度、下一个更大元素、回溯模板全排列、子集、组合总和、动态规划模板最长递增子序列、编辑距离、并查集模板岛屿数量、除法求值。以并查集为例Hot100里至少有4-5道题可以直接套用package com.leetcode.template; public class UnionFind { private final int[] parent; private final int[] rank; public UnionFind(int n) { parent new int[n]; rank new int[n]; for (int i 0; i n; i) { parent[i] i; } } public int find(int x) { // 路径压缩把节点直接挂到根上 if (parent[x] ! x) { parent[x] find(parent[x]); } return parent[x]; } public boolean union(int x, int y) { int rootX find(x); int rootY find(y); if (rootX rootY) { return false; } // 按秩合并矮树挂到高树上 if (rank[rootX] rank[rootY]) { parent[rootX] rootY; } else if (rank[rootX] rank[rootY]) { parent[rootY] rootX; } else { parent[rootY] rootX; rank[rootX]; } return true; } public boolean connected(int x, int y) { return find(x) find(y); } }写模板的时候要把注释留好解释路径压缩和按秩合并的作用。面试时如果让手写并查集能直接想起这套模板会顺畅很多。回溯模板也值得单独写Hot100里全排列、子集、组合总和、括号生成都可以共用一套模板public class BacktrackTemplate { private final ListListInteger result new ArrayList(); public ListListInteger permute(int[] nums) { dfs(nums, new boolean[nums.length], new ArrayList()); return result; } private void dfs(int[] nums, boolean[] used, ListInteger path) { if (path.size() nums.length) { result.add(new ArrayList(path)); return; } for (int i 0; i nums.length; i) { if (used[i]) { continue; } used[i] true; path.add(nums[i]); dfs(nums, used, path); path.remove(path.size() - 1); used[i] false; } } }关键点就是“选择-递归-撤销”三步。很多新手写回溯时忘记撤销或者直接把path对象加进result导致最终结果全是空列表这些都是本地调试一眼就能发现的问题。4. Hot100分类刷题策略4.1 按数据结构和算法类型分组Hot100题单网上一搜一大把但打开题单直接顺着刷的人大部分撑不过30题。更合理的做法是先按类型分组再按组攻坚。我自己整理过一个分组表这里按热点程度排个序参考题型分组代表题目本地模板重点数组与哈希两数之和、字母异位词分组、最长连续序列HashMap的get/put时机、排序技巧双指针与滑动窗口三数之和、盛最多水的容器、无重复字符的最长子串窗口扩展与收缩的边界条件链表反转链表、合并两个有序链表、环形链表、LRU缓存dummy节点技巧、快慢指针、双向链表二叉树二叉树中序遍历、最大深度、翻转二叉树、最近公共祖先递归终止条件、DFS/BFS选择回溯与递归全排列、子集、组合总和、括号生成撤销选择的时机、剪枝条件动态规划爬楼梯、打家劫舍、最长递增子序列、编辑距离dp数组含义、状态转移方程、初始值栈与堆有效括号、每日温度、数组中的第K个最大元素单调栈维护、PriorityQueue大小图与并查集岛屿数量、腐烂的橘子、课程表、除法求值坐标方向数组、拓扑排序、find/union贪心与区间合并区间、跳跃游戏、加油站排序后按边界维护“接雨水”这个系列可以单独拉出来说一下因为热搜词里也出现了“三维接雨水”。力扣的接雨水家族有两道高频题42. 接雨水一维双指针/单调栈和407. 接雨水 II三维优先队列BFS。本地模板里建议把这两题放一起刷边界的处理逻辑完全是两个难度级别对理解“水能从哪个方向流出去”这个问题非常有帮助。刷42时多花点时间弄懂单调栈的入栈出栈时机到407就会轻松很多。4.2 推荐的刷题顺序与时间安排第一轮我建议按标签刷每天只做同一类题。比如这周只做二叉树下周只做动态规划。这样做的目的是建立“肌肉记忆”看到一个题就能联想到对应的套路。第二轮打乱顺序随机刷模拟真实面试的状态。第三轮只刷错题和让你卡壳超过30分钟的题。每道题的时间分配也很重要。我给自己定的规矩是10分钟独立思考没有思路就看题解题解读懂后合上自己从头写写完用本地模板跑3-5个测试用例。这个方法看起来慢但效果非常好。有些题今天能做出来一周后可能完全想不起来这时候本地模板里的注释和测试用例就能帮你快速回忆。时间安排上每天2题比周末突击14题强。刷题这事讲究连续性和手感断一周基本就废了。我的习惯是工作日每天2题周末复盘本周所有题的代码重新跑一遍测试看有没有可以优化时间的。5. 把本地模板延伸到Java面试八股5.1 并发编程与线程池刷题刷到一定阶段后本地模板工程里的代码结构其实可以直接拿来当Java面试准备基地。我在concurrent包里放了一些高频面试点的可运行Demo比如线程池参数验证、线程等待机制的对比、死锁复现等。线程池是Java面试的重灾区。面试官经常问ThreadPoolExecutor核心参数光背参数没用最好现场演示一遍。我在本地写过一个Demo用饱和策略为AbortPolicy的线程池提交超出最大线程数的任务然后观察异常输出这样可以很直观地理解corePoolSize、maxPoolSize、workQueue三者之间的联动关系。package com.leetcode.concurrent; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolDemo { public static void main(String[] args) { ThreadPoolExecutor pool new ThreadPoolExecutor( 2, // corePoolSize 4, // maximumPoolSize 30, TimeUnit.SECONDS, new ArrayBlockingQueue(2), Thread::new, new ThreadPoolExecutor.AbortPolicy() ); for (int i 1; i 7; i) { int taskId i; try { pool.execute(() - { System.out.println(任务 taskId 被线程 Thread.currentThread().getName() 执行); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } catch (Exception e) { System.out.println(任务 taskId 提交被拒绝: e.getClass().getSimpleName()); } } pool.shutdown(); } }跑一遍这个Demo就很容易回答“线程池的任务提交顺序是什么”这个问题先核心线程再任务队列再非核心线程最后拒绝策略。线程池相关的还有个高频题是“线程等待都完成怎么做”CountDownLatch、Future.get()、CompletableFuture.allOf()这三个方案最好都在本地写一遍。5.2 Redis、集合源码与常用框架Redis在Java面试里的出镜率极高热搜词里就有一条“java使用redistemplate将redis的数减一”。这一类问题看起来简单但真的有很多人踩坑。典型场景stringRedisTemplate.opsForValue().increment(counter, -1);如果你用的是RedisTemplateString, Object并且没有指定序列化器那increment()大概率会报错ERR value is not an integer or out of range。原因在于value在Redis里存的是经过JDK序列化后的二进制数据不是字符串Redis服务端执行INCR时当然会失败。本地模板里建议放一个Redis操作验证类把StringRedisTemplate和RedisTemplate序列化器的区别跑清楚这块理解了很多生产问题都能解释通。HashMap也是必问项。Hot100里大量题用HashMap面试官基本都会延伸到“HashMap的put流程”“为什么线程不安全”“扩容机制”。我建议在javabase包里写一个很小的测试类用反射打印HashMap底层数组的容量变化直观观察扩容时机。这个在面试时讲出来绝对比干背源码有说服力。5.3 排序算法与Lambda等基础细节快速排序和冒泡排序是热搜词里的常客Hot100有些题虽然不直接考排序但Arrays.sort的底层原理、手写快排这些都是基础中的基础。我建议在sort包里把快排、归并、堆排各写一遍附加测试和注释。快排的partition写法和双指针思想其实在Hot100里很多地方都有呼应尤其TopK问题。多行文本块Java 15的文本块语法也是容易被问到的Java基础点。很多人在IDEA里写字符串拼接遇到复杂的JSON或多行SQL转义字符写到头大。本地模板里放一个多行字符串Demo一边刷题一边把这些语言细节过掉。还有Lambda和StreamHot100里虽然没有直接考但很多题解用Stream可以写得非常简洁比如统计单词频率、对Map按值排序。面试时如果能顺手用Stream处理集合观感会好很多。本地模板里可以准备几个常用的Stream操作分组计数、过滤、排序、flatMap全部写成可运行的Demo。6. 本地实战常见问题与排查6.1 环境与依赖类问题本地模板搭好之后最常遇到的第一类问题就是环境和依赖相关的。这里列几个我实际处理过的案例。JDK版本不匹配。IDEA里项目SDK选的JDK 17但Maven Runner里JRE却指向了JDK 8编译时会出现invalid source release: 17。排查时先看IDEA的Project Structure再看Settings的Maven配置确保两处一致。Maven依赖下载慢或者根本拉不下来。解决方案是在~/.m2/settings.xml里配置阿里云镜像配置好之后第一次reimport项目会快很多。如果你用了Lombok 1.18.32建议JDK 17配1.18.30以上版本否则会有兼容警告。IDEA启动报java was started but returned exit code-1这个多半是IDEA自带的JVM和系统JDK冲突。处理办法是修改IDEA安装目录下idea64.exe.vmoptions里的JVM参数把-Xmx调小一点或者确认一下系统PATH里的JDK没有指向损坏的JRE。还有一条冷门的Uncaught exception java.lang.NoClassDefFoundError: java/applet/Applet。这个一般是项目依赖了非常老的工具包比如某些旧版CGLIB它们还引用了已经被JDK 9移除的applet API。解决思路就是升级依赖版本或者在pom里排除掉老的不需要的传递依赖。6.2 代码与配置类问题第二类常见问题出在代码本身和框架配置上。我见过的几个高频槽点RedisTemplate的increment报错前面已经讲过核心就是key对应的value在服务端的类型不是数字字符串。还有一个坑是key本身带了序列化前缀比如\xac\xed\x00\x05t\x00\x03foo这种肉眼看不到但实际存在。排查办法是用Redis Desktop Manager或者redis-cli看一下实际存的key。泛型强转报ClassCastException。这个问题多发于把JSON反序列化结果直接赋给带泛型的类型比如ListUser转成了ListMap。本地写代码时如果遇到这种建议直接用Java的ListUser users objectMapper.readValue(json, new TypeReference() {})这种写法不要连环强转。JUnit测试里用SpringBootTest却启动失败大多数情况是自动装配的Redis、DataSource等服务没有可用实例。本地刷题项目其实不需要Spring Boot那套直接用main方法跑JUnit5单元测试就行。如果实在想用Spring Boot做Demo记得给测试类加SpringBootTest(properties spring.autoconfigure.exclude...)把不需要的自动配置排除掉。6.3 常用排查命令最后分享几个我在本地排查问题时的常用命令都很基础但非常管用# 查看Java版本和编译级别 java -version javac -version # 查看JAVA_HOME指向是否正确macOS/Linux echo $JAVA_HOME # Windows echo %JAVA_HOME% # 查看Maven有效配置确认镜像是否生效 mvn -version mvn help:effective-settings # 查看某个进程的JVM参数排查OOM时用 jps -l jcmd pid VM.flags # 强制刷新Maven依赖并重新编译遇到依赖缓存问题 mvn clean compile -U这些命令都不复杂但能解决本地模板百分之八十的环境问题。真正难的问题往往不是环境而是代码逻辑本身这时候老老实实用Debug断点看每一步变量的变化比猜要快得多。关于这套力扣Hot100 Java本地模板我在实际使用中最大的感受是它让刷题从“一件要完成的任务”变成了“一件可以积累的事”。当你把每道题的解法、测试用例、踩坑记录都留在本地工程里三个月后回头看那份代码就是自己的算法题库比任何刷题记录App都直观。最后建议你从今天开始哪怕先只搭一个空工程、写好pom.xml和一个模板类后面再慢慢填充也比一直想着“等有时间了再搞”强。
返回列表