1. 解码方法 1
这个题跟Leetcode——把数字翻译成字符串.基本思路一致
提示:
- 1 <= s.length <= 100
- s 只包含数字,并且可能包含前导零。
(1)直接递归
- 先截取一个,只要不是0肯定是符号条件的
- 接着截取两个判断是否符合条件,如果符合条件就截取
我们可以发现,如果没有条件限制的话,这题解法和爬楼梯完全一样,递归公式其实就是个斐波那契数列
- dp[i]=dp[i-1]+dp[i-2]
(直接递归包含大量重复计算,会超时)
public class Solution {
public int numDecodings(String s) {
return helper(s.toCharArray(), s.length(), 0);
}
private int helper (char[] chars, int length, int index) {
//递归的终止条件,找到了解码的方法
if (index >= length)
return 1;
//遇到0跳过,0肯定和上一个截取成两个了
if (chars[index] == '0') {
return 0;
}
//截取一个数字只要不是0肯定是符合条件的
int res = helper(chars, length, index + 1);
//判断截取两个的时候是否符合条件,如果也符合条件,就截取两个(符合条件,解码结果+1)
if (index < length - 1 && (chars[index] == '1' || chars[index] == '2' && chars[index + 1] <= '6')) {
res += helper(chars, length, index + 2);
}
return res;
}
}
由于包含大量的重复计算,直接导致运行超时(可以使用备忘录算法)
我们可以把计算的结果存到map中(一个数组),下次运算的时候如果map中有就直接从map中取,如果没有在计算,计算之后再把结果存到map中。
class Solution {
public int numDecodings(String s) {
int[] map = new int[s.length()];
Arrays.fill(map, -1);
return helper(s.toCharArray(), s.length(), 0, map);
}
private int helper(char[] chars, int length, int index, int[] map) {
if (index >= length)
return 1;
if (chars[index] == '0')
return 0;
//先从map中取,如果有就直接取出,如果没有在计算
if (map[index] != -1)
return map[index];
int res = helper(chars, length, index + 1, map);
if (index < length - 1 && (chars[index] == '1' || chars[index] == '2' && chars[index + 1] <= '6'))
res += helper(chars, length, index + 2, map);
//把计算的结果在存储到map中
map[index] = res;
return res;
}
}
(2)动态规划(判断边界条件)
如果没有条件限制的话,这题解法和爬楼梯完全一样,递归公式其实就是个斐波那契数列
只不过这里都有条件限制,但原理都差不多,我们只需要根据条件来判断哪一项该加,哪一项不该加
public class Solution {
// dp[i]=dp[i-1]+dp[i-2]
public int numDecodings(String s) {
int len = s.length();
if(len == 0) {
return 0;
}
// dp[i] 以 s[i] 结尾的前缀子串有多少种解码方法
//最终dp[i] 存的是 dp[i] 与 dp[i - 1]是否符合组合条件
// 计算的是 当前i 与 i-1 是否能组成两位,i == 1时需要特判
int[] dp = new int[len];
if (s.charAt(0) == '0') { //首位等于0, 直接返回
return 0;
}
dp[0] = 1; //初始化dp[0]
for (int i = 1; i < len; i++) {
if (s.charAt(i) != '0') //当前i处字符不为0,可以考虑dp[i] = dp[i - 1]
dp[i] = dp[i - 1];
if (s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2' && s.charAt(i) <= '6') { //当前为10 - 26 之间时,考虑两种情况
if (i == 1) //i为第二位时,相当于初始化第一个值
dp[i]++;
else
dp[i] += dp[i - 2]; //当前两位用做一组,取前两位
}
}
return dp[len - 1];
}
}
也可以这样写:
public class Solution {
public int numDecodings(String s) {
int len = s.length();
if (len == 0) {
return 0;
}
// dp[i] 以 s[i] 结尾的前缀子串有多少种解码方法
// dp[i] = dp[i - 1] * 1 if s[i] != '0'
// dp[i] += dp[i - 2] * 1 if 10 <= int(s[i - 1..i]) <= 26
int[] dp = new int[len];
char[] charArray = s.toCharArray();
if (charArray[0] == '0') {
return 0;
}
dp[0] = 1;
for (int i = 1; i < len; i++) {
if (charArray[i] != '0') {
dp[i] = dp[i - 1];
}
int num = 10 * (charArray[i - 1] - '0') + (charArray[i] - '0');
if (num >= 10 && num <= 26) {
if (i == 1) {
dp[i]++;
} else {
dp[i] += dp[i - 2];
}
}
}
return dp[len - 1];
}
}
(3)动态规划(减少边界条件判断)
- 这里在 i == 1 的时候需要多做一次判断,而这种情况比较特殊,为了避免每次都做判断,可以把状态数组多设置一位。为此修改状态定义,与此同时,状态转移方程也需要做一点点调整。
- dp[i] 定义成长度为 i 的前缀子串有多少种解码方法(以 s[i - 1] 结尾的前缀子串有多少种解法方法)
public class Solution {
// dp[i]=dp[i-1]+dp[i-2]
public int numDecodings(String s) {
int len = s.length();
if(len == 0) {
return 0;
}
//dp[i] 定义成长度为 i 的前缀子串有多少种解码方法(以 s[i - 1] 结尾的前缀子串有多少种解法方法)
//最终dp[i] 存的是 dp[i - 1] 与 dp[i - 2]
//状态转移方程,类似爬楼梯: dp[i]=dp[i-1]+dp[i-2]
int[] dp = new int[len + 1];
dp[0] = 1; //初始化dp[0]
if (s.charAt(0) == '0') { //首位等于0, 直接返回
return 0;
}
for (int i = 1; i < len + 1; i++) {
//第一种情况:截取前一个
if (s.charAt(i - 1) != '0')
dp[i] = dp[i - 1];
//第二种情况:截取前两个个
if (i >= 2 && (s.charAt(i - 2) == '1' || s.charAt(i - 2) == '2' && s.charAt(i - 1) <= '6')) { //当前为10 - 26 之间时,考虑两种情况
dp[i] += dp[i - 2];
}
}
return dp[len];
}
}
类似斐波那契数列空间复杂度是可以优化的,只需要两个变量即可,不需要申请一个数组,我们来对着修改一下
public int numDecodings(String s) {
int length = s.length();
int lastLast = 0;
int last = 1;
for (int i = 0; i < length; i++) {
int cur = 0;
//判断截取一个是否符合(只要不是0,都符合)
if (s.charAt(i) != '0')
cur = last;
//判断截取两个是否符合
if (i >= 1 && (s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2' && s.charAt(i) <= '6'))
cur += lastLast;
lastLast = last;
last = cur;
}
return last;
}
2. 解码方法 2
(1)动态规划
- 解析当前字符, 一位
- 解析当前字符与前一个字符, 两位
class Solution {
public int numDecodings(String s) {
int n = s.length();
int mod = (int) 1e9 + 7;
if(n == 0 || s.charAt(0) == '0')
return 0;
long[] dp = new long[n + 1];
char[] a = (" " + s).toCharArray();
dp[0] = 1;
for (int i = 1; i <= n; i++) {
if (a[i] == '*') {
//当前a[i] == '*'时:单独解码,9种情况
dp[i] = (9 * dp[i - 1]) % mod;
//当前a[i] == '*'时:与 i - 1位一起解码, 三种情况:a[i - 1]为:1,2,*
if (a[i - 1] == '1')
dp[i] = (dp[i] + 9 * dp[i - 2]) % mod;
else if (a[i - 1] == '2')
dp[i] =(dp[i] + 6 * dp[i - 2]) % mod;
else if (a[i - 1] == '*')
dp[i] =(dp[i] + 15 * dp[i - 2]) % mod;
} else if (a[i] == '0') {
//当前a[i] == '0'时:与 i - 1位一起解码, 三种情况:a[i - 1]为:1,2,*
if (a[i - 1] == '1')
dp[i] = dp[i - 2];
else if (a[i - 1] == '2')
dp[i] = dp[i - 2];
else if (a[i - 1] == '*')
dp[i] = 2 * dp[i - 2] % mod;
//当前a[i] == '0'时:等于0必须与前一位一起解码
else
return 0;
} else {
//当前位不为0和*时:单独解码
dp[i] = dp[i - 1];
//当前位不为0和*时:与 i - 1位一起解码, 三种情况:a[i - 1]为:1,2,*
if(a[i - 1] == '1')
dp[i] = (dp[i] + dp[i - 2]) % mod;
else if(a[i - 1] == '2' && a[i] >= '1' && a[i] <= '6')
dp[i] = (dp[i] + dp[i - 2]) % mod;
else if(a[i - 1] == '*'){
if( a[i] >= '7' && a[i] <= '9')
dp[i] = (dp[i] + dp[i - 2]) % mod;
else
dp[i] = (dp[i] + 2 * dp[i - 2]) % mod;
}
}
}
return (int) (dp[n] % mod);
}
}
3. 字符串解码
(1)辅助栈
- 难点在于括号内嵌套括号,需要从内向外生成与拼接字符串,这与栈的先入后出特性对应。
- 构建辅助栈 stack, 遍历字符串 s 中每个字符 c
- 当 c 为数字时,将数字字符转化为数字 multi,用于后续倍数计算;
- 当 c 为字母时,在 res 尾部添加 c;
- 当 c 为 [ 时,将当前 multi 和 res 入栈,并分别置空置0:
- 记录此 [ 前的临时结果 res 至栈,用于发现对应 ] 后的拼接操作;
- 记录此 [ 前的倍数 multi 至栈,用于发现对应 ] 后,获取 multi × […] 字符串。
- 进入到新 [ 后,res 和 multi 重新记录。
- 当 c 为 ] 时,stack 出栈,拼接字符串 res = last_res + cur_multi * res,其中
- last_res是上个 [ 到当前 [ 的字符串,例如 “3[a2[c]]” 中的 a;
- curMul是当前 [ 到 ] 内字符串的重复倍数,例如 “3[a2[c]]” 中的 2。
模拟一下这个过程:
以3[a2[c]]为例子
3
multi:3
mulStack:[]
resStack:[]
res:""
3[
multi:0
mulStack:[3]
resStack:[""]
res:""
3[a
multi:0
mulStack:[3]
resStack:[""]
res:"a"
3[a2
multi:2
mulStack:[3]
resStack:[""]
res:"a"
3[a2[
multi:0
mulStack:[3,2]
resStack:["a"]
res:""
3[a2[c
multi:0
mulStack:[3,2]
resStack:["a"]
res:"c"
3[a2[c]
multi:0
mulStack:[3,2] 出栈一个2
resStack:["a"] 出栈一个a
res:"c"
这里是重点:
2 次 res : cc 再加上栈中的前一个值 "acc "
对应代码为:
int curMul = mulStack.pop();
StringBuilder temp = new StringBuilder();
for (int i = 0; i < curMul; i++)
temp.append(res);
res = new StringBuilder(resStack.pop() + temp);
3[a2[c]]
multi:0
mulStack:[3] 出栈一个3
resStack:[""]
res:"acc"
3次res: "accaccacc"
输出结果res
具体代码为:
class Solution {
public String decodeString(String s) {
// 思路: 乘法和 递推公式 前一个和作为下一个加法的加数
// 3[a]2[bc] = 3a+2bc = (3a + "") + 2bc
// 3[a2[c]] = 3(2c + a) = 3(2c + a) + ""
// 1. 初始化倍数和res 及其对应栈
int multi = 0;
StringBuilder res = new StringBuilder();
Deque<Integer> mulStack = new LinkedList<>();
Deque<String> resStack = new LinkedList<>();
// 2. 遍历字符
char[] chars = s.toCharArray();
for (char ch : chars) {
// 3. 统计倍数,为啥要乘10?因为是从左往右扫描的,如果k不是个位数而是n位整数的话就要通过不停的乘10来更新值
if (ch >= '0' && ch <= '9')
multi = multi * 10 + (ch - '0');
// 4. 统计res
else if (ch >= 'a' && ch <= 'z')
res.append(ch);
// 5. 入栈并重置临时变量,【
//确保倍数所成都为括号里面字符
else if (ch == '[') {
mulStack.push(multi);
resStack.push(res.toString());
// 重置开始下一轮重新统计
multi = 0;
res = new StringBuilder();
// 6. 出栈做字符串乘法和加法,】
} else {
int curMul = mulStack.pop();
StringBuilder temp = new StringBuilder();
// 乘以当前统计字符串res
for (int i = 0; i < curMul; i++)
temp.append(res);
// 加上前一个统计字符串作为当前res
res = new StringBuilder(resStack.pop() + temp);
}
}
return res.toString();
}
}
(2)递归
思路和迭代一摸一样,唯一的差别在于【】被用作递归开始与终止的条件
class Solution {
public String decodeString(String s) {
return dfs(s, 0)[0];
}
//首先解释下String[]是什么
//当String[]的长度为2时,第二个元素为解码出的子字符串,第一个元素为解码出的子字符串的最后字符']'在s中的下标
//当String[]的长度为1时,数组中只存储了解码出的字符串
//dfs函数的意思:对字符串s从i往后的子串进行解码,并存在数组里;必要的时候还会在数组里存子串最后那个']'的下标
private String[] dfs(String s, int i){
StringBuilder res = new StringBuilder();
//multi为k[encoded_string]的k,是一个正整数
int multi = 0;
while (i < s.length()){
//当前字符为数字,需要更新multi的值
//为啥要乘10?因为是从左往右扫描的,如果k不是个位数而是n位整数的话就要通过不停的乘10来更新值
if (s.charAt(i) >= '0' && s.charAt(i) <= '9')
multi = 10 * multi + Integer.parseInt(String.valueOf(s.charAt(i)));
//当前字符为'[',此时需要递归地去解码'['后面的子串
else if (s.charAt(i) == '['){
//子串从'['的下一位开始,用tmp保存解码的结果和子串最后的']'在s中的下标
String[] tmp = dfs(s, i + 1);
//更新i的值,由于在上一行的递归中子串以及子串内部的子串都被求出来了,所以在外层就不用管它们了,直接把i跳到tmp[0]表示的位置
i = Integer.parseInt(tmp[0]);
//这个while循环达到了把k[encoded_string]内的encoded_string在res后拼接k次的效果(这里的encoded_string就是tmp[1])
while (multi > 0){
res.append(tmp[1]);
multi--;
}
}
//当前字符为']',返回这个子串结尾处的下标和其解码结果
else if (s.charAt(i) == ']'){
return new String[] {String.valueOf(i), res.toString()};
}
//当前字符为非数字、非'['']',则把它拼接到res后
else {
res.append(String.valueOf(s.charAt(i)));
}
//i后移
i++;
}
return new String[] {res.toString()};
}
}