0Pricing
DSA Interview Prep · 课时

字母异位词与字符频率映射

使用频率数组和哈希映射,以 O(n) 的复杂度解决字母异位词分组、有效字母异位词和字符串中的排列问题。

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

什么是异位词

如果两个字符串包含相同的字符,且每个字符的频率也相同,只是排列顺序不同,那么它们就是异位词。'listen' 和 'silent' 是异位词。最简单的正确性检查方法是分别对两个字符串排序并进行比较,时间复杂度为 O(n log n)。若要达到 O(n),请比较字符频率映射。异位词问题是字符串面试中的常见题型,因为它可以检验多种技巧:哈希、排序和频率数组。

def is_anagram_sort(s, t):
    return sorted(s) == sorted(t)  # O(n log n)

def is_anagram_counter(s, t):
    from collections import Counter
    return Counter(s) == Counter(t)  # O(n)

def is_anagram_array(s, t):
    if len(s) != len(t): return False
    freq = [0] * 26
    for a, b in zip(s, t):
        freq[ord(a) - ord('a')] += 1
        freq[ord(b) - ord('a')] -= 1
    return all(f == 0 for f in freq)  # O(n)

print(is_anagram_array('anagram', 'nagaram'))  # True
print(is_anagram_array('rat', 'car'))           # False

小写字母的频率数组

当字符集有界时(例如只有小写字母 a-z),可以使用大小为 26 的频率数组替代哈希映射。通过 ord(c) - ord('a') 进行索引,可以将 'a'→0、'b'→1、……、'z'→25 映射到对应位置。由于具有更好的缓存局部性且没有哈希开销,数组在实际运行中通常比字典更快。这一技巧会出现在有效异位词、字符串中的异位词排列和回文排列问题中。

def build_freq(s):
    freq = [0] * 26
    for c in s:
        freq[ord(c) - ord('a')] += 1
    return freq

def is_anagram_fast(s, t):
    return len(s) == len(t) and build_freq(s) == build_freq(t)

# Palindrome permutation: at most one odd-count character
def can_form_palindrome(s):
    freq = build_freq(s)
    odd_count = sum(1 for f in freq if f % 2 == 1)
    return odd_count <= 1

print(can_form_palindrome('carerace'))  # True ('racecar')
print(can_form_palindrome('hello'))     # False

异位词分组

请将字符串列表分组,使所有异位词彼此相邻。标准的 O(n×m log m) 解法使用排序后的字符串作为哈希映射的键。所有异位词都会生成相同的排序键,因此会进入同一个桶。O(n×m) 的变体使用字符计数元组作为键——计算速度较慢,但完全避免了排序。出于清晰性的考虑,几乎总是优先选择排序键方法。

from collections import defaultdict

def group_anagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(sorted(s))  # or ''.join(sorted(s))
        groups[key].append(s)
    return list(groups.values())

words = ['eat','tea','tan','ate','nat','bat']
result = group_anagrams(words)
for g in sorted(result, key=len, reverse=True):
    print(sorted(g))
# ['ate', 'eat', 'tea']
# ['nat', 'tan']
# ['bat']

使用计数元组构建异位词键

对于 O(n×m) 的异位词分组变体,请将每个字符串的频率表示为包含 26 个计数值的元组:tuple(freq_array)。这种方法避免了排序,但构建所有键需要 O(26×n×m) 的工作量。元组在 Python 中支持哈希,因此可以作为有效的字典键。当面试官要求“任意一种 O(n×m) 解法”时,值得提到这种变体——它体现了您对不同取舍的理解。

from collections import defaultdict

def group_anagrams_count(strs):
    groups = defaultdict(list)
    for s in strs:
        freq = [0] * 26
        for c in s:
            freq[ord(c) - ord('a')] += 1
        key = tuple(freq)  # tuple is hashable
        groups[key].append(s)
    return list(groups.values())

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

出现频率最高的前 K 个元素

请找出数组中出现频率最高的 k 个元素。计数器 + 堆:在 O(n) 的时间内构建频率映射,然后使用大小为 k 的最小堆,或使用 Counter.most_common(k),提取出现频率最高的 k 个元素。O(n) 的桶排序方法会创建按频率索引的桶(从 0 到 n),然后按频率从高到低收集元素——当 k 较大时,这种方法十分简洁。

from collections import Counter
import heapq

def top_k_frequent_heap(nums, k):
    freq = Counter(nums)
    return heapq.nlargest(k, freq, key=freq.get)

def top_k_frequent_bucket(nums, k):
    freq = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, cnt in freq.items():
        buckets[cnt].append(num)
    result = []
    for i in range(len(buckets)-1, -1, -1):
        result.extend(buckets[i])
        if len(result) >= k: break
    return result[:k]

print(top_k_frequent_heap([1,1,1,2,2,3], 2))   # [1, 2]
print(top_k_frequent_bucket([1,1,1,2,2,3], 2)) # [1, 2]

字符串排列的频率映射

请判断字符串 p 的任意排列是否为 s 的子字符串。长度为 |p| 的窗口的频率映射必须与 p 的频率映射相等。窗口滑动时,增加进入字符的计数,并减少离开字符的计数。每次比较两个 Counter 对象需要 O(26) 的时间,因此总体复杂度为 O(n×26) = O(n)。请跟踪“已满足”计数器,以便在 O(1) 的时间内检查是否相等。

def check_inclusion_fast(p, s):
    if len(p) > len(s): return False
    need = [0] * 26
    have = [0] * 26
    for c in p:
        need[ord(c)-ord('a')] += 1
    for i in range(len(p)):
        have[ord(s[i])-ord('a')] += 1
    if need == have: return True
    for i in range(len(p), len(s)):
        have[ord(s[i])-ord('a')]         += 1
        have[ord(s[i-len(p)])-ord('a')] -= 1
        if need == have: return True
    return False

print(check_inclusion_fast('ab', 'eidbaooo'))  # True
print(check_inclusion_fast('ab', 'eidboaoo'))  # False

使字符串成为异位词所需的最少字符数

给定两个字符串,请找出需要删除的最少字符数,使其中一个字符串成为另一个字符串的异位词。请分别计算两个字符串的频率映射;答案是各字符频率绝对差之和。只出现在其中一个字符串中的字符必须全部删除。这个 O(n) 的解法在频率映射上使用了“合并与差异”模式。

from collections import Counter

def min_steps_to_anagram(s, t):
    freq_s = Counter(s)
    freq_t = Counter(t)
    steps = 0
    # For each unique char across both strings:
    all_chars = set(freq_s) | set(freq_t)
    for c in all_chars:
        steps += abs(freq_s.get(c, 0) - freq_t.get(c, 0))
    return steps

# Or more concisely:
def min_steps_counter(s, t):
    diff = Counter(s) - Counter(t)
    return sum(diff.values())

print(min_steps_to_anagram('leetcode', 'practice'))  # 5
print(min_steps_counter('leetcode', 'practice'))      # 5

赎金信的频率映射

请检查 note 中的所有字符是否都可以由 magazine 中的字符提供(每个杂志字符只能使用一次)。先构建杂志字符的频率映射;然后遍历 note 中的每个字符并减少其计数。如果任何计数变为负数,则返回假值。对于限定为小写字母的输入,使用包含 26 个元素的数组而不是字典,可以在 O(n + m) 的时间和 O(1) 的空间内完成。

def can_construct(note, magazine):
    freq = [0] * 26
    for c in magazine:
        freq[ord(c) - ord('a')] += 1
    for c in note:
        freq[ord(c) - ord('a')] -= 1
        if freq[ord(c) - ord('a')] < 0:
            return False  # insufficient supply
    return True

print(can_construct('aa', 'aab'))    # True
print(can_construct('aa', 'ab'))     # False
print(can_construct('bg', 'efjbdfbdgbjjbghiklgdch'))  # True

最长异位词子字符串哈希

要检查同一字符串中的两个子字符串是否为异位词,请使用字符频率的多项式哈希,这种哈希具有交换性(与顺序无关)。XOR 具有交换性,并且可以在 O(1) 的时间内更新,但发生冲突的概率较高。更好的方法是使用质数乘积哈希(将每个字符映射到不同的质数;乘积与顺序无关)。这是一种适合高级面试的冷门技巧。

# Prime product hash: each char maps to a prime
PRIMES = [2,3,5,7,11,13,17,19,23,29,31,37,41,
          43,47,53,59,61,67,71,73,79,83,89,97,101]

def char_hash(s):
    h = 1
    for c in s:
        h *= PRIMES[ord(c) - ord('a')]
    return h

# Two windows with equal hash are likely anagrams
print(char_hash('listen'))  # same as:
print(char_hash('silent'))  # should match

频率映射模式检查清单

请认识以下频率映射面试模式:

  • 有效异位词:长度相同且频率相同 → 比较 Counter 是否相等,或比较数组
  • 异位词分组:使用排序后的字符串或频率元组作为字典键
  • 出现频率最高的前 k 个:计数器 + 堆,或桶排序
  • 字符串中的排列:滑动窗口 + 频率比较
  • 赎金信:建立供给字符的频率映射,再根据需求递减计数
  • 回文排列:最多只能有一个字符出现奇数次
所有这些问题最终都归结为同一个核心思想:将频率作为指纹。

from collections import Counter

# Palindrome permutation
def palindrome_permutation(s):
    return sum(v % 2 for v in Counter(s).values()) <= 1

# First unique character
def first_unique(s):
    freq = Counter(s)
    for i, c in enumerate(s):
        if freq[c] == 1:
            return i
    return -1

# Character replacement for longest repeat
def char_replacement(s, k):
    freq = Counter()
    left = best = max_freq = 0
    for right, c in enumerate(s):
        freq[c] += 1
        max_freq = max(max_freq, freq[c])
        if (right - left + 1) - max_freq > k:
            freq[s[left]] -= 1
            left += 1
        best = max(best, right - left + 1)
    return best

print(palindrome_permutation('carerace'))  # True
print(first_unique('leetcode'))             # 0
print(char_replacement('AABABBA', 1))      # 4

找出异数:使用 XOR 处理频率

当恰好有一个元素出现奇数次时,XOR 是处理频率问题的强大工具。一个数与自身进行 XOR 会抵消为 0:a XOR a = 0。将所有元素进行 XOR,若除一个值外的每个值都出现偶数次,最终结果就只会剩下那个出现奇数次的值。这样可以在 O(n) 的时间和 O(1) 的空间内完成,无需哈希映射。利用 XOR 的性质,还可以推广到查找两个出现奇数次的数字。

def single_number(nums):
    result = 0
    for n in nums:
        result ^= n  # XOR cancels pairs
    return result

print(single_number([4,1,2,1,2]))   # 4
print(single_number([2,2,1]))       # 1

# Find the unique character in an anagram check:
def find_difference(s, t):
    result = 0
    for c in s + t:
        result ^= ord(c)
    return chr(result)

print(find_difference('abcd', 'abcde'))  # 'e'

快速检查

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

课程回顾

本课您学习了:字符频率映射是检测异位词的核心工具——对于有界字符集,可以使用包含 26 个元素的数组;对于任意字符,则可以使用 Counter;使用排序字符串或频率元组作为字典键,可以分别以 O(n × m log m) 或 O(n × m) 的时间将所有异位词分组;以及在单元素奇数次计数问题中,XOR 可以干净地消除成对元素;当不需要字典时,它能以 O(n) 的时间和 O(1) 的空间完成计算。接下来我们将学习字符串编码、反转和回文技巧。

常见问题解答

「字母异位词与字符频率映射」课时是免费的吗?

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

「字母异位词与字符频率映射」这节课中我会学到什么?

使用频率数组和哈希映射,以 O(n) 的复杂度解决字母异位词分组、有效字母异位词和字符串中的排列问题。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

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

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

「字母异位词与字符频率映射」课时需要多长时间?

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

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

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

此课程中的所有课时

  1. 面试必备的 Python 字符串 API
  2. 子串的滑动窗口
  3. 字母异位词与字符频率映射
  4. 字符串编码、反转与回文
← 返回 DSA Interview Prep