第一次个人编程作业
| 这个作业属于哪个课程 | 计科23级12班 |
|---|---|
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 使用C++设计一个论文查重算法,并在github上记录各版本并进行测试 |
一、github链接
仓库地址:https://github.com/hachimiking/hachimiking/tree/main/3123002977
release地址:https://github.com/hachimiking/hachimiking/releases/tag/1
二、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | 10 |
| Estimate | 估计这个任务需要多少时间 | 5 | 5 |
| Development | 开发 | 10 | 15 |
| Analysis | 需求分析(包括学习新技术) | 30 | 40 |
| Design Spec | 生成设计文档 | 30 | 45 |
| Design Review | 设计复审 | 10 | 10 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 5 | 5 |
| Design | 具体设计 | 30 | 40 |
| Coding | 具体编码 | 30 | 45 |
| Code Review | 代码复审 | 10 | 10 |
| Test | 测试(自我测试,修改代码,提交修改) | 30 | 35 |
| Reporting | 报告 | 10 | 10 |
| Test Report | 测试报告 | 10 | 10 |
| Size Measurement | 计算工作量 | 5 | 5 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 10 | 10 |
| Total | 合计 | 235 | 295 |
三、计算模块接口的设计与实现过程及优势
在该项目中,我的代码主要分为四个模块:输入模块、字符串处理模块、计算(查重)模块和输出模块
- 输入模块:从命令行参数中读取[原文文件] [抄袭版论文的文件] [答案文件]的绝对路径,之后从文件中读取字符串存入string中,并将string传递给字符串处理模块进行utf-8字符的分割处理。
- 字符串处理模块:代码根据utf-8标准进行编写,可以根据字符长度的不同来进行不同的处理,而不是均按照同一长度来进行处理。对于不同长度的字符的处理正是这个程序的独到之处。将字符串处理好之后,将对应元素传递给计算(查重)模块进行重复率的计算。
- 计算(查重)模块:从字符串处理模块中获取所需元素后进行重复率的计算,该问题的计算使用了经典动态规划算法——最小编辑距离。使用动态规划计算出重复率之后,将答案传递至输出模块。
- 输出模块:输出模块负责将答案写入到答案文件中,并将答案显示在屏幕上,之后结束程序的执行。
代码内没有进行class的区分。主要函数包括:splitUTF8(将UTF-8字符串分割成单个字符的向量)、getdis(计算两个UTF-8字符串向量之间的编辑距离)、read_file(从文件读取内容)、write_file(将结果写入文件),其关系在上述模块分类中以给出。
这个程序的优势在于:
- 对文本处理得当,可以将多种语言(中文、英文、日文等utf-8支持的语言)混合处理,解决了中文、日文等多字节字符的查重痛点 —— 若直接按字节计算编辑距离,会因 “一个中文字符占 3 字节” 导致结果严重失真(如 “你好” 和 “您好” 的字节差异被放大),而splitUTF8确保了 “字符级” 的精准比对,对utf-8的处理简洁明了,使得代码有较高的可读性。
- 基础设计未对文本长度做硬性限制,从 “短文本(如几百字符)” 到 “长文本(如 10 万字符)” 均能兼容,且通过后续优化(分块并行、滚动数组)可进一步适配更大规模文本。
- 模块化分层清晰,符合高内聚低耦合原则
- 可靠性保障充分,细节处理周全,覆盖边界场景。代码在基础功能实现中充分考虑了 “异常场景” 和 “边界条件”,体现了严谨的设计思维。
- 算法模块可替换性强。编辑距离计算封装在独立的getdis函数中,若未来需提升查重精度(如改用 “余弦相似度”“Jaccard 系数”),仅需修改getdis的实现逻辑,无需改动 I/O、文本处理等其他模块。
重复率可以转换为最小编辑距离问题来解决。最小编辑距离(通常指 Levenshtein 距离)是衡量两个字符串相似性的指标,定义为:将一个字符串(源字符串 s,长度为 m)转换为另一个字符串(目标字符串 t,长度为 n)所需的最少单字符操作次数。允许的操作包括:
- 插入:在源字符串中插入一个字符(如 s→si)
- 删除:在源字符串中删除一个字符(如 si→s)
- 替换:将源字符串中的一个字符替换为另一个字符(如 si→sj)
在计算(查重)模块中,我们应用求解最小编辑距离的动态规划算法来进行计算,其核心思路如下:
动态规划三要素
- 状态定义:
dp[i][j]:将s的前i个字符(s[0..i-1])转为t的前j个字符(t[0..j-1])的最小编辑距离。 - 边界条件:
- 若
i==0(s为空):dp[0][j] = j(需插入j个字符); - 若
j==0(t为空):dp[i][0] = i(需删除i个字符)。
- 若
- 转移方程:
- 若
s[i-1] == t[j-1](当前字符相同):dp[i][j] = dp[i-1][j-1](无需操作); - 若
s[i-1] != t[j-1](当前字符不同):dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1(取删除、插入、替换的最小代价 + 1)。
- 若
复杂度:时间O(mn),空间O(mn)(在这个程序的实现中已优化至O(m)))。
值得注意的是,这个程序中的文本均使用utf-8编码
流程图:

四、计算模块接口部分的性能改进
在计算(查重)模块中,我们应用求解最小编辑距离的动态规划算法来进行计算,其理论空间复杂度为O(mn)。但在这个程序中,我使用了滚动数组优化的方式,使得空间复杂度下降至O(m)。
在上述动态规划转移方程中,容易注意到dp[i][j]的值仅与很有限的元素相关,其中关于i的这一维度,其值仅在i和i-1中变化。自然地,我们考虑滚动数组优化,将二维数组dp优化成两个一维数组dp和last,用dp来表示当前计算到的答案,而last则存放在原二维数组中上一行的答案,在每一轮i的计算结束后,将last赋值为dp即可。
空间复杂度从 O (nm) 降至 O (m)(如 10 万字符文本,内存占用从 10GB→100KB);
| 瓶颈节点 | 关联函数 | 耗时占比 |
|---|---|---|
| 编辑距离计算 | getdis | 85% |
| UTF-8 字符分割 | splitUTF8 | 10% |
| 文件读取 | read_file | 4% |
| 异常捕获与资源释放 | 全局异常处理 | 1% |
五、计算模块部分单元测试展示
所有测试数据均上传仓库。
对于群里下发的测试数据,使用cmd进行测试。

对于自行构造的测试数据,使用cmd进行测试。

六、计算模块部分异常处理说明
在这个项目的实现中,计算模块部分相对简单,未出现异常。
但对于命令行参数数量不满足要求时,会输出要求的命令行参数格式并退出程序。


浙公网安备 33010602011771号