第一次个人编程作业

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.核心算法
image
*矩阵构建

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;

算法亮点:

  1. 最优子结构设计:复杂问题---->重叠子问题
  2. 时空复杂度平衡
  3. 多操作类型支持
  4. 边界条件智能处理

空间优化潜力

// 可优化为只使用两行数组
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.性能分析
image

四、核心代码部分单元测试

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.异常处理方式

  • 提前预防
  • 异常捕获
  • 传播异常
  • 降级处理
posted @ 2025-09-23 21:36  Scaler-1024  阅读(9)  评论(0)    收藏  举报