0Pricing
DSA Interview Prep · 课时

频率统计与分组

使用 Counter 和 defaultdict 统计字符频率,按排序后的键将字母异位词分组,并找出出现频率最高的 k 个元素。

频率统计与分组 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。

频率统计:核心模式

频率统计是编程面试中最灵活多用的模式之一。通过统计列表或字符串中每个元素出现的次数,您可以在 O(n) 时间内处理重复项、字母异位词、出现频率最高的元素以及有效排列等问题——这比 O(n log n) 的排序后扫描方法高效得多。

Python 的 Counter 和 defaultdict(int) 是标准工具。两者都会建立从元素到计数的映射;Counter 还额外支持算术运算和 most_common。

from collections import Counter

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
freq  = Counter(words)
print(freq)                  # Counter({'apple':3,'banana':2,'cherry':1})
print(freq['apple'])         # 3
print(freq['grape'])         # 0 (not KeyError)
print(freq.most_common(2))   # [('apple',3),('banana',2)]

有效的字母异位词(LeetCode 242)

LeetCode 242“有效的字母异位词”:判断两个字符串是否互为字母异位词。如果两个字符串中每个字符的出现频率都相同,它们就是字母异位词。您可以比较它们的 Counter 对象,或者分别对两个字符串进行排序。使用 Counter 的复杂度为 O(n),排序的复杂度为 O(n log n)。Counter 方法是最优的,并且直接体现了定义。

from collections import Counter

def isAnagram(s, t):
    return Counter(s) == Counter(t)

# Alternative: manual frequency array for lowercase letters only
def isAnagram_arr(s, t):
    if len(s) != len(t):
        return False
    freq = [0] * 26
    for c in s: freq[ord(c) - ord('a')] += 1
    for c in t: freq[ord(c) - ord('a')] -= 1
    return all(f == 0 for f in freq)

print(isAnagram('anagram', 'nagaram'))  # True
print(isAnagram('rat', 'car'))          # False
print(isAnagram_arr('listen', 'silent'))  # True

字母异位词分组(LeetCode 49)

LeetCode 49“字母异位词分组”:给定一个字符串列表,将所有字母异位词归为一组。关键思路是:字母异位词具有相同的排序后字符序列。使用以字符串排序后的元组为键的 defaultdict(list)(元组是可哈希的)。每个分组都会累积到同一个键下。时间复杂度:O(n × L log L),其中 L 是最长字符串的长度。

from collections import defaultdict

def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(sorted(s))   # hashable canonical form
        groups[key].append(s)
    return list(groups.values())

print(groupAnagrams(['eat','tea','tan','ate','nat','bat']))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]

# Alternative key: tuple of 26 character counts (O(L) not O(L log L))
def groupAnagrams_v2(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(ord(c) - ord('a') for c in sorted(s))
        groups[tuple(Counter(s)[chr(ord('a')+i)] for i in range(26))].append(s)
    return list(groups.values())

出现频率最高的 K 个元素(LeetCode 347)

LeetCode 347“出现频率最高的 K 个元素”:返回出现频率最高的 k 个元素。直接方法的复杂度为 O(n log n):统计频率,按计数降序排序,然后取前 k 个。最优的 O(n) 方法使用桶排序:创建以频率为索引的桶(从 1 到 n),将每个元素放入对应频率的桶中,然后从最高频率到最低频率扫描这些桶,收集 k 个元素。

from collections import Counter

def topKFrequent(nums, k):
    freq  = Counter(nums)
    # Bucket sort by frequency
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, count in freq.items():
        buckets[count].append(num)
    result = []
    for i in range(len(buckets) - 1, -1, -1):
        result.extend(buckets[i])
        if len(result) >= k:
            return result[:k]
    return result

print(topKFrequent([1,1,1,2,2,3], 2))  # [1, 2]
print(topKFrequent([1], 1))             # [1]

按频率排序字符(LeetCode 451)

LeetCode 451“按频率排序字符”:重新排列字符串,使字符按频率降序排列。统计频率,按频率降序排列字符,然后将它们连接起来。使用 most_common 是最简洁的 Python 方法。时间复杂度:按频率对不同字符进行排序需要 O(n log n)。

from collections import Counter

def frequencySort(s):
    freq = Counter(s)
    return ''.join(ch * count for ch, count in freq.most_common())

print(frequencySort('tree'))    # 'eetr' or 'eert'
print(frequencySort('cccaaa'))  # 'cccaaa' or 'aaaccc'
print(frequencySort('Aabb'))    # 'bbAa' or 'bbaA'

任务调度器(LeetCode 621)

LeetCode 621“任务调度器”:给定任务和冷却时间 n,求完成所有任务所需的最短时间。关键洞察是:出现频率最高的任务决定了整体结构。将出现频率最高的任务排列为 max_count 个副本,并在它们之间设置 (n) 个间隔。最短总时间 = max((max_count - 1) * (n + 1) + 出现次数达到最高频率的任务数, 任务总数)。如果有足够多的不同任务来填充这些间隔,空闲时间就是 0。

from collections import Counter

def leastInterval(tasks, n):
    freq      = Counter(tasks)
    max_count = max(freq.values())
    # How many tasks share the max frequency
    num_max   = sum(1 for v in freq.values() if v == max_count)
    # Minimum slots needed based on most frequent task
    min_slots = (max_count - 1) * (n + 1) + num_max
    return max(min_slots, len(tasks))

print(leastInterval(['A','A','A','B','B','B'], 2))  # 8
print(leastInterval(['A','A','A','B','B','B'], 0))  # 6
print(leastInterval(['A','A','A','A','B','B','B','C','C','D'], 2))  # 10

使用 Counter 进行多数投票

LeetCode 169“多数元素”:找出出现次数超过 n/2 的元素。虽然 Boyer-Moore 投票算法是 O(1) 空间复杂度的最优解,但使用 Counter.most_common(1) 可以直接在 O(n) 时间和 O(n) 空间内解决问题。对于明确要求 O(1) 空间的面试,请将 Boyer-Moore 作为后续方案;对于允许使用额外空间的面试,Counter 更简洁。

from collections import Counter

def majorityElement_counter(nums):
    freq = Counter(nums)
    return freq.most_common(1)[0][0]

# Boyer-Moore O(1) space
def majorityElement_moore(nums):
    candidate, count = None, 0
    for num in nums:
        if count == 0:
            candidate = num
        count += (1 if num == candidate else -1)
    return candidate

nums = [2, 2, 1, 1, 2, 2, 2]
print(majorityElement_counter(nums))  # 2
print(majorityElement_moore(nums))    # 2

第一个不重复字符

LeetCode 387“字符串中的第一个唯一字符”:找出第一个恰好出现一次的字符的索引。两遍扫描方法:第一遍建立频率计数;第二遍找出计数为 1 的第一个字符。时间复杂度:O(n);空间复杂度:O(1),因为字母表固定为 26 个字符。

from collections import Counter

def firstUniqChar(s):
    freq = Counter(s)
    for i, ch in enumerate(s):
        if freq[ch] == 1:
            return i
    return -1

print(firstUniqChar('leetcode'))   # 0 (l)
print(firstUniqChar('loveleetcode'))  # 2 (v)
print(firstUniqChar('aabb'))       # -1

和为 K 的子数组(LeetCode 560)

LeetCode 560“和为 K 的子数组”:统计元素和为 k 的子数组数量。暴力方法的复杂度为 O(n²)。O(n) 方法是:维护一个不断累加的前缀和,以及一个记录此前出现过的前缀和频率的映射。对于每个位置 i,以 i 结尾且和为 k 的子数组数量,等于此前前缀和中等于(当前前缀和 - k)的数量。使用 {0: 1} 初始化该映射,以处理从索引 0 开始的子数组。

from collections import defaultdict

def subarraySum(nums, k):
    freq         = defaultdict(int)
    freq[0]      = 1   # prefix sum of 0 seen once (empty prefix)
    prefix_sum   = 0
    count        = 0
    for num in nums:
        prefix_sum += num
        # How many earlier prefix sums allow a k-sum subarray ending here
        count      += freq[prefix_sum - k]
        freq[prefix_sum] += 1
    return count

print(subarraySum([1, 1, 1], 2))            # 2
print(subarraySum([1, 2, 3], 3))            # 2
print(subarraySum([1, -1, 1, -1, 1], 0))   # 4

Counter 的算术运算与交集

Counter 支持算术运算:+ 合并(相加计数),- 相减(结果截断为 0),& 取最小值(交集),| 取最大值(并集)。这些运算可以简化诸如“找出多个字符串中的公共字符”或“删除最少字符,使一个字符串成为另一个字符串的字母异位词”等问题。

from collections import Counter

A = Counter('abccdd')
B = Counter('ccdde')

print('Add:      ', dict(A + B))  # sum of counts
print('Subtract: ', dict(A - B))  # A - B, clipped at 0
print('Intersect:', dict(A & B))  # min of shared counts
print('Union:    ', dict(A | B))  # max counts

# Min steps to make s anagram of t (LeetCode 1347)
s, t = 'leetcode', 'practice'
diff = Counter(t) - Counter(s)
print('Chars to add:', sum(diff.values()))  # 5

总结:何时使用频率统计

当问题涉及以下情况时,可以考虑使用频率统计:判断两个字符串经过重新排列后是否等价(字母异位词)、找出出现频率最高或最低的元素、验证集合是否包含正确的“材料”,或者将子数组或子字符串问题转化为“前缀和加映射”的问题。关键在于:组内元素的顺序并不重要,重要的只有计数。

为保证清晰性,请始终使用 Counter;只有在需要更精细的控制,或字母表有界且严格要求 O(1) 空间时,才切换为普通的 dict 或数组。

快速检查

请测试您对本课数据结构与算法 — 编程面试准备相关概念的理解。

课程回顾

本课您学到了:Counter 通过 most_common、算术运算符和默认为零的访问提供 O(n) 频率统计,按规范形式(排序后的元组)分组可以在 O(nL log L) 内解决字母异位词分组问题,以及带频率映射的前缀和可以将和为 k 的子数组问题从 O(n²) 降至 O(n)。接下来我们将学习最长连续序列问题和 LRU 缓存设计。

常见问题解答

「频率统计与分组」课时是免费的吗?

是的 — 「频率统计与分组」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。

「频率统计与分组」这节课中我会学到什么?

使用 Counter 和 defaultdict 统计字符频率,按排序后的键将字母异位词分组,并找出出现频率最高的 k 个元素。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DSA Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「频率统计与分组」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DSA Interview Prep 课中编写并运行代码吗?

能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 哈希函数原理与冲突处理
  2. 两数之和及其多种变体
  3. 频率统计与分组
  4. 最长连续序列与 LRU Cache
← 返回 DSA Interview Prep