免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

C++刷题统计工具开发:从数据结构设计到JSON持久化实战

C++刷题统计工具开发:从数据结构设计到JSON持久化实战 1. 从“刷题”到“统计”一个C程序员的效率革命如果你和我一样是个靠C吃饭的程序员或者正在这条路上努力那你对“刷题”这个词一定不陌生。无论是为了准备面试还是为了巩固算法基础又或是参加像CTF、华为OD机试这类竞赛刷题都是绕不开的日常。但不知道你有没有过这样的困惑刷了上百道题回头一看好像什么都学了又好像什么都没记住。哪些题型是薄弱环节最近一周的刷题效率是高是低《C Primer Plus》看了三遍但面对“具身智能大小脑C代码示例中的桥接层”这种具体场景时还是无从下手。这就是“刷题统计”这个简单想法背后的深层需求。它远不止是记录一个“已解决”的数量。对于C开发者而言刷题的过程本质上是将《深入浅出C》中的理论、八大排序算法、哈希表、单调栈等知识点在力扣LeetCode这类平台上进行高强度、场景化的实战演练。每一次ACAccepted背后可能都涉及对STL中std::hash的巧妙运用、对Lambda函数格式的精准把握或是处理“C计算超过整数最大值”的边界条件。然而如果没有系统的统计和分析这些宝贵的实战经验很容易变成零散的记忆碎片。因此一个用C自己实现的刷题统计工具其意义在于将感性的“刷了很多题”转化为理性的数据洞察。它可以帮助我们量化学习进度识别知识盲区比如总是搞不清欧拉路径和快速幂算法甚至优化刷题策略。更重要的是构建这个工具本身就是对C核心能力的一次综合锻炼从文件的读写记录刷题日志、字符串的处理解析题目信息、到数据结构的设计用哈希表或结构体链表来组织数据最后可能还需要一个简单的数据可视化输出。这本身就是一个极佳的“C项目”比单纯写一个“C小游戏”或死记“C八股文”更能提升工程能力。接下来我将从一个一线C开发者的角度手把手带你设计并实现一个本地、轻量但功能完整的刷题统计系统。我们会用到最朴素的C标准库避开复杂的框架专注于解决实际问题。无论你是在用Visual Studio 2017还是VSCode配置C/C环境无论你是困扰于v142构建工具缺失还是想寻找比“C最快的快读快写”更实用的文件处理技巧这篇文章都能给你带来直接可用的代码和接地气的思路。2. 核心需求分析与数据结构设计不止于计数在动手写第一行代码之前我们必须想清楚这个统计工具到底要统计什么。如果只是记录总题数一个记事本就能搞定。但我们要的是能指导后续学习决策的“仪表盘”。基于常见的刷题场景我梳理了以下几个核心数据维度题目基本信息唯一标识如力扣题号、标题、链接。解题状态与时间是否解决、首次通过日期、最后一次提交日期、累计耗时。技术标签这道题涉及的核心算法与数据结构如“哈希表”、“单调栈”、“快速幂”、“欧拉路径”以及C特定知识点如“Lambda函数”、“智能指针”、“std::move”。难度与熟练度题目难度简单/中等/困难以及个人主观评价的熟练度生疏/一般/熟练。提交记录同一道题可能多次提交优化解法、修复BUG记录每次提交的日期和结果AC/Wrong Answer/Time Limit Exceeded。看到这里你可能觉得这有点复杂。但别担心我们可以分步实现。首先我们需要一个核心的数据结构来承载这些信息。在C中我们有多种选择简单的结构体数组、std::vector、std::map或者自己用结构体链表实现。对于这个项目我推荐使用std::vector配合std::unordered_map哈希表。为什么std::vectorProblem作为主存储容器Problem是我们自定义的结构体。vector支持动态扩容顺序遍历效率高非常适合存储所有题目记录并进行批量处理如按时间排序、筛选。std::unordered_mapstd::string, Problem*以题目ID如“leetcode-1”为键指向vector中对应题目的指针为值。这为我们提供了O(1)时间复杂度的题目查找能力当你想要更新某道题的记录时无需遍历整个vector。下面是我们核心数据结构Problem的一个初步设计// problem.h #ifndef PROBLEM_H #define PROBLEM_H #include string #include vector #include ctime // 使用枚举提高代码可读性和安全性 enum class Difficulty { EASY, MEDIUM, HARD }; enum class Proficiency { UNFAMILIAR, GENERAL, PROFICIENT }; enum class Status { SOLVED, ATTEMPTED, UNSOLVED }; struct Submission { std::time_t timestamp; // 提交时间戳 bool accepted; // 是否通过 int timeCost; // 耗时毫秒 std::string note; // 提交备注如“暴力解法”、“优化后” }; struct Problem { std::string id; // 唯一标识如 leetcode-1 std::string title; std::string url; Difficulty difficulty; Status status; Proficiency proficiency; std::time_t firstSolvedDate; std::time_t lastSubmittedDate; int totalTimeCost; // 累计耗时 std::vectorstd::string tags; // 算法标签 std::vectorstd::string cppTags; // C特性标签 std::vectorSubmission submissionHistory; // 提交历史 // 一个便捷的构造函数 Problem(const std::string pid, const std::string t, Difficulty d) : id(pid), title(t), difficulty(d), status(Status::UNSOLVED), proficiency(Proficiency::UNFAMILIAR), firstSolvedDate(0), lastSubmittedDate(0), totalTimeCost(0) {} }; #endif // PROBLEM_H这个设计已经包含了我们之前提到的大部分核心字段。submissionHistory的引入尤其关键它让我们能追溯解题过程分析哪类错误如WA, TLE出现最多。tags和cppTags的分离有助于我们分别统计算法能力和语言特性的掌握情况。注意关于std::time_t这里使用std::time_t通常是自1970年1月1日以来的秒数来存储时间因为它简单且与C标准库函数如std::localtime兼容。在实际存储时我们可能会将其格式化为“YYYY-MM-DD”的字符串但在内存中保持time_t形式便于计算和排序。3. 数据持久化选择JSON还是自定义二进制格式数据存在内存里程序一关就没了。所以我们需要把刷题记录保存到文件中。这里有两个主流选择JSON格式和自定义二进制格式。方案一JSON推荐给大多数开发者优点人类可读易于调试。用现成的库如 nlohmann/json 解析和序列化非常简单几行代码就能搞定。与其它语言如Python交换数据也方便。缺点文件体积相对较大读写速度比二进制格式慢但对于个人刷题记录的数据量完全可以忽略。操作示例使用nlohmann/json库#include fstream #include nlohmann/json.hpp using json nlohmann::json; void saveToJson(const std::vectorProblem problems, const std::string filename) { json j; for (const auto p : problems) { json problemJson; problemJson[id] p.id; problemJson[title] p.title; problemJson[difficulty] static_castint(p.difficulty); // 枚举转int存储 // ... 序列化其他字段 j.push_back(problemJson); } std::ofstream file(filename); file j.dump(4); // 缩进4个空格美观 }你只需要用vcpkg或直接包含单头文件的方式引入这个库就能轻松实现读写。方案二自定义二进制格式优点极致紧凑读写速度快。适合对性能有极端要求或记录数据量极大的场景比如你刷了十万道题。缺点人类不可读调试困难。需要自己处理字节序、结构体对齐、字符串变长存储等复杂问题代码容易出错且格式一旦确定很难向后兼容。简易实现警告如果你决定用二进制千万不要简单地对整个Problem结构体进行fwrite因为std::string和std::vector内部包含指针直接写入的是指针值而非实际数据读回来会崩溃。必须手动序列化每个基本字段和容器的大小及内容。我的选择与建议 对于个人刷题统计工具我强烈推荐使用JSON格式。理由如下开发效率nlohmann/json库极大地简化了工作让我们能专注于业务逻辑而不是数据格式解析。可维护性你可以随时用文本编辑器打开problems.json检查或手动修正一条记录这在调试时非常有用。扩展性未来如果你想为这个工具增加一个Web前端JSON是天然的数据交换格式。性能足够即便你有几千条记录JSON文件的读写也在毫秒级毫无感知。因此在接下来的实现中我们将以JSON作为持久化方案。你需要做的就是在你的C项目中集成nlohmann/json库。如果你用的是VSCode可以通过CMake的find_package或直接#include json.hpp来使用。4. 核心功能模块实现记录、查询与统计有了数据结构和存储方案我们就可以开始搭建核心功能了。我们将系统划分为几个清晰的模块。4.1 问题记录管理器ProblemManager这个类是系统的大脑负责管理所有Problem实例的生命周期并提供增删改查接口。// problem_manager.h #ifndef PROBLEM_MANAGER_H #define PROBLEM_MANAGER_H #include problem.h #include unordered_map #include vector #include string class ProblemManager { private: std::vectorProblem problems_; // 主存储 std::unordered_mapstd::string, Problem* idIndex_; // 快速查找索引 std::string dataFile_ problems.json; // 内部辅助函数 Problem* findProblemById(const std::string id); void rebuildIndex(); // 在从文件加载后重建哈希索引 public: ProblemManager(); ~ProblemManager() default; // 核心CRUD操作 bool addProblem(const Problem problem); bool updateProblemStatus(const std::string id, Status newStatus, const Submission newSubmission Submission()); bool addTagToProblem(const std::string id, const std::string tag, bool isCppTag false); const Problem* getProblem(const std::string id) const; // 批量获取 const std::vectorProblem getAllProblems() const { return problems_; } std::vectorconst Problem* getProblemsByTag(const std::string tag) const; std::vectorconst Problem* getProblemsByDifficulty(Difficulty diff) const; // 持久化 bool loadFromFile(); bool saveToFile() const; }; #endif // PROBLEM_MANAGER_H实现要点与踩坑记录rebuildIndex()的重要性从JSON文件加载数据到problems_向量后vector可能因为重新分配内存导致原有指针失效。因此必须在加载完成后遍历problems_用每个Problem对象的新地址重新填充idIndex_。addProblem的冲突处理在添加新题目前务必检查idIndex_中是否已存在相同ID。如果存在应提示用户是覆盖、合并还是取消操作。这里我建议采用“更新”策略即如果ID存在则用新信息更新旧记录例如更新标题或链接而不是简单拒绝。updateProblemStatus的细节当一道题首次被解决newStatus Status::SOLVED时除了更新状态还应设置firstSolvedDate为当前时间并将本次提交记录到submissionHistory中。lastSubmittedDate则在每次提交无论是否通过时都更新。4.2 统计分析与报告生成这是工具的“价值输出”模块。我们将实现几个关键的统计函数。// analyzer.h #ifndef ANALYZER_H #define ANALYZER_H #include problem_manager.h #include map #include string struct Statistics { // 基础计数 int totalProblems 0; int solvedProblems 0; int attemptedProblems 0; int easySolved 0, mediumSolved 0, hardSolved 0; // 时间统计 long long totalTimeSpent 0; // 总耗时毫秒 std::time_t startDate 0; // 最早刷题日期 std::time_t endDate 0; // 最近刷题日期 // 标签云频率统计 std::mapstd::string, int tagFrequency; std::mapstd::string, int cppTagFrequency; // 每日/每周活动热力图数据简化版按日期计数 std::mapstd::string, int dailyActivity; // key: YYYY-MM-DD // 熟练度分布 int unfamiliarCount 0; int generalCount 0; int proficientCount 0; }; class Analyzer { public: static Statistics calculateStatistics(const ProblemManager manager); static void printReport(const Statistics stats); // 可以扩展更多分析函数如 // static std::vectorconst Problem* findWeakness(const ProblemManager manager); }; #endif // ANALYZER_HcalculateStatistics函数实现逻辑遍历所有题目对ProblemManager中的每个Problem。更新计数根据status和difficulty更新solvedProblems、attemptedProblems及各难度计数。累加时间将totalTimeCost累加到totalTimeSpent。更新日期范围比较并更新startDate和endDate取firstSolvedDate和lastSubmittedDate中的最小/最大值。统计标签遍历每个问题的tags和cppTags在对应的map中增加计数。生成活动图数据将lastSubmittedDate格式化为“YYYY-MM-DD”字符串在dailyActivitymap中对该日期的计数加1。统计熟练度根据proficiency更新对应计数。printReport函数示例输出 刷题统计报告 统计周期2023-10-01 至 2024-04-25 ---------------------------------- 总体概况 总题数 157 已解决 89 (56.7%) 尝试中 23 未开始 45 难度分布已解决 简单 45 (50.6%) 中等 38 (42.7%) 困难 6 (6.7%) ⏱️ 时间投入 总耗时 ~ 68 小时 30 分钟 平均每题耗时 ~ 46 分钟 ️ 高频算法标签Top 5 1. 哈希表 (34) 2. 双指针 (28) 3. 动态规划 (25) 4. 二分查找 (19) 5. 栈 (17) ⚙️ 高频C特性Top 3 1. STL容器 (89) 2. Lambda表达式 (45) 3. 智能指针 (22) 近期活跃度 最近7天提交 12 次 最活跃日期 2024-04-20 (8次提交) 熟练度分析 生疏需复习 15 题 一般掌握 52 题 熟练掌握 22 题这样的报告一目了然能让你快速定位自己的强项和短板。4.3 命令行交互界面CLI为了让工具好用我们需要一个简单的命令行界面。这里不追求复杂的GUI用cin/cout足矣但设计要清晰。// cli.h #ifndef CLI_H #define CLI_H #include problem_manager.h class CLI { private: ProblemManager manager_; void printMenu(); void handleAddProblem(); void handleUpdateStatus(); void handleAddTag(); void handleShowProblem(); void handleGenerateReport(); // ... 其他处理函数 public: CLI(ProblemManager mgr) : manager_(mgr) {} void run(); // 主循环 }; #endif // CLI_Hrun()函数的主循环可能长这样void CLI::run() { manager_.loadFromFile(); // 启动时加载数据 int choice 0; do { printMenu(); std::cin choice; std::cin.ignore(); // 清除换行符 switch (choice) { case 1: handleAddProblem(); break; case 2: handleUpdateStatus(); break; case 3: handleAddTag(); break; case 4: handleShowProblem(); break; case 5: handleGenerateReport(); break; case 6: manager_.saveToFile(); break; case 0: std::cout 再见数据已自动保存。\n; break; default: std::cout 无效选择请重新输入。\n; } } while (choice ! 0); }交互设计心得输入校验对于日期、枚举值如难度的输入一定要做校验和友好提示。例如如果用户输入了“简单”可以将其映射到Difficulty::EASY。即时反馈每次操作添加、更新成功后给出一条简短的确认信息。容错与恢复在saveToFile时可以考虑先写入一个临时文件成功后再替换原文件防止程序崩溃导致数据损坏。5. 高级特性探讨与性能优化基础功能实现后我们可以思考一些增强特性让工具更智能、更强大。5.1 与刷题平台如力扣的“半自动”同步完全手动录入题目信息很麻烦。一个可行的“半自动”方案是浏览器插件辅助编写一个简单的浏览器插件如Chrome扩展当你在力扣问题页面时插件可以抓取页面标题、难度、标签等信息。生成导入文件插件将这些信息格式化为一个预定义的JSON片段并保存为文件或复制到剪贴板。本地工具导入你的C统计工具提供一个“从文件导入”或“从剪贴板导入”的功能解析这个JSON片段自动创建或更新本地记录。这样你只需要在AC一道题后点一下插件按钮就能把关键信息录入系统大大提升了效率。这涉及到一些Web前端插件开发和跨进程通信的知识是一个不错的扩展方向。5.2 基于历史数据的智能推荐当数据积累到一定程度后我们可以让工具变得更“聪明”。例如实现一个“每日一题”或“薄弱环节强化”推荐功能。算法思路计算标签掌握度对于每个算法标签计算掌握度 (已解决且熟练度GENERAL的该标签题目数) / (接触过的该标签总题数)。识别薄弱标签将掌握度低于某个阈值如0.6的标签标记为薄弱项。推荐策略巩固薄弱项从题库可以维护一个本地常见题库列表中筛选出属于用户薄弱标签、且难度适中的未解决题目进行推荐。交叉练习推荐同时包含1个薄弱标签和1-2个已掌握标签的题目促进知识联结。复习提醒根据艾宾浩斯遗忘曲线对标记为“生疏”且解决时间超过一定天数如7天的题目进行推送提醒。这个功能将你的刷题记录从“静态档案”变成了“动态学习助手”。5.3 性能考量与优化建议尽管数据量不大但养成良好的性能习惯很重要。文件读写优化懒保存与自动保存不必每次操作都立刻保存文件。可以设置一个脏数据标志dirty_在数据被修改时置位然后在程序退出时、或每隔一段时间如每5分钟、或累积一定操作次数后自动保存。增量更新高级如果记录非常多可以考虑只将变动的Problem对象序列化并追加到日志文件定期合并。但这大大增加了复杂度非必需。内存与查询优化std::unordered_map用于ID查找已经很快平均O(1)。当需要按其他条件如标签、难度频繁筛选时可以考虑建立额外的索引。例如维护一个std::mapDifficulty, std::vectorProblem*。但这会增加数据更新的开销需要权衡。对于个人使用先遍历vector过滤通常就足够了除非你有上万条记录。使用移动语义在addProblem或从文件加载数据时如果Problem对象是临时创建的使用std::move可以避免不必要的拷贝。problems_.push_back(std::move(newProblem));6. 构建、测试与扩展你的项目6.1 项目构建与依赖管理一个清晰的项目结构有助于长期维护。建议如下/刷题统计工具 ├── CMakeLists.txt # 项目构建文件 ├── src/ │ ├── main.cpp # 程序入口 │ ├── problem.h/cpp │ ├── problem_manager.h/cpp │ ├── analyzer.h/cpp │ ├── cli.h/cpp │ └── utils.h/cpp # 时间格式化等工具函数 ├── lib/ # 放置第三方库如 nlohmann/json │ └── json.hpp ├── data/ # 数据文件目录 │ └── problems.json └── build/ # 构建输出目录建议对应的CMakeLists.txt核心部分cmake_minimum_required(VERSION 3.10) project(ProblemTracker) set(CMAKE_CXX_STANDARD 17) # 包含头文件目录 include_directories(${CMAKE_SOURCE_DIR}/lib) # 添加可执行文件 add_executable(problem_tracker src/main.cpp src/problem.cpp src/problem_manager.cpp src/analyzer.cpp src/cli.cpp src/utils.cpp )使用VSCode配合CMake Tools插件或者直接使用Visual Studio打开包含CMakeLists.txt的文件夹都可以轻松地构建和调试本项目。6.2 单元测试的重要性即使是个人工具写点简单的测试也能避免很多低级错误。例如测试ProblemManager的添加和查找功能// test_problem_manager.cpp (可使用 Catch2, Google Test 等框架这里简单示意) #include problem_manager.h #include cassert void testAddAndFind() { ProblemManager mgr; Problem p1(test-1, Two Sum, Difficulty::EASY); assert(mgr.addProblem(p1) true); assert(mgr.getProblem(test-1) ! nullptr); assert(mgr.getProblem(test-1)-title Two Sum); // 测试重复添加 assert(mgr.addProblem(p1) false); // 假设我们的逻辑是拒绝重复ID std::cout testAddAndFind passed!\n; }6.3 可能的扩展方向这个工具像一个乐高底座你可以根据自己的需求添加更多模块图形化界面GUI使用Qt或Dear ImGui为工具做一个本地图形界面用图表直观展示统计报告。数据导出支持将统计数据导出为CSV或Markdown格式方便嵌入到你的学习笔记或博客中。多平台支持将核心逻辑封装成库然后为不同平台如Web前端、移动端编写界面实现数据云同步注意数据安全。竞赛模式针对CTF或华为OD机试可以增加“竞赛”模式记录在限时环境下的解题情况并单独统计。7. 避坑指南与实战心得在实现过程中我踩过不少坑这里分享几个最典型的枚举的序列化与反序列化JSON库默认不支持C枚举。我们存的是int读回来也是int。在反序列化时一定要检查int值是否在枚举的有效范围内否则强制转换会导致未定义行为。一个安全的做法是使用static_cast并配套一个校验函数。Difficulty intToDifficulty(int val) { if (val 0 val 2) return static_castDifficulty(val); else return Difficulty::EASY; // 或抛出一个异常 }时间处理std::time_t和std::tm的转换以及时区问题很容易出错。建议将所有时间在存储时都转换为UTC时间戳在显示时再根据本地时区格式化。使用C11的chrono库是更现代、更安全的选择但需要处理与C库的交互。对于这个项目简单起见可以约定所有时间都以本地时间存储并在工具内部使用std::localtime和std::mktime但要小心它们的线程安全问题考虑用localtime_r/localtime_s。文件路径你的程序当前工作目录可能变化。最好使用绝对路径来定位data/problems.json文件。一个常见做法是如果存在配置文件则从配置文件读取数据目录否则在可执行文件同级目录或用户家目录下创建数据文件夹。std::string getDataFilePath() { // 尝试从环境变量或配置文件读取 // 否则返回一个默认路径如 ./data/problems.json // 更健壮的做法是使用 std::filesystem (C17) }输入处理在CLI中混合使用std::cin 和std::getline()会导致换行符残留问题。记住在用了读取数字或单词后如果后面要跟getline()读取整行必须先用std::cin.ignore()清除输入缓冲区中的换行符。数据备份在实现saveToFile()时可以先拷贝旧文件为problems.json.bak再将新内容写入problems.json。这样即使写入过程中程序崩溃你还有一个可用的备份。最后我想说的是这个“刷题统计C”项目最大的价值不在于你写出了多完美的代码而在于你通过解决一个真实的需求完整地走了一遍小型C软件的设计、开发、测试流程。你实践了面向对象设计、文件I/O、数据结构选择、第三方库集成、简单的数据分析甚至可能涉及一点算法。这比孤立地学习“C字符串转数组”或“C回调函数例子”要深刻得多。当你下次在力扣上AC一道关于“哈希表”的题目后顺手用自己写的工具记录下来看到统计报告中“哈希表”的计数又增加了一次那种感觉是任何现成的刷题APP都无法给予的。
返回列表