LeetCode刷题之字符串

310 阅读11分钟

242. 有效的字母异位词(Easy)

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

示例 1:

输出: true

示例 2:

输出: false  

说明:

  • 你可以假设字符串只包含小写字母。

进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

题解

这道题可以使用HashMap来存储两个字符串中字符出现的次数,然后比较即可,字符串只包含小写英文的话可以使用数组来存储出现次数

class Solution {
    public boolean isAnagram(String s, String t) {
        int[] counts = new int[26];
        
        //遍历s,统计每个字符出现的次数
        for (char c : s.toCharArray()) {
            counts[c - 'a']++;
        }
        
        //遍历t,统计出现次数,出现一次就减一
        for (char c : t.toCharArray()) {
            counts[c - 'a']--;
        }
        
        //遍历counts,如果全为0,则相等
        for (int count : counts) {
            if (count != 0) {
                return false;
            }
        }
        
        return true;
    }
}

409. 最长回文串(Easy)

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。 在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。

注意: 假设字符串的长度不会超过 1010。

示例 1:

"abccccdd"  
输出:  
7

解释: 我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。

题解

回文串是一个对称的结构,中间的那个字符可以只有一个,其他的得是偶数才行,使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。

class Solution {
    public int longestPalindrome(String s) {
        int[] cnts = new int[256];
        for (char c : s.toCharArray()) {
            cnts[c]++;
        }
        int palindrome = 0;
        for (int cnt : cnts) {
            //把奇数转换成偶数个
            palindrome += (cnt / 2) * 2;
        }
        if (palindrome < s.length()) {
            palindrome++;   // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间
        }
        return palindrome;
    }
}

205. 同构字符串

给定两个字符串 s 和 t,判断它们是否是同构的。如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。

所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。

示例 1:

输出: true

示例 2:

输出: false

示例 3:

输出: true

说明: 你可以假设 s 和 t 具有相同的长度。

解法一

“特征值”解法,两个字符同构,那么对应位置的字符出现的位置相同,比如上面的示例3,paper可以表示为12134title也可以表示为12134,只需要比较最后的特征值是否相同即可。

class Solution {
    public boolean isIsomorphic(String s, String t) {
        //准备两个数组,记录每个字符的特征值
        int[] schar = new int[256];
        int[] tchar = new int[256];

        StringBuilder sStr = new StringBuilder();
        StringBuilder tStr = new StringBuilder();

        int sCount = 0;
        int tCount = 0;

        for (int i = 0; i < s.length(); i++) {
            //如果当前字符没出现过,sCount++,再存储进去,然后添加进sStr
            if (schar[s.charAt(i)] == 0) {
                schar[s.charAt(i)] = ++sCount;
                sStr.append(schar[s.charAt(i)]);
            } else {
                sStr.append(schar[s.charAt(i)]);
            }

            if (tchar[t.charAt(i)] == 0) {
                tchar[t.charAt(i)] = ++tCount;
                tStr.append(tchar[t.charAt(i)]);
            } else {
                tStr.append(tchar[t.charAt(i)]);
            }
        }

        return sStr.toString().equals(tStr.toString());
    }
}

解法二

记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。这种解法和上种解法有着异曲同工之妙,都是一样的思想,关键点都是相对应的字符,特征值得相同,这个解法的特征值是当前字符前面是否出现过

public boolean isIsomorphic(String s, String t) {
    int[] preIndexOfS = new int[256];
    int[] preIndexOfT = new int[256];
    for (int i = 0; i < s.length(); i++) {
        char sc = s.charAt(i), tc = t.charAt(i);
        if (preIndexOfS[sc] != preIndexOfT[tc]) {
            return false;
        }
        //需要更新为i+1,如果不更新的话就无法判断当前字符前面是否出现过,比如“ab”和“aa”
        preIndexOfS[sc] = i + 1;
        preIndexOfT[tc] = i + 1;
    }
    return true;
}

647. 回文子串(Medium)

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。

示例 1:

输入: "abc"  
输出: 3  
解释: 三个回文子串: "a", "b", "c".

示例 2:

输入: "aaa"  
输出: 6  
说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".

注意: 输入的字符串长度不会超过1000。

解法一

很容易就想到使用暴力解法来解题,题目要求出回文串的数量,那么就需要判断出区间[i, j]里的子串是不是回文子串,遍历所有的子串,统计出结果。步骤:

  1. 定义一个判断区间[i, j]是不是回文子串的函数
  2. 遍历所有的区间
class Solution {
    public int countSubstrings(String s) {
        char[] chars = s.toCharArray();
        int result = 0;
        //外层的循环控制区间的start
        for (int i = 0; i < s.length(); i++) {
            //内层控制end,单个字符本身就是一个回文子串,所以end取i+1,最后再加上字符串的长度
            for (int j = i + 1; j < s.length() && j < s.length(); j++)
                if (isPalindrome(chars, i, j))
                    result++;
        }
        return result + s.length();
    }

    //判断是否为回文子串
    private boolean isPalindrome(char[] chars, int start, int end) {
        for (int i = start, j = end; j > i; i++, j--) {
            if (chars[i] != chars[j])
                return false;
        }
        return true;
    }
}

解法二:动态规划。

解法一有很多的重复,且本题问回文子串数量,因此可以考虑使用动态规划来进行优化。一个子串是回文串有两种情况。

  • 只有一个字符
  • 去掉首尾两个字符之后仍然是回文串,所以转移方程可得:flag[i][j] = true if (chars[i] == chars[j] && (j - i < 2 || flag[i + 1][j - 1]))
public int countSubstrings2(String s) {
    char[] chars = s.toCharArray();
    int result = 0;
    boolean[][] flag = new boolean[chars.length][chars.length];

    //控制右边界
    for (int j = 0; j < chars.length; j++) {
        //控制左边界
        for (int i = j; i >= 0; i--) {
            if (chars[i] == chars[j] && (j - i < 2) || flag[i + 1] == flag[j - 1]) {
                flag[i][j] = true;
                result++;
            }
        }
    }
    return result;
}

解法三:中心扩展

以字符串中的每一个字符都当作回文串中间的位置,然后向两边扩散,每当成功匹配两个左右两个字符,结果 res 自增1,然后再比较下一对。注意回文字符串有奇数和偶数两种形式,如果是奇数长度,那么中心位置就是中间那个字符的位置,所以左右两遍都从i开始遍历;如果是偶数长度的,那么中心是最中间两个字符的左边那个,右边那个就是 中心+1,这样就能 cover 所有的情况,而且都是不同的回文子字符串

public int countSubstrings3(String s) {
    int res = 0;
    //遍历所有的中心点
    int n = s.length();
    for (int i = 0; i < n; i++) {
        //子串长度为奇数
        helper(s, i, i, res);
        //子串长度为偶数
        helper(s, i, i + 1, res);
    }

    return res;
}

private void helper(String s, int i, int j, int res) {
    while (i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)) {
        //往外不断扩展
        i--;
        j++;
        res++;
    }
}

696. 计数二进制子串(Easy)

给定一个字符串 s,计算具有相同数量0和1的非空(连续)子字符串的数量,并且这些子字符串中的所有0和所有1都是组合在一起的。 重复出现的子串要计算它们出现的次数。

示例 1 :

输出: 6  
解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
请注意,一些重复出现的子串要计算它们出现的次数。

另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。

示例 2 :

输入: "10101"  
输出: 4  
解释: 有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。

注意:

s.length 在1到50,000之间。
s 只包含“0”或“1”字符。

暴力解法

这种解法和上题的暴力解法类似,都是准备一个函数验证某个子串是不是有效的,然后遍历所有子串。这种方法会超时。

public int countBinarySubstrings(String s) {
    int result = 0;

    if (s.length() < 2) {
        return result;
    }

    //遍历所有子串,外循环是子串长度
    for (int i = 2; i <= s.length(); i += 2) {
        //内循环是子串开始的下标
        for (int j = 0; j + i - 1 < s.length(); j++) {
            if (helper(s.toCharArray(), j, j + i - 1)) {
                result++;
            }
        }
    }

    return result;
}


//判断是否为有效子串
public boolean helper(char[] chars, int i, int j) {
    boolean flag = false;
    //左右两边的字符相等,肯定不是有效子串
    char left = chars[i];
    char right = chars[j];
    if (left == right) {
        return flag;
    }

    int lCount = 0;
    int mCount = (j - i + 1) / 2;

    //统计左边连续相等字符的个数
    int index = i;
    while (index < j) {
        if (chars[index++] == left) {
            lCount++;
        } else {
            break;
        }
    }

    //左边数量==右边数量,说明是有效子串
    if (lCount == mCount) {
        flag = true;
    }

    return flag;
}

往外扩展

和上题的“中心扩展”类似,找到“01”和“10”字符串往外扩展

private int count = 0;

public int countBinarySubstrings(String s) {
    //遍历开始点
    for (int i = 1; i < s.length(); i++) {
        //出现“01”的情况
        if (s.charAt(i - 1) == '0' && s.charAt(i) == '1') {
            BinarySubstring(s, i-1, i);
        }
        // 出现 ‘10’ 的情况
        if(s.charAt(i-1) == '1' && s.charAt(i) == '0') {
            BinarySubstring(s, i-1, i);
        }
    }
    return count;
}

private void BinarySubstring(String s, int start, int end) {
    char f = s.charAt(start);
    char e = s.charAt(end);
    //往外扩展
    while (start >= 0 && end < s.length() && s.charAt(start) == f && s.charAt(end) == e) {
        start++;
        end--;
        count++;
    }
}

分组计数

  • 给定的字符串s是由连续的多组01构成的,01的组是交替出现的,假设有4个1与3个0相连, 1111000 可以构成的子串有111000110010,共3个;假设有3个0与2个1相连00011,可以构成的子串有001101,共两个。
  • 由此可以发现,两个相邻的01组,可以构成的有效子串是比较短的那组的长度。
  • 所以只需要通过一次遍历,记录相连两分组的长度即可。preCount表示前一个分组的长度,curCount表示当前分组的长度。
public int countBinarySubstrings(String s) {
    int result = 0;
    char[] chars = s.toCharArray();

    //准备两个变量,curCount=1是因为当前元素就是当前组的第一个元素
    int preCount = 0;
    int curCount = 1;

    for (int i = 1; i < chars.length; i++) {
        //前一个元素和当前元素相等
        if (chars[i - 1] == chars[i]) {
            curCount++;
        } else {
            //当前组的数量已经统计完,计算有效子串的数量
            result += Math.min(preCount, curCount);
            //更新
            preCount = curCount;
            curCount = 1;
        }
    }
    
    //循环结束后还有最后一组要和前一组计数
    return result + Math.min(preCount, curCount);
}

9. 回文数(Easy)

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

示例 1:

输入: 121
输出: true

示例 2:

输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

示例 3:

输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。

进阶:
你能不将整数转为字符串来解决这个问题吗?

转换为字符串

  • 负数不是回文数
  • 转换为字符串之后采用双指针的方式来逐个比较首尾
public boolean isPalindrome(int x) {
    if (x < 0) {
        return false;
    }

    boolean flag = true;

    //转换为字符串
    String s = Integer.toString(x);
    int start = 0;
    int end = s.length() - 1;
    //首尾逐个比较
    while (start < end) {
        if (s.charAt(start) == s.charAt(end)) {
            start++;
            end--;
        } else {
            flag = false;
            break;
        }
    }

    return flag;
}

反转数字

  • 采用和反转链表类似的方法,将后半段数字反转。同样的,负数不能为回文数字
  • 个位为0但是本身不为0的数字(x % 10 == 0 && x != 0)不为回文数字,因为除了0没有任何数的最高位为0
public boolean isPalindrome(int x) {
    //x为负数,或者x的个位为0且不为0,如果x不等于0,那么其他数的高位不可能为0
    if (x < 0 || (x % 10 == 0 && x != 0)) {
        return false;
    }
    
    int halfReverse = 0;
    //如果x < halfReverse,说明已经反转了一半的数字
    while (x > halfReverse) {
        halfReverse = halfReverse * 10 + x % 10;
        x /= 10;
    }
    
    //x长度为奇数时,可以通过halfReverse / 10的方式去除中间的那个数
    return x == halfReverse || x == halfReverse / 10;
}