Споры о том, нужно ли разработчикам писать алгоритмический код на собеседованиях, бесконечны. В поддержку положительного ответа я уже публиковал рассказ об алгоритмических секциях с написанием кода в Яндексе и примерами задач, которые там можно встретить. Теперь я хочу развить эту тему и показать примеры реального продакшен-кода.
Все примеры когда-то написали конкретные разработчики в процессе решения достаточно рутинных задач. Я никак не улучшал код перед публикацией, лишь местами адаптировал его так, чтобы он был понятен без знакомства с нашей кодовой базой. Поэтому некоторые примеры кода могут показаться вам недостаточно классными, но в условиях постоянного давления сроков невозможно шлифовать абсолютно весь код.
В статье четыре примера. Два на C++, один на TypeScript и один на Python. Способность быстро писать относительно простые алгоритмы без багов — общая необходимость, она не зависит от специализации разработчика.
1. Поисковые подсказки в Яндекс.Маркете и ассоциативные массивы
В конце 2016-го года моя команда занималась поисковыми подсказками в мобильной версии Яндекс.Маркета. Нам нужно было адаптировать решения, уже принятые в большом Поиске, к реалиям поиска по товарам. Результаты той работы пользователи Маркета могут видеть и сейчас:
Поисковые подсказки в мобильном Яндекс.Маркете
Старые подсказки Маркета содержали только названия товаров и, скажем, среди них нельзя было встретить строчку «купить телевизор», хотя такой запрос пользователи задавали довольно часто. Поэтому среди прочего мы хотели добавить в саджест Маркета тексты поисковых запросов. Но эти запросы содержат специфические товарные стоп-слова или стоп-фразы — ничего не меняющие в смысле, но очень часто встречающиеся, например: «где купить», «дёшево», «лучший». Такие фразы пользователи дописывают в любые части любых запросов, и из-за этого первая версия нового алгоритма постоянно предлагала дописать к запросу что-то вроде «купить» и «дёшево», и это выглядело максимально странно.
Мы хотели подсказывать только содержательные продолжения запросов. Реализовать это можно было многими способами, но не хотелось тратить много времени на программирование идеального решения, так что сделали следующее: товарные стоп-фразы собрали в отдельный словарь, каждую фразу превратили в набор хешей от нормализованных слов и пар нормализованных слов; аналогичное действие совершили с поисковыми запросами. Затем запросы, в множестве хешей для которых встречались хеши стоп-фраз, выкидывались. Основную логику реализовывала функция:
bool ContainsStopHash(const TString& text,
TEasyParser& easyParser,
const THashSet<size_t>& stopHashes)
{
std::vector<TString> words;
easyParser.ParseUTF8Text(text, &words);
size_t unigramHash = 0;
size_t bigramHash = 0;
for (const TString& word : words) {
const size_t lastUnigramHash = unigramHash;
const size_t wordHash = word.hash();
unigramHash = wordHash;
bigramHash ^= wordHash;
if (stopHashes.contains(unigramHash) || stopHashes.contains(bigramHash)) {
return true;
}
bigramHash ^= lastUnigramHash;
}
return false;
}
Здесь TEasyParser
— класс, реализующий превращение текста в вектор слов, приведённых к начальным формам, THashSet
— аналог std::unordered_set
, а TString
— наш класс для строк.
Основное содержание метода — относительно простой цикл, но при реализации легко ошибиться: например, положить в множество хешей ноль или неправильно обработать последнее слово. В конце концов, можно просто написать слишком медленный алгоритм, не будучи знакомым с концепцией ассоциативных массивов.
2. Reservoir sampling на MapReduce
Положим, у меня есть очень-очень много данных, сгруппированных по ключам. Например, это могут быть поисковые запросы, сгруппированные по показам элементов поисковой выдачи.
Иногда возникает необходимость посмотреть на небольшое репрезентативное подмножество данных для конкретного набора ключей. Скажем, я часто смотрю на выборки показанных фактовых ответов — чтобы понять, не нужно ли как-то их обогатить или что-нибудь поправить в алгоритмах.
Пример фактового ответа
Общий объём данных для каждого из ключей может быть сколь угодно большим, поэтому нельзя, например, вычитать все данные и перемешать их, нужно делать что-то более хитрое. Тем не менее, можно адаптировать идею из стандартной реализации метода std::shuffle.
Если коротко, то идея следующая. Допустим, нужно случайным образом перемешать массив. Будем обрабатывать элементы по одному, слева направо; рассматривая очередной элемент с номером i
, сгенерируем случайное число от 0
до i
включительно и обменяем значениями текущий элемент и элемент на выбранной позиции.
Адаптировать этот алгоритм к нашей ситуации можно так. Представим, что мы перемешиваем входные данные случайным образом, а затем берём K
стартовых элементов — очевидно, это будет статистически корректный способ. Чтобы получить первые K
элементов, не нужно хранить весь перемешанный массив, по существу можно хранить только первые K
элементов. В алгоритм std::shuffle тогда можно внести такое изменение: если очередной элемент меняется местами с одним из первых K
элементов — помещаем его туда, а если нет, то не делаем ничего.
Другими словами: будем обрабатывать входные элементы по одному и при обработке очередного i
-го элемента будем выбирать случайный номер от 0
до i
включительно. Если этот номер меньше, чем K
— поместим текущий объект в ответ, при необходимости исключив из него какой-то ранее уже добавленный элемент. Код reducer'а выглядит так:
void Do(TMRReader* input, TMRWriter* output) override {
TVector<TNode> sample;
TMersenne<ui64> mersenne;
size_t passedItems = 0;
for (; input->IsValid(); input->Next()) {
++passedItems;
size_t position = mersenne.GenRand64() % passedItems;
if (position >= ItemsToTake) {
continue;
}
if (sample.size() < ItemsToTake) {
sample.push_back(input->GetRow());
} else {
sample[position] = input->GetRow();
}
}
Shuffle(sample.begin(), sample.end(), mersenne);
for (const TNode& node : sample) {
output->Add(node);
}
}
Здесь TMersenne
— наша реализация алгоритма mersenne twister, то есть хороший генератор псевдослучайных чисел, TNode
— структура, хранящая одну строку MapReduce-таблицы.
Эта реализация позволяет тратить объём дополнительной памяти, пропорциональный длине ответа и не зависящий от объёма входных данных.
3. Обработка параметров и TypeScript
Алгоритмы в широком смысле слова приходится писать не только бэкенд-разработчикам. В этом примере — отрывок кода клиентской части одного из наших сервисов.
При обработке url'ов иногда приходится сталкиваться с неудобствами в работе с параметрами. Например, url может содержать путь /action?param=¶m=1;2;3¶m=8
, тогда стандартные средства работы с параметрами url порождают такую структуру:
{
"param" : ["", "1;2;3", "8"]
}
С такой структурой неудобно работать, так как элементы массива невозможно обрабатывать единообразно. Было бы удобнее работать, например, с такой структурой:
{
"param" : ["1", "2", "3", "8"]
}
Для её получения была написана функция:
export type TParamValue = string | string[];
export type TParams = Record<string, TParamValue>;
export function normalizeParams(params: TParams): TParams {
const result = {};
for (const [paramName, paramValue] of Object.entries(params)) {
// массив может быть при наличии двух одинаковых query-параметров
if (Array.isArray(paramValue)) {
result[paramName] = paramValue.reduce((acc, part) => {
if (part) {
acc = acc.concat(part.split(';').filter(Boolean));
}
return acc;
}, []);
} else if (paramValue) {
result[paramName] = paramValue.split(';').filter(Boolean);
}
}
return result;
}
Здесь разработчику потребовалось продемонстрировать два важных навыка. Первый — обнаружить и обработать все краевые случаи. Второй — написать реализацию достаточно быстро и надёжно: если на реализацию, тестирование и отладку подобного кода тратить уж слишком много времени, поддерживать необходимый темп релизов никак не получится.
4. Python для аналитики
На основе результатов аналитики принимаются важные бизнес-решения; например, запускаются или отменяются проекты, оцениваются результаты работы команд, перераспределяются ресурсы разработки. При этом часто аналитические задачи довольно уникальны, для них нет готовых решений и нужно значительную работу делать с нуля. В таких условиях особенно легко ошибиться, поэтому для аналитиков способность не допускать багов и получать достоверные результаты особенно важна!
Ниже — отрывок аналитического кода. Автору захотелось исследовать взаимодействие с определёнными элементами поисковой выдачи — теми, у которых некоторое свойство попадает в список заранее определённых значений. Такая аналитика бывает полезна для того, чтобы понять, востребованы ли те или иные элементы, как с ними взаимодействуют пользователи, не нужно ли что-нибудь исправить или не стоит ли учесть в их ранжировании какие-нибудь дополнительные факторы.
count = 0
firstPos = -1
clicks = 0
for block in blocks:
result = bl.GetMainResult()
if result.IsA('TWebResult'):
url = result.Url
pos = result.Pos
propValue = result.PropValue
if propValue in interestingValues:
count += 1
if firstPos == -1:
firstPos = pos
for cl in bl.GetClicks():
clicks += 1
yield Record(count=count,firstPos=firstPos,clicks=clicks)
Интересные значения свойства заранее были сложены в set
, что позволяет очень быстро осуществлять проверку.
Даже в этом достаточно простом случае от аналитика потребовалось уверенно владеть ассоциативными массивами, а также учесть некоторые особенности наших данных — в частности, проверить, что каждый элемент имеет правильный тип.
5. Как это соотносится с собеседованиями
Все эти примеры иллюстрируют, что разработчики независимо от их специальности должны владеть базовой алгоритмической подготовкой, которая позволит им быстро и надёжно реализовывать простые алгоритмы, обрабатывая краевые случаи и при необходимости используя предоставляемые языком средства — такие как ассоциативные массивы. Давайте взглянем с этой точки зрения на примеры тех задач, что я разбирал на YouTube (раз, два) и в общем описании алгоритмических секций на Хабре.
Первая задача. Дан массив, состоящий из нулей и единиц. Необходимо вывести длину максимального непрерывного подмассива, состоящего только из единиц.
def foo(nums):
current = 0
best = 0
for n in nums:
if n > 0:
current += 1
best = max(best, current)
else:
current = 0
return best
Вторая задача. Даны две строки. Необходимо проверить, являются ли они анаграммами: отличаются ли они только порядком следования букв. Требуется сделать это за линейное время и при этом не менять входные параметры.
def dictFromString(s):
d = defaultdict(int)
for c in s:
d[c] += 1
return d
def areAnagrams(a, b):
return dictFromString(a) == dictFromString(b)
Третья задача. Генерация правильных скобочных последовательностей: дано число n
. Необходимо распечатать все правильные скобочные последовательности длины 2n
в лексикографическом порядке.
def generate(cur, open, closed, n):
if len(cur) == 2 * n:
print cur
return
if open < n:
generate(cur + '(', open + 1, closed, n)
if closed < open:
generate(cur + ')', open, closed + 1, n)
def parens(n):
generate('', 0, 0, n)
Итого, и в работе, и на собеседованиях программисту нужно уметь:
- реализовывать простые алгоритмы быстро и без багов;
- аккуратно обрабатывать краевые случаи либо писать код так, чтобы эти краевые случаи не возникали;
- уметь пользоваться базовыми конструкциями языка программирования (например, ассоциативными массивами);
- придумывать достаточно хорошие решения; пусть не идеальные, но подходящие для текущих ограничений.
Кандидаты, претендующие на позиции старших и ведущих специалистов, обязательно проходят и другие секции — архитектуру, machine learning, управление проектами, в общем, те, которые соответствуют их сильным сторонам. Поэтому не стоит думать, что алгоритмические секции на собеседованиях дают несправедливое преимущество программистам-олимпиадникам: чтобы претендовать на старшие позиции, нужно не только уметь бодро писать код, но и обладать более специфичными знаниями.
Большое спасибо Никите Макарову aka nkmakarov, Сергею Вавинову, Елене Кунаковой, Егору Омельченко за помощь в подготовке статьи и отрывков кода!
JustDont
Очень показательно, что для фронтэндеров в итоге накопали один очень мелкий кейс на прямолинейное применение reduce().
Впрочем, до тех пор, пока на собеседованиях фронтэндеров витает «мода» (задаваемая, в том числе, и вами) спрашивать про алгоритмы, структуры данных, паттерны, еще какую угодно хрень, кроме, собственно, фронтэнда — массово отфильтровывать такие компании как неадекватные достаточно легко и непринужденно.
И спасибо коронавирусу, теперь хотя бы мода на написание кода на доске и бумажке сильно уменьшится. И то хорошо.
wataru
В нормальных компаниях уже давно предлагают писать на ноутбуке. Иногда даже с подсветкой синтаксиса. Но, естественно, без полноценной ide, автодополнения и компилирования. И да — опечатки, перепутанные местами параметры или неточное название библиотечной функции вообще ни на что не влияют. Никто не проверяет код на компилябельность. Смотрят только логику и стиль.
JustDont
Моя практика как раз говорит о том, что хотя и не должны бы — но как раз таки очень часто собеседующий оказывается из тех, кто готов «докапываться до запятых» даже в коде на бумажке (или в notepad).
wataru
Ну это какой-то плохой интервьювер. В гугле такой графы в отчете "пишет код без опечаток" нет. Можно в заметках, конечно, написать, что кандидат "запятую не там поставил", но комитет эту часть просто проигнорирует.
Или это кто-то взял и из гугловых интервью сделал карго культ.
JustDont
О чём тут в комментах в основном и идёт речь.
wataru
Тут куча коментов вида "алгоритмы бесполезны и их нельзя спрашивать на интервью". Но на самом деле должно быть "многие компании неправильно справшивают про алгоритмы. Вроде как идею у гугла скопировали, но важные детали потеряли".
JustDont
Да ну? Прямо «алгоритмы бесполезны»? Не видел таких комментов.
Моя точка зрения, например — «на собеседованиях лучше выяснять способность работать работу, без какого-либо рода экстраполяций и проекций». Работа предполагает крутые алгоритмы? Спрашивайте их. Работа предполагает километры формочек? Спрашивайте, как кандидат организует тысячи формочек.
Гугл, кстати, не особо обосновал, что его идея реально работает и помогает. Вернее — обосновал, сделав это точно так же, как в прошлом обосновывал, почему надо спрашивать про круглые люки. Что, разумеется, вызывает очень много вопросов относительно строгости такого обоснования.
wataru
Вот только как это сделать более менее точно и экономически целесообразно никто еще не придумал.
Вот только на вакансию клепателя формочек с соответствующей зарплатой никто не пойдет. Всем подавай разработчика. Да и работадателям это не выгодно. Лучше за в 2 раза большую зарплату нанять специалиста, кто хоть что-то еще умеет. Вдруг послезавтра надо будет не только формочки клепать, а, о ужас, обойти список элементов интерфейса.
Но я согласен, интервью уровня гугла маленьким фирмам с более простыми задачами не нужны. Надо попроще задачки брать.
JustDont
Ну вот и я о том же. И не надо читать это как «алгоритмы бесполезны», там в посте совсем другая мысль прослеживается, и нападок на алгоритмы нет вообще.
Тысячи людей каждую минуту нанимают без особого опроса по алгоритмам — вы всё еще уверены, что «никто не придумал»? Для гугла, куда прёт огромный поток кандидатов — возможно, условия достаточно специфические, чтоб способы контор поменьше были бы неприменимы as is, но кто сказал, что их нельзя адаптировать?
Но есть еще те самые «экономически целесообразно», которые вы упомянули. И встаёт логичный вопрос: процесс найма гугла оптимизирует что именно в первую очередь — экономическую целесообразность процесса найма, или качество сотрудников? Понятно, что первое не приносится совсем в жертву второму и наоборот, но когда и то и другое находится на каком-то приемлемом уровне — что будет «дожиматься», экономия или качество?
Очень любопытное мнение. А я бы вот не позиционировал вакансию, как «клепателя формочек», если этих формочек у меня тысячи. Мне вот с порога очевидно, что тут как раз нужен программист, который умеет сильно больше, чем «могу делать формочки, а могу не делать». Почему я и предлагал выше спрашивать именно про то, как это всё можно организовать.
strannik_k
Как по мне, пусть хоть гуглом пользуется периодически на собеседовании) Если знает, что искать, и может за минуту-две по докам освежить в памяти и разобраться, как пользоваться той или иной функцией, то вполне нормальный разработчик. Он, как и любой другой, будет этим часто заниматься на работе.
wataru
Тут 2 проблемы.
Во первых, у каждого своя любимая ide. Тут и куча гемора в настройке зоопарка систем или разработке собственной, и в итоге все-равно будет не честно, что кому-то достанется любимая ide, а кому-то нет. На текущий момент все кандидаты в одинаковых условиях.
Во вторых, именно отсутствие автодополнения и т.п. позволяет забить на компилябельность кода. В этих условиях естественно игнорировать опечатки типа отсутствующей точки с запятой или перепутанные местами параметры стандартной функции. Это как на письменном экзамене с учебниками у всех будут требовать более полный и детальный ответ, чем на устном без конспектов.
Если вашу идею развить, то она превращается в тестовое задание — пиши в чем хочешь, гугли сколько надо. Но эти тестовые задания отнимают слишком много времени у кандидатов и у интервьюеров (проверять их дольше и сложнее). Кроме того, на этих тестовых заданиях можно, например, привлекать помощь друга.
strannik_k
У большинства свой ноутбук есть. Могут свой принести для удобства. Плюс, для фронтенда есть онлайн редакторы вроде codesandbox.
Ниже, не про проверку знаний алгоритмов.
Я думаю, на собеседованиях кандидату стоит давать возможность показать код своих проектов и проектов, которые не под nda. Обсудить, почему так сделано, что лучше по другому сделать. Все-таки о навыках программиста ничего не может сказать лучше, чем его код. Тут я не имею ввиду код, который пишется в стрессовых условиях на некоторых собеседованиях.
wataru
И будут наезжать на условный гугл с темой "без своего ноутбука к ним не поступишь! Чертова дискриминация на интервью. Нанимают только богатых!!!1" И какой-нибудь иск обязательно будет коллективный.
Ну, вот приходит к вам чувак, приносит отличный код. Что-то более менее вразумительное про него мямлит, а потом на работе не может условный fizz-buzz написать, или тратит на что-то того же уровня целую неделю, чтобы выдавить из себя 10 строк совершенно ублюдочного кода, попутно задалбывая всех коллег с просьбами о помощи. Потом выясняется что код с интервью — продукт условного парного программирования и 100 итераций код-ревью. Т.е. такого уровня код можно в итоге даже получить, но какой ценой. Об уровне кандидата готовый код далеко не всегда говорит. Чаще — о команде и процессах на прошлой работе. А выучить какое-то внятное объяснение по готовому сниппету не так и сложно.
Вторая проблема вашего подхода, что у многих вообще может не быть кода без nda, кроме студенческих проектов, которые стыдно показывать белому свету.
strannik_k
Ну вы прям крайности приводите в примерах. Совершенно ублюдочный код может писать и эксперт по алгоритмам)
Если нет, ну что поделать. Если есть, почему бы этим не воспользоваться?У всего есть недостатки.
Какая ваша цель — поставить всех в одинаковые условия и выбрать тех, кто в поставленных условиях лучше себя покажет? Или просто нанять хорошего разработчика?
Цель собеседуемого — получше себя показать и договориться о большей зарплате. Ему не выгодно, когда его ограничивают в средствах для достижения этого.
wataru
Да, но вы хотя бы раз видели, как он пишет не ублюдочный код. Ведь решенная задача на собеседовании, это не все. Нужно код вменяемый написать.
В идеале — нанять лучших из возможных кандидатов. Понятно, что если вы год не можете закрыть позицию, то стоит очень сильно понизить планку. Но если у вас достаточно кандидатов, то интервью типа гугловых и яндексовых тут подходят чтобы нанять хорошего разработчика.
Потому что это очень ненадежный сигнал. Так можно и просто спрашивать "вы хороший разработчик? только честно!". Единственный способ понять, хорошо ли кандидат пишет код, это увидеть как он этот код пишет. Значит надо дать какое-то задание, которое можно довольно быстро спроектировать и закодить. Вот и получаются задачи вида "разверните список".
Я не говорю, что смотреть старый код кандидата нельзя и это плохо. Но это может быть только дополнительным сигналом, типа образования и дипломов.
strannik_k
Или доверить собеседовать другому разработчику)