LeetCode 1576: 替换所有的问号(模拟) —— 题解

👋 欢迎阅读

🎯 欢迎来到「替换所有的问号」题解之旅! 本文将带你从"给问号填上不撞邻居的字母"这一直观场景出发,深入理解贪心 + 边界防护 的巧妙运用,并掌握如何逐个问号试填 26 个字母 来构造出合法的最终字符串。

在开始之前,建议你先:

  • 了解题目背景 :这是 LeetCode 1576 题,给定含小写字母和 ? 的字符串 s,把所有 ? 替换成字母,使得任意相邻两个字符都不相同 ,返回任意一个合法结果。本质上,每个 ? 只需避开左右邻居 ,问题转化为逐位贪心试填。

  • 明确学习目标 :掌握逐位贪心 + 邻居校验 技术,理解下标越界防护 i == 0 / i == n-1 的必要性,并熟练处理首尾问号 与连续问号等边界情况。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 s = "?zs" 输出 "azs",s = "ubv?w" 输出 "ubvaw")。

本文将从问题转化、贪心试填、邻居校验、边界防护 到代码实现,层层递进。即使你对贪心 还不熟悉,我们也会从"逐个问号填一个跟邻居都不一样的字母"这一直觉出发,让你轻松抓住核心思想 ------逐个试填,只要不撞邻居。现在,让我们一起填满问号,构造合法字符串吧! ✏️🎯

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容:《模拟》

一.题目

1576. 替换所有的问号 - 力扣(LeetCode)

二、算法分析

一、问题分析(前置分析)

  • 题目要求:把 s 中所有 ? 替换为小写字母,使任意相邻字符都不相同,返回任一合法结果。
  • 关键约束:字母仅 26 个 ;相邻必须不同;首位/末位只有一个邻居。
  • 核心思路:每个 ? 的约束只与左右两个邻居有关 ,互不影响,因此可以逐位贪心 :从小到大试字母,第一个与左右邻居都不同的 即可采用------由于 26 个字母中最多只有 2 个被邻居占用,必然存在可填字母,贪心不会失败。

📌 例子:为什么"最多试三次"就够了

s = "?a?":第一个 ? 只需避开右邻居 a,试到 b 即可(? 左邻居不存在);第二个 ? 只需避开左邻居 a,试到 b 即可 → "bab"。每个问号的左右邻居最多占 2 个字母 ,26 个字母里至少还有 24 个可选 ,所以"从小到大试"必定能在前几个字母内找到答案,这是贪心必然成功的根本原因。

二、算法策略

核心步骤:

  1. 遍历字符串 :i 从 0 到 n-1。
  2. 遇到问号则试填 :ch 从 'a' 试到 'z'。
  3. 邻居校验 :满足 (i == 0 || ch != s[i-1]) && (i == n-1 || ch != s[i+1]) 才可采用(边界位置自动跳过不存在的邻居)。
  4. 填上并跳出 :s[i] = ch; break;(找到即填,无需继续试)。
  5. 返回 :遍历结束返回 s。

📊 示例 (s = "?zs",等待填 ? 使相邻不同):

步骤 i si 试填 ch 左邻居 si-1 右邻居 si+1 校验 结果
i=0 0 ? 'a' 无(i==0) 'z' a != z ✅ s = "azs"
i=1 1 'z' --- --- --- 非问号,跳过 ---
i=2 2 's' --- --- --- 非问号,跳过 ---

最终得到 "azs" ✅(相邻 a-z、z-s 均不同),与题目示例一致(示例输出 "azs",任何合法答案均可)。

三、正确性说明(简单版本)

  • 约束局部性 :每个 ? 的合法性只取决于它左右两个邻居 ,而填值不会影响其他 ? 的邻居关系 (填完就固定),因此逐位贪心不影响全局最优,局部合法即全局合法。
  • 必然存在可填字母 :任一位置最多被左右邻居占用 2 个 字母(边界处最多 1 个),而字母表有 26 个 ,必然至少有一个字母可用 ,贪心不会填不出来。
  • 校验条件完整 :(i == 0 || ch != s[i-1]) 保证不与左邻居相同 (首字符无左邻居,短路跳过);(i == n-1 || ch != s[i+1]) 保证不与右邻居相同(末字符无右邻居)------两个条件合起来恰好覆盖"相邻不同"的全部要求。
  • 顺序填不影响正确性 :从左往右填,右边的 ? 校验时会看到已被填好 的左侧字符(非 ?),校验依然有效,不会因为填值顺序出错。

📌 例子:连续问号如何被依次化解

s = "???":i=0 时无左邻居、右邻居是 ?(未填),'a' 满足条件 → 填 'a';i=1 时左邻居 'a'、右邻居 ?,试 'a' 撞左邻居 → 试 'b' 通过 → 填 'b';i=2 时左邻居 'b',试 'a' 通过 → 填 'a',得到 "aba"。每个问号都只避开已确定的邻居,连续问号被逐个化解,最终相邻全不同 ✅。

四、实现细节(边界防护)

  • 初始化:n = s.size(),直接原地修改 s。
  • 边界防护:i == 0 与 i == n-1 的短路判断 是防越界的核心------若漏掉 i == 0 ||,i=0 时访问 s[-1] 会越界 (UB);若漏掉 i == n-1 ||,i=n-1 时访问 s[n] 越界。用 || 短路自动跳过不存在的邻居。
  • 关键操作 :if (s[i] == '?')(识别待填位置)、(i == 0 || ch != s[i-1]) && (i == n-1 || ch != s[i+1])(邻居校验)、s[i] = ch; break;(填值并终止试填)。

📌 例子:首尾问号的边界处理

s = "?"(单字符):n=1,i=0 既是首又是尾,两个条件都短路为真,第一个字母 'a' 直接通过 → 返回 "a";s = "?a":i=0 时 i == 0 短路(无左邻居),只需 ch != 'a' → 填 'b' → "ba"。首尾位置只有一个邻居,短路判断让同一套逻辑自然适配。

五、返回值(目标映射)

  • 返回 s:替换所有问号后的合法字符串(任意一个合法解均可),对应题目"返回最终的字符串(若有多种解法,返回任一)"。

三.代码

cpp 复制代码
class Solution
{
public:
    string modifyString(string s)
    {
        int n = s.size();

        // 1. 遍历字符串,逐个处理问号
        for (int i = 0; i < n; i++)
        {
            if (s[i] == '?')
            {
                // 2. 从小到大试字母,找到第一个不与左右邻居冲突的
                for (char ch = 'a'; ch <= 'z'; ch++)
                {
                    // 边界防护:i==0 时无左邻居、i==n-1 时无右邻居,用 || 短路跳过
                    if ((i == 0 || ch != s[i - 1]) && (i == n - 1 || ch != s[i + 1]))
                    {
                        s[i] = ch;      // 填上合法字母
                        break;          // 找到即可,无需继续尝试
                    }
                }
            }
        }
        return s;   // 3. 返回替换后的字符串
    }
};

四、易错点分析

难点1:边界校验必须用 || 短路

cpp 复制代码
if ((i == 0 || ch != s[i - 1]) && (i == n - 1 || ch != s[i + 1]))

i == 0 时 s[i-1] 即 s[-1](越界 ,UB);i == n-1 时 s[i+1] 即 s[n](越界)。靠 || 的短路特性 :i == 0 为真时直接跳过 后半部分的越界访问。若把顺序写反成 ch != s[i-1] || i == 0,短路失效(先访问 s[-1]),照样越界 ------短路判断的顺序不可颠倒。

难点2:右边的 ? 会不会影响当前校验

cpp 复制代码
ch != s[i + 1]    // s[i+1] 可能是 '?'

当右邻居还是 ? 时,ch != '?' 恒成立 (字母不可能等于问号),相当于不做限制 ------这是安全的:右邻居稍后填值时会主动避开 当前位置的字符,两者不可能冲突。同理左邻居若是 ?(未填),后续也会避开。"未填位置不构成约束" 是本解法能一遍扫完的关键。

难点3:原地修改与遍历顺序的配合

复制代码
s[i] = ch;    // 原地修改

从左往右填,左侧必然已经全部确定 (要么原本是字母,要么已在本轮填好),所以校验 s[i-1] 时读到的是最终值 ,判断有效。若改成从右往左填,则要保证右侧已确定------两个方向都可行,但必须保证"已确定的一侧"被正确校验;从左往右是最自然的顺序。

五、流程图

🎯 闭幕

🎉 恭喜你完成了「替换所有的问号」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 代码对每个 '?' 从 'a' 到 'z' 依次尝试 ,找到第一个不与左右邻居冲突的字符。为什么最多尝试 3 个字母就一定能找到合法字符? 如果字母表只有 2 个字母,还能保证有解吗?

  • 边界判断使用了 (i == 0 || ch != s[i - 1]) && (i == n - 1 || ch != s[i + 1])。为什么必须用 || 短路? 如果直接写 ch != s[i-1] && ch != s[i+1],在 i == 0 或 i == n-1 时会发生什么?

  • 如果字符串中存在 连续多个 '?' (如 "???"),当前算法能否正确处理?为什么修改前面的 '?' 不会影响后面 '?' 的合法性判断? 请举例说明。

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏
👤 关注作者 ,获取更多题解
💬 留言交流你的疑问或优化思路


📌 深入思考答案

  • 最多尝试 3 个字母 是因为每个 '?' 最多只有左右两个邻居,只要字母表大小 ≥ 3,就一定能找到一个既不同于左邻居又不同于右邻居的字符。若字母表只有 2 个字母,则可能无解(例如 "a?a",中间不能是 a,只能是 b,但若字母表只有 {a,b},b 与左右都不同,其实可以;但若 "a?b" 且字母表只有 {a,b},则 ? 不能是 a 也不能是 b,无解)。

  • 必须用 || 短路 ,否则 i == 0 时访问 s[-1] 会越界,i == n-1 时访问 s[n] 也会越界。短路运算保证在边界情况下跳过越界访问。

  • 连续多个 '?' 能正确处理 ,因为每次只修改当前 '?',且只与左右已确定的字符比较。修改后,该位置变成确定字符,后续 '?' 再比较时,左邻居就是刚刚填好的字符,逻辑依然成立。例如 "???",第一个填 'a',第二个不能是 'a' 填 'b',第三个不能是 'b' 填 'a',得到 "aba",合法。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
mlidongfeng1 小时前
【学习】ParallelKittens(PK)学习与分析
学习
朝朝辞暮i1 小时前
C++ 第 38 课:线程、SingleThreadedExecutor、MultiThreadedExecutor、Callback Group
开发语言·c++·算法·ros2
(Charon)1 小时前
【C++面试】互斥锁与读写锁:使用场景、实现原理与写饥饿问题
c++·算法
longlongzihan1 小时前
回溯法详解:LeetCode 46. 全排列
算法·leetcode·回溯
秋名RG1 小时前
时间复杂度、空间复杂度与算法稳定性详解
数据结构·算法·排序算法
白色的北极熊1 小时前
c语言 求余运算符
c语言·数据结构·算法
坤坤子吖1 小时前
Python基础语法学习:列表和元组
开发语言·笔记·python·学习
波尔德1 小时前
Origin 2024 绘制钟形正态分布曲线:填充颜色并导出透明 PNG
人工智能·算法
JWASX2 小时前
Java 转 go 学习 - 接口
学习·golang