【C++代码】找出字符串中第一个匹配项的下标,重复的子字符串--代码随想录

题目:找出字符串中第一个匹配项的下标

  • 给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1
题解
  • 本题是KMP 经典题目。KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配

  • KMP主要应用在字符串匹配上。主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。next数组就是一个前缀表(prefix table)。

  • 前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配

  • 为了清楚的了解前缀表的来历,可以举一个例子:要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

  • 如果暴力匹配,发现不匹配,此时就要从头匹配了。但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

  • 首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

  • 字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

  • 所以字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2。 等等…。

  • 这就是前缀表,那为啥就能告诉我们 上次匹配的位置,并跳过去呢?回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:

    • 【C++代码】找出字符串中第一个匹配项的下标,重复的子字符串--代码随想录_第1张图片

    • 然后就找到了下标2,指向b,继续匹配:如图:

    • 【C++代码】找出字符串中第一个匹配项的下标,重复的子字符串--代码随想录_第2张图片

    • 下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

  • 接下来就要说一说怎么计算前缀表。长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串)。长度为前2个字符的子串aa,最长相同前后缀的长度为1。长度为前3个字符的子串aab,最长相同前后缀的长度为0。以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:

    • 【C++代码】找出字符串中第一个匹配项的下标,重复的子字符串--代码随想录_第3张图片

    • 可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

  • 再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

    • 找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的 前缀表的数值。前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。最后就在文本串中找到了和模式串匹配的子串了。

  • 很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)

  • 使用next数组来匹配,有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。这仅仅是KMP算法实现上的问题,如果就直接使用前缀表可以换一种回退方式,找j=next[j-1] 来进行回退。主要就是j=next[x]这一步最为关键!getNext(int* next, const string& s)代码如下:

    • void getNext(int* next, const string& s) {
          int j = 0;
          next[0] = 0;
          for(int i = 1; i < s.size(); i++) {
              while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
                  j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
              }
              if (s[i] == s[j]) {
                  j++;
              }
              next[i] = j;
          }
      }
      
    • 此时如果输入的模式串为aabaaf,对应的next为 0 1 0 1 2 0,(其实这就是前缀表的数值了)。对应的KMP算法实现代码如下:

    • class Solution {
      public:
          void getNext(int* next, const string& s) {
              int j = 0;
              next[0] = 0;
              for(int i = 1; i < s.size(); i++) {
                  while (j > 0 && s[i] != s[j]) {
                      j = next[j - 1];
                  }
                  if (s[i] == s[j]) {
                      j++;
                  }
                  next[i] = j;
              }
          }
          int strStr(string haystack, string needle) {
              if (needle.size() == 0) {
                  return 0;
              }
              int next[needle.size()];
              getNext(next, needle);
              int j = 0;
              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;
          }
      };
      
  • 时间复杂度:O(n+m),其中 n 是字符串 haystack 的长度,m 是字符串 needle 的长度。我们至多需要遍历两字符串一次。空间复杂度:O(m),其中 m 是字符串 needle 的长度。我们只需要保存字符串 needle 的前缀函数。

题目:重复的子字符串

  • 给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
题解
  • 暴力的解法, 就是一个for循环获取 子串的终止位置, 然后判断子串是否能重复构成字符串,又嵌套一个for循环,所以是 O ( n 2 ) O(n^2) O(n2) 的时间复杂度。

  • 其实我们只需要判断,以第一个字母为开始的子串就可以,所以一个for循环获取子串的终止位置就行了。 而且遍历的时候 都不用遍历结束,只需要遍历到中间位置,因为子串结束位置大于中间位置的话,一定不能重复组成字符串

  • 移动匹配:当一个字符串s:abcabc,内部由重复的子串组成。那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s。所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。

  • 当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。

  • class Solution {
    public:
        bool repeatedSubstringPattern(string s) {
            string temp_s = s+s;
            temp_s.erase(temp_s.begin());
            temp_s.erase(temp_s.end()-1);
            if(temp_s.find(s)!=std::string::npos)
                return true;
            return false;
        }
    };
    
  • 时间复杂度: O(n),空间复杂度: O(1)。不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。

  • 这道题也可以用KMP算法解:KMP 算法虽然有着良好的理论时间复杂度上限,但大部分语言自带的字符串查找函数并不是用 KMP 算法实现的。这是因为在实现 API 时,我们需要在平均时间复杂度和最坏时间复杂度二者之间权衡。普通的暴力匹配算法以及优化的 BM 算法不用找了,学习BM算法,这篇就够了(思路+详注代码)_BoCong-Deng的博客-CSDN博客拥有比 KMP 算法更为优秀的平均时间复杂度;各种文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法。Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。字符串匹配的Boyer-Moore算法 - 阮一峰的网络日志 (ruanyifeng.com)

  • BM算法定义了两个规则:坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1。好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1

    • 假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。首先,"字符串"与"搜索词"头部对齐,从尾部开始比较。"S"与"E"不匹配。这时,“S"就被称为"坏字符”(bad character),即不匹配的字符。我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位。

    • 【C++代码】找出字符串中第一个匹配项的下标,重复的子字符串--代码随想录_第4张图片

    • 依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,“P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。我们由此总结出"坏字符规则”:后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置。如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。以"P"为例,它作为"坏字符",出现在搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移 6 - 4 = 2位。再以前面第二步的"S"为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个搜索词后移 6 - (-1) = 7位。依然从尾部开始比较,"E"与"E"匹配。比较前面一位,"LE"与"LE"匹配。比较前面一位,"PLE"与"PLE"匹配。比较前面一位,"MPLE"与"MPLE"匹配。我们把这种情况称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,“MPLE”、“PLE”、“LE”、"E"都是好后缀。比较前一位,发现"I"与"A"不匹配。所以,“I"是"坏字符”。

    • 根据"坏字符规则",此时搜索词应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?我们知道,此时存在"好后缀"。所以,可以采用"好后缀规则":后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置。举例来说,如果字符串"ABCDAB"的后一个"AB"是"好后缀"。那么它的位置是5(从0开始计算,取最后的"B"的值),在"搜索词中的上一次出现位置"是1(第一个"B"的位置),所以后移 5 - 1 = 4位,前一个"AB"移到后一个"AB"的位置。再举一个例子,如果字符串"ABCDEF"的"EF"是好后缀,则"EF"的位置是5 ,上一次出现的位置是 -1(即未出现),所以后移 5 - (-1) = 6位,即整个字符串移到"F"的后一位。

    • 这个规则有三个注意点:

      • (1)"好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。

      • (2)如果"好后缀"在搜索词中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。

      • (3)如果"好后缀"有多个,则除了最长的那个"好后缀",其他"好后缀"的上一次出现位置必须在头部。比如,假定"BABCDAB"的"好后缀"是"DAB"、“AB”、“B”,请问这时"好后缀"的上一次出现位置是什么?回答是,此时采用的好后缀是"B",它的上一次出现位置是头部,即第0位。这个规则也可以这样表达:如果最长的那个"好后缀"只出现一次,则可以把搜索词改写成如下形式进行位置计算"(DA)BABCDAB",即虚拟加入最前面的"DA"。

    • 回到上文的这个例子。此时,所有的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位。

    • 可以看到,“坏字符规则"只能移3位,“好后缀规则"可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。继续从尾部开始比较,“P"与"E"不匹配,因此"P"是"坏字符”。根据"坏字符规则”,后移 6 - 4 = 2位。从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据"好后缀规则”,后移 6 - 0 = 6位,即头部的"E"移到尾部的"E"的位置。

  • 字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定。在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束

  • 那么vector< char > 和 string 又有什么区别呢?其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。所以想处理字符串,我们还是会定义一个string类型。

你可能感兴趣的:(啃书《C++Primer5,c++,开发语言)