字符串操作
统计字符串中数字的次数
输入一个字符串(长度在 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");如果要自己实现数组插入的功能,需要:
将字符串转换成为字符数组,遍历一次,统计出空格的个数
构建出新的字符数组,初始化的大小 = 原来的字符数组长度 + 空格长度 x 3(空格是一个字符,CODE 是 4 个字符,替换一个空格增加了三个字符)。
遍历一次,复制,当不为空格时直接复制,当为空格时,则把 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}$。
通过解析规则,可以得到步骤:
去掉前面的空格
接下来的字符必须是数字,
+或-号如果是
+或-号,将符号记录下来没有符号默认是正数
接下来的字符必须是数字,遇到其他字符会直接结束
考虑溢出问题
将字符串转换为数字时,可以循序遍历:
判断溢出时,如果当前 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”。现给定两个字符串,判断它们是否是旋转字符串。
思路如下:
首先判断 str1 和 str2 的字符串长度是否相等,若不等,返回false
若长度相等,生成拼接字符串 str1 + str1 的大字符串 str
问题转化为字符串的模式匹配,即判断一个字符串是否包含另一个字符串
用暴力解法的步骤如下:
代码如下:
暴力解法的弊端是不匹配时,每次都要从头开始匹配,优化的思路是回溯到合适的位置。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)=T1bn−1+T2bn−2+T3bn−3+⋯+Tn−1b1+Tn
由于 Java 中 long 值溢出后会自动绕回,所以不需要再对结果求余。
这里计算子串的 hash 时每次都重新计算了,所以可以针对 hash 值计算做优化,每次去掉开头字符的 hash 然后加上末位字符的 hash,这种优化称为滚动 hash,优化后的 isMatch() 如下:
题目:字符统计
描述:给定一个长度为 n 的字符串 S,还有一个数字 L,统计长度大于等于 L 的出现次数最多的子串(不同的出现可以相交),如果有多个,输出最长的,如果仍然有多个,输出第一次出现最早的。
思路:枚举所有可能的子串,巧妙利用 hashmap,统计出现次数,key 是子串,value 是子串出现的次数,找出符合条件的那个:
也可以暴力遍历每一个子串,然后遍历其出现的次数。
Last updated