Impress me или как решить задачу на интервью

Месяца два назад в комментариях вот к этой статье участники сообщества привели с десяток решений несложной задачи. Решения интересные, в чём-то красивые, но они напоминают мне сферического коня в вакууме.

Поясню. Джуниор разработчик проходит собеседование. Ему дали следующее задание.

Одна из задач, которую мы даём соискателям: написать код, который выводит числа от 0 до 1000, которые делятся на 3, но не делятся на 5, и сумма цифр в которых меньше десяти

Интервьюер хочет убедиться что:

  1. Кандидат знает синтаксис языка программирования.

  2. Он умеет решать алгоритмические задачи в рамках имеющихся ресурсов.

  3. Его не придётся переучивать на стандарты использующиеся в компании.

Разработчик решает следующие проблемы:

  1. Ему нужно выставить себя в лучшем свете

  2. Ему нужно обойти других кандидатов

  3. И в конечном итоге получить вакансию

Давайте попробуем максимизировать пользу для обоих участников процесса.

Решение #1 от @Filex

List<Integer> numList = IntStream.range(MIN, MAX).boxed()
            .filter(i -> (i % GOOD_DIV == 0 && i % BAD_DIV != 0 && checkSumDigits(i)))
            .toList();

Интервьюер:

Начинающий программист демонстрирует навыки функционального программирования.Ну ок, начитанный. Но сможет ли он писать код без ошибок в реальной жизни? ФП сравнительно новая концепция, тем более в применении к языку ООП. Вряд ли в нашем legacy это пригодится.

Кандидат:

Я вижу что интервьюер хмурится. Я быстренько говорю что это моё хобби, для роста. И накидываю более традиционное решение

Решение #2 от @pin2t

for (int i = 0; i < 1000; i++) {
            if (i % 3 == 0 && i % 5 != 0 && (i / 100 + (i / 10) % 10 + i % 10) < 10) {
                out.println(i);
            }
        }

Интервьер:

Решение вполне себе рабочее, но уж слишком примитивное. Сможет ли начинающий программист расти? Понимает ли он что всякое число в коде это потенциальная проблема?

Кандидат:

Это я вам набрасал интуитивное решение. В реальной жизни я бы так поступил когда нужно срочно сделать прототип и показать заказчику. Потом разумеется будут написаны тесты и код будет приведён в соответствии со стандартами.

Решение #3 от @igolikov

for (int i = 3; i <= 1000; i += 3) {
            if (i % 5 != 0 && check(i)) {
                System.out.println(i);
            }
        }

Интервьер:

Молодец, отсеял 2/3 заведомо неправильных решений, оптимизировать может. Но циклы в Java вполне себе оптимизированы, а вот деление операция долгая. Тут оно не нужно в принципе. Начал с тройки, но почему упустил 1000, оно же не делится на три. Кандидат думает, но не вникает в детали.

Кандидат:

Тут либо всё делаешь аккуратно, либо не лезешь в предварительную оптимизацию которая есть зло.

Ну и моё решение

for (int i = 0; i < 10; ++i)
  for (int j = 0; j + i < 10; ++j)
     for (int k = 0; i + j + k < 9; ++k)
        if (k == 5 || k ==0)
           continue;
        if (i + j + k != 3 && i + j + k != 6)
           continue;

        print(i * 100 + j * 10 + k)

Интервьюер:

Кандидат знает, как работают процессоры. Он вникает в суть задачи. Может быть немного overqualified, но есть потенциал роста. Но сможет ли он писать поддерживаемый код?

Кандидат:

Обязательно упомянуть, что это лучшее решение с его точки зрения. Но если производительность не важна, лучше конечно написать более простое решение, которое понятно другим членам команды.

@Samhuawei
03.11.2022 12:45 UTC
Первоисточник

Комментарии

@s_f1
03.11.2022 08:10 UTC
+2
 print(i * 100 + j * 10 + k)
Зачем тут умножение?
@Samhuawei
03.11.2022 08:41 UTC
0

Да, это ещё один пункт который можно обсудить с интервьюёром. В условии задачи написано

который выводит числа от 0 до 1000, которые делятся на 3

Способ вывода не написан, возможно это сериализация для которой важен тип данных. Но слово "числа" намекает что нужен int.

@igolikov
03.11.2022 08:18 UTC
+3

Начал с тройки, но почему упустил 1000, оно же не делится на три. Кандидат думает, но не вникает в детали.

Тут либо всё делаешь аккуратно, либо не лезешь в предварительную оптимизацию которая есть зло.

for (int i = 3; i <= 1000; i += 3) {

Этот цикл заканчивается на 999

@Samhuawei
03.11.2022 08:43 UTC
0

Да, на месте кандидата я бы так и ответил, но тогда он скажет - а зачем тогда <= когда можно обойтись <

Случаи разные бывают, иногда компании нужны аккуратные, внимательные к деталям люди. Или те кто может обосновать то что он делает.

04.11.2022 06:24 UTC
+2

но ведь в условии явно записана 1000? как объяснить потом, что она должна превратиться в 999? И мы ведь не экономим "=".

@suburg
03.11.2022 11:49 UTC
+1

Можно было вообще на 900 закончить

У всех чисел от 900 до 999 сумма цифр заведомо не меньше 10

03.11.2022 15:50 UTC
+1

Но 900 делится на 5, как и 810, поэтому закончить можно на 801.

@LaRN
04.11.2022 18:58 UTC
0

По факту достаточно закончить на 900, т.к. сумма цифр д.б. меньше 9

@eleoleeye
03.11.2022 10:02 UTC
+6

Интервьюер:

Начинающий программист демонстрирует навыки функционального программирования.Ну ок, начитанный. Но сможет ли он писать код без ошибок в реальной жизни? ФП сравнительно новая концепция, тем более в применении к языку ООП. Вряд ли в нашем legacy это пригодится.

Кандидат:

**уходит с интерьвю**

@MinimumLaw
03.11.2022 11:05 UTC
+1

Одна из задач, которую мы даём соискателям: написать код, который выводит числа от 0 до 1000, которые делятся на 3, но не делятся на 5, и сумма цифр в которых меньше десяти

...

Ну и моё решение

А оно точно выведет 9? 54? 117?

@qw1
04.11.2022 18:40 UTC
+1
Да, автор заигрался в оптимизацию и посадил ошибку.
Его решение выдаёт 25 чисел, правильное решение «в лоб» — 65.

Но в принципе, ход мыслей верный, решение довольно легко исправить.
05.11.2022 13:49 UTC
0

Вопрос в том, почему не исправлено в статье. Нехорошо... Как минимум коммент под решением не помешал бы.

В целом есть замечательный признак делимости на три которых отлично ложится на условия задачи в части "сумма цифр меньше 10". Но уж больно он неявно в коде сделан. Или наоборот - намеренно явно и ошибочно.

Автора бы в студию.

@TuTuIIok
24.01.2023 12:45 UTC
0

Не, строка

if (i + j + k != 3 && i + j + k != 6)

Неправильная

@CooL-A-TiGeR
24.01.2023 12:45 UTC
0

нет, не выводит, это условие не верно написано:
for (int k = 0; i + j + k < 9; ++k) if (k == 5 || k ==0) continue;
if (i + j + k != 3 && i + j + k != 6)

@valery1707
03.11.2022 11:28 UTC
0

Обязательно упомянуть, что это лучшее решение с его точки зрения.

Наиболее производительное на данный момент. Более чем в два раза чем "простой" и вариант@igolikov.

Но если производительность не важна, лучше конечно написать более простое решение, которое понятно другим членам команды.

Производительность почти всегда важна, но не в ущерб читаемости и поддерживаемости.
Ваше же решение получилось хоть и производительным, но, на мой взгляд, практически не поддерживаемым - любое минимальное изменение параметров из задания будет требовать картинальной переработки всего кода.
И чаще всего такой write only код на проекте, в действительности, не нужен.

@YDR
04.11.2022 06:33 UTC
+1

оно реально получится самым быстрым? Три вложенных цикла, условия - не константы, на один разряд числа тратится целый int, логика работы не очевидна на первый взгляд, ...

Я, правда, в Java не писец, у меня деформация от ассемблера и С, вот второе и третье решение в ассемблер хорошо ложатся. Я бы еще даже сделал

for (int i = 3; 1 <= 1000/3; i ++)
{
  if (i % 5 != 0)
  {
    j = i*3;
    if (check(j))
    {
      System.out.println(j);
    }
  }
}

04.11.2022 09:18 UTC
0

заметил ошибку, должно быть for (int i = 1; i <= 1000/3; i ++)

04.11.2022 18:50 UTC
0
Тут вся сложность будет внутри функции check, которая должна разложить число j на цифры. Программист напишет это через операции div/mod 10/100, а компилятор превратит деления на 10/100 в сдвиги и умножения на «магические константы» типа 0x0CCCCCCCD, 0x66666667 и 0x51EB851F. Вроде как итераций цикла в три раза меньше, зато тело цикла раздувается. Плюс ещё проверка на кратность 5, тоже тяжелее, чем у автора. Впрочем, не исключено, что ваша версия будет быстрее.

Код автора
 for (int k = 0; i + j + k < 9; ++k)
        if (k == 5 || k == 0) continue;

можно ещё упростить
 for (int k = 1; i + j + k < 9; ++k)
        if (k == 5) continue;
@suburg
03.11.2022 11:54 UTC
0

Я бы вообще для начала спросил, как будет использоваться этот код, предъявляются ли требования к производительности и насколько вероятно изменение критериев в будущем. И только после этого думал про решение.

Хотя это конечно аналитическая профдеформация уже.

@ultrakiz
24.01.2023 12:44 UTC
0

Совершенно верно) за последний вариант кода можно и за дверь пойти. Затраты на поддержку такого легаси (тут, кстати можно придраться к отсутствию комментариев, если уж так =) могут намного превышать выгоду от оптимизации (которая может быть вобще не нужна тут).

@Maccimo
03.11.2022 16:10 UTC
+2

Каждый раз, когда я вижу continue, рука тянется к киянке.


ФП сравнительно новая концепция

Гхм, Лиспу более 60 лет, Хаскелю — более 30.

@qw1
04.11.2022 18:55 UTC
+1
Каждый раз, когда я вижу continue, рука тянется к киянке.
И return в середине функции тоже не нравится? Мы бы с вами не сработались )))
04.11.2022 21:01 UTC
-1

Возврат в середине функции иногда может быть оправдан, continue же — code smell в чистом виде, goto на минималках. Его обожают начинающие ждуниоры с кашей в голове.


Мы бы с вами не сработались )))

Открывающую фигурную скобку вы где ставите, заветам Кернигана и Ритчи следуете?

04.11.2022 23:25 UTC
0

Думается мне continue настолько же goto, насколько while является return'ом.
Пример кода из свежей Symfony:

foreach ($transitions as $transition) {
    if ($transition->getName() !== $transitionName) {
        continue;
    }

    $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);

    if ($transitionBlockerList->isEmpty()) {
        return true;
    }
}

Там далеко не джуниоры пишут проект, как мне кажется.

05.11.2022 14:30 UTC
0
continue же — code smell в чистом виде, goto на минималках. Его обожают начинающие ждуниоры с кашей в голове
Как-то я себе слабо представляю, что человек писал-писал break и continue, и тут его осенило: всё, хватит! Я теперь буду создавать новый блок вложенности, чтобы всё было явно прописано.

В моей же картине мира, противники continue — это либо такие старые бабки, которые учились на turbo pascal 2.0, где не было этой конструкции, и которые ворчат «не нужны нам ваши лямбды, и continue ваш сраный нам тоже не нужон!».

Либо такие математики-теоретики, адепты Вирта, которые любой код приводят к стандартным блокам «следования, ветвления, итерации». У них всё должно быть по строгим теоретическим схемам. Если уж пишем императивный код, то никакой декларативности, и наоборот.
05.11.2022 15:48 UTC
0

Против break никто не возражал, речь шла исключительно про continue.


не нужны нам ваши лямбды

Если уж доводить до абсурда, то берите сразу машинное обучение и квантовые вычисления.


Если уж пишем императивный код, то никакой декларативности, и наоборот.

Где continue, а где декларативность, не сравнивайте тёплое с мягким.

05.11.2022 16:41 UTC
0
Против break никто не возражал, речь шла исключительно про continue.
И то, и другое — скрытый goto. Есть рациональная причина, почему одно можно, а другое нельзя?

Если уж доводить до абсурда
Почему же? Лямбды — синтаксический сахар, как и continue, их можно выразить в старых конструкциях, но громоздким и избыточным кодом.
@thealfest
04.11.2022 17:53 UTC
+1
     for (int k = 1; i + j + k < 9; ++k)
        if (k == 5)
           continue;

@tsypanov
06.12.2022 08:24 UTC
0

Кандидат знает, как работают процессоры.

Какая именно особенность работы процессора использована в том коде?

@Gaurund
24.01.2023 12:41 UTC
0

А ошибки в вашем решении зачем?

@kiff2007200
24.01.2023 12:46 UTC
0

Ну и моё решение

А что 9 18 27 и тд не подходят ? Ваше решение их не выводит