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++,会从组内第二个字符开始再次统计,导致重复报数和错误结果。

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

相关新闻

从零搭建RAG私有知识库:LangChain+FAISS+Ollama实战指南

从零搭建RAG私有知识库:LangChain+FAISS+Ollama实战指南

1. 项目概述&#xff1a;RAG私有知识库到底解决什么问题RAG&#xff08;检索增强生成&#xff09;这个缩写&#xff0c;最近一年多几乎成了大模型落地场景里曝光率最高的词。但很多初次接触的朋友&#xff0c;看到的资料要么是纯理论讲原理&#xff0c;要么是上来就扔一堆代码&…

2026/10/20 19:48:43 阅读更多 →
TCP三次握手ACK为何等于Seq+1?字节流与确认号原理解析

TCP三次握手ACK为何等于Seq+1?字节流与确认号原理解析

很多人在刚开始接触 TCP 的时候&#xff0c;都会在一个很不起眼的细节上卡住&#xff1a;三次握手第二个报文里&#xff0c;服务端为什么要把 ACK 写成客户端的 Seq 加 1&#xff1f;更困惑的是&#xff0c;客户端最后回的那个 ACK&#xff0c;它的 Seq 和 Ack 又分别等于什么&…

2026/10/20 19:48:43 阅读更多 →
Webpack本地Mock开发模式:从中间件到动态数据完整落地指南

Webpack本地Mock开发模式:从中间件到动态数据完整落地指南

做了多年前端工程化&#xff0c;我可以很负责任地说一句&#xff1a;mock 数据这件事&#xff0c;看起来简单&#xff0c;真想做到好用、不坑人、能长期维护&#xff0c;远比大多数人想象得更讲究。尤其是把 mock 集成进 Webpack 构建流程时&#xff0c;藏在配置背后的坑一个接…

2026/10/20 19:48:43 阅读更多 →

最新新闻

Agent工程栈五层拆解:任务编排、工具调用、记忆管理、可观测性与多Agent协作

Agent工程栈五层拆解:任务编排、工具调用、记忆管理、可观测性与多Agent协作

1. 从五条动态里拆出来的工程信号10 月 7 日那天&#xff0c;我关注的几个技术社区里集中冒出来五条动态&#xff0c;主题各不相同&#xff0c;但放在一起看&#xff0c;指向的是同一件事&#xff1a;Agent 这套东西&#xff0c;正在从"演示能跑"往"工程能交付&…

2026/10/20 23:28:02 阅读更多 →
PPT认知工程:信息压缩、视觉锚点与逻辑呼吸感

PPT认知工程:信息压缩、视觉锚点与逻辑呼吸感

1. “PPT-Master”不是软件名&#xff0c;而是一类人的隐性职业标签最近在几个技术协作群和高校教学支持论坛里&#xff0c;频繁看到有人用“PPT-Master”自称或称呼他人——不是指某款叫这个名字的工具&#xff0c;也不是某个开源项目&#xff0c;而是一种带着自嘲又带点敬意的…

2026/10/20 23:28:02 阅读更多 →
AI Agent架构与任务规划算法深度解析:复杂任务系统的工程选型指南

AI Agent架构与任务规划算法深度解析:复杂任务系统的工程选型指南

过去一个多月&#xff0c;我把主流可查的AI Agent架构、任务规划算法和复杂任务处理系统从头到尾梳理了一遍&#xff0c;同时在自建的任务集上跑了一轮对比实验。起因是某公司准备上一套复杂任务处理平台&#xff0c;我被拉去做前期技术调研&#xff0c;给后续选型提供判断依据…

2026/10/20 23:28:02 阅读更多 →
XML大文件解析与树形查看器:从DOM到延迟加载的完整实践

XML大文件解析与树形查看器:从DOM到延迟加载的完整实践

简介&#xff1a;XMLViewer是一款面向开发者、测试人员及数据处理用户的轻量XML查看工具&#xff0c;核心用途是快速打开并浏览XML文件&#xff0c;清晰展示节点层级结构&#xff0c;同时辅助检测语法是否正确&#xff0c;减少手动排查标签不匹配、缩进异常等问题的成本&#x…

2026/10/20 23:28:02 阅读更多 →
ASP.NET值班管理系统:排班规则建模与EF Core领域设计

ASP.NET值班管理系统:排班规则建模与EF Core领域设计

简介&#xff1a;这是一套基于ASP.NET Web Forms开发的值班管理信息系统&#xff0c;面向初学者与中小型单位信息化建设者&#xff0c;解决排班登记、人员调度、值班记录查询等日常行政管理痛点。系统采用B/S架构&#xff0c;适配Windows Server 2003VS2010开发环境&#xff0c…

2026/10/20 23:28:02 阅读更多 →
SST-2评测任务配置逐项拆解:从64条开发样本到可复现评测

SST-2评测任务配置逐项拆解:从64条开发样本到可复现评测

1. 评测任务配置的底层逻辑&#xff1a;为什么一个配置文件值得逐项拆解很多人第一次接触评测框架时&#xff0c;注意力都放在模型本身——参数量、推理速度、准确率曲线。但真正跑过几轮完整评测的人会慢慢意识到&#xff0c;决定评测结果是否可信的&#xff0c;往往不是模型&…

2026/10/20 23:26:59 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →