愿旖旎头像
LeetCode 38: 外观数列 (模拟) —— 题解封面图

LeetCode 38: 外观数列 (模拟) —— 题解

  👋 欢迎阅读

🎯 欢迎来到「外观数列」题解之旅! 本文将带你从"把一串数字依次念出来"这一生活化场景出发,深入理解双指针分组扫描的巧妙运用,并掌握如何统计连续相同字符的个数来逐次生成第 n 项外观数列。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 38 题,外观数列第 1 项固定为 1,之后每一项都是对前一项"读出来"——连续相同的数字读成"个数 + 数字"。本质上,问题就是反复做一次游程编码(Run-Length Encoding)。

  • 明确学习目标:掌握双指针分组扫描技术,理解 left = right 跳组与内层 right < len 边界的原因,并熟练处理 n = 1 与连续相同字符长度等边界情况。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 n = 4 输出 "1211",n = 1 输出 "1")。

本文将从规则理解、双指针分组、逐个拼接、迭代生成到代码实现,层层递进。即使你对游程编码还不熟悉,我们也会从"数一数有几个相同的,再把个数和数字念出来"这一直觉出发,让你轻松抓住核心思想——按组统计个数,念出即下一项。现在,让我们一起读出上一项,生成下一项吧! 🔢🎯

 🔥 愿旖旎 · 个人主页

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

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

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


一.题目

38. 外观数列 - 力扣(LeetCode)

 ​

二、算法分析

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

  • 题目要求:返回外观数列的第 n 项(第 1 项为 1,每项都是对前一项的"读数")。
  • 关键约束:n ≥ 1;每一项长度逐项增长;项与项之间只能迭代推导。
  • 核心思路:数列没有闭式公式,无法一步算出第 n 项;只能从第 1 项出发,每轮做一次游程编码(把连续相同数字压缩成"个数 + 数字"),迭代 n-1 次得到答案。

📌 例子:读数规则如何生成数列

n = 1 得 1;读它"1 个 1"得 11(n = 2);读 11 为"2 个 1"得 21(n = 3);读 21 为"1 个 2、1 个 1"得 1211(n = 4)。每一项都由上一项唯一确定,且规律无法用公式跳过中间项——这就是必须逐次生成的原因。

二、算法策略(双指针分组扫描 · 迭代生成)

核心步骤:

  1. 初始化:ret = "1"(第 1 项)。
  2. 迭代 n-1 次:外层循环 i 从 1 到 n-1,每轮由 ret 生成新串 tmp。
  3. 双指针分组:left 与 right 均从 0 出发;内层 while (right < len && ret[right] == ret[left]) 让 right 一直探到该组末尾。
  4. 拼接"个数 + 字符":len2 = right - left 为该组长度,先拼 len2 再拼 ret[left]。
  5. 跳组继续:left = right 直接跳到下一组开头,重复直到 right == len。
  6. 更新并返回:ret = tmp 作为下一轮输入,最终返回第 n 项。

📊 示例(由 "1211" 生成 "111221"):

步骤leftright扫描到的组len2(个数)追加内容tmp
初始00———""
第 1 组01"1"1"1" + "1""11"
第 2 组12"2"1"1" + "2""1112"
第 3 组24"11"2"2" + "1""111221"

right 依次停在 1、2、4(= len)时,恰好切出 "1"、"2"、"11" 三组,拼得 "111221" ✅,正是 "1211" 的下一项。

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

  • 分组完备:left 始终指向某组起点,right 沿相同字符推进到首个不同字符(或串尾),因此 [left, right) 恰好圈出一整组,不重不漏。
  • 拼接语义正确:每组输出"个数 + 字符"两项,与"读数"规则逐字对应;组的先后顺序与原串一致,因此结果唯一且正确。
  • 末组不丢:内层循环以 right < len 为界,right 最终可能停在 len(而非 len-1),此时 len2 = len - left 仍能正确计算末组长度,最后一组必被输出。
  • 迭代次数正确:初始已是第 1 项,再压缩 n-1 次正好得到第 n 项,n = 1 时循环不执行直接返回 "1",不漏不多。

📌 例子:末组为什么不会漏

扫描 "111221"(len = 6):前两组是 "111"(left = 0 → right = 3)与 "22"(left = 3 → right = 5);第三组 "1" 使 right 推进到 6 = len,内层 while 因 right < len 不成立而退出,但 len2 = 6 - 5 = 1 仍被正常追加为 "11",最终得到 "312211" ✅——边界用 right 的越界而非 right+1 判断,末组才不会被吞掉。

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

  • 初始化:ret = "1"(第 1 项,题目保证 n ≥ 1,故 ret 一定非空);每轮新建 tmp 累积结果。
  • 边界防护:内层 while 必须带 right < len(否则 ret[right] 越界);left = right 允许 left 走到 len,但此时外层 right < len 已为假,不会越界访问;n = 1 时循环不进入,直接返回 "1"。
  • 复杂度:时间 O(n × L)(L 为末项长度,每轮线性扫描;实际 L 增长约 1.3 倍),空间 O(L)(ret 与 tmp 两个字符串)。
  • 关键操作:while (right < len && ret[right] == ret[left]) right++;(分组探测)、tmp += len2 + '0'; tmp += ret[left];(拼接个数与字符)、left = right;(跳组)。

📌 例子:初始值与循环次数的配套

n = 1 时循环条件 i = 1 < 1 为假,一次都不执行,直接返回 "1" ✅;n = 4 时循环执行 i = 1, 2, 3 共 3 次,依次得到 "11"、"21"、"1211" ✅。"初始值 = 第 1 项"与"循环 n-1 次"严格配套,若把初始值设为空串或循环写成 n 次,结果就会错位一项。

五、返回值(目标映射)

  • 返回 ret:外观数列的第 n 项,对应题目"返回外观数列的第 n 项"。

三.代码

class Solution
{
public:
    string countAndSay(int n)
    {
        string ret = "1";   // 第 1 项固定为 "1"(题目保证 n >= 1,故一定非空)

        // 1. 迭代生成:再压缩 n-1 次,即得到第 n 项
        for (int i = 1; i < n; i++)
        {
            string tmp = "";        // 本轮生成的新一项
            int len = ret.size();   // 当前项的长度

            // 2. 双指针分组:left 指向组起点,right 向后探测该组末尾
            for (int left = 0, right = 0; right < len;)
            {
                // 3. 探测:right 一直前进到首个不同字符(或串尾)
                while (right < len && ret[right] == ret[left])
                {
                    right++;
                }

                int len2 = right - left;    // 该组连续相同字符的个数
                tmp += len2 + '0';          // 先拼“个数”(0~9 转字符)
                tmp += ret[left];           // 再拼“字符”本身
                left = right;               // 跳到下一组开头(不能 left++)
            }

            ret = tmp;      // 4. 本轮结果作为下一轮的输入
        }
        return ret;         // 5. 返回第 n 项
    }
};

四、易错点分析

难点1:tmp += len2 + '0' 的整数转字符陷阱

int len2 = right - left;
tmp += len2 + '0';      // 只对 0~9 成立
tmp += ret[left];

len2 + '0' 是把单个数字转成对应字符的常用写法,但它只对 0~9 有效:若某组长度达到 10,10 + '0' 会得到字符 ':',既不是数字,也丢失了"10"的两位语义(正确做法应追加字符串 "10" 或先转 to_string)。本数列的已知性质是连续相同数字长度不超过 3(如 1113213211 中的 111),故 n ≤ 30 时不会触发;但依赖这一性质是脆弱的——把"个数"当单字符处理属于不严谨写法。

难点2:left = right 跳组,而不是 left++

left = right;    // 直接跳到下一组开头

while 结束时 right 已指向该组之后的第一个位置,即下一组的起点,所以 left 应直接跳到 right(一次跨过整组)。若写成 left++,left 只会前移一格,随后会重复扫描同一组内部,把 "11" 读成两次"1 个 1"而非"2 个 1",结果完全错误——"跳组"是双指针分组扫描的关键动作。

难点3:内层 while 必须带 right < len

while (right < len && ret[right] == ret[left])

right 会一路推进到 len(串尾之后一位)。若漏掉 right < len,当 right == len 时 ret[right] 会越界访问(UB),轻则读到垃圾字符导致分组错乱,重则崩溃。同时注意条件的顺序:right < len 必须写在前面,靠 && 的短路避免越界访问。

难点4:循环次数 n-1 与初始值的配套

string ret = "1";
for (int i = 1; i < n; i++)    // 恰好 n-1 次

初始 ret 已是第 1 项,故只需再生成 n-1 次。若误写成 i = 1; i <= n(多一次)会返回第 n+1 项;若写成 i = 0(多一次)同理。反之若把初始值改成 "" 并循环 n 次,则第一轮的输入是空串、无法产生 "1"。"初始值 = 第 1 项"与"循环 n-1 次"是一套固定搭配,改任何一半都要同步调整另一半。

五、流程图

 

 🎯 闭幕

🎉 恭喜你完成了「外观数列」问题的学习!

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

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

💡 深入思考

  • 代码中 ret 初始为 "1",循环 i = 1; i < n 共执行 n-1 次,得到第 n 项。为什么第 1 项固定是 "1"? 当 n = 1 时,代码直接返回什么?

  • 内层使用 双指针:left 指向当前组的起点,right 向后探测相同字符直到不同或结尾。right 探测结束后,tmp += len2 + '0' 和 tmp += ret[left] 分别表示什么? 为什么必须 先拼个数再拼字符?

  • 代码用 len2 + '0' 把连续个数转成字符。如果某组连续相同字符个数超过 9(例如 10 个 '1'),len2 + '0' 会变成什么? 这会导致结果错误吗?题目 n 的范围是否可能触发这种情况?

  • 处理完一组后执行 left = right,而不是 left++。为什么必须直接跳到下一组开头? 如果写成 left++,会对同一组内的字符重复处理吗?请举例说明。

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

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


📌 深入思考答案

  • 第 1 项固定为 "1" 是外观数列的定义起点;n = 1 时循环不执行,直接返回 "1",正确。

  • len2 是当前组 连续相同字符的个数,ret[left] 是该字符本身。报数规则是“几个几”,所以 先个数后字符,例如 "21" 表示“2 个 1”。

  • len2 + '0' 只适用于 0~9 的个数。若 len2 = 10,'0' + 10 会得到 ':',结果错误。但 LeetCode 中 n ≤ 30,外观数列的连续相同字符个数 不会超过 9(通常最多 3),因此安全;若 n 更大,应改用 to_string(len2)。

  • left = right 是因为 [left, right) 这一组已经处理完毕,right 正好指向下一组的第一个字符。若写成 left++,会从组内第二个字符开始再次统计,导致 重复报数 和错误结果。

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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2601_96587588/article/details/166839760

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--