【算法】用三种解法解决字符串替换问题的实例:华为OD机考双机位A卷 - 密码解密

题目描述

给定一段 "密文" 字符串 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',而且这两步替换不冲突,所以这种方法具有可行性,步骤如下:

  1. 将"10*"~"26*"分别映射到'j'-'z',并遍历字符串s进行替换,得到新字符串s',此时字符串中可能存在数字、'*'、字母以及其他符号;
  2. 将新字符串中的'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. 遍历加密字符串,将1-9的数字直接解密为对应的小写字母(a-i)
  2. 处理特殊标记'*',它表示前面的两个字符可能构成一个10-26之间的数字编码
  3. 如果'*'前面的两个字符构成10-26之间的数字,则将其解密为对应的小写字母(a-z)
  4. 其他字符(非数字非'*')直接保留在解密结果中

算法使用了双指针技术,通过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*被错误地先替换为a0*)。代码极其简洁,但正则表达式的编译、匹配和替换操作在底层通常比直接字符操作慢得多。

解法对比一览表

特性维度 解法一:库函数替换 解法二:双指针法 解法三:正则表达式
核心思路 顺序替换:先处理10*-26*,再处理1-9 单次遍历:根据当前字符和上下文即时解密并输出 反向替换 :从26*1反向匹配并替换
时间复杂度 潜在 O(n²) ,因str.replace可能导致字符串多次拷贝 O(n),仅遍历一次,操作高效 理论 O(n),但正则匹配和替换有较高常数开销
空间复杂度 较高,替换过程产生多个字符串副本 较低,主要使用结果字符串,几乎无额外开销 较高,存储模式列表,且regex_replace生成新字符串
代码可读性 中等,逻辑分两步,但替换函数封装清晰 较低,需要手动处理多种边界条件和指针移动 极高,逻辑高度抽象,接近自然语言描述规则
优势 思路直观,符合"先复杂后简单"的日常思维 时间空间效率俱佳,是算法题推荐的"标准解法" 代码简洁优雅,规则易于扩展和维护
劣势 效率最低,多次遍历和字符串拷贝开销大 实现相对复杂,容易在指针和边界处理上出错 运行时开销最大,不适合对性能要求极高的场景
相关推荐
罗湖老棍子21 小时前
信使(msner)(信息学奥赛一本通- P1376)四种做法
算法·图论·dijkstra·spfa·floyd·最短路算法
生成论实验室1 天前
生成论之基:“阴阳”作为元规则的重构与证成——基于《易经》与《道德经》的古典重诠与现代显象
人工智能·科技·神经网络·算法·架构
啊董dong1 天前
noi-2026年1月07号作业
数据结构·c++·算法·noi
m0_635647481 天前
Qt使用第三方组件库新手教程(一)
开发语言·c++·qt
l1t1 天前
DeepSeek辅助编写的利用唯一可选数求解数独SQL
数据库·sql·算法·postgresql
星火开发设计1 天前
二叉树详解及C++实现
java·数据结构·c++·学习·二叉树·知识·期末考试
WJSKad12351 天前
传送带物体检测识别_基于YOLO11与RGCSPELAN改进算法_工业视觉检测系统
人工智能·算法·视觉检测
CSDN_RTKLIB1 天前
C++取余符号%
开发语言·c++
仍然.1 天前
JavaDataStructure---排序
数据结构·算法·排序算法