最近在力扣上刷到一个题,如下:
- 找出字符串中第一个匹配项的下标
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。 示例 2:
输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 104 haystack 和 needle 仅由小写英文字符组成
猛一看,按照以往的编码习惯,那肯定是一顿indexOf输出呀,可这里要考查的点不会是问你会不会调用java的API那么简单,事实也确实没那么简单,没有头绪,只好先看题解,再去B站找大神的解题思路(文章末尾的相关的链接),再手算公共前后缀,最好再进入了编码练习,我这里把需求升级了一下,可以一次查找所有能匹配上的下标,下面把代码贴出来,记录一下。
使用KMP算法首先要求出“公共前后缀”数组,代码如下:
/**
* 获取公共前后缀长度数组
* @param pattern 模式串数组
* @param prefixTable 公共前后缀长度数组
*/
public static void getPrefixTable(char[] pattern, int[] prefixTable){
prefixTable[0] = 0; // 默认第0位为0
int pLen = pattern.length;
if (pLen > 1){
int i = 1; //模式串的下标,默认从下标为1的位置开始计算,因为前1位默认为0
int len = 0; //记录已经匹配到的公共前后缀的长度
while(i < pLen){ //循环次数为模式串的长度
if(pattern[i] == pattern[len]){ //当前循环到的字符,和上次匹配到公共前后缀长度的位置进行匹配,如果匹配到了
len++; //则将公共前后缀的长度+1
prefixTable[i] = len; //将prefixTable数组i位置的值赋值为连续匹配到的长度
i++; //进入下一轮循环
}else { //本次循环没有匹配上
if (len > 0){ //判断上次匹配到的公共前后缀的长度,如果上次已匹配成功过,len长度肯定大于0
len = prefixTable[len - 1]; // 则将公共前后缀的长度回朔
}else { //如果上次没有匹配到,本次也没有匹配到
prefixTable[i] = len; //那么当前位置的公共前后缀长度就为0(此时的len为0,直接=0也是一样的)
i++; //进入下一轮循环
}
}
}
}
prefixMove(prefixTable);
}
/**
* 将公共前后缀数组整体向后移一位,首位设置为-1
* @param prefixTable
*/
public static void prefixMove(int[] prefixTable){
int len = prefixTable.length;
if (len > 1){
for (int i = len - 1; i > 0; i--) {
prefixTable[i] = prefixTable[i - 1];
}
prefixTable[0] = -1;
}
}
得到了公共前后缀长度数组之后,就可以进行匹配查找了
/**
* 在一个较长的字符串中找出目标字符串的位置
* @param text 主串
* @param pattern 模式串
* @return 模式串在主串中的首个下标索引
*/
public static int[] search(char[] text, char[] pattern){
int[] indexResult = null;
StringBuffer resultBuff = new StringBuffer(); //查找结果变量
int m = text.length; //主串的长度
int n = pattern.length; //模式串的长度
if(m > n){
if(n == 0){
indexResult = new int[]{-1};
return indexResult;
} else if (n == 1) {
for (int i = 0; i < m; i++) {
if (text[i] == pattern[0]){
resultBuff.append(i); //将当前匹配到的索引记录下来
resultBuff.append(",");
}
}
}else {
int i = 0; //主串循环的索引
int j = 0; //模式串循环的索引
int[] prefixTable = new int[n]; //存储公共前后缀的数组
getPrefixTable(pattern, prefixTable); //调用计算公共前后缀的方法
while (i < m){ //遍历主串的长度
if((j == (n - 1)) && text[i] == pattern[j]){ //如果模式串遍历结束,且最后一位字符相等,即在主串中匹配到了一个完整的字符串
resultBuff.append((i - j)); //将当前匹配到的索引记录下来
resultBuff.append(",");
j = prefixTable[j]; //将模式串的下标移到公共前后缀指定的位置,继续匹配剩余的字符串
}
if(text[i] == pattern[j]){ //匹配到了一个字符,将主串和模式串的下标同时后移一位
i++;j++;
}else { //单个字符不匹配
j = prefixTable[j]; //将模式串的下标移到公共前后缀指定的位置,继续匹配
if(j == -1){ //如果模式串的下标为-1时,主串和模式串的下标同时后移一位
i++;j++;
}
}
}
}
//将匹配结果转换为数组,并返回
String strResult = resultBuff.toString();
if (strResult.length() > 1){
strResult = strResult.substring(0, strResult.length() - 1);
String[] strIndex = strResult.split(",");
int sLen = strIndex.length;
indexResult = new int[sLen];
for (int k = 0; k <sLen; k++) {
indexResult[k] = Integer.parseInt(strIndex[k]);
}
}
}else {
indexResult = new int[1];
if(m == n){
if(Arrays.equals(text, pattern)){
indexResult[0] = 0;
}else {
indexResult[0] = -1;
}
} else if (m < n) {
indexResult[0] = -1;
}
}
return indexResult;
}
最后调用main方法进行测试
public static void main(String[] args) {
String strText = "ababababcabaaabcacababcabaabbaabcababcabaaadsfbadsfababcabaadfasdfdasdfasdfasdfasdababcabaa";//主串
String strPattern = "ababcabaa"; //模式串
// String strText = "aabaaabaaac";//主串
// String strPattern = "aabaaac"; //模式串
char[] text = strText.toCharArray();
char[] pattern = strPattern.toCharArray();
int[] prefixTable = new int[pattern.length];
int[] idxResult = search(text, pattern);
System.out.println(Arrays.toString(idxResult));
// getPrefix2(pattern, prefixTable);
// System.out.println(Arrays.toString(prefixTable));
// prefixMove(prefixTable);
// System.out.println(Arrays.toString(prefixTable));
}
这里要特别感谢B站一位名叫“正月点灯笼”的大神,感兴趣的可以去看一下他的解题思路:
【KMP字符串匹配算法1】