38. 外观数列
给定一个正整数 n ,输出外观数列的第 n 项。
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。
你可以将其视作是由递归公式定义的数字字符串序列:
countAndSay(1) = “1”
countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
到这里有点不知所云,看下面例子就知道题目具体要求了。
示例1:
1
11
21
1211
111221 第一项是数字 1 描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11" 描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21" 描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211" 描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
示例2:
输入:n = 1
输出:“1”
解释:这是一个基本样例。
示例3:
输入:n = 4
输出:“1211”
解释:
countAndSay(1) = “1”
countAndSay(2) = 读 “1” = 一 个 1 = “11”
countAndSay(3) = 读 “11” = 二 个 1 = “21”
countAndSay(4) = 读 “21” = 一 个 2 + 一 个 1 = “12” + “11” = “1211”
提示:
1 <= n <= 30
看到题目首先想到递归,但是功力不足,想不出来,于是转战顺序遍历生成
顺序遍历
class Solution {
public:
string num = "1"; //当前的数字
int times = 1; //频率
char* before_num = new char[2]; //申请两个字节
string result; //根据当前数字得到的描述字符串
string countAndSay(int n) {
before_num[1] = '\0'; //把第二个位置变为'\0',结束符号
for (int i = 1; i<n; i++) { //循环n-1次
result = "";
before_num[0] = num[0];
for (int j = 1; j<num.size(); j++) { //循环整个数值长度,除了第一个
if (before_num[0] == num[j]) { //如果相同则增加次数
times++;
}
else { //如果遇到不相同的
string times_str = std::to_string(times); //转化为字符串
result.append(times_str);
result.append(before_num);
before_num[0] = num[j];
times = 1;
}
}
string times_str = std::to_string(times); //因为最后一个字符被跳掉了,再次执行一次
result.append(times_str);
result.append(before_num);
num = result;
times = 1;
}
return num;
}
};
编写过程中,学到了一些东西,如:
new char(X)与new char[X]区别
- new char(X)是申请一个字节的空间,然后初始化为X。
- new char[X]是申请X个字节的空间,没有初始化。
例如下面代码
//new char(X)
char *str = new char(97);//a
cout << str << endl;
//new char[X]
char *str2 = new char[2];
str2[0] = 98;//b
str2[1] = 99;//c
我们希望输出是
a
bc
但是实际输出是
后面部分乱码了,这是因为申请空间时,并没有把下一个字符变为结束符’\0’,使得编译器以为后面一连串的空间都是我们想要的,要解决这个问题,就多申请一个空间,然后把最后一个赋值为为’\0’
char *str2 = new char[3];
str2[0] = 98;//b
str2[1] = 99;//c
str2[2] = '\0';
std::to_string() 数字转化为string
用法:string a = std::to_string(5);
顺序遍历2
为什么上面要使用一个char*类型来存放一个字节的数据呢?
因为一开始不知道
str.append(num,chr)
就可以把num
个字符变量chr
追加到str
后面。或者直接使用重载的+即可!
str = str+chr
官方的代码很简洁
class Solution {
public:
string countAndSay(int n) {
string prev = "1";
for (int i = 2; i <= n; ++i) {
string curr = "";
int start = 0;
int pos = 0;
while (pos < prev.size()) {
while (pos < prev.size() && prev[pos] == prev[start]) {
pos++;
}
curr += to_string(pos - start) + prev[start];
start = pos;
}
prev = curr;
}
return prev;
}
};
改变了思路,主要通过两个索引指针来进行定位,而出现的频次直接通过前后索引相减得到!
复杂度分析
- 时 间 复 杂 度 : O ( N × M ) 时间复杂度:O(N \times M) 时间复杂度:O(N×M) 其中 N 为给定的正整数,M 为生成的字符串中的最大长度。
- 空 间 复 杂 度 : O ( M ) 。 空间复杂度:O(M)。 空间复杂度:O(M)。 其中 M 为生成的字符串中的最大长度。
递归求解
参考大佬的解法,下面是原文关于递归的描述,醍醐灌顶
使用递归求解,一定不要用大脑去模拟递归的过程。大脑能压几个栈?
正确的做法是:记住递归函数的定义。比如本题中的递归函数
countAndSay(int n)
含义是当取值为 n 时的外观数列。那么,必须先求出取值为n−1 时的外观数列,怎么求?根据递归函数的定义,就是
countAndSay(n - 1)
。至于countAndSay(n - 1)
怎么算的,我们不用管。只要知道这个函数能给我们正确的结果就行。
class Solution {
public:
string countAndSay(int n) {
if (n == 1) { // 递归的出口
return "1";
}
string before = countAndSay(n - 1);
string res;
char cur = before[0];
int count = 1;
for (int i = 1; i < before.size(); ++i) {
if (before[i] != cur) {
res += to_string(count) + cur;
cur = before[i];
count = 0;
}
count ++;
}
res += to_string(count) + cur;
return res;
}
};
计数的做法就是一开始自己编写的顺序遍历,利用一个变量来存储出现的次数,而且到最后把最后一个字符算进去。
复杂度分析
与上面相同。递归的时间复杂度和遍历是一样的,因为 1…n 中的每个数字都被计算了一次。