LeetCode刷题(ACM模式)-04字符串

news2024/11/19 23:33:18

参考引用:代码随想录

  • 注:每道 LeetCode 题目都使用 ACM 代码模式,可直接在本地运行,蓝色字体为题目超链接

1. 反转字符串

344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

  • 示例 1
    输入:s = [“h”,“e”,“l”,“l”,“o”]
    输出:[“o”,“l”,“l”,“e”,“h”]
  • 示例 2
    输入:s = [“H”,“a”,“n”,“n”,“a”,“h”]
    输出:[“h”,“a”,“n”,“n”,“a”,“H”]
  • 提示
    1 <= s.length <= 10^5
    s[i] 都是 ASCII 码表中的可打印字符

1.1 思路

  • 对于这道题目可以直接用 C++ 里的一个库函数 reverse。如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数;如果库函数仅仅是解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数
  • 在反转链表中,使用了双指针的方法。那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的
  • 对于字符串,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素,动画如下
    在这里插入图片描述

1.2 代码实现

// 时间复杂度: O(n)
// 空间复杂度: O(1)
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    // 使用双指针法来实现反转操作
        // 双指针分别指向字符串的首尾位置
        // 通过循环,交换指针所指向的字符,并同时向中间移动,直到达到中间位置
    void reverseString(vector<char> &s) {
        for (int i = 0, j = s.size() - 1; i < s.size() / 2; ++i, --j) {
            swap(s[i], s[j]);  // 使用 swap 库函数实现交换
        }
    }
};

int main(int argc, char *argv[]){
    vector<char> s {'h', 'e', 'l', 'l', 'o'};
    Solution solution;
    solution.reverseString(s);
    for (char c : s) {
        cout << c << " ";
    }
    cout << endl;

    return 0;
}

2. 反转字符串 II

541. 反转字符串II
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
1、如果剩余字符少于 k 个,则将剩余字符全部反转。
2、如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样

  • 示例 1
    输入:s = “abcdefg”, k = 2
    输出:“bacdfeg”
  • 示例 2
    输入:s = “abcd”, k = 2
    输出:“bacd”
  • 提示
    1 <= s.length <= 10^4
    s 仅由小写英文组成
    1 <= k <= 10^4

2.1 思路

  • 在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否有需要反转的区间。因为要找的也就是每 2 * k 区间的起点,这样写的程序会高效很多

2.2 代码实现

// 时间复杂度: O(n)
// 空间复杂度: O(1)
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    void reverse(string &s, int start, int end) {
        for (int i = start, j = end; i < j; ++i, --j) {
            swap(s[i], s[j]);
        }
    }
    string reverseStr(string s, int k) {
        for (int i = 0; i < s.size(); i += (2 * k)) {
            // 1. 每隔 2k 个字符的前 k 个字符进行反转
            // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
            if (i + k <= s.size()) {
                reverse(s, i, i + k - 1);
                continue;
            }
            // 3. 剩余字符少于 k 个,则将剩余字符全部反转
            reverse(s, i, s.size() - 1);
        }
        return s;
    }
};

int main(int argc, char *argv[]){
    string s = "abcdefg";
    int k = 2;
    
    Solution solution;
    cout << solution.reverseStr(s, k) << endl;

    return 0;
}

3. 替换空格

剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"

  • 示例 1
    输入:s = “We are happy.”
    输出:“We%20are%20happy.”
  • 提示
    0 <= s 的长度 <= 10000

3.1 思路

  • 首先扩充数组到每个空格替换成 “%20” 之后的大小。然后从后向前替换空格,也就是双指针法,动画如下(i 指向新长度的末尾,j 指向旧长度的末尾
    在这里插入图片描述
为什么要从后向前填充?
  • 从前向后填充时间复杂度就是 O ( n 2 ) O(n^2) O(n2),因为每次添加元素都要将添加元素之后的所有元素向后移动
  • 其实很多数组填充类的问题,都可以先给数组扩容带填充后的大小,然后在从后向前进行操作,这么做有两个好处:
    • 不用申请新数组
    • 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题

3.2 代码实现(双指针法)

// 时间复杂度:O(n)
// 空间复杂度:O(1)
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    string replaceSpace(string s) {
        int count = 0; // 记录字符串 s 中空格的数量
        int sOldSize = s.size(); // 记录原始字符串 s 的长度
        // 遍历字符串 s 中的每个字符,检查是否为 ' '(空格)
        for (int i = 0; i < s.size(); ++i) {
            if (s[i] == ' ') {
                count++;
            }
        }
        // 扩充字符串 s 的大小,也就是每个空格替换成 "%20" 之后的大小
        s.resize(s.size() + count * 2);
        int sNewSize = s.size(); // 更新字符串 s 的新大小
        // 从字符串末尾开始,逐个复制原始字符串 s 的字符到新字符串 s 中
        for (int i = sNewSize - 1, j = sOldSize - 1; j < i; --i, --j) {
            if (s[j] != ' ') { // 如果当前字符不是空格,则直接将其复制到新字符串 s 中
                s[i] = s[j];
            } else { // 如果当前字符是空格,则用字符串 %20 替换空格
                s[i] = '0';
                s[i - 1] = '2';
                s[i - 2] = '%';
                i -= 2; // 将 i 左移两个位置,以便继续插入下一个 %20 字符串
            }
        }
        return s;
    }
};

int main(int argc, char *argv[]) {
    string s = "We are happy.";

    Solution solution;
    cout << solution.replaceSpace(s) << endl;
    
    return 0;
}

3.3 字符串和数组对比

  • 字符串是若干字符组成的有限序列,也可以理解为是一个字符数组。在 C 语言中,把一个字符串存入一个数组时,也把结束符 ‘\0’ 存入数组,并以此作为该字符串是否结束的标志,例如这段代码
    char a[5] = "asd";
    for (int i = 0; a[i] != '\0'; i++) {
    }
    
  • 在 C++ 中,提供一个 string 类,string 类会提供 size 接口,可以用来判断 string 类字符串是否结束,就不用 ‘\0’ 来判断是否结束,例如这段代码
    string a = "asd";
    for (int i = 0; i < a.size(); i++) {
    }
    
  • 那么 vector<char> 和 string 又有什么区别呢?
    • 其实在基本操作上没有区别,但是 string 提供更多的字符串处理的相关接口,例如 string 重载了 +,而 vector 却没有,所以想处理字符串,还是会定义一个 string 类型

4. 翻转字符串里的单词

151. 反转字符串中的单词
给你一个字符串 s ,请你反转字符串中单词的顺序。单词是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的单词分隔开。返回单词顺序颠倒且单词之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

  • 示例 1
    输入:s = “the sky is blue”
    输出:“blue is sky the”
  • 示例 2
    输入:s = " hello world "
    输出:“world hello”
    解释:反转后的字符串中不能存在前导空格和尾随空格
  • 示例 3
    输入:s = “a good example”
    输出:“example good a”
    解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个
  • 提示
    1 <= s.length <= 10^4
    s 包含英文大小写字母、数字和空格 ’ ’
    s 中 至少存在一个单词

4.1 思路

  • 解题思路:先整体反转再局部反转。举个例子,源字符串为:"the sky is blue "
    • 1、移除多余空格
      • 移除多余空格 : “the sky is blue”
    • 2、将整个字符串反转
      • 字符串反转:“eulb si yks eht”
    • 3、将每个单词反转
      • 单词反转:“blue is sky the”

4.2 代码实现

// 时间复杂度: O(n)
// 空间复杂度: O(1)
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    // 同 反转字符串
    void reverse(string &s, int start, int end) {
        for (int i = start, j = end; i < j; ++i, --j) {
            swap(s[i], s[j]);
        }
    }
    // 快慢指针法:去除所有空格,并在相邻单词之间添加空格
    void removeExtraSpaces(string &s) {
        int slow = 0; // 整体思想参考 27.移除元素
        for (int i = 0; i < s.size(); ++i) {
            if (s[i] != ' ') { // 遇到非空格就处理,即删除所有空格
                // 手动控制空格,给单词之间添加空格
                // slow != 0 说明不是第一个单词,需要在单词前添加空格
                if (slow != 0) {
                    s[slow] = ' ';
                    slow++;
                }
                // 补上该单词,遇到空格说明单词结束
                while (i < s.size() && s[i] != ' ') {
                    s[slow++] = s[i++];
                }
            }
        }
        s.resize(slow); // slow 的大小即为去除多余空格后的大小
    }
    string reverseWords(string s) {
        // 去除多余的空格,保证单词之间之只有一个空格,且字符串首尾没空格
        removeExtraSpaces(s); 
        reverse(s, 0, s.size() - 1); // 对整个字符串进行翻转
        // 去除多余的空格后保证第一个单词的开始下标一定是 0
        int start = 0;
        for (int i = 0; i <= s.size(); ++i) {
            // 遍历字符串 s,当遇到空格或者遍历到字符串末尾时,说明一个单词结束
            // 下一步进行翻转:从 start 到当前位置(不包括当前位置)的字符翻转
            // 并更新 start 为下一个单词的起始位置
            if (i == s.size() || s[i] == ' ') {
                reverse(s, start, i - 1);
                start = i + 1;
            }
        }
        return s;
    }
};

int main(int argc, char *argv[]) {
    Solution solution;
    string s = "a good  example";
    cout << solution.reverseWords(s) << endl;
    
    return 0;
}

5. 左旋转字符串

剑指 Offer 58 - II. 左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串 “abcdefg” 和数字 2,该函数将返回左旋转两位得到的结果 “cdefgab”

  • 示例 1
    输入: s = “abcdefg”, k = 2
    输出: “cdefgab”
  • 示例 2
    输入: s = “lrloseumgh”, k = 6
    输出: “umghlrlose”
  • 提示
    1 <= k < s.length <= 10000

5.1 思路

  • 先局部反转再整体反转,具体步骤为
    • 反转区间为前 n 的子串
    • 反转区间为 n 到末尾的子串
    • 反转整个字符串

在这里插入图片描述

5.2 代码实现

// 时间复杂度: O(n)
// 空间复杂度:O(1)
#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        reverse(s.begin(), s.begin() + n);
        reverse(s.begin() + n, s.end());
        reverse(s.begin(), s.end());
        return s;
    }
};

int main(int argc, char *argv[]) {
    string s = "abcdefg";
    int n = 2;

    Solution solution;
    string result = solution.reverseLeftWords(s, n);
    cout << result << endl;
    
    return 0;
}

6. 实现 strStr()

28. 找出字符串中第一个匹配项的下标
实现 strStr() 函数:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

  • 示例 1
    输入:haystack = “sadbutsad”, needle = “sad”
    输出:0
    解释:“sad” 在下标 0 和 6 处匹配
    第一个匹配项的下标是 0 ,所以返回 0
  • 示例 2
    输入:haystack = “leetcode”, needle = “leeto”
    输出:-1
    解释:“leeto” 没有在 “leetcode” 中出现,所以返回 -1
  • 提示
    1 <= haystack.length, needle.length <= 10^4
    haystack 和 needle 仅由小写英文字符组成
    当 needle 是空字符串时应当返回 0
    本题是 KMP 经典题目

6.1 思路

6.1.1 什么是 KMP
  • 因为是由这三位学者发明的:Knuth,Morris 和 Pratt,所以取了三位学者名字的首字母。所以叫做 KMP
6.1.2 KMP 有什么用
  • KMP 主要应用在字符串匹配上
    • KMP 的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,再利用这些信息避免从头再去做匹配。所以如何记录已经匹配的文本内容,是 KMP 的重点,也是 next 数组的重任
6.1.3 前缀表
  • next 数组就是一个前缀表(prefix table)。前缀表有什么作用呢?

    • 前缀表是用来回退的,它记录了模式串与文本串不匹配时,模式串应该从哪里重新开始匹配
  • 要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf,动画如下

    • 可以看出,文本串中第六个字符 b 和模式串的第六个字符 f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符 b 继续开始匹配
  • 前缀表是如何记录的呢?

    • 首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置
  • 什么是前缀表?

    • 记录下标 i 之前(包括 i)的字符串中,有多大长度的相同前缀、后缀
      在这里插入图片描述
6.1.4 最长相等前后缀
  • 前缀
    • 不包含最后一个字符的所有以第一个字符开头的连续子串
  • 后缀
    • 不包含第一个字符的所有以最后一个字符结尾的连续子串

前缀表要求的就是相同前后缀的长度

  • 字符串 a 的最长相等前后缀长度为 0
  • 字符串 aa 的最长相等前后缀长度为 1
  • 字符串 aaa 的最长相等前后缀长度为 2
6.1.5 为什么一定要用前缀表
  • 刚刚匹配的过程在下标 5 的地方遇到不匹配,模式串是指向 f

在这里插入图片描述

  • 然后就找到了下标 2,指向 b,继续匹配
    在这里插入图片描述

  • 下标 5 之前这部分的字符串(也就是字符串 aabaa)的最长相等的前缀和后缀字符串是子字符串 aa,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么找到与其相同的前缀的后面重新匹配就可以了

    所以前缀表具有告知当前位置匹配失败,跳到之前已经匹配过的地方的能力

6.1.6 如何计算前缀表
  • 长度为前 1 个字符的子串 a,最长相同前后缀的长度为 0

在这里插入图片描述

  • 长度为前 2 个字符的子串 aa,最长相同前后缀的长度为 1

在这里插入图片描述

  • 长度为前 3 个字符的子串 aab,最长相同前后缀的长度为 0

在这里插入图片描述

  • 以此类推
    • 长度为前 4 个字符的子串 aaba,最长相同前后缀的长度为 1
    • 长度为前 5 个字符的子串 aabaa,最长相同前后缀的长度为 2
    • 长度为前 6 个字符的子串 aabaaf,最长相同前后缀的长度为 0
  • 那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图

在这里插入图片描述

  • 可以看出模式串与前缀表对应位置的数字表示的就是:下标 i 之前(包括 i)的字符串中,有多大长度的相同前缀后缀
  • 如何利用前缀表找到当字符不匹配的时候应该指针应该移动的位置?动画如下
    在这里插入图片描述

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

6.1.7 前缀表与 next 数组
  • 很多 KMP 算法都是使用 next 数组来做回退操作,那么 next 数组与前缀表有什么关系呢?
    • next 数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为 -1)
6.1.8 使用 next 数组来匹配
  • 以前缀表统一减一之后的 next 数组来做演示。有了 next 数组,就可以根据 next 数组来匹配文本串 s 和模式串 t。注意 next 数组是新前缀表(旧前缀表统一减一了)。匹配过程动画如下
    在这里插入图片描述
6.1.9 时间复杂度分析
  • 其中 n 为文本串长度,m 为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是 O(n),之前还要单独生成 next 数组,时间复杂度是 O(m)。所以整个KMP 算法的时间复杂度是 O(n+m) 的。暴力的解法显而易见是 O(n × m),所以 KMP 在字符串匹配中极大地提高了搜索的效率

6.2 代码实现(前缀表统一减一)

  • 代码构造 next 数组的逻辑流程动画如下(对应 getNext 函数)
    在这里插入图片描述
// 时间复杂度: O(n + m)
// 空间复杂度: O(m)
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    // 定义一个函数 getNext 来构造 next 数组
    void getNext(int *next, const string &s) {
        // 1. 初始化:定义两个指针 i 和 j,j 指向前缀末尾,i 指向后缀末尾
        int j = -1; // 该实现中前缀表要统一减一的操作
        // next[i] 表示 i(包括 i)之前最长相等的前后缀长度(其实就是 j)
        // 所以初始化 next[0] = j
        next[0] = j; 
        // 2. 处理前后缀不相同的情况
            // 因为 j 初始化为 -1,比较 s[i] 与 s[j+1]
            // i=0 时,它左边没有字符,无法与其他字符形成前后缀
            // 所以不能计算它的 next 值,故 i 从 1 开始
        for (int i = 1; i < s.size(); ++i) {
            while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同
                j = next[j]; // 向前回退
            }
            // 3. 处理前后缀相同的情况
            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);
        // 定义两个下标:j 指向模式串起始位置,i 指向文本串起始位置
        int j = -1; // 因为 next 数组里记录的起始位置为 -1
        for (int i = 0; i < haystack.size(); ++i) { // i 就从 0 开始
            // 因为 j 从 -1 开始的,所以此处是和 j + 1 进行比较
            while (j >= 0 && haystack[i] != needle[j + 1]) { 
                j = next[j]; // j 寻找之前匹配的位置
            }
            // 匹配,j 和 i 同时向后移动
            if (haystack[i] == needle[j + 1]) {
                ++j; // i 的自增在 for 循环中
            }
            // j 指向了模式串的末尾,则说明文本串里出现了模式串
            if (j == (needle.size() - 1)) {
                // 本题要在文本串中找出模式串出现的第一个位置 (从 0 开始)
                // 所以返回当前在 文本串 匹配 模式串 的位置 i 减去模式串的长度
                // 就是文本串中出现模式串的第一个位置
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};

int main(int argc, char *argv[]) {
    string haystack = "aabaabaafa";
    string needle = "aabaaf";

    Solution solution;
    cout << solution.strStr(haystack, needle) << endl;

    return 0;
}

6.3 代码实现(前缀表不减一)

// 时间复杂度: O(n + m)
// 空间复杂度: O(m)
#include <iostream>
#include <string>

using namespace std;

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;
    }
};

int main(int argc, char *argv[]) {
    string haystack = "aabaabaafa";
    string needle = "aabaaf";

    Solution solution;
    cout << solution.strStr(haystack, needle) << endl;

    return 0;
}

7. 重复的子字符串

459. 重复的子字符串
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成

  • 示例 1
    输入: s = “abab”
    输出: true
    解释: 可由子串 “ab” 重复两次构成
  • 示例 2
    输入: s = “aba”
    输出: false
  • 示例 3
    输入: s = “abcabcabcabc”
    输出: true
    解释: 可由子串 “abc” 重复四次构成。 (或子串 “abcabc” 重复两次构成)
  • 提示
    1 <= s.length <= 10^4
    s 由小写英文字母组成

7.1 思路

7.1.2 移动匹配法
  • 当一个字符串 s:abcabc,内部由重复的子串组成,那该字符串的结构一定是由前后相同的子串组成

在这里插入图片描述

  • 那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个 s,如图

在这里插入图片描述

  • 所以判断字符串 s 是否由重复子串组成,只要两个 s 拼接在一起,里面还出现一个 s 的话,就说明是由重复子串组成
    • 当然,在判断 s + s 拼接的字符串里是否出现一个 s 的的时候,要剔除 s + s 的首字符和尾字符,这样避免在 s + s 中搜索出原来的 s,要搜索的是中间拼接出来的 s
7.1.3 KMP 法
  • 在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串 s:abababab 来举例,ab 就是最小重复单位,如图所示

在这里插入图片描述

  • 如何找到最小重复子串
    • 步骤一:因为这是相等的前缀和后缀,t[0] 与 k[0] 相同, t[1] 与 k[1] 相同,所以 s[0] 一定和 s[2] 相同,s[1] 一定和 s[3] 相同,即:s[0]s[1] 与 s[2]s[3] 相同
    • 步骤二:因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同
    • 步骤三:因为这是相等的前缀和后缀,t[2] 与 k[2] 相同 ,t[3] 与 k[3] 相同,所以,s[2] 一定和 s[4] 相同,s[3] 一定和 s[5] 相同,即:s[2]s[3] 与 s[4]s[5] 相同
    • 步骤四:循环往复
    • 所以字符串 s,s[0]s[1] 与 s[2]s[3] 相同,s[2]s[3] 与 s[4]s[5] 相同,s[4]s[5] 与 s[6]s[7] 相同

在这里插入图片描述

7.2 代码实现(移动匹配法)

// 时间复杂度: O(n)
// 空间复杂度: O(1)
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        // 将原始字符串 s 拼接在其后面一次,这样 t 中包含了两个 s
        string t = s + s;
        // 从 t 的开头和结尾分别删除了一个字符
        // 这是为了去除 t 中包含的重复部分,也就是第一个 s
        t.erase(t.begin());
        t.erase(t.end() - 1);
        // 在 t 中查找第一个出现的 s,如果找到了
        // 则说明原始字符串 s 是由重复的子串构成的
        // std::string::npos 是一个特殊的值,表示在字符串中没有找到匹配项
        if (t.find(s) != std::string::npos) {
            return true;
        }
        return false;
    }
};

int main(int argc, char *argv[]) {
    string s = "abcabcabcabc";

    Solution solution;
    // boolalpha 功能:把 bool 值显示为 true 或 false
    cout << boolalpha << solution.repeatedSubstringPattern(s) << endl;
    
    return 0;
}

7.3 代码实现(KMP 法)

  • 前缀表统一减一
// 时间复杂度: O(n)
// 空间复杂度: O(n)
#include <iostream>
#include <string>

using namespace std;

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();
        // 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀
        // 如果 len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可
        // 以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串
        if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) {
            return true;
        }
        return false;
    }
};

int main(int argc, char *argv[]) {
    string s = "abcabcabcabc";

    Solution solution;
    // boolalpha 功能:把 bool 值显示为 true 或 false
    cout << boolalpha << solution.repeatedSubstringPattern(s) << endl;
    
    return 0;
}
  • 前缀表不减一
// 时间复杂度: O(n)
// 空间复杂度: O(n)
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    void getNext (int* next, const string& s){
        next[0] = 0;
        int j = 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;
        }
    }
    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] != 0 && len % (len - (next[len - 1] )) == 0) {
            return true;
        }
        return false;
    }
};

int main(int argc, char *argv[]) {
    string s = "abcabcabcabc";

    Solution solution;
    // boolalpha 功能:把 bool 值显示为 true 或 false
    cout << boolalpha << solution.repeatedSubstringPattern(s) << endl;
    
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/603620.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PAT A1162 Postfix Expression

1162 Postfix Expression 分数 25 作者 陈越 单位 浙江大学 Given a syntax tree (binary), you are supposed to output the corresponding postfix expression, with parentheses reflecting the precedences of the operators. Input Specification: Each input file …

权限管理与Shiro入门

权限管理与Shiro入门 学习目标&#xff1a; 理解前端权限控制思路 理解有状态服务和无状态服务通过拦截器实现JWT鉴权 能够理解shiro以及shiro的认证和授权 前端权限控制 需求分析 需求说明 基于前后端分离的开发模式中&#xff0c;权限控制分为前端页面可见性权限与后…

操作系统复习3.1.1-内存非连续存储

基本分页存储管理 背景 固定分区带来的是内部内存碎片难以利用 而动态分配带来的外部内存碎片难以利用 而基本分页存储管理解决了这个问题&#xff0c;将一个进程分散地装入不同分区&#xff0c;避免了紧凑的处理 思想 将一个进程(43MB)装入内存&#xff0c;一个分页大小为…

【fluent】利用UDF和Scheme变量实现根据条件满足情况保存dat、case数据案例文件

一、问题背景 前一段时间在闲鱼上&#xff0c;遇到有一个人问我“在udf中如何实现某一个变量满足一定取值范围内才保存dat和case文件”。 而后我帮他解决了这个问题&#xff0c;在此处也将解决办法公益性地分享出来。 主要参考的是CFD online上的一篇讨论fluent udf, saving…

chatgpt赋能python:Python函数:介绍及应用

Python函数&#xff1a;介绍及应用 Python是一种功能强大的编程语言。函数是Python编程中最常用的组件之一。函数是用来执行特定的程序并返回结果的工具&#xff0c;可以在一个程序中调用多次。在本文中&#xff0c;我们将介绍Python函数的应用和使用方法。 Python函数的定义…

ant-design-vue将英文改为中文 DatePicker日期控件

ant-design设置DatePicker日期控件中文显示 ant-design-vue将英文改为中文 我们在使用 ant-design-vue 的时候 会遇到默认的语言是 英语 大部分我们需要转成为中文 这时候我们就需要进行配置 首先我们改单一组件的语言&#xff1a; <template><a-date-picker v-model:…

互联网医院牌照申请条件|互联网医院牌照申请流程

​ 随着互联网技术的迅速发展&#xff0c;互联网医院已成为医疗服务领域的重要组成部分。互联网医院是指通过互联网提供医疗服务的机构&#xff0c;其开展医疗业务需经过相关管理部门的批准&#xff0c;且必须持有互联网医院牌照。 申请条件 互联网医院牌照的申请条件如下&a…

互联网医院资质的申请条件和流程有哪些

​随着互联网的快速发展&#xff0c;互联网医疗逐渐成为了医疗行业的重要组成部分&#xff0c;互联网医院也因此出现了。互联网医院是指通过网络技术&#xff0c;开展远程医学服务、健康管理、医学教育等活动的医疗机构。 互联网医院牌照申请需要的资料和条件 要想成功申请互…

压缩感知重构之分段正交匹配追踪算法

算法的重构是压缩感知中重要的一步&#xff0c;是压缩感知的关键之处。因为重构算法关系着信号能否精确重建&#xff0c;国内外的研究学者致力于压缩感知的信号重建&#xff0c;并且取得了很大的进展&#xff0c;提出了很多的重构算法&#xff0c;每种算法都各有自己的优缺点&a…

Arthas-Class/Classloader相关命令使用

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 开头&#xff1a; 我们先说下生产使用频率较高的有哪些&#xff1a;dump、jad、mc、retransfo…

FMEA教程

1. 2. 3. 4. 5. PFMEA是“过程失效模式与后果分析”的英文“Process Failure Mode and Effect Analysis”的缩写。为了更好地促进AQP PFMEA软件的应用&#xff0c;我们将以系列文章分享运用AQP PFMEA软件有效开展PFMEA工作的理念和方法&#xff0c;同时系统性介绍AQP PFMEA软…

leetcode96--不同的二叉搜索树[java]

不同的二叉搜索树 leetcode 96 题 不同的二叉搜索树题目描述暴力递归解题思路代码演示执行效率 递归 缓存解题思路代码演示执行效率 动态规划专题 leetcode 96 题 不同的二叉搜索树 原题链接: 难度—中等 https://leetcode.cn/problems/unique-binary-search-trees/ 题目描述 …

chatgpt赋能python:分解gif:使用Python将动态图片拆分成单帧图片

分解gif&#xff1a;使用Python将动态图片拆分成单帧图片 随着互联网上图片的使用越来越普遍&#xff0c;动态图片也成为了大家经常使用的图片之一。GIF动态图作为一种常见的动态图片格式&#xff0c;有时需要将其拆分成单帧图片&#xff0c;以便于使用或修改其中的某一帧。本…

目标检测笔记(九):详细介绍并实现可视化深度学习中每层特征层的网络训练情况

文章目录 为什么要解析特征层如何可视化特征层可视化结果如何 ❤️ &#x1f9e1; &#x1f49b; &#x1f49a; &#x1f499; &#x1f49c; &#x1f5a4; &#x1f90d; &#x1f90e; &#x1f494; ❣️ &#x1f495; &#x1f49e; &#x1f493; &#x1f497; &#…

SpringCloud(三)

文章目录 Eureka注册中心Eureka的结构和作用搭建eureka-server创建eureka-server服务引入eureka依赖编写启动类编写配置文件启动服务 服务注册1&#xff09;引入依赖2&#xff09;配置文件3&#xff09;启动多个user-service实例 服务发现1&#xff09;引入依赖2&#xff09;配…

chatgpt赋能python:Python冒泡法排序:一种简单且高效的排序方法

Python 冒泡法排序&#xff1a;一种简单且高效的排序方法 在计算机科学中&#xff0c;排序算法是一种将给定数据集合重新排列为按照一定顺序排列的有序序列的方法。而冒泡排序算法是其中最简单、最基础的一种排序算法。 什么是冒泡排序&#xff1f; 冒泡排序&#xff0c;顾名…

代码随想录第49天

1.买卖股票的最佳时机&#xff1a; 贪心 因为股票就买卖一次&#xff0c;那么贪心的想法很自然就是取最左最小值&#xff0c;取最右最大值&#xff0c;那么得到的差值就是最大利润。 C代码如下&#xff1a; class Solution { public:int maxProfit(vector<int>& …

一键部署个人ChatGPT Web网站

一键部署个人ChatGPT Web网站 githubVercel使用自己的域名 本文将向大家介绍如何通过Github和Vercel这两个具,轻松搭建自己的ChatGPT Web网站&#xff0c;并且我们还可以添加密码保护以防止恶意滥用。 github 首先&#xff0c;我们需要拥有一个Github账号和Vercel账&#xff0…

【Set集合】概述和特点

Set集合概述和特点 Set集合概述 Set作为Collection集合的子接口&#xff0c;没有新增的功能&#xff0c;Collection集合可用的功能Set集合也可以用。 Set集合特点 Set集合存储的元素是无序的&#xff0c;而且不允许存储重复的元素&#xff0c;每当有新的元素存入的时候&#x…

操作系统复习3.2.1-虚拟内存

传统存储的问题 一次性&#xff1a;一次性装入作业才能运行 驻留性&#xff1a;不是所有部分都需要长时间存放在内存中 定义和特征 将要用的部分载入内存&#xff0c;不用的部分调出外存&#xff0c;逻辑上扩大内存 虚拟内存的最大容量为计算机的寻址范围决定 实际容量则为内…