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

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

Одна из задач, которую мы даём соискателям: написать код, который выводит числа от 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, но есть потенциал роста. Но сможет ли он писать поддерживаемый код?

Кандидат:

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

Комментарии (25)


  1. s_f1
    03.11.2022 11:10
    +2

     print(i * 100 + j * 10 + k)
    Зачем тут умножение?


    1. Samhuawei Автор
      03.11.2022 11:41

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

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

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


  1. igolikov
    03.11.2022 11:18
    +3

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

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

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

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


    1. Samhuawei Автор
      03.11.2022 11:43

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

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


      1. YDR
        04.11.2022 09:24
        +2

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


    1. suburg
      03.11.2022 14:49
      +1

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

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


      1. vak0
        03.11.2022 18:50
        +1

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


    1. LaRN
      04.11.2022 21:58

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


  1. eleoleeye
    03.11.2022 13:02
    +4

    Интервьюер:

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

    Кандидат:

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


  1. MinimumLaw
    03.11.2022 14:05
    +1

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

    ...

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

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


    1. qw1
      04.11.2022 21:40
      +1

      Да, автор заигрался в оптимизацию и посадил ошибку.
      Его решение выдаёт 25 чисел, правильное решение «в лоб» — 65.

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


      1. MinimumLaw
        05.11.2022 16:49

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

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

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


  1. valery1707
    03.11.2022 14:28

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

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

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

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


    1. YDR
      04.11.2022 09:33
      +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);
          }
        }
      }


      1. YDR
        04.11.2022 12:18

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


      1. qw1
        04.11.2022 21:50

        Тут вся сложность будет внутри функции 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;


  1. suburg
    03.11.2022 14:54

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

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


  1. Maccimo
    03.11.2022 19:10
    +2

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


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

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


    1. qw1
      04.11.2022 21:55
      +1

      Каждый раз, когда я вижу continue, рука тянется к киянке.
      И return в середине функции тоже не нравится? Мы бы с вами не сработались )))


      1. Maccimo
        05.11.2022 00:01
        -1

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


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

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


        1. virtustilus
          05.11.2022 02:25

          Думается мне 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;
              }
          }

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


        1. qw1
          05.11.2022 17:30

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

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

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


          1. Maccimo
            05.11.2022 18:48

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


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

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


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

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


            1. qw1
              05.11.2022 19:41

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

              Если уж доводить до абсурда
              Почему же? Лямбды — синтаксический сахар, как и continue, их можно выразить в старых конструкциях, но громоздким и избыточным кодом.


  1. thealfest
    04.11.2022 20:53
    +1

         for (int k = 1; i + j + k < 9; ++k)
            if (k == 5)
               continue;