第一次个人编程作业

项目 链接
这个作业属于哪个课程 软件工程
这个作业要求在哪里 个人项目
这个作业的目标 文本查重代码实现,并测试,分析
代码仓库 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] 表示 s1i 个字符和 s2j 个字符的最小编辑距离。

相似度计算:

  • 公式:
    [
    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 的完整代码,包含针对 calculateSimilaritycalculateLevenshteinDistance 的测试用例,用于验证计算模块的功能完整性:

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)。
  • 复杂测试
    • 覆盖插入、删除和替换三种操作,确保动态规划计算正确。

posted @ 2025-03-09 00:36  夜浅。  阅读(51)  评论(0)    收藏  举报