资讯动态

上机练习41天:从两数之和到超时排查,算法进阶的完整经验

发布时间:2026/10/14 17:16:11 来源:尧图企业网站定制
1. 第41天的练习现场从一道“简单题”聊起今天打开本地练习环境习惯性看了一眼最近一周的提交记录每天至少两次提交最长间隔没超过24小时。这个节奏从第1天坚持到第41天中间断过吗说实话断过两次一次是家里临时有事一次是纯粹不想动。但“断”和“停”是两回事断了第二天补回来节奏就不会散。这也是我今天想先聊的话题上机练习不是看某一天练了多少而是看你能不能把“每天碰代码”这件事变成肌肉记忆。回到练习内容本身。今天做的第一道题看起来真的很简单给定一个整数数组和一个目标值返回两个元素的下标使它们相加等于目标值。就是那道被很多人当作“入门第一题”的两数之和。但这道题我用了三种不同解法来练手分别对应暴力枚举、哈希表、以及排序加双指针。很多人觉得做一遍会了就过了但上机练习的意义恰恰在于同一个问题用不同思路各写一遍你才能真正理解不同数据结构带来的复杂度差异。暴力解法就是两层循环逐个组合去试时间复杂度是O(n²)。哈希表解法只用一次遍历每到一个数就检查补数是否存在复杂度降到O(n)。排序加双指针的做法需要先排序虽然时间复杂度是O(n log n)但它的优势在于空间复杂度是O(1)而且不需要额外的哈希表结构。这三种方案各有适用场景没有绝对的好坏。我自己在笔试和面试中遇到类似的题第一反应永远是先确认数据规模n小于1000可以暴力n到10万级别必须有哈希表如果题目要求不能开额外空间那就只能排序加双指针。// 哈希表版本一次遍历O(n) public int[] twoSum(int[] nums, int target) { MapInteger, Integer map new HashMap(); for (int i 0; i nums.length; i) { int complement target - nums[i]; if (map.containsKey(complement)) { return new int[]{map.get(complement), i}; } map.put(nums[i], i); } return new int[]{}; }这段代码本身没什么特别的但值得注意的一个小细节是如果数组中存在重复元素哈希表存索引时后出现的值会覆盖前面存过的索引。这道题要求返回任意一对即可所以不受影响但如果题目变成“返回所有不重复的组合”同一个哈希表做法就需要额外处理去重逻辑。这就是上机练习和“看题解”之间的差别题解只告诉你标准答案不会告诉你这个答案在什么变体下会失效。2. 上机练习踩过的三个坑边界条件、输入解析和测试数据41天下来我发现自己绝大部分的提交失败不是算法思路错了而是栽在三个非常具体的坑上。这三个坑几乎每个上机练习的人都会遇到提前说出来能帮你少走不少弯路。第一个坑是边界条件。二分查找的经典边界条件是left right还是left right取决于你的区间定义是闭区间还是左闭右开。如果你用的是闭区间[left, right]那循环条件必须是left right并且left mid 1、right mid - 1如果你用的是左闭右开[left, right)循环条件就要写成left right并且更新时right mid。我之前好几次提交失败就是因为把两种写法混在一起左边用闭区间的写法更新右边却继承了左闭右开的习惯。这个问题写代码时不容易发现因为样例数据往往只有三五个跑出来全对一旦测评数据里有极端情况比如数组长度是0或者1就立刻暴露。第二个坑是输入解析。很多上机练习题的输入格式是“第一行一个整数n第二行n个整数”看起来人畜无害。但如果n的规模是10万甚至100万用Scanner逐行读取就会超时。实测过同一道题数据量100万时Scanner读取耗时大约2000毫秒换成BufferedReader加字符串分割后只用了200毫秒左右差了整整10倍。很多新手的代码在本地跑得好好的一提交就显示运行超时原因可能根本不在算法而在输入输出的实现方式上。// 不要在大数据量下用这个 Scanner sc new Scanner(System.in); int n sc.nextInt(); int[] arr new int[n]; for (int i 0; i n; i) { arr[i] sc.nextInt(); } // 大数据量推荐这个 BufferedReader br new BufferedReader(new InputStreamReader(System.in)); int n Integer.parseInt(br.readLine()); int[] arr new int[n]; String[] parts br.readLine().split( ); for (int i 0; i n; i) { arr[i] Integer.parseInt(parts[i]); }第三个坑是把样例数据当全部。很多上机练习题会给出两三组样例用来解释输入输出格式但真正的测评数据里藏着大量边界情况数组只有一个元素、目标值不存在、输入带有前导空格、数据量正好是上限值。我现在的习惯是每次写完代码后至少自测四组数据最小规模n1或空数组、最大规模n取题目上限、重复元素、极端值比如负数或Integer.MAX_VALUE。这个习惯用了大概两周我的第一次提交通过率就从百分之四五十提升到了百分之八十以上。3. 从“能跑通”到“跑得稳”一次超时问题的完整排查链路程序能跑出正确答案和能在大数据量下稳定跑完是两件完全不同的事。第30天左右我遇到一道题当时觉得做得挺顺结果连续三次提交都是“运行超时”。这道题本身不复杂给定一个无序数组找出出现次数超过一半的那个元素。我最初的写法很朴素用一个双重循环外层枚举每个元素内层统计它出现了多少次一旦发现某个元素出现次数超过数组长度的一半就返回。逻辑上完全正确样例也都通过但测评数据里有10万个元素O(n²)的复杂度意味着在最坏情况下需要执行约50亿次比较操作超时几乎是必然的。排查过程是这样的我先把n10万的数据手动构造出来在本地跑肉眼可见地卡顿大概等了十几秒才出结果。这已经说明问题不在测评环境而在我的实现本身。于是我把复杂度从O(n²)降到O(n)用哈希表统计每个元素出现的次数然后再遍历一遍哈希表找出计数超过半数的元素。修改之后本地瞬间出结果再提交就通过了。// 摩尔投票法O(n)时间O(1)空间 public int majorityElement(int[] nums) { int candidate 0, count 0; for (int num : nums) { if (count 0) { candidate num; } count (num candidate) ? 1 : -1; } return candidate; }后来我更进一步把哈希表方案换成了摩尔投票法。这里说句实话哈希表方案已经能通过这道题了但既然题目保证“一定存在超过一半的元素”摩尔投票法只需要一个计数变量空间复杂度O(1)代码也更简洁。每次上机练习我都会在通过之后多问自己一句这个解法还有没有优化空间已经AC的代码复杂度还能不能降一档这个过程比单纯刷题收获大得多。那次超时之后我做了一个小实验来强化对复杂度的直觉随机生成不同规模的数据分别用O(n²)和O(n)的算法去执行记录耗时。数据规模从100、1000、1万到10万O(n²)的耗时从接近0秒飙升到十几秒O(n)的耗时则始终在几十毫秒以内。这个实验特别直观当你亲眼看到数据规模从1万增加到2万O(n²)的耗时从几十毫秒涨到一两百毫秒立刻就能理解“复杂度每差一个量级实际运行时间可能差出几十倍”到底意味着什么。4. 每天两小时怎么安排才不白练我在41天里沉淀的练习节奏很多刚开始上机练习的人会问每天练多久练什么怎么坚持就我41天的经验来说与其纠结“练几个小时”不如先定一个底线每天至少提交一次代码哪怕只是一道最简单的题。这个底线的作用不是让水平突飞猛进而是维持手感。手感这东西非常真实隔三五天不碰代码再打开编辑器时连花括号的缩进都会犹豫两秒。我目前的日常安排大致是这样每天两小时分成三段。前20分钟做一道简单题目的有两个一是热身让脑子从日常琐事切换到算法模式二是积累信心简单题秒杀了今天的状态就有了着落。中间60分钟做一道有挑战性的题目或者把昨天没做完的题继续推进。最后40分钟用来复盘看自己的代码哪里写得啰嗦翻一翻其他人的题解总结一种更优雅的写法并记录在练习笔记里。这里得特别说一下复盘环节它其实是整个练习过程中最容易被忽略、也最值得投入时间的部分。一开始我也是一天拼着做五六道题做完就扔结果一周后回头再看很多题目只剩一个模糊的印象代码的思路早就忘光了。后来我改变了策略大幅减少做题数量但每道题做完都会做三件事——一是把代码重新从头到尾读一遍找冗余二是把这道题的核心思路用一两句话写在笔记里三是顺手把时间复杂度、空间复杂度和边界条件记在旁边。比如“前缀和哈希表用于连续子数组和为k的计数问题注意前缀和相等时哈希表计数要累加”。这样坚持到第41天翻看笔记时看到的不是一道一道孤立的题目而是一条一条可以复用的解题线索。关于工具和习惯我也有几点优化。代码编辑器不需要太花哨能高亮、能自动缩进、能一键运行就够了核心精力还是要放在思考上。本地工程用Git管理每天练习结束提交一次提交信息写清楚“第几天练习内容”比如“Day41: two-sum, majority-element”。这样隔一段时间回头看提交记录练习轨迹一目了然。笔记用最简单的纯文本文件按日期整理不要搞得太复杂复杂了容易坚持不下来。OJ上做错的题和看过的题解也可以把链接存到笔记里方便以后回顾。5. 第41天最想吐槽也最想感谢的事坚持本身说实话第41天这个节点听起来挺励志的但实际感受并没有那种“坚持一个月所以今天特别兴奋”的感觉。真实的状态恰恰相反今天也想偷懒今天的题也差点没做出来期间还有两次拿起手机刷视频刷了半小时才回到编译器前。所以我想聊一个更直白的问题坚持这件事本身到底是被什么支撑下来的我的答案是把“必须做出什么成绩”的压力卸掉换成“今天只是照常练习”的惯性。哪怕状态很差我也会逼自己写一段代码并提交哪怕这道题是最简单的题。这个动作的意义在于维持闭环打开编辑器、读题、写代码、运行、提交、看到通过结果。整套流程走完人才会进入一种“我还在继续”的心理状态。一旦跳过一天这种闭环就断了第三天再启动时阻力会明显变大。另一个支撑点是“记录”。当我第41天回看第1天写的代码时惊讶地发现自己居然也写过这么初级的东西到现在至少能熟练处理哈希表、双指针、滑动窗口、二分查找这些常规套路了。这些进步在日复一日的视角里完全看不出来只有把时间跨度拉长才能意识到积累的意义。所以如果你也在练习中段产生过“练了这么久好像没什么用”的怀疑我的经验是别靠感觉判断靠记录判断。给刚开始上机练习的朋友三个建议都是我自己踩过坑之后才真正相信的第一不要贪多一天一道有质量的题好过一天十道但每道都浅尝辄止第二一定一定不要跳过复盘复盘半小时胜过闷头刷三道新题第三如果哪天真的一点都不想练就挑一道最简单的题做做完今天就达标了。你要对抗的不是某一天的懒惰而是整个节奏的中断。第41天暂时没有做出什么惊天动地的成果但今天写代码时明显比第1天顺畅了很多敲错了括号能及时发现想不出解法时知道该往哪个方向试提交失败时不再慌张而是按部就班地构造测试数据排查问题。这些变化看起来很不起眼但正是上机练习最宝贵的收获。接下来我打算把练习重点从“会做常见题”转到“做复杂综合题”也开始尝试限时模拟真实考试场景。第41天不是终点只是一个刚好值得停下来记录一下的路标。

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

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

免费获取报价 →
↑