1. 力扣Hot100子串问题解析
最近在力扣社区刷Hot100题单时,发现子串类问题出现的频率相当高。这类问题通常要求我们在字符串中寻找满足特定条件的连续字符序列,看似简单实则暗藏多种解题思路。今天就用C++来系统梳理这类题目的解题套路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 子串问题核心解法
2.1 滑动窗口法
滑动窗口是解决子串问题的利器,特别适合"最长无重复字符子串"这类题目。基本思路是维护左右两个指针构成的窗口,通过调整指针位置来寻找最优解。
cpp复制int lengthOfLongestSubstring(string s) {
unordered_set<char> window;
int left = 0, max_len = 0;
for(int right = 0; right < s.size(); ++right) {
while(window.count(s[right])) {
window.erase(s[left++]);
}
window.insert(s[right]);
max_len = max(max_len, right - left + 1);
}
return max_len;
}
注意:使用unordered_set而非set可以提升查找效率,时间复杂度从O(nlogn)降到O(n)
2.2 前缀和技巧
对于涉及子串统计的问题,前缀和能有效降低时间复杂度。以"和为K的子数组"为例:
cpp复制int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> prefix_sum;
prefix_sum[0] = 1;
int sum = 0, count = 0;
for(int num : nums) {
sum += num;
if(prefix_sum.count(sum - k)) {
count += prefix_sum[sum - k];
}
prefix_sum[sum]++;
}
return count;
}
2.3 双指针法
在处理回文子串问题时,双指针法展现出独特优势。以"最长回文子串"为例:
cpp复制string longestPalindrome(string s) {
int start = 0, max_len = 1;
auto expand = [&](int l, int r) {
while(l >= 0 && r < s.size() && s[l] == s[r]) {
if(r - l + 1 > max_len) {
max_len = r - l + 1;
start = l;
}
l--; r++;
}
};
for(int i = 0; i < s.size(); ++i) {
expand(i, i); // 奇数长度
expand(i, i+1); // 偶数长度
}
return s.substr(start, max_len);
}
3. 典型题目实战解析
3.1 无重复字符的最长子串
这道题是滑动窗口的经典应用。关键点在于:
- 使用哈希集合记录窗口内字符
- 遇到重复字符时移动左指针
- 实时更新最大长度
时间复杂度O(n),空间复杂度O(字符集大小)
3.2 找到字符串中所有字母异位词
cpp复制vector<int> findAnagrams(string s, string p) {
vector<int> res;
if(s.size() < p.size()) return res;
vector<int> p_count(26, 0);
vector<int> window(26, 0);
for(char c : p) p_count[c-'a']++;
for(int i = 0; i < s.size(); ++i) {
window[s[i]-'a']++;
if(i >= p.size()) window[s[i-p.size()]-'a']--;
if(window == p_count) res.push_back(i-p.size()+1);
}
return res;
}
技巧:固定长度的滑动窗口可以用数组代替哈希表,提升效率
3.3 最小覆盖子串
这道hard题目综合了滑动窗口和哈希表的使用:
cpp复制string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for(char c : t) need[c]++;
int left = 0, valid = 0;
int start = 0, len = INT_MAX;
for(int right = 0; right < s.size(); ++right) {
char c = s[right];
if(need.count(c)) {
window[c]++;
if(window[c] == need[c]) valid++;
}
while(valid == need.size()) {
if(right - left + 1 < len) {
start = left;
len = right - left + 1;
}
char d = s[left++];
if(need.count(d)) {
if(window[d] == need[d]) valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
4. 性能优化与常见陷阱
4.1 容器选择策略
- 字符范围明确时优先用数组替代unordered_map
- C++中unordered_map比map快,但要注意哈希冲突
- 对于固定窗口,考虑用vector+计数方式
4.2 边界条件处理
常见易错点包括:
- 空字符串输入
- 所有字符都相同的情况
- 子串长度等于整个字符串
- 包含特殊字符或数字的字符串
4.3 调试技巧
- 打印窗口内容辅助调试:
cpp复制cout << "Window: " << s.substr(left, right-left+1) << endl;
- 使用assert验证中间结果:
cpp复制assert(window.size() <= need.size());
- 对于复杂逻辑,先写伪代码再实现
5. 模板化解题思路
通过大量练习,我总结出子串问题的通用解题框架:
- 初始化窗口边界和结果变量
- 移动右指针扩展窗口
- 满足条件时尝试收缩左指针
- 更新最优解
- 处理边界情况
以这个框架为基础,大多数子串问题都能迎刃而解。关键在于识别问题特征,选择合适的辅助数据结构。
