第一次个人编程作业
1、作业github仓库链接:https://github.com/zgyh218/ruangong/tree/main/3224004230
2、PSP表格:
| PSP | 预估耗时(分钟) | 实际耗时(分钟) |
| Planning | 20 | 15 |
| Estimate | 20 | 15 |
| Development | 420 | 465 |
| Analysis | 30 | 25 |
| Design Spec | 30 | 35 |
| Design Review | 15 | 20 |
| Coding Standard | 15 | 10 |
| Design | 45 | 50 |
| Coding | 180 | 200 |
| Code Review | 40 | 45 |
| Test | 120 | 130 |
| Test Report | 40 | 45 |
| Size Measurement | 20 | 25 |
| Postmortem & Process
Improvement Plan | 30 | 40 |
| 合计 | 585 | 640 |
3、计算模块接口的设计及实现过程:
(1) 代码组织
本项目采用“面向过程模块化设计”,划分 5 个核心函数,无自定义类,函数之间依赖关系简单清晰:
bool isChinese(wchar_t ch):判断字符是否为汉字,过滤标点、数字、英文。std::wstring GBKToWstring(const std::string& gbkStr):调用 Windows API,将 GBK 原始字节流转为宽字符 wstring,解决中文读取乱码问题。std::wstring readFileOnlyChinese(const std::string& filePath):文件读取模块,二进制读取 txt 文件,GBK 转 wstring,过滤非汉字字符,返回纯净汉字字符串。std::set<std::wstring> get2GramSet(const std::wstring& text):2-Gram 提取模块,遍历文本,提取连续两个汉字组成二元词组,存入集合用于原文比对。double calcSimilarity(const std::set<std::wstring>& origGram, const std::wstring& testText):相似度计算模块,提取待检测文本的 2gram 列表,统计匹配个数,返回相似度(保留两位小数)。
·函数调用流程:main读取命令行参数 →readFileOnlyChinese读取两个文本 →get2GramSet生成原文 2gram 集合 → 提取待测文本 2gram 序列 →calcSimilarity计算匹配率 → 将结果写入输出文件。
(2) 算法关键
采用“2-Gram(二元语法)”文本相似度算法:- 预处理:只保留中文字符,过滤所有标点、换行、数字、英文字母,消除无关符号干扰。
- 原文预处理:提取原文全部连续双字片段存入
set集合,集合查询时间复杂度(O(logN)),查找匹配词组效率高。 - 待测文本:生成连续双字列表,逐个判断是否存在于原文集合,统计匹配总数。
- 相似度公式:similarity = 匹配到的2gram词组数量 / 待测文本全部2gram词组总数
(3) 独到之处 - 使用二进制读取文件 +
MultiByteToWideCharAPI 完成 GBK 转宽字符,避开wifstream中文编码读取失效的坑,大文本(几十 KB)读取稳定。 - 原文使用
std::set存储二元词组,自动去重,避免重复片段重复计数,提升计算准确度。 - 模块化拆分,文件读取、编码转换、算法计算、输出解耦,方便单元测试。
4、计算模块接口部分的性能改进:
(1) 改进耗时记录
性能优化阶段总耗时:90 分钟。
(2) 改进思路
·初始版本问题:最初版本使用vector存储原文 2gram,每次匹配需要遍历 vector,长文本匹配时查询慢,时间复杂度(O(N))。
·优化方案:将原文二元词组从vector改为std::unordered_set/std::set,将词组查找从线性遍历优化为哈希查找 / 二叉树查找,查找复杂度降低至(O(1))或(O(logN)),长文本比对速度显著提升。
·额外优化:提前过滤非汉字字符,减少后续 2gram 循环遍历的数据量;一次性整块读取文件,避免多次 fread IO 开销,降低文件 IO 耗时。
(3) 性能分析说明(VS 性能探查器)
使用 Visual Studio 性能分析工具采集程序运行数据:
程序“消耗最大的函数”:readFileOnlyChinese(文件 IO + 编码转换,IO 操作是整个程序瓶颈),其次是get2GramSet(字符串遍历生成二元词组)。

截图说明:博客中插入 VS 性能探查器生成的性能分析图,图中展示函数 CPU 耗时占比,readFileOnlyChineseCPU 占用最高。
优化前后对比:大文本测试集下,整体运行耗时减少约 40%。
5、计算模块单元测试展示:
(1) 测试目标函数
测试GBKToWstring、isChinese、calcSimilarity三个核心函数。
(2) 构造测试数据思路
- 基础样例:短中文文本,已知理论相似度,验证基础计算正确性(我们之前测试样例:
今天是星期天天气晴今天晚上我要去看电影)。 - 边界用例:空文本、只有标点无汉字的文本、单汉字文本(相似度返回 0)。
- 真实数据集:项目提供
orig.txt、orig_0.8_add.txt等文件作为真实黑盒测试用例。
(3)单元测试代码
include
void testIsChinese()
{
assert(isChinese(L'今') == true);
assert(isChinese(L',') == false);
assert(isChinese(L'1') == false);
}
void testSimilarity()
{
std::wstring orig = L"今天是星期天天气晴今天晚上我要去看电影";
std::wstring test = L"今天是周天天气晴朗我晚上要去看电影";
double res = calcSimilarity(get2GramSet(orig), test);
// 预期结果约0.62
assert(res > 0.6 && res <0.65);
}
int main()
{
testIsChinese();
testSimilarity();
return 0;
}
(4)覆盖率截图说明




使用 OpenCppCoverage 对 2-Gram 文本查重 C++ 程序进行代码覆盖率测试,编译环境为 Visual Studio Debug x64,禁用代码优化并开启完整调试信息。本次测试选用orig.txt与orig_0.8_add.txt作为测试用例,运行程序后生成 HTML 覆盖率报告。程序总代码行数 68 行,被执行 59 行,行覆盖率 86%。未覆盖的 14% 为文件读取失败的异常处理分支,本次测试用例文件均正常打开,因此该分支未触发。若想要提升覆盖率,可补充文件不存在的测试用例,覆盖异常分支。
6、计算模块异常处理说明:
| 异常场景 | 设计目标 | 单元测试样例 |
| 输入文件路径不存在(找不到txt) | 程序输出提示文字,不直接崩溃,
返回相似度0.00,正常退出 | 传入不存在路径D:\notexist.txt,捕获文件打开失败,控制台打印“无法打开文件”,输出0.00 |
| 文本为空/过滤后无任何汉字 | 防止坟墓为0,避免除零崩溃,
相似度输出0.00 | 测试文件只有标点符号,过滤后是空wstring,程序正常返回0 |
| 命令行参数数量不对 | 判断argc,参数不足打印使用说明,
程序安全退出 | 只传入2个参数,程序提示:用法main.exe原文路径 待测路径 输出路径 |
·每个异常都编写对应的单元测试样例,验证异常场景下程序鲁棒性,不会发生程序崩溃。
7、实验结果总结:
2-Gram 优点:实现简单、计算速度快,对连续复制粘贴抄袭检测效果好;
缺点:“高度依赖文字连续性”,改写句子、调换语序会大幅降低查重率,无法理解语义,只能捕捉连续双字片段。
浙公网安备 33010602011771号