代码改变世界

LeetCode 420 - 强密码检验器 - 指南

2025-12-12 14:45  tlnshuju  阅读(0)  评论(0)    收藏  举报

在这里插入图片描述
在这里插入图片描述

摘要

这道题是 LeetCode 第 420 题:强密码检验器(Strong Password Checker)
乍一看挺简单:“判断一个密码是否够强”,但实际上这题的规则相当复杂。要同时考虑 长度、字符种类、重复字符、替换、插入和删除操作

这题在算法上属于一个典型的 字符串修复 + 贪心策略 + 分类讨论 问题,难度在 LeetCode 上是 Hard
本篇我们会用 Swift 来实现一个清晰、可运行的版本,并讲透整个逻辑。

描述

题意总结一下:

给定一个字符串 password,我们要让它变成“强密码”。所谓强密码,要满足三个条件:

  1. 长度要求
    必须在 [6, 20] 之间。
  2. 字符种类要求
    至少要有一个小写字母、一个大写字母、一个数字。
  3. 连续字符要求
    不允许出现连续三个一样的字符。

可以通过三种操作实现修改:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

我们要找出 最少的修改次数

举几个例子:

  • "a" → 长度太短、缺大写、缺数字,至少要补5步。
  • "aA1" → 长度刚好够,但太短,得补3步。
  • "1337C0d3" → 本身已经完美,0步!

题解答案

这道题的核心在于分情况处理:

  1. 如果长度 < 6
    主要靠插入字符补足长度,同时考虑字符种类不全的情况。

  2. 如果长度 > 20
    主要靠删除字符缩短长度,但删除时要尽量减少三连字符的数量。

  3. 长度在 [6, 20] 内
    不用插入或删除,只要通过替换字符来打破重复或补充缺少的字符类型。

所以整体上,我们要关注三个指标:

  • missingTypes:缺少的字符类型数量(如没大写、没数字)
  • replace:为打断三连字符而需要替换的次数
  • deleteCount:当超长时需要删除的次数

最终的答案根据不同情况计算。

题解代码分析

下面是 Swift 实现的完整代码(可直接在 Xcode Playground 运行)

import Foundation
class 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 }.count
var repeats = [Int]()
var i = 0
// 找出连续重复字符段长度
while i < chars.count {
var j = i
while 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 - 20
var 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] -= del
toDelete -= 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 = i
while 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 转换。