LeetCode 字符串问题
@
28. 实现 strStr() KMP算法
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
-
算法思路:
- 遍历haystack每个起始位置,向后对比needle,算法复杂度O(m x n)
- 使用KMP算法,对needle建立最大前缀和辅助数组,算法复杂度O(m + n)
-
KMP算法解释:
- next[i] 表示以第 i 个字符为止的前 next[i] 个字符与字符串开头前 next[i] 个字符相等,即相同的最大前缀
- needle = "AABAAACD" next = [0 1 0 1 2 1 0 0]
- 假设当前对比第5个字符A,p = 5,其不相同,查询其前一个字符的最大前缀和为 next[p - 1],可知前面 2 个字符一定是AA,因此可以直接对比 needle 第 3 个字符,也就是第 next[p - 1] 个字符
- haystack = "AABAA'B'AAACD" needle = "AABAA'A'CD"
- 对比到两个字符串第 6 个字符('B' 'A')时发现不同,此时查看 needle 的 next 数组前一个字符的最大前缀长度,即 next[5] = 2,说明 haystack 第 6 个字符前面的 2 个字符和 needle 前 2 个字符相同,因此无需从头判断,直接从 needle 的第 3 个字符处继续与 haystack 第 6 个字符判断即可
class Solution {
public:
int strStr(string haystack, string needle) {
int k = 0, m = haystack.length(), n = needle.length();
vector<int> next(n, 0); // 0 表示不存在相同的最大前缀
if (n == 0) return 0;
calNext(needle, next);
for (int i = 0; i < m; i++) {
while(k > 0 && needle[k] != haystack[i]) {
k = next[k - 1]; // 部分匹配,往前回溯
}
if (needle[k] == haystack[i]) {
++k;
}
if (k == n) {
return i - n + 1;
}
}
return -1;
}
void calNext(const string& needle, vector<int>& next) {
for (int j = 1, p = 0; j < needle.length(); j++) {
while (p > 0 && needle[p] != needle[j]) {
p = next[p - 1]; // 不相同,往前回溯
}
if (needle[p] == needle[j]) {
p++; // 相同,更新相同的最大前缀长度
}
next[j] = p;
}
}
};
76. 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。Link
- 滑动窗口、对 t 中字符统计个数
- 1、预先统计 t 中每个字符的个数
- 2、对 t 中的字符标记为 true
- 记录滑动窗口内的字符
- 1、对字符的个数减 1
- 如果字符个数减 1 大于等于 0,说明是 t 中的字符,此时令 cnt++
- 2、当 cnt 等于 t 的长度,说明当前窗口包含了 t 中的字符
- 记录此时的窗口大小、起始位置
- 左边界右移,当字符为 t 中的字符时,标志为 true,并且在个数为 0 的基础加 1 将大于 0,说明 t 中的字符移出了窗口,令 cnt--
- 3、检查窗口大小是否改变,改变则输出子字符串,否则说明未找到
- 1、对字符的个数减 1
class Solution {
public:
string minWindow(string s, string t) {
vector<int> chars(128, 0);
vector<bool> flag(128, false);
// 统计 t 中的字符个数,并进行标记
for (int i = 0; i < t.size(); i++) {
++chars[t[i]];
flag[t[i]] = true;
}
int min_l = 0, l = 0, min_size = s.size() + 1, cnt = 0;
for (int r = 0; r < s.size(); ++r) {
// 实现移入滑动窗口内的字符个数减 1 操作
if (--chars[s[r]] >= 0) {
++cnt;
}
while (cnt == t.size()) {
if (r - l + 1 < min_size) {
min_l = l;
min_size = r - l + 1;
}
// 将 t 中的字符移出窗口
if (flag[s[l]] && ++chars[s[l]] > 0) {
--cnt;
}
++l;
}
}
return min_size > s.size() ? "" : s.substr(min_l, min_size);
}
};
- 滑动窗口解法 2
- 1、统计 t 中的字符个数
- 2、滑动窗口内字符判断
- 字符个数大于 0,说明是 t 的字符,此时 cnt--
- 移入窗口内的字符,其个数减 1
- 3、cnt 为 0,说明窗口内包含 t 中所有字符
- 滑动窗口左边界循环右移,直到遇到 t 的字符,即 ++need[s[l]] <= 0 不成立时
- 当 s[l] 为 t 的字符时:其值为 0,左加加后为 1
- 当 s[l] 不为 t 的字符时:其值小于 0,左加加后 小于等于 0
- 记录符合条件的窗口坐边界、窗口长度,并将 cnt++,说明 t 中字符移出窗口
- 滑动窗口左边界循环右移,直到遇到 t 的字符,即 ++need[s[l]] <= 0 不成立时
- 4、观察窗口长度是否变化,判断有无子字符串输出
class Solution {
public:
string minWindow(string s, string t) {
int len = t.size();
if (s.size() < len) return "";
vector<int> need(128, 0);
// 统计 t 中每个字符的个数
for (char &c : t) {
need[c]++;
}
// 滑动窗口求最小的子串
int l = 0, r = 0, start = 0, size = INT_MAX;
int cnt = len;
while (r < s.size()) {
char c = s[r];
// 只有 c 为 t 中的字符时,才有 if 条件成立,说明窗口内引入了一个有用字符
if (need[c] > 0) {
cnt--;
}
// 将滑动窗口内字符的数量减 1
need[c]--;
// 说明 t 中的所有字符均在滑动窗口内了
if (cnt == 0) {
// 因为先前统计了 t 中字符的数量,滑动窗口内遇到字符将其减 1
// 只有 t 中的字符值为 0,故 ++ 操作后为 1 大于 0
while (l < r && ++need[s[l]] <= 0) {
l++;
}
// 更新符合条件的子字符串长度
int cur = r - l + 1;
if (cur < size) {
size = cur;
start = l;
}
// 将左边界右移,同时将 cnt++,因为一个有效字符移出了滑动窗口
l++;
cnt++;
}
r++; // 右移右边界
}
return size == INT_MAX ? "" : s.substr(start, size);
}
};
3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。Link
- 滑动窗口统计字符、同一字符出现两次即计算
- 1、建立容器存储每个字符出现的个数
- 2、当字符个数超过 1 时,说明出现了重复字符
- 记录重复字符前的窗口长度,即为子串长度
- 移动窗口左边界,直到将重复字符移出
- |- -count[s[l]] <= 0 说明移出的为非重复字符,因为这些字符只出现了一次,减 1 后为 0
- 3、到达字符串结尾时再次更新最大子串长度
- 计算窗口大小的触发条件时出现了重复字符,但字符串遍历完也需要触发并计算窗口大小
class Solution {
public:
int lengthOfLongestSubstring(string s) {
vector<int> count(128, 0);
int l = 0, size = 0;
int n = s.size();
for (int i = 0; i < s.size(); i++) {
char c = s[i];
if (++count[c] > 1) {
// 当前 i 指向的是重复字符,因此字符串长度为 i - l
size = max(size, i - l);
// 循环移动左边界,并将字符个数减 1
while (--count[s[l]] <= 0) {
l++;
}
// l 指向重复字符,将其移出需要再加 1
l++;
}
}
// 统计到达字符串末尾时的 子字符串长度
return max(size, n - l);
}
};
394. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。Link
- 递归思想、遇左 '[' 便向下递归
- 1、使用数字存储编码次数、字符串存储结果
- 2、'[' 前必定是一个数字 “3[a2[bc]]”
- 向下递归、遇 ']' 则返回结果
- "a2[bc]"
- res = "a" -> num = 2 -> return bc -> "a" + 2 * "bc"
- 字符串根据次数、递归返回的结果得到当前的解码结果
- 向下递归、遇 ']' 则返回结果
- 3、遍历结束返回结果
- 索引为引用形式,防止重复计算
class Solution {
public:
string decodeString(string s) {
int i = 0;
return recur(s, i);
}
string recur(string& s, int& i) {
string res = "";
int num = 0;
int n = s.size();
while (i < n) {
if (isdigit(s[i])) {
num = num * 10 + (s[i] - '0');
}else if (isalpha(s[i])) {
res += s[i];
}else if (s[i] == '[') {
string temp;
i++;
// "3 -> [a2[bc]]"
// "2 -> [bc]"
temp = recur(s, i);
while (num > 0) {
res += temp;
num--;
}
}else {
return res; // 返回结果
}
i++;
}
return res;
}
};