第一次个人编程作业
作业GitHub仓库链接:https://github.com/LiuBoo0785/3124004175
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率。 |
一、PSP 表
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 15 | 18 |
| · Estimate | · 估计这个任务需要多少时间 | 15 | 18 |
| Development | 开发 | 180 | 220 |
| · Analysis | · 需求分析(包括学习新技术) | 30 | 35 |
| · Design Spec | · 生成设计文档 | 15 | 18 |
| · Design Review | · 设计复审 | 10 | 12 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 10 | 12 |
| · Design | · 具体设计 | 20 | 25 |
| · Coding | · 具体编码 | 60 | 75 |
| · Code Review | · 代码复审 | 15 | 18 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 20 | 25 |
| Reporting | 报告 | 60 | 76 |
| · Test Report | · 测试报告 | 20 | 25 |
| · Size Measurement | · 计算工作量 | 15 | 18 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 25 | 33 |
| 合计 | 255 | 314 |
二、计算模块接口的设计与实现过程
2.1 代码组织结构
项目采用 Maven 标准目录结构,共设计 3 个核心类、1 个测试类,遵循单一职责原则:
3124004175/
├── src/main/java/papercheck/
│ ├── Main.java # 主入口:命令行参数解析与流程编排
│ ├── TextSimilarity.java # 核心算法:预处理 + LCS + Dice 系数
│ └── FileHandler.java # 文件 IO:UTF-8 读写、结果格式化
├── src/test/java/papercheck/
│ └── TextSimilarityTest.java # 14 个单元测试
└── pom.xml # Maven 构建配置
2.2 类与函数关系图
┌────────────────────────────────────────────────────┐
│ Main │
│ - main(String[] args) │
│ ├─→ FileHandler.readFile(path) │
│ └─→ TextSimilarity.calculate(orig, copy) │
│ └─→ FileHandler.writeFile(path, similarity) │
└────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────────┐
│ FileHandler │ │ TextSimilarity │
│ + readFile(path) │ │ + calculate(s1, s2) │
│ + writeFile(p, sim) │ │ + preprocess(text) │
└──────────────────────┘ │ - lcsLength(s1, s2) │
└─────────────────────────┘
- Main:仅负责命令行参数校验和流程串联,不含业务逻辑
- TextSimilarity:纯算法类,所有方法静态化,无状态依赖
- FileHandler:仅处理文件读写和编码转换,不参与计算
2.3 关键算法流程图
┌──────────────┐
│ 读取原文文件 │
└──────┬───────┘
▼
┌──────────────┐
│ 读取抄袭文件 │
▼
┌──────────────┐
│ preprocess │ ← 过滤非中文字符
│ (两端各自) │ (剥离 HTML 标签、标点、空白)
▼
┌──────────────┐
│ LCS 动态规划 │ ← 滚动数组求最长公共子序列
│ (O(n×m)) │
▼
┌──────────────┐
│ Dice 系数 │ ← 2 × LCS / (len1 + len2)
│ 计算相似度 │
▼
┌──────────────┐
│ 格式化两位小数│
│ 写入答案文件 │
└──────────────┘
2.4 算法关键与独到之处
(1)预处理策略:中文字符过滤
public static String preprocess(String text) {
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c >= '\u4e00' && c <= '\u9fa5') {
sb.append(c);
}
}
return sb.toString();
}
独到之处:通过 Unicode 范围 [\u4e00-\u9fa5] 只保留 CJK 中日韩统一表意文字。这一步一举两得:
- 自动剥离老师测试文件中的 HTML 网页标签(如
<td class="blob-code">) - 过滤标点、空白、英文、数字,让纯文本和网页版测试文件统一处理
(2)LCS 滚动数组优化
标准 LCS 需要 O(n×m) 的二维数组,对于 10000 字符文本需要约 400MB 内存,可能超过 2048MB 限制。改用两行一维数组交替覆盖:
int[] prev = new int[n + 1];
int[] curr = new int[n + 1];
for (int j = 1; j <= m; j++) {
for (int i = 1; i <= n; i++) {
if (arr1[i-1] == cj) {
curr[i] = prev[i-1] + 1;
} else {
curr[i] = prev[i] >= curr[i-1] ? prev[i] : curr[i-1];
}
}
int[] tmp = prev; prev = curr; curr = tmp;
}
独到之处:空间复杂度从 O(n×m) 降至 O(min(n,m)),10000 字符文本仅需约 40KB。
(3)Dice 系数相似度公式
return 2.0 * lcsLength / (n + m);
相比 LCS/max(n,m),Dice 系数对长度差异更对称、更鲁棒,是文本相似度的标准度量方式。
三、计算模块接口部分的性能改进
3.1 改进思路
在开发首个版本后,我使用 JProfiler / VisualVM 性能分析工具对程序进行 CPU 和内存采样,找出性能瓶颈:
| 改进点 | 改进前 | 改进后 | 效果 |
|---|---|---|---|
| 空间复杂度 | 二维数组 O(n×m) ≈ 400MB | 滚动数组 O(min(n,m)) ≈ 40KB | 内存降低 99% |
| 字符访问 | String.charAt() 重复方法调用 |
char[] 数组直接下标访问 |
减少 JNI 调用开销 |
| 数组长度 | 默认按原文长度 | 交换使 s1 为较短串 | 进一步省空间 |
| 有效文本 | 含标点空白 | 预处理只保留中文 | 减少 n 和 m |
3.2 性能分析图(VisualVM CPU 采样)
┌─────────────────────────────────────────────────────┐
│ CPU 占比 Top 3 方法 │
├─────────────────────────────────────────────────────┤
│ TextSimilarity.calculate() ███████████ 95% │
│ TextSimilarity.preprocess() █ 3% │
│ FileHandler.readFile() █ 2% │
└─────────────────────────────────────────────────────┘
3.3 程序中消耗最大的函数
TextSimilarity.calculate() 是 CPU 消耗最大的函数,占比约 95%。
该函数内的双层嵌套循环是主要消耗点:
for (int j = 1; j <= m; j++) { // 外层:抄袭版长度
for (int i = 1; i <= n; i++) { // 内层:原文长度
// LCS 状态转移
}
}
时间复杂度 O(n×m),对于 10000 字符文本约 1 亿次迭代,实测耗时约 169ms,远低于 5 秒限制。
3.4 改进耗时记录
性能改进共花费约 25 分钟,主要包括:
- 编写 10000 字符长文本测试用例验证性能
- 用 VisualVM 采样定位热点函数
- 实施滚动数组优化
- 用
char[]替代charAt()
四、计算模块部分单元测试展示
4.1 单元测试代码展示
本项目共设计 14 个单元测试用例,覆盖完全相同、完全不同、增删改、空文本、HTML 过滤、性能等场景。以下展示部分核心测试代码:
测试 1:完全相同的文本相似度应为 1.0
@Test
@DisplayName("完全相同的文本相似度应为1.0")
void testIdenticalText() {
String text = "今天是星期天天气晴今天晚上我要去看电影";
assertEquals(1.0, TextSimilarity.calculate(text, text), 0.001);
}
构造思路:验证算法在两个文本完全相同时的正确性。LCS 应等于原文长度,Dice = 2N/(N+N) = 1.0
测试 2:抄袭版仅增字
@Test
@DisplayName("抄袭版仅增字:原文'你好世界'→抄袭版'你好美丽世界'")
void testOnlyAdd() {
double result = TextSimilarity.calculate("你好世界", "你好美丽世界");
assertEquals(0.8, result, 0.001);
}
构造思路:原文 4 字,抄袭版 6 字,LCS = 4(你好世界),Dice = 2×4/(4+6) = 0.8
测试 3:HTML 标签过滤测试
@Test
@DisplayName("HTML标签应被预处理过滤掉,仅保留中文")
void testHtmlTagFiltered() {
String orig = "一位真正的作家永远只为内心写作";
String copy = "<td class=\"blob-code\">一位真正的作家永远只为内心写作</td>";
assertEquals(1.0, TextSimilarity.calculate(orig, copy), 0.001);
}
构造思路:老师提供的 del/dis 测试文件是 GitHub 网页 HTML 格式,预处理后应只保留中文,与纯文本原文完全匹配,相似度应为 1.0
测试 4:长文本性能测试
@Test
@DisplayName("10000字符长文本应在5秒内完成计算")
void testLongTextPerformance() {
StringBuilder sb = new StringBuilder(10000);
String base = "一位真正的作家永远只为内心写作";
for (int i = 0; i < 720; i++) sb.append(base);
String orig = sb.toString();
String copy = sb.insert(5000, "随机干扰文字测试用").toString();
long start = System.currentTimeMillis();
double result = TextSimilarity.calculate(orig, copy);
long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed < 5000, "计算耗时 " + elapsed + "ms 超过5秒限制");
assertTrue(result > 0.9, "相似度应大于0.9,实际: " + result);
}
构造思路:构造 10080 字符长文本,验证程序在 5 秒内完成计算,满足作业性能要求
4.2 全部测试用例清单
| 编号 | 测试场景 | 预期结果 | 实际结果 |
|---|---|---|---|
| 1 | 完全相同的文本 | 1.0 | ✅ 通过 |
| 2 | 完全不同的文本 | 0.0 | ✅ 通过 |
| 3 | 抄袭版仅增字 | 0.8 | ✅ 通过 |
| 4 | 抄袭版仅删字 | 0.8 | ✅ 通过 |
| 5 | 抄袭版仅改字 | 0.5 | ✅ 通过 |
| 6 | 空文本处理 | 0.0 | ✅ 通过 |
| 7 | 标点空格过滤 | 1.0 | ✅ 通过 |
| 8 | HTML 标签过滤 | 1.0 | ✅ 通过 |
| 9 | 中英文混合 | 0.0 | ✅ 通过 |
| 10 | 长文本性能(<5s) | >0.9 | ✅ 通过 |
| 11 | 单字符边界 | 1.0/0.0 | ✅ 通过 |
| 12 | 对称性测试 | 相等 | ✅ 通过 |
| 13 | 预处理独立测试 | 正确 | ✅ 通过 |
| 14 | null 输入安全 | 0.0 | ✅ 通过 |
测试结果:14 个测试用例全部通过,Tests run: 14, Failures: 0, Errors: 0
4.3 测试覆盖率截图说明
使用 JaCoCo 插件生成测试覆盖率报告,运行 mvn test 后在 target/site/jacoco/index.html 查看:


五、计算模块部分异常处理说明
5.1 异常设计目标
本程序设计了 3 类核心异常,覆盖文件操作和参数校验场景:
| 异常类型 | 触发场景 | 设计目标 | 处理方式 |
|---|---|---|---|
IOException |
文件不存在/无权限 | 防止程序因文件异常崩溃 | 捕获并输出错误信息,退出码 1 |
IllegalArgumentException |
命令行参数数量不正确 | 提示用户正确用法 | 输出 Usage,退出码 1 |
NullPointerException |
输入文本为 null | 防御性编程 | 返回相似度 0.0 |
5.2 异常单元测试样例
异常 1:命令行参数数量异常
场景:用户未提供足够的命令行参数
// Main.java 中的处理逻辑
if (args.length != 3) {
System.err.println("Usage: java -jar main.jar [原文路径] [抄袭版路径] [输出路径]");
System.exit(1);
}
测试场景:当 args 为空数组或长度不为 3 时,应输出 Usage 并以非零退出码退出
异常 2:文件不存在异常
场景:用户提供的文件路径不存在
// Main.java 中的处理逻辑
try {
String origText = FileHandler.readFile(origPath);
// ...
} catch (IOException e) {
System.err.println("文件处理错误: " + e.getMessage());
System.exit(1);
}
对应单元测试:调用 FileHandler.readFile("不存在的路径.txt") 应抛出 IOException,Main 捕获后输出错误信息并以退出码 1 退出,不会异常崩溃
异常 3:null 输入异常
场景:算法输入文本为 null
@Test
@DisplayName("null 输入应安全返回0.0")
void testNullInput() {
assertEquals(0.0, TextSimilarity.calculate(null, "你好"), 0.001);
assertEquals(0.0, TextSimilarity.calculate("你好", null), 0.001);
}
设计目标:算法内部对 null 做防御性检查,避免调用 preprocess(null) 时产生 NullPointerException。当任一输入为 null 时,直接返回相似度 0.0,保证程序健壮性
5.3 异常处理流程
┌───────────────────────┐
│ args.length != 3 ? │
└─────────┬─────────────┘
yes ↓ no
┌───────────┐ │
│ 输出 Usage │ │
│ exit(1) │ │
└───────────┘ ▼
┌──────────────┐
│ 读取文件 │
└──────┬───────┘
IOException?
yes ↓ no ↓
┌────────────┐ │
│ 输出错误 │ │
│ exit(1) │ │
└────────────┘ ▼
┌──────────────┐
│ null 检查 │
│ → 返回 0.0 │
└──────┬───────┘
▼
┌──────────────┐
│ 计算并写入 │
└──────────────┘
六、PSP 表(实际耗时)
| PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
|---|---|---|
| Planning | 计划 | 18 |
| · Estimate | · 估计这个任务需要多少时间 | 18 |
| Development | 开发 | 220 |
| · Analysis | · 需求分析(包括学习新技术) | 35 |
| · Design Spec | · 生成设计文档 | 18 |
| · Design Review | · 设计复审 | 12 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 12 |
| · Design | · 具体设计 | 25 |
| · Coding | · 具体编码 | 75 |
| · Code Review | · 代码复审 | 18 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 25 |
| Reporting | 报告 | 76 |
| · Test Report | · 测试报告 | 25 |
| · Size Measurement | · 计算工作量 | 18 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 33 |
| 合计 | 314 |
实际耗时与预估对比分析
- 总耗时:预估 255 分钟,实际 314 分钟,超出 23%
- 超时原因:
- 长文本测试用例的
StringBuilder.insert()越界问题调试耗时超出预期 - Maven 环境配置(JAVA_HOME 指向 JDK 8 导致编译失败)耗时
- Checkstyle/PMD 插件配置学习成本
- 长文本测试用例的
- 节省时间项:
- 预处理策略一次设计正确,未返工
- LCS 算法实现一次通过,无逻辑错误
过程改进计划
- 开发前先确认构建环境(JDK 版本、Maven 配置),避免环境问题
- 长文本测试用例应先用小规模数据验证边界
- 性能测试应提前规划,留足调优时间
七、附录
7.1 GitHub 仓库结构
3124004175/
├── src/
│ ├── main/java/papercheck/
│ │ ├── Main.java
│ │ ├── TextSimilarity.java
│ │ └── FileHandler.java
│ └── test/java/papercheck/
│ └── TextSimilarityTest.java
├── lib/ # JUnit5 依赖
├── pom.xml # Maven 配置(JaCoCo+Checkstyle+PMD)
├── README.md
├── PSP表格.md
├── 性能分析报告.md
├── main.jar # 可执行 jar 包
└── .gitignore
7.2 程序使用方法
java -jar main.jar [原文文件绝对路径] [抄袭版文件绝对路径] [答案文件绝对路径]
示例:
java -jar main.jar C:\\tests\\orig.txt C:\\tests\\orig_add.txt C:\\tests\\ans.txt
7.3 Commit 签入记录
按功能分阶段提交,符合作业「代码有进展即签入 Github」要求:
chore: 初始化项目结构与PSP表格feat: 实现LCS查重算法核心(TextSimilarity/FileHandler/Main)test: 新增14个JUnit5单元测试用例build: 添加main.jar与JUnit5依赖chore: 集成JaCoCo/Checkstyle/PMD插件,补充PSP表格和性能分析报告
7.4 测试结果文件
老师提供的测试文件运行结果:
| 测试文件 | 修改类型 | 输出结果 | 耗时 |
|---|---|---|---|
| orig_0.8_add | 增字 | 0.91 | <1s |
| orig_0.8_del | 删字 | 0.89 | <1s |
| orig_0.8_dis_1 | 轻度打乱 | 0.97 | <1s |
| orig_0.8_dis_10 | 中度打乱 | 0.84 | <1s |
| orig_0.8_dis_15 | 重度打乱 | 0.67 | <1s |
所有测试均在 5 秒内完成,内存占用远低于 2048MB 限制。

浙公网安备 33010602011771号