从 AC 到优雅:C 语言经典 OJ 题《潜伏者》代码优化全记录
一、前言很多初学 C 语言的小伙伴刷 OJ 时都会陷入一个误区只要代码能 AC、能跑通就万事大吉。但往往自己写的代码过半个月再回头看连自己都看不懂满屏的arr1/arr2/arr3、冗余的变量、东拼西凑的逻辑、全靠注释才勉强读懂的功能。今天我们就拿一道经典的字符串映射入门题为例从一份能稳定 AC 的基础代码出发一步步拆解优化全程不改变核心逻辑新手也能看懂最终得到一份规范、优雅、易读、易维护的代码。二、原题完整介绍这是一道经典的密码破译与字符映射题题目描述如下一题目背景R 国和 S 国正陷入战火之中双方都互派间谍潜入对方内部伺机行动。历尽艰险后潜伏于 S 国的 R 国间谍小 C 终于摸清了 S 国军用密码的编码规则。二题目描述S 国军方内部欲发送的原信息经过加密后在网络上发送原信息的内容与加密后所得的内容均由大写字母 A∼Z 构成无空格等其他字符。S 国对于每个字母规定了对应的密字。加密的过程就是将原信息中的所有字母替换为其对应的密字。密码规则必须严格遵守每个字母只对应一个唯一的密字。不同的字母对应不同的密字。密字可以和原字母相同。现在小 C 通过内线掌握了 S 国网络上发送的一条加密信息及其对应的原信息。小 C 希望能通过这条信息破译 S 国的军用密码。破译过程中如果出现以下情况之一直接判定为失败所有信息扫描完毕但发现存在某个或某些字母在原信息中没有出现即没有收集齐 A~Z 全部 26 个字母的映射关系。扫描中发现掌握的信息里有明显的自相矛盾或错误违反上述 S 国密码的编码规则。如果破译成功请利用破译的密码翻译另外一条从 S 国刚刚截取到的加密信息。三输入格式共三行每行为一个长度在 1 到 100 之间的字符串。第一行为小 C 掌握的一条加密信息。第二行为第一行的加密信息所对应的原信息。第三行为 R 国司令部要求小 C翻译的加密信息。输入数据保证所有字符串仅由大写字母 A∼Z 构成且第一行长度与第二行相等。四输出格式共一行。若破译密码失败请输出Failed。否则请输出利用密码翻译电报中加密信息后得到的原信息。三、最初的 AC 代码先看我们最开始写的、能稳定通过所有测试用例的基础版代码。逻辑完全正确但有很多可以优化的空间#includestdio.h #include string.h void print(char arr[], int num)//输入字符串 { fgets(arr, num, stdin); arr[strcspn(arr, \n)] \0; } int Jud1(char arr[], int num)//检测26个字母是否出现 { int arr1[26] { 0 }; int i 0; while (arr[i] ! \0) { arr1[(arr[i]-A)]; i; } for (int i 0; i 26; i) { if (arr1[i] 0) { return 1; } } return 0; } int Jud2(char arr2[], int M,char arr1[], int N, char arr[256])//判断自相矛盾或错误 { if (M ! N) return 1; char used[256] { 0 }; for (int i 0; i M; i) { if (used[arr1[i]] 1) return 1; //防止多对一 used[arr1[i]] 1; if (arr[(arr2[i])] 0) { arr[(arr2[i])]arr1[i]; } else if (arr[(arr2[i])] ! arr1[i]) { return 1;//防止一对多 } } return 0; } void Last(char arr1[],char arr2[], int num)//对照表加密字符数组字符数量 { char arrlast[101] {0}; int i; for (i 0; arr2[i] ! \0; i) { arrlast[i] arr1[(arr2[i])]; } arrlast[i] \0; printf(%s, arrlast); } int main() { char arr1[101] { 0 };//第一行为小 C 掌握的一条加密信息 char arr2[101] { 0 };//第二行为第一行的加密信息所对应的原信息 char arr3[101] { 0 };//第三行为 R 国司令部要求小 C 翻译的加密信息。 char arr[256] { 0 };//对照表 int M 0; print(arr1, 101); print(arr2, 101); print(arr3, 101); int fail1 Jud1(arr2, 101); if (fail1 1) { printf(Failed); return 0; } int N Jud2(arr1, strlen(arr1), arr2, strlen(arr2), arr); if (N 1) { printf(Failed); return 0; } Last(arr, arr3, 101); return 0; }四、源代码逻辑拆解虽然这份代码看起来有点乱但它的解题思路是非常清晰且完全正确的。我们来拆解一下它的核心逻辑一整体解题流程读取数据读取题目给出的三行字符串。规则校验 1完整性检查 “原信息” 是否包含了 A~Z 全部 26 个字母。如果缺了直接输出Failed。规则校验 2唯一性检查 “密文” 和 “原文” 的映射关系是否矛盾。不能 “一对多”同一个密文字母不能对应两个不同的原文字母。不能 “多对一”两个不同的密文字母不能对应同一个原文字母。如果出现矛盾直接输出Failed。翻译输出如果校验全部通过利用建立好的映射表翻译第三行字符串并输出。二函数功能详解为了方便后续优化对比我们先给这份代码的四个函数做个 “身份认证”print(char arr[], int num)功能安全的输入函数。逻辑使用fgets读取一行并用strcspn去掉末尾的换行符\n防止后续处理出错。Jud1(char arr[], int num)功能校验 “完整性”。逻辑开一个大小为 26 的数组arr1遍历字符串统计每个字母出现的次数。最后检查数组里是不是 26 个位置都有计数即都不为 0。Jud2(char arr2[], int M, char arr1[], int N, char arr[256])功能校验 “唯一性” 并建立映射表。逻辑先检查两个字符串长度是否一致。开一个used数组防止 “多对一”确保每个原文只用了一次。开一个arr数组即主函数里的对照表防止 “一对多” 并建立映射关系。Last(char arr1[], char arr2[], int num)功能翻译并输出。逻辑开一个临时数组arrlast遍历待翻译字符串查表后把结果存入临时数组最后统一打印。五、逐点优化不改变逻辑只提升代码质量我们的优化原则很明确不改变上面拆解的核心解题逻辑只做规范、可读性、简洁性的提升优化后的代码必须和原版完全等价稳定 AC。优化 1语义化命名告别「arr 地狱」这是最基础、也最能提升代码可读性的优化。优化前char arr1[101] { 0 };//第一行为小 C 掌握的一条加密信息 char arr2[101] { 0 };//第二行为第一行的加密信息所对应的原信息 char arr3[101] { 0 };//第三行为 R 国司令部要求小 C 翻译的加密信息。 char arr[256] { 0 };//对照表 int Jud2(char arr2[], int M,char arr1[], int N, char arr[256])1.1优化后char mi[101] {0}; // 已知加密信息密文 char yuan[101] {0}; // 加密信息对应的原信息明文 char ask[101] {0}; // 需要翻译的目标加密串 char map[256] {0}; // 密文→明文映射对照表 // 函数参数也同步语义化 int Jud2(char mi[], int len_mi, char yuan[], int len_yuan, char map[256])1.2为什么这么改原版的arr1/arr2/arr3必须靠注释才能区分用途一旦注释丢失、或者传参时写反直接出现致命 bug我们最开始就踩过arr1和arr2传反的坑。改成语义化命名后哪怕没有注释也能一眼看懂每个变量的用途从根源上减少传参错误、逻辑混淆的问题代码可读性直接翻倍。优化 2砍掉冗余参数和无效变量代码里的无效内容不仅会增加阅读负担还会埋下 bug 隐患我们直接全部清理。① 去掉无用的函数参数Jud1函数的num参数定义了但全程没有用到 ——C 语言的字符串以\0结尾我们直接遍历到结束即可不需要额外传数组长度。// 优化前 int Jud1(char arr[], int num) // 优化后 int Jud1(char arr[])② 删掉无效变量main函数里定义的int M 0;全程没有被使用属于无效变量直接删掉。③ 简化布尔判断C 语言里非 0 值即为真我们不需要写if (fail1 1)直接简化为if (fail1)更符合 C 语言的常规写法。// 优化前 int fail1 Jud1(arr2, 101); if (fail1 1) // 优化后 if (Jud1(yuan))优化 3简化逻辑去掉无效计算我们的核心需求是「判断字母是否出现过」而不是「统计字母出现的次数」原版代码做了多余的加法计算我们可以直接简化。3.1优化前int Jud1(char arr[], int num) { int arr1[26] { 0 }; int i 0; while (arr[i] ! \0) { arr1[(arr[i]-A)]; // 多余的计数加法 i; } for (int i 0; i 26; i) { if (arr1[i] 0) return 1; } return 0; }3.2优化后int Jud1(char arr[]) { int cnt[26] {0}; // while改for把变量定义、遍历、结束条件整合更紧凑 for (int i 0; arr[i]; i) cnt[arr[i] - A] 1; // 只标记「是否出现过」无多余计算 for (int i 0; i 26; i) if (cnt[i] 0) return 1; return 0; }3.3为什么这么改我们只需要知道字母有没有出现不需要知道出现了几次直接赋值1标记即可省去了加法运算逻辑更直接代码更紧凑。同时把while循环改成for循环符合 C 语言字符串遍历的常规写法可读性更强。优化 4简化输出逻辑减少不必要的内存开销原版的Last翻译函数定义了一个临时数组存储翻译结果再统一输出。但我们完全不需要存储翻译一个字符就直接输出一个省去了不必要的内存开销。4.1优化前void Last(char arr1[],char arr2[], int num) { char arrlast[101] {0}; int i; for (i 0; arr2[i] ! \0; i) { arrlast[i] arr1[(arr2[i])]; } arrlast[i] \0; printf(%s, arrlast); }4.2优化后void Last(char map[], char s[]) { for (int i 0; s[i]; i) putchar(map[s[i]]); }4.3为什么这么改省去了 101 字节的临时数组开销虽然对现代电脑来说微不足道但这是嵌入式开发、资源受限场景下的好习惯代码从 8 行简化到 3 行逻辑更直接彻底避免了「字符串结束符漏写」的潜在 bug不会出现乱码问题优化 5注释精准化拒绝无效注释很多初学者会陷入每行都写注释的误区但好的代码本身就是注释注释只需要补充代码说不清楚的关键信息。5.1优化前int M 0; print(arr1, 101); print(arr2, 101); print(arr3, 101); int fail1 Jud1(arr2, 101); if (fail1 1) { printf(Failed); return 0; }5.2优化后// 读取三行输入 print(mi, 101); print(yuan, 101); print(ask, 101); // 校验1明文必须包含A~Z全部26个字母 if (Jud1(yuan)) { printf(Failed); return 0; } // 校验2密码映射不能一对多/多对一 if (Jud2(mi, strlen(mi), yuan, strlen(yuan), map)) { printf(Failed); return 0; }5.3为什么这么改删掉了废话注释只在逻辑分段、核心校验的地方加注释一眼就能看懂代码的结构注释和语义化命名配合哪怕是第一次看代码的人也能快速理清解题流程六、优化后的最终代码// 潜伏者密码翻译密文-明文一一对应校验 翻译 #includestdio.h #include string.h // 读取一行字符串自动去除fgets读取的换行符 void print(char arr[], int num) { fgets(arr, num, stdin); arr[strcspn(arr, \n)] \0; } // 校验字符串是否包含A~Z全部26个大写字母 // 返回值0包含全部1缺少字母 int Jud1(char arr[]) { int cnt[26] {0}; for (int i 0; arr[i]; i) cnt[arr[i] - A] 1; for (int i 0; i 26; i) if (cnt[i] 0) return 1; return 0; } // 校验密码映射是否存在矛盾一对多/多对一 // 返回值0校验通过1存在矛盾 int Jud2(char mi[], int len_mi, char yuan[], int len_yuan, char map[256]) { if (len_mi ! len_yuan) return 1; char used[256] {0}; for (int i 0; i len_mi; i) { // 禁止多对一同一个明文不能对应多个密文 if (used[yuan[i]]) return 1; used[yuan[i]] 1; // 禁止一对多同一个密文不能对应多个明文 if (map[mi[i]] 0) map[mi[i]] yuan[i]; else if (map[mi[i]] ! yuan[i]) return 1; } return 0; } // 翻译并输出结果 void Last(char map[], char s[]) { for (int i 0; s[i]; i) putchar(map[s[i]]); } int main() { char mi[101] {0}; // 已知加密信息密文 char yuan[101] {0}; // 加密信息对应的原信息明文 char ask[101] {0}; // 需要翻译的目标加密串 char map[256] {0}; // 密文→明文映射对照表 // 读取三行输入 print(mi, 101); print(yuan, 101); print(ask, 101); // 校验1明文必须包含A~Z全部26个字母 if (Jud1(yuan)) { printf(Failed); return 0; } // 校验2密码映射不能一对多/多对一 if (Jud2(mi, strlen(mi), yuan, strlen(yuan), map)) { printf(Failed); return 0; } // 输出翻译结果 Last(map, ask); return 0; }七、优化总结给初学者的 4 个建议很多人觉得「优化是炫技」但对初学者来说优化的核心不是把代码写得越短越好而是养成良好的编码习惯让代码更易读、更易维护、更少出错。先求 AC再求优雅永远先把代码写对、跑通所有测试用例再谈优化。不要上来就追求极简代码很容易陷入「为了短而牺牲可读性」的误区。命名是代码的第一要务见名知意的变量名、函数名能帮你减少 80% 的低级 bug。告别arr1/a/b/x这种无意义的命名哪怕多打几个字母也能给后续的调试、维护省大量时间。冗余是 bug 的温床没用的变量、参数、代码直接删掉。代码越干净逻辑链路越短越不容易出现隐藏的 bug。注释要精准不要写废话不要写i;//i自增1这种废话注释注释只用来补充「代码本身说不清楚的信息」比如函数功能、核心规则、容易踩坑的边界条件。八、结尾刷 OJ 题的核心从来不是「AC 了就完事」而是通过每一道题锻炼自己的逻辑思维和编码规范。今天我们用这道密码破译题完成了从「能跑」到「优雅」的全流程优化全程没有改变核心逻辑却让代码的可读性、规范性提升了一个档次。大家刷 OJ 的时候有没有过类似的优化经历或者有什么自己的编码小技巧欢迎在评论区交流