字符串匹配算法

字符串匹配一直是一个热门的算法。本文主要讲两种,普通方法(回溯)+高级方法(KMP)。

一、回溯法求子串位置(普通方法)

  算法思想:

  给定两个指针:i、j,分别指向主串的第pos个位置和子串的第一个位置。比较两个指针所指的字符:如果相等,则继续比较后续字符;若不等,则 i 指针跑到pos的下一个位置上,j 指针继续从第1个位置开始比较……直到找到对应的子串。
  我们发现每次失配时,指针 i 都会重新回到之前位置的下一个位置上,这边是“回溯”的体现。

  给定两个指针:i、j,分别指向主串的第pos个位置和子串的第一个位置。比较两个指针所指的字符:如果相等,则继续比较后续字符;若不等,则 i 指针跑到pos的下一个位置上,j 指针继续从第1个位置开始比较……直到找到对应的子串。
  我们发现每次失配时,指针 i 都会重新回到之前位置的下一个位置上(有点绕,多想想),这边是“回溯”的体现。

int Index(SString *S, SString *T, int pos) {
    int i = pos;    int j = 1;
    while(S[i] != '\0' && T[j] != '\0') {    //只要有一个串走完了,那么就退出循环
        if(S[i] == T[j]) {    //如果相等,则继续比较后续字符
            i++;    j++;
        }else {
            i = i-j+2;    //i指针跑到隔壁位置上————回溯法的体现
            j = 1;    //重置j指针
        }
    }
    if(T[j] == '\0')    return i-j;    //返回子串在主串的第一个匹配字符的位置
    else return 0;    //不匹配,返回0
}

  算法分析:

  若设n和m分别是主串和子串的长度,则一般情况下,该算法的时间复杂度为O(n + m)。但是在最坏情况下,它的复杂度为O(n * m)【主串和子串的前面字符都匹配,只有最后一个字符不一样】于是我们想找一种更好、更稳定的算法

二、KMP算法(非回溯法)

  对第一种方法的改进 

  在第一种方法下,我们每次失配都要重新把 i 指针放到其隔壁的位置上,而经过实践,我们发现有时候这样做不高效,如果能直接移动模式串就好了。
于是我们提出了改进方法:在新的算法模式下,我们不需要对 i 指针进行回溯,我们只用向右移动模式串就行了。
  但是这样我们需要解决一个问题:模式串要移动多远,即主串中第 i 个字符应与模式的第几个字符比较?

  算法思想:
  假设主串为“s1, s2, s3 …… sn”,模式串为“p1, p2, p3 …… pm”

  1.若设失配后主串第 i 项应该与模式中第 k 个字符继续比较,则模式中前 k-1 个字符的子串必须满足以下公式——“p1, p2, p3 …… pk-1” == “si-k+1, si-k+2, si-k+3 …… si-1”  

  2.而已经得到“部分匹配”(j 指针前的所有字符均和主串对应字符匹配)的结果是——“pj-k+1, pj-k+2, pj-k+3 …… pj-1” == “si-k+1, si-k+2, si-k+3 …… si-1”  

  3.由以上两式,可推得下面的式子——“p1, p2, p3 …… pk-1” == “pj-k+1, pj-k+2, pj-k+3 …… pj-1”

  4.由此——当匹配过程中主串第 i 个字符和模式第 j 个字符不匹配时,只需把模式移动至它的第 k 项和主串第 i 个字符对齐就行了!  

  next[ ]函数的构造

  既然模式的每一项都有不匹配的可能,那么我们用一个数组next[ ],来写出每一项不匹配的时候所对应的 k 值**(next[ j ] = k)**。
那么如何求出每一项的k值呢?规则如下图——

  

   由上图可知,next[ ]函数值仅取决于模式串本身,而与其相匹配的主串无关
因此,我们求每一项next值的方法如下——

  1.规定:next[1] = 0;
    设:next[j+1] = k;

  2.比较第 k 和第 j 个字符是否匹配——
    2.1. 如果匹配(T[ k ] == T[ j ]),则next[ j + 1 ] = next[ j ] + 1;
    2.2. 如果不匹配(T[ k ] != T[ j ])再看k的next[ k ]
    2.2.1. 若next[ k ] == T[ j ],则next[ j + 1 ] = next[ k ] + 1;
    2.2.2. 若next[ k ] != T[ j ],那么继续找next[ k ]这一项的 k 值,再比较和第 j 个字符是否匹配,如果不匹配就一直找下去……

  3.最后,如果你幸运找到了匹配项,则next[ j + 1 ]就是你找到的那一项的项数 + 1
  如果不幸一直没找到,那么next[ j + 1 ]就等于1。
  我们来看一下获取next( )函数的代码——

//获取next[]数组的函数
void get_next(SString T, int next[]) {
    int i = 1;    //用于遍历模式串
    next[1] = 0;    //规定
    int j = 0;    //next数组里面的数
    while(i < T(0)) {
        if(j == 0 || T[i] = T[j]) {    //一旦第k项和第j项匹配,那么next数组的j+1项的值就是你找到的那一项的项数 + 1
            i++;    j++;
            next[i] = j;
        }else {
            j = next[j];    //如果不匹配,就找第j项对应的k值
        }
    }
}

  现在,我们就有了模式串每一项所对应的k值了!
  例如,对于模式串“a b a a b c a c”,我们可以得到如下的next函数值:

 

  匹配步骤:

  在求得模式的next[ ]函数之后,匹配可如下进行:
  假设以制造 i 和 j 分别指示主串和模式中正待比较的字符,令 i 的初值为 pos, j 的初值为1(模式的第一个字符)——

  • 若在匹配过程中,si = pj,则 i 和 j 分别增加1;
  • 否则(si = pj),i 不变,j 退到 next[ j ] 的位置上再比较:若相等,则 i 和 j 分别增加1;不等则继续比较下一个 next 值的位置……以此类推
  • 这样一直进行下去,直到出现两种情况——
    1. j 退到某个 next 值时字符比较相等,则两指针各自增1,继续比较下去
    2. j 退到其值为0(即模式串的第一个字符就失配),则此时需要把整个模式串向右滑动一位,即从主串的 s+1 位置起和模式重新进行匹配

  最后,就是KMP的代码实现,实际上它和回溯法的代码差不多。

int Index(SString S, SString T, int pos) {
    int i = pos;    int j = 1;
    while(i <= S[0] && j <= T[0]) {
        if(next[j] == 0 || S[i] == T[j]) {    //匹配,则比较下一个字符;要么就是模式串的第一个字符就失配,直接从主串的 s+1 位置起和模式重新进行匹配
            i++;    j++;
        }else {
            j = next[j];    //不匹配,移动模式串到下一个k处,再比较是否匹配
        }
    }
    if(j >= T(0))    return i-T(0);    //返回子串在主串的第一个匹配字符的位置
    else return 0;    //不匹配,返回0
}

  

  

posted @ 2021-07-28 21:15  LyAsano  阅读(282)  评论(0编辑  收藏  举报