“论文查重”个人项目说明
准备
| 所属课程 | 计科24级56班 |
|---|---|
| 作业要求 | “论文查重”个人项目 |
| 作业目标 | 完成项目设计,实现一个能够比较原始论文和疑似抄袭论文文本相似程度的程序,并按照题目规定的格式输出重复率;熟悉项目开发流程 |
github地址:Ajie8021

正文
一、PSP预估:
| PSP2.1(Personal Software Process Stages) | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| Planning/计划 | ||
| Estimate/估计这个任务需要多少时间 | 0.5h | 1h |
| Development/开发 | ||
| Analysis/需求分析(包括学习新技术) | 2.0h | 2.0h |
| Design Spec/生成设计文档 | 1.0h | 1.0h |
| Design Review/设计复审 | 0.5h | 0.5h |
| Coding Standard/代码规范(为目前的开发制定合适的规范) | 0.5h | 0.5h |
| Design/具体设计 | 1.0h | 1.0h |
| Coding/具体编码 | 5.0h | 4.0h |
| Code Review/代码复审 | 1.0h | 0.5h |
| Test/测试(自我测试,修改代码,提交修改) | 3.0h | 1.0h |
| Reporting报告 | ||
| Test Repor/测试报告 | 1.0h | 0.5h |
| Size Measurement/计算工作量 | 0.5h | 10min |
| Postmortem & Process Improvement Plan/事后总结, 并提出过程改进计划 | 1.5h | 0.5h |
| 合计 | 17.5h | 12h |
二、计算模块分析
1. 计算模块接口的设计与实现过程
(1)模块职责
论文查重系统的计算模块负责根据经过预处理的原文和待查重文本计算文本相似度,是整个系统的核心业务模块。
根据前期需求分析与详细设计,计算模块不负责:
- 文件读取;
- 文件写入;
- 命令行参数处理;
- HTML 文件读取;
- 最终结果格式化。
这些功能分别由文件服务、文本预处理、程序入口和结果格式化模块负责。而计算模块只关注一个核心问题:
给定原文和待查重文本,计算二者的相似度。
这种职责划分可以降低模块之间的耦合,使相似度算法能够独立开发、测试和替换。
(2)计算模块接口设计
根据详细设计,计算模块首先定义统一的接口:
public interface SimilarityCalculator {
double calculate(String originalText, String plagiarismText);
}
接口包含一个核心方法:
double calculate(String originalText, String plagiarismText);
参数含义如下:
| 参数 | 类型 | 含义 |
|---|---|---|
originalText |
String |
原始文本 |
plagiarismText |
String |
待查重文本 |
| 返回值 | double |
两个文本的相似度 |
接口设计采用面向接口编程的方式,使上层程序不需要知道具体采用哪一种相似度算法。
程序调用关系为:
PlagiarismApplication
↓
SimilarityCalculator
↓
LcsSimilarityCalculator
因此,如果后续验证发现 LCS 算法不能完全满足题目要求,可以在不修改上层业务流程的情况下替换计算实现。
例如未来可以增加:
SimilarityCalculator
├── LcsSimilarityCalculator
├── AnotherSimilarityCalculator
└── ...
而 PlagiarismApplication 不需要因此重新设计。
(3)为什么采用接口
直接在主程序中编写 LCS 算法虽然实现简单,但会产生较强的耦合:
Main
└── 直接调用 LCS
如果后续修改算法,就需要修改主程序。
当前设计改为:
Main
↓
PlagiarismApplication
↓
SimilarityCalculator
↓
LcsSimilarityCalculator
这样做有三个主要好处:
① 降低耦合:
上层业务只依赖接口,不依赖具体算法。
② 便于单元测试:
可以单独测试 LcsSimilarityCalculator,不需要通过命令行和文件系统才能验证算法。
③ 便于算法替换:
如果官方样例验证发现当前算法需要调整,可以增加新的实现,而不需要修改整个程序结构。
(4)当前接口的具体实现
当前版本使用:
public class LcsSimilarityCalculator
implements SimilarityCalculator
作为接口的实现类,其核心计算方法为:
@Override
public double calculate(String originalText, String plagiarismText) {
...
}
即实现采用最长公共子序列(LCS,Longest Common Subsequence)计算两个文本之间的公共字符序列长度。
我在这里设:
originalText 长度 = N
plagiarismText 长度 = M
LCS 长度 = L
相似度计算方式为:
similarity = L / N
即以原文长度作为分母。
(5)LCS 的基本计算过程
LCS 使用动态规划求解。
设dp[i][j]表示原文前 i 个字符与待查重文本前 j 个字符的最长公共子序列长度。
当original[i - 1] == plagiarism[j - 1]时:
dp[i][j] = dp[i - 1][j - 1] + 1
否则:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
最终:
L = dp[N][M]
再根据当前公式计算相似度。
2. 计算模块接口部分的性能改进
(1)初始设计的性能问题
最直接的 LCS 实现可以使用二维数组:
int[][] dp = new int[n + 1][m + 1];
这种实现的时间复杂度为O(N × M),空间复杂度也是O(N × M)。
这对于普通短文本而言没有明显问题,但论文文本可能达到较大的字符数量,此时二维 DP 会产生数量非常大的状态空间,内存开销可能成为明显问题。因此,在详细设计阶段已经将 LCS 的空间占用作为性能风险考虑。
(2)当前实现:滚动数组优化
当前 LcsSimilarityCalculator 没有保存完整的二维 DP 表,而是只保留相邻的两行。这里的核心思想为:
上一行
↓
当前行
↓
计算完成后交换
即:
previous[j]
current[j]
而不是用dp[i][j]来完整保存所有行,因此空间复杂度由O(N × M)降低为O(M)
同时程序会根据两个字符串长度选择较短的文本作为 DP 的列方向,因此实际空间复杂度可以进一步表示为O(min(N, M)),不过时间复杂度仍然为O(N × M)。也就是说,目前的优化主要针对空间复杂度,并没有改变 LCS 的渐进时间复杂度。
(3)优化前后对比
| 项目 | 二维 DP | 当前滚动数组 |
|---|---|---|
| LCS 状态保存 | 全部保存 | 只保存相邻两行 |
| 时间复杂度 | O(NM) |
O(NM) |
| 空间复杂度 | O(NM) |
O(min(N,M)) |
| 实现复杂度 | 较低 | 略高 |
| 大文本内存风险 | 较高 | 较低 |
| 当前是否采用 | 否 | 是 |
因此,当前实现已经进行了一个明确的空间性能改进。
(4)为什么暂时没有继续优化时间复杂度
目前不能仅凭理论复杂度直接确定 LCS 是最终最优方案,原因是当前项目还有一个更加重要的问题,即:
当前算法是否符合题目的实际重复率计算要求尚未通过官方样例完全验证。
如果官方样例验证发现LCS / 原文长度本身不能得到正确结果,那么继续对当前 LCS 实现进行大量性能优化没有意义。
因此目前采用:
先验证正确性
↓
再进行性能分析
↓
最后针对热点优化
而不是:
先大量优化
↓
再发现算法本身不符合要求
这也是本项目过程改进计划中的重要原则。
(5)后续 Profiler 性能分析计划
根据题目要求,后续有时间的话,会使用 Profiler 对程序进行性能分析。
我的计划流程为:
建立性能测试数据
↓
运行完整查重程序
↓
Profiler 采集运行数据
↓
寻找 CPU / 内存热点
↓
确认热点函数
↓
针对热点进行优化
↓
重新运行测试
↓
比较优化前后结果
重点观察:
LcsSimilarityCalculator.calculate();- LCS 动态规划循环;
TextPreprocessor;- 大文件读取;
- 字符串对象创建;
- 内存峰值。
最终报告中会记录下优化前后的执行时间、内存占用、热点函数,但目前尚未进行这一步,因此本博客不虚构 Profiler 数据。
3. 计算模块部分单元测试展示
(1)测试类
计算模块对应的单元测试类为:
src/test/java/com/xxx/plagiarism/
└── similarity/
└── LcsSimilarityCalculatorTest.java
该测试类目前包含 10 个测试用例。
执行整个项目的测试命令:
mvn test
本次实际测试结果为:
Running com.xxx.plagiarism.similarity.LcsSimilarityCalculatorTest
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
即:
| 指标 | 结果 |
|---|---|
| 测试用例 | 10 |
| 通过 | 10 |
| 失败 | 0 |
| 错误 | 0 |
| 跳过 | 0 |
| 通过率 | 100% |
(2)测试内容
当前计算模块测试覆盖了基本相似度计算和边界情况,包括:
- 两个完全相同的文本;
- 两个完全不同的文本;
- 部分字符相同;
- 空字符串;
- 原文为空;
- 待查重文本为空;
- 单字符文本;
- 文本长度不同;
- LCS 基本计算结果;
- 多种输入组合下的相似度计算。
这些测试用于验证计算模块在正常输入和边界输入下能够按照当前设计工作。
(3)单元测试与其他模块的区别
计算模块测试不需要启动完整程序,测试可以直接创建:
LcsSimilarityCalculator calculator =
new LcsSimilarityCalculator();
然后调用:
double result =
calculator.calculate(original, plagiarism);
再使用 JUnit 对结果进行断言。
这种测试方式避免了:
命令行
↓
文件系统
↓
文本预处理
↓
计算模块
↓
输出文件
等其他因素对计算模块测试的干扰,因此,一旦测试失败,可以较准确地定位到相似度计算逻辑。
(4)测试结果与当前项目整体测试结果
当前项目一共执行了:23 tests
其中计算模块:10 tests
最终 Maven 测试结果:
Tests run: 23, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
因此当前计算模块的单元测试全部通过。
但是需要明确:
单元测试通过只能证明当前实现满足已经编写的测试预期,不能证明当前 LCS 算法已经满足题目的全部作业样例要求。
后续仍然需要进行样例验证。
4. 计算模块部分异常处理说明
(1)参数异常
计算模块不负责命令行参数解析,命令行参数由 Main 负责检查。
例如:
参数数量不是 3 个
参数为空
属于程序入口层的输入异常,而不是相似度计算模块的业务异常。
这种职责划分可以避免计算模块承担不属于自身的功能。
(2)null 参数处理
计算模块的 calculate() 方法要求输入有效的文本对象,当前实现对 null 参数进行检查。
如果调用:
calculator.calculate(null, "abc");
或者:
calculator.calculate("abc", null);
则属于非法方法调用,当前实现通过:
IllegalArgumentException
进行处理。
这样可以避免在 LCS 循环内部因为空引用产生难以定位的异常。
(3)空字符串处理
空字符串属于合法的边界输入,需要和 null 区分。
当前设计中:
original = ""
plagiarism = ""
两个文本都为空时,返回1.0,表示二者没有内容差异。而:
original = ""
plagiarism = "abc"
则返回0.0,避免出现0 / 0等非法计算。
(4)文件异常与计算模块的边界
如果原文文件不存在FileReadException,由 FileService 负责处理。
如果输出文件无法写入FileWriteException,由 FileService 负责处理。
计算模块只接收已经读取成功的字符串,因此不存在直接访问文件系统的问题。
整体异常处理流程为:
文件路径错误
↓
FileService
↓
FileReadException
↓
Application / Main
↓
输出错误信息并结束
而计算模块自身的异常流程为:
非法 null 参数
↓
IllegalArgumentException
这种异常职责划分使不同类型的问题能够在不同层次得到处理。
5. 计算模块设计总结
当前计算模块已经形成:
当前实现具有以下特点:
- 通过接口隔离具体算法;
- LCS 算法独立于文件和命令行处理;
- 使用滚动数组降低空间复杂度;
- 对
null和空字符串等边界情况进行了处理; - 具有独立的 10 个单元测试;
- 当前 10 个计算模块测试全部通过;
- 可以在不修改上层业务代码的情况下替换具体算法。
当前性能方面已经完成:
二维 DP
O(NM) 空间
↓
滚动数组
O(min(N,M)) 空间
但时间性能尚未通过 Profiler 进行实际测量,因此后续仍需要进行性能基准测试和 Profiler 分析。
因此下一阶段应优先完成:
其他样例验证
↓
确认计算公式及预处理规则
↓
补充回归测试
↓
JaCoCo 覆盖率
↓
Profiler 性能分析
↓
针对热点进行性能优化
这样才能最终确认计算模块在正确性、性能、可测试性和异常处理四个方面均满足项目要求。
三、结果展示
| 样例名 | 重合率 |
|---|---|
| orig_0.8_add.txt | 100% |
| orig_0.8_del.txt | 82% |
| orig_0.8_dis_1.txt | 97% |
| orig_0.8_dis_10.txt | 84% |
| orig_0.8_dis_15.txt | 69% |

浙公网安备 33010602011771号