LeetCode 420 - 强密码检验器


文章目录
- 摘要
- 描述
- 题解答案
- 题解代码分析
- 代码详细解析
- ① 检查字符类型
- ② 查找连续重复段
- ③ 根据长度情况分支
- 示例测试及结果
- 时间复杂度
- 空间复杂度
- 总结
摘要
这道题是 LeetCode 第 420 题:强密码检验器(Strong Password Checker)。
乍一看挺简单:“判断一个密码是否够强”,但实际上这题的规则相当复杂。要同时考虑 长度、字符种类、重复字符、替换、插入和删除操作。
这题在算法上属于一个典型的 字符串修复 + 贪心策略 + 分类讨论 问题,难度在 LeetCode 上是 Hard。
本篇我们会用 Swift 来实现一个清晰、可运行的版本,并讲透整个逻辑。

描述
题意总结一下:
给定一个字符串 password,我们要让它变成“强密码”。所谓强密码,要满足三个条件:
- 长度要求:
必须在[6, 20]之间。 - 字符种类要求:
至少要有一个小写字母、一个大写字母、一个数字。 - 连续字符要求:
不允许出现连续三个一样的字符。
可以通过三种操作实现修改:
- 插入一个字符
- 删除一个字符
- 替换一个字符
我们要找出 最少的修改次数。
举几个例子:
"a"→ 长度太短、缺大写、缺数字,至少要补5步。"aA1"→ 长度刚好够,但太短,得补3步。"1337C0d3"→ 本身已经完美,0步!
题解答案
这道题的核心在于分情况处理:
-
如果长度 < 6:
主要靠插入字符补足长度,同时考虑字符种类不全的情况。 -
如果长度 > 20:
主要靠删除字符缩短长度,但删除时要尽量减少三连字符的数量。 -
长度在 [6, 20] 内:
不用插入或删除,只要通过替换字符来打破重复或补充缺少的字符类型。
所以整体上,我们要关注三个指标:
missingTypes:缺少的字符类型数量(如没大写、没数字)replace:为打断三连字符而需要替换的次数deleteCount:当超长时需要删除的次数
最终的答案根据不同情况计算。

题解代码分析
下面是 Swift 实现的完整代码(可直接在 Xcode Playground 运行)
import Foundationclass Solution {func strongPasswordChecker(_ password: String) -> Int {let chars = Array(password)var hasLower = false, hasUpper = false, hasDigit = false// 检查字符类型for c in chars {if c.isLowercase { hasLower = true }else if c.isUppercase { hasUpper = true }else if c.isNumber { hasDigit = true }}let missingTypes = [hasLower, hasUpper, hasDigit].filter { !$0 }.countvar repeats = [Int]()var i = 0// 找出连续重复字符段长度while i < chars.count {var j = iwhile j < chars.count && chars[j] == chars[i] { j += 1 }if j - i >= 3 { repeats.append(j - i) }i = j}let n = chars.count// 太短:主要补长度和类型if n < 6 {return max(6 - n, missingTypes)}// 太长:先删除if n > 20 {var deleteCount = n - 20var toDelete = deleteCount// 优先削减重复段,按 (len % 3) 的顺序删for k in 0..<3 {for idx in 0..<repeats.count {if toDelete == 0 { break }if repeats[idx] < 3 || repeats[idx] % 3 != k { continue }let del = min(toDelete, k + 1)repeats[idx] -= deltoDelete -= del}}// 剩余部分还需替换let replace = repeats.reduce(0) { $0 + $1 / 3 }return deleteCount + max(missingTypes, replace)}// 3️⃣ 正常长度:只需替换重复或补类型let replace = repeats.reduce(0) { $0 + $1 / 3 }return max(missingTypes, replace)}
}
代码详细解析
我们把代码逻辑拆开讲
① 检查字符类型
for c in chars {if c.isLowercase { hasLower = true }else if c.isUppercase { hasUpper = true }else if c.isNumber { hasDigit = true }
}
统计是否包含三种类型(小写、大写、数字),后面用于计算 missingTypes。
② 查找连续重复段
while i < chars.count {var j = iwhile j < chars.count && chars[j] == chars[i] { j += 1 }if j - i >= 3 { repeats.append(j - i) }i = j
}
例如 "aaaBBBcc" 会得到 [3, 3],表示有两处三连。
③ 根据长度情况分支
-
短密码(<6)
直接max(6 - n, missingTypes)。
举例"aA1"长度3,缺0种,结果为max(6-3,0)=3。 -
长密码(>20)
优先删除(尽量减少重复段),再替换。
删除时的贪心顺序是:- 先删
len % 3 == 0的段 - 再删
len % 3 == 1 - 最后删
len % 3 == 2
这样能最大化减少替换次数。
- 先删
-
正常长度(6~20)
不需删除,直接看要替换多少次打断三连,同时要补类型。
示例测试及结果
let solution = Solution()print(solution.strongPasswordChecker("a")) // 输出: 5
print(solution.strongPasswordChecker("aA1")) // 输出: 3
print(solution.strongPasswordChecker("1337C0d3")) // 输出: 0
print(solution.strongPasswordChecker("aaaaa")) // 输出: 2
print(solution.strongPasswordChecker("Aa123")) // 输出: 1
输出结果:
5
3
0
2
1
可以看到结果完全符合预期。
特别是 "aaaaa" 这种情况,长度不够还存在连续重复,所以要补 2 步。
时间复杂度
- 遍历字符和计算重复段都是 O(n)。
- 删除和替换阶段的处理也是 O(n)。
时间复杂度:O(n)
空间复杂度
我们额外用了一个 repeats 数组来记录连续段长度,
最多存储 n/3 个元素。
空间复杂度:O(n)
总结
这道题虽然是“密码强度检测”,但其实是一个挺复杂的算法题。
它融合了:
- 贪心策略(先删重复)
- 分类讨论(长度分段)
- 模拟操作(插入、删除、替换)
在真实项目中,这种算法思想其实挺实用的,比如:
- 用户注册系统的密码强度提示
- 输入规则实时校验(前端 + 后端)
- 安全策略模拟测试
Swift 在字符串处理上相对安全(Unicode友好),
但因为没有直接的字符下标操作,所以需要注意 Array 转换。
