91. 解码方法
一条包含字母 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) 。
解析
递归和动态规划两种解法,重点理解如何从递推转换成动态规划。
方法1:递归
从后往前递推。
算法思路:
以 22067
为例,从后往前遍历:
- 首先如果为 7 ,很显然是 1 种,即 7 -> G
- 如果为 67,很显然还是 1 种,即 67 -> FG
- 如果为 067,结果为 0
- 如果为 2067,结果为 numDecodings(20 67) + numDecodings(2 067) = numDecodings(20 67) -> TFG
- 如果为 22067,结果numDecodings(2 2067) + numDecodings(22 067) = numDecodings(2 2067) -> BTFG。
从中,我们可以看出规律:
- 如果开始的数为0,结果为 0;
- 如果开始的数加上第二个数 <= 26, 结果为 numDecodings(start+1) + numDecodings(start+2);
- 如果开始的数加上第二个数 > 26, 结果为 numDecodings(start+1).
参考代码1:
class Solution {
// 递归 时间O(n) 空间O(n)
public int numDecodings(String s) {
if (s == null || s.length() == 0) {
return 0;
}
return helper(s, 0);
}
// 递归的套路,加一个index控制递归的层次
private int helper (String s, int start) {
// 递归终止条件
if (s.length() == start) {
return 1;
}
// 以 0 位开始的数是不存在的
if (s.charAt(start) == '0') {
return 0;
}
/**
* 递归的递推公司应该是如果 index 的后两位 <= 26,
* 则:helper(s, start) = helper(s, start + 1) + helper(s, start + 2)
* 否则:helper(s, start) = helper(s, start + 1)
*/
int ans1 = helper(s, start + 1);
int ans2 = 0;
if (start < s.length() - 1) {
int ten = (s.charAt(start) - '0') * 10;
int one = (s.charAt(start + 1) - '0');
if (ten + one <= 26) {
ans2 = helper(s, start + 2);
}
}
return ans1 + ans2;
}
}
复杂度分析
- 时间复杂度:O(n)。
- 空间复杂度:O(n)。
方法2:动态规划
递归解法存在大量的重复计算从中可以看出,在计算中进行了大量的重复计算,因此。可以想办法将重叠子问题记录下来,避免重复计算。
引入一个数组dp[],用来记录以某个字符为开始的解码数。动态规划其实就是一个填表的过程。整个过程的目标就是要填好新增的dp[]数组。
参考代码2:从后往前推
class Solution {
// 从后往前推
// DP 时间O(n) 空间O(n)
public int numDecodings(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int len = s.length();
int[] dp = new int[len + 1];
dp[len] = 1;
if (s.charAt(len - 1) == '0') {
dp[len - 1] = 0;
} else {
dp[len - 1] = 1;
}
for (int i = len - 2; i >= 0; i--) {
if (s.charAt(i) == '0') {
dp[i] = 0;
continue;
}
if ((s.charAt(i) - '0') * 10 + (s.charAt(i + 1) - '0') <= 26) {
dp[i] = dp[i + 1] + dp[i + 2];
} else {
dp[i] = dp[i + 1];
}
}
return dp[0];
}
}
参考代码3:从前往后推
class Solution {
/**
* 1. 定义子问题
* s[i] : i-1...i 每个元素可以译码方法总数
* 2. 定义状态
* dp[i] 表示以s[i]结尾的前缀字符串有多种解码方法
* 3. DP方程
* (1) 如果当前字符不为0,则当前字符解码方法总数为累加前一个前缀字符解码数 dp[i] += dp[i-1]
* (2) 前一个字符为1 或者 (2 且 当前字符 <= 6)
*/
public int numDecodings(String s) {
if(s == null) {
return 0;
}
int n = s.length();
if (n == 0 || s.charAt(0) == '0') {
return 0;
}
int[] dp = new int[n];
dp[0] = 1;
for (int i = 1; i < n; i++) {
// 如果当前字符不为0,则当前字符解码方法总数为累加前一个前缀字符解码数
if (s.charAt(i) != '0') {
dp[i] += dp[i-1];
}
// 前一个字符为1 或者 (2 且 当前字符 <= 6)
if (s.charAt(i-1) == '1' || (s.charAt(i-1) == '2' && s.charAt(i) <= '6')) {
if (i - 2 > 0) {
dp[i] += dp[i-2];
} else {
dp[i]++;
}
}
}
return dp[n-1];
}
}
参考代码4:空间优化
细心的话,会发现我们其实并不需要申请一个长度为len+1的数组来存储中间过程。其实dp[i]只和dp[i+1]以及dp[i+2]相关。
因此,此处可以继续空间压缩。
class Solution {
// 时间O(n) 空间O(1)
public int numDecodings(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int len = s.length();
int help = 1;
int res = 0;
if (s.charAt(len - 1) != '0') {
res = 1;
}
for (int i = len - 2; i >= 0; i--) {
if (s.charAt(i) == '0') {
help = res;
res = 0;
continue;
}
if ((s.charAt(i) - '0') * 10 + (s.charAt(i + 1) - '0') <= 26) {
res += help;
//help用来存储res以前的值
help = res-help;
} else {
help = res;
}
}
return res;
}
}
部分图片来源于网络,版权归原作者,侵删。