【LeetCode 38】外观数列(Count and Say)从规则理解到代码实现全解析

10 阅读3分钟

【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. "1" 开始

  2. 重复执行 n-1 次:

    • 从左到右遍历当前字符串
    • 统计连续相同字符的个数
    • 拼接:数量 + 字符
  3. 最终得到第 n 项


如何统计连续字符?

假设当前字符串:

1211

从左到右:

1 → 1个1
2 → 1个2
11 → 2个1

拼接结果:

111221

实现关键点

必须处理好:

  1. 连续相同字符计数
  2. 字符变化时结算一段
  3. 最后一段必须被处理

一个非常实用的技巧:

遍历到 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;

说明一段连续字符结束:

例如:

1113 + "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(当前字符串长度)

七、常见易错点

  1. 忘记处理最后一组字符
  2. 循环写成 < res.length
  3. 没有重置 count
  4. 拼接顺序写反(字符在前,数量在后)