字符串匹配算法的学习及分析

来源:互联网 发布:php new soapclient 编辑:程序博客网 时间:2024/06/01 11:17
继续上一篇。这周用了两天时间学习了http://blog.csdn.net/WINCOL/article/details/4795369这里提到的几种字符串匹配算法,想明白后自己认真实现了一下,并进行了比较,结果出乎我当初的预料,strstr果然还是像作者本人所说的一如既往的好。总结了自己的分析过程以及一点点思考,记录如下:

    分析前思考:

    1)到底哪种方法好?

    2)那些性能好的算法,快在哪儿?关键语句是哪些?

    3)平时写代码的时候应该怎么用?


    - 1 - 算法复杂度以及真正在实现的过程中的实际性能
          
           复杂度与实际的实现无关

           对于同一个算法,不同的写法会在实际性能中有很大的差别!所以实现也要写得简洁高效!

    - 2 - Brute Force(BF或蛮力搜索) 算法:

         这是世界上最简单的算法了。
         首先将匹配串模式串左对齐,然后从左向右一个一个进行比较,如果不成功则模式串向右移动一个单位。
速度最慢。

         改进呢?

         我们注意到Brute Force 算法是每次移动一个单位,一个一个单位移动显然太慢,是不是可以找到一些办法,让每次能够让模式串多移动一些位置呢?当然是可以的。

         我们也注意到,Brute Force 是很不intelligent 的,每次匹配不成功的时候,前面匹配成功的信息都被当作废物丢弃了,当然,就如现在的变废为宝一样,我们也同样可以将前面匹配成功的信息利用起来,极大地减少计算机的处理时间,节省成本。^_^

            注意,蛮力搜索算法虽然速度慢,但其很通用。

            大学学数据结构的时候,模式匹配这一节,一共讲两个算法,1是BF,2是KMP。BF的实现代码用i,j来做,但MS的strstr的实现方式看着更好一些,不过性能基本没有太大的变化。复杂度O(n+m),最坏情况O(n*m)

             再看glibc的strstr实现,算作BF算法的改进,也就是把前面匹配成功的信息利用起来,以提高效率。这个改进最关键的就在于两个变量:b和c,b存放模式串中的第一个字符,c存放模式串中的第二个字符,然后每次出现不匹配的情况就开始找下一个b出现的位置,再找c出现的位置。
//glibc strstr
/*
* My personal strstr() implementation that beats most other algorithms.
* Until someone tells me otherwise, I assume that this is the
* fastest implementation of strstr() in C.
* I deliberately chose not to comment it. You should have at least
* as much fun trying to understand it, as I had to write it :-).
*
* Stephen R. van den Berg, berg@pool.informatik.rwth-aachen.de    */
char *bruteforce_strstr_glibc (const char *phaystack, const char *pneedle)
{
    const unsigned char *haystack, *needle;
    chartype b; // b是一个“定量”,始终用于存放模式串的第一个字符
    const unsigned char *rneedle;

    haystack = (const unsigned char *) phaystack;

    if ((b = *(needle = (const unsigned char *) pneedle)))
    {
        chartype c;
        haystack--;       /* possible ANSI violation */
                          /* for unify processing afterward */

        以下这种颜色的部分是一个小循环,只循环一次,找到模式串中第一个字符在匹配串中的位置,找到后退出,此后再不执行此段代码
        {
            chartype a;
            do
                if (!(a = *++haystack))
                    goto ret0;         /* if haystack ends, return 0 */
            while (a != b);            /* find the first postion that equals */
        }

        /* first char equals, go on */
        if (!(c = *++needle))
            goto foundneedle;          /* only one char in needle, found it */
        ++needle;                      /* needle go to 3rd char 把needle指针置到第3位,在crest后才会使用,此时表明前两个字符已匹配成功*/
        goto jin;

        for (;;) //这个for是一个大循环,一直到程序结束
        {
            {
                chartype a;
                if (0)
                    // 如果第二个字符相等则直接进入crest的第三个字符比较
                    // 显然这部分代码也只执行一次!
                    jin:{
                        if ((a = *++haystack) == c)   /* check if the second char in needle equals 
c是一个“定量”,始终用于存放模式串的第二个字符*/
                            goto crest;
                    }
                //以下的绿色代码部分,就是执行一个循环体,每次循环体里头执行两个字符的比较,do内一次,while内一次
                /*********************************************************************************/
                else
                    a = *++haystack;                  /* set a = next char to be compared 这是从第三个字符后开始的某个字符不等时,回来执行的代码,此时haystack为匹配串中与模式串相等的第一个字符所在的位置,因此a为第二个将要被匹配的字符*/
                do
                {
                    // a从下一个字符起,不断地++与第一个字符比较,直到比到相等的为止,相当于前面紫色部分的代码
                    for (; a != b; a = *++haystack)   /* search haystack to find a char equals the 1st char of needle b是一个“定量”,始终用于存放模式串的第一个字符*/
                    {
                        if (!a)
                            goto ret0;
                        if ((a = *++haystack) == b)   /* one loop compare 2 chars */
                            break;
                        if (!a)
                            goto ret0;
                    }
                }
                while ((a = *++haystack) != c);      /* until second char equals c是一个“定量”,始终用于存放模式串的第二个字符 */
                /*********************************************************************************/
            }
crest:/* 2 char equals, go on 每次循环体里头执行两个字符的比较,do内一次,while内一次,任何一次不匹配则回到上文,从第一个字符开始从头匹配
           直到比到第2字符相等时,进入该部分代码段,进行后续比较*/
            {
                chartype a;
                {
                    const unsigned char *rhaystack;
                    /* rhaystack =next char, haystack-- set haystack point at the postion to be returned */
                    if (*(rhaystack = haystack-- + 1) == (a = *(rneedle = needle)))/* go on comparing from 3rd char of needle */
                        do
                        {
                            if (!a)
                                goto foundneedle;
                            if (*++rhaystack != (a = *++needle))
                                break;
                            if (!a)
                                goto foundneedle;
                        }
                        while (*++rhaystack == (a = *++needle));/* until reach a char not equal */
                    needle = rneedle; /* took the register-poor aproach, needle point to 3rd char needle总是指向匹配串的第三个字符*/
                }
                if (!a)
                    break;
            }
        }
    }
foundneedle:
    return (char *) haystack;
ret0:
    return 0;
}

********************************************************************************************************
********************************************************************************************************
    - 3 - KMP算法:

          首先介绍的就是KMP 算法。

          原始论文:Knuth D.E., Morris J.H., and Pratt V.R., Fast pattern matching in strings, SIAM Journal on Computing, 6(2), 323-350, 1977.

这个算法实在是太有名了,大学上的算法课程除了最笨的Brute Force 算法,然后就介绍了KMP 算法。也难怪,呵呵。谁让Knuth D.E. 这么world famous 呢,不仅拿了图灵奖,而且还写出了计算机界的Bible <The Art of Computer Programming>( 业内人士一般简称TAOCP). 稍稍提一下,有个叫H.A.Simon 的家伙,不仅拿了Turing Award ,顺手拿了个Nobel Economics Award ,做了AI 的爸爸,还是Chicago Univ 的Politics PhD ,可谓全才。
           原来KMP里的K指的就是高爷爷啊。。。以前从来没有注意过...

              KMP 的思想是这样的:

           利用不匹配字符的前面那一段字符的最长前后缀来尽可能地跳过最大的距离
           即,当出现不匹配的时候,i不回溯,j回溯到next[j];

          个人理解,KMP只有对模式串里有重复子串出现的情况才会快!(当一个字符串以0为起始下标时,next[i]可以描述为"不为自身的最大首尾重复子串长度"。)

            公式:
         
          
          注:这个公式是把数组从1开始算起,即j的初始值为1。
                若在代码里实现,将数组从0开始算起,则j的初始值为0,此时,当j=0时,next[j] = -1。

          下面举例说明KMP算法的应用:
j01234567串abaababcnext[j]-10011232

j = 0, 不比较,next[0] = -1 // 公式的情况1

j = 1, 不用比较,next[1] = 0 // 公式的情况3

j = 2, t0 != t1,因此,不存在真前缀,next[2] = 0 // 公式的情况3

j = 3, 因为next[2] = 0,不存在真前缀,所以比较t0和t2 // 可以将k的初值设为0,检查初值,为0则表示上一次不存在真前缀
         t0 = t2 => next[3] = 0+1 = 1 = k // 公式情况2,第一次出现k,k第一次被赋了有意义的初值

j = 4, k = 1 != 0,因此,比较t1和t3   k = next[3] = 1
         整个算法不容易理解之处如下解释:
         t1 != t3,说明t0t1 != t2t3, 即j+1= 4的时候,上一次j=3时的最大真前缀t0 = t2变化成t0t1, t2t3已经不能成为j+1 = 4的最大真前缀了,
         那么就要找再上一次找到的真前缀,以使t0...t(上一次的k) = t(j-上一次的k+1)...tj
         k = f(上一次的k) = next[next[3]] = next[k] = next[1] = 0 => 比较t0 = t3,因此,next[4] = k + 1 = 0 + 1 = 1 = 新的k的值

         整个说法容易混淆的地方在于三个k:k表示j=3时的k, 上一次的k表示j=2时的k, 新的k表示j=4时的k

j = 5, k = 1 != 0, 因此,比较 t1和t4
         t1 = t4,说明t0t1 = t3t4,即next[5] = k+1 = 1+1 = 2 = 新的k的值


j = 6, k = 2 != 0, 因此,比较t2和t5
         t2 = t5, 说明t0t1t2 = t3t4t5,即next[6] = k + 1 = 2+1 = 3

j = 7, k = 3 != 0, 因此,比较t3和t6
         t3 != t6, 因此要找j=6的上一次的最大真前缀
         k = next[next[6]] = next[3] = next[k] = 1 => 比较t1和t6 => t1 = t6,所以next[7] = k + 1= 2 = 新的K的值

      说明:对j=7来说,如果此时t1!=t6则,再找k = next[next[3]] = next[1] = 0再继续比较,直接相等为止,来计算新的K

代码实现如下:
void getNext (const char *pt, int *next)
{
/*    if(NULL == pt)
        return;*/
    int pt_len = strlen(pt);
    next[1] = 0;

    int k(-1), j(0);
    next[0] = -1;
    while(j < pt_len-1)
    {
        if(pt[k] == pt[j] || -1 == k) // 注意这里,比较j,实际是计算下一个j(即++j), 因为j从0开始,而next[0]已被赋过初值了~所以循环的第一次是计算
            next[++j] = ++k;          // next[1]的值
        else
            k = next[k];                  // k = next[next[j]] = next[k];
    }
}


********************************************************************************************************
********************************************************************************************************

    - 4 - Sunday算法


可供参考的链接,算是sunday的文章写分析得比较好懂的:
http://blog.sina.com.cn/s/blog_56ffb59b0100oolh.html

char *sunday (const char *pText, const char *pPattern)
{
    int pattern_len = strlen(pPattern);
    int text_len = strlen(pText);

    const int ASCII_SIZE = 128;
    int shift_table[ASCII_SIZE];

    //construct delta shift table
    //moving distance
    for(int i = 0; i < ASCII_SIZE; ++i)
        shift_table[i] = pattern_len+1;
    const char *p;
    for(p = pPattern; *p; ++p)
        shift_table[*p] = pattern_len - (p - pPattern); // 移动的距离

    const char *pTextEnd = pText + text_len;
    const char *t, *text = pText;

    //start searching
    while(text<pTextEnd)
    {
        p = pPattern;
        --text;
        //to find the first character
        while(text<pTextEnd && *++text!=*p)
            ;
        for(t = text; *++p; )
            if(*p != *++t)
                break;

        if(!*p)
            return (char*)text;

        text += shift_table[text[pattern_len]];
    }

    return NULL;
}


总结:

1. 通过此次分析比较,发现各种不同的算法,虽然分析上可以说很高效,但在实际比较起来,还看写程序员怎么去实现,高效的代码实现和高效的算法一样重要

2. 字符串匹配最重要的就在于,当出现不匹配的时候,串向后滑动多大距离,滑得越快,效益越高~

3. 对于不同的情况,可以用采用不同的算法。后面介绍的几种算法,其实是在串足够随机的情况下进行测试的。因此,实际中选用哪种方法,如何去实现,需要进一步分析。
   考虑的不光是时间,还有空间。
   比如sunday算法要求的字符串等。
   
4. 对算法的分析在于理解大师们解决问题的方法。实际写代码的时候,遇到类似情况,可以采取多种方法结合在一起进行分析和实际。

5. 没有绝对好的算法,总是有前提条件的。学到其中的思想,并应用到实际中去,提高能力才是最最重要的!!

原创粉丝点击