Java基础算法学习:不要光看,一定要敲起来
不管是准备校招面试、打蓝桥杯这类竞赛,还是刚入门想把Java基础打牢,基础算法都是一道绕不过去的坎。我身边太多人学Java时啃完语法、看完集合框架就开始“飘”,一到手写排序、二分查找就卡壳。我自己带过的新人也是一样,视频看了好几遍,笔记抄了一整本,真让他上黑板写个快排,直接愣住。问题出在哪?就是没有“敲起来”。看一百遍算法讲解,不如亲手敲一遍,让报错打你的脸,你才能真的记住。
这篇内容我打算从Java基础算法到底要学哪些、怎么动手敲才能发现问题、面试和蓝桥杯怎么练、常见踩坑怎么避这几个角度,把我自己走过弯路之后总结出来的方法完整写一遍。适合刚学完Java基础语法正准备进阶的人、算法基础薄弱但要面Java开发岗的人,也适合在算法上刷了又忘、始终找不到状态的竞赛党。
1. 为什么说基础算法是Java开发的“基本功”
1.1 面试必考:算法题是筛人的第一道滤网
Java开发岗的面试基本上逃不过算法题这一关。不是说每个岗位都要你手撕红黑树,但像反转链表、两数之和、二分查找、排序这类基础题目,几乎是“入场券”级别的存在。面试官让我面候选人的时候,我最怕遇到的情况就是:简历里写着“熟悉Java集合”,结果让人手写一个基于数组的栈,连扩容逻辑都说不清楚。
很多人觉得面试考算法是“卷”,是“八股文”。但你换个角度想,算法题考的其实是一个人的基本功:边界条件处理是不是严谨、代码风格是不是干净、遇到没写过的题能不能拆解成步骤。这些能力在平时写业务代码时不会直接暴露,但一到线上故障排查、接口性能调优、数据迁移脚本编写,有没有算法底子,写出来的代码完全是两个层次。
1.2 竞赛场景:蓝桥杯这类比赛只认算法功底
蓝桥杯的Java组题目,跟你在公司里写的Spring Boot业务代码完全是两码事。它不考框架、不考中间件,考的就是字符串处理、数字计算、递归回溯、动态规划、图论这些基础算法和数据结构。我记得拿到的“蓝桥杯 数字题目”这类题,核心就是进制转换、大数取模、数字拆分,这里面每一步都建立在基础算法之上。
打这类比赛没有捷径,同一个题型你见过的次数多了,考场上才能条件反射式地想出解法。我之前有个同事带学生打蓝桥杯,他说了一句很实在的话:一等奖不是教出来的,是刷出来的。虽然有点绝对,但方向是对的,竞赛面前,坚持刷题就是最有效的准备方式。
1.3 日常开发里的算法影子
哪怕你不面大厂、不打竞赛,算法也会在日常开发中冒出来。举个最简单的例子,你写一个接口要给前端返回树形菜单,一次从数据库查出所有节点,自己组装成树,这个过程本质上就是用HashMap做父子关系的匹配;你处理一批订单数据要按金额排序,底层就是排序算法;你用Redis做缓存,Key的哈希分布背后是哈希函数的设计思想。
框架源码里更是处处是算法。HashMap的桶位计算和扩容机制、ArrayList的扩容拷贝、ConcurrentHashMap的并发控制,这些源码看懂的前提,是你自己实现过类似的简单版本。所以“Java工程师不需要懂算法”这个说法,真的只适用于永远停留在CRUD层面的人。
2. Java基础算法必知必会:一份不注水的清单
2.1 排序算法:先手写冒泡、快排、归并这五种
排序算法是基础算法里最直观的一类,也是面试手写频率最高的。先列一个我建议所有学Java的人都亲手实现过的清单:冒泡排序、选择排序、插入排序、快速排序、归并排序。前三种是“慢但简单”的O(n²)排序,后两种是“快但也更好出错”的O(nlogn)排序。
拿冒泡排序来说,绝大多数人第一次写都会写错内层循环的边界。我给个我常用的模板,你照着敲一遍然后再自己默写:
public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } // 优化:如果没有发生交换,说明已经有序 if (!swapped) { break; } } }这个flag优化很关键,日常开发里如果数据基本有序,它能直接让算法变成近乎O(n)的复杂度。快速排序的核心则在于partition,我建议所有细节都不要图省事跳过:
public static void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } int base = arr[left]; int i = left; int j = right; while (i < j) { // 从右往左找比base小的 while (i < j && arr[j] >= base) { j--; } // 从左往右找比base大的 while (i < j && arr[i] <= base) { i++; } if (i < j) { swap(arr, i, j); } } // 把基准数放到中间的位置 swap(arr, left, i); quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); } private static void swap(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }记住快排是“不稳定的排序”,相等的元素相对顺序会被打乱。如果业务上有稳定排序的需求,就选归并排序。
2.2 二分查找:Java里一定要吃透的查找算法
二分查找看起来只有几行,但它是面试里翻车率最高的题之一。先看标准模板,这种写法是我踩坑无数之后固定下来的:
public static int binarySearch(int[] arr, int target) { int left = 0; int right = arr.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) { return mid; } else if (arr[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; }这里两个坑很多人会踩。第一个,mid写成(left + right) / 2,当left和right接近int最大值时会溢出,正确写法是left + (right - left) / 2。第二个,循环条件是left <= right还是left < right,取决于搜索区间的定义,我习惯用左闭右闭区间,所以用<=",写错了就会漏查边界元素或出现死循环。
二分查找的进阶版本包括查找第一个等于target的下标、最后一个等于target的下标,这类题目在LeetCode和面试里出现频率很高。把基础版吃透了,变体就不难,核心就是搞清楚最终收缩到哪个边界。
2.3 字符串处理:Java开发最常见的算法场景
字符串这块在Java里是实实在在的高频场景,无论是白板题还是实际业务都会遇到。Java给字符串提供了非常丰富的API,比如Character.isLetter、isDigit、isLetterOrDigit,很多人面试时写判断字符串中是否包含非字母数字字符,都是自己一堆正则加循环,其实一个字符数组遍历分分钟搞定:
public static boolean isAlphanumeric(String s) { if (s == null || s.isEmpty()) { return false; } for (char c : s.toCharArray()) { if (!Character.isLetterOrDigit(c)) { return false; } } return true; }字符串算法的大类题型还有:判断回文串、统计字符出现频率、反转字符串、最长公共前缀、括号匹配。处理这些题的基本功是先把String、StringBuilder、char[]之间的关系理清楚。再强调一遍,字符串拼接循环里不要用+,一定要用StringBuilder,不然长字符串拼接就是性能灾难。
2.4 递归与分治:建立拆解问题的思维模型
递归是很多人的“心理阴影”,但递归其实是Java基础算法里最有魅力的一块。很多看起来复杂的题,比如汉诺塔、二叉树遍历、全排列,一旦找到递归结构,代码可以写得非常短。我学递归时最核心的心得就一条:不要试图在脑子里跑完每一层递归,你只需要保证递归三要素成立即可。
递归三要素是:递归出口(base case)、递归表达式、返回值处理。以阶乘为例:
public static int factorial(int n) { if (n <= 1) { // 出口 return 1; } return n * factorial(n - 1); // 表达式 }还有一点要提醒你:能不用递归的地方别硬用。比如求斐波那契数列,纯递归的复杂度是O(2^n),n到40就卡死了。用迭代或者记忆化搜索(用一个数组保存中间结果),直接降到O(n)。这就涉及到算法思维里很重要的“空间换时间”思想。
2.5 数据结构基础:算法题离不开的容器
算法再厉害,没有数据结构支撑就是空中楼阁。Java基础算法阶段,我建议你按这个顺序把手写一遍:数组的动态扩容、单链表的增删改查与反转、栈的入栈出栈、队列的入队出队、HashMap的简单实现、二叉树的层序遍历。
这些数据结构在Java集合框架里都有现成实现,ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,但你学基础算法时必须自己实现一遍。原因很简单:使用别人的轮子不需要知道轮子的辐条怎么装,可一旦轮子出问题你就只能干瞪眼。
我用一个表来总结我自己学数据结构时的对应关系,方便你对照着敲:
| 数据结构 | Java集合对应类 | 手写练习要点 |
|---|---|---|
| 动态数组 | ArrayList | 扩容、缩容、随机访问 |
| 单向链表 | LinkedList | 节点Node定义、反转、快慢指针 |
| 栈 | ArrayDeque/Stack | 括号匹配、最小栈 |
| 队列 | ArrayDeque/Queue | 循环队列、双端队列 |
| 哈希表 | HashMap | hash函数、冲突链表、扩容 |
| 二叉树 | TreeMap/自定义 | 前中后序、层序、深度 |
3. 动手敲起来:用正确的方式暴露问题
3.1 先手写实现,再调用库函数
我看到很多人学算法时有一个很浪费时间的习惯:一边看视频一边抄代码,抄完就觉得自己会了。下次自己写还是无从下手。正确顺序应当是:先关掉视频自己手写,写不出来了再去看一眼思路,然后关掉重新写。这个过程很痛苦,但正是这份痛苦在帮你把算法内化成肌肉记忆。
我在面试候选人的时候,经常能看到有人熟练地背出Arrays.sort的用法,但一问排序原理就沉默。这不是说你不能用库函数,实际开发里你用Arrays.sort完全没问题,但学习阶段你至少要能够脱离sout、脱离IDE的自动补全,把核心逻辑写出来。
3.2 调试三板斧:打印、断点、日志
敲代码一定会遇到“看起来都对,运行结果不对”的情况。这时候不要慌,用下面三个方法逐层排查。第一个是打印法,在循环开始、循环结束、关键变量变化处加System.out.println,这是最原始也最直接的方式,很适合刚起步时用。第二个是断点调试,用IDEA的Debug模式,一步一步看变量变化,一旦发现某个数不对,问题基本就在那一两行附近。
第三个是日志级别的排查,当你的代码越来越复杂,打印语句删不干净时,你可以用日志框架或者干脆写一个开关:
private static final boolean DEBUG = true; private static void log(String msg, Object... args) { if (DEBUG) { System.out.printf(msg, args); System.out.println(); } }这样调试完只需要把DEBUG改成false,不用一行行删打印语句。这个小技巧是我在一个老项目里学到的,实测非常省事。
3.3 边界条件测试:算法题隐藏的“命门”
同一个算法,写对核心逻辑只能拿一半分,边界条件全过才叫真正写完。给你一个我常用的边界测试清单,无论写什么算法题,先跑一遍这些用例:
- 空数组或null数组
- 单个元素的数组
- 两个元素的数组
- 已经有序的数组
- 完全逆序的数组
- 所有元素都一样
- 含有大量重复元素的数组
拿排序来说,一个只有两个元素且逆序的数组 [2, 1],就能测出冒泡排序的内层循环范围写得对不对。二分查找则要额外测试target小于所有元素、大于所有元素、不在数组中且应该插入的位置是第一个或最后一个。
我强烈建议你给自己的每个手写算法配上一个main方法,专门用来跑这些边界用例。这个习惯一旦养成,面试手写代码时你会很自然地先和面试官确认边界输入,这一点非常加分。
3.4 从“跑得对”到“跑得快”:复杂度意识
写完一个算法,别急着庆祝,问自己一句:这个解法的时间复杂度和空间复杂度是多少?如果数组中一亿个元素,还能不能跑?
初学者最容易忽视的问题是:代码能跑,但复杂度太高。比如在循环里反复调用List.indexOf(),或者把字符串相加写在一个大循环里,数据量一大就超时。我自己的经验是,每次写完代码都先估算一下复杂度,当你的解法达到O(n²)时,就停下来想想有没有更好的方案。
这个习惯会直接影响你后面写业务代码。比如在数据量大的循环里查字典,很多人下意识用List,而正确的选择从来都是HashMap,为什么?因为ArrayList的contains是O(n),而HashMap的get是O(1)。这样的差别,在千万级数据面前是几秒和几十毫秒的差别。
4. 面试与竞赛双线实战:刷题的正确姿势
4.1 面试向:高频基础题吃透,思路优先
Java开发岗面试最常考的基础算法题,我根据自己面试别人的经验和被面试经验,整理了这么几道必刷题:反转单链表、两数之和、判断括号是否有效、合并两个有序数组、快速排序、二分查找、求最大子数组和、二叉树前中后序遍历。这些题在LeetCode上都有对应原题,也都是“leecode必刷基础算法题”里的高频钉子户。
刷这些题时,不要只背答案。面试官看重的是你的思考过程和代码组织能力。以两数之和为例,面试时你要能在短时间内说清楚三种解法:暴力O(n²)的思路、用哈希表O(n)的思路、排序加双指针的思路,并且分析各自的时间和空间复杂度。你要是张嘴就能说出“我直接双重循环”,那基本注定了低分。
我见过不少候选人,题目刷了三四百道但面评很差,原因是背题痕迹太重。你要记住,面试考算法不是在考你记忆力,而是在看你遇到问题时的解题框架。拿个新的变形题扔给你,如果你只会“这题我见过”,那基本就废了。
4.2 蓝桥杯向:Java竞赛题的数字与套路
蓝桥杯Java组的题目,和前端的“字节题”风格很像,喜欢考察数字处理和逻辑推理。诸如进制转换、大数运算、回文数判断、公约数公倍数、日期计算、排列组合,都是高频考点。Java在这类题上的优势是提供了BigInteger和BigDecimal,还有极其丰富的字符串API。
举个例子,蓝桥杯里经常出现类似“给定一个正整数n,求n的阶乘结果的后三位数字”这种题,如果你用int直接算,很快溢出。这时候你要么用BigInteger,要么用取模运算在每一轮乘法后取后三位,后者性能好太多。Java竞赛里这类“取模防溢出”的技巧非常重要,你刷题时应该有意识地练习% 1000000007这类常见取模操作。
另一个高频方向是图论和动态规划的入门题,比如最短路径、背包问题、最长上升子序列。这些题在蓝桥杯省赛里年年出现,对算法基础的要求就更高了。我的建议是:先把排序、递归、DFS/BFS这些基础打扎实,再碰动态规划和图论,别一口吃成胖子。
4.3 刷题平台怎么用:LeetCode与Java题单的高效玩法
现在刷算法题,首选平台就是LeetCode。打开首页你能看到海量题目,不加以筛选地乱刷就是最错误的做法。我的刷题策略是按标签刷,先点开数组、链表、字符串、二分查找、排序这些基础标签,每个标签内部按通过率从高到低刷20道左右。通过率高的题往往是经典模板题,用来建立信心和理解套路都很合适。
LeetCode的每道题都有讨论区和官方题解。但你要记住,看题解的时机很有讲究,自己实在想不出来再看,看完合上页面自己重写一遍。直接抄一遍就跑,效果约等于零。更合理的复盘方式是写完之后,去题解区找那些点赞最高的思路,看看自己有没有遗漏的优化方向,再决定要不要重写。
我还会用一个很笨但很有用的方法:维护一份错题本,记录题目链接、我的错误思路、正确的解题方向、以及这道题对应的模板。每周固定抽出一天重做错题本里的三道题。实测下来,这个方法的遗忘速度远低于单纯刷题。
5. 常见问题与踩坑实录:我把新手期踩过的坑都写在这里
5.1 Java环境配置:磨刀不误砍柴工
学习算法前,先把Java环境配好。这里说的“配好”不是指装个JDK就完事,而是要理解环境变量为什么会配错、启动失败怎么排查。多数教程会让你在系统变量里新建JAVA_HOME,然后在PATH中添加%JAVA_HOME%\bin。如果你安装了多个JDK,PATH里哪个排前面,命令行里执行的就是哪个版本,这是最常见的“版本不对”问题来源。
如果你遇到“java不是内部或外部命令”的提示,九成是PATH配置没有生效或拼写错误;如果你执行java -version显示的版本和你想用的JDK不一致,就检查PATH中JDK路径的先后顺序。还有JAVA_HOME的路径不要加末尾的反斜杠,不要在值里加花括号之外的多余空格,这些都是新手期极容易踩的坑。
5.2 手写算法的高频错误:数组越界、死循环、溢出
手写算法翻车最多的是三类错误。
第一类,数组越界。比如遍历数组时用了for (int i = 0; i <= arr.length; i++),多出来的那一次必然越界。还有一种隐蔽情况:在快排partition或者二分查找中取了arr[i-1]或arr[j+1]而没有先判断边界是否合法。
第二类,死循环。典型场景是二分查找中指针移动条件写反,或者快排里i和j移动后忘记重新判断i < j。死循环在本地调试时很好发现,但面试现场写白板就比较尴尬。我建议你在写完循环后,立刻检查循环体内的指针是否一定朝收敛方向移动,这是预防死循环最有效的一步。
第三类,整数溢出。mid = (left + right) / 2在left和right巨大时会溢出,正确答案是写成left + (right - left) / 2。还有一个容易忽略的场景:进行数字运算类算法题时,累加和可能超过int范围,记得用long接收中间结果。
5.3 心态崩塌自救指南:刷了忘、忘了刷怎么办
算法学习最挫败的时刻不是不会写,而是昨天刚会的题今天又不认识了。这不是你记忆力差,是正常现象。我自己的体验是,一个算法至少要三次以上的重复接触才能真正内化:第一次接触理解思路,第二次独立实现,第三次在一周后重新默写不出来再看答案。
整个过程很像背单词,艾宾浩斯遗忘曲线同样适用于算法。所以我强烈建议你使用“隔天复习 + 每周复盘”的节奏,而不是疯狂地一天刷20道新题然后第二天全忘光。另一个行之有效的方法是给别人讲题。你在自己的笔记里用文字讲,有条件的在社区发题解,讲的过程倒逼你把逻辑漏洞暴露出来,比闷头刷十道都有用。
6. 相信坚持的力量:我给Java学习者的实操路线
6.1 按阶段推进,别想一口吃成胖子
Java基础算法学习,我根据自己带人带项目的经验,画了一条我验证过多次的路线。第一阶段是打牢Java语法,顺序结构、分支、循环、类与对象、集合框架的基本使用,这个阶段不必太深入算法。第二阶段是数据结构实现,按照我刚才列的单子把数组、链表、栈、队列、哈希表每个手写一遍基础操作。
第三阶段集中突击基础算法,排序、二分、递归、字符串、双指针,这是整条路线的核心阶段,耗时也最久,建议保持每天至少1小时的独立编码时间。第四阶段进入刷题期,LeetCode按标签刷,蓝桥杯真题按年份刷,这个阶段可以持续到你找到工作或比完赛为止。
不要把“学算法”当成一个孤立的负重任务,它应当是你日常编码习惯的一部分。每写一道业务题,想一想有没有更优的数据结构;每看一段框架源码,问一句这里用的是哪个算法思想。
6.2 建立最小可持续的打卡机制
常有人问我:“大佬,你一天刷多少道题才够?”我的回答是,一天能稳定完成2道并完全理解,坚持三个月的效果远好于周末突击20道。因为算法学习的本质是建立起模式识别能力,这种能力需要睡眠、需要间隔、需要长时间积累才能形成。
我的打卡方式是每天在待办清单里留一个固定任务:“手写一道基础算法题并跑通测试用例”。这个任务足够小,不会让人有任何心理压力;但又足够具体,确保每天都在跟算法打交道。我会在GitHub上建一个仓库,把每天的代码提交上去,用commit记录来量化自己的坚持。回看commit记录的时候,那种“我真的在变强”的实感,是支撑我走下去最大的精神动力。
6.3 输出是最好的内化:写题解、记笔记、讲给你听
最后一个建议,也是我认为坚持得最久的人都在做的事情:输出。你学到一个算法,闭卷把它默写出来是初级输出;写一篇题解把解题思路、边界测试、复杂度分析都讲清楚,是高级输出;能把一道题给一个完全不懂的人讲懂,是最顶级的输出。
我自己最开始学快速排序的时候,总觉得理解了,直到我在博客里想写一篇快排的文章,写到partition交换那一步卡住了,才发现自己根本没完全理清。那次之后我彻底理解了“做出来”和“讲清楚”之间的距离。学Java基础算法,真正难的不是那些算法本身,而是你有没有勇气每天打开编辑器、真实地敲一遍、坦然地面对错误。
我带过的新人里,进步最快的从来不是天赋最好的,而是那个每天雷打不动提交两段代码的人。基础算法没有那么多捷径,我相信坚持的力量,也希望读到这里的你,从今天起动手敲起来,等着你的下一篇题解。