1. 前言
在本文中,我们将研究一种非常有趣的问题:如何找到包含所有元音字母的最小子字符串的长度。这个问题看起来很简单,但是实际上却有多种解法。本文将介绍其中的一种基于滑动窗口算法实现的方法。
2. 问题描述
给定一个字符串,要求找到该字符串中包含所有元音字母的最小子字符串的长度。
在本问题中,元音字母包括 a、e、i、o 和 u,可以是大写或小写。
3. 暴力枚举算法
我们可以通过枚举所有子字符串,并检查是否包含所有元音字母来解决本问题。具体地,对于每个子字符串,我们可以使用一个哈希表来记录其中出现的元音字母,如果哈希表中包含所有元音字母,则说明该子字符串是一个解。
以下是暴力枚举算法的 C++ 代码实现:
class Solution {
public:
int findSubstring(string s) {
int n = s.length(), ans = n;
unordered_set<char> vowels = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
for (int i = 0; i < n; i++) {
unordered_map<char, int> counts;
bool found = true;
for (int j = i; j < n; j++) {
if (vowels.count(s[j])) {
counts[s[j]]++;
}
if (counts.size() == vowels.size()) {
ans = min(ans, j - i + 1);
found = true;
break;
}
}
}
return ans;
}
};
时间复杂度:O(n^3)
空间复杂度:O(n)
3.1. 暴力枚举算法的优化
在上述暴力枚举算法中,我们使用哈希表来记录每个子字符串中出现的元音字母。这个哈希表的大小最大为 10,也就是元音字母的数量。因此,我们可以换一种方式,使用一个长度为 10 的数组来记录每个元音字母出现的次数。具体地,我们可以根据 ASCII 码值将每个字母映射到数组中对应的位置。
以下是优化后的算法的 C++ 代码实现:
class Solution {
public:
int findSubstring(string s) {
int n = s.length(), ans = n;
vector<int> counts(10);
int vowels[10] = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
auto is_vowel = [&](char c) {
for (int i = 0; i < 10; i++) {
if (c == vowels[i]) {
return true;
}
}
return false;
};
for (int i = 0; i < n; i++) {
fill(counts.begin(), counts.end(), 0);
bool found = true;
for (int j = i; j < n; j++) {
if (is_vowel(s[j])) {
counts[s[j] % 97]++;
}
bool all_vowels = true;
for (int k = 0; k < 10; k++) {
if (counts[k] == 0) {
all_vowels = false;
break;
}
}
if (all_vowels) {
ans = min(ans, j - i + 1);
found = true;
break;
}
}
}
return ans;
}
};
时间复杂度:O(n^2)
空间复杂度:O(1)
4. 滑动窗口算法
另一种解决本问题的方法是使用滑动窗口算法。具体地,我们可以使用一个滑动窗口来遍历字符串,并维护当前滑动窗口包含的元音字母的数量。如果当前滑动窗口包含所有元音字母,则我们可以尝试缩小滑动窗口的左边界,直到无法满足包含所有元音字母的条件为止。
以下是滑动窗口算法的 C++ 代码实现:
class Solution {
public:
int findSubstring(string s) {
int n = s.length(), ans = n;
vector<int> counts(10);
int vowels[10] = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
auto is_vowel = [&](char c) {
for (int i = 0; i < 10; i++) {
if (c == vowels[i]) {
return true;
}
}
return false;
};
int left = 0, right = -1;
while (left < n && !is_vowel(s[left])) {
left++;
}
while (right + 1 < n && !is_vowel(s[right + 1])) {
right++;
}
if (!(left <= right && right < n)) {
return 0;
}
counts[s[left] % 97]++;
counts[s[right] % 97]++;
while (left <= right && right < n) {
bool all_vowels = true;
for (int i = 0; i < 10; i++) {
if (counts[i] == 0) {
all_vowels = false;
break;
}
}
if (all_vowels) {
ans = min(ans, right - left + 1);
counts[s[left] % 97]--;
left++;
} else {
right++;
if (right < n && is_vowel(s[right])) {
counts[s[right] % 97]++;
}
}
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
4.1. 滑动窗口算法的优化
在上述滑动窗口算法中,我们使用一个滑动窗口来遍历字符串,并维护当前滑动窗口包含的元音字母的数量。具体来说,我们维护一个长度为 10 的数组 counts,其中 counts[i] 表示在当前滑动窗口中元音字母 i 出现的次数。
在每次滑动窗口时,我们将左边界向右移动一位,将右边界向右移动一位。如果新的右边界对应的字符是元音字母,则将 counts 数组中相应元素加 1。检查当前滑动窗口是否包含所有元音字母时,我们只需要检查 counts 数组中的所有元素是否均大于 0 就行了。
在上述算法中,我们使用两个 while 循环来找到第一个左边界和右边界对应的位置。这两个循环的时间复杂度为 O(n),因此可以考虑将它们合并到第一个 while 循环中。
以下是优化后的算法的 C++ 代码实现:
class Solution {
public:
int findSubstring(string s) {
int n = s.length(), ans = n;
vector<int> counts(10);
int vowels[10] = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
auto is_vowel = [&](char c) {
for (int i = 0; i < 10; i++) {
if (c == vowels[i]) {
return true;
}
}
return false;
};
int left = 0, right = -1;
while (left < n) {
if (right + 1 < n && is_vowel(s[right + 1])) {
right++;
counts[s[right] % 97]++;
} else {
left++;
if (left <= right && is_vowel(s[left - 1])) {
counts[s[left - 1] % 97]--;
}
}
bool all_vowels = true;
for (int i = 0; i < 10; i++) {
if (counts[i] == 0) {
all_vowels = false;
break;
}
}
if (all_vowels) {
ans = min(ans, right - left + 1);
if (is_vowel(s[left])) {
counts[s[left] % 97]--;
}
left++;
}
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
5. 总结
本文介绍了两种解决找到包含所有元音字母的最小子字符串的长度问题的方法:暴力枚举算法和滑动窗口算法。
暴力枚举算法的时间复杂度为 O(n^3),空间复杂度为 O(n);
滑动窗口算法的时间复杂度为 O(n),空间复杂度为 O(1)。
相比之下,滑动窗口算法更为高效,因此是更合适的解决方案。