Подготовка к алгоритмическим задачам

Модуль collections в Python: Counter, defaultdict, deque и другие
Коротко
| Класс | Зачем |
|---|---|
Counter | подсчёт элементов |
defaultdict | словарь со значением по умолчанию |
deque | очередь с двух концов за O(1) |
namedtuple | кортеж с именованными полями |
OrderedDict | словарь с операциями над порядком |
ChainMap | объединение словарей без копирования |
Модуль collections — стандартная библиотека, ничего устанавливать не нужно. Знание этих шести классов заметно сокращает код в алгоритмических задачах.
Counter: подсчёт за одну строку
Самый полезный класс модуля. Считает, сколько раз встретился каждый элемент.
from collections import Counter counts = Counter('абракадабра') print(counts) # Counter({'а': 5, 'б': 2, 'р': 2, 'к': 1, 'д': 1}) print(counts['а']) # 5 print(counts['я']) # 0 — отсутствующий ключ не вызывает ошибку
Последняя строка важна: Counter возвращает ноль вместо KeyError. Это избавляет от постоянных проверок.
Полезные методы
counts = Counter('абракадабра') counts.most_common(3) # [('а', 5), ('б', 2), ('р', 2)] counts.most_common() # все, по убыванию частоты sum(counts.values()) # 11 — общее количество list(counts.elements()) # развернуть обратно в элементы
most_common(k) внутри использует кучу и работает за O(n log k) — быстрее полной сортировки при малом k.
Арифметика счётчиков
a = Counter('aabbc') b = Counter('abbbd') print(a + b) # сложение частот print(a - b) # вычитание, отрицательные отбрасываются print(a & b) # минимум по каждому ключу (пересечение) print(a | b) # максимум по каждому ключу (объединение)
Операция & решает задачу «сколько общих букв у двух строк» одной строкой.
Типичные применения
undefined
Решай алгоритмические задачи как профи

анаграммы: строки равны как мультимножества
def is_anagram(a: str, b: str) -> bool: return Counter(a) == Counter(b)
самые частые элементы
def top_k(nums: list, k: int) -> list: return [x for x, _ in Counter(nums).most_common(k)]
есть ли дубликаты
def has_duplicates(nums: list) -> bool: return any(c > 1 for c in Counter(nums).values())
print(is_anagram('листок', 'столик')) # True print(top_k([1, 1, 1, 2, 2, 3], 2)) # [1, 2]
## defaultdict: словарь без проверок
Обычный словарь падает при обращении к несуществующему ключу. `defaultdict` вместо этого создаёт значение по умолчанию.
```python
from collections import defaultdict
# было
graph = {}
for u, v in edges:
if u not in graph:
graph[u] = []
graph[u].append(v)
# стало
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
Аргумент — фабрика, то есть функция без аргументов, а не само значение:
defaultdict(list) # [] defaultdict(int) # 0 defaultdict(set) # set() defaultdict(lambda: 'нет') # своё значение
Главная ловушка
Обращение к ключу создаёт его. Даже простое чтение:
d = defaultdict(int) print(d['новый']) # 0 print(d) # defaultdict(<class 'int'>, {'новый': 0}) — ключ появился! print(len(d)) # 1
Если это нежелательно, используйте .get() или обычный словарь. На больших циклах такое «случайное» создание ключей может незаметно съесть память.
Вложенные структуры
# двумерный словарь matrix = defaultdict(lambda: defaultdict(int)) matrix['a']['b'] += 1 # группировка words = ['кот', 'ток', 'дом', 'мод'] groups = defaultdict(list) for word in words: groups[''.join(sorted(word))].append(word) print(dict(groups)) # {'кот': ['кот', 'ток'], 'дмо': ['дом', 'мод']}
Последний пример — готовое решение задачи о группировке анаграмм.
deque: очередь с двух концов
Список в Python медленно работает с началом: pop(0) и insert(0, x) стоят O(n). deque даёт O(1) с обоих концов.
from collections import deque d = deque([1, 2, 3]) d.append(4) # справа d.appendleft(0) # слева d.pop() # справа d.popleft() # слева, O(1) — в отличие от list.pop(0)
Обязателен для BFS:
def bfs(graph: dict, start): visited = {start} queue = deque([start]) while queue: node = queue.popleft() # со списком было бы O(n) for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return visited
Полезный параметр maxlen — автоматическое окно последних N элементов:
last_five = deque(maxlen=5) for i in range(10): last_five.append(i) print(last_five) # deque([5, 6, 7, 8, 9], maxlen=5)
namedtuple: кортеж с именами
Превращает безымянные индексы в читаемые поля.
from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) p = Point(3, 4) print(p.x, p.y) # 3 4 print(p[0]) # 3 — работает и как обычный кортеж # распаковка x, y = p
Занимает столько же памяти, сколько обычный кортеж, но код становится понятнее: p.x вместо p[0].
Полезные методы:
p._replace(x=10) # новый объект (кортежи неизменяемы) p._asdict() # {'x': 3, 'y': 4} Point._fields # ('x', 'y')
В современном коде часто предпочитают dataclass или typing.NamedTuple — они поддерживают аннотации типов:
from typing import NamedTuple class Point(NamedTuple): x: int y: int
OrderedDict: когда обычного словаря мало
С Python 3.7 обычные словари сохраняют порядок вставки, поэтому OrderedDict нужен реже. Но у него остались уникальные возможности.
from collections import OrderedDict od = OrderedDict([('a', 1), ('b', 2), ('c', 3)]) od.move_to_end('a') # переместить в конец od.move_to_end('c', last=False) # переместить в начало od.popitem(last=False) # удалить первый (FIFO)
Это делает OrderedDict готовой основой для LRU-кэша:
class LRUCache: def __init__(self, capacity: int) -> None: self.capacity = capacity self.cache = OrderedDict() def get(self, key): if key not in self.cache: return -1 self.cache.move_to_end(key) # освежаем return self.cache[key] def put(self, key, value) -> None: if key in self.cache: self.cache.move_to_end(key) elif len(self.cache) == self.capacity: self.cache.popitem(last=False) # удаляем самый давний self.cache[key] = value
Ещё отличие: OrderedDict учитывает порядок при сравнении, обычный словарь — нет.
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1) # False dict(a=1, b=2) == dict(b=2, a=1) # True
ChainMap: словари без копирования
Объединяет несколько словарей в один вид без создания копии.
from collections import ChainMap defaults = {'theme': 'light', 'lang': 'ru'} user = {'theme': 'dark'} config = ChainMap(user, defaults) print(config['theme']) # dark — первый словарь важнее print(config['lang']) # ru — взято из defaults
Отличие от {**defaults, **user} — данные не копируются. Изменения в исходных словарях сразу видны через ChainMap, а память не удваивается. Удобно для многоуровневых конфигураций: аргументы командной строки, переменные окружения, файл настроек, значения по умолчанию.
Что чем заменить
| Задача | Плохо | Хорошо |
|---|---|---|
| Подсчёт элементов | ручной словарь с проверками | Counter |
| Группировка | if key not in d: d[key] = [] | defaultdict(list) |
| Очередь | list.pop(0) | deque.popleft() |
| Точка, запись | кортеж с индексами | namedtuple |
| LRU-кэш | ручной список | OrderedDict |
| Слияние настроек | {**a, **b} | ChainMap |
Частые ошибки
defaultdict создаёт ключи при чтении. Проверяйте наличие через in или .get(), если это критично.
Передача значения вместо фабрики. defaultdict([]) вызовет ошибку — нужен defaultdict(list), без скобок у аргумента.
Counter с нечисловыми значениями. Арифметика работает только с числами; попытка сложить счётчики строк даст неожиданный результат.
Использование списка как очереди. Самая дорогая ошибка: скрытый квадрат вместо линии.
Одна изменяемая фабрика на всех. defaultdict(lambda: some_list) вернёт один и тот же список для всех ключей. Нужна фабрика, создающая новый объект: defaultdict(list).
Что запомнить
Counterсчитает элементы и поддерживает арифметику множеств.defaultdictизбавляет от проверок наличия ключа, но создаёт ключи при чтении.deque— единственный правильный выбор для очереди в Python.namedtupleделает кортежи читаемыми без затрат памяти.OrderedDictнужен радиmove_to_endиpopitem(last=False)— основа LRU.ChainMapобъединяет словари без копирования данных.
