题目:Rot13
语言:C
难度:5 kyu
分析日期:2026-10-18
用户代码来源:用户在本次对话中直接粘贴,属于 user pasted 证据。
1. 题目要求
实现:
char *rot13(const char *src);
函数需要返回一个新字符串,对输入中的 ASCII 英文字母进行 ROT13:
A <-> N
B <-> O
...
M <-> Z
a <-> n
b <-> o
...
m <-> z
非字母字符必须原样保留:
空格、数字、标点、换行等都不能被删除或转换
Codewars 测试会释放函数返回的字符串,因此函数必须使用动态内存分配,并返回一个以 '\0' 结尾的合法 C 字符串。
2. ROT13 的数学模型
ROT13 本质上是长度为 26 的字母循环:
字母编号 = 当前字符 - 起始字母
新编号 = (字母编号 + 13) % 26
新字符 = 新编号 + 起始字母
大写字母:
result = (src[i] - 'A' + 13) % 26 + 'A';
小写字母:
result = (src[i] - 'a' + 13) % 26 + 'a';
使用模运算后,不需要分别写"是否超过 Z"和"是否超过 z"两个分支。
3. 你的版本演进
你的代码经历了:
初版:只扫描字符串长度,尚未分配和生成结果
↓
第二版:加入动态数组和大小写转换
↓
最终版:修正空间大小、显式写入终止符,并保留非字母
4. 初版分析
初版代码:
#include <stdlib.h>
char *rot13(const char *src)
{
// returned string will be freed by the tests:
int i = 0;
while (src[i] != '\0') {
i++;
}
这段代码目前只完成了第一步:计算输入字符串长度。
已经做对的地方
while (src[i] != '\0') {
i++;
}
循环结束时:
i == strlen(src)
这为后续分配 i + 1 个字符提供了基础。
尚未完成的问题
初版还缺少:
-
为返回字符串分配空间;
-
遍历
src并写入转换结果; -
保留非字母字符;
-
写入最后的
'\0'; -
return re; -
函数和代码块的完整闭合。
因此初版不是一个完整的可提交实现。
5. 第二版分析
你第二版的核心片段是:
char *re = calloc(i, sizeof(char *));
while (src[i] != '\0') {
if (src[i] > 'A' && src[i] < 'Z') {
if (src[i] + 13 <= 'Z') {
re[i] = (char)(src[i] + 13);
} else {
re[i] = (char)(src[i] + 13 - 'Z' - 1 + 'A');
}
}
if (src[i] > 'a' && src[i] < 'z') {
if (src[i] + 13 <= 'z') {
re[i] = (char)(src[i] + 13);
} else {
re[i] = (char)(src[i] + 13 - 'z' - 1 + 'A');
}
}
i++;
}
return re;
这版同时存在几个独立问题。
5.1 没有把 i 重置为 0
如果这是一个完整函数,那么前面已经执行过:
while (src[i] != '\0') {
i++;
}
此时 i 已经等于字符串长度,src[i] 正好是 '\0'。
因此后面的:
while (src[i] != '\0')
会一次也不执行。
分配空间后必须先写:
i = 0;
5.2 > 和 < 排除了边界字母
第二版写的是:
src[i] > 'A' && src[i] < 'Z'
src[i] > 'a' && src[i] < 'z'
这不包含:
'A'、'Z'、'a'、'z'
正确判断必须使用:
src[i] >= 'A' && src[i] <= 'Z'
src[i] >= 'a' && src[i] <= 'z'
最小反例:
输入:"AZaz"
期望:"NMnm"
第二版:边界字符没有被正确转换
5.3 小写回绕时使用了大写起点
第二版的小写回绕写成:
src[i] + 13 - 'z' - 1 + 'A'
这里最后应该使用小写 'a',而不是大写 'A':
src[i] + 13 - 'z' - 1 + 'a'
否则:
'z' -> 'N'
而正确结果应该是:
'z' -> 'm'
5.4 非字母字符没有被复制
当字符是空格或数字时,两个 if 都不成立:
if (大写) { ... }
if (小写) { ... }
此时 re[i] 没有被写入。
由于你使用了 calloc,未写入位置通常保持为 '\0'。但 '\0' 是 C 字符串终止符,因此输出会被提前截断。
例如:
输入:"C is cool!"
处理过程:
'C' -> 'P',re[0] = 'P'
' ' 未赋值,re[1] = '\0'
调用者看到的结果可能只有:
"P"
正确做法是显式保留非字母:
else {
re[i] = src[i];
}
5.5 calloc(i, sizeof(char *)) 类型不正确
第二版写的是:
calloc(i, sizeof(char *))
但数组元素类型是 char,应写成:
calloc(i + 1, sizeof(char))
或者更推荐:
malloc((i + 1) * sizeof(*re))
sizeof(char *) 通常会比 sizeof(char) 大,因此在很多平台上表现为"多分配了空间",但这不是正确的类型表达,而且没有解决终止符所需的额外一个字节。
5.6 没有明确写入字符串终止符
返回字符串必须满足:
re[length] = '\0';
不能把字符串是否结束寄托在 calloc 的默认零值上。即使当前内存初始化碰巧让测试通过,明确写入终止符才是正确的字符串构造方式。
6. 最终版分析
你的最终版是:
#include <stdlib.h>
char *rot13(const char *src)
{
// returned string will be freed by the tests:
int i = 0;
while (src[i] != '\0') {
i++;
}
char *re = calloc(i + 1, sizeof(char));
i = 0;
while (src[i] != '\0') {
if (src[i] >= 'A' && src[i] <= 'Z') {
if (src[i] + 13 <= 'Z') {
re[i] = (char)(src[i] + 13);
} else {
re[i] = (char)(src[i] + 13 - 'Z' - 1 + 'A');
}
} else if (src[i] >= 'a' && src[i] <= 'z') {
if (src[i] + 13 <= 'z') {
re[i] = (char)(src[i] + 13);
} else {
re[i] = (char)(src[i] + 13 - 'z' - 1 + 'a');
}
} else {
re[i] = src[i];
}
i++;
}
re[i] = '\0';
return re;
}
6.1 最终版已经修复了空格截断问题
你在文字说明中说:
非字母字符不会被赋值,calloc 会留下 '\0',字符串会截断。
但这描述的是第二版,而不是你贴出的最终版。
最终版已经有:
else {
re[i] = src[i];
}
所以:
空格、数字、标点都会被原样复制
例如:
"C is cool!" -> "P vf pbby!"
最终版在题目常规输入下已经解决了这个主要 bug。
6.2 最终版做对的地方
| 代码 | 作用 | 评价 |
|---|---|---|
calloc(i + 1, sizeof(char)) |
为正文和终止符分配空间 | 正确 |
i = 0 |
重新从源字符串开头扫描 | 正确 |
>=、<= |
包含 A/Z/a/z |
正确 |
else if |
大写、小写、非字母三类互斥 | 正确 |
else { re[i] = src[i]; } |
保留非字母 | 关键修复 |
re[i] = '\0' |
显式结束字符串 | 正确 |
return re |
返回动态分配的字符串 | 符合题目约定 |
6.3 最终版仍可改进的地方
分配失败检查
当前代码直接使用:
char *re = calloc(i + 1, sizeof(char));
如果内存分配失败,re 会是 NULL,后续写入会产生未定义行为。
更稳妥的生产代码应检查:
if (re == NULL) {
return NULL;
}
Codewars 通常不会专门测试内存分配失败,但这是 C 代码中的工程安全点。
使用 size_t 表示字符串长度
当前使用:
int i;
标准字符串长度和内存大小更适合使用 size_t。这样可以避免在极长字符串上使用 int 表示长度的限制。
可以用模运算减少分支
当前写法分别处理"加 13 后是否超过 Z":
if (src[i] + 13 <= 'Z') {
...
} else {
...
}
这种写法并非错误,但可以统一写成:
re[i] = (char)('A' + (src[i] - 'A' + 13) % 26);
这样更直接地表达了 ROT13 的数学定义,也避免手写回绕公式。
7. 推荐实现
下面的版本保留题目要求的函数签名,使用 size_t、模运算、非字母原样复制,并检查分配失败:
#include <stddef.h>
#include <stdlib.h>
char *rot13(const char *src)
{
size_t length = 0;
size_t i;
char *result;
while (src[length] != '\0') {
++length;
}
result = malloc((length + 1) * sizeof(*result));
if (result == NULL) {
return NULL;
}
for (i = 0; i < length; ++i) {
unsigned char c = (unsigned char)src[i];
if (c >= 'A' && c <= 'Z') {
result[i] = (char)('A' + (c - 'A' + 13) % 26);
} else if (c >= 'a' && c <= 'z') {
result[i] = (char)('a' + (c - 'a' + 13) % 26);
} else {
result[i] = src[i];
}
}
result[length] = '\0';
return result;
}
8. 控制流图
flowchart TD
A[扫描 src 得到 length] --> B[分配 length + 1 个 char]
B --> C{分配成功?}
C -->|否| D[返回 NULL]
C -->|是| E[逐字符扫描]
E --> F{大写字母?}
F -->|是| G[按大写字母表做 ROT13]
F -->|否| H{小写字母?}
H -->|是| I[按小写字母表做 ROT13]
H -->|否| J[原样复制非字母]
G --> K{还有字符?}
I --> K
J --> K
K -->|是| E
K -->|否| L[写入 '\0']
L --> M[返回动态字符串]
9. 正确性说明
对每个输入位置 i,算法只会进入三条互斥路径之一:
大写字母
若:
'A' <= src[i] && src[i] <= 'Z'
则:
result[i] = 'A' + (src[i] - 'A' + 13) % 26;
结果仍然位于 'A' 到 'Z',并且向后移动 13 位。
小写字母
若:
'a' <= src[i] && src[i] <= 'z'
则:
result[i] = 'a' + (src[i] - 'a' + 13) % 26;
结果仍然位于 'a' 到 'z',并保持小写。
非字母
否则:
result[i] = src[i];
因此空格、数字、标点和其他字符不会改变,也不会因为默认的 '\0' 导致输出提前结束。
最后:
result[length] = '\0';
确保返回值是合法的 C 字符串。
10. 复杂度分析
设输入长度为 n。
| 项目 | 结果 |
|---|---|
| 第一次扫描长度 | O(n) |
| 第二次转换字符 | O(n) |
| 总时间复杂度 | O(n) |
| 结果字符串空间 | O(n) |
| 除结果外的额外工作空间 | O(1) |
必须分配 O(n) 的结果空间,因为函数需要返回一份新的字符串。
11. 测试矩阵
| 输入 | 期望输出 | 覆盖点 |
|---|---|---|
"" |
"" |
空字符串 |
"A" |
"N" |
大写前半段 |
"N" |
"A" |
大写回绕 |
"Z" |
"M" |
大写边界 |
"a" |
"n" |
小写前半段 |
"n" |
"a" |
小写回绕 |
"z" |
"m" |
小写边界 |
"Hello" |
"Uryyb" |
普通单词 |
"C is cool!" |
"P vf pbby!" |
空格与标点保留 |
"12345" |
"12345" |
数字原样保留 |
"A1 B2!" |
"N1 O2!" |
字母、数字、空格、标点混合 |
"abcdefghijklmnopqrstuvwxyz" |
"nopqrstuvwxyzabcdefghijklm" |
全部小写 |
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
"NOPQRSTUVWXYZABCDEFGHIJKLM" |
全部大写 |
"a-z_AZ" |
"n-m_NM" |
标点和下划线保留 |
针对第二版缺陷的最小回归测试:
"AZaz" -> "NMnm" // 检查边界字母
"z" -> "m" // 检查小写回绕使用 'a'
"C is cool!" -> "P vf pbby!" // 检查非字母不能留下 '\0'
12. 版本对比
| 版本 | 主要状态 | 结论 |
|---|---|---|
| 初版 | 只完成长度扫描,未构造返回字符串 | 未完成 |
| 第二版 | 加入转换,但未重置 i、排除边界字母、非字母未复制、大小写回绕有错误 |
不正确 |
| 最终版 | 修复 i = 0、i + 1、边界判断、非字母复制和终止符 |
题目常规输入下基本正确 |
| 推荐版 | 使用 size_t、模运算和分配失败检查 |
更清晰、更稳健 |
13. 结论
你指出的"非字母字符导致 calloc 中的 '\0' 让字符串截断"确实是第二版的关键问题。
但你贴出的最终版已经加入:
else {
re[i] = src[i];
}
因此最终版已经修复了这个问题。最终版在 Codewars 的常规测试范围内基本可以通过;推荐版主要是进一步改善表达清晰度、边界处理和内存分配失败时的安全性。
最值得保留的核心结构是:
扫描长度
-> 分配 length + 1
-> 字母按大小写做 ROT13
-> 非字母原样复制
-> 写入 '\0'
-> 返回
来源
-
用户在本次对话中粘贴的初版、第二版和最终版 C 代码:用于个人实现评审。