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),也就是常数级别。

总结

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

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

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

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

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

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

相关推荐
gfdhy5 小时前
【c++】哈希算法深度解析:实现、核心作用与工业级应用
c语言·开发语言·c++·算法·密码学·哈希算法·哈希
Warren985 小时前
Python自动化测试全栈面试
服务器·网络·数据库·mysql·ubuntu·面试·职场和发展
百***06015 小时前
SpringMVC 请求参数接收
前端·javascript·算法
一个不知名程序员www6 小时前
算法学习入门---vector(C++)
c++·算法
云飞云共享云桌面6 小时前
无需配置传统电脑——智能装备工厂10个SolidWorks共享一台工作站
运维·服务器·前端·网络·算法·电脑
福尔摩斯张6 小时前
《C 语言指针从入门到精通:全面笔记 + 实战习题深度解析》(超详细)
linux·运维·服务器·c语言·开发语言·c++·算法
橘颂TA6 小时前
【剑斩OFFER】算法的暴力美学——两整数之和
算法·leetcode·职场和发展
Dream it possible!7 小时前
LeetCode 面试经典 150_二叉搜索树_二叉搜索树的最小绝对差(85_530_C++_简单)
c++·leetcode·面试
xxxxxxllllllshi7 小时前
【LeetCode Hot100----14-贪心算法(01-05),包含多种方法,详细思路与代码,让你一篇文章看懂所有!】
java·数据结构·算法·leetcode·贪心算法
前端小L7 小时前
图论专题(二十二):并查集的“逻辑审判”——判断「等式方程的可满足性」
算法·矩阵·深度优先·图论·宽度优先