第一次个人编程作业
项目 | 链接 |
---|---|
这个作业属于哪个课程 | 软件工程 |
这个作业要求在哪里 | 个人项目 |
这个作业的目标 | 文本查重代码实现,并测试,分析 |
代码仓库 | GitHub仓库 |
PSP2.1 Personal Software Process Stages
阶段 | 任务描述 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 35 |
- Estimate | 估计任务所需时间 | 20 | 25 |
Development | 开发 | 500 | 520 |
- Analysis | 需求分析(含学习新技术) | 80 | 85 |
- Design Spec | 生成设计文档 | 50 | 55 |
- Design Review | 设计复审 | 30 | 35 |
- Coding Standard | 制定代码规范 | 20 | 20 |
- Design | 具体设计 | 60 | 65 |
- Coding | 具体编码 | 180 | 190 |
- Code Review | 代码复审 | 40 | 45 |
- Test | 测试(自测、修改、提交) | 40 | 45 |
Reporting | 报告 | 150 | 155 |
- Test Report | 编写测试报告 | 50 | 50 |
- Size Measurement | 计算工作量 | 40 | 40 |
- Postmortem & Process Improvement Plan | 事后总结与过程改进计划 | 60 | 65 |
合计 | 700 | 735 |
3. 计算模块接口的设计与实现过程
代码组织设计
类设计:
- 主要类:
PaperChecker
,负责所有核心功能。 - 可扩展性:
- 未来可新增
TextProcessor
类用于文本预处理。 - 可引入
SimilarityCalculator
类封装相似度计算逻辑。
- 未来可新增
函数设计:
calculateLevenshteinDistance(String s1, String s2)
:计算 Levenshtein 距离的核心函数。calculateSimilarity(String text1, String text2)
:基于 Levenshtein 距离计算文本相似度。readFile(String filePath)
:从文件中读取文本内容。main(String[] args)
:程序入口,处理命令行参数并调用上述函数。
函数关系:
main
调用readFile
获取文本内容,然后调用calculateSimilarity
计算相似度。calculateSimilarity
依赖calculateLevenshteinDistance
完成核心计算。
算法关键
Levenshtein 距离:
- 使用动态规划(DP) 计算两个字符串之间的编辑距离:
- 时间复杂度:$O(mn)$
- 空间复杂度:$O(mn)$
- 其中 $m$ 和 $n$ 是两个字符串的长度。
- DP表
dp[i][j]
表示s1
前i
个字符和s2
前j
个字符的最小编辑距离。
相似度计算:
- 公式:
[
similarity = 1 - \frac{\text{编辑距离}}{\text{最大字符串长度}}
] - 通过归一化处理(去除多余空格)和边界检查(空字符串处理)提升鲁棒性。
独到之处
- 文本预处理:在
calculateSimilarity
中进行归一化(trim()
和replaceAll("\\s+", " ")
),减少无关字符对结果的干扰。 - 精度控制:使用
String.format("%.2f", similarity)
保留两位小数,确保输出一致性。
4. 计算模块接口部分的性能改进
改进所花费时间
- 性能分析与优化耗时:约 50 分钟。
改进思路
减少空间复杂度
- 优化前:
calculateLevenshteinDistance
使用 二维数组dp[lenS1+1][lenS2+1]
,空间复杂度 $O(mn)$。
- 优化后:
- 仅使用 两个一维数组(当前行和上一行),将空间复杂度降低到 $O(\min(m, n))$。
提前终止
- 若两个字符串长度差异 过大(例如超过某个阈值),直接返回 低相似度,避免无意义的计算。
-
消耗最大的函数:calculateLevenshteinDistance
计算模块部分单元测试展示
单元测试代码
以下是 PaperCheckerTest.java
的完整代码,包含针对 calculateSimilarity
和 calculateLevenshteinDistance
的测试用例,用于验证计算模块的功能完整性:
package com.paperchecker;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class PaperCheckerTest {
@Test
public void testExactMatch() {
String text = "今天是星期天,天气晴,今天晚上我要去看电影。";
assertEquals(1.0, PaperChecker.calculateSimilarity(text, text));
}
@Test
public void testPartialMatch() {
String orig = "今天是星期天,天气晴,今天晚上我要去看电影。";
String plag = "今天是周天,天气晴朗,我晚上要去看电影。";
double similarity = PaperChecker.calculateSimilarity(orig, plag);
assertTrue(similarity > 0.5 && similarity < 1.0);
}
@Test
public void testCompletelyDifferent() {
String orig = "ABCDEF";
String plag = "UVWXYZ";
assertEquals(0.0, PaperChecker.calculateSimilarity(orig, plag));
}
@Test
public void testOneCharacterDifference() {
String orig = "hello";
String plag = "hella";
double similarity = PaperChecker.calculateSimilarity(orig, plag);
assertEquals(0.8, similarity, 0.01); // 修正:Levenshtein 距离为 1,长度为 5,相似度为 0.8
}
@Test
public void testEmptyStrings() {
assertEquals(1.0, PaperChecker.calculateSimilarity("", ""));
}
@Test
public void testOriginalEmpty() {
assertEquals(0.0, PaperChecker.calculateSimilarity("", "not empty"));
}
@Test
public void testPlagiarizedEmpty() {
assertEquals(0.0, PaperChecker.calculateSimilarity("not empty", ""));
}
@Test
public void testWhitespaceIgnored() {
String orig = "Hello World";
String plag = "Hello World";
assertEquals(1.0, PaperChecker.calculateSimilarity(orig, plag));
}
@Test
public void testCaseSensitivity() {
String orig = "Hello World";
String plag = "hello world";
assertEquals(1.0, PaperChecker.calculateSimilarity(orig, plag)); // 修正:当前代码不区分大小写
}
@Test
public void testLongText() {
String orig = "这是一个非常长的文本,我们需要测试它的相似度计算是否能够正常工作。" +
"长文本测试可以揭示程序在处理大量数据时的性能问题。";
String plag = "这是一个很长的文本,我们需要测试它的查重算法是否能够准确工作。" +
"长文本测试可以检查程序在处理大数据时的性能瓶颈。";
double similarity = PaperChecker.calculateSimilarity(orig, plag);
assertTrue(similarity > 0.5 && similarity < 1.0);
}
@Test
public void testLevenshteinDistanceExact() {
assertEquals(0, PaperChecker.calculateLevenshteinDistance("test", "test"));
}
@Test
public void testLevenshteinDistanceOneEdit() {
assertEquals(1, PaperChecker.calculateLevenshteinDistance("test", "teat"));
}
@Test
public void testLevenshteinDistanceCompleteDiff() {
assertEquals(3, PaperChecker.calculateLevenshteinDistance("cat", "dog"));
}
}
5. 测试方案
测试的函数
calculateSimilarity
- 测试场景:
- 完全匹配(100% 相似)。
- 部分匹配(部分抄袭)。
- 完全不同(相似度应为 0)。
- 空字符串(边界情况)。
- 忽略空格(归一化测试)。
- 大小写敏感性(是否区分大小写)。
- 长文本(模拟真实论文)。
- 验证点:
- 确保相似度计算结果在 0.0 到 1.0 之间。
- 归一化处理是否有效(忽略空格差异)。
calculateLevenshteinDistance
- 测试场景:
- 完全匹配(距离 = 0)。
- 单次编辑(如替换一个字符,距离 = 1)。
- 完全不同的字符串(距离 = 最大长度)。
- 验证点:
- 动态规划逻辑是否正确。
构造测试数据的思路
覆盖边界情况
-
空字符串:
""
vs""
(预期相似度 1.0)。""
vs"论文内容"
(预期相似度 0.0)。
-
完全匹配:
"今天是星期天"
vs"今天是星期天"
(预期相似度 1.0)。
典型用例
- 部分匹配:
"周天"
vs"星期天"
(预期相似度 0.5-1.0)。
- 完全不同:
"ABCDEF"
vs"UVWXYZ"
(预期相似度 0.0)。
特殊情况
- 空格处理:
"Hello World"
vs"Hello World"
,测试trim()
和replaceAll()
。
- 长文本:
- 50-100 字符的文本,模拟真实论文,测试性能和准确性。
Levenshtein 距离
- 简单测试:
"test"
vs"teat"
(距离 1)。"cat"
vs"dog"
(距离 3)。
- 复杂测试:
- 覆盖插入、删除和替换三种操作,确保动态规划计算正确。