• 【算法练习Day8】 kmp算法&&找出字符串中第一个匹配项的下标&&反转字符串中的单词&&重复的子字符串


    、​在这里插入图片描述

    ​📝个人主页:@Sherry的成长之路
    🏠学习社区:Sherry的成长之路(个人社区)
    📖专栏链接:练题
    🎯长路漫漫浩浩,万事皆有期待

    kmp算法

    kmp算法是一种快速查找字符串的算法,主要用途是在一个字符串里查找是否包含另一个字符串,kmp在理解上不是很友好,首先我们要搞清楚什么是前后缀、最长相等前后缀、前缀表、next数组如何实现前缀表,最后就是kmp算法借助前缀表的实现。

    前后缀的概念

    前缀就是包含第一个字符,而不包含最后一个字符的所有字串都可以被称为前缀。

    后缀是包含最后一个字符,且不包含第一个字符的所有字串都可以被称为后缀。

    举个例子:hello 前缀可以是h,he,hel,hell 后缀可以是:e,el,ell,ello

    最长相等前后缀

    最长相等前后缀顾名思义,就是前后缀相等的情况下,我们取最长的那一段。

    前缀表,next数组如何实现?

    前缀表十分重要,它是KMP算法的核心思想,先说它的作用,它的作用是实现匹配字符串时,如果遇到主串(用于对比的字符串)和模式串(拿来做对比的字符串)某个字符不相等时,将指针返回到上一个最长相等前后缀处再进行对比,这样做的好处是可以使字符不相等时退回某处,而不至于像暴力求解一样一遇到不相等就要重头对比,在主串有一定规律时,此方法显得十分方便。

    下面我们再来说一说实现的方法:
    定义一个数组名字叫next数组,网上大体有三种方法,第一种就是数组正常存储返回地址,第二种数组存储返回地址-1,第三种将返回地址整体向左移动。

    我们说到next数组就是一个前缀表,它来存我们字符串里字符匹配失败时,应该返回的位置用第二种方法实际上是返回到最长相等前缀的位置

    void getNext(int* next, const string& s){
        int j = -1;
        next[0] = j;
        for(int i = 1; i < s.size(); i++) { // 注意i从1开始
            while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
                j = next[j]; // 向前回退
            }
            if (s[i] == s[j + 1]) { // 找到相同的前后缀
                j++;
            }
            next[i] = j; // 将j(前缀的长度)赋给next[i]
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这一段就是c++的前缀表代码段,我们用j来表示前缀的最长长度为多少,i来向后遍历,当我们这个字符串的两个位置不相等时,判断j是否大于0,因为要涉及到j-1,只要两个位置不相等就使j一直回退,如果两位置相等,那么j向前走一步,意为最长长度加1,然后在数组每一个位置,记录当前匹配失败时,回退到那个位置。

    关于kmp算法的使用我们来看具体的例题。

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

    28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

    这道题就是典型可以应用kmp算法的题型

    class Solution {
    public:
        void getNext(int* next, const string& s) {
            int j = -1;
            next[0] = j;
            for(int i = 1; i < s.size(); i++) 
            { // 注意i从1开始
                while (j >= 0 && s[i] != s[j + 1]) 
                { // 前后缀不相同了
                    j = next[j]; // 向前回退
                }
                if (s[i] == s[j + 1]) 
                { // 找到相同的前后缀
                    j++;
                }
                next[i] = j; // 将j(前缀的长度)赋给next[i]
            }
        }
        int strStr(string haystack, string needle) 
        {
            if (needle.size() == 0) 
            {
                return 0;
            }
            int next[needle.size()];
            getNext(next, needle);
            int j = -1; // // 因为next数组里记录的起始位置为-1
            for (int i = 0; i < haystack.size(); i++) 
            { // 注意i就从0开始
                while(j >= 0 && haystack[i] != needle[j + 1]) 
                { // 不匹配
                    j = next[j]; // j 寻找之前匹配的位置
                }
                if (haystack[i] == needle[j + 1]) 
                { // 匹配,j和i同时向后移动
                    j++; // i的增加在for循环里
                }
                if (j == (needle.size() - 1) ) 
                { // 文本串s里出现了模式串t
                    return (i - needle.size() + 1);
                }
            }
            return -1;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    第一部分的代码就是上面所说的前缀和,后面的是用循环来遍历,也就是应用前缀和数组next,遍历查找,如果两字符串某个字符不相等,那么就使j一直回退,相等时候j++,当j++到模式串的最后一个字符了,则说明该字符串在主串中,由于字符不相同时,只有j会回退,i仍然向前走,所以当i走完全部,j还没有到到最后一个字符,说明该字符串不存在于主串内。

    重复的子字符串

    459. 重复的子字符串 - 力扣(LeetCode)

    移动匹配

    判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。

    class Solution {
    public:
        bool repeatedSubstringPattern(string s) {
            string t = s + s;
            t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
            if (t.find(s) != std::string::npos) return true; // r
            return false;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    kmp算法

    这道题刚看的时候,不太像使用kmp算法的题目,但是可以用kmp算法,其中是有一些技巧的:字符串的相等前后缀,错开的那一部分则为重复子串,用该重复子串重复n次,则可以取得原字符串。这个结论可以模拟一下。用next数组模拟,数组中最后一位存取的就是最长前后缀的长度,用字符串总长度,对总长度减去最长前后缀的长度,再取模,如果能整除,则该字符串是由前x个字符由n次变换得到,若不能整除则说明字符串不是由重复字串得到的。这里使用了前缀表统一减一的实现方式:

    class Solution {
    public:
        void getNext (int* next, const string& s)
        {
            next[0] = -1;
            int j = -1;
            for(int i = 1;i < s.size(); i++){
                while(j >= 0 && s[i] != s[j + 1]) 
                {
                    j = next[j];
                }
                if(s[i] == s[j + 1]) 
                {
                    j++;
                }
                next[i] = j;
            }
        }
        bool repeatedSubstringPattern (string s) {
            if (s.size() == 0) {
                return false;
            }
            int next[s.size()];
            getNext(next, s);
            int len = s.size();
            if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) 
            {
                return true;
            }
            return false;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    总结:

    今天我们进行了字符串的复习和双指针的回顾,还有kmp算法的应用,kmp算法还是挺有难度的,相关的思想需要多复习回顾。接下来,我们继续进行算法练习·。希望我的文章和讲解能对大家的学习提供一些帮助。

    当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

    在这里插入图片描述

  • 相关阅读:
    二叉树层序遍历算法
    【基于FreeRTOS的STM32F103系统】Heap_4内存管理机制程序详解
    Java自学的话怎么样最有效果?
    SQL优化
    新唐(nuvoton)MCU软件开发指南—环境搭建设置
    ABB 5SHY3545L0010可控硅模块
    简单聊聊 倒排索引
    SpringBoot保姆级教程(七)Thymeleaf
    MCU内存基础知识
    Nginx+Tomcat负载均衡、动静分离集群
  • 原文地址:https://blog.csdn.net/m0_73258399/article/details/133352786