«Напишите программу, которая выводит на экран числа от 1 до 100. При этом вместо чисел, кратных трем, программа должна выводить слово Fizz, а вместо чисел, кратных пяти — слово Buzz. Если число кратно пятнадцати, то программа должна выводить слово FizzBuzz. Задача может показаться очевидной, но нужно получить наиболее простое и красивое решение.»
Под наиболее простым и красивым решением я понимаю наиболее компактный алгоритм, который использует наименьшее количество условных операторов и операций сравнения. Такой алгоритм будет иметь наименьшую временную сложность, к нему и будем стремиться.
Судя по всему, под очевидным решением предполагается использование в цикле четырех условных операторов. Первый проверяет кратность числа трем, второй пяти, третий пятнадцати, а четвертый выводит число в случае невыполнения первых трех условий. Однако, после первого же прочтения задачи возникает желание избавиться от условия проверки на кратность пятнадцати, так как слова Fizz и Buzz подобраны таким образом, чтобы при одновременном выполнении условий кратности трем и пять можно было вывести FizzBuzz. Таким образом можно обойтись тремя условиями.
#include <iostream>
using namespace std;
int main()
{
for(int i=1;i<101;i++)
{
if(i%3==0) cout<<"Fizz";
if(i%5==0) cout<<"Buzz";
if(i%3!=0 && i%5!=0) cout<<i;
cout<<endl;
}
}
Данный код имеет уже три условных оператора и четыре операции сравнения.
Эта задача хороша тем, что ее формулировка заставляет задуматься об алгоритме, который выводит текст на экран при выполнении условий кратности, для того чтобы избавиться от лишнего условия для вывода FizzBuzz. Но этот путь является неверным, и пойдя по нему сократить количество условий очень сложно, если вообще возможно.
Когда все идеи, основанные на предыдущем методе заканчиваются, в голову приходит идея использования строк. Например, с помощью строк можно избавиться от одной операции сравнения.
#include <iostream>
#include <string>
using namespace std;
int main()
{
for(int i=1;i<101;i++)
{
string str="";
if(i%3==0) str="Fizz";
if(i%5==0) str+="Buzz";
if(str=="") str=to_string(i);
cout<<str+'\n';
}
}
Этот вариант решения задачи является наиболее распространенным в сети, но на мой взгляд решению по прежнему не хватает изящности и просты, а именно к этому и нужно стремиться согласно условию задачи.
А что если попробовать не приписывать к строке нужный текст, а наоборот убирать текст из строки вида «ЧислоFizzBuzz»?
#include <iostream>
#include <string>
using namespace std;
int main()
{
for(int i=1;i<101;i++)
{
string n=to_string(i), f = "Fizz", b="Buzz";
if(i%3!=0) f=""; else n="";
if(i%5!=0) b=""; else n="";
cout<<n+f+b+"\n";
}
}
Полученный алгоритм содержит всего две операции сравнения и два условных оператора.
Аналогичных способов решения данной задачи в сети я не нашел. Надеюсь, данный материал сможет помочь кому-то при трудоустройстве или при решении аналогичных задач.
Комментарии (25)
kafeman
09.03.2016 15:04+4Мерить эффективность в количестве операторов ветвления?
if(str=="") str=to_string(i);
Даже в самой простой реализацииstrcmp
тут будет еще одно условие.kafeman
09.03.2016 15:10+2Хотя… Если сделать
if (str[0] == '\0')
То, может, и не увеличится. В любом случае, переводint
вstd::string
тут неэффективен.
vlivyur
09.03.2016 15:42+5Полученный алгоритм содержит всего две операции сравнения и два условных оператора
И четыре вызова подпрограмм. Что даёт неограниченный потенциал в неучтённых операциях сравнения и циклов.
FizzBuzz, или почему программисты не умеют программировать
deniskreshikhin
09.03.2016 15:44+1На си вроде это самое простое решение:
main(i){for(;i<101;puts(i++%5?"":"Buzz"))printf(i%3?i%5?"%d":0:"Fizz",i);}
А вообще это бесконечная тема: http://codegolf.stackexchange.com/questions/58615/1-2-fizz-4-buzzkafeman
09.03.2016 15:58Что есть
i
? Количество аргументов командной строки? Тогда этот код не всегда будет работать верно...
zagayevskiy
09.03.2016 16:02+3Подскажите, пожалуйста, по какому критерию оно простое?
deniskreshikhin
09.03.2016 16:11По длине -)) Ну вообще не понятно как такая глупая задача как fizz buzz может оцениваться с каких-то алгоритмических позиций
zagayevskiy
09.03.2016 16:00Я бы, как собеседующий, таким решением был огорчён. Когда смотришь на это, вообще не очевидно, что происходит. Я бы попросил вас выводить Fizz, когда делится на 5, Buzz, когда делится на 15, FizzBuzz, когда делится на 2 и число — иначе. Пришлось бы вам полностью ваше решение переписывать, да.
GamePad64
09.03.2016 16:03+4Так-с,
using namespace std;
Невероятное зло. Нет, я понимаю, что во всех дурацких учебниках есть эта строчка, но в реальном коде её использовать очень плохо. Используйте fully-qualified name или импортируйте только то, что вам нужно, а не всё пространство имён, напримерusing cout = std::cout
;- Зачем внесли объявление string в тело цикла? allocation/deallocation на каждую итерацию, очень медленно.
- Строковые операции — это тоже медленно.
n. FizzBuzz на хабре? Серьёзно?
P. S. Первый, "классический" вариант — самый адекватный
forgotten
09.03.2016 16:27+5Я бы эту задачу рассмотрел с несколько другой стороны. А какое решение является наиболее устойчивым к изменению требований?
Что, если завтра вместо вывода cout, например, нужно будет делать сетевой запрос? Два отдельных запроса "Fizz" и "Buzz" станут неэквивалентны одному "FizzBuzz", и все эти блестящие оптимизации на уменьшение числа условий выйдут боком.
Что, если завтра добавится третий, четвёртый пятый вариант, и нужно будет на любую комбинацию делителей выводить какую-то свою строку? Как быстро этот супер-оптимизированный код станет вконец нечитабельным?
Короче, я бы в этой задаче смотрел на другое. Если кандидат поэкономил условные операторы — попросил бы переписать под новые условия и посмотрел, как он это будет делать.
Vaner
09.03.2016 17:30-5Я напишу отдельный комментарий, чтобы не отвечать по отдельности всем, кто высказался по поводу сомнительности использования строк и времени их обработки. Хочу сказать, что в рамках этой статьи я рассматривал поставленную задачу, как задачу оптимизации непосредственно самого алгоритма. Приведенные примеры кода на C++ являются не более чем демонстрацией алгоритма. Если бы я написал примеры реализации алгоритма в виде блок схем, то все вопросы касательно использования строк и отсутствия рассмотрения обработки строк в качестве критерия временной сложности отпали бы. Да, я согласен со всеми полезными исправлениями и замечаниями к коду, спасибо всем большое за это, но меня при решении этой задачи интересовала именно оптимизация самого алгоритма действий, без привязки к конкретной реализации на конкретном языке программирования.
kafeman
09.03.2016 17:34+8Если бы я написал примеры реализации алгоритма в виде блок схем, то все вопросы касательно использования строк и отсутствия рассмотрения обработки строк в качестве критерия временной сложности отпали бы.
Нет.
при решении этой задачи интересовала именно оптимизация самого алгоритма
Такой подход очень трудно назвать словом «оптимизация».
gearbox
10.03.2016 22:03за деление в этой задаче надо бить по пальцам. Два счетчика решают проблему и сводят все к инкременту и сравнению целых.
rostik_tsekhmistro
15.03.2016 16:38В принципе задача не сложная и тремя условиями можно вполне обойтись. И время работы будет даже больше при использовании строк, да и просто зачем усложнять задачу.
Daniro_San
18.03.2016 07:57Уж простите, не удержался от написания такой красоты
for(int i=1; i<=100; ++i)
if(!(i%3)||!(i%5))
std::cout<< (!(i%3)?!(i%5)? «FizzBuzz»:«Fizz»:«Buzz» ) <<std::endl;
else std::cout<<i<<std::endl;gearbox
18.03.2016 14:50if(i%3 && i%5) continue;
rostik_tsekhmistro
18.03.2016 18:53-1Это не обизательно
gearbox
18.03.2016 19:50не, ну так то и руки мыть перед едой не обязательно. Ранний выход из итерации за счет инверсии условия облегчает запись условия — она становится более читаемой, при этом код, следующий дальше — не меняется, он так же выполняется только при выполнении того условия что в Вашем варианте. При этом если это не однострочник а блок — мы раскрываем блок и уменьшаем степень вложенности в коде. В общем вполне себе распространенный прием в программировании. А вообще у нас на раёне нормальные пацаны так делают:
for(int i=1, c=1, d=1; i<=100; ++i, ++c, ++d){ if(3 == c) c = 0; if(5 == d) d = 0; if(c && d ) std::cout<< i; if(!c) std::cout<< «Fizz»; if(!d) std::cout<< «Buzz»; std::cout <<std::endl; }
и никаких делений с остатками и выкрутасов с тернарниками. Потом другие пацаны прийдут — прочитать смогут, предъявлять не будут.
Daniro_San
19.03.2016 10:09Тогда уж так
for(int i=1; i<=100; ++i)
if(i%3 && i%5)
{
std::cout<<i<<std::endl;
continue;
}
std::cout<< (!(i%3)?!(i%5)? «FizzBuzz»:«Fizz»:«Buzz» ) <<std::endl;
А как нормальные пацаны отнесуться к этому?
input([ «FizzBuzz» if not x%3 and not x%5 else «Fizz» if not x%3 else «Buzz» if not x%5 else x for x in range(1, 101)])
lair
18.03.2016 22:51Напишите программу, которая выводит на экран числа от 1 до 100. При этом вместо чисел, кратных трем, программа должна выводить слово Fizz, а вместо чисел, кратных пяти — слово Buzz. Если число кратно пятнадцати, то программа должна выводить слово FizzBuzz. Задача может показаться очевидной, но нужно получить наиболее простое и красивое решение.
Вообще, надо заметить, что здесь изрядно переврано исходное условие задачи.
lair
Вы это серьезно?
А стоимость операций со строками вы не учитываете вообще?
Ну и да, вы бы все-таки писали результирующую временную сложность (в нотации большого О) для каждого получившегося у вас варианта.