暴力枚举子串频次的最优解是固定长度降序滑动窗口法,时间复杂度O(n²),优于纯暴力O(n³);需处理空串、越界、哈希冲突等边界,推荐substr+unordered_map组合。

暴力枚举所有子串并统计频次
最直接的思路是生成字符串中所有可能的子串,用哈希表计数,再遍历找出最大频次对应的子串。适用于长度 ≤ 100 的输入,否则时间复杂度 O(n³) 会明显卡顿(生成子串 O(n²),每次插入/查找哈希表平均 O(n))。
关键点:
- 子串起始位置
i从0到n-1,长度len从1到n-i,用s.substr(i, len)提取 - 必须用
std::unordered_map<:string int></:string>而非map,避免O(log k)插入开销(k是不同子串数) - 若多个子串频次相同,题目未指定返回规则时,按「长度最长优先,长度相同时字典序最小」处理较合理
用后缀数组 + 高度数组优化到 O(n²)
当原字符串长度达 500+,暴力法超时,需降维。后缀数组(SA)本身构建是 O(n log n),但后续统计仍需配合高度数组(LCP)做区间极小值查询,实际编码复杂度高;更实用的是用 std::set 或 std::vector 存所有后缀指针,对每个起始位置 i,用二分在已排序后缀集合中找有多少个后缀以 s.substr(i, len) 开头 —— 这本质是模拟后缀自动机的部分能力,但实现更轻量。
简化实操建议:
立即学习“C++免费学习笔记(深入)”;
- 预处理所有后缀:
std::vector<:string> suffixes</:string>,存s.substr(i)共n个 -
std::sort(suffixes.begin(), suffixes.end()),然后对每个i和len,用std::lower_bound和std::upper_bound查以s.substr(i, len)为前缀的后缀数量 - 注意:
substr(i, len)可能越界,需加std::min(len, (int)s.size() - i)保护
用 unordered_set 避免重复计算相同子串
暴力法里,"aaa" 会产生三个 "a"、两个 "aa"、一个 "aaa",但同一子串多次生成再哈希是冗余的。可在双层循环中加一层缓存:对每个起始位置 i,用 std::unordered_set<:string></:string> 记录本行已处理过的子串,跳过重复。
但这只节省常数时间,不改变渐进复杂度。真正有效的是反向思维:固定子串长度 len,滑动窗口扫一遍,用 unordered_map 统计该长度下所有子串频次,再更新全局最优。这样内存局部性更好,且便于 early stop(例如当 len == n 时频次必为 1,无需继续)。
- 外层
len从n递减到1,首次遇到频次 ≥ 2 的子串即可返回(满足「最频繁」且「尽可能长」) - 每轮只需
O(n)时间和O(n)空间,总复杂度O(n²),比纯暴力稳定 - 注意:C++11 起
std::string移动语义生效,substr()返回右值时开销小,但旧编译器建议用string_view(C++17)避免拷贝
边界与性能陷阱
实际写的时候,最容易栽在三类地方:
- 空字符串或单字符输入:
s.empty()时应直接返回空串;s.size() == 1时唯一子串就是它自己,频次为 1 - 子串长度为 0:
s.substr(i, 0)返回空串,若不提前排除,会导致空串频次虚高(尤其当原串含多个位置可取长度 0) - 哈希冲突误判:
unordered_map默认哈希函数对短字符串高效,但若自定义类型或极端数据(如全 'a' 的长串),可能退化。此时改用std::map换取稳定O(n² log n)更稳妥 - 内存爆炸:长度 1000 的字符串有约 5e5 个子串,每个平均长度 500 → 单纯存所有子串字符串需 250MB+,必须用
string_view或滚动哈希(如std::hash<:string_view></:string_view>)压缩空间
滚动哈希虽快,但存在哈希碰撞风险,生产环境需双哈希或校验原始字符串;教学或小数据场景,老老实实 substr + unordered_map 最不易出错。


















