Алгоритмы на Go — паттерн важнее зубрёжки
Алгоритмическая задача на собеседовании редко требует придумать что-то новое. Почти всегда за ней стоит узнаваемый паттерн: два указателя для прохода с двух концов, стек для сопоставления вложенности, хеш-таблица для O(1)-поиска вместо вложенного цикла, индекс записи для фильтрации на месте. Сильный кандидат не вспоминает решение, а узнаёт паттерн и сразу называет сложность.
Go добавляет к этому свою специфику. Стек моделируется срезом: push это append, pop это reslice. Множество — это map[T]struct{} с нулевым по размеру значением. Разворот строки требует перехода к []rune, иначе многобайтовые UTF-8 символы развалятся. А оценка сложности обязана различать настоящий O(1) и amortized O(1) у append, который иногда реаллоцирует backing array. Эта тема разбирает алгоритмические приёмы по слоям — каждый со своей идиомой на Go и честной оценкой времени и памяти.
Карта темы
- Два указателя — два индекса навстречу или с фиксированным разрывом дают O(n) время и O(1) память на проходе по последовательности.
- Стек (LIFO) — последний вошёл — первый вышел; в Go это срез с
appendи reslice, основа сопоставления и обхода. - Проверка скобок — стек хранит ожидаемые закрывающие скобки; несовпадение или пустой стек на закрытии — строка невалидна.
- Хеш-таблица для поиска —
mapдаёт O(1)-доступ по ключу и сворачивает вложенный цикл O(n²) до одного прохода O(n). - Множество и дедупликация —
map[T]struct{}как множество для проверки принадлежности и удаления дубликатов за O(1). - Слияние интервалов — сортировка по началу плюс один проход, сливающий пересечения, даёт O(n log n).
- Компакция на месте — индекс записи
jи индекс чтенияiфильтруют срез без новой аллокации за O(n) и O(1) памяти. - Срез-zip — попарное объединение двух срезов до меньшей длины с преаллокацией результата.
- LRU-кэш — хеш-таблица плюс двусвязный список дают O(1) на
get,putи вытеснение. - Динамическое программирование — перекрывающиеся подзадачи и оптимальная подструктура; скользящее окно сводит память к O(1).
- Полный перебор — исчерпывающее перечисление пространства кандидатов, когда структуры для срезания нет; цена экспоненциальная, её нужно ограничивать.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Решать вложенным циклом то, что решает хеш-таблица | O(n²) вместо O(n) — частая причина TLE на собеседовании |
Считать append/pop через срез настоящим O(1) | Это amortized O(1); рост backing array иногда реаллоцирует |
| Разворачивать строку по байтам | Многобайтовые UTF-8 символы разваливаются — нужен []rune |
| Фильтровать срез, создавая новый | Лишняя аллокация там, где хватает индекса записи на месте |
Путать множество map[T]bool и map[T]struct{} | struct{}{} не занимает памяти под значение — идиоматичнее для set |
| Сливать интервалы без сортировки | Пропущенные пересечения — слияние требует упорядоченного входа |
| Брать полный перебор там, где есть структура | Экспоненциальная цена вместо полиномиальной |
| Хранить в LRU только map без списка | Нет O(1)-вытеснения самого старого элемента |
Значение для собеседований
Алгоритмическая секция есть почти на каждом Go-интервью. Проверяют не знание конкретной задачи, а способность распознать паттерн, реализовать его идиоматично на Go и оценить сложность.
Что обычно проверяют:
- Узнаёте ли вы приём «два указателя» и когда он даёт O(n)/O(1).
- Как смоделировать стек срезом и где LIFO — естественная структура (скобки, обход).
- Когда хеш-таблица или множество сворачивают квадрат до линии.
- Как сделать фильтрацию или разворот на месте, без лишней аллокации.
- Чем
get/putLRU-кэша достигают O(1) и зачем там двусвязный список. - Что такое перекрывающиеся подзадачи и как скользящее окно убирает лишнюю память.
Типичный неверный ответ: «pop со среза — это O(1), значит стек всегда O(1)». Это запускает разбор того, что reslice действительно O(1), но push через append — amortized O(1): при исчерпании cap он выделяет новый backing array и копирует элементы.