1.算法汇总
首先,来看一张汇总表,本文会将表里的每种算法作详细介绍。代码和逻辑比较长,可以根据目录跳着看。
2.暴力算法
在文本中可能出现匹配的任何地方都检查是否存在。原理很简单,直接看代码就可以懂。
实现代码:
//暴力子字符串查找
public class ViolenceSubStringSearch
{
@SuppressWarnings("unused")
public static int search(String pat, String txt)
{
int M = pat.length();
int N = txt.length();
for(int i = 0; i <= N-M; i++)
{
int j;
for(j = 0; j < M; j++)
{
if(txt.charAt(i + j) != pat.charAt(j));
break;
}
if(j == M)
{
return i; //找到匹配
}
}
return N; //未找到匹配
}
}
运行轨迹:
3.KMP算法
KMP算法的基本思想是当出现不匹配是,就能知晓一部分文本的内容(因为在匹配失败之前它们已经和模式匹配)。我们可以利用这些信息避免将指针回退到所有这些已知的字符之前。
KMP的主要思想是提前判断如何重新开始查找,而这种判断只取决于模式本身。
在KMP子字符串查找算法中,不会回退文本指针i,而是使用一个数组dfa[][]来记录匹配失败时模式指针j应该回退多远。dfa[][]称为确定有限状态自动机(DFA)。
如何构造dfa,即DFA应该如何处理下一个字符?
和回退是的处理方式相同,除非在pat.charAt(j)处匹配成功,这时DFA应该前进到状态j+1.例如,对于ABABAC,要判断在j=5时匹配失败后DFA应该怎么做。通过DFA可以知道完全回退之后算法会扫描BABA并到达状态3,因此可以将dfa[][3]复制到dfa[][5]并将C所对饮的元素的值设为6.因为在计算DFA的地j个状态时只需要知道DFA是如何处理前j-1个字符的,所以总能从尚不完整的DFA中得到所需的信息。
最后一个关键的细节,如何维护重启位置X,因为X< j,所以可以由已经构造的DFA部分来完成这个任务–X的下一个值是dfa[pat.charAt(j)][X].
总结下,对于每个j,DFA会:
- 将dfa[][X]复制到dfa[][j](对于失败的情况)
- 将dfa[pat.charAt(j)][j]设为j+1(对于匹配成功的情况)
- 更新X。
如下图:
//KMP子字符串查找
public class KMP
{
private final int R; // the radix
private int[][] dfa; // the KMP automoton
private char[] pattern; // either the character array for the pattern
private String pat; // or the pattern string
/**
* Preprocesses the pattern string.
*
* @param pat the pattern string
*/
public KMP(String pat)
{
this.R = 256;
this.pat = pat;
// build DFA from pattern
int m = pat.length();
dfa = new int[R][m];
dfa[pat.charAt(0)][0] = 1;
for (int x = 0, j = 1; j < m; j++)
{
for (int c = 0; c < R; c++)
{
dfa[c][j] = dfa[c][x]; // Copy mismatch cases.
}
dfa[pat.charAt(j)][j] = j+1; // Set match case.
x = dfa[pat.charAt(j)][x]; // Update restart state.
}
}
/**
* Preprocesses the pattern string.
*
* @param pattern the pattern string
* @param R the alphabet size
*/
public KMP(char[] pattern, int R)
{
this.R = R;
this.pattern = new char[pattern.length];
for (int j = 0; j < pattern.length; j+