KMP算法详解

KMP算法
  • 模式匹配:子串定位运算,即在主串中找出子串出现的位置。
  • 在串匹配中,将主串 S 称为目标串,子串 T 称为模式串。
  • 如果在主串 S 中能够找到子串 T,则称,匹配成功,返回第一个和子串 T 中第一个字符相等的字符在主串 S 中的序号;否则称为匹配失败,返回 0 。
串的存储结构
  • 串的存储结构如下:

    typedef struct {  
          char *ch;     // 若非空则按串长分配存储区,否则 ch 为 NULL 
          int length;   // 串长度 
    } HString; 
    
    #define maxstrlen 255      // 可在 255 以内定义最大串长。 
    typedef unsigned char SString[maxstrlen+1];     // 0 号单元存放串的长度。 
    // SString a 等价于 unsigned char a[maxstrlen+1]
    
    #define CHUNKSIZE 80    // 可由用户定义的块大小
    typedef struct Chunk {  // 结点结构     
        char ch[CHUNKSIZE];    
        struct Chunk  *next;
    } Chunk; 
    
    typedef struct {        // 串的链表结构     
        Chunk *head, *tail; // 串的头和尾指针
        int  curlen;        // 串的当前长度
    } LString;
    
    
    • 在堆区和栈区存储时,默认将串的 0 号单元存储串的长度 length 。
朴素的模式匹配算法
  • 从主串 S 的第 pos 个字符起和模式 T 的第一个字符比较之,若相同,则继续比较后续字符;否则从主串 S 的下一个字符起再重新和模式 T 的字符比较之。

    int Index(SString S, SString T, int pos)
    {   
        int i , j;
     	i = pos;  
        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];  // (此时j值已经越界)找到了,返回序号  
        else    
            return 0;        //(找不到,返回0)
    } // Index
    
    
    主串Si-j+1Si-j+2……Si-1Si
    模式串T1T2……Tj-1Tj
    • 从高位向低位看(从右向左),当主串第 i 位与模式串第 j 位出现失配时,需要退回到起始匹配位置并寻找其下一位重新匹配。
    • 由上表可知,模拟串的起始位为 T1,T1 距 Tj j - 1 个元素,所以 Si 距离其起始位置也为 j - 1 个元素,所以其起始元素为 Si-j+1,下一位为 Si-j+2
    • 从最坏情况来看,假设主串有 m 个字符,模式串有 n 个字符,则第一次比较 n 次,第二次比较 n 次,主串最后取到 m - n + 1 个字符,仍然比较 n 次。所以时间复杂度为 O(m*n),若能找到一种算法可以大幅度降低时间复杂度,则可提高运算效率,由此引出一种高效匹配算法——KMP算法
KMP算法
  • 引例:主串:a b a b c a b c a c b a b ,模式串:a b c a c

    • 第一次匹配:

      ababcabca
      abcac
    • 可知第一次匹配时,在第 3 位出现失配,而 T2 已经完成了比较,可知 S2 为 b,而 T1 为 a,所以不必从 S2 再次比较,可以直接将 S3 与 T1 进行比较。

    • 第二次匹配:

      ababcabca
      abcac
    • 可知第三次匹配时,在第 5 位出现失配,而 T4 已经完成了比较,可知 S6 为 a,而 T1 为 a,所以不必从 S6 再次比较,可以直接将 S7 与 T2 进行比较。

    • 第三次匹配:

      ababcabcac
      abcac
  • 根据上例可知,不是每次失配都要从主串起始位置的下一位开始比较,可以按照一定规律跳过无效比较进而减少时间复杂度。

  • 第一次失配时,主串比较了 a b (S1S2),第二次从模式串第 1 位开始比较,第二次失配时,主串比较了 a b c a (S3S6),第三次从模式串第 2 为开始比较。

  • 通过多次试验观察可知,下一次起始比较位与主串前后相同位数有关,第一次 a b,无相同位数,所以下一次起始位为 0 + 1 = 1,第二次 a b c a,第一位与第四位相同,所以下一次起始位置为 1 + 1 = 2 。

  • 由此可得,若主串比较 a b c a b,在第六位失配,前后相同位数为两位(a b),所以下一次起始位置为 2 + 1 = 3 。所以下一次比较的起始位置应为:前后相同位数 + 1 。但这只是我们肉眼观察得到的,计算机无法通过观察得到此规律,所以我们需要用计算机角度理解此规律。

  • 在此处我们引入 next[] 数组,再此数组内存放失配位与重新比较位的关系,串的 0 位存放串的长度,所以无法在 0 位失配,所以数组的 0 位为 0,当第一位失配时,下一次比较仍需从第一位开始比较。next[] 数组中的数据与主串无关,仅与模式串有关。

  • 以模式串 a b a c a b 为例,在 next[] 数组中,第 0 位为 0,第 1 位为 0,可列出以下表格:

    模式串ab
    失配位012
    next00
    • 若模式串在第二位出现失配,则需从第一位重新开始比较,所以 T2 的 next 值为 1。
    模式串aba
    失配位0123
    next001
    • 若模式串在第三位出现失配,则说明主串对应第三位不是 a ,则需查看 T3 的前一位(即T2)的 next 值,T2 的 next 值为 1,T1 与T2 不同,所以 T3 的 next 值为 1(T2 的 next 值)。
    模式串abac
    失配位01234
    next0011
    • 若模式串的第四位出现失配,则说明主串对应第四位不是 c,则需查看 T4 的前一位的 next 值,T3 的 next 值为 1,T1 与 T3 的值相等(都为 a),所以将 T3 的 next 值加 1 填入 T4 中。
    模式串abaca
    失配位012345
    next00112
    • 若模式串的第五位出现失配,则说明主串对应第五位不是 a,则需查看 T5 的前一位的 next 值,T4 的 next 值为 2,T2 与 T4 不相等,则需查看 T2 的 next 值,为 1,T1 与 T4 不相等,所以要从第一位重新开始比较,所以 T5 的 next 值为 1。
    模式串abacab
    失配位0123456
    next001121
    • 若模式串的第六位出现失配,则说明主串对应第六位不是 b,则需查看 T6 的前一位的 next 值,T5 的 next 值为 1,T1 与 T5 相等,所以将 T5 的 next 值加 1 填入T6 中。
    模式串abacab
    失配位0123456
    next0011212
  • 根据上述分析我们完成了对 next 表的构建,当 next = 0 时说明对应的主串比较位应向后挪动一位,即起始位置应由 S1 变成 S2 ,而其他值就是当失配位失配后的重新开始比较的位置,例如当第六位失配后,只需从第二位重新开始比较即可。

  • 引入 next[] 的意义就在于,定位主串初始比较位 i 指针的不必回退,而是通过指向模式串初始比较位的 j 指针的改变降低时间复杂度(此处指针与C语言指针的意义不同),参看 next 数组生成算法:

    void get_next(SString T, int next[]) 
    {
    	i = 1; 
        j = 0; 
        next[1] = 0;
    	while (i < T[0]) {        // i小于模式串长度
        	if ( j == 0 || T[i] == T[j] )  
            { 
                ++i;  
                ++j;  
                next[i] = j;  // 
            }
            else  
                j = next[j];  // j指针回退
        }
    }  
    
  • 对应的 KMP算法为:

    int Index_KMP(SString S, SString T, int pos) 
    {
        i = pos;   
        j = 1;
        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;
    }
    
  • 通过上述 next[] 算法规则,当模式串为 a a a a b 时,会得出以下 next 数组:

    模式串aaaab
    失配位12345
    next01234
    • 当 T2 失配时,S2 已经确定不是 a 了,所以再次从第一位进行比较结果也是相同的,同理可得 T3、T4 也是相同的道理,由此可见 next 数组不能彻底地将时间复杂度降到最低,仍有优化的余地。
  • 所以在 next[] 后我们再次引入 nexrval[] 来减少 next[] 的无效比较,再次引例:

    模式串abaabcac
    失配位12345678
    next01122312
    nextval01021302
    • 第一位的 nextval[] 必定为 0。
    • 第二位的 next[] 值为 1,第二位与第一位不同(T2 与 T1),不算重复比较,所以从第一位(nextval[]值)重新开始比较。
    • 第三位的 next[] 值为 1,但第三位与第一位相同,算重复比较,所以挪动主串后重新开始比较。
    • 第四位的 next[] 值为 2,第四位与第二位不同,不算重复比较,所以从第二位重新开始比较。
    • 第五位的 next[] 值为 2,但第五位与第二位相同,算重复比较,所以需要再次读取第二位的 next[] 值为 1,第五位与第一位不同,所以从第一位重新开始比较。
    • 第六位的 next[] 值为 3,第六位与第三位不同,不算重复比较,所以从第三位重新开始比较。
    • 第七位的 next[] 值为 1,但第七位与第一位相同,算重复比较,所以挪动主串后重新开始比较。
    • 第八位的 next[] 值为 2,第八位与第二位不同,不算重复比较,所以从第二位重新开始比较。
  • 由此可得 nextval[] 的生成算法为:

    void get_nextval(SString T, int nextval[]) 
    {
        int i,j;
        i = 1; 
        j = 0; 
        nextval[1] = 0;
        while (i < T[0]) {
            if ( j == 0 || T[i] == T[j] )  
            { 
                ++i;  
                ++j; (j就是当前下表的next值)
                if(T[i]!=T[j])  
                    nextval[i] = j; 
                else 
                    nextval[i] = nextval[j]; 
            }
            else  
                j = next[j];
        }
    }  
    
    
  • 只需将上述 KMP算法中的 next[] 替换为 nextval[] 即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值