简介:本资源是面向高校计算机专业学生及算法初学者的数据结构实战训练包,聚焦SWUSTOJ平台80道经典题目,覆盖数组、链表、栈、队列、树、图、哈希表、排序查找、动态规划等核心知识点,有效解决理论学习后缺乏系统编码实践的痛点。压缩包共86个文件,含83个可编译运行的C++源码(.cpp)与3个配套可执行程序(.exe),涵盖二叉树遍历与判定、图的DFS/BFS、最小生成树、哈希冲突处理、堆排序、括号匹配、约瑟夫问题等典型题型,代码结构清晰、注释完整,便于调试与理解。资源大小仅449KB,轻量易下载,已有609人学习使用。读者可直接导入IDE编译运行,获得从输入输出规范、边界条件处理到时间复杂度优化的全流程参考,特别适合课程设计、期末复习与OJ刷题能力提升。
1. 这不是题库,是西南科技大学OJ数据结构真题的「可执行解题手册」
如果你正在刷SWUSTOJ的数据结构题目,却卡在“编译通过但样例不通过”“本地能跑线上WA”“递归深度超限”“指针野访问段错误”上——这份80道代码压缩包,本质是一份带完整运行环境、输入输出契约、边界处理逻辑的「可复现解题手册」。它不只给出AC代码,更暴露了西南科大OJ评测系统的真实约束:比如利用先序遍历创建的二叉树要求输入格式为含空结点标记(如#)的字符串;循环队列必须严格按MAXSIZE-1有效容量实现;哈希表(开放定址法)需明确定义探测序列与装填因子阈值。这些细节在严蔚敏《数据结构(C语言版)》习题集里不会写,但在真实OJ环境中决定生死。它适合两类人:一是刚学完链表/栈/二叉树基础,需要对照标准实现反向验证自己代码逻辑漏洞的初学者;二是准备华为OD、银行科技岗等技术笔试,需快速建立“题目→数据结构选型→边界编码→OJ适配”闭环的求职者。所有代码均以C++编写,无第三方依赖,g++ 7.5+ 可直接编译,且每道题命名直指核心操作(如逆置单链表.cpp),避免抽象命名带来的理解成本。
2. 从输入解析到结构构建:二叉树类题目中的三重契约
西南科大OJ对二叉树题目的输入输出有明确契约,脱离该契约的代码即使逻辑正确也会被判错。本节以输出利用先序遍历创建的二叉树的中序遍历序列.cpp为例,拆解其输入解析、树构建、遍历输出三个环节的强制约定。
2.1 输入格式的隐式规则与健壮解析
OJ输入并非标准二叉树数组表示,而是带空结点标记的先序字符串序列,例如:ABD##E##CF##G##。其中#代表空结点,##表示某结点左右子树均为空。关键在于:输入流读取时不能简单用cin >> ch,因为存在连续#和字母混排,需逐字符读取并跳过空白符。
#include <iostream> #include <string> #include <stack> using namespace std; struct TreeNode { char val; TreeNode* left; TreeNode* right; TreeNode(char x) : val(x), left(nullptr), right(nullptr) {} }; // 关键:逐字符读取,跳过空格、换行等分隔符 char getNextChar() { char ch; while (cin.get(ch)) { if (ch != ' ' && ch != '\n' && ch != '\t') return ch; } return '#'; } TreeNode* buildTree() { char ch = getNextChar(); if (ch == '#') return nullptr; TreeNode* root = new TreeNode(ch); root->left = buildTree(); // 递归构建左子树 root->right = buildTree(); // 递归构建右子树 return root; }提示:
getNextChar()函数是OJ适配核心。若使用cin >> ch,遇到ABD##E##时,>>会将#作为分隔符吞掉,导致后续读取错位。必须用cin.get()逐字抓取,显式过滤空白符。
2.2 树构建过程中的内存管理与递归终止条件
构建过程采用经典先序递归,但需注意两点硬性约束:
- 空结点必须返回
nullptr而非new TreeNode('#')—— 否则中序遍历时会输出非法字符; - 每个
new TreeNode必须有对应delete逻辑(虽OJ不检查内存泄漏,但本地调试时避免堆污染)。
以下为安全构建与释放模板:
void deleteTree(TreeNode* root) { if (!root) return; deleteTree(root->left); deleteTree(root->right); delete root; } // 在main中调用: int main() { TreeNode* root = buildTree(); // ... 执行中序遍历 deleteTree(root); // 必须释放,否则Valgrind报内存泄露 return 0; }2.2.1 中序遍历的非递归实现与栈空间控制
OJ对栈深度有限制(通常≤10000),深度过大的递归遍历易触发Runtime Error: Segmentation fault。因此中序遍历推荐使用显式栈模拟:
void inorderIterative(TreeNode* root) { stack<TreeNode*> stk; TreeNode* curr = root; while (curr || !stk.empty()) { while (curr) { // 一路向左压栈 stk.push(curr); curr = curr->left; } curr = stk.top(); // 访问栈顶 cout << curr->val; stk.pop(); curr = curr->right; // 转向右子树 } cout << endl; }注意:此实现时间复杂度O(n),空间复杂度O(h)(h为树高),比递归更可控。若题目要求输出空格分隔(如
A B D E C F G),需在cout << curr->val后加<< " ",但末尾多一个空格会被OJ判为PE(Presentation Error),需用vector暂存后统一输出。
2.3 输出格式的精确匹配:空格、换行与特殊字符
OJ判题严格比对输出流,包括末尾换行。中序遍历序列要求输出为无空格连续字符串(如BDAECFG),而非带空格分隔。若误输出B D A E C F G\n,将直接WA。验证方法:将程序输出重定向到文件,用diff -b对比标准答案:
# 编译并生成输出 g++ -o inorder inorder.cpp ./inorder < input.txt > output.txt # 与标准答案比对(忽略空格差异) diff -b output.txt expected_answer.txt| 参数 | 说明 | OJ常见陷阱 |
|---|---|---|
input.txt | 包含ABD##E##CF##G##的纯文本文件 | 文件末尾多空行导致getNextChar()阻塞 |
expected_answer.txt | 对应中序结果BDAECFG | 答案末尾无换行,但代码cout << endl多输出\n |
diff -b | 忽略空格、制表符、换行符差异 | 若答案含空格而代码无,则-b仍报错,需用-w |
3. 链表与栈的底层操作:从指针操作到OJ边界防御
链表和栈是OJ高频考点,但学生常因指针误操作或边界未覆盖而失败。本节以逆置单链表.cpp和利用栈完成后缀表达式的计算.cpp为例,揭示OJ环境下的指针安全规范与栈操作容错设计。
3.1 单链表逆置的三种实现及其OJ适配性分析
SWUSTOJ要求链表节点结构体为:
struct ListNode { int data; ListNode* next; ListNode(int x) : data(x), next(nullptr) {} };3.1.1 迭代法:最安全的OJ首选方案
递归法在链表长度>1000时易栈溢出,迭代法无此风险,且逻辑清晰:
ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr) { ListNode* nextTemp = curr->next; // 保存下一节点 curr->next = prev; // 反转当前指针 prev = curr; // 移动prev curr = nextTemp; // 移动curr } return prev; // 新头结点 }关键参数说明:
nextTemp必须在curr->next = prev前保存,否则链表断裂。OJ测试用例包含空链表(head == nullptr)、单节点链表(head->next == nullptr),此代码天然支持。
3.1.2 递归法:仅限小规模数据的演示方案
虽简洁但有风险,仅作理解用:
ListNode* reverseListRecursive(ListNode* head) { if (!head || !head->next) return head; // 边界:空或单节点 ListNode* newHead = reverseListRecursive(head->next); head->next->next = head; // 反转连接 head->next = nullptr; // 断开原连接 return newHead; }注意:OJ对递归深度限制通常为1000,若测试用例含10000节点链表,此函数必RE。生产环境禁用。
3.2 后缀表达式计算中的栈操作与异常防御
后缀表达式如"3 4 + 2 *"需计算为14。OJ输入为空格分隔的字符串,需分割后逐项处理。关键防御点:操作数栈溢出、除零、非法字符。
#include <sstream> #include <cctype> int evalRPN(const string& tokens) { stack<long long> stk; // 用long long防int溢出 istringstream iss(tokens); string token; while (iss >> token) { if (token == "+" || token == "-" || token == "*" || token == "/") { if (stk.size() < 2) throw runtime_error("Invalid expression"); long long b = stk.top(); stk.pop(); long long a = stk.top(); stk.pop(); if (token == "+") stk.push(a + b); else if (token == "-") stk.push(a - b); else if (token == "*") stk.push(a * b); else if (token == "/") { if (b == 0) throw runtime_error("Division by zero"); stk.push(a / b); // 截断除法,符合OJ要求 } } else { // 安全转换:检查是否为数字(含负号) bool isNum = true; for (char c : token) { if (!isdigit(c) && c != '-') { isNum = false; break; } } if (!isNum) throw runtime_error("Invalid token: " + token); stk.push(stoll(token)); } } if (stk.size() != 1) throw runtime_error("Invalid expression"); return (int)stk.top(); }3.2.1 输入分割的健壮性处理
OJ输入可能含多余空格(如"3 4 + 2 *"),istringstream自动跳过连续空格,比手动find_first_of(' ')更可靠。若用getline(iss, token, ' '),遇多个空格会读入空字符串,导致stoll("")崩溃。
3.2.2 数值范围与类型选择
题目未限定操作数范围,但OJ测试用例含2^31级大数。int在乘法时易溢出(如100000 * 100000),故栈元素用long long,最终结果再转int。除法使用a / b而非a / (double)b,因OJ要求整数截断(7/2=3,非3.5)。
4. 图与哈希表的工程化实现:邻接表构建与冲突解决策略
图论与哈希表是OJ中后期难点,涉及动态内存分配与复杂逻辑。本节聚焦邻接表到邻接矩阵.cpp和哈希表(链地址法处理冲突).cpp,解析其工程化实现要点。
4.1 邻接表转邻接矩阵:顶点编号映射与稀疏优化
SWUSTOJ图题输入格式为:首行n m(n顶点数,m边数),随后m行u v w(u→v有权重w)。邻接表存储需先建立顶点到索引的映射,因输入顶点名可能为字母(如A B 10)或数字(1 2 5),而邻接矩阵需固定大小二维数组。
#include <unordered_map> #include <vector> #include <algorithm> struct Graph { unordered_map<string, int> name2idx; // 顶点名→索引映射 vector<vector<int>> matrix; // 邻接矩阵,-1表示无边 int n; // 顶点数 Graph(int maxN) : n(maxN) { matrix = vector<vector<int>>(maxN, vector<int>(maxN, -1)); } void addEdge(const string& u, const string& v, int w) { if (name2idx.find(u) == name2idx.end()) { name2idx[u] = name2idx.size(); } if (name2idx.find(v) == name2idx.end()) { name2idx[v] = name2idx.size(); } int i = name2idx[u], j = name2idx[v]; if (i < n && j < n) matrix[i][j] = w; } void printMatrix() { for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (j > 0) cout << " "; cout << matrix[i][j]; } cout << endl; } } };提示:
name2idx.size()动态增长,但matrix初始化时需预估最大顶点数(OJ通常≤100)。若实际顶点数超maxN,matrix[i][j]越界访问将导致RE。安全做法是先扫描输入获取真实顶点数,再构造Graph。
4.2 链地址法哈希表:桶数组与链表节点的内存协同
哈希表题目要求实现插入、查找、删除,冲突用链地址法(每个桶是链表)。关键设计点:
- 桶数组大小需为质数(降低冲突率),OJ常用
997; - 链表节点需包含
key和value,因题目可能要求根据key查value; - 删除操作必须处理头结点与中间节点两种情况。
const int TABLE_SIZE = 997; struct HashNode { string key; int value; HashNode* next; HashNode(const string& k, int v) : key(k), value(v), next(nullptr) {} }; class HashMap { private: vector<HashNode*> buckets; int hash(const string& key) { int h = 0; for (char c : key) h = (h * 31 + c) % TABLE_SIZE; return (h + TABLE_SIZE) % TABLE_SIZE; // 保证非负 } public: HashMap() : buckets(TABLE_SIZE, nullptr) {} void put(const string& key, int value) { int idx = hash(key); HashNode* curr = buckets[idx]; // 查找是否存在key while (curr) { if (curr->key == key) { curr->value = value; // 更新值 return; } curr = curr->next; } // 头插法插入新节点 HashNode* newNode = new HashNode(key, value); newNode->next = buckets[idx]; buckets[idx] = newNode; } int get(const string& key) { int idx = hash(key); HashNode* curr = buckets[idx]; while (curr) { if (curr->key == key) return curr->value; curr = curr->next; } return -1; // 未找到 } void remove(const string& key) { int idx = hash(key); HashNode* curr = buckets[idx]; HashNode* prev = nullptr; while (curr) { if (curr->key == key) { if (prev == nullptr) { // 删除头结点 buckets[idx] = curr->next; } else { // 删除中间节点 prev->next = curr->next; } delete curr; return; } prev = curr; curr = curr->next; } } };4.2.1 哈希函数的选择与冲突率控制
使用h = (h * 31 + c) % TABLE_SIZE是Java String哈希的经典实现,比简单sum ASCII % TABLE_SIZE分布更均匀。TABLE_SIZE = 997(质数)可减少同余冲突。若OJ测试用例含大量相似字符串(如"a", "aa", "aaa"),此哈希函数仍可能聚集,此时需改用std::hash<string>(C++11)。
4.2.2 内存泄漏防护与析构函数
上述代码未提供析构函数,OJ不检查内存,但本地调试需补充:
~HashMap() { for (int i = 0; i < TABLE_SIZE; ++i) { HashNode* curr = buckets[i]; while (curr) { HashNode* next = curr->next; delete curr; curr = next; } } }5. OJ实战调试技巧:用gdb定位段错误与Valgrind检测内存违规
当代码在本地通过但OJ报Runtime Error,90%源于内存违规。本节提供两套可立即上手的调试方案,直击Segmentation fault与Invalid read/write。
5.1 gdb动态调试:精准捕获野指针与越界访问
以单链表的删除操作的实现.cpp为例,若删除不存在的节点导致崩溃,用gdb定位:
# 编译时加调试信息 g++ -g -o delete_node delete_node.cpp # 启动gdb并加载输入文件 gdb ./delete_node (gdb) run < input.txt # 程序崩溃后,查看调用栈 (gdb) bt # 输出类似: # #0 0x0000000000400a12 in deleteNode (head=0x0, pos=1) at delete_node.cpp:25 # #1 0x0000000000400b5c in main () at delete_node.cpp:50 # 查看崩溃行变量值 (gdb) frame 0 (gdb) print head # $1 = (ListNode *) 0x0 (gdb) print pos # $2 = 1关键技巧:
bt(backtrace)显示崩溃位置;frame 0进入最内层函数;nullptr。若head为0x0而代码执行head->next,即确认空指针解引用。
5.2 Valgrind内存检测:发现隐藏的越界与泄漏
Valgrind可检测malloc/new未配对free/delete,以及数组越界:
# 编译时禁用优化(-O0)确保行号准确 g++ -O0 -g -o tree tree.cpp # 运行Valgrind valgrind --leak-check=full --show-leak-kinds=all ./tree < input.txt # 典型输出: # ==12345== Invalid write of size 4 # ==12345== at 0x400A2F: buildTree() (tree.cpp:45) # ==12345== by 0x400B1C: main (tree.cpp:88) # ==12345== Address 0x5a1c044 is 0 bytes after a block of size 4 alloc'd # ==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) # ==12345== by 0x400A1A: buildTree() (tree.cpp:42)此输出明确指出:第45行对刚分配的4字节内存进行了越界写(0 bytes after),根源在第42行malloc尺寸不足。OJ环境无Valgrind,但本地检测可提前规避90%的RE。
5.3 OJ特供调试法:输出中间状态到stderr
当无法使用gdb/Valgrind时(如在线IDE),用cerr输出关键变量,OJ不捕获stderr,不影响判题:
// 在关键指针操作前插入 cerr << "[DEBUG] curr=" << curr << ", curr->next=" << (curr ? curr->next : nullptr) << endl; // 或输出链表当前状态 void printList(ListNode* head) { cerr << "[LIST] "; while (head) { cerr << head->data << "->"; head = head->next; } cerr << "null" << endl; }运行后查看OJ的"运行信息"页(非"输出"页),可看到cerr内容,快速定位空指针或环形链表。
本文还有配套的精品资源,点击获取