一、使用串的基本操作函数
Index(S,T,pos) 定位操作。若主串S中存在与串T值相同的字串,则返回它在主串S中第pos个字符之后第一次出现的位置;否则函数值为0
在Java中,对应函数为 S.indexOf(String T)
二、BF模式匹配算法
分别用i和j表示主串S和字串T中当前正在比较的字符位置。算法思想为:从主串S的第pos个字符起,与模式串的第一个字符进行比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符起再重新和模式串的字符比较。依此类推,直至模式串T中的每个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功,函数返回值为和模式串T中的第一个字符相等的字符在主串S中的序号,否则成匹配不成功,函数返回值为0。
在匹配成功的情况下,考虑以下两种极端情况。其中m为主串长度,n为模式串长度。
(1)最好情况下,每趟不成功的匹配都发生在模式串的第一个字符与主串中相应字符的比较。此时的时间复杂度为O(m+n) 例如: S="aaaaaaaba" T="ba"
(2)最坏情况下,每趟不成功的匹配都发生在模式串的最后一个字符与主串中相应字符的比较。此时的时间复杂度为O(m*n) 例如: S="aaaaaaab" T="aab"
图1展示了模式串T=“abcac”
和主串的匹配过程(pos=1)
BF算法思路简单。但当匹配失败时,主串的指针i总是回溯到i-j+1位置,模式串的指针j总是恢复到首字符位置j=1。因此,算法时间复杂度高。
三、KMP算法
(以下分析元素下标皆从1开始)
改进点:失配时,不需回溯i指针,而是利用已经比较过的信息,仅将模式串向右移动到一个合适的位置,并从这个位置开始和主串进行比较,这个合适的位置仅与字串本身的结构有关,而与主串无关。KMP算法的时间复杂度为O(m+n)。
回顾图1的匹配过程,在第三趟匹配中,i=7、j=5时失配,于是又从i=4、j=1处重新开始比较。然而,i=4和j=1、i=5和j=1、i=6和j=1这三次比较都是不必进行的,因为从第三趟匹配的结果可知,主串中第4、5、6个字符是’b’,’c’,’a’,而字串中的第一个字符是‘a‘,因此它无须再和这三个字符进行比较,仅需将子串向右滑动3个字符的位置继续进行i=7、j=2时的字符比较即可。
关键在于明确的知道每个时刻需要移动的距离。在此之前,引入三个概念:前缀、后缀和部分匹配值
- 前缀:除最后一个字符以外,字符串的所有头部字串。
- 后缀:除第一个字符以外,字符串的所有尾部字串。
- 部分匹配值:前缀和后缀的最大公共元素长度。
下面以“ABCDABD”为例来理解这三个概念:
部分匹配值的作用:
移动位数=已匹配成功的字符数-失配字符的上一个字符所对应的部分匹配值 即:move=(j-1)-next[j-1]
根据上述方法得到模式串“abcac”的部分匹配值为00010
。写成数组的形式,就得到了next数组:
字符 | a | b | c | a | c |
---|---|---|---|---|---|
next | 0 | 0 | 0 | 1 | 0 |
修改next数组:
字符 | a | b | c | a | c |
---|---|---|---|---|---|
next | 0 | 0 | 0 | 1 | 0 |
右移1位 | -1 | 0 | 0 | 0 | 1 |
我们注意到:
- 第一个元素右移后空出来的位置填入了-1。这是因为当第一个字符就不匹配时,只能往后移动一位
- 最后一个元素在右移中溢出。因为原来的模式串中,最后一个元素的部分匹配值是其下一个元素使用的,但显然没有下一个元素,故可以舍去。(这里如果不舍去,将会有额外的作用,例如求循环节。)
这样,上式就被改写成move=(j-1)-next[j](如果下标从0开始,则是move=j-next[j])
即:移动位数=已匹配成功的字符数-失配字符所对应的部分匹配值
失配后与i重新对应的j的位置:j=j-move=j-((j-1)-next[j])=next[j]+1(如果下标从0开始,则是j=j-move=j-(j-next[j]) = next[j])
对人而言,用上述方法很容易求next数组,但对于计算机来说,可以用一种更加高效的方法来求:
设T位子串,当next[j]已经求得,next[j+1]的值可以分两种情况分析。
- 若子串中字符\(t_j\)等于\(t_i\),显然next[i+1]=j+1,因为j位当前T最长相等前后缀长度。
- 若\(t_j\)不等于\(t_i\),将\(t_{i-j+1}\)…\(t_i\)当作主串,将\(t_1\)…\(t_j\)当作子串。类似于失配问题,需移动子串,即令j=next[j],继续进行比较,若满足(1),则求得next[j+1]。
KMP算法仅在主串与子串有很多“部分匹配”时才显得更高效,其主要优点是主串不回溯。
代码
import java.util.Scanner;
// //下标从0开始,只找第一个出现的匹配结果
public
class KMP
{
static int sLen;
static int pLen;
static char[] S = new char[1000009];
static char[] P = new char[10009];
static int[] next = new int[10009];
static void GetNext()
{
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
if (k == -1 || P[j] == P[k])
{
k++;
j++;
next[j] = k;
}
else
{
k = next[k];
}
}
}
static int KMP()
{
int i = 0;
int j = 0;
while (i < sLen && j < pLen)
{
if (j == -1 || S[i] == P[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j == pLen)
return i - j + 1; //下标是从0开始的
else
return -1;
}
public
static void main(String[] args)
{
Scanner scan = new Scanner(System.in);
System.out.print("请输入主串:");
String s1 = scan.nextLine();
sLen = s1.length();
S = s1.toCharArray();
System.out.print("请输入子串:");
String s2 = scan.nextLine();
pLen = s2.length();
P = s2.toCharArray();
GetNext();
System.out.println(KMP());
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。