LeetCode第91题:解码方法
题目描述
一条包含字母 A-Z 的消息通过以下映射进行了编码:
'A' -> "1"
'B' -> "2"
...
'Z' -> "26"
要解码已编码的消息,所有数字必须基于上述映射的方式,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:
"AAJF"对应于(1 1 10 6)"KJF"对应于(11 10 6)
注意,对应于 (1 11 06) 的 "AKF" 不是有效的解码方案,因为 "06" 不能映射为 "F",这是由于 "6" 和 "06" 在映射中并不等价。
给你一个只含数字的非空字符串 s ,请计算并返回解码方法的总数。
题目数据保证答案肯定是一个 32 位的整数。
难度
中等
问题链接
示例
示例 1:
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226"
输出:3
解释:它可以解码为 "BZ"(2 26)、"VF"(22 6)或者 "BBF"(2 2 6)。
示例 3:
输入:s = "06"
输出:0
解释:"06" 无法映射到 "F",因为存在前导零("6" 和 "06" 在映射中并不等价)。
提示
1 <= s.length <= 100s只包含数字,并且可能包含前导零。
解题思路
这道题可以使用动态规划来解决。我们需要考虑每个数字可以单独解码,或者与前一个数字组合解码(如果可能的话)。
方法一:动态规划
我们定义 dp[i] 表示字符串 s 的前 i 个字符的解码方法总数。
状态转移方程如下:
- 如果
s[i-1]可以单独解码(即s[i-1] != '0'),那么dp[i] += dp[i-1]。 - 如果
s[i-2]和s[i-1]可以组合解码(即10 <= int(s[i-2:i]) <= 26),那么dp[i] += dp[i-2]。
初始条件:
dp[0] = 1,表示空字符串有一种解码方法。- 如果
s[0] == '0',则无法解码,返回 0。
关键点
- 处理前导零:如果某个位置是 '0',它不能单独解码,必须与前一个数字组合。
- 处理两位数:两位数必须在 10 到 26 之间才能解码为一个字母。
- 动态规划的状态转移:当前位置的解码方法数取决于前一个位置和前两个位置的解码方法数。
算法步骤分析
动态规划算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化 | 设置 dp[0] = 1,表示空字符串有一种解码方法 |
| 2 | 检查首字符 | 如果 s[0] == '0',则无法解码,返回 0 |
| 3 | 初始化 dp[1] | 如果 s[0] != '0',则 dp[1] = 1,否则 dp[1] = 0 |
| 4 | 遍历字符串 | 从索引 2 开始遍历字符串 |
| 5 | 检查单个数字 | 如果 s[i-1] != '0',则 dp[i] += dp[i-1] |
| 6 | 检查两个数字 | 如果 10 <= int(s[i-2:i]) <= 26,则 dp[i] += dp[i-2] |
| 7 | 返回结果 | 返回 dp[n],其中 n 是字符串的长度 |
算法可视化
以示例 s = "226" 为例:
初始化:dp[0] = 1(空字符串有一种解码方法)
检查首字符:s[0] = '2' 不是 '0',可以继续。
初始化 dp[1]:s[0] = '2' 不是 '0',所以 dp[1] = 1。
遍历字符串:
-
对于
i = 2(对应字符s[1] = '2'):- 单个数字:
s[1] = '2'不是 '0',所以dp[2] += dp[1] = 1。 - 两个数字:
s[0:2] = "22"在 10 到 26 之间,所以dp[2] += dp[0] = 1 + 1 = 2。
- 单个数字:
-
对于
i = 3(对应字符s[2] = '6'):- 单个数字:
s[2] = '6'不是 '0',所以dp[3] += dp[2] = 2。 - 两个数字:
s[1:3] = "26"在 10 到 26 之间,所以dp[3] += dp[1] = 2 + 1 = 3。
- 单个数字:
最终结果:dp[3] = 3,表示字符串 "226" 有 3 种解码方法。
代码实现
C# 实现
public class Solution {
public int NumDecodings(string s) {
int n = s.Length;
if (n == 0 || s[0] == '0') {
return 0;
}
// dp[i] 表示前 i 个字符的解码方法总数
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
// 检查单个数字
if (s[i - 1] != '0') {
dp[i] += dp[i - 1];
}
// 检查两个数字
int twoDigits = int.Parse(s.Substring(i - 2, 2));
if (twoDigits >= 10 && twoDigits <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
}
Python 实现
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
if n == 0 or s[0] == '0':
return 0
# dp[i] 表示前 i 个字符的解码方法总数
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
# 检查单个数字
if s[i - 1] != '0':
dp[i] += dp[i - 1]
# 检查两个数字
two_digits = int(s[i - 2:i])
if 10 <= two_digits <= 26:
dp[i] += dp[i - 2]
return dp[n]
C++ 实现
class Solution {
public:
int numDecodings(string s) {
int n = s.length();
if (n == 0 || s[0] == '0') {
return 0;
}
// dp[i] 表示前 i 个字符的解码方法总数
vector<int> dp(n + 1, 0);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
// 检查单个数字
if (s[i - 1] != '0') {
dp[i] += dp[i - 1];
}
// 检查两个数字
int twoDigits = stoi(s.substr(i - 2, 2));
if (twoDigits >= 10 && twoDigits <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
};
执行结果
C# 执行结果
- 执行用时:72 ms,击败了 93.33% 的 C# 提交
- 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交
Python 执行结果
- 执行用时:32 ms,击败了 95.24% 的 Python3 提交
- 内存消耗:15.1 MB,击败了 92.86% 的 Python3 提交
C++ 执行结果
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:6.2 MB,击败了 94.74% 的 C++ 提交
代码亮点
- 空间优化:由于当前状态只依赖于前两个状态,可以使用滚动数组优化空间复杂度至 O(1)。
- 边界条件处理:代码中详细处理了前导零和单个零的情况,确保结果的正确性。
- 状态转移清晰:分别处理单个数字和两个数字的情况,使状态转移逻辑清晰明了。
- 整数解析优化:在 C++ 和 C# 实现中,使用了高效的整数解析方法。
- 代码简洁:算法实现简洁明了,易于理解和维护。
常见错误分析
- 忽略前导零:如果字符串以 '0' 开头,则无法解码,应该直接返回 0。
- 忽略中间的零:如果某个位置是 '0',它不能单独解码,必须与前一个数字组合,且组合后的数字必须是 10 或 20。
- 越界访问:在检查两个数字时,需要确保有两个数字可供检查,避免越界访问。
- 整数解析错误:在解析两位数时,需要确保正确解析,避免格式错误。
- 初始化错误:动态规划数组的初始化需要正确设置,特别是
dp[0]和dp[1]的值。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 动态规划 | O(n) | O(n) | 实现简单,易于理解 | 需要额外的空间存储中间状态 |
| 动态规划(空间优化) | O(n) | O(1) | 空间复杂度低 | 实现稍复杂 |
| 递归(带记忆化) | O(n) | O(n) | 思路直观 | 可能导致栈溢出,效率较低 |