第一次个人编程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477 |
| 这个作业的目标 | 完成论文查重项目,学习如何进行单元测试和性能改进 |
一、GitHub链接
https://github.com/fanfanlilili/3123004282
二、PSP表格
| PSP2.1阶段 | Personal Software Process Stages (个人软件过程阶段) | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 15 |
| Estimate | 估计这个任务需要多少开发时间 | 5 | 10 |
| Development | 开发 | 420 | 600 |
| Analysis | 需求分析(包括学习新技术) | 300 | 200 |
| Design Spec | 生成设计文档 | 30 | 30 |
| Design Review | 设计复审 | 10 | 20 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 40 |
| Design | 具体设计 | 30 | 60 |
| Coding | 具体编码 | 100 | 120 |
| Code Review | 代码复审 | 20 | 30 |
| Test | 测试(自我测试,修改代码,提交代码) | 20 | 100 |
| Reporting | 报告 | 40 | 60 |
| Test Repor | 测试报告 | 20 | 20 |
| Size Measurement | 计算工作量 | 5 | 5 |
| Postmortem & Process Improvement Plan | 事后总结 | 20 | 30 |
| 合计 | 1030 | 1310 |
三、模块接口的设计与实现过程
| 层级 | 名称 | 作用 |
|---|---|---|
| 工具函数 | generate_ngrams_from_bytes | 把 UTF-8 中文内容切成 n-gram |
| 工具函数 | calculate_similarity_optimized | 基于 n-gram 计算两文本重复率 |
| 工具函数 | read_file | 一次性读文件到 string |
| 主流程 | main | 顺序调用上面 3 个函数,完成“读→算→写” |
1.主函数流程图

2.calculate_similarity_optimized流程图

3.generate_ngrams_from_bytes流程图

4.关键部分
(1)N-Gram生成:基于UTF-8编码过滤非中文字符,滑动窗口生成连续的中文字符
(2)相似度计算:通过哈希集合快速匹配N-Gram,统计重复比例
(3)预分配内存:提升性能
四、性能改进(耗时180分钟)
1.改进思路
主要从UTF-8解析、内存分配以及查找优化着手,其中查找优化是最主要的改进,明显降低了时间复杂度
(1)使用指针p直接便利字节流,避免索引计算,同时引入滑动窗口机制以避免存储所有中文字符
(2)预分配ngram向量和char_buffer空间以减少动态扩容次数
(3)使用哈希查找法,将时间复杂度从n^2降低到1
2.改进前CPU占用情况

3.改进后CPU占用情况

很明显,占用最多的函数就是get_chinese_chars函数
五、单元测试(将Gtest框架引入VS2022)
1.测试覆盖的函数和场景(一共12个用例)
| 函数 | 场景 |
|---|---|
| get_chinese_chars | 验证是否能正确提取中文字符以及不完整三序列、空输入的处理 |
| generate_ngrams | 验证不同长度输入下的N-Gram生成 |
| calculate_similarity | 验证完全相同、无重叠、部分重叠等场景的重复率计算 |
| read_file | 验证文件内容是否能正常读取 |
2.部分用例说明
(1)测试get_chinese_chars
TEST(GetChineseCharsTest, ExtractsChineseCharactersCorrectly) {
// 输入包含ASCII、双字节非中文、三字节中文、四字节字符
string input =
"A" // 单字节ASCII
"\xC3\xB1" // 双字节非中文(ñ)
"\xE4\xBD\xA0" // 三字节中文(你)
"\xE5\xA5\xBD" // 三字节中文(好)
"\xF0\x9F\x98\x8A";// 四字节字符(😊)
vector<string> expected = { "\xE4\xBD\xA0", "\xE5\xA5\xBD" };
vector<string> result = get_chinese_chars(input);
ASSERT_EQ(result, expected);
}
(2)测试generate_ngrams
TEST(GenerateNGramsTest, SufficientLength) {
vector
int n = 3;
vector
vector
ASSERT_EQ(result, expected);
}
TEST(GenerateNGramsTest, NotEnoughLength) {
vector
vector
ASSERT_TRUE(result.empty());
}
TEST(GenerateNGramsTest, ExactLength) {
vector
vector
vector
ASSERT_EQ(result, expected);
}
(3)测试calculate_similarity
TEST(CalculateSimilarityTest, NoCommonNGrams) {
string orig_content = "我爱你中国"; // 字符:我、爱、你、中、国(5个字符,生成3个N-Gram)
string copy_content = "天地人和"; // 字符:天、地、人、和(4个字符,生成2个N-Gram)
string orig_path = create_temp_file(orig_content);
string copy_path = create_temp_file(copy_content);
string output_path = "test_output.txt";
double similarity = calculate_similarity(orig_path, copy_path, 3);
// 原N-Gram: ["我愛你", "愛中國"]
// 抄袭版N-Gram: ["天地人", "地和人"](假设无重叠)
EXPECT_DOUBLE_EQ(similarity, 0.0);
remove_temp_file(orig_path);
remove_temp_file(copy_path);
remove(output_path.c_str());
}
(4)测试read_file
TEST(ReadFileTest, ReadsContentCorrectly) {
string content = "测试文件读取";
string path = create_temp_file(content);
string read_content = read_file(path);
ASSERT_EQ(read_content, content);
remove_temp_file(path);
}
3.覆盖率

4.单元测试情况

12个测试用例通过十个,属于正常现象,该程序没有引入英文字符的解析(仅解析中文字符)
六、异常情况处理及其单元测试用例
1.空路径时报错
(1)原码
if (path.empty()) {
throw invalid_argument("错误:文件路径为空");
}
(2)用例
TEST(ReadFileExceptionTest, EmptyPathThrowsInvalidArgument) {
EXPECT_THROW({
read_file(""); // 空路径
}, invalid_argument);
}
2.文件不存在时报错
(1)原码
// 检查文件是否成功打开(新增详细错误信息)
if (!file.is_open()) {
// 获取系统错误信息(跨平台)
ifdef _WIN32
DWORD error_code = GetLastError();
LPSTR error_msg = nullptr;
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
nullptr,
error_code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&error_msg,
0,
nullptr
);
string msg = "错误:无法打开文件 '" + path + "'。系统错误:" + error_msg;
LocalFree(error_msg); // 必须释放 FormatMessageA 分配的内存
else
string msg = "错误:无法打开文件 '" + path + "'。错误码:" + to_string(errno);
endif
throw runtime_error(msg);
}
(2)用例
TEST(ReadFileExceptionTest, NonExistentFileThrowsRuntimeError) {
std::string non_existent_path;
ifdef _WIN32
non_existent_path = "C:\nonexistent_file_987654321.txt"; // Windows 绝对路径
else
non_existent_path = "/tmp/nonexistent_file_987654321.txt"; // Linux/macOS 绝对路径
endif
// 确保文件不存在(跨平台)
ifdef _WIN32
DeleteFileA(non_existent_path.c_str()); // 忽略错误(假设不存在)
else
unlink(non_existent_path.c_str()); // 忽略错误(假设不存在)
endif
EXPECT_THROW({
read_file(non_existent_path);
}, runtime_error);
}
3.空文件时报错
(1)原码
// 检查文件是否为空(新增)
file.seekg(0, ios::end);
if (file.tellg() == 0) {
throw runtime_error("错误:文件 '" + path + "' 为空");
}
file.seekg(0, ios::beg); // 回到文件开头
(2)用例
TEST(ReadFileExceptionTest, EmptyFileThrowsRuntimeError) {
std::string temp_path = create_temp_file(""); // 创建空文件
EXPECT_THROW({
read_file(temp_path);
}, runtime_error);
delete_temp_file(temp_path); // 清理临时文件
}
浙公网安备 33010602011771号