"模拟"是算法中最直观的解题思路之一------按照题目描述的规则/过程,一步步还原操作逻辑,不需要复杂的算法设计,但需要理清细节,处理好边界条件。本文将通过5道经典模拟题,拆解不同场景下的模拟思路与代码实现。
一、替换所有的问号
题目描述:
给定仅含小写字母和 '?' 的字符串 s,将所有 '?' 替换为小写字母,使最终字符串无连续重复字符(不能修改非 '?' 字符,题目保证非 '?' 字符无连续重复)。
示例:
- 输入:
s = "?zs",输出:"azs"('z'是无效替换,会导致连续重复) - 输入:
s = "ubv?w",输出:"ubvaw"(不能替换为'v'或'w')
解题思路:
遍历每个 '?',尝试从 'a' 到 'z' 的字符,找到与左右相邻字符都不重复的字符进行替换:
- 若当前是字符串开头,只需检查右侧字符;
- 若当前是字符串结尾,只需检查左侧字符;
- 否则同时检查左右字符。
完整代码:
cpp
class Solution {
public:
string modifyString(string s) {
for(int i = 0; i < s.size(); i++)
{
if(s[i] == '?')
for(char c = 'a'; c <= 'z'; c++)
{
if((i == 0 || s[i - 1] != c) && (i == s.size() - 1 || s[i + 1] != c))
{
s[i] = c;
break;
}
}
}
return s;
}
};
复杂度分析:
- 时间复杂度: O ( n × 26 ) O(n×26) O(n×26),遍历字符串( O ( n ) O(n) O(n)),每个问号最多尝试26个字符(常数级),总复杂度 O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1),仅修改原字符串(题目允许原地修改)。
二、提莫攻击
题目描述:
提莫攻击会让艾希中毒 duration 秒,若在中毒期间再次攻击,中毒计时器重置。给定非递减的攻击时间数组 timeSeries 和 duration,返回艾希的总中毒秒数。
示例:
- 输入:
timeSeries = [1,4], duration = 2,输出:4(中毒时间:1-2秒、4-5秒,共4秒)
解题思路:
遍历攻击时间,计算相邻两次攻击的间隔 与 duration 的较小值(间隔≥duration 则中毒满 duration 秒,否则中毒间隔秒),最后加上最后一次攻击的 duration 秒。
完整代码:
cpp
class Solution {
public:
int findPoisonedDuration(vector<int>& timeSeries, int duration) {
if(timeSeries.empty()) return 0; // 处理空数组边界
int ret = 0;
for(int i = 1; i < timeSeries.size(); i++)
{
int interval = timeSeries[i] - timeSeries[i - 1];
ret += min(interval, duration);
}
return ret + duration;
}
};
复杂度分析:
- 时间复杂度: O ( n ) O(n) O(n),遍历攻击时间数组一次。
- 空间复杂度: O ( 1 ) O(1) O(1),仅用常数级额外变量。
三、Z字形变换
题目描述:
将字符串 s 按指定行数 numRows 以"Z形"排列,之后逐行读取得到新字符串。
示例:
- 输入:
s = "PAYPALISHIRING", numRows = 3,输出:"PAHNAPLSIIGYIR"(Z形排列后逐行读取)
解题思路:
找Z形排列的字符下标规律:
- 主间隔:
d = 2 * numRows - 2(每一行的垂直列字符间隔)。 - 首行/尾行:仅取间隔为
d的字符(下标i, i+d, i+2d...)。 - 中间行(第
k行):除主间隔字符外,还需取斜列字符(下标d-k, d-k+d, ...)。 - 边界条件:
numRows = 1时直接返回原字符串。
完整代码:
cpp
class Solution {
public:
string convert(string s, int numRows) {
if(numRows == 1) return s;
string ret;
int n = s.size(), d = 2 * numRows - 2;
// 首行
for(int i = 0; i < n; i += d)
ret += s[i];
// 中间行
for(int k = 1; k < numRows - 1; k++)
{
for(int i = k, j = d - k; i < n || j < n; i += d, j += d)
{
if(i < n) ret += s[i];
if(j < n) ret += s[j];
}
}
// 尾行
for(int i = numRows - 1; i < n; i += d)
ret += s[i];
return ret;
}
};
复杂度分析:
- 时间复杂度: O ( n ) O(n) O(n),每个字符仅被访问一次。
- 空间复杂度: O ( n ) O(n) O(n),存储结果字符串(必要输出)。
四、外观数列
题目描述:
"外观数列"是递归定义的数字字符串:countAndSay(1) = "1",countAndSay(n) 是对 countAndSay(n-1) 的"行程长度编码"(统计连续相同字符的数量+字符)。
示例:
- 输入:
n = 4,输出:"1211"(1→11→21→1211)
解题思路:
从 countAndSay(1) = "1" 开始,循环模拟n-1次编码过程:
- 遍历当前字符串,用双指针统计连续相同字符的长度(
right - left)。 - 拼接"长度+字符"到新字符串,更新
left = right继续统计。
完整代码:
cpp
class Solution {
public:
string countAndSay(int n) {
string ret = "1";
for(int i = 1; i < n; i++)
{
string tmp;
int len = ret.size();
for(int left = 0, right = 0; right < len;)
{
// 统计连续相同字符
while(right < len && ret[left] == ret[right]) right++;
tmp += to_string(right - left) + ret[left];
left = right;
}
ret = tmp;
}
return ret;
}
};
复杂度分析:
- 时间复杂度: O ( m ) O(m) O(m),
m是第n个外观数列的长度(随n指数增长,但每个字符仅被遍历一次)。 - 空间复杂度: O ( m ) O(m) O(m),存储当前轮次的外观数列。
五、数青蛙
题目描述:
字符串 croakOfFrogs 是多个"croak"(青蛙鸣叫)的混合,每个青蛙需按 'c'→'r'→'o'→'a'→'k' 的顺序鸣叫。返回最少需要多少只青蛙,若字符串无效则返回 -1。
示例:
- 输入:
croakOfFrogs = "croakcroak",输出:1(一只青蛙叫两次) - 输入:
croakOfFrogs = "crcoakroak",输出:2(需两只青蛙)
解题思路:
用数组统计每个鸣叫阶段的青蛙数量,模拟青蛙的鸣叫流程:
- 建立映射:
'c'→0, 'r'→1, 'o'→2, 'a'→3, 'k'→4。 - 遍历字符:
- 若为
'c':优先复用完成鸣叫(处于k阶段)的青蛙,否则新增一只。 - 若为其他字符:需检查前一个阶段是否有青蛙,有则转移到当前阶段,否则字符串无效。
- 若为
- 最终检查:若中间阶段(非
k)有剩余青蛙,说明字符串无效;否则k阶段的数量即为最少青蛙数。
完整代码:
cpp
class Solution {
public:
int minNumberOfFrogs(string croakOfFrogs) {
string t = "croak";
int n = t.size();
vector<int> stage(n, 0); // 统计每个阶段的青蛙数量
unordered_map<char, int> idxMap;
for(int i = 0; i < n; i++) idxMap[t[i]] = i;
for(char ch : croakOfFrogs) {
if(ch == 'c') {
// 复用完成鸣叫的青蛙(k阶段),否则新增
if(stage[n-1] > 0) stage[n-1]--;
stage[0]++;
} else {
int currIdx = idxMap[ch];
int prevIdx = currIdx - 1;
// 前一个阶段无青蛙,字符串无效
if(stage[prevIdx] == 0) return -1;
stage[prevIdx]--;
stage[currIdx]++;
}
}
// 中间阶段有剩余,字符串无效
for(int i = 0; i < n-1; i++) {
if(stage[i] > 0) return -1;
}
return stage[n-1];
}
};
复杂度分析:
- 时间复杂度: O ( m ) O(m) O(m),
m是字符串长度,遍历一次字符串。 - 空间复杂度: O ( 1 ) O(1) O(1),仅用固定大小的数组和哈希表(字符数固定为5)。