LeetCode 395 - 至少有 K 个重复字符的最长子串


文章目录

摘要

在日常开发中,我们经常需要分析字符串,比如日志过滤、关键字提取、或者做文本分析。这类问题的核心都是"在一个字符串中找出满足某种约束的最长片段"。

LeetCode 第 395 题 "至少有 K 个重复字符的最长子串" 就是这种典型问题之一。它考察了我们对分治思想 的理解,以及如何结合递归与频次统计快速定位符合条件的子串。

虽然看起来像是一个"滑动窗口"问题,但其实这题最优解并不是窗口遍历,而是一个非常有意思的 "分而治之" 过程。

描述

题目要求:

给定一个字符串 s 和一个整数 k,我们需要找到 s 中的最长子串 ,这个子串的每一个字符都至少重复了 k 次。

简单来说,就是:

所有字符的出现次数都要 ≥ k。

如果找不到符合要求的子串,返回 0。

示例 1:

txt 复制代码
输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa"

示例 2:

txt 复制代码
输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb"

题解答案

这个问题的关键在于:

哪些字符"不够格"(出现次数 < k),它们就不能出现在最终的结果中。

因为任何包含这些字符的子串都不可能满足条件。

所以可以把这些"不合格的字符"作为分割点,把原字符串拆成若干子串,然后递归地在每个子串中寻找最长合法部分。

这其实是一种 "分治法(Divide and Conquer)" 的思路。

题解代码分析

下面是完整的 Swift 实现,逻辑清晰,可直接运行

swift 复制代码
import Foundation

class Solution {
    func longestSubstring(_ s: String, _ k: Int) -> Int {
        // 基础情况:如果字符串长度小于 k,肯定不可能有合法子串
        if s.count < k { return 0 }
        
        // 统计字符出现次数
        var freq: [Character: Int] = [:]
        for ch in s {
            freq[ch, default: 0] += 1
        }
        
        // 找出第一个不满足条件的字符
        for (ch, count) in freq {
            if count < k {
                // 用这个字符作为"分割点"
                let parts = s.split(separator: ch)
                // 对每个部分递归求解
                var maxLen = 0
                for part in parts {
                    maxLen = max(maxLen, longestSubstring(String(part), k))
                }
                return maxLen
            }
        }
        
        // 如果所有字符都满足条件,那整个字符串就是结果
        return s.count
    }
}

代码解析

整个算法的思想可以分为三个阶段来看:

1. 统计字符出现次数

我们先遍历字符串,用字典(freq)记录每个字符出现的次数。

swift 复制代码
var freq: [Character: Int] = [:]
for ch in s {
    freq[ch, default: 0] += 1
}

如果字符串长度小于 k,那肯定直接返回 0,不用多想。

2. 找出不合格的字符

如果某个字符出现次数小于 k,它就是整个问题的"拦路虎"。

因为任何包含它的子串都不可能满足要求。

swift 复制代码
for (ch, count) in freq {
    if count < k {
        // 找到第一个不合格字符
        ...
    }
}
3. 分治拆分 + 递归求解

一旦发现"不合格的字符",就以它为分界线把字符串切开。

举个例子:

txt 复制代码
s = "aaabbcc", k = 3

这里 'b' 只出现了 2 次,不达标。

那么就以 'b' 为分割点,把字符串分成两部分:

  • "aaa"
  • "cc"

然后递归在每一部分中分别求解。

最后取这两部分结果的最大值。

这一段的核心逻辑是:

swift 复制代码
let parts = s.split(separator: ch)
var maxLen = 0
for part in parts {
    maxLen = max(maxLen, longestSubstring(String(part), k))
}
return maxLen
4. 所有字符都合格

如果遍历完所有字符后都没有发现"不合格的字符",那说明整个字符串本身就是合法的,直接返回 s.count

swift 复制代码
return s.count

示例测试及结果

我们来测试几个案例

swift 复制代码
let solution = Solution()

print(solution.longestSubstring("aaabb", 3))      // 输出:3
print(solution.longestSubstring("ababbc", 2))     // 输出:5
print(solution.longestSubstring("aabcabb", 2))    // 输出:4
print(solution.longestSubstring("abcd", 2))       // 输出:0

输出结果:

txt 复制代码
3
5
4
0

解释:

  • "aaabb""aaa" 是最长的合法子串;
  • "ababbc""ababb" 满足条件;
  • "aabcabb" → 最长合法子串是 "abba""bcabb"
  • "abcd" → 所有字符只出现一次,不满足 k=2

时间复杂度

这道题的复杂度分析比较有趣。

最坏情况下,每次递归都可能遍历整个字符串,因此:

  • 时间复杂度约为 O(26 * n),因为我们最多只会针对 26 个字母做拆分。

对一般情况来说,性能表现非常稳定。

空间复杂度

递归栈的深度最多为 26 层(因为每次可能由不合格的字符切分一次),

同时字典统计频次的空间是 O(26)。

所以整体空间复杂度是 O(26 + 递归深度)O(26),也就是常数级别。

总结

这道题的精髓就在于"分治"两个字。

我们不是去暴力枚举所有子串,而是通过统计信息快速确定哪些字符"拖后腿",再把它们作为分界线递归处理剩余部分。

这种思路非常适合解决**"全局约束 + 局部分割"**类的问题,比如:

  • "最长有效括号"
  • "满足特定条件的连续区间"
  • "日志分片分析"

在实际开发中,如果你在做文本分析(比如统计出现频率、提取关键词等),这种方法也能派上用场。

比如在做"日志聚类"时,你也可以先找出低频字符做分割,从而减少计算量。

相关推荐
Espresso Macchiato4 小时前
Leetcode 3710. Maximum Partition Factor
leetcode·职场和发展·广度优先遍历·二分法·leetcode hard·leetcode 3710·leetcode双周赛167
hz_zhangrl4 小时前
CCF-GESP 等级考试 2025年9月认证C++四级真题解析
开发语言·c++·算法·程序设计·gesp·c++四级·gesp2025年9月
少许极端4 小时前
算法奇妙屋(六)-哈希表
java·数据结构·算法·哈希算法·散列表·排序
羊羊小栈4 小时前
基于「多模态大模型 + BGE向量检索增强RAG」的新能源汽车故障诊断智能问答系统(vue+flask+AI算法)
vue.js·人工智能·算法·flask·汽车·毕业设计·大作业
Da Da 泓4 小时前
shellSort
java·数据结构·学习·算法·排序算法
2013编程爱好者5 小时前
计算时间复杂度
c++·算法·排序算法
巴里巴气5 小时前
第15题 三数之和
数据结构·算法·leetcode
小许学java5 小时前
数据结构-Map和Set
数据结构·算法·set·map·哈希表·哈希冲突·哈希桶
前端架构师-老李5 小时前
面试问题—你接受加班吗?
面试·职场和发展