频率统计与分组
使用 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)) # 4Counter 的算术运算与交集
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 反馈 — 无需本地设置。