题目描述
给定一段 "密文" 字符串 s,其中字符都是经过 "密码本" 映射的,现需要将 "密文" 解密并输出。映射的规则('a'~'i')分别用('1'~'9')表示;('j'~'z')分别用("10"~"26")表示。约束:映射始终唯一。
输入描述
"密文" 字符串
输出描述
明文字符串
备注
翻译后的文本长度在 100 以内
用例
| 输入 | 20**19**20* |
| 输出 | tst |
| 说明 | 无 |
解法一:调用库函数find+replace
该题的主要难点在与字符'j'-'z'的替换,涉及到的字符串是"10*"~"26*",要处理"*"问题,如果能首先替换输入字符串s中的所有"10*"~"26*",接下来只需要替换'a'~'i',而且这两步替换不冲突,所以这种方法具有可行性,步骤如下:
- 将"10*"~"26*"分别映射到'j'-'z',并遍历字符串s进行替换,得到新字符串s',此时字符串中可能存在数字、'*'、字母以及其他符号;
- 将新字符串中的'1'~'9'分别替换为'j'-'z',输出即可。
时间复杂度:潜在 O(n²)。
cpp
#include <iostream>
#include <sstream>
#include <vector>
using namespace std;
/**
* @brief 字符串替换函数,将源字符串中的所有旧子串替换为新子串
*
* @param str 引用传递的源字符串,会被直接修改
* @param oldStr 要被替换的旧子串(常量引用,不修改)
* @param newStr 替换用的新子串(常量引用,不修改)
*/
void strreplace(string& str, const string& oldStr, const string& newStr) {
// 初始化查找位置为字符串开头
size_t pos = 0;
// 循环查找旧子串,直到找不到为止
while ((pos = str.find(oldStr, pos)) != string::npos) {
// 从pos位置开始,将长度为oldStr.length()的子串替换为newStr
str.replace(pos, oldStr.length(), newStr);
// 更新下一次查找的起始位置为替换后的新子串末尾
pos += newStr.length();
}
}
int main() {
string str;
getline(cin, str);
// 创建一个密码对照表,用于存储字符到加密字符串的映射关系
vector<pair<char, string>> secretCode;
// 循环生成10到26的加密规则
for (int i = 10; i <= 26; i++) {
char c = i + 'a' - 1;
// 生成加密字符串:数字后面跟一个*,例如"10*", "11*", ..., "26*"
string code = to_string(i) + '*';
// 将字符和对应的加密字符串添加到密码对照表中
secretCode.push_back(make_pair(c, code));
}
for (int i = 0; i < secretCode.size(); i++) {
strreplace(str, secretCode[i].second, string(1, secretCode[i].first));
}
for (int i = 0; i < str.length(); i++) {
if (str[i] >= '1' && str[i] <= '9') {
str[i] = str[i] - '1' + 'a';
}
}
cout << str << endl;
return 0;
}
这种方法最符合直觉,即先将两位数的密文(如10*)替换成字母,再将一位数的密文(如1)替换成字母 。其最大问题在于性能 。自定义的strreplace函数在每次找到目标时都会调用str.replace,这可能涉及原始字符串的多次内存重分配和数据拷贝,大数据量下性能会显著下降。
解法二:双指针法
这种解密算法的主要步骤如下:
- 遍历加密字符串,将1-9的数字直接解密为对应的小写字母(a-i)
- 处理特殊标记'*',它表示前面的两个字符可能构成一个10-26之间的数字编码
- 如果'*'前面的两个字符构成10-26之间的数字,则将其解密为对应的小写字母(a-z)
- 其他字符(非数字非'*')直接保留在解密结果中
算法使用了双指针技术,通过right指针遍历整个加密字符串,在遇到'*'时使用left指针定位可能的两位数编码起始位置,实现了高效的解密过程,整个算法的时间复杂度为 O(n)。
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
string strSec;
getline(cin, strSec);
int right = 0; // right指针用于遍历加密字符串
int left = 0; // left指针用于在遇到'*'时指向可能的两位数编码起始位置
string strRes; // strRes用于存储解密后的结果
// 使用双指针遍历加密字符串进行解密
while (right < strSec.size()) {
// 情况1:当前字符是1-9的数字,直接解密为对应的字母(a-i)
if (strSec[right] >= '1' && strSec[right] <= '9') {
// 将字符转换为整数
int num = strSec[right] - '0';
// 转换为对应的字母:1->a, 2->b, ..., 9->i
strRes.push_back('a' + num - 1);
right++;
}
// 情况2:当前字符不是'*'且不是数字,直接添加到结果中
else if (strSec[right] != '*') {
strRes.push_back(strSec[right]);
right++;
}
// 情况3:当前字符是'*',表示可能是两位数编码的结束标记
else if (strSec[right] == '*') {
// 如果'*'前面不足两个字符,无法构成两位数编码,直接添加'*'
if (right < 2) {
strRes.push_back(strSec[right]);
} else {
// 设置左指针指向'*'前面两个字符的位置
left = right - 2;
// 检查'*'前面的两个字符是否构成10-26之间的数字
if ((strSec[left] == '1' || strSec[left] == '2') && (strSec[left + 1] >= '0' && strSec[left + 1] <= '9')) {
// 将两个字符转换为整数(10-26)
int num = (strSec[left] - '0') * 10 + (strSec[left + 1] - '0');
// 从结果中删除之前添加的两个单个数字字符
strRes.erase(strRes.end() - 2, strRes.end());
// 将两位数转换为对应的字母:10->a, 11->b, ..., 26->z
strRes.push_back('a' + num - 1);
} else {
// 如果不构成有效的两位数编码,直接添加'*'
strRes.push_back(strSec[right]);
}
}
right++;
}
}
cout << strRes << endl;
return 0;
}
这是最经典的单次扫描算法 。它效率高的关键在于:一次遍历中,通过查看当前字符及其后的*,就能决定是输出一个字母(对应1-9或10-26)还是原样输出其他字符 。代码通过right指针遍历,并在遇到*时用left指针回溯检查,从而正确合并两位数字。这是解决此类"根据上下文解析"问题的标准且高效的方法。
解法三:正则表达式匹配
理论时间复杂度:O(n)(线性时间)
cpp
#include <iostream>
#include <string>
#include <regex>
using namespace std;
int main() {
// 定义字符串变量s,用于存储用户输入的密文
string s;
// 从标准输入读取一行数据存入s
getline(cin, s);
// 创建一个向量,用于存储正则表达式模式和对应的解密字符
// 每个元素是一个pair,first是正则表达式对象,second是解密后的字符
vector<pair<regex, char>> patterns;
// 从26到1反向遍历(重要!确保先处理两位数的加密串,避免10*被拆分为1和0*处理)
for (int i = 26; i >= 1; --i) {
// 生成正则表达式模式:
// - 对于两位数(i>=10),模式是"数字*",如"10*"、"26*",注意*在正则中是特殊字符需要转义
// - 对于个位数(i<10),模式就是数字本身,如"1"、"9"
string key = to_string(i) + (i >= 10 ? "\\*" : "");
// 将正则表达式和对应的解密字符添加到向量中
// 96 + i 是ASCII码转换:97是'a',所以96+1='a',96+26='z'
// 使用emplace_back直接在向量中构造pair对象,避免额外的拷贝
patterns.emplace_back(regex(key), static_cast<char>(96 + i));
}
// 遍历预编译的正则表达式模式列表,依次进行解密替换,patterns列表是按照从长到短(26到1)的顺序构建的,这确保了正确的解密顺序
for (const auto& pattern : patterns) {
// 使用正则表达式替换函数进行解密:
// - s:待解密的字符串
// - pattern.first:正则表达式对象,匹配加密字符串(如"10*"、"1"等)
// - string(1, pattern.second):替换为对应的解密字符(如'a'、'z'等)
// 函数返回替换后的新字符串,并重新赋值给s,实现逐步解密
s = regex_replace(s, pattern.first, string(1, pattern.second));
}
cout << s << endl;
return 0;
}
这种方法的优势在于抽象和表达能力强 。它通过预定义一系列规则(26* -> z, 25* -> y, ..., 1 -> a),并从长模式到短模式(从26到1)依次替换 ,巧妙地避免了错误匹配(例如,防止10*被错误地先替换为a和0*)。代码极其简洁,但正则表达式的编译、匹配和替换操作在底层通常比直接字符操作慢得多。
解法对比一览表
| 特性维度 | 解法一:库函数替换 | 解法二:双指针法 | 解法三:正则表达式 |
|---|---|---|---|
| 核心思路 | 顺序替换:先处理10*-26*,再处理1-9 |
单次遍历:根据当前字符和上下文即时解密并输出 | 反向替换 :从26*到1反向匹配并替换 |
| 时间复杂度 | 潜在 O(n²) ,因str.replace可能导致字符串多次拷贝 |
O(n),仅遍历一次,操作高效 | 理论 O(n),但正则匹配和替换有较高常数开销 |
| 空间复杂度 | 较高,替换过程产生多个字符串副本 | 较低,主要使用结果字符串,几乎无额外开销 | 较高,存储模式列表,且regex_replace生成新字符串 |
| 代码可读性 | 中等,逻辑分两步,但替换函数封装清晰 | 较低,需要手动处理多种边界条件和指针移动 | 极高,逻辑高度抽象,接近自然语言描述规则 |
| 优势 | 思路直观,符合"先复杂后简单"的日常思维 | 时间空间效率俱佳,是算法题推荐的"标准解法" | 代码简洁优雅,规则易于扩展和维护 |
| 劣势 | 效率最低,多次遍历和字符串拷贝开销大 | 实现相对复杂,容易在指针和边界处理上出错 | 运行时开销最大,不适合对性能要求极高的场景 |