
1. 为什么“看懂题解”和“自己写出AC”之间隔着一条鸿沟很多刚接触Java基础算法的人都有一个共同习惯拿到一道题先翻题解看完觉得逻辑特别清晰每一步都合理心里想着“这也没多难”然后关掉页面准备自己写一遍。结果常常是写了半天连编译都过不了或者代码能跑一提交就超时、越界、死循环。这不是智商问题也不是基础差而是“阅读代码”和“生产代码”本来就是两种完全不同的能力。1.1 眼睛会了手不会大脑的自动补全效应看别人写的代码大脑会无意识跳过很多细节。比如二分查找里常见的while (left right)你看到的时候觉得“这不是理所当然吗”但自己去写很可能写成了while (left right)跑起来发现少处理了一个边界或者进入了死循环。再比如双指针交换数组元素题解里一行swap(nums, i, j)带过你心里也明白但自己写的时候i和j是下标还是值还是引用的对象很容易混。这种细节在阅读时被大脑自动补全了编译器却不会替你做任何补全。这个现象在认知心理学里叫“知识的诅咒”一旦你理解了某个逻辑就很难再站在不理解的角度去审视它。看题解的人大脑已经在帮他脑补了所有跳跃的步骤而写代码的人面对的是白纸一张每一个循环、每一次判空、每一个参数传递都必须显式写出来。所以标题里那句“一定要敲起来才能发现问题”本质就是只有当你被迫把每一步都落实到代码上的时候那些被大脑自动忽略的漏洞才会暴露出来。1.2 Java语言本身的“隐藏门槛”算法题用伪代码描述的时候根本不会涉及语言特性但落到Java实现处处都是坑。数组是定长的想追加元素得自己扩容所以很多新手写“数组去重”时第一反应是新建一个数组结果长度不好确定要么开大了出现一堆0要么开小了越界。用ArrayList又没养成习惯导致代码一会儿数组一会儿集合类型转换乱成一团。字符串是不可变的每次拼接都会生成新对象循环里拼1000次就是1000个对象。虽然OJ测试数据小的时候看不出来但到了真实场景这种写法就是性能隐患。Java里应该用StringBuilder这个知识点几乎每个教程都提但没真正写过一个字符串拼接的算法很难建立起“字符串操作必须考虑可变性”的肌肉记忆。还有Integer的缓存问题-128到127之间的Integer对象直接用比较结果是true超出这个范围就变成false。我用这个知识点考过几个自称“学过Java基础”的新人十个里有八个栽在这里。你说他不会他懂。但没敲过代码没被这个坑绊倒过他就不会在写哈希表题目时主动想到用它。这些都属于“Java语法之外、但Java算法之内”的东西。光看教程永远学不会动手敲一遍让报错教给你才是最快的学习方式。1.3 三层报错编译错、运行错、逻辑错写算法题踩的坑基本可以分为三个层级错误类型典型表现排查难度新手常见状态编译错误缺分号、括号不匹配、类型不兼容最容易编译器会精确报行号盯着代码看了十分钟也找不到错在哪一行运行错误数组越界、空指针、栈溢出中等需要看栈追踪信息一运行就报错但不知道哪个用例触发逻辑错误结果不对、死循环、超时最难程序能跑但答案永远差一点最崩溃因为编译器不提示IDE也不报警编译错误其实是最友好的错误因为编译器直接告诉你第几行有问题。很多新手在这一步就劝退了觉得“我是不是不适合写代码”。真不是编译错只能说明语法还没形成肌肉记忆写多了自然消失。运行错误就要开始动脑子了比如NullPointerException你得学会看栈追踪信息找到是哪个对象为null。但有些情况下比如循环里调用list.get(i)i超了范围报错信息只会告诉你IndexOutOfBoundsException具体是第几次循环崩的完全得靠你自己去查。逻辑错误才是真正考验算法理解深度的地方。程序能跑通测试用例也过了两个但提交上去就是有一个边界条件没过。这种时候你才会意识到之前“读懂”的那道题其实根本没理解透。这种体验敲过代码的人一定懂。2. 从哪些题型起步Java基础算法的推荐打磨清单很多人被“算法”两个字吓住觉得要学完数据结构与算法一整本书才能动手。其实Java基础算法阶段需要打磨的题型远没有想象中那么多。我把新手最该练的题型分成了四组每一组都有清晰的学习递进关系和验收标准。2.1 数组与字符串几乎所有题的地基数组操作是所有算法题的基础玩法。翻转数组、合并两个有序数组、移除指定元素、寻找最长连续递增序列这些题看似简单但每道都能折射出一个Java语言特性。比如“翻转数组”用双指针从两端向中间交换考察的是下标运算。新手最容易犯的错是循环条件写成i nums.length结果翻完了又翻回来。再比如“合并两个有序数组”如果要求原地合并到第一个数组那就必须倒着遍历否则元素覆盖会丢数据。这个“从后往前”的思路不亲手写一次光看题解是体会不到为什么要这么做的。字符串题里反转字符串、判断回文串、统计单词数量是最常见的三件套。这里就能练到Character类的用法、StringBuilder的reverse()方法、以及正则表达式在哪里能用、哪里别乱用。我的建议是数组和字符串至少把每类练5道直到提交通过率稳定在70%以上再进入下一组。这一阶段的关键不是刷题量而是每次提交报错后能不能自己定位到是下标问题、边界问题还是类型问题。2.2 排序与二分最早形成肌肉记忆的两类算法排序是后面很多算法的基础操作。面试中手写快排或者归并排序是高频考点但其实新手不需要一上来就背快排模板先用选择排序、插入排序、冒泡排序入门理解“比较-交换”的基本逻辑再过渡到快排的分治思想这样梯度更平滑。我比较推荐插入排序作为第一个练手排序因为这货最贴近真实生活中的排序直觉扑克牌抓牌后往手里插牌。而且它的最好时间复杂度是O(n)当数据基本有序时表现非常亮眼。很多人学了各种高级排序后反而不会用插入排序这是不对的工程实践中小规模数据的排序插入排序往往是最高效的选择。二分查找更不用说了Java里Arrays.binarySearch()封装了现成实现但算法题考的是你手写二分的能力。二分查找有左闭右闭、左闭右开两种写法每种对应不同的边界处理。关键是要搞清楚你要找的是“某个确定值”还是“第一个满足条件的数”还是“最后一个满足条件的数”三种需求的细节完全不同。2.3 双指针、链表、栈与队列开始建立“数据结构感”双指针是基础算法中性价比最高的技巧之一。左右指针用于有序数组的查找比如两数之和快慢指针用于链表成环检测、寻找链表中点。这里的核心问题就是什么时候移动左指针什么时候移动右指针以及循环终止条件应该是什么。链表题的Java实现因为绕不开节点对象、指针引用、判空这些细节是很多新手的第一个坎。反转链表、合并两个有序链表、删除倒数第N个节点这三道题被反复考。写链表题时画图比写代码更重要先把节点之间的指向关系画清楚再落成代码成功率会高很多。栈与队列方面括号匹配、用队列实现栈、用栈实现队列是基本功。Java里Deque接口和LinkedList实现类的关系需要实际操作一遍才能熟练。很多人这里才发现Stack类其实早就过时了官方推荐用ArrayDeque这种“你以为你会了其实用的是老掉牙API”的情况就在敲代码过程中自然暴露出来。2.4 哈希表与递归从“看得懂”到“用得对”到了哈希表才算真正开始接触“空间换时间”的算法思想。两数之和、字母异位词分组、最长不重复子串每一道题都训练如何用HashMap把时间复杂度从O(n^2)降到O(n)。这里重点掌握getOrDefault、computeIfAbsent这些方法能在代码里少写三个if判断。递归是很多新手的精神内耗点。但其实递归没那么玄就记住三件事终止条件是什么、返回值是什么、调用自己和子问题之间的关系是什么。青蛙跳台阶、菲波那切数列、二叉树的前中后序遍历都是递归入门的经典题目。Java里写递归时要注意的是递归太深会栈溢出所以该改成迭代的题要果断改。这四组题型并不要求全部刷完再进入下一步而是建议按照“数组字符串 → 排序二分 → 双指针链表栈队列 → 哈希表递归”的顺序按周推进。我见过不少人想一口吃成胖子直接上手背包问题、图论算法结果三周就放弃了。基础算法阶段不需要那么闯。3. 动手敲才会现形的三个翻车现场下面用三个真实高频翻车场景具体展示标题里那句话的分量。这三个问题光读完全可以跳过但一动手必踩。3.1 二分查找的等号与死循环这是最经典的“看懂了但写不对”的坑。请看下面这份错误实现public int binarySearch(int[] nums, int target) { int left 0, right nums.length - 1; while (left right) { int mid (left right) / 2; if (nums[mid] target) { return mid; } else if (nums[mid] target) { left mid; } else { right mid; } } return -1; }这里有两个致命问题。第一left right意味着当left right时循环退出此时就漏掉了对最后一个元素的判断。如果目标值恰好在这个位置函数直接返回了-1。第二当nums[mid] target时left mid而不是left mid 1这会导致当left和right相邻时mid卡在同一个位置左右下标永远不变化直接死循环。经典的区间“收缩不动”问题。正确写法应该是public int binarySearch(int[] nums, int target) { int left 0, right nums.length - 1; while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { return mid; } else if (nums[mid] target) { left mid 1; } else { right mid - 1; } } return -1; }注意两点循环条件用移动下标时用mid 1和mid - 1。这个细节里没有任何高深理论但不被死循环折磨一次你根本不会记住它。用left (right - left) / 2而不是(left right) / 2是因为当left和right都很大的时候两者相加可能超出int范围。这在刷题时可能遇不到但工程上就是实打实的隐患。从一开始就养成好习惯以后不用回头改。3.2 快慢指针的空指针与空转再看一个链表题里的经典场景。寻找链表的中间节点用快慢指针public ListNode middleNode(ListNode head) { ListNode slow head, fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; } return slow; }这段代码本身没啥问题但如果把循环条件写成while (fast.next ! null fast ! null)顺序反了当fast为null时前一个条件的fast.next就已经空指针了。这一步细节看起来微不足道但真写反了程序一运行就崩。更隐蔽的是fast fast.next.next这一步实际上要求fast.next必须非空所以循环条件里必须先判断fast.next ! null。这种“判断条件前后顺序决定程序生死”的经验只有被空指针折腾过一次、调了半天才发现是条件顺序反了才会真正长记性。3.3 HashMap的计数习惯与Integer缓存陷阱用HashMap做字符计数统计是地地道道的Java基础算法高频操作。下面这段看题解时觉得毫无问题的代码MapCharacter, Integer countMap new HashMap(); for (char c : str.toCharArray()) { if (countMap.containsKey(c)) { countMap.put(c, countMap.get(c) 1); } else { countMap.put(c, 1); } }逻辑完全正确但代码啰嗦。Java 8 之后可以用getOrDefaultfor (char c : str.toCharArray()) { countMap.put(c, countMap.getOrDefault(c, 0) 1); }这一个改动就能消灭三行if-else。但你没亲手写过第一种直接用第二种其实不太能体会到这种“少了一堆分支判断”的爽感。再往下就是“用比较Integer”的坑。统计完之后如果要比较两个Integer值是否相等新手经常直接写countMap.get(a) countMap.get(b)当计数为127时没问题计数为128时就神奇地返回false。原因就是我在前面提到过的Integer缓存机制。这个坑踩过一次就再也不会忘。4. 让“坚持”不再是口号可持续训练的四条机制标题里提到“相信坚持的力量”这句话听着像鸡汤但实际操作过的人会明白坚持这件事本身并不是靠意志力硬撑而是要设计一套可以持续运转的机制。我总结下来有四条特别管用。4.1 固定时间盒不靠状态靠习惯不要等“有空了”“状态好了”才去写算法而是固定每天某个时间段打开编辑器。哪怕只有30分钟也要坐下来。十分钟完全进入不了状态那正好别做新题把昨天做过的题拿出来重新写一遍就当热身。这个做法的本质是把算法训练从“靠记忆力驱动”变成“靠习惯驱动”。人类大脑每天做决策的次数是有限的如果你要额外决定“今天要不要写题”“什么时候写”还没开始动笔精力就先耗掉一截。固定时间盒省掉的正是这部分决策成本。4.2 错题本记“错误假设”不是抄题目我的错题本和别人不太一样不用抄题目也不用贴最优解代码只记三件事我当时是怎么理解这道题的我在哪一步写出来的东西和预期不一样下次遇到类似场景我应该在哪个环节提前检查举个例子二分查找死循环后我记的是“当left和right相邻时mid可能原地不动所以移动下标时必须保证区间严格缩小”。下一次写二分我会主动检查循环结束时有没有遗漏最后一个元素。记录“错误假设”比记录“正确答案”重要得多因为正确答案属于题解作者但错误假设属于你自己。看清自己的思维漏洞在哪里比背十道模版题都有用。4.3 用周回顾代替“刷题数量”很多人喜欢给自己定目标这周刷20道题。结果为了凑数全挑自己会的简单题。刷得很爽但三周之后发现原地踏步。我更推荐的思路是每天少量新题搭配旧题重写周末做一次分类回顾。打开本周做过的题把它们按考点归类几道是数组双指针几道是二分边界几道是递归终止条件。然后想一想这周哪一类题错误率最高下周就在这一类上多安排一道题。这个做法的好处是把注意力从“我做了多少题”转移到“我补上了哪些漏洞”后者才是算法能力真实增长的来源。4.4 找一个外部反馈点自己闷头刷题最大的问题是容易陷入“自我感觉良好”。觉得自己已经掌握了一类题结果过两周回头看完全不会写了。这种情况需要外部反馈来打碎你的错觉。最简单的办法是找一个同样在学Java的人每周互相给对方讲一道自己做过的题。能用自己的话把思路和边界情况说清楚说明你真的理解了。讲的过程中你会发现自己原来有好多地方“我以为懂了其实说不出来”。另一个办法是对比多个题解。很多人刷题只看一份题解能被过就立刻下一道。我现在的习惯是自己AC之后至少再看两份讨论区的不同解法。不是要学最优解而是借不同的思路发现自己原来的盲区。5. 一套可以直接抄的七周基础算法路线最后给出一套我实际带过很多新人使用的七周路线每种题型每周都有明确的训练目标和验收标准照着走就能在一个月半到两个月的时间里把Java基础算法的骨架真正打扎实。5.1 前四周从数组走到二分和指针第一周主题是数组与字符串基础。练翻转数组、合并数组、删除元素、反转字符串、字符串压缩这些题。Java侧重的API是System.arraycopy、Arrays.copyOf、StringBuilder。验收标准是双指针翻转数组的代码能在十分钟内从空白编辑器完整写出来不靠记忆模板而是靠理解下标移动逻辑。第二周进入排序。先手写选择排序、插入排序、冒泡排序再过渡到快速排序和归并排序。这周最重要的不是学会所有排序而是能说出为什么跨过一定规模之后O(n^2)的排序就不适合了。第三周二分查找。练经典二分、查找第一个等于目标的位置、查找最后一个小于目标的位置、旋转排序数组中的查找。验收标准能清楚说出自己写的是开区间还是闭区间以及为什么这个选择会影响循环条件和下标更新。第四周双指针。练有序数组的两数之和、平方数之和、反转字符串中的元音字母、移动零。这周的注意点开始转向“什么时候移动哪个指针”对运行时的状态变化要有清晰预判。5.2 后三周从链表走到递归入门第五周链表操作。反转链表、合并两个有序链表、删除链表倒数第N个节点、判断链表是否有环。写链表题必须有画图习惯提交前检查每一个next赋值的前后顺序。Java方面这周重点是理解引用传递和对象赋值的关系。第六周栈与队列。括号匹配、用栈实现队列、用队列实现栈、每日温度单调栈入门。这周要理解为什么查找括号匹配时用栈而不是单纯计数。做单调栈题时理解“下一个更大元素”这种场景下维护一个递减栈的意义。第七周哈希表与递归入门。两数之和、字母异位词分组、最长不重复子串、青蛙跳台阶、二叉树的前中后序遍历。这周的验收标准是递归写出来后能够在编辑器上画清楚每一层的调用和返回过程。画不出来就是还没完全理解。这套路线的核心思想就是不追求每天刷很多题而是按周突破一个类别让每个知识点都有时间消化。很多人学算法最大的问题是学得太杂今天看动态规划、明天看图论每样都只看了个开头就放弃。按周推进每一类题型都形成完整闭环反而能在更短的时间内积累起真正的信心。我个人实际的体会是算法学习真正让人上头的时刻不是第一次AC一道难题而是过了一段时间回头看发现自己当初折腾了两小时才解决的题现在十分钟就能写完。那个时刻你会真正理解“坚持”这两个字的意义。它真的不是靠热血而是靠一次次动手敲代码把漏洞补上、把边界记牢最终量变引发质变。如果你现在还在“看题解”阶段试着放下那份题解打开编辑器从明天开始每天只写一道题七周后再回来看这篇内容你会发现自己已经走出了很远。