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