题目描述

✅ 1002. 查找共用字符

image-20260929000441368

题意分析

返回每个单词都拥有的字符,并保留能够共同提供的重复份数。字符不要求连续,也不要求在单词中处于相同位置,答案顺序任意。

因为输入只包含小写英文字母,可以分别统计 26 个字母的出现次数。问题不是普通集合交集,而是每种字符最多能取多少份的多重集合交集。

解法:逐位维护最小词频

核心思路

[!blue]

对某个字符,设它在各个单词中的出现次数分别为若干个频次。共同答案中的份数不能超过任意一个单词的频次,所以最多只能取这些频次的最小值;反过来,每个单词都至少拥有这么多份,因此取这个最小值一定可行。不同字符之间没有顺序限制,可以独立计算。

用 freq[c] 记录当前单词中字母 c 的次数,用 minFreq[c] 记录已经处理的所有单词中该字母的最小次数。每读完一个单词,就对全部 26 个位置执行 minFreq[c] = min(minFreq[c], freq[c])。这样处理完前几个单词后,minFreq 始终就是它们的共同字符频次。

minFreq 先填最大整数,第一次取最小时就会被首个单词的真实频次替换。题目保证至少有一个单词,因此初值不会残留到输出阶段。每个单词都重新建立清零的 freq,没有出现的字母频次为 0,也必须参与比较;只要某个单词缺少这个字母,它就不能出现在共同答案中。

最后逐个字母读取 minFreq,把对应的单字符字符串重复加入答案。按字母顺序生成只是一种方便的输出方式,符合答案顺序任意的要求。

解题步骤

  1. 建立长度为 26 的 minFreq,全部初始化为最大整数。
  2. 对每个单词建立新的零频次表 freq,扫描字符并用 字符 - 'a' 定位计数位置。
  3. 遍历所有 26 个位置,用当前词频更新 minFreq 的逐项最小值,包括频次为 0 的位置。
  4. 所有单词处理完后,将字母 c 对应的字符串加入答案 minFreq[c] 次。次数为 0 的字母不输出。

代码实现

class Solution {
    public List<String> commonChars(String[] words) {
        int[] minFreq = new int[26];

        // 初始大值作为取最小的中性值,首个单词会给出真实频次
        Arrays.fill(minFreq, Integer.MAX_VALUE);

        for (String word : words) {
            // 每个单词独立计数,不能累加前面单词
            int[] freq = new int[26];

            for (int i = 0; i < word.length(); i++) {
                freq[word.charAt(i) - 'a']++;
            }

            for (int i = 0; i < 26; i++) {
                // 未出现字母的零也必须参与最小值合并
                minFreq[i] = Math.min(minFreq[i], freq[i]);
            }
        }

        List<String> answer = new ArrayList<>();

        for (int i = 0; i < 26; i++) {
            for (int count = 0; count < minFreq[i]; count++) {
                answer.add(String.valueOf((char) ('a' + i)));
            }
        }

        return answer;
    }
}
func commonChars(words []string) []string {
    minFreq := [26]int{}
    // 初始大值作为取最小的中性值,首个单词会给出真实频次
    for i := range minFreq {
        minFreq[i] = int(^uint(0) >> 1)
    }

    for _, word := range words {
        // 每个单词独立计数,不能累加前面单词
        freq := [26]int{}
        for i := 0; i < len(word); i++ {
            freq[word[i]-'a']++
        }
        for i := 0; i < 26; i++ {
            // 未出现字母的零也必须参与最小值合并
            if freq[i] < minFreq[i] {
                minFreq[i] = freq[i]
            }
        }
    }

    answer := []string{}
    for i, count := range minFreq {
        for ; count > 0; count-- {
            answer = append(answer, string(byte('a'+i)))
        }
    }
    return answer
}

复杂度分析

设单词数为 m,全部单词的字符总数为 S,答案包含 R 个字符。

  • 时间复杂度:$O(S + 26m + R)$。统计扫描全部字符,每个单词合并 26 项,再输出 R 项。由于每个单词非空、字母表固定,且答案不长于最短单词,可以简化为 $O(S)$。
  • 空间复杂度:$O(1)$ 辅助空间,只保存两个长度为 26 的频次表;返回结果另占 $O(R)$。

关键点总结

[!green]

  • 某个字母的共同份数等于它在所有单词中的最小频次,既不能更多,也一定能取到这么多。
  • 临时频次表只描述一个单词,全局频次表描述已处理单词的交集。
  • 缺失字母的零频次决定该字母不能保留,必须参与合并。
  • 输出每个字符的全部共同份数,不能用普通集合去重。

易错点总结

[!yellow]

  • 将 minFreq 初始化为 0 再取最小值,会使所有结果永远停留在 0。
  • 不清空每个单词的临时频次,会把多个单词的出现次数累加,失去单词之间取交集的含义。
  • 只更新当前单词出现过的字母,会忽略缺失字符对应的零,使答案保留不共用的字符。
  • 每个字母只输出一次,会丢失重复份数;需要按最终最小频次重复输出。
  • 逐位置比较字符或寻找共同子串,会错误地附加题目没有要求的位置和顺序限制。

相似题目

题目 难度 关联与区别
350. 两个数组的交集 II 简单 把两组多重集合交集推广到多个单词,对每个字符取所有词中出现次数的最小值。
383. 赎金信 简单 同样按字符需求次数处理重复,本题找所有来源共同能提供的字符,而不是验证一个固定目标。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/86172247
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!