Codewars:Rot13(C)解题分析

题目: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 个字符提供了基础。

尚未完成的问题

初版还缺少:

  1. 为返回字符串分配空间;

  2. 遍历 src 并写入转换结果;

  3. 保留非字母字符;

  4. 写入最后的 '\0';

  5. return re;

  6. 函数和代码块的完整闭合。

因此初版不是一个完整的可提交实现。

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'
  -> 返回

来源

  1. Codewars Rot13 题目页面

  2. Codewars Challenge API

  3. 用户在本次对话中粘贴的初版、第二版和最终版 C 代码:用于个人实现评审。

相关推荐
鸽鸽程序猿1 小时前
【项目】【在线判题系统】b端题目增删改
开发语言·python
Lhappy嘻嘻1 小时前
读懂 JVM:内存模型、类加载机制、分代回收
java·开发语言·jvm
Code_Solitude2 小时前
c语言:关于qsoet函数知识点
c语言·数据结构·算法
weixin_307779132 小时前
C++代码实现MATLAB中的spectralcluster函数功能
开发语言·c++·算法·matlab
鸽鸽程序猿2 小时前
【项目】【在线判题系统】c端:竞赛报名
c语言·开发语言
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 个人资料管理的需求分析与接口设计
java·开发语言·交友
1024奇点2 小时前
【仓颉语言入门 · 第二篇 第 8 课】
开发语言·数据库·ide·mysql·开源
gopher95113 小时前
RUST:序列化与反序列化 serde + derive 详解
开发语言·后端·rust
H.莓飛3 小时前
【Linux】自主 Shell
linux·运维·服务器·c语言·开发语言·visual studio