В большинстве компаний собеседование на позицию инженера-программиста устроено одинаково: звонок рекрутера, один или два раунда по кодингу, раунд по проектированию систем для middle и senior позиций и поведенческий раунд. Ниже приведены 20 вопросов с ответами, сгруппированных по раундам. Перед публикацией каждый пример кода проверяли против версии с полным перебором на тысячах случайных входных данных.
Что проверяет каждый раунд
| Этап | Обычный формат | Что оценивают |
|---|---|---|
| Скрининг с рекрутером | от 20 до 30 минут по телефону | Опыт, мотивация, вилка зарплаты, организационные вопросы |
| Кодинг | от 45 до 60 минут в общем редакторе | Решение задач, чистый код, тестирование, разговор во время работы |
| Основы информатики | Вплетены в кодинг или отдельный раунд | Понимаете ли вы системы, которыми пользуетесь |
| Проектирование систем | от 45 до 60 минут у доски или в документе | Компромиссы, масштаб и ясность изложения |
| Поведенческий раунд | от 30 до 45 минут | Ответственность, командная работа, как вы справляетесь с провалами |
Собеседования для выпускников в основном про кодинг и поведенческие вопросы. На старших позициях добавляется проектирование систем, и там ждут более глубоких ответов о прошлых проектах. Про звонок рекрутера читайте в нашем материале вопросы на телефонном скрининге.
Вопросы по кодингу
В каждом раунде по кодингу сначала проговаривайте подход, до того как начнете печатать, называйте сложность по времени и по памяти и в конце проверьте решение на небольшом примере. Часть про сложность разобрана в нашей шпаргалке по Big O.
1. Two Sum: вернуть индексы двух чисел, сумма которых равна целевому значению
Сохраняйте индекс каждого числа в хеш-таблице. Для каждого нового числа проверяйте, есть ли в таблице его дополнение до целевого значения.
def two_sum(nums, target):
seen = {}
for i, x in enumerate(nums):
if target - x in seen:
return [seen[target - x], i]
seen[x] = i
return []
print(two_sum([2, 7, 11, 15], 9)) # [0, 1]
O(n) по времени и O(n) по памяти. Вариант с полным перебором проверяет каждую пару за O(n²). Интервьюеры обычно хотят, чтобы вы сначала назвали его, а потом улучшили.
2. Валидные скобки: проверить, что каждая скобка закрыта в правильном порядке
Кладите открывающие скобки в стек. Встретив закрывающую скобку, проверьте, что на вершине стека лежит её парная открывающая.
def is_valid(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
else:
stack.append(ch)
return not stack
print(is_valid("([]{})"), is_valid("(]")) # True False
O(n) по времени и O(n) по памяти. Важна последняя строка: например, строка «((» оставляет элементы в стеке, и она не валидна.
3. Объединить перекрывающиеся интервалы
Отсортируйте интервалы по времени начала, затем либо расширьте последний объединённый интервал, либо начните новый.
def merge_intervals(intervals):
merged = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
print(merge_intervals([[1, 3], [2, 6], [8, 10], [15, 18]]))
# [[1, 6], [8, 10], [15, 18]]
O(n log n) по времени из-за сортировки. Уточните у интервьюера, нужно ли объединять соприкасающиеся интервалы, например [1, 2] и [2, 3]. Эта версия объединяет их.
4. Развернуть связный список
Пройдите список один раз и направьте ссылку каждого узла на предыдущий.
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
def reverse_list(head):
prev = None
while head:
head.next, prev, head = prev, head, head.next
return prev
O(n) по времени и O(1) дополнительной памяти. Если множественное присваивание покажется интервьюеру слишком хитрым, запишите его тремя строками с временной переменной. Понятно важнее, чем коротко.
5. Вернуть k самых частых элементов
Подсчитайте частоты через хеш-таблицу, затем возьмите k наибольших значений с помощью кучи.
from collections import Counter
import heapq
def top_k_frequent(nums, k):
counts = Counter(nums)
return [x for x, _ in heapq.nlargest(k, counts.items(), key=lambda p: p[1])]
print(top_k_frequent([1, 1, 1, 2, 2, 3], 2)) # [1, 2]
O(n log k) по времени. Частый уточняющий вопрос: можно ли обойтись сортировкой по корзинам (bucket sort) по частоте, что даёт O(n).
6. Спроектировать LRU-кеш с O(1) на get и put
Нужны быстрый поиск и быстрая перестановка по времени последнего использования. Хеш-таблица плюс двусвязный список дают и то и другое. В Python OrderedDict как раз и есть такая комбинация.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key)
return self.data[key]
def put(self, key, value):
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False)
Некоторые интервьюеры попросят реализовать связный список самостоятельно. Сначала объясните устройство: таблица указывает на узлы списка, самый недавний узел стоит в одном конце, а вытеснение происходит с другого конца.
7. Кратчайший путь в сетке со стенами
Поиск в ширину находит кратчайший путь, когда каждый шаг стоит одинаково.
from collections import deque
def shortest_path(grid):
rows, cols = len(grid), len(grid[0])
if grid[0][0] or grid[rows - 1][cols - 1]:
return -1
queue = deque([(0, 0, 0)])
seen = {(0, 0)}
while queue:
r, c, steps = queue.popleft()
if (r, c) == (rows - 1, cols - 1):
return steps
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and not grid[nr][nc] and (nr, nc) not in seen:
seen.add((nr, nc))
queue.append((nr, nc, steps + 1))
return -1
print(shortest_path([[0, 0, 0], [1, 1, 0], [0, 0, 0]])) # 4
O(rows × cols) по времени и по памяти. Отмечайте клетки как посещённые в момент добавления в очередь, а не при извлечении, иначе одна и та же клетка попадёт в очередь много раз.
8. Найти первую плохую версию
Версии с 1 по n сначала хорошие, а начиная с некоторого момента плохие. Бинарный поиск находит первую плохую версию за O(log n) проверок.
def first_bad_version(n, is_bad):
lo, hi = 1, n
while lo < hi:
mid = (lo + hi) // 2
if is_bad(mid):
hi = mid
else:
lo = mid + 1
return lo
Этот паттерн подходит для любого вопроса «да или нет», который меняет ответ один раз, а такие вопросы встречаются часто. Список задач, покрывающих эти паттерны, см. в материале Blind 75.
Вопросы по основам информатики
9. В чём разница между процессом и потоком?
Процесс представляет собой запущенную программу со своим адресным пространством памяти. Потоки работают внутри процесса и делят его память. Потоки дешевле создавать и переключать, и они могут напрямую обмениваться данными, но именно из-за этого обмена нужны блокировки и возникают гонки. Процессы изолированы, поэтому падение одного не затрагивает остальные.
10. В чём разница между TCP и UDP?
TCP устанавливает соединение и гарантирует упорядоченную и надёжную доставку с повторной отправкой и управлением потоком. UDP отправляет независимые пакеты без гарантии доставки и порядка, зато с меньшими накладными расходами. TCP подходит для веб-страниц, API и передачи файлов. UDP подходит для живого голоса, видео и игр, где опоздавший пакет всё равно бесполезен. Подробнее читайте в нашем материале вопросы по компьютерным сетям на собеседовании.
11. Что происходит, когда вы вводите URL в браузере и нажимаете Enter?
Браузер по DNS получает IP-адрес для домена, открывает TCP-соединение и для HTTPS выполняет рукопожатие TLS. Затем он отправляет HTTP-запрос, а сервер, часто стоящий за балансировщиком нагрузки, возвращает ответ. Браузер разбирает HTML, запрашивает CSS, JavaScript и изображения, на которые тот ссылается, и отрисовывает страницу. Углубляйтесь в ту часть, которую выберет интервьюер.
12. Как устроена хеш-таблица и какова её временная сложность?
Хеш-функция сопоставляет каждый ключ с корзиной в массиве. Поиск, вставка и удаление в среднем занимают O(1). Когда два ключа попадают в одну корзину, таблица разрешает коллизию цепочками или пробированием другой ячейки, а когда заполняется слишком сильно, она увеличивается. Худший случай составляет O(n), если коллизий много.
13. Когда выбрать SQL-базу, а не NoSQL?
Выбирайте SQL, когда данные связанные, нужны соединения (JOIN) и транзакции, а схема достаточно стабильна: заказы, платежи, учётные записи пользователей. Выбирайте документное или ключ-значение хранилище, когда шаблоны доступа простые, структура данных меняется или нужно масштабировать запись на много машин. Многие системы используют и то и другое. Интервьюеру нужен не победитель, а понимание компромисса. Слой API, который обычно стоит сверху, разобран в нашем материале вопросы по REST API на собеседовании.
Вопросы по проектированию систем
14. Спроектировать сокращатель URL
Сначала уточните требования: сколько новых ссылок в день, сколько переходов, истекают ли ссылки, допустимы ли свои алиасы. Затем опишите API (создание и перенаправление), способ генерации коротких кодов (счётчик, закодированный в base62, или случайный код с проверкой уникальности), хранение (хранилище ключ-значение от кода к URL) и кеш для популярных ссылок, потому что чтений намного больше, чем записей. Если останется время, закончите аналитикой и защитой от злоупотреблений. Всё решение мы разбираем в нашем гиде по проектированию систем.
15. Спроектировать rate limiter
Начните с того, где он работает (на API-шлюзе, на пользователя или на ключ API) и что происходит, когда клиент превышает лимит (HTTP 429 с временем повтора). Чаще всего выбирают алгоритм «токенового ведра» (token bucket): у каждого клиента есть ведро, которое пополняется с фиксированной скоростью и вмещает максимальное число токенов, а каждый запрос забирает один токен. Так допускаются короткие всплески при соблюдении средней скорости. Если серверов несколько, храните счётчики в общем хранилище, например в Redis, чтобы каждый сервер видел одно и то же значение. Для дополнительной практики смотрите наш материал вопросы по проектированию систем на собеседовании.
Поведенческие вопросы
Для каждой истории используйте понятную структуру: ситуация, что вы сделали и результат, с цифрами, если они есть.
16. Расскажите о себе
Дайте двухминутный рассказ о карьере, который заканчивается тем, почему вы хотите именно эту роль. Упомяните один-два проекта с конкретными результатами. В нашем гиде по ответу на вопрос «расскажите о себе» есть примеры.
17. Расскажите о сложном баге, который вы исправили
Пример ответа: «Сервис оформления заказов начал несколько раз в день выходить по таймауту без всякой закономерности. Я добавил идентификаторы запросов в логи и выяснил, что каждый таймаут возникал из-за одного запроса к базе данных, который выполнялся только тогда, когда в корзине было больше 50 товаров. В запросе не хватало индекса по колонке соединения. Я добавил индекс, время медленного запроса упало примерно с 4 секунд до меньше 50 миллисекунд, и настроил оповещение о задержке этого запроса, чтобы в следующий раз заметить проблему раньше».
18. Расскажите о разногласии с коллегой по команде
Пример ответа: «Коллега хотел переписать наш сервис уведомлений на новом фреймворке, а я считал, что перед запуском риск слишком высок. Вместо спора на встрече я предложил, чтобы каждый письменно расписал стоимость и риски. Мы договорились исправить две самые серьёзные проблемы в текущем коде до запуска и запланировать переписывание на следующий квартал. Запуск прошёл вовремя, а переписывание мы сделали через два месяца с нормальным планом тестирования».
19. Расскажите о проекте, который провалился
Выберите реальный провал, за часть которого вы отвечали. Объясните, что пошло не так, что вы сделали бы иначе и что изменили после этого. Интервьюеров интересуют честность и умение учиться, а не безупречная история.
20. Почему вы хотите работать здесь?
Свяжите что-то конкретное из продукта компании или её инженерной работы с собственным опытом. Фраза «Я два года пользуюсь вашим API, и хотел бы работать над платформенной частью» лучше, чем «вы отличная компания».
Вопросы, которые стоит задать интервьюеру
- Как выглядит типичная неделя для человека в этой команде?
- Как планируется работа и кто определяет приоритеты?
- Как здесь устроены код-ревью и деплой?
- Что считалось бы успехом в первые шесть месяцев?
- Какую самую сложную техническую задачу сейчас решает команда?
Подробнее см. вопросы, которые стоит задать в конце собеседования.
Как готовиться
- Выберите один язык и решайте на нём все учебные задачи. Чаще всего выбирают Python, потому что код в нём короткий. Сам язык разобран в нашем материале вопросы по Python на собеседовании.
- Тренируйтесь по паттернам, а не по количеству задач: хеш-таблицы, два указателя, скользящее окно, бинарный поиск, BFS и DFS, кучи и динамическое программирование.
- Решайте задачи вслух и с таймером. Правильное решение, сделанное молча, оценивается ниже, чем понятное решение, проговоренное вслух.
- Тренируйтесь в том редакторе, которым будете пользоваться. Многие компании используют CoderPad, у которого есть бесплатная песочница.
- Подготовьте пять историй из своей работы, которые можно адаптировать под большинство поведенческих вопросов.
- Для middle и senior позиций проведите хотя бы несколько полных тренировок по проектированию систем с человеком, который задаёт уточняющие вопросы.
Тренируйте собеседования по кодингу вслух
YesToTheOffer расшифровывает ваши ответы, чтобы вы могли пересмотреть, как объясняли решение, а помощь с кодом по скриншотам работает на скорости 0.2x. 30 бесплатных минут, без подписки.



