原题地址:https://leetcode.com/problems/decode-ways/
题目描述
A message containing letters from A-Z is being encoded to numbers using the following mapping:
一段仅包含英文A-Z字符的信息被按如下映射编码成了一个数字串:
'A' -> 1
'B' -> 2
...
'Z' -> 26
Given an encoded message containing digits, determine the total number of ways to decode it.
给出一个编码后的数字串,求总共有几种解码方式。
For example,
示例,
Given encoded message “12”, it could be decoded as “AB” (1 2) or “L” (12).
编码后的数字串为”12”,他可以以AB(1 2)或L(12)的方式解码。
The number of ways decoding “12” is 2.
因此”12”的解码方式共有2种。
解题思路
我们可以很容易的想到这个题应该用动态规划来解这个题,在每一步种考虑当前是取下一个数字来解码还是取下两个数字来解码,简单说来就是f(x)=f(x+1)或者f(x)=f(x+1)+f(x+2)的问题。
对于动态规划的问题,我们常用的一种方法是递归,然而对于这个题,如果我们用递归的话很容易就超时了。事实上,当我第一次在leetcode上提交时,就是用递归来做的,而且也毫无意外的超时了。因为会有多次重复的递归操作。这时候我们还可以采用的一种以空间换时间的方式来存储已经求过一次的值,然而这样的话依然不是很好。我们应该想一种不用递归的方法。
对于动态规划,除了递归之外还常用的就是循环了。例如,求阶乘时,我们不用f(x)=x*f(x-1),而是递归从1开始求积(1*2*3*…*x);求斐波那契数列时,我们不用f(x)=f(x-1)+f(x-2),而是从1开始求和(1+2,2+3,3+5….)。对于这个题,我们同样可以用循环来解。
我们不难注意到,对于上述两例,有如下规律:
阶乘:常用的方法是从大往小调用,f(x)=x*f(x-1);而循环的方式是从小往大求积。
斐波那契数列:常用的方法是从大往小调用f(x)=f(x-1)+f(x-2);而循环的方式是从小往大求和。
循环的方式总是跟递归的方式按相反的方向前进
声明:以上仅仅是个人发现的小规律,其正确性有待验证,请不要轻信。如有错误,还望大家指正。
我们按上面的规律来观察这个题,如果按递归的方式的话,当然是(大概思路)求第i位的值时调用第i+1和i+2位的值。所以,用循环的话我们应该反过来,先求后面的,再求前面的。
我们用x表示第i+1之后的所有字符的解码方式数目,y表示第i+2位开始往后的所有字符的解码方式数目,然后根据第i位值即可使用x和y来求得第i位之后所有字符的解码方式数目。
算法描述
- 设最后一位之后的解码方式为y=1
- 如果最后一位为0,则x=0(即单独一个0无法解码),否则x=1
- 往前移动,如果当前数字为1,其后面跟什么字符都是可以的,因此x+y是其解码方式数目,更新x和y
- 如果当前数字为2,则其后面只能是0,1,2,3,4,5,6,如果其后一位是这些数字,则x+y是其解码方式数目,更新x和y;否则,x是其解码方式数目,更新y=x。
- 如果当前数字为0,以0开头的字符串不可能正确解码,因此更新y=x,x=0.
- 如果当前数字为3-9中的一个数字,则以其开头的字符串一定只能以其为单独字符进行解码,所以x就是其解码方式数目,更新x不变,y=x
- 重复以上3-6,直至第1个字符,最后返回x的值就是我们要求的值。
简析
算法时间复杂度为O(n),使用常数空间,时间特性和空间特性都很优秀。
为了正确的返回结果,我们还需要处理几组特殊值。
- 空串,返回0
- 数字串长度为1,如果其值为”0”,则返回0,否则返回1
其余情况均可以按照算法来进行。
代码
int numDecodings(char* s) {
int len = strlen(s); // 获取待解码字符串的长度
char *p = s + len - 1; // 指针指向最后一个字符
if (len == 0) // 如果是空串,返回0(特殊值)
return 0;
else if (len == 1) // 如果只有一个字符,根据该字符是否为0返回
return *p == '0' ? 0 : 1; // 如果为"0"返回0,否则返回1
int x, y = 1, tmp; // 初始化y=1,即最后一位之后的解码方式只有一种
x = *p == '0' ? 0 : 1; // 根据最后一个字符初始化x
while (--p >= s) { // 从后至前遍历字符串
if (*p == '0') { // 当前字符为0
y = x; // y赋值x,以备下次循环使用
x = 0; // x置零,以0开头的串不可解码
}
else if (*p == '1') { // 当前字符为1,后面是任意字符都可以有两种解码方式
tmp = x; // 暂存x
x += y; // x=x+y
y = tmp; // y赋值x,以备下次循环使用
}
else if (*p == '2') { // 当前字符为2
if (*(p + 1) < '7') { // 后面是0-6中的字符则有两种解码方式
tmp = x;
x += y;
y = tmp;
}
else // 后面是7-9中点字符,当前字符只有一种解码方式
y = x;
}
else // 当前为3-9中的字符,当前字符只有一种解码方式
y = x;
}
return x;
}
运行情况
Status:Accept
Time:4ms
优化代码
代码中有些地方是可以优化的,对于上面的代码,不管待解码的字符串长度是多少,都会遍历整个字符串。其实对于某些特殊情况下可以提前结束循环。
提前结束循环的情况仅仅是由于0字符的特殊情况,除0外点其他字符不论如何搭配都至少有一种解码方式,但是如果出现 00/30/40/50/60/70/80/90 之中的任意一个子串,则无法解码;或者如果第一个字符是0的话也无法解码。
判断提前结束的方式有两种:
- 可以判断x和y是否都为0,因为x的增长方式只有x+=y,如果x和y都为0的话,不论如何增长,x都永远为0;
- 可以只在当前字符为0的时候判断,判断其是否是第一个字符或者其前面的字符是否是0或者3-9之中的字符,如果是则结束循环。
优化一
int numDecodings(char* s) {
int len = strlen(s); // 获取待解码字符串的长度
char *p = s + len - 1; // 指针指向最后一个字符
if (len == 0) // 如果是空串,返回0(特殊值)
return 0;
else if (len == 1) // 如果只有一个字符,根据该字符是否为0返回
return *p == '0' ? 0 : 1; // 如果为"0"返回0,否则返回1
int x, y = 1, tmp; // 初始化y=1,即最后一位之后的解码方式只有一种
x = *p == '0' ? 0 : 1; // 根据最后一个字符初始化x
while (--p >= s) { // 从后至前遍历字符串
if (*p == '0') { // 当前字符为0
if (p == s) // 如果第一个字符是0,返回0
return 0;
if (*(p - 1) > '2' || *(p - 1) == '0') // 如果前面的字符不是1或2,则返回0
return 0;
else {
y = x; // y赋值x,以备下次循环使用
x = 0; // x置零,以0开头的串不可解码
}
}
else if (*p == '1') { // 当前字符为1,后面是任意字符都可以有两种解码方式
tmp = x; // 暂存x
x += y; // x=x+y
y = tmp; // y赋值x,以备下次循环使用
}
else if (*p == '2') { // 当前字符为2
if (*(p + 1) < '7') { // 后面是0-6中的字符则有两种解码方式
tmp = x;
x += y;
y = tmp;
}
else // 后面是7-9中点字符,当前字符只有一种解码方式
y = x;
}
else // 当前为3-9中的字符,当前字符只有一种解码方式
y = x;
}
return x;
}
运行情况
Status:Accept
Time:2ms
优化二
int numDecodings(char* s) {
int len = strlen(s); // 获取待解码字符串的长度
char *p = s + len - 1; // 指针指向最后一个字符
if (len == 0) // 如果是空串,返回0(特殊值)
return 0;
else if (len == 1) // 如果只有一个字符,根据该字符是否为0返回
return *p == '0' ? 0 : 1; // 如果为"0"返回0,否则返回1
int x, y = 1, tmp; // 初始化y=1,即最后一位之后的解码方式只有一种
x = *p == '0' ? 0 : 1; // 根据最后一个字符初始化x
while (--p >= s) { // 从后至前遍历字符串
if (*p == '0') { // 当前字符为0
y = x; // y赋值x,以备下次循环使用
x = 0; // x置零,以0开头的串不可解码
}
else if (*p == '1') { // 当前字符为1,后面是任意字符都可以有两种解码方式
tmp = x; // 暂存x
x += y; // x=x+y
y = tmp; // y赋值x,以备下次循环使用
}
else if (*p == '2') { // 当前字符为2
if (*(p + 1) < '7') { // 后面是0-6中的字符则有两种解码方式
tmp = x;
x += y;
y = tmp;
}
else // 后面是7-9中点字符,当前字符只有一种解码方式
y = x;
}
else // 当前为3-9中的字符,当前字符只有一种解码方式
y = x;
// 如果xy都是0,返回0
if (x == 0 && y == 0)
return 0;
}
return x;
}
运行情况
Status:Accept
Time:1ms
总结
使用递归求值时超时,使用循环方式仅用几毫秒。时间复杂度O(n),常数级空间。
对于代码优化,第一种方式中可以提前结束,减少遍历次数,但是其中有多层分支判断;第二种方式可以提前结束,减少遍历次数,而且分支判断相对来说简单,因此性能最好。
测试数据
"" : 0
"0" : 0
"00" : 0
"100" : 0
"11" : 2
"10" : 1
"01" : 0
"1010" : 1
"12345678910" : 3
"22316258210" : 12
// 个人学习记录,若有错误请指正,大神勿喷
// sfg1991@163.com
// 2015-05-13