串的模式匹配算法

最近在学数据结构中的串,现在来写一写我对串的不同模式匹配算法的理解:
声明:首先需要注意这里面的串的存储结构,它并不是普通的像C语言中的字符串一样在每个串末尾有一个结尾标志‘/0’,然而这里是通过在数组下标为0的元素赋值为整个串的有效长度值,例如字符串friend,储存它的数组最后一个元素T[6]中存储字符’d’;第一个元素T[0]存储有效长度6;第一个存储字符的元素T[1]存储的字符’f’。然后需要注意的是这里所说的有效长度的问题,这里字符的储存都是从下标为1的元素开始,在连续的空间里面。不要理解错了。
1.朴素的模式匹配算法
核心思想是:对主串S的每一个字符作为子串的开头,与要匹配的字符串进行匹配,对主串做大循环,每个字符开头做长度为T[0]的小循环,直到匹配成功或全部遍历完成为止。
下面是对应的代码:

int Index(String S,STring T,int pos)
{
 int i=pos;
 int j=1;
 while(i<=S[0]&&j<=T[0])
 {
  if(S[i]==T[j])
  {
   ++i;
   ++j;
  }
  else
  {
   i=i-j+2;
   j=1;
  }
  if(j>T[0])
  {
   return i-T[0];
  }
  else return 0;
 }
}

解析:
第一句:

int Index(String S,STring T,int pos)

这里面:(1)String是定义串的抽象数据结构。
(2)pos是表示在寻找子串的时候,是从第pos个字符开始往后寻找的。
(3)也就是说将来要返回的值是子串在主串S中第pos个字符之后的位置。
②循环条件

while(i<=S[0]&&j<=T[0])

因为i大于S[0]时表示的是已经全部遍历并且未找到合条件的子串;j大于T[0]表示的是找到了对应的子串。所以条件该这么写。

 if(S[i]==T[j])
 {
  ++i;
  ++j;
 }

主串中的S[i]与子串中的T[j]相等则继续循环判断下一组。

else
 {
  i=i-j+2;
  j=1;
 }

否则,i 将退回到上次匹配首位的下一位。这里一定要能够理解

   i=i-j+2;

这一条语句,其实这里可能画图对理解有较好的帮助;但在这里我直接写出我自己的理解:首先需要理解 i-j 一定是主串S中此时与串T相比较的第一个元素之前的一个元素;其实这一点既可以画个图判断一下,也可以自己直接想象出来。
然后对于 i-j+2 其实就很好理解了,它自然就是S中与T相比较的第一个元素的之后一个元素。

 if(j>T[0])
 {
  return i-T[0];
 }

j>T[0] 表示的是找到满足条件的子串的情况,这在之前已经说过,这里主要是理解下面的语句:

return i-T[0];

因为最后的S[i]与T[j]相等后,一定会再执行一遍 ++i 与 ++j,所以此时的 i 表示的是在S中与T的所有字符中相等的最后一个字符的下一个位置。那么,它再减去T的长度,就可以得到在S中与T相等的子串的第一个元素的下标,也就是要求的子串T在主串S中的第pos个字符之后的位置。
2.朴素模式匹配算法的问题:
例如主串S=abcdefgab 子串T=abcdex;如果用朴素模式匹配算法,第一步:当主串中是从第一个开始进行比较时,会发现前五个元素都对应相等,然后 i 停在了第六个元素,然后执行了 i=i-j+2; 语句后,i 就会回到之前与T中第一个字符相比较的位置的下一个位置,再次执行比较语句 if(S[i]==T[j]) 发现不相等,i 便再一次执行语句 i=i-j+2; 再返回到刚才与T中第一个字符元素相比较的位置的下一个位置。
然后就这样一直循环判断会发现从S中的第二个字符b开始,一直到第五个字符e,这几个的判断有一个规律:首先bcde自身都与T中第一个字符a不相等;在比较了第一次发现S与T的前5个字符都对应相等,则可以说明T中第一个字符a与S中的bcde也是不相等的!那么也就是说如果我们足够了解T自身的情况的话,在比较了第一次之后面的有些步骤就可以省略,比如这里在第1次循环之后的第2345次循环都是多余的。
但是由于第一次比较的结果中发现S与T中第6个元素是不相等的,而T自身中第一个字符a与第六个字符x本身也是不相等的,所以由第一次判断之后并不能直接得到T中第一个元素a与S中第6个元素不相等的结论,所以后面的比较就应该从S中的第6个字符开始。
3.KMP模式匹配算法
①先看一个例子:S=“abcababca” ,T=“abcabx”。按照之前的经验,首先比较第一次之后,发现前五个字符都是对应相等的,然后就是T自身的规律是:第一个字符a与后面的第二,三,五个字符不相等,而与第四个字符相等;那么在第一次的比较之后我们就还是可以省略一些步骤:T中a与S中第二,三个字符b,c不相等,则下一次循环可以直接跳过,然后 i 现在来到了4;发现T中一二个字符ab与T中的第4,5个字符相等,所以这1,2个元素在进行了第一次比较的条件下自然也是与S中第4,5个元素相等,所以在进行以S中第4个字符为开头与T进行比较时,前两个比较就可以省略,然后继续比较S中第六个和T中第三个元素。
也就是说在进行了第一次比较之后,可以省略一些不必要的步骤,然后直接进行到以S中第四个字符开头,并且省略前两步,此时 i 到了6的这里。再依次进行。
通过之前的讲解我们发现:
(1)由于第一次的比较一定会使得 i 停在与T中字符不相等的位置,那么此时的 i 之前的各个元素一定是对应相等的,那么我们就可以利用这个相等得到一些信息使算法简化。
(2)在省略掉不必要的步骤之后,进行下一步时,发现 i 其实是不需要回溯的。
(3)从T是“abcdex”变到T=“abcabx”,在第二次的比较中,分为j 从1开始和j从3开始两种,并且发现 j的这种变化关键取决于T串的结构中是否有重复的问题。
因此,为了研究T串自身的特征,引入了一个数组next,下面是它的函数定义:

           0,当j=1时 
next[j]=Max{k|1<k<j,且'p1...pk-1'='pj-k+1...pj-1'}
           1,当'p1...pj-1'中没有重复的时候

例如子串T=“abcdex”,按照上面的函数定义,推导出next数组的过程如下:
(1)当j=1时,next[1]=0;
(2)当j=2时,j由1到j-1 就只有字符“a”,属于第三种情况,'p1…pj-1’中没有重复,所以next[2]=1;
(3)当j=3时,j由1到j-1 串是“ab",a与b不是相同的字符,还是属于第三中没有重复的情况,所以next[3]=1;
以后同理,最终此T串的next数组为:011111
例2:T=“abcabx”
(1)当j=1时,next[1]=0;
(2)当j=2时,j由1到j-1 就只有字符“a”,属于第三种情况,‘p1…pj-1’中没有重复,所以next[2]=1;
(3)当j=3时,j由1到j-1 串是“ab",a与b不是相同的字符,还是属于第三种没有重复的情况,所以next[3]=1;
(4)当j=4时,j由1到j-1=3,串是“abc”,还是属于第三种没有重复的情况,所以next[4]=1;
(5)当j=5时,j由1到j-1=4,串是“abca”,此时前缀字符a与后缀字符a相等,即’p1’=‘p4’,所以k-1=1;j-k+1=5-k+1=4都可以得出此时k值为2;
(6)当j=6时,j由1到j-1=5,串是“abcab”,此时有“p1p2”=“p4p5”,所以k-1=2;或j-k+1=6-k+1=4都可以算出k的值为3
因此此时的next数组就变为011123
同时可以总结出经验:如果前后缀一个字符相等,k值是2;如果是两个字符相等,k值是3;前后缀是n个字符相等,则k值是n+1
例3:T=“ababaaaba”
(1)当j=1时,next[1]=0;
(2)当j=2时,j由1到j-1 就只有字符“a”,属于第三种情况,'p1…pj-1’中没有重复,所以next[2]=1;
(3)当j=3时,j由1到j-1 串是“ab",a与b不是相同的字符,还是属于第三中没有重复的情况,所以next[3]=1;
(4)当j=4时,j由1到j-1串是“aba”,“p1”=“p3”,根据之前的规律可以知道这里的k值应为2;
(5)当j=5时,j由1到j-1串是“abab”,“p1p2”=“p3p4”,所以k值为3;
(6)当j=6时,j由1到j-1串是“ababa”,所以这里"p1p2p3"=“p3p4p5”,所以k值为4;需要注意的是这里的前缀和后缀中有一个相同的字符,第3个字符a;但是在之前的定义中并没有说前缀和后缀是不能重复的,所以不要仅从概念上单纯的认为前缀和后缀一定是一前一后毫无交集的。
(7)当j=7时,j由1到j-1串是“ababaa”,可以判断出来这里只有一头一尾"a"=“a”,所以k=2;
(8)当j=8时,j由1到j-1串是“ababaaa”,还是只有"a"=“a”,所以k=2;
(9)当j=9时,j由1到j-1串是“ababaaab”,此时前缀“ab”与后缀“ab”相等,所以next[9]=3;
因此得到的next数组就是:011234223
例4:T=“aaaaaaaab”
(1)当j=1时,next[1]=0;
(2)当j=2时,j由1到j-1 就只有字符“a”,属于第三种情况,'p1…pj-1’中没有重复,所以next[2]=1;
(3)当j=3时,j由1到j-1串是“aa”,前缀字符“a“与后缀字符”a“相等,next[3]=2;
(4)当j=4时,j由1到j-1的串是“aaa”,前缀字符是"aa"后缀字符是“aa”,两者相等,next[4]=3;
(5)…同理可得j=5到j=9时,每一个j值对应的next[j]对应的值,分别为4~8;
所以数组next中存的值就为012345678
说明:这里举了4个例子,每一个例子都对应一种特殊的情况,并不是说只要掌握了前面的next[j]函数的计算方法就可以理解它的作用;这里的四个例子都很特殊,需要借助它们来理解短短的几行代码就实现了这个next[j]值的求法的原理。所以这里绝对不是无端的重复。
另外,严格按照next函数自身的定义来推导next[j]值,对理解它的作用是有帮助的,不要认为一些格式上的东西是没有必要的。
②现在来看用代码实现next数组的推导:
~~~
void get_next(String T,int *next)
{
int i,j;
i=1;
j=0;
next[1]=0;
while(i<T[0])
{
if(j==0||T[i]==T[j])
{
i=i+1;
next[i]=(j=j+1);
}
else
{
j=next[j];
}
}
}
~~~
解析:
(1)首先下面几句:

 i=1;
j=0;
next[1]=0;

很容易看出来这是在初始化i和j,并且在为next数组中的next[1]赋值,因为所有的next[1]都为0,所以直接赋值即可,为什么不交给下面的循环来实现呢?可以代入试一下,发现满足判断条件j==0,于是执行下面的语句,结果是next[2]=1;然后再仔细看语句本身,会发现在执行next[i]=j;之前都会有语句i=i+1;而且初始化i=1;所以直接带入循环就会缺少给next[1]赋值为0的过程。
然后来看i的变化,i 表示的是next数组中的每个元素的下标,所以i 是需要从1开始依次递增的,并且是在每一次需要计入数据的时候加1
再来看j :j 的含义很丰富
(1)首先为了后面的表述,需要申明每一次循环得到的i 和j;注意这里指的是

if(j==0||T[i]==T[j])
 {
  i=i+1;
  next[i]=(j=j+1);
 }

这一步循环,也就是说这里的j值并不是回溯得到的。那么此时的j 值(未进入到if语句中的j值)就表示的是T[i] (i表示的是已进入到if 语句中执行了i+1的语句)之前的字符串中前缀和后缀相等的字符的个数。
然后如果满足if 的判断语句“T[i]==T[j]”(i和j都是未执行if 下面的语句)再执行语句: i=i+1; next[i]=(j=j+1); 就可以发现它正是在按照next函数的定义来为每个next数组的元素赋值。即每个next[j]元素的值就是T中第j个元素之前的字符串中前缀与后缀相等的元素个数加上1的值。
这就是j 的第一个含义。

然后j 的第二个含义是用来理解j 的回溯语句:j=next[j];
例如字符串“abcabcabxy”当i=9时,j=6,然后执行if的判断语句发现没有满足条件,于是开始执行else语句,然后就会发现j 的值在第一步从6变到3;并且再看j=3时,指向的正是第一个c,并且如果字符串换为“abcabcabcabxy” i=12,j=9,也是执行else部分,然后j的值就会从9变到6,再退回到3;并且指向的也是第一个c;然后再观察这个9,6,3的变化会发现都是“一组”字符串的最后一个,“一组“表示的是像abcabcabc中的abc或abababab中的ab这样的一个字符串重复基础串。并且此时在两个字串“abcabcabxy”和“abcabcabcabxy” 中的x也都在“基础串”的最后一个位置,这是不是说明它每次退回的位置都是前一个”基础串“中与x在自身所在”基础串“中相同的位置呢?
可以在作相同的举例:串为“abcabcabcax”,i=11,j=8进行if 语句的判断后,发现不满足条件,然后就执行else语句,5=j=next[8],然后2=j=next[5];最后一步回溯先不看,后面再讲;然后就可以直接证明出之前的结论是正确的:它每次退回的位置都是前一个”基础串“中与x在自身所在”基础串“中相同的位置。
那么究竟为什么就会这样呢?下面开始分析它的成因:
串“abcdeabcdeabcdxy”

i=1,   j=0,  next[1]=0;
i=2,   j=1,  next[2]=1;
      j=0,  
i=3,   j=1,  next[3]=1;
      j=0,  
i=4,   j=1,  next[4]=1; 
      j=0,  
i=5,   j=1,  next[5]=1; 
      j=0,
i=6,   j=1,  next[6]=1;
i=7,   j=2,  next[7]=2;
i=8,   j=3,  next[8]=3;
i=9,   j=4,  next[9]=4;
i=10,  j=5,  next[10]=5;
i=11,  j=6,  next[11]=6;
i=12,  j=7,  next[12]=7;
i=13,  j=8,  next[13]=8;
i=14,  j=9,  next[14]=9;
i=15,  j=10, next[15]=10;
      j=next[10] => j=5;
      j=next[5] => j=1;
      j=next[1] => j=0;
i=16,  j=j+1 => j=1  next[16]=1;

以上是整个next数组的推导过程,这里需要通过i 和j 的变化来理解代码的执行原理。
第一段:

i=1,   j=0,  next[1]=0;
i=2,   j=1,  next[2]=1;
      j=0,  
i=3,   j=1,  next[3]=1;
      j=0,  
i=4,   j=1,  next[4]=1; 
      j=0,  
i=5,   j=1,  next[5]=1; 
      j=0,
i=6,   j=1,  next[6]=1;

第一句初始赋值,第二句是由于j==0满足了if条件执行了下面的语句,所以next[2]=1;然后又由于T[i]与Tji]与T[j]不相等,j也不为0,所以执行回溯语句,j又变为了0;这样从j=2~j=5 一直是这样重复的。并且最后的next[6]=1也是由于前面j=0,然后通过if判断语句并执行后面的语句产生的。
第二段:

i=7,   j=2,  next[7]=2;
i=8,   j=3,  next[8]=3;
i=9,   j=4,  next[9]=4;
i=10,  j=5,  next[10]=5;
i=11,  j=6,  next[11]=6;
i=12,  j=7,  next[12]=7;
i=13,  j=8,  next[13]=8;
i=14,  j=9,  next[14]=9;
i=15,  j=10, next[15]=10;

从这里的第一个开始,由于前面T[i]==T[j](T[6]=T[1])成立,所以进入到if判断语句并且执行对应的程序段,结果就依次呈现,从i=7~i=15,一直这样重复;发现:
(1)i-j 值为5从这里开始一直成立
(2)这个5正好是“基础串”(abcde)的长度
第三段:

       j=next[10] => j=5;
       j=next[5] => j=1;
       j=next[1] => j=0;
i=16,  j=j+1 => j=1  next[16]=1;

由于i=15时,j=10,T[i]与T[j]不相等,j值回溯,先回溯到 j=5,再回溯到 j=1,最后再到 j=0;发现从10~5也是跨过了5个字符的长度。
所以5也是每次回溯的跨度。相当于每次退回到上一个周期的对应位置时,需要减去周期。
然后值得注意的一点就是每个举例的串结尾都是xy,x很好理解,而y其实是因为如果结尾是x时,j=x的下标后,不再满足while循环的判断语句,就会跳出循环,那么对于在讨论问题时需要让 j 回溯,所以在最后再加上一个y,会更严谨。
以上就是我对语句

   j=next[j];

这条语句的 j 值回溯功能的理解。

因此这个next数组的建造就完成了。下面是将这个next数组真正放在模式匹配算法当中,它如何发挥作用,以及这个算法的原理将在下面讲解:
首先是代码:

int Index_KMP(String S,String T,int pos)
{
 int i=pos;
 int j=1;
 int next[255];
 get_next(T,next);
 while(i<=S[0] && j<=T[0])
 {
  if(j==0||S[i]==T[j])
  {
   i++;
   j++;
  }
  else 
  {
   j=next[j];
  }
 }
 if(j>T[0]) 
 {
  return i-T[0];
 }
 else 
 {
  return 0;
 }
}

解析:
①首先前两句:

 int i=pos;
 int j=1;

(1)将 i 赋值为pos,因为需要查找的是串T在串S中第pos个字符之后的位置。
(2)T串需要从第一个字符开始匹配。
②下面两句就是为T串的next数组个元素赋值。

 int next[255];
 get_next(T,next);

也就是通过前面定义获得的next数组的函数,来得到T串对应的next数组。

while循环的条件:

while(i<=S[0] && j<=T[0])

关键:当 i >S的长度时,表示找完S串,发现没有与T串相等的子串;当 j >T串长度时,表示在S串中找到了相等的子串。

if(j==0||S[i]==T[j])
  {
   i++;
   j++;
  }
j=0:首先需要知道它一定是回溯到这里的,然后执行下面的语句:i++;j++;表示的就是S串中的 i 值加一,也就是位于S串中的子串的首字符位置需要向下移动一个。并且 j 加一,j 就成了1,表示T串也要从第一个字符开始。

else 
  {
   j=next[j];
  }

(1)这里就是 j 值的回溯;关键:与前面所讲的定义next数组的函数时的回溯是一样的道理:它每次退回的位置都是前一个”基础串“中与x在自身所在”基础串“中相同的位置。也就是此时的 j 在T串前面的周期中的位置。根据之前的讲述,多多思考,会更好得到理解这些生硬的语句。
(2)结合这里并理解上面的 j==0语句:当S[i]!=T[1] 时,就会执行 j=next[1];此时 j 变为了0;就是说T串的第一个字符就与S串的第 i 个字符不相等。就会让S串的 i 加一,移动到下一个字符; j 先回溯到0,再加一,表示重新来到第一个。
(3)其实从这里,还可以进一步理解执行j=next[j]回溯时,除了先从后往前回到各个周期中的位置之外,最后两次回溯都是j 先到1,再到0这样的原因。带入到上面的匹配算法中就可以发现,j 回溯到1,是为了直接让S[i]与T[1]进行比较,如果两者不相等,那么 i 需要后移,并且 j 需要回溯到0,才能执行到 j++ 从而也能够从第一个字符开始。

 if(j>T[0]) 
{
 return i-T[0];
}

此时j 是由于之前的S[I]==T[j]成立,就是前面所说的正确找到对应的串,然后由于它还是要进入到循环中执行i++;j++;所以最后的j 正好比T串的长度大1;所以满足这里的if判断语句,就代表找到了。那么就需要输出该子串在S串中的第pos个字符之后的位置:

 return i-T[0];

其实这里就和之前所讲的普通的模式匹配算法对正确的子串在主串中的位置输出是一样的:因为最后的S[i]与T[j]相等后,一定会再执行一遍 ++i 与 ++j,所以此时的 i 表示的是在S中与T的所有字符中相等的最后一个字符的下一个位置。那么,它再减去T的长度,就可以得到在S中与T相等的子串的第一个元素的下标,也就是要求的子串T在主串S中的第pos个字符之后的位置。

以上就是一般的KMP模式匹配算法的整个建立,原理和运行过程,还有KMP模式匹配算法的改进,我将在后面的博客中写道。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值