1. 暴力查找
给出主串txt, 模式串pat, 查找pat在txt中出现的位置,不存在则返回-1
package algorithm;
public class SubstrSearchDemo {
/*
* 在txt主串中查找子串pat第一次出现的位置
*/
public static int search(String pat,String txt) {
int M=pat.length();
int N=txt.length();
//pat在txt的起始位置为[0,N-M) pat本身长度为M
//在位置N-M-1处往后找还不能匹配 后面也就不可能匹配了
for(int i=0;i<N-M;i++)
{
int j;
for(j=0;j<M;j++)//j扫描模式串pat
{
if(pat.charAt(j)!=txt.charAt(i+j))//不等直接跳出
break;
}
if(j==M)//j==M说明[0,M-1]都匹配了 匹配成功
return i;
}
return -1;//未找到返回-1
}
public static void main(String[] args) {
String txt="helloworld";
String pat="llo";
System.out.println(search(pat, txt));//2
}
}
时间复杂度:O(MN)
暴力法的一种优化:查找失败时,i不需要回退到0,回退到i-j的位置即可, j是已经匹配成功的序列的长度
证明:反证法:假设区间[0,i-j-1]存在一个start,以该start为字符序列的开始位置,可以匹配成功,则不会到达当前j的位置,在j之前就已经匹配成功了,但是现在到达了j位置,所以说明以[0,i-j-1]内的任何位置为起点都不能匹配成功,证毕
package algorithm;
public class SubstrSearchDemo {
/*
* 在txt主串中查找子串pat第一次出现的位置
*/
public static int search(String pat,String txt) {
int M=pat.length();
int N=txt.length();
int i,j;
//i指向txt中匹配过的字符序列的末端
for(i=0,j=0;i<N&&j<M;i++)
{
if(pat.charAt(j)==txt.charAt(i))
j++;
else {
i-=j;//i不用回退到0
j=0;//j从pat的首字符开始
}
}
if(j==M)
return i-M;
return -1;//未找到返回-1
}
public static void main(String[] args) {
String txt="helloworld";
String pat="llo";
System.out.println(search(pat, txt));//2
}
}
2. indexOf方法
indexOf方法是String类中的方法,查找某个子串第一次在主串中的出现位置,它的代码如下:
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
//先找到主串中找到和子串第一个字符匹配的位置
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
//从匹配到子串的首字符的下一个位置继续匹配
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {//匹配成功
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
上述代码的思路和暴力法本质一样,我们自己写的暴力法是直接用一个循环,循环终止的条件是遇到不匹配的字符; indexOf方法是先找到一个相等的字符,如何从这个相等的字符开始一个循环,循环终止的条件是遇到不匹配的字符;
3. KMP算法
一个例子:
假设字母表中只有两个字符,查找的模式字符串pat为 B A A A A A A A A A 。现在,假设已经匹配了模式中的 5 个字符,第 6 个字符匹配失败。当发现不匹配的字符时,可以知道文本txt中的前 6 个字符肯定是 B A A A A B(前 5 个匹配,第 6 个失败),文本指针现在指向的是末尾的字符 B
txt: B A A A A B A A A A
pat: B A A A A A A A A A
这里不需要回退文本指针 i,因为正文中的前 4 个字符都是 A,均与模式的第一个字符B不匹配。另外,i 当前指向的字符 B 和模式的第一个字符相匹配,所以可以直接将 i 加 1,以比较文本中的下一个字符和模式中的第二个字符
(i变成i+1)
在 KMP 子字符串查找算法中,不会回退文本指针 i,而是使用一个数组 dfa[][] 来记录匹配
失败时模式指针 j 应该回退多远
那么j回退到哪呢? j回退到dfa[txt.charAt(i)][j]
解释dfa[txt.charAt(i)][j]
数组的意思:对于每个文本串中 的字符c,在比较了 c 和 pat.charAt(j) 之后,dfa[c][j]
表示的是应该和下个文本字符比较的模式字符的位置
当 i 和 j 所指向的字符匹配失败时(从文本的 i-j+1 处开始检查模式的匹配情况),模式可能匹配的下一个位置 应 该 从
i-dfa[txt.charAt(i)][j]
处开始。按照算法,从该位置开始的dfa[txt.charAt(i)][j]
个字符和模式的前dfa[txt.charAt(i)][j]
个字符应该相同,因此无需回退指针 i,只需要将 j 设为dfa[txt.charAt(i)][j]
并将 i 加 1 即可,这正是当 i 和 j 所指向的字符匹配时的行为
现在的问题:如何求dfa数组?
看下面一幅图:
解释:
dfa[c][j]
的含义和上面介绍的一样: c=txt.charAt(i)
j是当前pat串中的指针位置,当比较c和pat[j]
之后,j下一个位置就是j=dfa[c][j]
图中一共有6个状态:
- 状态0表示匹配刚刚开始,此时可以理解成是一个空串
- 在状态0的基础上,可能会遇到3种字符(假设txt中只有3种),遇到B C的话直接还是回到起点0,因为pat的第一个字符是A,如果遇到A说明pat的第一个字符匹配成功,成功进入下一状态1,1可以理解成字符串”A“
- 在状态1的基础上,可能会遇到3种字符(假设txt中只有3种),遇到C的话直接还是回到起点0,因为pat的第一个字符是A,如果遇到A说明pat的第一个字符匹配成功,第2个字符匹配失败,停留在状态1,如果遇到B成功进入下一状态2,1可以理解成字符串”AB“
- 其余状态依次类推
代码实现:
package algorithm;
public class SubstrSearchDemo {
String pat;
String txt;
int [][]dfa;
int R=256;//先设置字符种类有256种
public void initDFA() {
int M=pat.length();
dfa=new int[R][M];
dfa[pat.charAt(0)][0] = 1;//设置dfa的第一列值
for(int X=0,j=1;j<M;j++)
{
System.out.println("当前状态:"+j+" 重启状态:"+X+" 当前pat字符:"+pat.charAt(j));
for(int c=0;c<R;c++)
{
//将重置状态的情况先复制到当前的状态 后面改变一种匹配成功的情况
dfa[c][j]=dfa[c][X];// 复制匹配失败情况下的值 先假设256种字符都匹配失败(虽然不可能)
}
System.out.println("设置匹配成功之前:");
for(int h=0;h<pat.length();h++)
System.out.print("\t"+h);
System.out.println();
for(int R='A';R<='C';R++)
{
System.out.print((char)(R)+"\t");
for(int k=0;k<pat.length();k++)
System.out.print(dfa[R][k]+"\t");
System.out.println();
}
//pat.charAt(j)==txt.chatAt(i) 匹配成功
//dfa的定义是dfa[txt.charAt(i)][j]
//状态j下遇到字符c匹配成功进入下一个状态j+1
dfa[pat.charAt(j)][j]=j+1;// 设置匹配成功情况下的值 在之前的假设所有失败的情况下选择匹配成功的
System.out.println("设置匹配成功之后:");
for(int h=0;h<pat.length();h++)
System.out.print("\t"+h);
System.out.println();
for(int R='A';R<='C';R++)
{
System.out.print((char)(R)+"\t");
for(int k=0;k<pat.length();k++)
System.out.print(dfa[R][k]+"\t");
System.out.println();
}
//X可能是原来的状态 也可能往前进一个状态 也可能往后退一个状态
X=dfa[pat.charAt(j)][X];// 更新状态
System.out.println("----------------------------------------");
}
}
/*
* 在txt主串中查找子串pat第一次出现的位置
*/
public int search() {
int M=pat.length();
int N=txt.length();
int i,j;
for(i=0,j=0;i<N&&j<M;i++) {
j=dfa[txt.charAt(i)][j];
}
if(j==M)
return i-M;
return -1;//未找到返回-1
}
public static void main(String[] args) {
SubstrSearchDemo obj=new SubstrSearchDemo();
obj.txt="AABRABABACBRAACAADABRA";
obj.pat="ABABAC";
obj.initDFA();
System.out.println();
System.out.println(obj.search());//12
System.out.println(obj.txt.indexOf(obj.pat));//使用indexOf方法验证
}
}
时间复杂度:O(M+N)
上面代码中,以图中的例子为例:
第一次循环:第一个字符是’A‘: dfa[A][0]=0+1
其他的两个字符B C对应的dfa[B][0]=0
dfa[B][0]=0
第二次循环:第二个字符是’B’: dfa[B][1]
=1+1 其他的两个字符A C对应的dfa[A][1]=1
dfa[C][1]=0
X的状态什么时候会更新?
假设当前遇到的pat字符是c,并且c在之前的最近的一个状态j时已经匹配成功过一次,则X的下一个状态为j+1