Поиск, хеширование и ДП
Поиск через хеш-таблицу и множество, LRU-кэш, динамическое программирование и полный перебор на Go — с разменом сложности.
8 вопросов
MiddleКодОчень частоРешите Two Sum за O(n), вернув два индекса
Решите Two Sum за O(n), вернув два индекса
Держите map[int]int из значения в индекс. Итерируйте for i, n := range nums; для каждого n ищите target-n через comma-ok — если найдено по индексу j, верните []int{j, i}, иначе сохраните seen[n] = i. Это O(n) против перебора O(n²) вложенным циклом.
Типичные ошибки
- ✗Сортировать сначала и терять исходные индексы, которые требует задача
- ✗Делать два прохода, когда хватает одного с comma-ok
- ✗Считать перебор вложенным циклом O(n) в среднем
Уточняющие вопросы
- →Почему хранение значение→индекс позволяет найти дополнение за один проход?
- →Как обработать дубликаты, образующие пару, например
[3,3]с target 6?
JuniorКодЧастоПодсчитайте, сколько раз каждое значение встречается в слайсе целых
Подсчитайте, сколько раз каждое значение встречается в слайсе целых
Создайте результат через make(map[int]int), затем пройдите по слайсу, делая m[v]++ для каждого значения. Чтение отсутствующего ключа возвращает нулевое значение 0, поэтому m[v]++ работает на первом появлении без явной проверки. Каждое обращение к map — O(1), поэтому весь подсчёт — один проход O(n), а ключи map заодно образуют множество различных значений.
Типичные ошибки
- ✗Защищаться через
ok, думая, что отсутствующий ключ паникует - ✗Считать, что map нельзя инкрементировать на месте
- ✗Забывать, что отсутствующий целочисленный ключ читается как 0
Уточняющие вопросы
- →Как найти самое частое значение, имея счётчики?
- →Как перечислить только различные значения из этой map?
JuniorКодЧастоВычислите n-е число Фибоначчи за O(n) времени и O(1) памяти
Вычислите n-е число Фибоначчи за O(n) времени и O(1) памяти
Верните n для n < 2, затем держите два скользящих значения a, b := 0, 1 и обновляйте a, b = b, a+b в цикле от 2 до n, возвращая b. Это O(n) по времени и O(1) по памяти — гораздо лучше наивной рекурсии, экспоненциальной по сложности. Мемоизированная рекурсия кеширует в map[int]int.
Типичные ошибки
- ✗Использовать наивную двойную рекурсию, экспоненциальную по времени
- ✗Хранить всю последовательность, когда хватает двух скользящих переменных
- ✗Разбивать
a, b = b, a+bна два оператора, портя обновление
Уточняющие вопросы
- →Почему наивная рекурсия
fib(n-1) + fib(n-2)работает за экспоненциальное время? - →Как параллельное присваивание
a, b = b, a+bизбавляет от временной переменной?
MiddleКодЧастоНайти пользователей с наибольшей суммой шагов, не пропустивших ни дня
Найти пользователей с наибольшей суммой шагов, не пропустивших ни дня
Заполните map userID → {daysIn, stepsSum} только по дню 0 — кто не был в день 0, не может быть в каждом дне. Для каждого следующего дня инкрементируйте daysIn и stepsSum только тем, кто уже в map. Затем по map оставьте тех, у кого daysIn == len(statistics), найдите среди них максимум stepsSum и соберите всех с этим максимумом. Пустой вход даёт пустой Result.
Типичные ошибки
- ✗Добавлять новых пользователей из поздних дней, которых не было в каждом дне
- ✗Использовать вложенный поиск вхождения вместо одного прохода-накопления
- ✗Возвращать одного победителя вместо сбора всех с максимумом
Уточняющие вопросы
- →Почему заполнение по дню 0 — ключ к отказу от поиска вхождения?
- →Какова временная сложность относительно общего числа записей?
MiddleТеорияЧастоКак LRU-кэш достигает O(1) для get, put и вытеснения?
Как LRU-кэш достигает O(1) для get, put и вытеснения?
LRU-кэш сочетает хэш-таблицу с двусвязным списком. Таблица ведёт ключ прямо к его узлу списка за O(1). Список держит узлы в порядке свежести — наименее недавно использованный на одном конце, самый свежий на другом. При каждом get или put узел перемещается к свежему концу перепривязкой соседей за O(1), а вытеснение снимает наименее свежий конец, тоже O(1).
Типичные ошибки
- ✗Использовать односвязный список, из-за чего удаление узла O(n)
- ✗Отказаться от map и сканировать список в поисках ключа
- ✗Сканировать все записи для выбора жертвы вместо снятия конца
Уточняющие вопросы
- →Почему список должен быть двусвязным, а не односвязным?
- →Чем политика LRU отличается от истечения по TTL?
MiddleКодИногдаИсключить всех innocents из suspects (разность множеств)
Исключить всех innocents из suspects (разность множеств)
Постройте множество map[int]struct{} из innocents (значение нулевого размера означает только членство, без полезной нагрузки). Пройдите range suspects, добавляя каждое значение, которого нет в множестве по comma-ok — O(n+m) по времени, O(m) по памяти. Поскольку оба входа отсортированы, слияние двумя указателями даёт альтернативу с O(1) памяти.
Типичные ошибки
- ✗Сканировать
innocentsна каждого suspect и считать это O(n) вместо O(n·m) - ✗Изменять
suspectsна месте черезappend, портя слайс вызывающего - ✗Строить множество из
suspects, теряя исходный порядок и дубликаты
Уточняющие вопросы
- →Оба входа отсортированы — как слияние двумя указателями уменьшит память до O(1)?
- →Почему для множества членства предпочесть
map[int]struct{}, а неmap[int]bool?
MiddleКодИногдаСгенерируйте слайс из n уникальных случайных целых
Сгенерируйте слайс из n уникальных случайных целых
Держите map[int]struct{} как множество и слайс-результат. Цикл, пока len(res) == n: берите rand.Int(), и если он уже в множестве, пропускайте через continue; иначе добавляйте его и записывайте в множество. Значение struct{} не занимает памяти, а множество даёт проверку дубликата за O(1), поэтому ожидаемая стоимость близка к O(n) при большом диапазоне.
Типичные ошибки
- ✗Пропускать проверку дубликатов, считая, что
rand.Int()не сталкивается - ✗Сортировать-затем-дедуп, что может дать меньше
nзначений - ✗Использовать линейный скан слайса вместо map для поиска за O(1)
Уточняющие вопросы
- →Почему этот цикл может долго крутиться, если диапазон случайных мал, а
nблизко к его размеру? - →Как перемешивание
0..mсгенерируетnуникальных значений без отбрасывания?
MiddleКодРедкоПодберите пароль по его md5-хэшу перебором над известным алфавитом
Подберите пароль по его md5-хэшу перебором над известным алфавитом
Перебирайте строки-кандидаты по порядку — трактуйте счётчик шага как число в системе с основанием len(alphabet), декодируя каждый шаг в соответствующую строку над алфавитом. Для каждого кандидата считайте hashPassword(guess) и сравнивайте с h через bytes.Equal; возвращайте на первом совпадении. Поиск — O(a^n) для пароля длины n над алфавитом из a символов — экспоненциальный, поэтому работает только для коротких паролей.
Типичные ошибки
- ✗Думать, что
md5обратим или отменяется повторным хэшированием - ✗Сравнивать хэши через
==на слайсах вместоbytes.Equal - ✗Недооценивать рост O(a^n) для длинных паролей
Уточняющие вопросы
- →Как заранее вычисленная rainbow-таблица изменит временную стоимость этой атаки?
- →Как соль и медленный хэш вроде bcrypt делают этот перебор непрактичным?