Leetcode——解码方法1,2(把数字翻译成字符串)/ 字符串解码

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)动态规划

  1. 解析当前字符, 一位
  2. 解析当前字符与前一个字符, 两位

在这里插入图片描述

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()};
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yawn__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值