从 AC 到优雅:C 语言经典 OJ 题《潜伏者》代码优化全记录

📅 2026/7/14 7:29:15 👁️ 阅读次数 📝 编程学习
从 AC 到优雅:C 语言经典 OJ 题《潜伏者》代码优化全记录

一、前言

很多初学 C 语言的小伙伴刷 OJ 时,都会陷入一个误区:只要代码能 AC、能跑通,就万事大吉。但往往自己写的代码,过半个月再回头看,连自己都看不懂:满屏的arr1/arr2/arr3、冗余的变量、东拼西凑的逻辑、全靠注释才勉强读懂的功能。

今天我们就拿一道经典的字符串映射入门题为例,从一份能稳定 AC 的基础代码出发,一步步拆解优化,全程不改变核心逻辑,新手也能看懂,最终得到一份规范、优雅、易读、易维护的代码。


二、原题完整介绍

这是一道经典的密码破译与字符映射题,题目描述如下:

(一)题目背景

R 国和 S 国正陷入战火之中,双方都互派间谍,潜入对方内部,伺机行动。历尽艰险后,潜伏于 S 国的 R 国间谍小 C 终于摸清了 S 国军用密码的编码规则。

(二)题目描述

S 国军方内部欲发送的原信息经过加密后在网络上发送,原信息的内容与加密后所得的内容均由大写字母 A∼Z 构成(无空格等其他字符)。S 国对于每个字母规定了对应的密字。加密的过程就是将原信息中的所有字母替换为其对应的密字。

密码规则(必须严格遵守):

  1. 每个字母只对应一个唯一的密字。
  2. 不同的字母对应不同的密字。
  3. 密字可以和原字母相同。

现在,小 C 通过内线掌握了 S 国网络上发送的一条加密信息及其对应的原信息。小 C 希望能通过这条信息,破译 S 国的军用密码。

破译过程中如果出现以下情况之一,直接判定为失败:

  1. 所有信息扫描完毕,但发现存在某个(或某些)字母在原信息中没有出现(即没有收集齐 A~Z 全部 26 个字母的映射关系)。
  2. 扫描中发现掌握的信息里有明显的自相矛盾或错误(违反上述 S 国密码的编码规则)。
  3. 如果破译成功,请利用破译的密码,翻译另外一条从 S 国刚刚截取到的加密信息。

(三)输入格式

共三行,每行为一个长度在 1 到 100 之间的字符串。

  1. 第一行:为小 C 掌握的一条加密信息
  2. 第二行:为第一行的加密信息所对应的原信息
  3. 第三行:为 R 国司令部要求小 C翻译的加密信息
  4. 输入数据保证所有字符串仅由大写字母 A∼Z 构成,且第一行长度与第二行相等。

(四)输出格式

共一行。若破译密码失败,请输出Failed。否则请输出利用密码翻译电报中加密信息后得到的原信息。


三、最初的 AC 代码

先看我们最开始写的、能稳定通过所有测试用例的基础版代码。逻辑完全正确,但有很多可以优化的空间:

#include<stdio.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. 读取数据:读取题目给出的三行字符串。
  2. 规则校验 1(完整性):检查 “原信息” 是否包含了 A~Z 全部 26 个字母。如果缺了,直接输出Failed
  3. 规则校验 2(唯一性):检查 “密文” 和 “原文” 的映射关系是否矛盾。
    • 不能 “一对多”:同一个密文字母不能对应两个不同的原文字母。
    • 不能 “多对一”:两个不同的密文字母不能对应同一个原文字母。
    • 如果出现矛盾,直接输出Failed
  4. 翻译输出:如果校验全部通过,利用建立好的映射表,翻译第三行字符串并输出。

(二)函数功能详解

为了方便后续优化对比,我们先给这份代码的四个函数做个 “身份认证”:

  1. print(char arr[], int num)
    • 功能:安全的输入函数。
    • 逻辑:使用fgets读取一行,并用strcspn去掉末尾的换行符(\n),防止后续处理出错。
  2. Jud1(char arr[], int num)
    • 功能:校验 “完整性”。
    • 逻辑:开一个大小为 26 的数组arr1,遍历字符串,统计每个字母出现的次数。最后检查数组里是不是 26 个位置都有计数(即都不为 0)。
  3. Jud2(char arr2[], int M, char arr1[], int N, char arr[256])
    • 功能:校验 “唯一性” 并建立映射表。
    • 逻辑
      • 先检查两个字符串长度是否一致。
      • 开一个used数组,防止 “多对一”(确保每个原文只用了一次)。
      • 开一个arr数组(即主函数里的对照表),防止 “一对多” 并建立映射关系。
  4. 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(我们最开始就踩过arr1arr2传反的坑)。

改成语义化命名后,哪怕没有注释,也能一眼看懂每个变量的用途,从根源上减少传参错误、逻辑混淆的问题,代码可读性直接翻倍。

优化 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为什么这么改?
  • 删掉了废话注释,只在逻辑分段、核心校验的地方加注释,一眼就能看懂代码的结构
  • 注释和语义化命名配合,哪怕是第一次看代码的人,也能快速理清解题流程

六、优化后的最终代码

// 潜伏者密码翻译:密文<->明文一一对应校验 + 翻译 #include<stdio.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 个建议

很多人觉得「优化是炫技」,但对初学者来说,优化的核心不是把代码写得越短越好,而是养成良好的编码习惯,让代码更易读、更易维护、更少出错

  1. 先求 AC,再求优雅永远先把代码写对、跑通所有测试用例,再谈优化。不要上来就追求极简代码,很容易陷入「为了短而牺牲可读性」的误区。

  2. 命名是代码的第一要务见名知意的变量名、函数名,能帮你减少 80% 的低级 bug。告别arr1/a/b/x这种无意义的命名,哪怕多打几个字母,也能给后续的调试、维护省大量时间。

  3. 冗余是 bug 的温床没用的变量、参数、代码,直接删掉。代码越干净,逻辑链路越短,越不容易出现隐藏的 bug。

  4. 注释要精准,不要写废话不要写i++;//i自增1这种废话注释,注释只用来补充「代码本身说不清楚的信息」:比如函数功能、核心规则、容易踩坑的边界条件。

八、结尾

  1. 刷 OJ 题的核心,从来不是「AC 了就完事」,而是通过每一道题,锻炼自己的逻辑思维和编码规范。
  2. 今天我们用这道密码破译题,完成了从「能跑」到「优雅」的全流程优化,全程没有改变核心逻辑,却让代码的可读性、规范性提升了一个档次。
  3. 大家刷 OJ 的时候,有没有过类似的优化经历?或者有什么自己的编码小技巧?欢迎在评论区交流~