一条包含字母 A-Z 的消息通过以下方式进行了编码:
‘A’ -> 1
‘B’ -> 2
…
‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
示例 1:
输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
思路分析:一开始吧,我想着用递归进行解码,就是每次判断字符串的前两个字符,看它们能够组合成一个字母的序号,如果能则分两种情况进行解码(两个字符分开,两个字符合为一个),直到字符串为空。
方法一:递归法
class Solution {
public:
int numDecodings(string &s) {
if (s == ""){//如果s串为空了
return 1;
}
if (s[0] == '0'){//首字符不能为零,因为零没有字母对应
return 0;
}
else if (s.size() >= 2 && (s[0] - '0') * 10 + (s[1] - '0') <= 26){//如果前两个字母能合在一起
string s1 = s.substr(1);//将第一个单独作为一个字母的编码
string s2 = s.substr(2);//将前两个字符作为一个字母的编码
if (s[1] != '0'){
return numDecodings(s1) + numDecodings(s2);
}
else{
return numDecodings(s2);
}
}
else{//如果前两个字母不能合在一起,只能将首字符解码为一个字母的编码
string s1 = s.substr(1);
return numDecodings(s1);
}
}
};
尝试进行优化:不难发现,这个递归的算法中存在大量的求子串的语句,当测试字符串比较长时,将消耗大量的时间、空间。
string s1 = s.substr(1);//将第一个单独作为一个字母的编码
string s2 = s.substr(2);//将前两个字符作为一个字母的编码
下边将这部分去掉,取而代之的是下标标记法。
class Solution {
public:
int numDecodings(string &s) {
return myNumDecodings(s, 0);
}
//index表示的是正在解码s串的下标位置
int myNumDecodings(string &s, int index){
int strSize = s.size();
if (index >= strSize){//如果已经解码完成
return 1;
}
if (s[index] == '0'){//(s[index]不能为零,因为零没有字母对应
return 0;
}
else if (index + 1 < strSize && (s[index] - '0') * 10 + (s[index + 1] - '0') <= 26){//如果index 、index + 1两个下标对应的数字能合在一起
if (s[index] != '0'){
return myNumDecodings(s, index + 1) + myNumDecodings(s, index + 2);
}
else{//第二个字符为0时,前两个字符必须合在一起
return myNumDecodings(s, index + 2);
}
}
else{//如果前两个字母不能合在一起,只能将首字符解码为一个字母的编码
return myNumDecodings(s, index + 1);
}
}
};
递归算法的通病,当测试数据比较大的时候,时间复杂度(递归深度)巨增!
下面尝试将其改写为非递归算法。
方法二:利用stack的辅助,将方法一改写为非递归算法。
class Solution {
public:
int numDecodings(string &s) {
int strSize = s.size();
int result = 0;//保存结果
stack indexStack;//辅助栈,当遇到两中可解码的情况的时候,进行保存一种情况
int nowIndex = 0;//正在解码的下标位置
while (!indexStack.empty() || nowIndex <= strSize){
if (nowIndex >= strSize || s[nowIndex] == '0'){//如果已经解码完成(此次解码成功),或者遇到了0(说明此次解码失败)
if (nowIndex >= strSize){
result += 1;
}
if (!indexStack.empty()){
nowIndex = indexStack.top();//回到上一次的保存的解码现场
indexStack.pop();
continue;
}
else{
break;
}
}
if (nowIndex + 1 < strSize && (s[nowIndex] - '0') * 10 + (s[nowIndex + 1] - '0') <= 26){//如果index 、index + 1两个下标对应的数字能合在一起
if (s[nowIndex + 1] != '0'){
indexStack.push(nowIndex + 2);//保存解码两个的现场
nowIndex += 1;//解码一个
}
else{//如果s[nowIndex + 1]是零,则两个必须合在一起解码
nowIndex += 2;
}
}
else {
nowIndex += 1;//默认解码一个
}
}
return result;
}
};
有点懵,改成非递归算法,时间复杂度貌似更大了。。。
方法三:动态规划
class Solution {
public:
int numDecodings(string &s) {
int strSize = s.size();
vector dp(strSize + 1, 0);//dp[i]表示解码的方法数
if(s.size() == 0 || (s.size() == 1 && s[0] == '0')) {
return 0;
}
if(s.size() == 1) {
return 1;
}
dp[0] = 1;
for(int i = 0; i < strSize; ++i){
dp[i+1] = (s[i] == '0' ? 0 : dp[i]);//s[i] == '0' 说明此次解码失败,
if(i > 0 && (s[i-1] == '1' || (s[i-1] == '2' && s[i] <= '6'))){
dp[i+1] += dp[i-1];//加上一次解码两个字符
}
}
return dp[strSize];
}
};
int numDecodings(string s) {
if (s.empty() || s[0] == '0') {
return 0;
}
vector dp(s.size() + 1, 1);//全部初始化为1
for (int i = 2; i < dp.size(); ++i){
dp[i] = ((s[i-1] == '0') ? 0 : dp[i-1]);
if (s[i-2] == '1' || (s[i-2] == '2' && s[i-1] <= '6')){
dp[i] += dp[i-2];
}
}
return dp.back();
}
根据评论区的提醒,这道题也是“爬楼梯”的另一种表述。
和爬楼梯思想完全一致。只考虑当前迈一步,当前迈两步。(映射到这里就是考虑当前位cur,还是考虑前一位prev)
int numDecodings(string s) {
if (s.empty() || s[0] == '0') {
return 0;
}
if (s.size() == 1){
return 1;
}
int ans = 0, cur = 1, prev = 1;
for (int i = 1; i < s.size(); ++i){
ans = 0;
if (s[i] != '0') {//(只考虑当前位)
ans += cur;
} //向前看一位
if (s[i-1] == '1' || (s[i-1] == '2' && s[i] <= '6')){
ans += prev;
}
prev = cur;
cur = ans;
}
return ans;
}