浅谈数据结构-字符串匹配

模式匹配是数据结构中字符串的一种基本运算,给定一个子串,要求在某个字符串中找出与该子串相同的所有子串,这就是模式匹配

假设P是给定的子串,T是待查找的字符串,要求从T中找出与P相同的所有子串,这个问题成为模式匹配问题。P称为模式,T称为目标。如果T中存在一个或多个模式为P的子串,就给出该子串在T中的位置,称为匹配成功;否则匹配失败。

蛮力算法(BF算法)

算法思想

目标串T的的第一个字符起与模式串P的第一个字符比较。

若相等,则继续对字符进行后续的比较;否则目标串从第二个字符起与模式串的第一个字符重新比较。

直至模式串中的每个字符依次和目标串中的一个连续的字符序列相等为止,此时称为匹配成功,否则匹配失败。

QQ截图20150817111824

算法性能

假设模式串的长度为m,目标串的长度为n:N为外循环,M为内循环。

BF算法存在回溯,严重影响到效率,最坏的情况的是N*M,所以算法的复杂度为O(mn).暴力算法中无法利用已知的信息,也就是模式串的信息,减少匹配。比如在第四步中,t[5]和p[4]不匹配,然后又回溯(图有点问题),t[3]和P[0]肯定不同,因为之前匹配过了,我们得知t[3]=p[1],而p[0]和p[1]不同。

代码

int bf(const char *text, const char *find)
{
    //异常判断
    if (*text == '/0' || *find == '/0')
    {
        return -1;
    }
    
    int find_len = strlen(find);
    int text_len = strlen(text);
    if (text_len < find_len)
    {
        return -1;
    }
    //去除const属性
    char *s =const_cast<char*>(text);
    char *p = s;
    char *q = const_cast<char*>(find);
   //执行BF算法
    while (*p != '')
    {
        //匹配成功,指针前移
        if (*p == *q)
        {
            p++;
            q++;
        }
        //否则,回溯,通过记录之前的指针位置,重新赋值。
        else
        {
            //s++,p指向回溯的位置.
            s++;
            p = s;
            //q重新指向初始位置
            q =const_cast<char*>(find);
        }
        //执行成功了,返回位置。
        if (*q == '')
        {
            return (p - text) - (q - find);
        }
    }
    return -1;
}
暴力算法

KMP算法

Knuth-Morris-Pratt算法(简称KMP),是由D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的一个改进算法,消除了BF算法中回溯问题,完成串的模式匹配。KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法。

算法思想

QQ截图20150817172924

在上图中,在S=”ababcabcacbaa”中查找T=”abcac”,如果使用KMP匹配算法,当第一次搜索到S[2] 和T[2]不等后,S下标不是回溯到1,第二次发生不匹配时,S下标也不是回溯到开始,T的下标不是回溯到开始,而是根据T中T[4]==’b’的模式函数。

关键思想:在匹配过程中,若发生不匹配的情况。

如果next[j] >= 0,则目标串的指针 i 不变,将模式串的指针 j 移动到 next[j] 的位置继续进行匹配;

若next[j] = -1,则将 i 右移1位,并将 j 置0,继续进行比较。

程序算法思路:

如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;

如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P从开始位置移动next[j],然后开始匹配

算法代码

int KmpSearch(char* s, char* p)  
{  
    int i = 0;  
    int j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen)  
    {  
        //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
        if (j == -1 || s[i] == p[j])  
        {  
            i++;  
            j++;  
        }  
        else  
        {  
            //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
            //next[j]即为j所对应的next值        
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}
KMP算法

部分匹配表NEXT数组

前面讲解了KMP算法思想,其中一个关键是next数组。

next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。

next数组在KMP算法中作用:在匹配字符失败时,会查找next数组,数组中数值告诉模式串应该跳到那个位置。如果next[j]为0或-1,则从头开始(模式串前缀没有相同的),如next[j] = k 且k>0,代表下次匹配跳跃到j之前的某个字符,跳过了K个字符。(模式串中的后缀和前缀有k字符相同),比如next[j] = 2,那么模式串中后2位和模式串前2位相同的。

最大长度表

在理解部分匹配表,先掌握前缀后缀。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示。

原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):

NEXT数组-部分匹配表

next数组的数值的含义时,此字符前相同的前缀和后缀的最大值,最大长度表表示当前串中相同的前缀和后缀,所以通过第①步骤求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:将第①步骤中求得的值整体右移一位,然后初值赋为-1,如下表格所示:

算法思想

若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;

若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。

假定给定模式串ABCDABCE,且已知next [j] = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。

如果pk != pj 呢?说明“p0 pk-1 pk”  ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。

若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,从而next [j + 1] = k’ + 1 = next [k' ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0。

代码:

void GetNext(char* p,int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀  
        if (k == -1 || p[j] == p[k])   
        {  
            ++k;  
            ++j;  
            next[j] = k;  
        }  
        else   
        {  
          //k经常为0,next[k]为-1,又跳到上面分支,k为0意味新前缀,到这分支然后为-1,新的开始
            k = next[k];  
        }  
    }  
}
部分匹配组

 getNext函数的进一步优化

注意到,上面的getNext函数还存在可以优化的地方,比如:

                 i=3

S: a   a   a   b   a   a   a   a   b

P: a   a   a   a   b

                 j=3

此时,i=3、j=3时发生失配,next[3]=2,此时还需要进行 3 次比较:

i=3, j=2;  

i=3, j=1;  

i=3, j=0。

而实际上,因为i=3, j=3时就已经知道a!=b,而之后的三次依旧是拿 a 和 b 比较,因此这三次比较都是多余的。

此时应当直接将P向右滑动4个字符,进行 i=4, j=0的比较。

一般而言,在getNext函数中,next[i]=j,也就是说当p[i]与S中某个字符匹配失败的时候,用p[j]继续与S中的这个字符比较。

如果p[i]==p[j],那么这次比较是多余的(如同上面的例子),此时应该直接使next[i]=next[j]。

完整的实现代码如下:

void getNextUpdate(const std::string& p, std::vector<int>& next)
{
    next.resize(p.size());
    next[0] = -1;

    int i = 0, j = -1;

    while (i != p.size() - 1)
    {
        //这里注意,i==0的时候实际上求的是nextVector[1]的值,以此类推
        if (j == -1 || p[i] == p[j])
        {
            ++i;
            ++j;
            //update
            //next[i] = j;
            //注意这里是++i和++j之后的p[i]、p[j]
            next[i] = p[i] != p[j] ? j : next[j];
        }
        else
        {
            j = next[j];
        }
    }
}
改进算法
原文地址:https://www.cnblogs.com/polly333/p/4739037.html