28. 找出字符串中第一个匹配项的下标
题目链接
题目描述:
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = “hello”, needle = “ll” 输出: 2
示例 2: 输入: haystack = “aaaaa”, needle = “bba” 输出: -1
说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。
思路:
这题考查KMP算法
需要了解一些概念:
- KMP名字由来?用来做什么的?
- 什么是文本串?
- 什么是模式串?
- 什么是前缀表?如何构建?
理解前缀和后缀
字符串的前缀
是指不包含最后一个字符的所有以第一个字符开头的连续子串;
字符串的后缀
是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
精髓 —— 找最长相等的 前缀 和 后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的 前缀 的后面重新匹配就可以了!
(前缀表数组中每个位置记录的就是当前子串中最长相等的前后缀长度)
next数组就是前缀表,有的会将数值统一减一,这涉及到具体实现上的习惯。
构造next数组(针对模式串计算):
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
注意下标回退时,可能会连续回退,要保证其next取下标的时候不能越界
动画演示
以模式串aabaaf
为例,手撕其对应next数组的构建过程↓
难点:
经典KMP,必须掌握。
文本串长度——n,模式串长度——m
时间复杂度:O(n+m)
空间复杂度:O(m)
class Solution {
//构造next数组(针对模式串)
public void getNext(int[] next, String s) {
int j = -1; //统一减一
next[0] = j;
for (int i = 1; i < s.length(); i++) {
while (j >= 0 && s.charAt(i) != s.charAt(j+1)) {
j = next[j]; //回退
}
if (s.charAt(i) == s.charAt(j+1)) {
j++; //更新当前子串最长公共前后缀长度
}
next[i] = j; //更新next数组
}
}
//字符串模式匹配
public int strStr(String haystack, String needle) {
int[] next = new int[needle.length()];
getNext(next, needle);
int j = -1;
for (int i = 0; i < haystack.length(); i++) { //从0开始
while (j >= 0 && haystack.charAt(i) != needle.charAt(j+1)) {
j = next[j];
}
if (haystack.charAt(i) == needle.charAt(j+1)) {
j++;
}
if (j == needle.length()-1) {
return i-needle.length()+1;
}
}
return -1;
}
}
next数组统一不减一
class Solution {
//构造next数组(针对模式串)
private void getNext(int[] next, String s) {
int j = 0; //统一不减一
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s.charAt(j) != s.charAt(i))
j = next[j - 1]; //回退
if (s.charAt(j) == s.charAt(i))
j++; //更新当前子串最长公共前后缀长度
next[i] = j; //更新next数组
}
}
public int strStr(String haystack, String needle) {
int[] next = new int[needle.length()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) { //从0开始
while (j > 0 && needle.charAt(j) != haystack.charAt(i))
j = next[j - 1];
if (needle.charAt(j) == haystack.charAt(i))
j++;
if (j == needle.length())
return i - needle.length() + 1;
}
return -1;
}
}
时长:
40min
收获:
熟悉相关概念:文本串、模式串、前缀表、最长公共(相等)前后缀
掌握next数组的构造过程
459. 重复的子字符串
题目链接
题目描述:
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。
示例 2:
输入: “aba”
输出: False
示例 3:
输入: “abcabcabcabc”
输出: True
解释: 可由子字符串 “abc” 重复四次构成。 (或者子字符串 “abcabc” 重复两次构成。)
思路1:
因为题中模式串没有给出,所以模式串是变化的
- 由当前下标作为结尾截取模式串
- 遍历剩余文本串看看是否匹配
- 如果不匹配,回到1,更新下标
- 如果匹配,若已经遍历到结尾,满足题目要求!;否则,继续遍历剩余文本串
难点:
模式串是未知的
时间复杂度:O(n)
?
空间复杂度:O(n)
class Solution {
public boolean repeatedSubstringPattern(String s) {
String pattern = "";
for (int i = 0; i < s.length()/2; i++) {
pattern = s.substring(0, i + 1);
if (s.length() - pattern.length() < 0) { //剪枝:如果字符串剩余长度还没模式串长,那没有重复的模式串
return false;
}
int j = i + pattern.length();
while (j < s.length()) {
String substring = s.substring(j-pattern.length()+1, j + 1);
if (!substring.equals(pattern)) {
break;
}
if (j == s.length()-1) { //如果在检查剩余字符串,直到最后的位置都没break,满足!
return true;
}
j += pattern.length(); //文本串下标更新:每次跳跃当前模式串的长度
}
}
return false;
}
}
思路2:
KMP
累了累了,回头再研究。。。
时长:
30min
收获:
字符串总结
复杂的字符串题目非常考验对代码的掌控能力。
下面小结一下Java字符串中常用内容:
String常用方法
int length():
返回字符串的长度: return value.length
char charAt(int index):
返回某索引处的字符return value[index]
boolean isEmpty():
判断是否是空字符串:return value.length == 0
String toLowerCase():
使用默认语言环境,将 String 中的所有字符转换为小写
String toUpperCase():
使用默认语言环境,将 String 中的所有字符转换为大写
String trim():
返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj):
比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):
与equals方法类似,忽略大小写
String concat(String str):
将指定字符串连接到此字符串的结尾。 等价于用“+”
int compareTo(String anotherString):
比较两个字符串的大小
String substring(int beginIndex):
返回一个新的字符串,它是此字符串的从 beginIndex开始截取到最后的一个子字符串。
String substring(int beginIndex, int endIndex) :
返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
boolean endsWith(String suffix):
测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix):
测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的 子字符串是否以指定前缀开始
boolean contains(CharSequence s):
当且仅当此字符串包含指定的 char 值序列时,返回 true
int indexOf(String str):
返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):
返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str):
返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex):
返回指定子字符串在此字符串中最后 一次出现处的索引,从指定的索引开始反向搜索 注:indexOf和lastIndexOf方法如果未找到都是返回-1
String replace(char oldChar, char newChar):
返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
String replace(CharSequence target, CharSequence replacement):
使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement):
使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement):
使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
boolean matches(String regex):
告知此字符串是否匹配给定的正则表达式。
String[] split(String regex):
根据给定正则表达式的匹配拆分此字符串。
String[] split(String regex, int limit):
根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
String与基本数据类型转换
字符串 → 基本数据类型、包装类
Integer包装类的public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
基本数据类型、包装类 → 字符串
调用String类的public String valueOf(int n)可将int型转换为字符串
相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换
String与字符数组转换
字符数组 → 字符串
String 类的构造器:String(char[]) 和 String(char[], int offset, int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。
字符串 → 字符数组
public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。
String、StringBuilder、StringBuffer
StringBuffer类有三个构造器:
StringBuffer():初始容量为16的字符串缓冲区
StringBuffer(int size):构造指定容量的字符串缓冲区
StringBuffer(String str):将内容初始化为指定字符串内容
常用方法:
StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
StringBuffer delete(int start,int end):删除指定位置的内容
StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
StringBuffer insert(int offset, xxx):在指定位置插入xxx
StringBuffer reverse() :把当前字符序列逆转
StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样
面试题:对比String、StringBuffer、StringBuilder
String(JDK1.0):不可变字符序列
StringBuffer(JDK1.0):可变字符序列、效率低、线程安全
StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全
注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder 会改变其值。
最后:
双指针法是字符串处理的常客。
双指针法在数组,链表和字符串中很常用。
双指针法总结