简介:面向Java学习者与开发者的数据结构与算法资料包,以韩顺平老师的系列课程为主线,帮助夯实算法基础、备战技术面试或提升编码能力。包体共140个文件,涵盖课堂视频链接、配套课件(pptx/pdf)、可运行源码(java/class)、学习笔记与数据结构图解,覆盖数组、链表、栈、队列、集合、哈希表、树、图以及常见排序搜索算法等核心知识点。源码可直接运行验证学习效果,课件与思维导图适合系统梳理知识脉络,笔记和图解便于快速回顾复盘关键难点。资源包整体约24.06MB,目前已有182人学习浏览。借助这套资料可高效搭建完整学习路径,既对照实例理解理论,也能动手实践巩固能力,为算法进阶或专项面试准备打牢基础。
1. 打开 Java 数据结构与算法分享包:别急着看视频,先把这个清单吃透
这份 Java 数据结构与算法分享包,我拆开后的第一反应是:压缩包里躺着一堆 .class 文件,光看名字就知道这是从某套课程里直接导出的编译产物。它不像教程那样按章节排好,更像是一个“代码仓库 + 视频入口 + 笔记图解”的混合体。里面的类覆盖了单链表、逆波兰计算器、哈夫曼编码、图、最小生成树、马踏棋盘和贪心算法,正好是初学者最容易卡住的几个山头。适合两类人:一类是准备面试想把算法补扎实的,另一类是课上听懂了但没手写过代码、需要一个可参考实现的。下面我会按“哪个先跑、怎么跑、哪里容易翻车”的顺序把资源拆给你看。
2. 源码包里的 10 个 class 文件:每个类对应什么算法,先跑哪个最划算
压缩包里没有按章节排好的目录,只看到一堆编译后的 class 文件和零散素材。所以第一步不是打开视频,而是先把类名和算法知识对应起来,我对照了一份配套笔记,整理成下面这张表。
2.1 从类名反推实现:这些 class 到底在说什么
| class 文件名 | 对应的数据结构/算法 | 学习重点 |
|---|---|---|
| SingleLinkedListDemo | 单链表 | 头节点、增删改查、指针移动 |
| HeroNode | 链表/树的节点定义 | 节点 data 与 next/left/right 的设计 |
| PolandNotation | 波兰表达式(前缀/后缀表示法) | 中缀转后缀、栈求值 |
| ReversePolishMultiCalc | 逆波兰计算器 | 多位数、小数、运算符优先级 |
| HuffmanCode | 哈夫曼树与编码 | 构建哈夫曼树、前缀编码、压缩解压 |
| Graph | 图结构 | 邻接矩阵/邻接表、DFS/BFS |
| KruskalCase | 克鲁斯卡尔算法 | 最小生成树、并查集判环 |
| HorseChessboard | 马踏棋盘(骑士周游) | 回溯搜索、贪心剪枝 |
| GreedyAlgorithm | 贪心算法 | 集合覆盖、局部最优选择 |
清单里 HeroNode 这个名字出现了两次,不是打包时重复放错文件,而是因为这个节点类在单链表案例里被用作节点,在树相关的案例里也被复用了。导入 IDE 时要注意别选错副本,以带 .java 源码的位置为准。
拿到包以后,我建议先用上面这张表做一次“知识体检”。这套资料的编排逻辑其实是线性结构先上,然后是树和图,最后是算法策略。如果你直接跳去跑马踏棋盘,多半会被递归和回溯绕晕,所以学习顺序比学习速度更重要。
2.2 学习路线:按“线性→树→图→算法策略”推进
我自己跑通这套资源用的顺序是固定的,你照着走基本不会卡壳:
- 先跑 SingleLinkedListDemo,理解 HeroNode 的自引用结构和指针移动。
- 再跑 PolandNotation / ReversePolishMultiCalc,理解栈在表达式求值里的作用。
- 接着看 HuffmanCode,把树、优先队列和贪心频率统计串起来。
- 然后进 Graph,掌握图的存储方式,再跑 KruskalCase。
- 最后挑战 HorseChessboard 和 GreedyAlgorithm。
这个顺序的设计理由很直接:逆波兰计算器用到了栈,哈夫曼编码用到了树和优先队列,图的 DFS/BFS 又依赖栈或队列的思想,而马踏棋盘本质是图的深度优先搜索。每一步都建立在前一步的基础上,所以我在 4.2 里看到克鲁斯卡尔代码时,已经很熟悉并查集怎么写了。
如果你是为了面试突击,可以把第 3 步哈夫曼提前,因为它单独成块,不依赖图那部分。但如果你是想把基础打牢,还是按上面顺序来。
2.3 跑通第一个 Demo:编译、运行与目录组织
先从解压开始,注意看包里到底有哪些东西:
unzip "Java数据结构分享.zip" -d java-ds-algo cd java-ds-algo find . -name "*.class" | sortunzip用-d指定解压目录,避免当前目录被文件铺满;find列出所有 class 文件,确认目录结构。如果包里有.java源码,下一步就能直接编译;如果只有.class文件,那你需要先用反编译工具或者javap把类结构读出来。
用 JDK 自带的 javap 查看某一个类的结构:
javap -p -c SingleLinkedListDemo.class | head -100-p表示显示私有成员,-c表示显示字节码。对于没有源码的 class,这是最直接的“黑匣子打开方式”。但注意,javap 只能还原方法签名和字节码,不能帮你恢复出完整的 Java 源码,所以更适合确认类之间的调用关系,不适合逐行阅读。
如果包里带了 Java 源码,重新编译再跑会更顺手:
javac -encoding UTF-8 *.java java -cp . SingleLinkedListDemo-encoding UTF-8是为了避免中文注释乱码;-cp .指定当前目录为 classpath。很多时候报NoClassDefFoundError就是因为忘了加这个-cp .。
如果你想自己写一个最小可运行的 HeroNode 来对照理解,可以这样:
class HeroNode { int no; String name; HeroNode next; HeroNode(int no, String name) { this.no = no; this.name = name; } }这个类就是典型的自引用结构:自己持有自己类型的next引用。课程源码里的 HeroNode 通常还会带一个toString或链表操作方法,但核心定义就是这段代码。只要把这个结构看懂,后面的链表增删改查都不难理解。
3. 逆波兰计算器与哈夫曼编码:两个最值得精读的算法实现
整个资源包里,我认为最值得逐行精读的就是 ReversePolishMultiCalc 和 HuffmanCode。一个讲透了栈在表达式处理中的用法,一个讲透了树在编码压缩中的应用。这两个类读懂以后,再看图相关代码会轻松很多。
3.1 ReversePolishMultiCalc 的双栈设计与优先级表
ReversePolishMultiCalc 这个类名里的 Multi 明确告诉你:它支持多位数运算,所以不能按单字符处理。核心流程分两步:中缀转后缀,后缀求值。
中缀转后缀的简化实现如下:
import java.util.*; public class ReversePolishMultiCalc { private static int priority(String op) { return switch (op) { case "+", "-" -> 1; case "*", "/" -> 2; default -> 0; }; } public static List<String> toSuffix(String expr) { List<String> result = new ArrayList<>(); Stack<String> stack = new Stack<>(); int i = 0; while (i < expr.length()) { char c = expr.charAt(i); if (Character.isDigit(c) || c == '.') { StringBuilder sb = new StringBuilder(); while (i < expr.length() && (Character.isDigit(expr.charAt(i)) || expr.charAt(i) == '.')) { sb.append(expr.charAt(i++)); } result.add(sb.toString()); continue; } if (c == '(') { stack.push("("); } else if (c == ')') { while (!stack.isEmpty() && !stack.peek().equals("(")) { result.add(stack.pop()); } stack.pop(); } else { String op = String.valueOf(c); while (!stack.isEmpty() && priority(op) <= priority(stack.peek())) { result.add(stack.pop()); } stack.push(op); } i++; } while (!stack.isEmpty()) result.add(stack.pop()); return result; } }逻辑并不复杂:遇到数字和小数点就连续读入,构成一个完整数字 token;遇到左括号压栈,右括号弹到左括号为止;运算符则根据优先级弹出栈顶高优先级或同优先级的运算符。注意括号优先级不放进priority表里,而是靠单独分支处理,否则括号也会被当成运算符弹出。
这里有一个容易漏掉的点:如果表达式里出现负数,比如-3 + 5,这个实现会把负号当成减号直接压栈,最后算出3 - 5 = -2。课程源码里一般会在入库前做一次预处理,把负号和前面的数字拼成一个负数 token。
后缀求值部分更短,但顺序错了就是血泪教训:
public static double evaluate(List<String> suffix) { Stack<Double> stack = new Stack<>(); for (String token : suffix) { if (token.matches("-?\\d+(\\.\\d+)?")) { stack.push(Double.parseDouble(token)); } else { double b = stack.pop(); double a = stack.pop(); switch (token) { case "+" -> stack.push(a + b); case "-" -> stack.push(a - b); case "*" -> stack.push(a * b); case "/" -> stack.push(a / b); } } } return stack.pop(); }这里最需要注意的是:遇到运算符先弹出来的是右操作数b,后弹出来的是左操作数a。减法除法顺序写反,结果必然不对。我见过很多实现直接在代码里写成stack.push(stack.pop() - stack.pop()),表面上看着行,实际结果全反。ReversePolishMultiCalc 既然叫 Multi,说明它处理的是多位数和小数,所以这里统一用double而不是int。
3.2 HuffmanCode 的构建、编码与解码
哈夫曼编码的核心是构建一棵带权路径最短的二叉树。课程源码里一般先把字节数组的所有权值统计出来,再用优先队列反复合并最小权值节点。
节点类定义:
class HuffmanNode implements Comparable<HuffmanNode> { byte data; int weight; HuffmanNode left, right; HuffmanNode(byte data, int weight) { this.data = data; this.weight = weight; } @Override public int compareTo(HuffmanNode o) { return this.weight - o.weight; } }data存原始字节,weight存出现次数。实现Comparable接口是为了让优先队列能自动按权值排序。这里有个细节:compareTo直接返回this.weight - o.weight在常规场景没问题,但如果权值可能很大,更稳妥的写法是Integer.compare(this.weight, o.weight),可以避免极端的溢出风险。
构建哈夫曼树:
public static HuffmanNode buildHuffmanTree(Map<Byte, Integer> freqMap) { PriorityQueue<HuffmanNode> queue = new PriorityQueue<>(); for (Map.Entry<Byte, Integer> entry : freqMap.entrySet()) { queue.offer(new HuffmanNode(entry.getKey(), entry.getValue())); } while (queue.size() > 1) { HuffmanNode left = queue.poll(); HuffmanNode right = queue.poll(); HuffmanNode parent = new HuffmanNode(null, left.weight + right.weight); parent.left = left; parent.right = right; queue.offer(parent); } return queue.poll(); }每次从优先队列里弹出权值最小的两个节点,合成一个新的父节点,父节点的data为null,表示它不是叶子节点。循环直到队列剩下一个节点,就是整棵哈夫曼树的根。这个合并过程本身就是贪心:每次选局部最小的两个节点合并,最终得到全局最优的编码树。
编码表生成:
public static void buildCodeTable(HuffmanNode node, String code, Map<Byte, String> codeTable) { if (node == null) return; if (node.left == null && node.right == null) { codeTable.put(node.data, code.length() == 0 ? "0" : code); return; } buildCodeTable(node.left, code + "0", codeTable); buildCodeTable(node.right, code + "1", codeTable); }左子树走 0,右子树走 1,到叶子节点就把整条路径写入编码表。这里有一个必须处理的边界:如果整棵树只有一个叶子节点,递归走到它时code还是空字符串,编码表里会放一个空串进去,解码时完全没法用。所以我在叶子节点判断里加了一句:如果code长度是 0,就强制编码为"0"。这是课程源码里最容易忽略的角落,解压乱码十有八九和它有关。
3.3 参数设置与调试入口
| 模块 | 关键参数 | 调试点 |
|---|---|---|
| ReversePolishMultiCalc | 运算符优先级表、小数点判断 | 打印中缀转后缀的 token 列表 |
| HuffmanCode | 字节统计数组长度 256、补位长度 | 打印每个字节的编码表,比对原文与解压结果 |
我一般会先把toSuffix单独调通,再跑evaluate;HuffmanCode 则先在小字符串上验证编码表,再上文件压缩。这样问题能被限制在单步里,不会整个程序一跑就变成黑匣子。
4. 克鲁斯卡尔、马踏棋盘与贪心算法:图论和算法策略的落地差异
如果说前两章是线性结构和树,那这一章就进入图论和算法策略了。Graph、KruskalCase、HorseChessboard、GreedyAlgorithm 这四个类放在一起,刚好覆盖了图的存储、最小生成树、回溯搜索和贪心选择四种典型场景。
4.1 Graph 的存储与遍历
Graph 类负责图的存储。常见两种方式:邻接矩阵适合稠密图,邻接表适合稀疏图。课程源码里如果实现了马踏棋盘,那一定是邻接表或直接用坐标偏移,因为棋盘很大但每个点的可走边数很少。
邻接矩阵初始化代码:
public class Graph { private int[][] adjMatrix; private char[] vertexData; public Graph(int size, char[] vertexData) { this.adjMatrix = new int[size][size]; this.vertexData = vertexData; } public void addEdge(int i, int j, int weight) { adjMatrix[i][j] = weight; adjMatrix[j][i] = weight; } }无向图需要同时设置两个对称位置:adjMatrix[i][j]和adjMatrix[j][i];有向图只设置一边。这个看起来很简单,但 KruskalCase 里如果下标从 1 开始,而矩阵从 0 开始,读数据时很容易越界。我习惯统一从 0 开始,然后在算法内部用vertex - 1做转换。
4.2 KruskalCase:最小生成树的权值陷阱
克鲁斯卡尔算法的思想是把边按权值排序,从小到大依次选边,选之前用并查集判断这条边会不会成环。
并查集的 find 和 union:
public int find(int[] parent, int x) { if (parent[x] != x) { parent[x] = find(parent, parent[x]); } return parent[x]; } public void union(int[] parent, int x, int y) { int rootX = find(parent, x); int rootY = find(parent, y); if (rootX != rootY) { parent[rootX] = rootY; } }find做了路径压缩:查找过程中把路径上的节点直接挂到根节点下面,避免下一次递归再走一遍长路径。union的合并方向是先比较根,如果根相同说明已经连通,不需要合并。这个“先 find 再比较”就是判环的核心。
选边主循环:
for (Edge edge : sortedEdges) { int rootA = find(parent, edge.start); int rootB = find(parent, edge.end); if (rootA != rootB) { parent[rootA] = rootB; mst.add(edge); } }这里最典型的坑是有人写成parent[edge.start] != parent[edge.end],因为路径压缩之后parent[x]并不一定就是根节点,它可能是某棵子树的父节点。必须用find的结果比较,不能直接读parent数组。我在调试这段代码时,会在union里加一行打印rootX和rootY,肉眼确认合并方向,一旦出现环,看输出立刻就知道是哪条边选错了。
4.3 HorseChessboard:回溯深度优先与贪心优化
马踏棋盘本质是深度优先搜索。代码骨架:
public boolean backtrack(int row, int col, int step) { board[row][col] = step; if (step == rows * cols) { return true; } for (int[] next : getNext(row, col)) { if (board[next[0]][next[1]] == 0) { if (backtrack(next[0], next[1], step + 1)) { return true; } board[next[0]][next[1]] = 0; } } board[row][col] = 0; return false; }step表示当前是第几步,board里的值如果为 0 表示还没走过。走完所有格子返回 true;如果一条路走到头,回溯时要把格子清零,把选择权还给上一层。
这个实现如果直接跑 8×8 棋盘,很多起点会慢到怀疑人生。关键优化在getNext返回的候选顺序上:
List<int[]> nextSteps = getNext(row, col); nextSteps.sort(Comparator.comparingInt(a -> getNext(a[0], a[1]).size()));这个优化来自经典的骑士周游贪心规则:每一步都先尝试“下一步可选位置最少”的那个方向。直觉是,越早走死路越少的分支,越不容易浪费时间在死路里反复回溯。课程源码里如果已经写了这个优化,你会看到sort相关的代码;如果没有,建议你自己加上,这是整套资源里性价比最高的一处改动。
4.4 GreedyAlgorithm:集合覆盖的写法
贪心算法里的经典案例是集合覆盖,比如从多个集合里挑出几个,使它们的并集覆盖全部元素。代码核心是每轮选覆盖最多未覆盖元素的集合。
public static Set<String> greedyCover(Map<String, Set<String>> stations) { Set<String> allAreas = new HashSet<>(); for (Set<String> areas : stations.values()) { allAreas.addAll(areas); } Set<String> selected = new HashSet<>(); while (!allAreas.isEmpty()) { String best = null; int maxCover = 0; for (Map.Entry<String, Set<String>> entry : stations.entrySet()) { int cover = 0; for (String area : entry.getValue()) { if (allAreas.contains(area)) cover++; } if (cover > maxCover) { maxCover = cover; best = entry.getKey(); } } selected.add(best); allAreas.removeAll(stations.get(best)); } return selected; }每一轮都对所有候选集合计算“当前未覆盖区域里它能覆盖多少个”,选最多那个,然后把覆盖掉的区域从allAreas里移除。这个算法不保证全局最优,比如某些场景下多选一个集合反而更优,但在课程这个广播覆盖实例里已经足够。GreedyAlgorithm类名很直白,它就是贪心思想的代表。
5. 避坑与常见问题:跑这些源码最容易翻车的五个地方
这一章是我自己跑这套资料时踩过的坑,也顺手问了几个用过同一套资源的朋友,挑了五个最有共性的写出来。每条都按“现象→原因→解决”来。
5.1 启动就报 NoClassDefFoundError
现象:命令行执行java SingleLinkedListDemo直接抛NoClassDefFoundError,后面还跟着HeroNode。
原因:classpath 没有包含当前目录;也经常是因为资源包里的 class 文件带包名,而你没建对应目录结构。class 文件是编译产物,它内部会记录自己属于哪个包,如果你直接在当前目录运行,而包路径对不上,JVM 就找不到依赖类。
解决:先执行javap -p SingleLinkedListDemo.class,看类声明前面有没有package信息。如果有,命令改成java -cp . 包名.SingleLinkedListDemo。我一直习惯在所有 Java 命令后面加-cp .,避免默认类路径不一致。
5.2 逆波兰计算器算出负数时结果不对
现象:输入表达式-3 + 5,期望结果是 2,程序却按减号处理,算出3 - 5 = -2。
原因:把负号当成双目运算符压栈了。toSuffix里遇到-就直接当运算符处理,没有判断它到底是负号还是减号。
解决:在读 token 时增加判断:如果-出现在表达式开头、或者前一个有效字符是左括号或另一个运算符,那它就是负号,应该和后面的数字合并成一个负数 token。这个判断要放在多位数拼接逻辑之前,否则-3会被拆成减号和 3。
5.3 哈夫曼解压出来乱码
现象:HuffmanCode 压缩后再解压,文件内容出现一串替换字符。
原因:写入压缩数据时按字符串“0101”逐字符存储,而没有真正按 bit 位写入;解压时补位信息没记录,导致最后几位移位错乱。哈夫曼编码结果是变长的字符串,最后不是 8 的倍数时必须补位,如果补了多少位没写进文件,解压端根本不知道哪些是有效数据。
解决:压缩时先统计原始字节数,单独记录到文件头部;编码串最后不足 8 位补 0,并把补位数量写成一个整数。解压时先读头部,只处理原始字节数对应的 bit 数,超出部分全部丢弃。这个修正做掉之后,乱码问题基本就消失了。
5.4 马踏棋盘回溯很慢甚至像死循环
现象:HorseChessboard 默认起点跑 8×8 棋盘,超过 10 秒没结果。
原因:回溯方向固定且没有剪枝。如果getNext每次都按同一个固定顺序尝试,某些起点会走进大量死路分支,最坏情况接近指数级。
解决:对每个当前位置的下一步,先算出下一步候选位置的数量,按数量升序去尝试。这个规则源自骑士周游的经典贪心优化:先走选择最少的位置。我实测下来,加了这个sort之后,多数起点的运行时间从分钟级降到秒级以下。
5.5 克鲁斯卡尔算法选了环边
现象:最小生成树边数够了,但画出来有环。
原因:并查集里用了parent[start] != parent[end]判断,而不是find(start) != find(end)。路径压缩后parent数组值未必是根节点,直接比较会把两个本来连通的分支漏掉。
解决:每次判断前都调用find,用根节点比较。如果已经选完边还在生成环,建议在union里把rootX和rootY打印出来,顺着输出看是哪条边的合并造成了环,定位会快很多。
6. 进阶:把这套代码改造成自己的数据结构练习框架
资源跑通只是第一步,真正能留下东西的是把这些零散 class 整理成一份随时能自测的练习框架。
6.1 给每个算法补一个 main 入口
很多文件本身就是工具类,没有 main,跑起来全靠 IDE 临时运行。我建议给每个算法类补一个静态 main,输入用最小场景:链表插三个节点、表达式给一个固定字符串、棋盘用固定坐标。这样以后回归测试只需要一条命令。
public static void main(String[] args) { String expr = "1+((2+3)*4)-5"; List<String> suffix = ReversePolishMultiCalc.toSuffix(expr); System.out.println(suffix); System.out.println(ReversePolishMultiCalc.evaluate(suffix)); }main 里只放最小验证路径,不要读文件、不要接收参数,先保证算法本身正确。我习惯把这组 main 统一定义成smokeTest,每个类都能独立运行,哪里改坏了立刻能发现。
6.2 用日志把递归过程可视化
马踏棋盘、回溯这类递归,黑匣子式调试很难受。可以在backtrack开头加一行打印:
if (step <= 3 || step >= rows * cols - 3) { System.out.printf("step=%d at (%d,%d)%n", step, row, col); }只看头和尾,能看到回溯是否收缩回来。如果全量打印,8×8 棋盘会刷屏,所以加一个阈值控制输出量。这个技巧我在调克鲁斯卡尔和马踏棋盘时都用上了,效果很直接。
从那以后,我每次拿到这种“只有 class 和零散源码”的资料包,都强制自己先跑通一个最小 main,再改包名,最后补测试用例。数据结构这行最怕黑匣子,跑通一个例子比看十遍课件都管用。希望帮到你。
本文还有配套的精品资源,点击获取