字符串相关

本文深入讲解了多种字符串处理算法,包括字母统计、回文串识别、字符串同构判断、回文子串计数及二进制子串分析,提供了详细的代码实现与优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

两个字符串包含的字母是否完全相同

力扣242

由于本题的字符串只包含 26 个小写字符,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,不再使用 HashMap。

class Solution {
    public boolean isAnagram(String s, String t) {
        int[] num = new int[26];
        for (char c : s.toCharArray()) {
            num[c - 'a']++;
        }
        for (char c : t.toCharArray()) {
            num[c - 'a']--;
        }
        for (int i : num) {
            if (i != 0){
                return false;
            }
        }
        return true;
    }
}

借助哈希表

class Solution {
    public boolean isAnagram(String s, String t) {
        Map<Character,Integer> map = new HashMap<>();
        for (char c : s.toCharArray()) {
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        for (char c : t.toCharArray()) {
            Integer cnt = map.get(c);
            //s中 不包含字母c 
            if (cnt == null) return false;
            map.put(c, cnt - 1);
        }
        for (Character character : map.keySet()) {
            if (map.get(character) != 0) return false;
        }
        return true;
    }
}

将 s 和 t 排序

class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) return false;
        char[] str1 = s.toCharArray();
        char[] str2 = t.toCharArray();
        Arrays.sort(str1);
        Arrays.sort(str2);
        return Arrays.equals(str1, str2);
    }
}

Arrays.sort()方法采用了快排和优化的归并排序算法,平均时间复杂度都是O(nlogn),比较两个字符串的成本是 O(n),这里O(nlogn) 占主导地位

计算一组字符集合可以组成的回文字符串的最大长度

力扣409

class Solution {
    //aabbbc,最长回文字符串为 abbba 或 abcba
    //将出现奇数次的b减一,最终无论将剩下的 'b || c'中的谁放最中间,都是在最终基础上加一
    public int longestPalindrome(String s) {
        if (s == null) return 0;
        if (s.length() == 1) return 1;
        // A~Z:65-90;a~z:97-122
        int[] cnt = new int[58];
        for (char c : s.toCharArray()) {
            cnt[c - 'A']++;
        }
        int res = 0;
        for (int i : cnt) {
            //每次都加偶数
            // i 为 偶数, i & 1 = 0;i 为 奇数, i & 1 = 1
            //偶数次直接相加;若字母出现奇数次,则 次数-1,此时变偶数次,分布在对称的两边
            res += i - (i & 1);
            //或者这样写,例如:3 / 2 * 2 = 2
            //res += i / 2 * 2;
        }
        //res小于原有长度,说明有字母出现次数为奇数次,那么这个字符可以放在回文字符串中间,在res基础上再加一
        return (res < s.length()) ? res + 1 : res;
    }
}

或者只统计出现奇数次的次数,最后将每组奇数次的个数减一(变成偶数次),最终再放一个在回文串中间

class Solution {
    public int longestPalindrome(String s) {
        int[] cnt = new int[128];
        for (char c : s.toCharArray()) {
            cnt[c]++;
        }
        int count = 0;
        for (int i : cnt) {
            count += (i % 2);// i % 2 可写成 i & 2
        }
        return count == 0 ? s.length() : (s.length() - count + 1);
    }
}

字符串同构

力扣205
题目可以假设两字符串长度相等

记录字符上一次出现的位置,注意两个数组初始化每个元素为0,将第一个出现的字符的位置定为 1 而不是 0
例如:"ab","aa", 
若 preIndex1[sc] = i,preIndex2[tc] = i;
当 i = 0 时
对于两个数组的第一个字符'a',preIndex1[a] = 0,preIndex2[a]=0;
当 i = 1时,由于数组本身元素初始化为 0 , 所以 b 在数组中的上一个位置被记录为 0,
但 a 在数组中的上一个出现的位置已被保存为 0,这样结果就变成 true 了
class Solution {
    public boolean isIsomorphic(String s, String t) {
        int[] preIndex1 = new int[128];
        int[] preIndex2 = new int[128];
        for (int i = 0; i < s.length(); i++) {
            char sc = s.charAt(i);
            char tc = t.charAt(i);
            if (preIndex1[sc] != preIndex2[tc]) return false;
            preIndex1[sc] = i + 1;
            preIndex2[tc] = i + 1;
        }
        return true;
    }
}
将第一个出现的字母映射成 1,第二个出现的字母映射成 2

对于 egg
e -> 1
g -> 2
也就是将 egg 的 e 换成 1, g 换成 2, 就变成了 122

对于 add
a -> 1
d -> 2
也就是将 add 的 a 换成 1, d 换成 2, 就变成了 122

egg -> 122, add -> 122
都变成了 122,所以两个字符串同构。
class Solution {
    public boolean isIsomorphic(String s, String t) {
        return isIsomorphicHelper(s).equals(isIsomorphicHelper(t));
    }
    private String isIsomorphicHelper(String s){
        int[] index = new int[128];
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            //字符第一次出现
            if (index[c] == 0){
                index[c] = i + 1;
            }
            sb.append(index[c]);
        }
        return sb.toString();
    }
}

换一种写法,本质还是记录字符第一次出现的位置
indexOf()方法记录字符第一次出现时的索引
在这里插入图片描述

class Solution {
    public boolean isIsomorphic(String s, String t) {
       char[] c1 = s.toCharArray();
       char[] c2 = t.toCharArray();
        for (int i = 0; i < s.length(); i++) {
            if (s.indexOf(c1[i]) != t.indexOf(c2[i])){
                return false;
            }
        }
        return true;
    }
}

回文子字符串个数(Medium)

力扣647

Input: "aaa"
Output: 6
Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
中心扩展法

比如对一个字符串 ababa,选择最中间的 a 作为中心点,往两边扩散,第一次扩散发现 left 指向的是 b,right 指向的也是 b,所以是回文串,继续扩散,同理 ababa 也是回文串。

这个是确定了一个中心点后的寻找的路径,然后我们只要寻找到所有的中心点,问题就解决了。

中心点一共有多少个呢?看起来像是和字符串长度相等,但你会发现,如果是这样,比如搜不到 abba,想象一下单个字符的哪个中心点扩展可以得到这个子串?似乎不可能。所以中心点不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abba,就可以有中心点 bb扩展一次得到,所以最终的中心点由 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。

如果上面看不太懂的话,还可以看看下面几个问题:

为什么有 2 * len - 1 个中心点?
aba 有 5 个中心点,分别是 a、b、c、ab、ba
abba 有 7 个中心点,分别是 a、b、b、a、ab、bb、ba

以 abb 为例,共有 5 个中心点 : a , b ,b ,ab, bb   
0 <= i < 5
i = 0 时,left = 0,right = 0,中心点为 a, ans = 1
i = 1 时,left = 0,right = 1,中心点为 ab, ans = 1
i = 2 时,left = 1,right = 1,中心点为 b, ans = 2
i = 3 时,left = 1,right = 2,中心点为 bb, ans = 3
i = 4 时,left = 2,right = 2,中心点为 b, ans = 4
class Solution {
    public int countSubstrings(String s) {
        int ans = 0;
        int len = s.length();
        for (int i = 0; i < 2 * len - 1; i++) {
            int left = i / 2;
            int right = left + i % 2;
            while (left >= 0 && right <= len - 1 && s.charAt(left) == s.charAt(right)){
                left--;
                right++;
                ans++;
            }
        }
        return ans;
    }
}

或者这样写

private int cnt = 0;

public int countSubstrings(String s) {
    for (int i = 0; i < s.length() - 1; i++) {
        extendSubstrings(s, i, i);     // 奇数长度
        extendSubstrings(s, i, i + 1); // 偶数长度
    }
    return cnt;
}

private void extendSubstrings(String s, int start, int end) {
    while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
        start--;
        end++;
        cnt++;
    }
}
动态规划解法

dp[i][j] 表示 i 到 j 的字符串能不能构成回文串

状态转移方程 d p [ i ] [ j ] =   ( s [ i ] = = s [ j ] )   & &   d p [ i + 1 ] [ j − 1 ] dp\left[ i \right] \left[ j \right] =\ \left( s\left[ i \right] ==s\left[ j \right] \right) \ \&\&\ dp\left[ i+1 \right] \left[ j-1 \right] dp[i][j]= (s[i]==s[j]) && dp[i+1][j1]

这个状态转移方程是什么意思呢?

  • 当只有一个字符时,比如 a自然是一个回文串。
  • 当有两个字符时,如果是相等的,比如 aa,也是一个回文串。
  • 当有三个及以上字符时,比如 ababa 这个字符记作串 1,把两边的 a 去掉,也就是 bab 记作串 2,可以看出只要串2是一个回文串,那么左右各多了一个 a 的串 1 必定也是回文串。所以当 s[i]==s[j] 时,自然要看 dp[i+1][j-1] 是不是一个回文串。

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int countSubstrings(String s) {
        int cnt = 0;
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        for (int j = 0; j < n; j++) {
            for (int i = 0; i <= j; i++) {
                if (s.charAt(i) == s.charAt(j) && ((j - i) <= 1 || dp[i + 1][j - 1])){
                    dp[i][j] = true;
                    cnt++;
                }
            }
        }
        return cnt;
    }
}

回文数

力扣9
尝试用数学方法将数字翻转

class Solution {
    public boolean isPalindrome(int x) {
        int revertednum = 0;
        int num = x;
        if (x == 0) return true;
        if (num < 0 || num % 10 == 0) return false;
        while (num != 0){
            revertednum = 10 * revertednum + num % 10;
            num /= 10;
        }
        return x == revertednum;
    }
}

为了提高效率,可以在翻转一半时提前结束
在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean isPalindrome(int x) {
        if (x == 0) return true;
        if (x < 0 || x % 10 == 0) return false;
        int right = 0;
        while (x > right){
            right = right * 10 + x % 10;
            x /= 10;
        }
        return x == right || x == right / 10;
    }
}

将数字转为字符串,翻转,再比较是否相同
效率很低

class Solution {
    public boolean isPalindrome(int x) {
        String s = new StringBuilder(x + "").reverse().toString();
        return (x + "").equals(s);
    }
}

四种 int 转为 String 的方法

    private static void demo1() {
		//int ----> String int转换成String
		int i = 100;
		String s1 = i + "";				//推荐用
		String s2 = String.valueOf(i);			//推荐用
		
		Integer i2 = new Integer(i);
		String s3 = i2.toString();
		
		String s4 = Integer.toString(i);
		System.out.println(s1);
		
		//String----> int String 转换int
		String s = "200";
		Integer i3 = new Integer(s);
		int i4 = i3.intValue();			//将Integer转换成了int数
		
		int i5 = Integer.parseInt(s);		//将String转换为int,推荐用这种
	}

统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数

力扣696

Input: "00110011"
Output: 6
Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".

第一个想法是仿照中心扩展法,从一对不相同的 ‘0 1’或’1 0’开始,往外扩展

class Solution {
    public int countBinarySubstrings(String s) {
        int cnt = 0;
        int left = 0,right = 0;
        for (int i = 0; i < s.length() - 1; i++) {
            if (s.charAt(i) != s.charAt(i + 1)){
                cnt++;
                left = i - 1;
                right = i + 2;
                while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(i) && s.charAt(right) == s.charAt(i + 1)){
                    cnt++;
                    left--;
                    right++;
                }
            }
        }
        return cnt;
    }
}

这么做时间复杂度太高

我们可以将字符串 s 按照 0 和 1 的连续段分组,例如 s = 00111011,可以得到分类数组 counts={2,3,1,2}。

这里 counts 数组中两个相邻的数一定代表的是两种不同的字符。假设 counts 数组中两个相邻的数字为 u 或者 v,它们对应着 u 个 0 和 v 个 1,或者 u 个 1 和 v 个 0。它们能组成的满足条件的子串数目为 min { u, v },即一对相邻的数字对答案的贡献。

我们只要遍历所有相邻的数对,求它们的贡献总和,即可得到答案。

对于某一个位置 i,其实我们只关心 i - 1 位置的 counts 值是多少,所以可以用一个 last 变量来维护当前位置的前一个位置,这样可以省去一个 counts 数组的空间。

例如:11001100 ----> 2222     
连续 1 和连续 0 数量相同的子字符串个数为:2 + 2 + 2 = 6
1011100 ---> 1132
连续 1 和连续 0 数量相同的子字符串个数为:1 + 1 + 2 = 4,分别为 10、01、1100、10
class Solution {
    public int countBinarySubstrings(String s) {
      int ptr = 0,last = 0,res = 0;
        while (ptr < s.length()){
            char c = s.charAt(ptr);
            int count = 0;
            while (ptr < s.length() && s.charAt(ptr) == c){
                ptr++;//移动的指针
                count++;//连续的 0 或 1 数量
            }
            res += Math.min(last, count);
            last = count;
        }
        return res;
    }
}

另一种写法

class Solution {
    public int countBinarySubstrings(String s) {
        int preCnt = 0,curCnt = 1,n = 0;
        for (int i = 0; i < s.length() - 1; i++) {
            if (s.charAt(i) == s.charAt(i + 1)) {
                curCnt++;
            }else {
                preCnt = curCnt;
                curCnt = 1;
            }
            //其实就是上一种写法 min(last,count)的另一种写法
            if (curCnt <= preCnt) n++;
        }
        return n;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值