String类题目

目录

1.1 无重复字符的最长字串(中等):滑动窗口/双指针

1.2 N字形变换(中等):很绕,理解即可

1.3 字符串转换整数(中等):实现API题

1.4 整数转罗马数字(中等):

1.5 最长公共前缀(简单):依次遍历

1.6 有效括号(简单):栈的先入后出特性

1.7 外观数列(中等):遍历 + 双指针

1.8 找出字符串中第一个匹配项的下标(简单):一次遍历

1.9 字符串相乘(中等):一次遍历

1.10 字母异位词分组(中等):排序

1.11 最后一个单词的长度(简单):从后遍历

1.12 简化路径(中等):双端队列

1.13 解码方法(中等):动态规划

1.14 复原IP地址(中等):回溯法

1.15 字符串总结


1.1 无重复字符的最长字串(中等):滑动窗口/双指针

题目:给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

思想:

滑动窗口作法:

通过枚举其中一个字符串可以发现:

  • 假设选择字符串中的第k个字符作为起始位置,并且得到了不包含重复字符的最长字串的结束位置为rk

  • 当我们选择第k + 1个字符作为起始位置时,首先从k + 1rk的字符显然是不重复的,并且少了原本的第k个字符,因此可以继续增大rk,直到右侧出现了重复字符为止

采用滑动窗口:

  • 用两个指针表示字符串中的某个字串的左右边界,左指针表示字串的起始位置,右指针为上文的rk

  • 每一次将左指针向右移动一格,开始枚举下一个字符作为起始位置吗,不断地向右移动右指针,保证两个指针对应的字串中没有重复的元素

  • 每轮结束后,找到的最长的子串的长度即为答案

去重操作:

  • 在判断是否具有重复的字符,常用的数据结构为哈希集合

  • 当左指针向右移动时,从哈希集合中移除一个字符,当右指针向右移动时,向哈希集合中添加一个字符

双指针做法:

  • 开始时,左指针移动,并记录字符出现过的次数;

    • 此时记录的巧妙方法:设置一个int数组,数组开始时长度为128,对应所有字符数,当对应字符出现过,将数组默认值+1

  • 当有一个字符出现超过一次时,右指针右移,并将对应位置元素出现次数减一,直到字符出现次数不再大于1


总结:理解什么时候移动指针,如何计算长度,如何删除原有元素


代码

class Solution {
    public int lengthOfLongestSubstring(String s) {
        //设置一个哈希集合存储元素:可以去重
        Set set = new HashSet<>();
​
        //设置右指针rk:开始时右指针位于数组的左侧(还未开始移动)
        int rk = -1;
        int res = 0;
​
        for(int i = 0; i < s.length(); i++){
            //此时i就是左指针,每次左指针右移时,哈希集合中删除上一个元素
            if(i > 0){
                set.remove(s.charAt(i - 1));
            }
            //每次右指针右移时,尝试增大rk,并向哈希集合中添加元素(此时要判断是否重复, 不重复才添加元素)
            while(rk + 1 < s.length() && !set.contains(s.charAt(rk + 1))){
                set.add(s.charAt(rk + 1));
                rk++;
            }
            //此时的长度就是从 i 到 rk个字符,长度为 rk - i + 1
            res = Math.max(res, rk - i + 1);
        }
​
        return res;
    }
}
​
//双指针
class Solution {
    public int lengthOfLongestSubstring(String s) {
        //假设初始字符串的出现的次数全为0
        int[] count = new int[128];
        int res = 0;
​
        //设置左右指针,开始时左指针右移
        for(int i = 0, j = 0; i < s.length(); i++){
            //对于出现过的字符,将其出现次数 + 1
            count[s.charAt(i)]++;
            //当字符重复出现时,右指针不断右移,直到删掉了重复元素
            while(count[s.charAt(i)] > 1){
                --count[s.charAt(j++)];
            }
            res = Math.max(res, i - j + 1);
        }
        return res;
    }
}

1.2 N字形变换(中等):很绕,理解即可

题目:将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

行数为3:
P   A   H   N
A P L S I I G
Y   I   R
​
行数为4
P     I    N
A   L S  I G
Y A   H R
P     I

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

思想:按顺序遍历字符串s

  • res[i] += c:将字符c存入对应行si

  • i += flag:更新当前字符c对应的行索引

  • flag = -flag达到Z字形转折点时,执行反向


总结:这里的方法过于巧妙,一般人想不出,记录一下


代码

class Solution {
    public String convert(String s, int numRows) {
        //如果给定行数小于2,直接返回
        if(numRows < 2){
            return s;
        }
        //创建一个列表存储最终结果
        List rows = new ArrayList<>();
​
        //根据给定的numRows,确定共有几行,即有几个si
        for(int i = 0; i < numRows; i++){
            rows.add(new StringBuilder());
        }
​
        //设置flag,开始时为-1
        int flag = -1;
        //设置行索引
        int i = 0;
​
        for(char c : s.toCharArray()){
            //res[i] += c
            rows.get(i).append(c);
            //达到z字形转折点:0 或者最后一个索引位置;将flag变为相反数,达到Z字形的目的
            if(i == 0 || i == numRows - 1){
                flag = -flag;
            }
            i += flag;
        }
        StringBuilder res = new StringBuilder();
        for(StringBuilder row : rows){
            res.append(row);
        }
        return res.toString();
    }
}

1.3 字符串转换整数(中等):实现API题

题目:请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数 myAtoi(string s) 的算法如下:

  1. 读入字符串并丢弃无用的前导空格

  2. 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。

  3. 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。

  4. 将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。

  5. 如果整数数超过 32 位有符号整数范围 [−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为 −231 ,大于 231 − 1 的整数应该被固定为 231 − 1

  6. 返回整数作为最终结果。

思想:根据给的步骤,我们可以得出几点:

  • 根据步骤1,需要去除前导空格

  • 根据步骤2,设置一个变量sign,初始化为1,若存在负号,则变为-1

  • 根据步骤3,判断是否为数字则直接使用ASCII码比较,'0'<= char <= '9'

  • 根据步骤3,当遇到第一个不是数字的字符时,退出循环

  • 根据步骤5,转换后数字超过int需要截取,对于所有结果都要考虑索引越界的情况


总结:根据题目,找出题目已有限制,基于限制进行求解


代码

class Solution {
    public int myAtoi(String s) {
        //先将字符串转换为数组,方便计算
        char[] chars = s.toCharArray();
​
        int len = s.length();
​
        //1.去掉前置空格,使用index来判断有多少个空格,从index后取值即可去掉前置空格
        int index = 0;
        while(index < len && chars[index] == ' '){
            index++;
        }
        //若字符串中都是空格,直接返回0
        if(index == len){
            return 0;
        }
​
        //从index开始取值,就能去掉前置空格
        char start = chars[index];
​
        //记录一个sign,判断为正还是为负,若为空,则不处理
        int sign = 1;
        if(start == '+'){
            index++;
        }else if(start == '-'){
            index++;
            sign = -1;
        }
        //若不包含正负号,则直接跳过if-else if判断
        int res = 0;
​
        while(index < len){
            char currChar = chars[index];
​
            //如果不是数字,结束循环
            if(currChar < '0' || currChar > '9'){
                break;
            }
            
            //索引越界的判断
            //当前res*10已经大于最大值,或者res*10刚好等于最大值,但个位数大于int限制,直接返回最大值
            if(res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && (currChar - '0') > Integer.MAX_VALUE % 10)){
                return Integer.MAX_VALUE;
            }
            //当前res*10已经小于最小值,或者res*10刚好等于最小值,但个位数大于int限制,直接返回最小值
            if(res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && (currChar - '0') > -(Integer.MIN_VALUE % 10))){
                return Integer.MIN_VALUE;
            }
​
            res = res * 10 + sign * (currChar - '0');
​
            index++;
        }
        return res;
    }
}

1.4 整数转罗马数字(中等):

题目:罗马数字包含以下七种字符: IVXLCDM

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II27 写做 XXVII, 即为 XX + V + II

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。

  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。

  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。

思想:将罗马数字对应的所有数值表示出来,共13个字符,将它们按照降序排列,每次寻找到最大的一个不超过num的值

由于罗马数字的唯一表示法,给定的一个整数num,求得它的罗马数字值需要经过两步:

  • 先找到第一个不超过num的最大数值value,然后用num减去这个最大数值,将value记录

  • 继续寻找不超过num - balue的最大数值,将接下来的数值继续记录,直到num = 0


总结:最大值一定是峰值,可将该题转换思路进行解决


代码

class Solution {
    //按照降序排列,然后依次寻找不大于num的最大值
    int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
    String[] strs = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
​
    public String intToRoman(int num) {
        StringBuffer res = new StringBuffer();
​
        //遍历这个value
        for(int i = 0; i < values.length; i++){
            int value = values[i];
            String str = strs[i];
            //如果当前值大于等于value,说明找到了第一个不大于num的最大值
            //这里用while而不是if:比如3 : III, 只能使用while才能将所有I存入数组,如果是其它判断完就结束
            while(num >= value){
                num -= value;
                res.append(str);
            }
            if(num == 0){
                break;
            }
        }
        return res.toString();
    }
}

1.5 最长公共前缀(简单):依次遍历

题目:编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""

思想:依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新到最长公共前缀;

  • 当尚未遍历完所有的字符串时,最长公共前缀已经是空字符串,则最长公共前缀一定是空字符串,直接返回

步骤:

  • 从数组中的第一个字符串开始,先比较第一个字符串和第二个字符串的公共前缀,并将公共前缀更新,依次遍历下去

  • 比较公共前缀时,注意最多比较到两个字符串中较小长度的那一个


总结:依次比较每一个字符串与下一个字符串的最长公共前缀,然后每次更新最长公共前缀并返回即可


代码

class Solution {
    public String longestCommonPrefix(String[] strs) {
        //若strs为空,直接返回
        if(strs == null || strs.length == 0){
            return "";
        }
​
        //从第一个字符串开始比较
        String prefix = strs[0];
        int len = strs.length;
​
        //依次比较字符串每次的最长公共前缀,并用一个prefix存储每次的值
        for(int i = 1; i < len; i++){
            prefix = getPrefix(prefix, strs[i]);
​
            //若某一次最长公共前缀已经为空,直接结束循环
            if(prefix.length() == 0){
                break;
            }
        }
        return prefix;
    }
​
    public String getPrefix(String str1, String str2){
        //从最小长度开始寻找
        int len = Math.min(str1.length(), str2.length());
        int index = 0;
​
        //确定 str1 和 str2 的最长前缀长度
        while(index < len && str1.charAt(index) == str2.charAt(index)){
            index++;
        }
        return str1.substring(0, index);
    }
}

1.6 有效括号(简单):栈的先入后出特性

题目:给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

思想:使用栈来进行有效判断:先将有效括号存入一个Map集合中,方便计算

  • 开始时,栈为空,将s的第一个字符存入

  • 第二次循环,判断栈顶元素和s的第二个字符是否匹配,是则将元素出栈,否则继续入栈;依此类推

  • 最终检查栈中是否有元素,有就说明不匹配


总结:依次比较每一个字符串与下一个字符串的最长公共前缀,然后每次更新最长公共前缀并返回即可


代码

class Solution{
    
    Map map = new HashMap<>(){{
        put(')', '(');
        put('}', '{');
        put(']', '[');
    }};
    
    public boolean isValid(String s){
        Deque stack = new LinkedList<>();
        int len = s.length();
        for(int i = 0; i < len; i++){
            //判断栈顶元素和`s`的第二个字符是否匹配,是则将元素出栈,否则继续入栈;依此类推
            if(stack.size() > 0 && map.get(s.charAt(i)) == stack.peek()){
                stack.pop();
            }else{
                stack.push(s.charAt(i));
            }
        }
        //检查栈中是否有元素,有就说明不匹配
        return stack.size() == 0;
    }
}

1.7 外观数列(中等):遍历 + 双指针

题目:给定一个正整数 n ,输出外观数列的第 n 项。

「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。

你可以将其视作是由递归公式定义的数字字符串序列:

  • countAndSay(1) = "1"

  • countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。

例如,数字字符串 "3322251" 的描述如下图:

思想:外观数列,其实就是依次统计字符串中连续相同字符的个数;因此:定义Si代表countAndSay(i),若要求出Sn,则需要先求出Sn-1,也就是从左到右扫描字符串Sn-1中连续相同字符的最大数量,然后将字符串的统计数目转换为数字字符串再连接上对应的字符

  • S1 = 1开始,按照上述方法依次生成S2,S3,...,Sn


总结:重点是想清楚每次分别实现每一次的Sn的搜寻工作,两个指针的方式是一个选择


代码

class Solution {
    public String countAndSay(int n) {
        //S1 = 1
        String str = "1";
​
        for(int i = 2; i <= n; i++){
            //设置一个存储Sn的字符串
            StringBuffer sn = new StringBuffer();
            //声明两个指针,都从字符串的第一个值开始搜索,判断有几个str.charAt(start)
            int start = 0;
            int pos = 0;
​
            while(pos < str.length()){
                //当pos小于str.length()时,判断有几个str.charAt(start)
                while(pos < str.length() && str.charAt(pos) == str.charAt(start)){
                    pos++;
                }
                sn.append(Integer.toString(pos - start)).append(str.charAt(start));
                //此时说明已找到了pos个相同的start,从start后接着找
                start = pos;
            }
            str = sn.toString();
        }
        return str;
    }
}

1.8 找出字符串中第一个匹配项的下标(简单):一次遍历

题目:给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

思想:将字符串neddle和字符串 haystack 的所有长度为 m的子串均匹配一次;

  • 具体实现:让 haystack 每次的值为 i + jneedle值为 j即可


总结:重点如何让长度为 m的子串均匹配一次;


代码

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length();
        int m = needle.length();
​
        //将字符串needle和haystack的所有长度为m的子串均匹配一次
        for(int i = 0; i + m <= n; i++){
            boolean flag = true;
            //将needle和haystack长度为m的子串匹配
            for(int j = 0; j < m; j++){
                if(haystack.charAt(i + j) != needle.charAt(j)){
                    flag = false;
                    break;
                }
            }
            if(flag){
                return i;
            }
        }
        return -1;
    }
}

1.9 字符串相乘(中等):一次遍历

题目:给定两个以字符串形式表示的非负整数 num1num2,返回 num1num2 的乘积,它们的乘积也表示为字符串形式。

注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。

思想:num1num2可以每次分别取个位、十位、百位等等等相乘,将相对应的值存储在一个结果数组res中;结果数组的长度为 num1 + num2的长度之和

  • 比如num1的第i个数和num2的第j个数的乘积可以使用如下方式给出:

  • int sum = res[i + j + 1] + value1 * value2;

    res[i + j + 1] = sum % 10;

    res[i + j] += sum / 10;


总结:重点是每次的乘积如何存储在结果数组中


代码

class Solution {
    public String multiply(String num1, String num2) {
        //如果 num1 或者 num2 有一个为0,结果一定为0
        if(num1.equals("0") || num2.equals("0")){
            return "0";
        }
​
        //取得 num1 和 num2 的值
        int len1 = num1.length();
        int len2 = num2.length();
​
        //创建一个数组记录最终的结果值
        int[] res = new int[len1 + len2];
​
        //从每个数的个位开始相乘,并从右向左移动
        for(int i = len1 - 1; i >= 0; i--){
            int value1 = num1.charAt(i) - '0';
            for(int j = len2 - 1; j >= 0; j--){
                int value2 = num2.charAt(j) - '0';
                //将 value1 * value2 并加上每次的进位
                int sum = res[i + j + 1] + value1 * value2;
                res[i + j + 1] = sum % 10;
                res[i + j] += sum / 10;
            }
        }
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < res.length; i++){
            //判断最终结果的第一个数字是否为 0;不能写为res[0] == 0;不然有时候数组第一个值为0,每次都会continue
            if(i == 0 && res[i] == 0){
                continue;
            }
            sb.append(res[i]);
        }
        return sb.toString();
    }
}

1.10 字母异位词分组(中等):排序

题目:给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

思想:两个字符串互为字母异位词,则两个字符串包含的字符一定相同,同一组字母异位词中的字符串排序后具备相同的字符,可以使用排序后的字符作为一组字母异位词的标志

  • 使用哈希表存储每一组字母异位词,哈希表的键为一组字母异位词的排序后的字符,哈希表的值为一组字母异位词列表

  • 遍历每个字符串,对于每个字符串得到该字符串所在的一组字母异位词的标志,将当前字符串加入该族字母异位词的列表中,遍历全部字符串后,哈希表中的每个键值对即为一组字母异位词


总结:重点是想清楚每次分别实现每一次的Sn的搜寻工作,两个指针的方式是一个选择


代码

class Solution {
    public List> groupAnagrams(String[] strs) {
        //创建一个哈希表存储最终结果:键为一个字符串的所有字符,值为所有的字母异位词,因此应该是一个二维列表
        Map> map = new HashMap<>();
​
        //遍历字符串
        for(String str : strs){
            //将字符串的值重排序后,存入键中:如此一来,所有字母互位词的排序都是一样的,会存到一个list中
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);
            //在map中寻找是否有key相关联的值,没有则返回一个新列表
            List list = map.getOrDefault(key, new ArrayList());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList>(map.values());
    }
}

1.11 最后一个单词的长度(简单):从后遍历

题目:给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

思想:从后向前遍历,找到第一个不为空的字符,然后从该字符开始,统计最后一个单词的长度


总结:从后向前遍历是通用解法,只不过需要先排除最后一个单词后面也有为空的情况


代码

class Solution {
    public int lengthOfLastWord(String s) {
        char[] cs = s.toCharArray();
        int res = 0;
        int index = s.length() - 1;
        //从后往前找到第一个不为空的元素
        while(s.charAt(index) == ' '){
            index--;
        }
        //从这个元素开始,寻找不为空元素的个数
        for(int i = index; i >= 0; i--){
            if(cs[i] == ' '){
                break;
            }
                res++;
        }
        return res;
    }
}

1.12 简化路径(中等):双端队列

题目:给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

  • 始终以斜杠 '/' 开头。

  • 两个目录名之间必须只有一个斜杠 '/'

  • 最后一个目录名(如果存在)不能'/' 结尾。

  • 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.''..')。

返回简化后得到的 规范路径

思想:由于文件名经过 '/'分割后只有四种情况:

  • 空字符串

  • 一个点 '.'

  • 两个点 '..'

  • 其他目录名

对于前两种,可以不进行处理,因为不会切换目录;对于后两种我们将其放入栈中,具体是:

  • 两个点 '..',需要将目录切换到上一级,即弹出栈顶元素

  • 目录,将其存入栈中

最终,从栈底到栈顶的字符串用 '/'相连接,并在最前面加上 '/'表示根目录即可


总结:从后向前遍历是通用解法,只不过需要先排除最后一个单词后面也有为空的情况


代码

class Solution {
    public String simplifyPath(String path) {
        //将字符串按照'/'分开
        String[] names = path.split("/");
​
        //使用栈来存储元素
        Deque deque = new LinkedList<>();
​
        //只处理两种情况: ".." 和 "目录名"
        for(String name : names){
            if("..".equals(name)){
                if(!deque.isEmpty()){
                    deque.pollLast();
                }
            }else if(name.length() > 0 && !".".equals(name)){
                deque.offerLast(name);
            }
        }
​
        //对栈中的元素进行处理,从栈底到栈顶依次加上'/'
        StringBuffer sb = new StringBuffer();
        if(deque.isEmpty()){
            sb.append("/");
        }else{
            while(!deque.isEmpty()){
                sb.append("/");
                sb.append(deque.pollFirst());
            }
        }
        return sb.toString();
    }
}

1.13 解码方法(中等):动态规划

题目:一条包含字母 A-Z 的消息通过以下映射进行了 编码

'A' -> "1"
'B' -> "2"
...
'Z' -> "26"

解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:

  • "AAJF" ,将消息分组为 (1 1 10 6)

  • "KJF" ,将消息分组为 (11 10 6)

注意,消息不能分组为 (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6""06" 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数

题目数据保证答案肯定是一个 32 位 的整数。

思想:使用动态规划解决此问题即可:假设fi表示字符串s的前i个字符s[1,2,...,i]的解码方法数,我们针对最后一个字符解码时有两种情况:

  • 情况一:最后一次解码时使用了一个字符,则只要s[i] != 0,就可以被解码为某个字母,解码方法数可以表示为:fi = fi - 1, s[i] != 0

  • 情况二:最后一次解码时使用了两个字符,则只要s[i - 1] != 0,并且s[i] + s[i - 1]组成的整数小于等于26即可;解码方法数可以表示为:fi = fi - 2, s[i - 1] != 0 && 10*s[i − 1] + s[i]≤26

  • 两种情况之和即为最终答案

边界条件为:f0 = 1,空字符串可以解码为空字符串


总结:从后向前遍历是通用解法,只不过需要先排除最后一个单词后面也有为空的情况


代码

class Solution {
    public int numDecodings(String s) {
        //设置一个数组,用来存储结果值
        int n = s.length();
        int[] f = new int[n + 1];
        //空字符串解析为空字符串,有且仅有一种解码方法
        f[0] = 1;
​
        //利用动态规划方程求解
        for(int i = 1; i <= n; i++){
            //第一种情况:最后一次解码时使用了一个字符;注意此时下标减1,保持和语言中下标从0开始一致
            if(s.charAt(i - 1) != '0'){
                f[i] += f[i - 1];
            }
            //第二种情况: 最后一次解码时使用了两个字符
            if(i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)){
                f[i] += f[i - 2];
            }
        }
        return f[n];
    }
}

1.14 复原IP地址(中等):回溯法

题目有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201""192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""[email protected]"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

思想:题目要求寻找所有可能的IP地址,因此可以采用回溯法,寻求所有的可行解,并返回

  • 注意:从第二个元素开始,要排除前导为0的情况


总结:寻求所有可能结果,首选回溯法


代码

class Solution {
    //创建两个列表,一个用来存储最终结果,一个用来存储临时结果值
    List res = new ArrayList<>();
    List temp = new ArrayList<>();
​
    public List restoreIpAddresses(String s) {
        //从 第0个segId 开始寻找,地址一共有四段
        dfs(s, 0, 4);
        return res;
    }
​
    public void dfs(String s, int i, int k){
        //当搜索到最后一个阶段,则将每一段的值用 "." 分开存入
        if(k == 0){
            if(i == s.length()){
                res.add(String.join(".", temp));
            }
            return;
        }
​
        //若未到最后一个阶段,则从i开始进行搜索;每个地址只有三个数,因此j < i + 3
        for(int j = i; j < s.length() && j < i + 3; j++){
            //第二个数开始,前导不能为0
            if(s.charAt(i) == '0' && j > i){
                return;
            }
            //拿到第 i 到 j 个元素并转换为int类型
            int v = Integer.parseInt(s.substring(i, j + 1));
            if(v >= 0 && v <= 255){
                temp.add(s.substring(i, j + 1));
                dfs(s, j + 1, k - 1);
                //回溯
                temp.remove(temp.size() - 1);
            }
        }
    }
}

1.15 字符串总结

字符串类题目可以有几种求解思路:

  • 将字符串转换为数组,或者直接用字符串的charAt()方法,将字符串内的元素当作一个个数组索引一样考虑

  • 字符串问题可以多使用StringBuffer等,具有丰富的API,能够方便的求解

你可能感兴趣的:(算法)