ch4_6 KMP 字符串匹配 && ch4_7 重复子字符串

在这里插入图片描述

  1. 将原串的指针移动至本次「发起点」的下一个位置(b 字符处);匹配串的指针移动至起始位置。
  1. 尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。
    在这里插入图片描述

当使用朴素匹配法时, 当字符串与模式串中的字符不匹配时,
则字符串中的指针调整至下一个发起点;

匹配串中的指针调整至起始位置, 两个指针位置上的字符重新匹配;



Knuth-Morris-Pratt 算法的核心为前缀函数,记作 π(i),其定义如下:

对于长度为 m 的字符串 s,其前缀函数π(i)(0≤i<m) 表示 s 的子串 s[0:i] 的最长的相等的真前缀与真后缀的长度。

特别地,如果不存在符合条件的前后缀,那么 π(i)=0。其中真前缀与真后缀的定义为不等于自身的的前缀与后缀。

我们举个例子说明:字符串 aabaaabaabaaab 的前缀函数值依次为 0,1,0,1,2,2,30,1,0,1,2,2,3。

π(0)=0,因为 aa 没有真前缀和真后缀,根据规定为 0(可以发现对于任意字符串 π(0)=0 必定成立);

π(1)=1,因为 aaaa 最长的一对相等的真前后缀为 aa,长度为 11;

π(2)=0,因为 aabaab 没有对应真前缀和真后缀,根据规定为 00;

π(3)=1,因为 aabaaaba 最长的一对相等的真前后缀为 aa,长度为 11;

π(4)=2,因为 aabaaaabaa 最长的一对相等的真前后缀为 aaaa,长度为 22;

π(5)=2,因为 aabaaaaabaaa 最长的一对相等的真前后缀为 aaaa,长度为 22;

π(6)=3,因为 aabaaabaabaaab 最长的一对相等的真前后缀为 aabaab,长度为 33。

有了前缀函数,我们就可以快速地计算出模式串在主串中的每一次出现。

1.KMP

前缀串:指一串字符中,以第一个字符为开头的所有连续子串,但不包括最后一个字符;

后缀串: 指一串字符中,以最后一个字符为结尾的所有连续子串, 但不包括第一个字符;

1.1 kmp 优势

KMP的出现使得:

当字符串与模式串中的字符不匹配时:
1. 首先在模式串中检查已经匹配成功的部分, 是否存在相同的前缀和后缀,
1.1 如果存在, 则模式串指针跳转到相同前缀的后一个位置开始匹配; 但是,如果跳转后的模式串指针位置上的元素 与 字符串中指针位置上的元素仍然不匹配时,(并且,如果此时模式串指针前面已经不存在相同的前缀和后缀了,如果存在,重复上面步骤) 则模式串指针 重新回到模式串的起始位置;

通过以上分析可知:
KMP 算法相比于朴素法的优势在于, 使用KMP时:

  1. 字符串的指针不会往前回退, 只会不断往后移动;
  2. 模式串中的指针,回退过程中, 总是先寻找当前位置的子串上是否存在相同的前缀串, 然后回退到相同前缀串的后一个位置, 直到没有相同前缀串后, 模式串的指针才回退到初始位置;

在模式串中, 指针回退到下一个匹配点的位置与字符串无关,

1.2 next数组

具体讲来:
对于模式串 abcabd 的字符 d 而言,由它发起的下一个匹配点跳转必然是字符 c 的位置。
因为字符 d 位置的相同「前缀」和「后缀」字符 ab 的下一位置就是字符 c。

由此可见,在模式串中由某个位置回退到前面的匹配位置, 这个过程是与字符串无关的,我们将这一个过程称为寻找next 点;

所以,第一步我们先生成next 数组, 数组中的每个位置上的数值,代表了相同位置上的模式串中, 当该位置上的元素与字符串上的元素不匹配时, 模式串的指针应该跳转到具有相同前缀串的后一个位置上;

1.2 next数组的生成 ,

生成最大相同前缀后缀表,即构造next 数组的过程;

生成next 数组的关键点:

1. 后缀串的起点指针,用来遍历是模式串的,始终是向后移动的;
2. 判断两个指针对应位置上的元素是否相等, 来对前缀串的终止指针,进行移动;
3. 将 j j j 此时的位置 赋值给 n e x t [ i ] next[i] next[i];

根据模式串构造出 next 数组的逻辑步骤:

  1. 初始化 next[0] = 0;
  2. 使用后缀串的起点指针 i i i遍历模式串;
  3. 根据两种情况,移动前缀串指针的位置;
  4. 处理前缀串 与后缀串 相同的情况, 移动 j j j
  5. 处理前缀串 与后缀串 不同的情况, 回退 j j j
  6. next[i]赋值: 后缀串起点指针遍历过程中, 将当前移动 j j j 的位置赋值给 next[i] ;
       void getNext(int* next, const string& p){
           //1. next[0] 规定初始值;
           next[0] = 0;
           // i 后缀串的起始位置, 不断后移,用来遍历模式串;
           // j 前缀串的终止位置, 双向移动, 可回退,并且该数值存放到next[i] 中;
           int j = 0;
           for(int i = 1; i < p.size(); i++){// 2. 使用后缀串指针遍历 模式串;
               // 两个指针对应位置上元素不同时, 跳转前缀串指针;
               while( j - 1 > 0 && p[i] != p[j] )  j = next[j - 1];
               //  两个指针对应位置上元素相同时, 前缀串指针正常后移;
               if( p[i] == p[j] )    j++;
               // 赋值 next[i]
               next[i] = j;
           }
           
       }
 

1.3 模式串匹配字符串过程

开始模式串匹配字符串,
该过程中,调用上述的next[] 数组;

两个串匹配的关键点:

**1. 字符串的指针用来遍历字符串,始终是向后移动的, 模式串的指针根据情况移动;
2. 判断两个指针对应位置上的元素是否相等, 来对模式串的指针,进行移动;
**

具体步骤:

  1. 判断模式串 是否为空串; 若是, 返回 0;

  2. 新建一个 next 数组, 大小为模式串的长度;

  3. 调用 getNext 函数,生成 next 数组;

  4. 创建两个指针, i , j i, j i,j 遍历, i i i 遍历字符串, j j j 遍历模式串;

  5. 开始遍历字符串,该过程中, 字符串指针始终后移;

    5.1: 当两个指针对应位置上的元素不同时, 模式串指针,跳转回退;
    5.2 :当两个指针对应位置上的元素相同时, 模式串指针向后移动;
    5.3: 当模式串到达终止位置时,此时字符串指针位置为 i i i,
    返回此时字符串中,匹配模式串的起始位置, 即为 i − p . s i z e + 1 i - p.size + 1 ip.size+1;

  6. 当遍历字符串结束后, 之前如果没有return, 表明模式串指针没有走到模式串的结尾, 表明 字符串中 没有匹配模式串的 子串, 返回-1; 结束;

1.4 code

lc28: 字符串中 匹配 模式串

class Solution {
public:
       void getNext(int* next, const string& p){
           //1. next[0] 规定初始值;
           next[0] = 0;
           // i 后缀串的起始位置, 不断后移,用来遍历模式串;
           // j 前缀串的终止位置, 双向移动, 可回退,并且该数值存放到next[i] 中;
           int j = 0;
           for(int i = 1; i < p.size(); i++){// 2. 使用后缀串指针遍历 模式串;
               // 两个指针对应位置上元素不同时, 跳转前缀串指针;
               while( j  > 0 && p[i] != p[j] )  j = next[j - 1];
               //  两个指针对应位置上元素相同时, 前缀串指针正常后移;
               if( p[i] == p[j] )    j++;
               // 赋值 next[i]
               next[i] = j;
           }

       }

       int  strStr(string haystack, string needle){
          // i 用做字符串指针,始终后移, j 用作模式串指针, 根据情况移动;
           int i = 0, j = 0;
           // 新建一个数组, 大小为模式串的大小;
           int next[needle.size()];
           
           // 2.  调用 生成 next 数组的函数;
           getNext(next, needle);
           //3. 开始遍历 字符串;
          for(int i = 0; i < haystack.size(); i++){

              //  当两个指针对应位置上元素不匹配时, 模式串指针回退;
              while ( j > 0 && haystack[i] != needle[j])  j = next[j - 1];

              // 当两个指针对应位置上元素相同时,  移动模式串指针;
              if(  haystack[i] == needle[j] )  j++;
              

              //  当模式串指针, 到达模式串的结尾时,  返回字符串中的匹配起始位置;
              if( j == needle.size())  return (i - needle.size() + 1);
          }
           
          // 如果之前没有返回, 表明模式串没有走到最后,匹配没有完成;
           return  -1; 

       }
  
};

2. 字符串中的重复子串判断

lc 459 重复的子字符串

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。

示例 2:
输入: “aba”
输出: False

2.1 问题分析

  1. 直接对字符串自身, 构造next 数组(next数组长度 = 字符串长度);
    如果该 next 数组的 最后一个位置上的 数值 不为0, 表明该 字符串中 存在最大相同前后缀;

    最大相同前后缀的长度 = 数组最后一个位置上的数值 = ;

该最大相同前后缀代表的是: 字符串中减掉第一个周期重复串后的长度;
所以 第一个周期的重复串长度 = 字符串长度 - 最大相同前后缀;

  1. 如果最大相同前后缀长度不为0 , 并且 字符串 能够被 第一周期重复串整除,表明该字符串可以由 重复串 构成;

2.2 code

class Solution {
public:
 


        void getNext(int* next, const string& p) {
        // i, 后缀串的起点指针,方向,单向后移;   j 前缀穿的终止指针, 方向是双向的;
        int j = 0;
        next[0] = 0;

        // 开始遍历模式串,生成next 数组;
        for (int i = 1; i < p.size(); i++) {
            // 循环, 当两指针对应位置上元素不同时,  前缀串指针 回退;
            while (p[i] != p[j] && j > 0) j = next[j - 1];

            // 条件, 如果两个指针对应位置上元素相同时,  前缀穿指针后移;
            if (p[i] == p[j]) j++;

            //  将 j 的位置赋值给 next[i]
            next[i] = j;
        }
    }

   bool repeatedSubstringPattern(string s) {
            // 准备next 数组
            int next[s.size()];
            getNext(next, s);

            //  最大相同前缀后缀长度 =   next 数组中最后一个位置上数值;
            //  第一个重复周期的子串长度 =   字符串长度 - 最大相同前后缀长度;
            int maxLR = next[s.size() - 1];
            int period1 = s.size() - maxLR;

            // 如果最大相同前后缀长度不为0 ,且字符串长度能够被周期串长度 整除, 表明该字符串可以有 周期串 构成;
        if ( maxLR != 0 &&  s.size()% period1 == 0)  return  true;
        else return false;

    }

        

};


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值