外观数列的第一项是 1,之后每一项是对前一项的"读法":把前一项里连续相同的数字分成一组,读作"个数加数字"。1 读作"一个 1",所以第二项是 11;11 读作"两个 1",第三项是 21;21 读作"一个 2,一个 1",第四项是 1211。给定 n,返回第 n 项。做法就是按定义模拟 n - 1 轮。这道题没有算法上的窍门,值得实测的是这个数列本身的增长规律。本文用真实数据验证数列的增长比例和数字构成,给出五种语言的实现,并把生成过程做成分步演示。
完整题目见官方页面(链接在文末)。基准解用正则表达式 (\d)\1* 一次性匹配每一段连续相同的数字,再替换成"长度加数字",与手写的双指针分组是两种独立的写法。一共 12 组用例,n 取 1 到 7、10、15、20、25、30,都以基准为准逐项比对整个字符串:
输入 [1] 输出 "1" 输入 [2] 输出 "11" 输入 [3] 输出 "21" 输入 [4] 输出 "1211" 输入 [5] 输出 "111221" 输入 [6] 输出 "312211"
我用同一个模拟函数生成了前 50 项,统计每一项的长度、与上一项长度的比值,以及每一项里出现过的数字:
n | 第 n 项的长度 | 与上一项长度之比 | 出现过的数字 1 | 1 | - | 1 2 | 2 | 2.00000 | 1 3 | 2 | 1.00000 | 12 4 | 4 | 2.00000 | 12 5 | 6 | 1.50000 | 12 10 | 20 | 1.42857 | 123 20 | 302 | 1.33628 | 123 30 | 4462 | 1.30850 | 123 40 | 63138 | 1.30423 | 123 50 | 894810 | 1.30316 | 123 (康威常数约 1.30357727,本表最后几行的比值可以对照。)
比值逐渐稳定:第 50 项与第 49 项长度之比约为 1.30316,已经很接近数学上已知的康威常数 1.30357727。第 50 项的长度就有 894810,n 再大一点,字符串长度就是百万级、千万级。另一个观察是:从第 3 项起,数列里出现过的数字始终只有 1、2、3,前 50 项里没有出现过 4 及以上的数字;这里我只验证了 n 不超过 50,没有更一般的证明。
每一轮从头扫描当前字符串:从位置 i 开始,向右找到第一个与 s[i] 不同的位置 j,这一段的长度是 j - i,把"长度加数字"追加到结果,然后从 j 继续。n 次迭代的总代价约等于所有项长度之和,因为长度约每项乘 1.3,总和是最后一项长度的常数倍。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。
Python · 解法代码def solve(n): s = "1" for _ in range(n - 1): # 每一轮:把上一项按"连续相同数字"分组,读作"个数 + 数字" parts = [] i = 0 while i < len(s): j = i while j < len(s) and s[j] == s[i]: j += 1 parts.append(str(j - i) + s[i]) i = j s = "".join(parts) return s
$ python3 solution.py # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON [1] → "1" [2] → "11" [3] → "21" [4] → "1211" [5] → "111221" [6] → "312211"
Java · 解法代码class Solution { public String countAndSay(int n) { String s = "1"; for (int k = 1; k < n; k++) { // 每一轮:把上一项按"连续相同数字"分组,读作"个数 + 数字" StringBuilder sb = new StringBuilder(); int i = 0; while (i < s.length()) { int j = i; while (j < s.length() && s.charAt(j) == s.charAt(i)) j++; sb.append(j - i).append(s.charAt(i)); i = j; } s = sb.toString(); } return s; } }
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl [1] → "1" [2] → "11" [3] → "21" [4] → "1211" [5] → "111221" [6] → "312211"
Go · 解法代码import "strconv" func countAndSay(n int) string { s := "1" for k := 1; k < n; k++ { // 每一轮:把上一项按"连续相同数字"分组,读作"个数 + 数字" buf := make([]byte, 0, len(s)*2) i := 0 for i < len(s) { j := i for j < len(s) && s[j] == s[i] { j++ } buf = strconv.AppendInt(buf, int64(j-i), 10) buf = append(buf, s[i]) i = j } s = string(buf) } return s }
$ go build -o bin . && ./bin < cases.jsonl [1] → "1" [2] → "11" [3] → "21" [4] → "1211" [5] → "111221" [6] → "312211"
JavaScript · 解法代码function countAndSay(n) { let s = "1"; for (let k = 1; k < n; k++) { // 每一轮:把上一项按"连续相同数字"分组,读作"个数 + 数字" const parts = []; let i = 0; while (i < s.length) { let j = i; while (j < s.length && s[j] === s[i]) j++; parts.push(String(j - i) + s[i]); i = j; } s = parts.join(""); } return s; }
$ node run.js < cases.jsonl [1] → "1" [2] → "11" [3] → "21" [4] → "1211" [5] → "111221" [6] → "312211"
C++ · 解法代码#include <string> using namespace std; class Solution { public: string countAndSay(int n) { string s = "1"; for (int k = 1; k < n; k++) { // 每一轮:把上一项按"连续相同数字"分组,读作"个数 + 数字" string t; size_t i = 0; while (i < s.size()) { size_t j = i; while (j < s.size() && s[j] == s[i]) j++; t += to_string(j - i); t += s[i]; i = j; } s = t; } return s; } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl [1] → "1" [2] → "11" [3] → "21" [4] → "1211" [5] → "111221" [6] → "312211"
共 12 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 12/12 Java 通过 12/12 Go 通过 12/12 JavaScript 通过 12/12 C++ 通过 12/12
n 取 20、30、40、45,对比手写分组与正则的耗时,每个规模取三次中最快的一次:
n | 暴力解 | 本文解法
20 | 0.167 ms | 0.101 ms
30 | 2.531 ms | 1.696 ms
40 | 35.805 ms | 24.649 ms
45 | 135.116 ms | 94.708 ms两种做法的耗时都随 n 指数级增长(大约每增加 10 项,耗时增加十几倍(1.3 的 10 次方约 13.8),与长度每项约乘 1.3 一致),手写分组比正则快约 1.4 到 1.7 倍,差别不大。n = 45 时,第 45 项是长度 237746 的字符串,Python 耗时 94.7ms。这说明耗时主要由输出的长度决定,而不是算法细节。
Python 版本另外对 n 从 1 到 35 都和正则版本做了对拍:
随机对拍 35 组(n 从 1 到 35 的随机值),solve 与 brute 的结果全部一致。
另外四种语言只在上面 12 组固定用例上验证过,n 最大到 30。
下面的演示生成第 6 项,从 1 开始逐项推演。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。每一项都是先给出上一项,再一段一段地"读"出来:演示的最终结果是第 6 项 312211。