Анаграммы и карты частот символов
Решайте задачи group-anagrams, valid-anagram и permutation-in-string с помощью массивов частот и хеш-таблиц, получая решения за O(n)
«Анаграммы и карты частот символов» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.
Что такое анаграмма
Две строки являются анаграммами, если содержат одни и те же символы с одинаковыми частотами, но в разном порядке. «ток» и «кот» — анаграммы. Самая простая проверка правильности — отсортировать обе строки и сравнить их: 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 наибольших частот, используя минимальную кучу размера k или Counter.most_common(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. При перемещении окна увеличивайте счётчик входящего символа и уменьшайте счётчик выходящего. Сравнение двух объектов-счётчиков каждый раз занимает 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Карта частот для записки о выкупе
Проверьте, можно ли составить записку из символов журнала, используя каждый символ журнала не более одного раза. Постройте карту частот символов журнала, затем для каждого символа записки уменьшайте соответствующее количество. Если какое-либо количество станет отрицательным, верните ложь. Время работы составляет O(n + m), а для входных данных только со строчными буквами пространственная сложность — O(1), если вместо словаря использовать массив из 26 элементов.
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Памятка по шаблонам карт частот
Распознавайте следующие шаблоны задач на собеседованиях, связанные с картами частот:
- Проверка анаграммы: одинаковая длина + одинаковые частоты → равенство счётчиков или сравнение массивов
- Группировка анаграмм: отсортированная строка или кортеж частот в качестве ключа словаря
- 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 элементов для ограниченных алфавитов или счётчик для произвольных символов, ключи словаря в виде отсортированных строк или кортежей частот объединяют все анаграммы за O(n × m log m) или O(n × m) соответственно, и XOR аккуратно устраняет пары в задачах с одним элементом, имеющим нечётную частоту, обеспечивая время O(n) и пространственную сложность O(1), когда словарь не нужен. Далее мы рассмотрим кодирование строк, разворот и методы работы с палиндромами.
Часто задаваемые вопросы
Урок «Анаграммы и карты частот символов» бесплатный?
Да — полный текст урока «Анаграммы и карты частот символов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Анаграммы и карты частот символов»?
Решайте задачи group-anagrams, valid-anagram и permutation-in-string с помощью массивов частот и хеш-таблиц, получая решения за O(n) Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DSA Interview Prep?
Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Анаграммы и карты частот символов»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DSA Interview Prep?
Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- API строк Python для собеседований
- Скользящее окно для подстрок
- Анаграммы и карты частот символов
- Кодирование строк, разворот и палиндромы