第一次个人编程作业
GitHub链接:https://github.com/Scaler1024/3123004364/tree/master
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13468 |
这个作业的目标 | 实现一个论文查重程序,熟悉Github进行源代码管理以及学习软件测试 |
一、PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 15 | 20 |
Development | 开发 | 120 | 100 |
· Analysis | · 需求分析 (包括学习新技术) | 40 | 70 |
· Design Spec | · 生成设计文档 | 25 | 13 |
· Design Review | · 设计复审 | 15 | 15 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 14 | 11 |
· Design | · 具体设计 | 20 | 30 |
· Coding | · 具体编码 | 180 | 153 |
· Code Review | · 代码复审 | 20 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 183 |
Reporting | 报告 | 50 | 55 |
· Test Report | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 23 |
二、接口设计与实现过程
1.系统架构
- 主程序模块:程序入口,输入原文文件路径、抄袭文件路径、结果文件路径
- 文件处理模块:文件读写readFile(),writeFile()
- 文本处理模块:核心算法实现
- calculateEditDistanceSimilarity():计算基于编辑距离的相似度
- computeEditDistance():计算Levenshtein编辑距离
2.核心算法
*矩阵构建
int[][] dp = new int[m + 1][n + 1];
// 初始化:空字符串到任意字符串的转换代价
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
- 状态扭转
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // 字符相同,无需操作
} else {
dp[i][j] = Math.min(Math.min(
dp[i - 1][j] + 1, // 删除操作
dp[i][j - 1] + 1), // 插入操作
dp[i - 1][j - 1] + 1 // 替换操作
);
}
}
}
- 相似度归一化
return 1.0 - (double) editDistance / maxLength;
算法亮点:
- 最优子结构设计:复杂问题---->重叠子问题
- 时空复杂度平衡
- 多操作类型支持
- 边界条件智能处理
空间优化潜力
// 可优化为只使用两行数组
int[] prev = new int[n + 1];
int[] curr = new int[n + 1];
// 空间复杂度从O(m×n)降为O(n)
三、计算模块接口部分的性能改进
1.性能优化时间记录
优化项 | 分析瓶颈耗时(分钟) | 实施优化耗时(分钟) | 验证效果耗时(分钟) |
---|---|---|---|
内存管理与I/O模型重构 | 13 | 28 | 9 |
编辑距离算法空间优化 | 8 | 17 | 6 |
工程健壮性与可观测性优化 | 6 | 12 | 4 |
2.性能瓶颈分析
初始版本:
- computeEditDistanceOptimized():动态规划算法复杂度高,占总耗时的 65-75%
- FileUtil.readMultipleChunks():I/O 操作频繁,占总耗时的 20-30%
- calculateStreamingSimilarityWithMemoryLimit():控制逻辑和内存管理,占总耗时的 5-10%
3.优化思路与实施
- 编辑距离算法优化:将标准动态规划优化为滚动数组版本,空间复杂度从 O(m×n) 降为 O(n)
- I/O 操作优化:将多次离散读取优化为批量读取,减少系统调用次数
- 内存管理优化:优化内存使用策略,减少垃圾回收压力
4.性能分析
四、核心代码部分单元测试
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import util.FileUtil;
import util.TextUtil;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
public class CoreUnitTest {
@TempDir
Path tempDir;
// 测试1: 编辑距离计算(核心算法)
@Test
void testComputeEditDistanceOptimized() {
// 相同字符串
assertEquals(0, TextUtil.computeEditDistanceOptimized("hello", "hello"));
// 完全不同字符串
assertEquals(3, TextUtil.computeEditDistanceOptimized("kitten", "sitting"));
// 空字符串
assertEquals(3, TextUtil.computeEditDistanceOptimized("", "abc"));
assertEquals(5, TextUtil.computeEditDistanceOptimized("hello", ""));
// 一个字符差异
assertEquals(1, TextUtil.computeEditDistanceOptimized("cat", "bat"));
// Unicode字符
assertEquals(1, TextUtil.computeEditDistanceOptimized("中文", "中文文"));
}
// 测试2: 相似度计算
@Test
void testCalculateEditDistanceSimilarity() {
// 完全相同
assertEquals(1.0, TextUtil.calculateEditDistanceSimilarity("same", "same"), 0.001);
// 完全不同
assertEquals(0.0, TextUtil.calculateEditDistanceSimilarity("abc", "xyz"), 0.001);
// 空文本
assertEquals(1.0, TextUtil.calculateEditDistanceSimilarity("", ""), 0.001);
// 部分相似
double similarity = TextUtil.calculateEditDistanceSimilarity("kitten", "sitting");
assertTrue(similarity > 0.5 && similarity < 1.0);
// 一个为空
assertEquals(0.0, TextUtil.calculateEditDistanceSimilarity("text", ""), 0.001);
}
// 测试3: 文件读取(边界情况)
@Test
void testReadChunkEfficiently() throws IOException {
Path testFile = tempDir.resolve("test.txt");
Files.write(testFile, "Hello World!".getBytes());
// 正常读取
assertEquals("Hello", FileUtil.readChunkEfficiently(testFile.toString(), 0, 5));
// 偏移超出文件大小
assertEquals("", FileUtil.readChunkEfficiently(testFile.toString(), 100, 5));
// 读取部分内容
assertEquals("World!", FileUtil.readChunkEfficiently(testFile.toString(), 6, 10));
// 块大小大于剩余内容
assertEquals("World!", FileUtil.readChunkEfficiently(testFile.toString(), 6, 20));
// 空文件
Path emptyFile = tempDir.resolve("empty.txt");
Files.createFile(emptyFile);
assertEquals("", FileUtil.readChunkEfficiently(emptyFile.toString(), 0, 5));
}
// 测试4: 批量读取多个块
@Test
void testReadMultipleChunks() throws IOException {
Path testFile = tempDir.resolve("multi.txt");
Files.write(testFile, "This is a test content for multiple chunks".getBytes());
long[] offsets = {0, 5, 10, 100}; // 最后一个偏移超出范围
String[] chunks = FileUtil.readMultipleChunks(testFile.toString(), offsets, 5);
assertEquals(4, chunks.length);
assertEquals("This ", chunks[0]);
assertEquals("is a ", chunks[1]);
assertEquals("test ", chunks[2]);
assertEquals("", chunks[3]); // 超出范围的返回空字符串
}
// 测试5: 加权平均计算
@Test
void testCalculateWeightedAverage() {
// 正常列表
List<Double> similarities = Arrays.asList(0.8, 0.9, 0.7);
assertEquals(0.8, TextUtil.calculateWeightedAverage(similarities), 0.001);
// 空列表
List<Double> emptyList = Arrays.asList();
assertEquals(0.0, TextUtil.calculateWeightedAverage(emptyList), 0.001);
// 单个元素
List<Double> single = Arrays.asList(0.5);
assertEquals(0.5, TextUtil.calculateWeightedAverage(single), 0.001);
// 包含边界值
List<Double> extremes = Arrays.asList(0.0, 1.0, 0.5);
assertEquals(0.5, TextUtil.calculateWeightedAverage(extremes), 0.001);
}
// 测试6: 安全块大小计算
@Test
void testCalculateSafeChunkSize() {
// 默认计算(无自定义大小)
int chunkSize1 = PaperCheck.calculateSafeChunkSize(1024 * 1024, 0);
assertTrue(chunkSize1 >= 256 * 1024 && chunkSize1 <= 16 * 1024 * 1024);
// 自定义大小在合理范围内
int chunkSize2 = PaperCheck.calculateSafeChunkSize(1024 * 1024, 512 * 1024);
assertEquals(512 * 1024, chunkSize2);
// 自定义大小超过上限
int chunkSize3 = PaperCheck.calculateSafeChunkSize(1024 * 1024, 100 * 1024 * 1024);
assertTrue(chunkSize3 <= 16 * 1024 * 1024);
// 极小文件
int chunkSize4 = PaperCheck.calculateSafeChunkSize(100, 0);
assertTrue(chunkSize4 >= 256 * 1024); // 仍然保持最小块大小
}
// 测试7: 文件大小获取(异常情况)
@Test
void testGetFileSizeException() {
// 不存在的文件应该抛出异常
assertThrows(IOException.class, () -> {
FileUtil.getFileSize("nonexistent_file.txt");
});
}
// 测试8: 文件写入格式
@Test
void testWriteFileFormat() throws IOException {
Path outputFile = tempDir.resolve("output.txt");
// 测试各种相似度值的格式化输出
FileUtil.writeFile(outputFile.toString(), 0.0);
assertEquals("0.00", Files.readString(outputFile));
FileUtil.writeFile(outputFile.toString(), 0.5555);
assertEquals("55.55", Files.readString(outputFile));
FileUtil.writeFile(outputFile.toString(), 1.0);
assertEquals("100.00", Files.readString(outputFile));
FileUtil.writeFile(outputFile.toString(), 0.9999);
assertEquals("99.99", Files.readString(outputFile));
}
}
测试覆盖的核心方法
computeEditDistanceOptimized() - 编辑距离核心算法
calculateEditDistanceSimilarity() - 相似度计算
readChunkEfficiently() - 高效文件块读取
readMultipleChunks() - 批量读取优化
calculateWeightedAverage() - 加权平均计算
calculateSafeChunkSize() - 内存安全块大小计算
边缘情况覆盖
✅ 空字符串和空文件处理
✅ 文件偏移超出范围
✅ 块大小超过文件内容
✅ 不存在的文件异常
✅ 边界值相似度(0%, 50%, 100%)
✅ 内存限制下的块大小计算
✅ Unicode字符处理
✅ 数值格式化精度
五、计算模块部分异常处理
1.IllegalArgumentException(参数不合法)
触发条件:命令行参数数量不足(少于3个)、自定义块大小参数格式错误(非数字字符串)等
测试用例:
@Test
public void shouldThrowExceptionWhenArgsLessThan3() {
String[] args = {"file1.txt", "file2.txt"};
assertThrows(IllegalArgumentException.class, () -> {
PaperCheck.main(args);
});
}
2.NumberFormatException
触发条件:参数转换错误等
测试用例:
@Test
public void shouldThrowExceptionWhenInvalidChunkSize() {
String[] args = {"file1.txt", "file2.txt", "result.txt", "invalid"};
assertThrows(NumberFormatException.class, () -> {
PaperCheck.main(args);
});
}
3.IOException
触发条件:文件被锁定占用、文件编码等
@Test
public void shouldThrowExceptionWhenFileLocked() throws Exception {
assertThrows(IOException.class, () -> {
// 另一个进程正在写入该文件
FileUtil.readChunkEfficiently("locked_file.txt", 0, 1024);
});
}
4.异常处理方式
- 提前预防
- 异常捕获
- 传播异常
- 降级处理