【LeetCode 38】外观数列(Count and Say)从规则理解到代码实现全解析
一、题目要求
给定一个正整数 n,输出第 n 项「外观数列」。
外观数列的规则是:
- 第 1 项是
"1" - 从第 2 项开始,每一项都是对上一项的“读法描述”
所谓“读法描述”,就是:
按顺序统计连续相同数字的个数,然后读出来
示例
n = 1
1
n = 2
读 "1"
一个 1 → 11
n = 3
读 "11"
两个 1 → 21
n = 4
读 "21"
一个 2,一个 1 → 1211
n = 5
读 "1211"
一个1 一个2 两个1 → 111221
前几项完整序列
1
11
21
1211
111221
312211
...
二、解题思路
这道题本质就是:
不断读取上一个字符串,生成下一个字符串
整个过程是一个典型的:
字符串分组统计问题
核心流程:
-
从
"1"开始 -
重复执行 n-1 次:
- 从左到右遍历当前字符串
- 统计连续相同字符的个数
- 拼接:数量 + 字符
-
最终得到第 n 项
如何统计连续字符?
假设当前字符串:
1211
从左到右:
1 → 1个1
2 → 1个2
11 → 2个1
拼接结果:
111221
实现关键点
必须处理好:
- 连续相同字符计数
- 字符变化时结算一段
- 最后一段必须被处理
一个非常实用的技巧:
遍历到 j <= length,让循环“多走一步”,强制触发最后一次结算。
三、完整代码(JavaScript)
var countAndSay = function(n) {
let res = "1";
for (let i = 2; i <= n; i++) {
let cur = "";
let count = 1;
for (let j = 1; j <= res.length; j++) {
if (res[j] === res[j - 1]) {
count++;
} else {
cur += count + res[j - 1];
count = 1;
}
}
res = cur;
}
return res;
};
四、逐行代码解析
初始化第一项
let res = "1";
外观数列从 "1" 开始。
构造第 2 项到第 n 项
for (let i = 2; i <= n; i++)
每一轮根据上一轮结果生成新的字符串。
当前轮构造字符串
let cur = "";
let count = 1;
cur:新字符串count:连续字符数量
遍历上一轮字符串
for (let j = 1; j <= res.length; j++)
注意这里是:
<= res.length
不是 <
目的:
让循环最后多执行一次,用来结算最后一组字符。
如果连续字符相同
if (res[j] === res[j - 1]) {
count++;
}
继续累加。
如果字符发生变化
cur += count + res[j - 1];
count = 1;
说明一段连续字符结束:
例如:
111 → 3 + "1"
拼接后重置计数器。
更新当前结果
res = cur;
本轮生成完成,作为下一轮输入。
返回最终答案
return res;
五、运行过程示例(n = 5)
初始
res = "1"
第2轮
读 1 → 一个1
res = "11"
第3轮
读 11 → 两个1
res = "21"
第4轮
读 21 → 一个2 一个1
res = "1211"
第5轮
读 1211 → 一个1 一个2 两个1
res = "111221"
六、时间复杂度分析
每一轮都会遍历字符串。
字符串长度大约指数增长,但 n 最大一般较小(题目限制)。
时间复杂度近似:
O(n × 当前字符串长度)
空间复杂度:
O(当前字符串长度)
七、常见易错点
- 忘记处理最后一组字符
- 循环写成
< res.length - 没有重置 count
- 拼接顺序写反(字符在前,数量在后)