字符串操作

统计字符串中数字的次数

输入一个字符串(长度在 100 以内),统计其中数字字符出现的次数。例如输入 Ab100cd200,输出 6。

只需要接收输入,对字符串的每一个字符进行判断,判断字符是否在 0 - 9 之间,统计字符个数即可:

public class CountOfNum {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String str = scanner.nextLine();
        long num = 0;
        num = str.chars().filter(c -> c >= '0' && c <= '9').count();
        System.out.println(num);
    }
}

替换空格为指定字符串

如果给定一个字符串,需要把其中的空格全部替换为 CODE 这样的字符串,可以直接通过标准库中的 String.replaceAll() 方法实现:

str.replaceAll(" ", "CODE");

如果要自己实现数组插入的功能,需要:

  1. 将字符串转换成为字符数组,遍历一次,统计出空格的个数

  2. 构建出新的字符数组,初始化的大小 = 原来的字符数组长度 + 空格长度 x 3(空格是一个字符,CODE 是 4 个字符,替换一个空格增加了三个字符)。

  3. 遍历一次,复制,当不为空格时直接复制,当为空格时,则把 CODE 这几个字符复制过去。

代码如下:

翻转句子中的单词

有一字符串 I love coding,要求将里面单词的顺序翻转,但是单词内部字母顺序不变,即翻转后为 coding love I。详细要求:

  • 字符串的前后可能有空格,翻转之后字符串前后不允许出现空格。

  • 单词之间可能存在多个空格,但是翻转之后统一为一个空格。

  • 不能直接调用 split() 方法。

解答思路:

  • 自定义 split() 方法,将字符串分割成若干个子串。

  • 利用中心对称,将字符串转换成为倒序的。

  • 遍历子串,去掉多余的空格,每个有效的子串后面增加一个空格。

  • 去掉结果最后多余的空格。

代码如下:

算法的时间复杂度是 $O(n)$,空间复杂度也是 $O(n)$。

另外,如果没要求分割和 split() 方法一模一样,可以用 StringTokenizer 类:

寻找最长回文子串

回文串是指不管是顺着读还是逆着读,都是一样的。判断一个字符串是否是回文串,根据中心对称判断即可。现在问题是一个字符串 s,找到 s 里面包含的最长的回文串。例如输入 abdbdc,输出 dbd

直接的解法是对字符串的每一段子串判断,即将 abdbdc 分为子串:

  • 以 a 开头的:a,ab,abd,abdb,abdbd,abdbdc。

  • 以 b 开头的:b,bd,bdb,bdbd,bdbdc。

  • 以 d 开头的:d,db,dbd,dbdc。

  • 以 b 开头的:b,bd,bdc。

  • 以 d 开头的:d,dc。

  • 以 c 开头的:c。

对所有子串判断是否为回文串,然后记录最长的一个。代码如下:

该方法的时间复杂度是 $O(n^3)$,空间复杂度是 $O(1)$。

由于回文串的顺序和逆序相同,所以回文串都有一个中心,即对称轴,比如 abba,对称轴是两个 b 字符之间,abcba 的对称轴则是 c,也就是以下规律:

  • 字符数为奇数的回文串的中心是一个字符。

  • 字符数为偶数的回文串的中心是两个字符的间隙。

要兼容这两种情况,就需要设计一个方法,传入两个索引参数,如果两个参数相同,中心往两边拓展时,拓展出来的字符数是奇数。如果两个索引参数是相邻的两个数,那么拓展出来的字符数就是偶数。

优化后的实现如下:

该方法在时间复杂度上,由于中心是 n,拓展的时候每个中心最多能把所有数拓展完,即 n/2 次,所以是 $O(n^2)$,而空间上,只有常数个临时变量,所以空间复杂度为 $O(1)$。还要其他比较巧妙的做法,比如动态规划。

字符串转整数

问题是:将给定字符串转换成为 32 位的有符号的整数,规则如下:

  • 如果字符串的前面有空格,那么可以忽略

  • 符号只能存在于数字前面,也就是类似“+3”,“-2”,而 “2-1” 则是只能读取到 2,后面不规则的需要忽略。

  • 如果超过了 32 位有符号整数的范围,需要将其截断,大于 $2^{31}-1$ 则返回 $2^{31}-1$,小于 $-2^{31}$ 则返回 $-2^{31}$。

通过解析规则,可以得到步骤:

  1. 去掉前面的空格

  2. 接下来的字符必须是数字,+-

    1. 如果是+-号,将符号记录下来

    2. 没有符号默认是正数

  3. 接下来的字符必须是数字,遇到其他字符会直接结束

  4. 考虑溢出问题

将字符串转换为数字时,可以循序遍历:

判断溢出时,如果当前 sum 值大于 Integer.MAX_VALUE/10,即 214748364,此时后面还有数字则一定溢出;如果当前值等于 Integer.MAX_VALUE/10,就需要判断后面一个数字是否大于 7,若大于 7 也会溢出。

执行只需遍历一遍,因此时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。

拼接字符串找出最小数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。

题目要求字符串拼接后的数字最小,即排序问题。如果只有两个字符串 s1, s2,就需要比较 s1+s2 和 s2+s1,如果 s1+s2 小于 s2+s1,那么 s1 就在前面,s2 就在后面。

对于一个数组,要使所有的拼接起来是最小,则需要两两比较,进行排序即可。这种排序规则保证了任意相邻元素的组合都是局部最优的,从而整个序列的拼接结果全局最优。

可以直接调用自带的排序方法,代码如下:

旋转词之 KMP 算法

将某字符串 s1 的前面任意连续字符移动到该字符串后面,形成新字符串 s2,s2 即为 s1 的旋转字符串。如“1234”的旋转字符串有“1234”、“2341”、“3412”、“4123”。现给定两个字符串,判断它们是否是旋转字符串。

思路如下:

  1. 首先判断 str1 和 str2 的字符串长度是否相等,若不等,返回false

  2. 若长度相等,生成拼接字符串 str1 + str1 的大字符串 str

  3. 问题转化为字符串的模式匹配,即判断一个字符串是否包含另一个字符串

用暴力解法的步骤如下:

代码如下:

暴力解法的弊端是不匹配时,每次都要从头开始匹配,优化的思路是回溯到合适的位置。KMP 通过构建部分匹配表(next 数组),使模式串在匹配失败时能跳过已匹配的部分,减少重复匹配的开销。

首先明确前缀指从头开始但不包含最后一个字符的所有子串,后缀指从尾部开始但不包含第一个字符的所有子串。

在模式串的第 i 个位置发生失配,说明前面 0 ~(i-1) 个字符是匹配的,即模式串和主串这部分是完全相同的,此时只需要找出已经匹配的字符里最长公共前后缀,将模式串移动到该位置即可。因为已经匹配的字符只和模式串有关,与主串无关,所以失配时,只需移动模式串就行。

用一个 next 数组记录模式串的子串的最长公共前后缀长度,即记录前面已经匹配的信息里面,有效的信息,让模式串尽量移动到有效的位置。例如模式串“ABABA”:

子串
前缀
后缀
最长前后子缀
最长公共前后缀长度

A

0

AB

A

B

0

ABA

A, AB

A, BA

A

1

ABAB

A, AB, ABA

B, AB, BAB

AB

2

ABABA

A, AB, ABA, ABAB

A, BA, ABA, BABA

ABA

3

假设 sources[i] 和 patterns[j] 处发生失配,已知 next[j]=k,说明 patterns[] 中前 j-1 个元素中,最长公共前后缀长度是 k,那么模式串 pattern[] 上的指针应该移动到第 k 个位置继续和主串 source[] 上的 i 继续比较。

KMP 算法包含两次匹配过程,一次是模式串和主串的匹配,另外一次是在求解模式串的 next[] 数组时,模式串的最长前缀子串和最长后缀子串的匹配。next 数组的求解代码如下:

过程如下:

还有一种更现代化写法,初始化 next[0]=0 和 j=0,且利用循环手动回退:

下面用 getNext() 方法计算 next 数组,kmp(String, String) 调用 next[] 数组进行匹配回溯,isMatch(String, String) 主要是判断两个字符串是不是互为旋转词。

然后是另一种求 next 数组对应的 kmp 算法:

kmp 算法的空间复杂度为 O(m),时间复杂度为 O(n+m)(计算 next[] 数组的时间复杂度 + 遍历比较的复杂度)。

Rabin-karp 算法

判断一个字符串是否是另一字符串的子串,有比 kmp 更简单的算法,比如 Rabin-Karp 算法,其主要思想:

  • 假设主串的长度为 M,目标字符串的长度为 N。

  • 计算子串的 hash 值。

  • 计算主串中每一个长度为 N 的连续子串的 hash 值,与子串的 hash 值对比。如果 hash 值不相等,那么则说明字符串肯定不匹配,如果 hash 值相等,则还需要使用朴素算法来判断。

如果两个不同的字符串出现 hash 相等情况,称为哈希冲突,但概率很低。

由于每一个字符有自己的 ASCII 码,我们可以设置一个进制为 b = 31, 假设字符为 $T = T_1T_2T_3...T_{n-1}T_n$,那么其 hash 值可以表示为:

Hash(T)=T1bn1+T2bn2+T3bn3++Tn1b1+TnHash(T)=T_1b^{n-1}+T_2b^{n-2}+T_3b^{n-3}+\cdots+T_{n-1}b^{1}+T_n

由于 Java 中 long 值溢出后会自动绕回,所以不需要再对结果求余。

这里计算子串的 hash 时每次都重新计算了,所以可以针对 hash 值计算做优化,每次去掉开头字符的 hash 然后加上末位字符的 hash,这种优化称为滚动 hash,优化后的 isMatch() 如下:

题目:字符统计

描述:给定一个长度为 n 的字符串 S,还有一个数字 L,统计长度大于等于 L 的出现次数最多的子串(不同的出现可以相交),如果有多个,输出最长的,如果仍然有多个,输出第一次出现最早的。

思路:枚举所有可能的子串,巧妙利用 hashmap,统计出现次数,key 是子串,value 是子串出现的次数,找出符合条件的那个:

也可以暴力遍历每一个子串,然后遍历其出现的次数。

Last updated