美文网首页
从0开始——串(KPM算法)

从0开始——串(KPM算法)

作者: c枫_撸码的日子 | 来源:发表于2018-11-13 21:33 被阅读0次

    1.串的定义

    由0个或者多个字符串组成的有限序列,又称为字符串。

    2.串的抽象数据结构

    ADT 串
    Data
      串的元素仅由字符组成,相邻元素具有前驱和后继关系。
    Operation
      Index(S,T,pos)//查找子串
      Replace(S,T,V);//替代
      等等
    

    串这一章中,最重要的就是KPM(看毛片)算法的掌握和理解!!!

    3.朴素的模式匹配算法

    这里假设主串S的长度存储在S[0]中,子串T的长度存储在T[0]中,
    因为T[0]和S[0]存储的是长度,所以实际字符从T[1]和S[1]开始
    回溯的时候:
    i=i-j+2;//回到首次匹配的下一个位置(如果从0开始,i=i-j+1)
    j=1;//子串从第一个位置开始匹配 (如果从0开始,j=0)

    //返回子串T在主串S从第pos个字符之后匹配的位置,不存在返回0
    
    int Index(String S,String T,int pos)
    {
        int i = pos;//从第pos个字符之后开始匹配
        int j = 1;//子串从第一个字符开始匹配
        
        while(i<=S[0] && j<=T[0])//主串没有结束,且子串没到末尾时候 
        {
            if(S[i]==T[i])
            {
                i++;//继续往下匹配 
                j++;
            }
            else
            {
                i=i-j+2;//回到首次匹配的下一个位置
                j=1;//子串从第一个位置开始匹配 
            }
            if(j>T[0])//如果完全匹配上了,j就会>T[0]至少一个长度
                return i-T[0];
            else
                return 0; 
            
        } 
    } 
    

    4.KMP模式匹配算法

    参考文章:

    1. 详解KMP算法

    2. KMP原理、分析及C语言实现

    3.小甲鱼-KMP算法视频详解

    小甲鱼的视频讲的蛮清晰的,静下心来看,跟着小甲鱼思考,就能弄懂这个KMP算法!

    其实KMP算法的核心就在于next数组,弄懂这个数组,就弄明白KMP算法了。
    这个next数组,就是指在子串T(也称为模式串T)匹配失败后,j应该回溯到哪个位置继续匹配。
    next的值取决于前缀和后缀有几个相同的字母。
    next的值 = 最大前缀和后缀相同的字母 + 1

    next数组详解

    T[0]=9,表示数组的长度。
    j表示当前T串下标
    前缀:必须包含第一个字母,且不包含最后一个字母
    后缀:必须包含最后一个字母,且不包含第一个字母
    当j=1时,1到j-1没有任何字母,next[1]=0
    当j=2时,1到j-1字母为a,没有前缀和后缀,next[2]=0+1=1
    当j=3时,1到j-1字母为ab,前缀:a,后缀:b,没有相同的字母next[2]=0+1=1
    当j=4时,1到j-1字母为aba,前缀:a 后缀:a ,有1个相同的字母,next[2]=1+1=2
    当j=5时,1到j-1字母为abab,前缀:ab 后缀:ab ,有2个相同的字母,next[2]=2+1=3
    当j=6时,1到j-1字母为ababa,前缀:aba 后缀:aba ,有3个相同的字母,next[2]=3+1=4
    当j=7时,1到j-1字母为ababaa,前缀:a 后缀:a ,有1个相同的字母,next[2]=1+1=2
    当j=8时,1到j-1字母为ababaaa,前缀:a 后缀:a ,有1个相同的字母,next[2]=1+1=2
    当j=9时,1到j-1字母为ababaaab,前缀:ab 后缀:ab ,有2个相同的字母,next[2]=1+1=3
    得到下面表格:

    模式串T 9 a b a b a a a b a
    下标j 0 1 2 3 4 5 6 7 8 9
    next数组 ^ 0 1 1 2 3 4 2 2 3

    这里定义i和j
    i:表示后缀的位置
    j:表示前缀的位置

    i=3,j=1,T[3]=T[1]=a
    此时:i和j都应+1,i++=4,j++=2,
    而前缀=a,后缀=a,最大相同前缀为1,
    next[i]=1(最大相同前缀和后缀)+1=2,此时j也等于2,因此next[i]=j

    当i=4,j=2,T[4]==T[2]=b
    此时:i和j都应+1,i++=5,j++=3,
    而前缀=ab,后缀=ab,最大相同前缀为2,
    next[i]=2(最大相同前缀和后缀)+1=3,此时j也等于3,因此next[i]=j

    当i=5,j=3,T[5]==T[3]=a
    此时:i和j都应+1,i++=6,j++=4,
    而前缀=aba,后缀=aba,最大相同前缀为3,
    next[i]=3(最大相同前缀和后缀)+1=4,此时j也等于4,因此next[i]=j

    T[i]==T[j]情况下,翻译成代码:

    if(T[i]==T[j])
    {
      i++;
      j++;
      next[i]=j;
    }
    

    当i=6,j=4,T[6]=a,不等于T[4]=b
    此时:j应该回溯,那么应该回溯到哪个位置呢?

    回顾一下next数组
    1.next数组的含义:当模式串T匹配失败时,next数组的值指导应该回溯到T串的哪个位置开始下一轮的匹配
    2.当i=6时,T串为"ababaa,我们当做母串,j=4,T串为"abab,我们当做子串
    3.在1和2的基础上,我们思考一下在哪里匹配失败了,在j==4的时候匹配失败了是吧,那失败之后,next数组就来指导下一轮的匹配,
    因此 j==next[j]==next[4]=2,从j串的第2个位置开始下一轮匹配
    T[i]不等于T[j]情况下,翻译成代码:

    else
    {
      j=next[j];//j回溯
    }
    

    另外,无论什么模式串T,next[1]==0,永远为0,因为没有任何前缀和后缀嘛!

    next[1]=0;
    

    获得next数组的代码
    int i=1;//i表示后缀,所以从1开始
    int j=0;//j表示前缀,所以从0开始(数组的第一位是0呀)
    T[0]存储模式串T的长度

    注意:由于T[0]存储的是长度,因此没必要判断T[0]是否和T[1]相等,
    所以当j=0的时候,
    i++=2;//第二个字符
    j++=1;//第一个字符
    next[i]=next[2]=1;和表格的相同

    void get_next(String T,int *next)
    {
        int i=1;//i表示后缀,所以从1开始
        int j=0;//j表示前缀,所以从0开始(数组的第一位是0呀)
        next[1]=0;//无论什么模式串T,next[1]==0,永远为0,因为没有任何前缀和后缀嘛!
        while(i<T[0])//当i小于模式串T的长度时
        {
          //由于T[0]存储的是长度,因此没必要判断T[0]是否和T[1]相等,直接往后判断
          if(j==0 || T[i]==T[j])
          {
              i++;
              j++;
              next[j]=j;
          }
          else
          {
              j=next[j];//j回溯
          }    
        }
    }
    

    KMP算法的实现
    其实KMP算法和之前的朴素算法最大的不同就是这个next数组,避免不必要的回溯。

    //返回子串T在主串S从第pos个字符之后匹配的位置,不存在返回0
    int Index_KMP(String S,String T,int pos)
    {
        int i = pos;//从第pos个字符之后开始匹配
        int j = 1;//子串从第一个字符开始匹配
        int next[255];//用于保存next数组
        
        
        get_next(T,next);//对T串分析,获得next的值 
        showNext(next,T[0]);//打印一下next的值
        while(i<=S[0] && j<=T[0])//i<主串S的长度,且j<子串T的长度,继续循环 
        {  
            //j==0,直接++,从第一个位置开始比较,                     
            if(j==0 || S[i]==T[j])//S[i]=T[j],继续往下比较
            {                     
                i++;
                j++;
            }
            else//匹配失败,则从next数组获得下一轮T串应该从哪个位置开始比较 
            {
                j=next[j];
            }
            
        }    
    
        if(j>T[0])//说明T串匹配完毕 
        {
            printf("j=%d,T[0]=%d 匹配成功,从第%d个位置开始完成匹配\n",j,T[0],i-T[0]);
            return i-T[0];
        }
        else
        {
            printf("匹配失败\n");
            return 0;
        }   
    } 
    

    KMP算法的终极优化
    假设S="aaaabcde",T="aaaaax",next=012345,因为前面的a是相同的,因此没必要从5回溯到4在回溯到3...最后到0去比较

    void get_next(String T,int *next)
    {
        int i=1;//i表示后缀,所以从1开始
        int j=0;//j表示前缀,所以从0开始(数组的第一位是0呀)
        next[1]=0;//无论什么模式串T,next[1]==0,永远为0,因为没有任何前缀和后缀嘛!
        while(i<T[0])//当i小于模式串T的长度时
        {
          //由于T[0]存储的是长度,因此没必要判断T[0]是否和T[1]相等,直接往后判断
          if(j==0 || T[i]==T[j])
          {
              i++;
              j++;
              if(T[i]!=T[j])//当i和j++后,如果T[i]和前缀T[j]不同
                next[j]=j;
              else//否则,直接去next[j]的值
                next[j]=next[j];
          
          }
          else
          {
              j=next[j];//j回溯
          }    
        }
    }
    

    到此,KPM算法毕业!!!

    完整源码

    S串: " ababababcababaaabaddd";
    T串:" ababaaaba";

    运行结果
    #include <stdio.h>
    #include <windows.h>
    typedef char* String;
    
    void get_next(String T,int *next)
    {
        int i=1;
        int j=0;
        next[1]=0;
        while(i<T[0])
        {
            if(j==0 || T[i]==T[j])
            {
                i++;
                j++;
                if(T[i]!=T[j])
                    next[i]=j;
                else
                    next[i]=next[j];
            }
            else
            {
                j=next[j];//j回溯 
            }
        }
    }
    //打印数组的值 
    void showNext(int *next,int len)
    {
        int i=1;
        printf("next的值为:");
        while(i<=len)
        {
            printf("%d ",next[i]);
            i++;
        }
        printf("\n");
    } 
    //返回子串T在主串S从第pos个字符之后匹配的位置,不存在返回0
    int Index_KMP(String S,String T,int pos)
    {
        int i = pos;//从第pos个字符之后开始匹配
        int j = 1;//子串从第一个字符开始匹配
        int next[255];//用于保存next数组
        
        
        get_next(T,next);//对T串分析,获得next的值 
        showNext(next,T[0]);//打印一下next的值
         
        while(i<=S[0] && j<=T[0])//i<主串S的长度,且j<子串T的长度,继续循环 
        {
            if(j==0 || S[i]==T[j])//j==0,直接++,从第一个位置开始比较,                     
            {                    //因为T[0]和S[0]存储的长度 
                i++;
                j++;
            }
            else//匹配失败,则从next数组获得下一轮T串应该从哪个位置开始比较 
            {
                j=next[j];
            }
            
        } 
        if(j>T[0])//说明T串匹配完毕 
        {
            printf("j=%d,T[0]=%d 匹配成功,从第%d个位置开始完成匹配\n",j,T[0],i-T[0]);
            return i-T[0];
        }
        else
        {
            printf("匹配失败\n");
            return 0;
        }
        
    } 
    
    int main()
    {
        char S[255] = " ababababcababaaabaddd";
        S[0]=18;
        char T[255] = " ababaaaba";
        T[0]=9;
    
        Index_KMP(S,T,1);
        system("pause");
        return 1;
    } 
    

    相关文章

      网友评论

          本文标题:从0开始——串(KPM算法)

          本文链接:https://www.haomeiwen.com/subject/mzocfqtx.html