字符串匹配算法

1. 朴素算法

朴素算法是最简单的字符串匹配算法,也是人们接触得最多的字符串匹配算法。

2. Rabin-Karp算法

一个时间复杂度为O((N-M+1)*M)的字符串匹配算法,即Rabin-Karp算法。Rabin-Karp算法的预处理时间是O(m), 匹配时间OO((N-M+1)*M),既然与朴素算法的匹配时间一样,而且还多了一些预处理时间,那为什么我们 还要学习这个算法呢?

虽然Rain-Karp在最坏的情况下与朴素的世间复杂度一样,但是实际应用中往往比朴素算法快很多。而且该算法的 期望匹配时间是O(N+M)(参照《算法导论》)。

在朴素算法中,我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么, 是否有别的方法可以用来判断目标字符串是否包含子串呢?

答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对目标字符串和子串进行比较, 我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。 通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。 这个新方法在速度上比暴力法有显著提升。

Rabin-Karp算法的思想:

  1. 假设子串的长度为M,目标字符串的长度为N
  2. 计算子串的hash值
  3. 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次)
  4. 比较hash值
  5. 如果hash值不同,字符串必然不匹配,如果hash值相同,还需要使用朴素算法再次判断

为了快速的计算出目标字符串中每一个子串的hash值,Rabin-Karp算法并不是对目标字符串的 每一个长度为M的子串都重新计算hash值,而是在前几个字串的基础之上, 计算下一个子串的 hash值,这就加快了hash之的计算速度,将朴素算法中的内循环的世间复杂度从O(M)将到了O(1)。

关于hash函数的详细内容,可以参考这里或者《算法导论》。

#include<stdio.h>
#include<string.h>

// d is the number of characters in input alphabet
#define d 256 

/*  pat  -> pattern
    txt  -> text
    q    -> A prime number
*/
void search(char *pat, char *txt, int q)
{
    int M = strlen(pat);
    int N = strlen(txt);
    int i, j;
    int p = 0;  // hash value for pattern
    int t = 0; // hash value for txt
    int h = 1;

    // The value of h would be "pow(d, M-1)%q"
    for (i = 0; i < M-1; i++)
        h = (h*d)%q;

    // Calculate the hash value of pattern and first window of text
    for (i = 0; i < M; i++)
    {
        p = (d*p + pat[i])%q;
        t = (d*t + txt[i])%q;
    }

    // Slide the pattern over text one by one 
    for (i = 0; i <= N - M; i++)
    {

        // Chaeck the hash values of current window of text and pattern
        // If the hash values match then only check for characters on by one
        if ( p == t )
        {
            /* Check for characters one by one */
            for (j = 0; j < M; j++)
            {
                if (txt[i+j] != pat[j])
                    break;
            }
            if (j == M)  // if p == t and pat[0...M-1] = txt[i, i+1, ...i+M-1]
            {
                printf("Pattern found at index %d 
", i);
            }
        }

        // Calulate hash value for next window of text: Remove leading digit, 
        // add trailing digit           
        if ( i < N-M )
        {
            t = (d*(t - txt[i]*h) + txt[i+M])%q;

            // We might get negative value of t, converting it to positive
            if(t < 0) 
              t = (t + q); 
        }
    }
}

/* Driver program to test above function */
int main()
{
    char *txt = "GEEKS FOR GEEKS";
    char *pat = "GEEK";
    int q = 101;  // A prime number
    search(pat, txt, q);
    getchar();
    return 0;
}

  

3. KMP算法

 KMP算法之所以叫做KMP算法是因为这个算法是由三个人共同提出来的,就取三个人名字的首字母作为该算法的名字。其实KMP算法与BF算法的区别就在于KMP算法巧妙的消除了指针i的回溯问题,只需确定下次匹配j的位置即可,使得问题的复杂度由O(mn)下降到O(m+n)。

  在KMP算法中,为了确定在匹配不成功时,下次匹配时j的位置,引入了next[]数组,next[j]的值表示P[0...j-1]中最长后缀的长度等于相同字符序列的前缀。

  对于next[]数组的定义如下:

 1) next[j] = -1  j = 0

 2) next[j] = max(k): 0<k<j   P[0...k-1]=P[j-k,j-1]

 3) next[j] = 0  其他

 如:

 P      a    b   a    b   a

 j      0    1   2    3   4

 next    -1   0   0    1   2

 即next[j]=k>0时,表示P[0...k-1]=P[j-k,j-1]

 因此KMP算法的思想就是:在匹配过程称,若发生不匹配的情况,如果next[j]>=0,则目标串的指针i不变,将模式串的指针j移动到next[j]的位置继续进行匹配;若next[j]=-1,则将i右移1位,并将j置0,继续进行比较。

#include<stdio.h>
#include<string.h>
void getNext(char *p, int *next);

int KMPMatch(char *s ,char *p)
{
    int next[100] = {0};
    int M = strlen(s);
    int N = strlen(p);
    getNext(p,next);
    int i = 0, j = 0;
    while( i < M ) 
    {
        if(next[j] == -1 || s[i] == p[j])
        {
            i++;
            j++;
        }else
        {
            j = next[j];
        }
        if(j == N)
        {
            return i - N;
        }
    }
    return -1;

}

void getNext(char *p , int *next)
{
    int j , k ;
    next[0] = -1;
    j = 0;
    k = -1;
    while(j < strlen(p))
    {
        if(k == -1 || p[j] == p[k])
        {
            k++;
            j++;
            next[j] = k;
        }else{
            k = next[k];
        }
    }
}

int main()
{
    char *s = "lovely puppy , jianghaha";
    char *p = "jiang";

    printf( "匹配位置:%d
" , KMPMatch(s , p)) ;
    return 0;
}

因此KMP算法的关键在于求算next[]数组的值,即求算模式串每个位置处的最长后缀与前缀相同的长度, 而求算next[]数组的值有两种思路,第一种思路是用递推的思想去求算,还有一种就是直接去求解。 

1.按照递推的思想:

   根据定义next[0]=-1,假设next[j]=k, 即P[0...k-1]==P[j-k,j-1]

   1)若P[j]==P[k],则有P[0..k]==P[j-k,j],很显然,next[j+1]=next[j]+1=k+1;

   2)若P[j]!=P[k],则可以把其看做模式匹配的问题,即匹配失败的时候,k值如何移动,显然k=next[k]。

4. Boyer-Moore算法

待补充。http://blog.jobbole.com/52830/

5. Sunday算法

http://blog.163.com/yangfan876@126/blog/static/80612456201342205056344

原文地址:https://www.cnblogs.com/LyningCoder/p/3945984.html