最近在学习算法。刚学习完字符串匹配的几种算法:BF算法、MP算法:KMP算法,BM算法和BMH算法。参考的书籍是算法之美,原书的代码都是用C++写的。我不懂C++,只学过C#,这里就用C#做个总结(自己是个菜鸟,表达错误的地方,希望大家指正)。
1、BF算法
BF算法实现原理是:从主串和模式串的首位置开始,依次比较主串和模式串的各个位置,如果匹配错误,主串就返回第二个位置,模式串返回首位置,重新匹配。以此类推,直到模式串匹配成功,返回匹配成功的位置。如果没有发生匹配就返回-1。
代码如下:
public static int MatchStr(string s1, string s2,int n)
{
int i = n-1, j = 0; //n为匹配的起始位置
bool IsMatch=false ;
while (!IsMatch)
{
if (s1[i] == s2[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
//如果模式串匹配成功,j++,就会得到模式串长度的值。
if (j == s2.Length)
{
IsMatch = true;
n = i - j+1;
}
}
if (IsMatch)
{
Console.WriteLine("普通方法匹配成功");
}
else
{
n = -1;
Console.WriteLine("普通方法匹配失败");
}
return n;
}
2、MP算法
BF算法一旦匹配失败,i,j的值就会频繁的回溯,导致复杂度增大。MP算法避免的i值回溯,只利用j值的回溯进行匹配。
MP算法原理:模式串与主串进行匹配,进行到i处(模式串在j处)发现不匹配,如果模式串j处之前有前缀n个字符与主串i处
之前n个字符相匹配,则可将模式串j移动到n处,重新与i进行匹配(此时i的位置不变)即可。依次类推,直到匹配结束。
现在的问题就是求n的值为多少。
假设模式串与主串在i处不匹配(模式串在j处),那么说明主串的s1[i-j~i-1]的子串与模式串的s2[0~j-1]是匹配成功的。那么寻找
主串i之前的n个字符,等价于寻找模式串j之前的n个字符与模式串的前缀n个字符是相匹配的,即n的值与主串无关,只与模式串自
身的j位置有关。n的计算方法计算如下:假定模式串为(ababcabababc)
当j=0时,代表第一个字符就匹配失败,规定此时n为-1;
当j=1时,代表第二个字符匹配失败。此时n为0;
当j=2时,则是在aba处匹配失败,前面并不能找到合格的n。则n为0;
当j=3时,则是在abab处匹配失败,则首字母a与j-1处的a是匹配的,则此时n为1;
当j=4时,则是在ababc处匹配失败,则前缀ab,与j-2~j-1处ab匹配,则此时n为2;
......
我们发现,j处的n值与j-1处的n值有关。
推理如下:
假设j-1处匹配失败,值为n,意思是:s2[0~n-1]=s2[j-1-n~j-2]。
若在j处匹配失败,有两种情况:
情况1:s2[j-1]=s2[n],那么s2[0~n]=s2[j-1-n~j-1],此时可得到j处匹配失败的n值为n+1;
情况2:s2[j-1]!=s2[n]。那么说明匹配成功的前缀字符一定不是n+1,但是会不会有更小的前缀字符匹配成功。
已经知道在j-1处的值为n,说明匹配成功的前缀字符为n,那么s2[n]处的匹配成功的前缀字符m,必定也是j-1处的匹配成功的前缀
s2[j-1]=s2[m],那么j处匹配失败的n值为m+1。否则n的值就依次循环到字符开始。
......
此时各处的n值组合起来就是next[]数组,在匹配过程中,若在j处匹配失败,就把模式串移动next[j]个位置,重新与主串i匹配就可以了。
实现MP算法中next[]数组的代码如下:
private static int[] MpNext(string s1)
//s1为模式串
{
int[] next = new int[s1.Length + 1];
//数组的长度可以为s1.length,这里为s1.length+1,在多次匹配中才有意义。
int i = 1, j = 0;
next[0] = -1;
next[1] = 0;
while (i < s1.Length)
{
if (j==-1||s1[i] == s1[j])
//j==-1的条件是为了避免i=0时候,next[0]=-1,造成的下标越界
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
}
return next;
}
利用MP算法进行匹配的代码如下:
public static int MpMatchStr(string s1, string s2, int n)
{
int[] next = MpNext(s2);
int i = n - 1,j = 0;
bool IsMatch = false ;
while (!IsMatch)
{
if (j == -1 || s1[i] == s2[j])
{
i++;
j++;
}
else
{
j = next[j];
}
if (j == s2.Length)
{
IsMatch = true;
n = i - j + 1;
}
}
if (IsMatch)
{
Console.WriteLine("MP方法匹配成功");
}
else
{
n = -1;
Console.WriteLine("MP方法匹配失败");
}
return n;
}
3、KMP算法
KMP算法是由MP算法发展过来的。同样是借助一个next[]数组。但是next[]数组的n的值有一点不同。
KMP算法原理基本上与MP相同。不同处在于:如果主串中的i处与模式串j处不匹配,且MP算法的next[j]为n,
则下一次匹配则为主串中的i处与模式串中的n处进行匹配。但是若模式串n处的值与j处的值相等,则主串中的i处
必然不等于n处,所以此处next[j]的值可以修正为一个更小的值m。
实现KMP算法中next[]数组的代码如下:
private static int[] KmpNext(string s1)
{
int[] next = new int[s1.Length + 1];
int i = 1, j = 0;
next[0] = -1;
next[1] = 0;
while (i < s1.Length)
{
if (j == -1 || s1[i] == s1[j])
{
i++;
j++;
if (i < s1.Length && s1[i] == s1[j])
{
next[i] = next[j];
}
else
{
next[i] = j;
}
}
else
{
j = next[j];
}
}
return next;
}
利用KMP算法匹配字符串的代码与MP的代码一致。
4、BM算法
BM算法与KMP算法的很大区别在于,KMP算法是从左往后开始匹配,而BM算法则是从右往左进行匹配。
BM算法原理:利用坏字符数组和好后缀数组进行匹配。那么什么叫坏字符数组和好后缀数组呢。假设主串的n+s2.length与模式串的s2.length开始匹配。若匹配到主串的i处(模式串的j处)失败,此处主串i处字符便称为坏字符,则会有两种情况。
第一种情况:坏字符从未在模式串中出现,那么直接将模式串的开始位置对准i+1处,重新进行匹配。即主串的i+s2.length处与模式串的s2.length开始匹配。
第二种情况:坏字符在模式串的j处之前的m处出现,则可将模式串的m处与主串的i处对齐,重新进行匹配,即主串的i+s2.length-m-1处与模式串的s2.length开始匹配。
那么什么是好后缀数组呢。同样假设主串的n+s2.length与模式串的s2.length开始匹配。若匹配到主串的i处(模式串的j处)失败,若模式串j+1之后的后缀在j之前出现过,那么这个后缀就称为好后缀。则会有三种情况出现:
第一种情况:好后缀在j的前面m处出现过。那么将模式串向右移动s2.length-j-1+m,使好后缀与主串好后缀对齐。然后从主串的s2.length-j-1+m+i处与模式串的s2.length进行匹配。
第二种情况:模式串j+1之后的后缀在j之前并没有出现过,但是有更短的后缀,同时是模式串的前缀,那么这个也是好后缀,假设这个后缀的长度为s,可以将模式串的前缀与主串的好后缀对齐,然后从主串的s2.length+j+i+1-s处与模式串的s2.length处开始匹配。
第三种情况:不存在好后缀。
所以如果一旦匹配,那么可以根据坏字符和好后缀得到两个模式串移动的数值,取其最大的数值进行模式串移动则可以大大的提高匹配的效率。
首先对于坏字符数组:bmBc[]:
对于第一种情况:如果模式串不存在坏字符,那么bmBc[坏字符]就是s2.length;
对于第二种情况:如果模式串距离j最近的m处存在坏字符,那么bmBc[坏字符]就是s2.length-m-1。
那么依据坏字符数组,每次匹配失败后,重新匹配时,主串i的位置就是i+bmBc[坏字符]。
坏字符数组bmBc[]的代码实现:
private static int[] BmbmBc(string s1)
{
int[] bmBc = new int[256]; //这里为ASCII码中的256个字符,因为不能预知模式串中的字符
for (int i = 0; i < bmBc .Length ; i++)
{
bmBc[i] = s1.Length; //首先把所有值赋值为模式串的长度。
}
for (int i = 0; i < s1.Length-1 ; i++)
{
bmBc[s1[i]] = s1.Length - i -1; //依次计算模式串中字符的bmBc值。
}
return bmBc;
}
对于好后缀数组:bmGs[]:
对于情况一:好后缀的长度是知道的,为s2.length-j-1,但是在j前何处出现好后缀并不知道。可以依据循环的对比方式取得模式串移动的距离。(详见代码)
对于情况二:好后缀的长度是不知道,但是一定比s2.length-j-1,此时好后缀的位置也是可以确定的,模式串的开头和结尾。
对于情况三:不存在好后缀,则bmGs[]的值为s2.length。
坏字符数组bmGs[]的代码实现:
private static int[] BmbmGs(string s1)
{
int[] bmGs = new int[s1.Length] ;
for (int i = s1.Length -1; i > 0; i--)
{
for (int s = i - 1; s >= 0; s--) //对于第一种情况,s代表出现好后缀的位置
{
if (s1[s] != s1[i]) //s1[i]处与主串是不匹配的,所以要找与s1[i]不同的值
{
if (i ==s1.Length - 1) //此时是末尾第一个字符就不匹配
{
bmGs[i] = i - s;
}
for (int k = i+1; k {
if (s1[k - i + s] != s1[k]) break; //好后缀匹配失败,跳出
else
{
if (k ==( s1.Length - 1)) //好后缀匹配成功
{
bmGs [i] =i - s;
}
}
}
}
if (bmGs[i] > 0) break; //表示已经取得了一个好后缀的最小移动位置,就不用继续循环了
}
if (bmGs[i] == 0) //对应于第二种情况。第二种情况是第一种情况失败后开始的
{
for (int k = Math.Min(s1.Length - 1 - i, i); k > 0; k--) //k代表好后缀长度,好后缀一定比 s1.Length-1-i、i都小
{
int x = s1.Length - 1, y = k - 1; //x,表示模式串尾部。
while (s1[x] == s1[y])
{
if (y == 0) //好后缀匹配成功
{
bmGs[i] = s1.Length - k;
break;
}
x--;
y--;
}
}
if (bmGs[i] == 0) //代表第三种情况。一二种情况都没出现。
{
bmGs[i] = s1.Length;
}
}
}
return bmGs;
}
利用BM算法实现字符串匹配的代码:
public static int BmMatchStr(string s1, string s2, int n)
{
int[] bmBc = BmbmBc(s2);
int[] bmGs = BmbmGs(s2);
int x=n+s2.Length -2;
int y=s2.Length -1;
bool IsMatch = false;
while (!IsMatch)
{
if (s1[x] == s2[y])
{
if (y == 0)
{
IsMatch = true;
n = x+1;
}
x--;
y--;
}
else
{
int offset = Math.Max(bmBc[s1[x]], bmGs[y]); //偏移的最大值
x += offset;
y = s2.Length - 1;
}
}
if (IsMatch)
{
Console.WriteLine("BM方法匹配成功");
}
else
{
n = -1;
Console.WriteLine("BM方法匹配失败");
}
return n;
}
5、BMH算法
BMH算法算是BM算法的一个简化。BMH仅仅是依靠坏字符数组进行模式串的移动。略微不同的是,无论在何处匹配失败,
都会依照主串中已经完成匹配的子串最右端的字符作为坏字符进行移动。坏字符数组代码同BM算法。BMH算法匹配字符串略。
测试:
static void Main(string[] args)
{
string s1 = "GCATCGCAGAGAGTATACAGTACG";
string s2 = "GCAGAGAG";
int n = Program.MatchStr(s1, s2, 1);
Console.WriteLine("普通方法匹配的位置为:" + n);
int m = Program.MpMatchStr(s1, s2, 1);
Console.WriteLine("MP方法匹配的位置为:" + m);
int x = Program.MpMatchStr(s1, s2, 1);
Console.WriteLine("KMP方法匹配的位置为:" + x);
int y = Program.BmMatchStr(s1, s2, 1);
Console.WriteLine("BM方法匹配的位置为:" + y);
}
结果如下: