这事儿我跟你实话实说,当年我刚开始搞算法的时候,也被这个问题绕晕过。一边是《剑指Offer》《算法导论》堆得像山一样的书,另一边是LeetCode上几千道题目闪着诱人的红绿光芒。很多小伙伴问我:“到底该怎么选?” 其实吧,这俩根本不冲突,关键看你现在的阶段和目的。咱们今天就掰开揉碎了聊聊,顺便给你画一条清晰的路线,哪怕你是刚入门的小白,也能照着一步步走。
先别急着刷题,先搞清楚“为什么”
在扔给你一堆资源之前,我得先问你几个问题,你自己在心里默念一下答案:
- 你学算法是为了什么? 是为了找大厂工作?还是为了提升编程思维?或者纯粹是兴趣爱好?
- 你现在的Java基础怎么样? 集合类熟不熟?会不会用Stream API?指针(虽然Java没有指针,但我懂你的意思)概念清不清晰?
- 你每天能投入多少时间? 是每天两小时,还是只能周末突击?
这三个问题决定了你的学习路径。比如,如果你是为了面试大厂,那LeetCode必须刷,而且得高强度刷;如果你是为了夯实基础,那书本的知识体系更完整。我见过太多人一头扎进题海,结果题目刷了不少,遇到真实场景还是懵圈,就是因为缺乏系统性的知识框架。
算法书:构建你的“内功心法”
很多人一听到“看算法书”就头大,觉得枯燥、难懂。但其实,算法书的作用就像盖房子前的地基勘测和蓝图设计。你不看这个,直接开始堆砖头(刷题),房子可能能盖起来,但迟早会塌,或者盖得歪歪扭扭。
我推荐几本真正值得啃的书,分阶段来看:
入门阶段:培养兴趣,建立直觉
《算法4》(Algorithms, 4th Edition) by Robert Sedgewick
这本书简直是算法界的“圣经”之一,而且它用的是Java写的!这对咱们来说太友好了。作者不仅讲算法,还注重性能分析和实际应用。书中的代码风格非常规范,读起来像是在读一篇篇优美的散文,而不是枯燥的教科书。
比如,当你学到二分查找的时候,书里不会只甩给你一个公式,而是会带你一步步推导,然后给出一个Java实现:
public class BinarySearch {
public static int rank(int key, int[] a) {
// 在a[]里搜索key
int lo = 0;
int hi = a.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // 防止溢出
if (key < a[mid]) hi = mid - 1;
else if (key > a[mid]) lo = mid + 1;
else return mid;
}
return -1;
}
}
你看,这段代码是不是清晰明了?而且书里还会告诉你,为什么mid = lo + (hi - lo) / 2比mid = (lo + hi) / 2更好,因为后者在lo和hi都很大的时候可能会溢出。这种细节,只有在书本里才能学到。
《大话数据结构》 by 程杰
如果你觉得《算法4》还有点厚,那这本中文书就是你的“入门甜筒”。它用漫画和故事的形式讲解数据结构,读完不会觉得累,反而会觉得“哎,原来链表长这样啊”。特别适合完全零基础的同学。
进阶阶段:深化理解,应对面试
《剑指Offer》 by 何海涛
这本书可以说是国内IT面试的“红宝书”。里面的题目很多都源自真实的面试题,比如二叉树遍历、栈和队列的应用、动态规划入门等。它的难度适中,讲解也很到位。
比如,里面有一个经典题目:重建二叉树。给定前序遍历和中序遍历的结果,重建二叉树。书里会一步步教你如何递归地解决这个问题:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
if (pre.length == 0 || in.length == 0) {
return null;
}
TreeNode node = new TreeNode(pre[0]);
for (int i = 0; i < in.length; i++) {
if (pre[0] == in[i]) {
node.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1), Arrays.copyOfRange(in, 0, i));
node.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length), Arrays.copyOfRange(in, i + 1, in.length));
break;
}
}
return node;
}
}
注意,这里用到了Arrays.copyOfRange,这是Java集合库里的常用工具,你得熟练掌握。
《算法竞赛入门经典》(紫书) by 刘汝佳
如果你稍微有点竞赛兴趣,或者想挑战更高难度的题目,这本书是必经之路。它的题目设计很巧妙,从简单到困难循序渐进。
高阶阶段:体系化,查漏补缺
《算法导论》(CLRS)
这本书就不用多说了,算法领域的“百科全书”。但我不建议初学者直接啃它,因为太厚、太理论化。你可以把它当作工具书,遇到不懂的概念时去查阅。比如,当你学到红黑树的时候,可以翻翻这本书,看看它的旋转操作和插入删除的细节,保证你的理解是准确的。
《编程珠玑》 by Jon Bentley
这本书薄薄一本,但每一章都是一个经典问题的深入探讨。它教会你的不是具体的算法,而是如何思考。比如,第一章就抛出一个问题:如何高效地排序1000万个7位整数?读完你会明白,有时候最简单的思路(比如位图排序)反而最有效。
LeetCode:打造你的“外功招式”
书本是内功,LeetCode是外功。内功深厚,外功才能发挥最大威力。但外功练得不够,内功再深也打不出伤害。所以,刷题是必须的,但要有策略地刷。
为什么LeetCode重要?
- 面试必经之路:几乎所有互联网公司的技术面试都会涉及算法题,LeetCode是最常见的题库。
- 快速验证学习成果:看书的时候你觉得懂了,一刷题才发现根本不会。LeetCode能逼着你把知识转化为代码。
- 培养解题直觉:刷得多了,你会对某些题型形成条件反射。比如,看到“最短路径”就想Dijkstra,看到“子集”就想回溯。
怎么刷?别当无头苍蝇
很多小伙伴一上来就从头到尾刷第一页,结果刷到第十章就放弃了。这是大忌!我给你一个更聪明的策略:
第一步:按专题刷
不要漫无目的地刷,而是按照数据结构或算法类型来刷。比如:
- 链表专题:Reverse Linked List, Merge Two Sorted Lists, Linked List Cycle
- 二叉树专题:Binary Tree Inorder Traversal, Maximum Depth of Binary Tree
- 动态规划专题:Climbing Stairs, Coin Change, Longest Increasing Subsequence
这样的好处是,你能在短时间内集中攻克一类问题,形成知识闭环。
第二步:从简单题开始
LeetCode上有Easy、Medium、Hard三种难度。新手建议从Easy开始,建立信心。比如:
- Two Sum:最简单的哈希表应用,理解“空间换时间”的思想。
- Valid Parentheses:栈的经典应用,理解后进先出(LIFO)的特性。
第三步:Medium题是核心
面试中,Medium难度的题目占比最高。这也是你真正需要下功夫的地方。比如:
- Longest Substring Without Repeating Characters:滑动窗口的经典题目,理解双指针的运用。
- Median of Two Sorted Arrays: harder的题目,考察二分查找的变种应用。
第四步:Hard题量力而行
除非你目标是顶尖公司(如Google、Facebook),否则Hard题可以作为挑战,不必强求全部掌握。但至少要读懂别人的解法,理解思路。
刷题的技巧:别只盯着答案看
我见过太多人刷题的误区:看一道题,想不出来,直接看答案,然后抄一遍,觉得“懂了”。其实这完全没用!正确的做法是:
- 独立思考至少15分钟:哪怕一点思路都没有,也要自己尝试。这个过程能锻炼你的思维肌肉。
- 对照答案,找出差距:看完答案后,对比自己的思路,看哪里没想到。是数据结构选错了?还是边界条件处理漏了?
- 手动实现一遍:不要复制粘贴!亲手敲代码,调试通过,这才是你的东西。
- 过几天再复盘:遗忘曲线告诉我们,不复习等于白学。建议在一周后重新做这道题,看是否还能独立做出来。
推荐刷题顺序:周赛 + 精选
LeetCode官网有一个“学习计划”功能,里面有很多精选的刷题路线。比如:
- LeetCode 101:适合新手的101道经典题目,覆盖常见算法类型。
- ** hot 100**:LeetCode官方的热门100题,基本上面试常考的都涵盖了。
我建议你按照“周赛 + 精选”的组合来刷。每周参加一次LeetCode的周赛(即使只做出一道题也好),感受一下时间压力下的解题状态。同时,每天刷2-3道精选题目,保持手感。
实战项目:让算法“活”起来
光看书、光刷题,有时候会觉得枯燥,而且不容易形成长期记忆。我强烈建议你做一个实战项目,把学到的算法应用到真实场景中。这样不仅能巩固知识,还能丰富你的简历。
项目建议一:简单的地图导航系统
这个听起来很厉害,但其实核心就是一个最短路径算法。你可以用Dijkstra算法来实现一个小型的地图导航。
需求分析:
- 输入:两个城市节点(比如北京和上海)
- 输出:最短路径及距离
技术栈:
- Java作为主要语言
- 使用图的邻接表表示法
- Dijkstra算法求最短路径
代码片段:
import java.util.*;
class Graph {
private int numVertices;
private LinkedList<Edge>[] adjLists;
// 内部类:边
static class Edge {
int dest;
int weight;
Edge(int dest, int weight) {
this.dest = dest;
this.weight = weight;
}
}
Graph(int vertices) {
numVertices = vertices;
adjLists = new LinkedList[vertices];
for (int i = 0; i < vertices; i++) {
adjLists[i] = new LinkedList<>();
}
}
void addEdge(int src, int dest, int weight) {
adjLists[src].add(new Edge(dest, weight));
adjLists[dest].add(new Edge(src, weight)); // 无向图
}
// Dijkstra算法
void dijkstra(int src) {
PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
int[] dist = new int[numVertices];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
pq.add(new Edge(src, 0));
while (!pq.isEmpty()) {
Edge current = pq.poll();
int u = current.dest;
for (Edge edge : adjLists[u]) {
int v = edge.dest;
int weight = edge.weight;
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.add(new Edge(v, dist[v]));
}
}
}
System.out.println("Shortest distances from node " + src + ":");
for (int i = 0; i < numVertices; i++) {
System.out.println(i + " -> " + dist[i]);
}
}
}
这个项目的亮点在于,你不仅学会了Dijkstra算法,还实践了图的存储、优先队列的使用,这些都是在LeetCode里能学到的,但组合在一起做项目,才是真正的应用。
项目建议二:文本编辑器里的搜索功能
这个听起来很普通,但你可以用KMP算法来实现一个高效的文本搜索功能。
需求分析:
- 输入:一个长文本(比如一篇文章)和一个模式串(比如要搜索的词)
- 输出:模式串在长文本中的所有出现位置
技术栈:
- Java
- KMP算法
代码片段:
public class KMPSearch {
// 构建next数组
private static int[] computeLPSArray(String pattern) {
int m = pattern.length();
int[] lps = new int[m];
int len = 0;
int i = 1;
lps[0] = 0; // lps[0] always is 0
while (i < m) {
if (pattern.charAt(i) == pattern.charAt(len)) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
// KMP搜索
public static List<Integer> search(String text, String pattern) {
List<Integer> result = new ArrayList<>();
int n = text.length();
int m = pattern.length();
int[] lps = computeLPSArray(pattern);
int i = 0; // index for text
int j = 0; // index for pattern
while (i < n) {
if (pattern.charAt(j) == text.charAt(i)) {
i++;
j++;
}
if (j == m) {
result.add(i - j);
j = lps[j - 1];
} else if (i < n && pattern.charAt(j) != text.charAt(i)) {
if (j != 0) {
j = lps[j - 1];
} else {
i++;
}
}
}
return result;
}
public static void main(String[] args) {
String text = "ABABDABACDABABCABAB";
String pattern = "ABABCABAB";
System.out.println("Found pattern at indices: " + search(text, pattern));
}
}
这个项目让你体会到,KMP算法相比普通的暴力搜索,时间复杂度从O(n*m)降到了O(n+m),效率提升巨大。这就是算法的价值所在。
项目建议三:简单的压缩工具
这个可以用霍夫曼编码来实现。霍夫曼编码是一种贪心算法,常用于数据压缩。
需求分析:
- 输入:一个字符串
- 输出:压缩后的二进制串,以及解码后的原始字符串
这个项目稍微复杂一点,但非常有成就感。你可以参考LeetCode上的相关题目,比如“Decode Ways”,然后扩展成完整的压缩工具。
如何平衡“看书”和“刷题”?
很多小伙伴在这两者之间纠结,今天想看书,明天又想刷题,结果两头都没做好。我给你一个时间表,你可以参考:
第一阶段:入门(1-2个月)
- 目标:熟悉基本数据结构和简单算法
- 时间分配:70%看书,30%刷题
- 具体安排:
- 早上:看《算法4》或《大话数据结构》相关章节,理解概念
- 晚上:刷对应类型的Easy题,比如链表相关的题目
- 周末:复习本周内容,整理笔记
第二阶段:进阶(2-4个月)
- 目标:掌握中高级算法,应对面试
- 时间分配:50%看书,50%刷题
- 具体安排:
- 早上:看《剑指Offer》或《算法竞赛入门经典》
- 晚上:刷Medium题,按专题刷,比如这周刷动态规划,下周刷回溯
- 周末:参加LeetCode周赛,检验学习效果
第三阶段:冲刺(1-2个月)
- 目标:查漏补缺,高强度刷题
- 时间分配:30%看书,70%刷题
- 具体安排:
- 早上:刷Hot 100或公司真题
- 下午:复习薄弱知识点,比如某些算法的细节
- 晚上:做模拟面试,限时解题
一些实用的学习技巧
做笔记:不要只是看书或刷题,要自己整理笔记。可以用Markdown写,或者用Notion这种工具。笔记里可以包含:算法思路、代码实现、时间复杂度、易错点等。
画图理解:很多算法(尤其是树、图相关的),用脑子想很容易乱。拿出一张纸,画一画节点之间的关系,思路会清晰很多。比如,画一画二叉树的遍历过程,或者画一画Dijkstra算法的每一步。
讲给别人听:费曼学习法告诉我们,如果你能把一个概念讲给别人听懂,那你才是真正掌握了。你可以找一个小伙伴,或者对着摄像头讲,甚至写博客。我在知乎上看过很多算法解析文章,写得好的,自己对知识的理解也会更深。
不要死记硬背:算法不是诗词,不需要背诵全文。你要理解的是思想和模式。比如,二分查找的核心思想是“分治”,动态规划的核心是“最优子结构”和“重叠子问题”。明白了这些,换一种题目你也能做出来。
保持耐心:算法学习是一个漫长的过程,不可能一蹴而就。我见过很多人,刷了半个月题,感觉没啥进步,就放弃了。其实这是正常的,
