← 返回题目列表

让字符频率唯一的最少删除次数如何用贪心求解?

中等 第 22 / 29 题 更新于 2026/08/01
贪心哈希表字符频率最少删除

简化版

先统计每个字符频率,再让每个频率都唯一。对每个频率 f,如果它已经被占用,就不断减 1 并增加删除次数,直到 f=0 或遇到未占用频率。贪心点是每次尽量保留当前频率大一些,删除次数最少。

详细版

Set 记录已经使用过的频率。遍历字符频率时,若当前频率冲突,就持续 f--,每减一次代表删除一个字符。减到未使用频率后加入集合;如果减到 0,表示该字符全部删除,不需要把 0 加入集合。

因为只允许删除字符,频率只能下降不能上升,所以在冲突时把当前频率降到最近的可用频率是局部最优。时间复杂度近似 O(n + 字符种类 * 最大频率调整),小写字母场景可视作 O(n)

完整版教学

一、题目到底在约束什么

目标是让所有出现过的字符频率互不相同。比如 "aaabbbcc" 的频率是:

a:3
b:3
c:2

ab 都是 3,冲突。我们可以删除一个 b,让频率变成 3,2,2,仍然冲突;更好的做法可能是把 b 降到 1,得到 3,1,2

二、为什么只能向下调整频率

题目允许删除字符,不允许新增字符。所以频率只能从 f 变成 f-1, f-2, ... , 0,不能变大。

操作频率变化
删除 1 个字符f -> f-1
删除多个字符f -> 更小非负数
新增字符不允许

记忆钩子:频率唯一题,删除只能往下找坑位,不能往上补。

这个单向性让贪心变得自然:冲突时找离当前最近的空频率。

三、贪心策略为什么是“降到最近可用频率”

如果频率 f 已经被用过,当前字符必须降低到某个未使用频率。降低得越少,删除次数越少,并且保留更多字符不会让后续更难,因为使用的是最靠近 f 的空位。

已用频率:{3,2}
当前 f=3
可选降到:1 或 0
降到 1 删除 2 个,比降到 0 删除 3 个更好

这就是局部最优:在所有合法下降结果里,选择最大可用频率。

四、用 Set 维护已用频率

遍历每个字符的频率:

while f > 0 && used contains f:
    f--
    deletions++
if f > 0:
    used.add(f)

0 不加入集合,因为多个字符都删光是允许的;它们已经不再出现,不参与“出现字符频率唯一”的约束。

频率 [3,3,2]
第一个 3:used={3}
第二个 3:降到 1,used={3,1}
2:used={3,1,2}

总删除 2。

五、代码模板

int minDeletions(String s) {
    int[] cnt = new int[26];
    for (char c : s.toCharArray()) cnt[c - 'a']++;
    Set<Integer> used = new HashSet<>();
    int ans = 0;
    for (int f : cnt) {
        while (f > 0 && used.contains(f)) {
            f--;
            ans++;
        }
        if (f > 0) used.add(f);
    }
    return ans;
}

如果字符集不是小写字母,可以用 Map<Character, Integer> 统计,后面的贪心逻辑不变。

六、为什么遍历顺序通常不影响结果

因为每个频率冲突时都会下降到当前可用的最高位置。也可以先把频率降序排序,再处理,这样直觉上更清楚:大频率优先占大坑,小频率往下挪。

写法特点
Set 直接处理代码短,小写字母常用
频率排序后处理证明更直观

对于小写字母最多 26 种,任意写法性能都足够。

七、常见误区与追问

  • 误区:把冲突频率统一减 1 就结束。 减 1 后仍可能和其他频率冲突,要循环检查。
  • 误区:把频率 0 加入 used。 多个字符被删光是允许的,0 不应参与唯一性。
  • 误区:试图把小频率增加。 题目只能删除,不能新增字符。
  • 追问:为什么降到最近可用频率最优? 删除次数最少,且不会占用比它更高的可用位置。
  • 追问:字符集很大怎么办? 用 HashMap 统计频率,再用 HashSet 处理已用频率。
  • 追问:复杂度如何? 统计是 O(n),调整次数不超过删除总次数,整体可接受。

八、加强记忆

让频率唯一的关键是“频率只能往下掉”。每个字符拿着自己的频率去找坑,坑被占了就删一个字符继续往下找;找到空坑就占住,掉到 0 就代表删没了。Set 记录已用频率,while 负责处理连续冲突,别只减一次就停。