串和KMP

串和KMP

:有限的字符序列,告知空间首地址,约定一个结束标志。

不同语言的串设计不同,在数据结构中脱离语言给出一个新的串的定义

 // 串头
 typedef struct{
  char *str;
  int length;
 }StrType;
 ​
 int strAssign(StrType *str,const char *ch){
  if(str-str){   //str中已有数据,先释放再通过ch赋值
      free(str->str);
  }
  // 求ch的长度
  int len = 0;
  while(ch[len]){
      ++len;
  }
  if(len==0){
      str->str=NULL;
      str->len=0;
  }else{
      // 多申请两个空间,把\0和首地址空出来,首地址存放监视哨
      str->str=malloc(sizeof(char)*(len+2));
      for(int i=0;i<=len;++i){
          str->str[i+1]=ch[i];
      }
      str->length=len;
  }
  return 1;
 }
  • 暴力匹配

    字符串模式匹配:在主串中找到与模式串相同的子串并返回其首个元素所在位置

    时间复杂度:O(mn),时间复杂度高

     // 从1号开始(0号存放了监视哨)
     // 假设法:主串两个指针(k是假设正确的位置,i用来从k开始遍历验证的指针),
     // 模式串一个指针(j是模式串的指针)
     int index_simple(const StrType *str,const StrType *substr){
         int i=1;
         int j=1;
         int k=i;   // 记录假设值
         while(i<=str->length && j<=subStr->length){
             if(str->str[i]==substr->str[j]){
                 ++i;
                 ++J
             }else{
                 ++k;
                 j=1;
                 i=k;
             }
         }
         // 说明与模式串完全匹配
         if(j>subStr->length){
             return k;
         }
         return 0;
         
     }

  • KMP

    只回溯模式串,不一定是回溯到==1,比较指针i不回溯

    next数组:当主串与模式串的某一个字符不匹配时,模式串要回退的位置专门用于存放子串失配后从模式串哪个位置进行比较。当失效点出现时,要回退到哪个位置,就是next数组存放的元素

    • next[j] = 第 j 个字符前面 j-1位字符组成的子串的前后缀重合字符数 + 1

    • next[0]=next[1]=1

    例:

     a  b  a  a  b  c
     |---------- |  |
          j-1       j

    前后缀都不包含自己本身,因为包含本身的话前后缀最大公共序列就是本身了

    • 前缀:a ab aba abaa

    • 后缀:b ab aab baab

 void getNext(const StrType *substr,int next[] ){
     int i=1,j=0;    // i表示当前处理到的字符位置,j表示前后缀匹配的长度
     next[1]=0;      // next[1]永远是0,因为第一个字符前面没有前缀
     while(i < substr->length){
         // 若j==0,说明退无可退重新回到最开始的位置进行匹配
         // 若substr->str[i] == substr->str[j]则说明前后缀匹配
         // 更新i、j并且更新next[j]=j
         if(j==0 || substr->str[i] == substr->str[j]){
             ++i;
             ++j;
             next[i]=j;
         }else{
             j=next[j];  // 若不满足则回退回原先记录的最大公共序列数
         }
     }
 }
 int indexKMP(const StrType *str,const StrType *substr){
     int i=1;
     int j=1;
     while(i<=str->length && j<=substr->length){
         if(j==0 || str->str[i] == substr->str[i]){
             i++;
             j++;
         }else{
             j=next[j];
         }
         if(j>substr->length){
             return i-substr->length;
         }
         return 0;
     }
 }

假设模式串是 "abab"

ij当前匹配next[j] 说明
10a vs -j==0 → next[1]=0
21b vs a不匹配 → j=next[1]=0
20b vs -j==0 → i=3, j=1, next[2]=1
31a vs a匹配 → i=4, j=2, next[3]=2
42b vs b匹配 → i=5, j=3, next[4]=3
🪄疑问:为什么不需要去枚举前后缀所有的结果最后找最大公共序列?

答:

因为上述算法在求next[i]时,服用了前面已经算过的结果

利用了动态递推

  1. 代码中两个指针 i 和 j 的含义

在下面这行逻辑里:

 if (j == 0 || substr->str[i] == substr->str[j]) {
     ++i;
     ++j;
     next[i] = j;
 } else {
     j = next[j];
 }
  • i:当前要扩展的位置(新字符)

  • j:当前前后缀相等的“长度”

当我们试图把 P[i] 加入已有匹配的前缀后缀时:

  • 如果 P[i] == P[j]: 那么前后缀匹配长度就能再多 1 → 更新 next[i+1] = j+1

  • 如果 P[i] != P[j]: 说明现在这个后缀不行了 → 但也许更短的后缀可以匹配 → 所以回退到 next[j] 再尝试 j = next[j]; (这一步就体现了“复用之前的匹配结果”)


把算法思想一句话总结:

这段代码通过在遍历中动态维护“前后缀相等长度 j”, 复用之前计算过的 next 信息,避免了重新枚举全部前缀后缀, 实现了“线性时间”求出最长公共前后缀长度数组。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值