そろそろ剰余を使わないFizzBuzzについて一言(ry 時代遅れひとりFizzBuzz祭り 剰余無し編

FizzBuzzというニッチな市場*1において更にニッチな「剰余を使わない」分野ではあるものの、どうも需要があるようなので*2書いておく。

ひとまず既存ネタから幾つか再録して解説する。それから新作を出そうと思う。

既存ネタ:「事前に答えを用意しておく」アルゴリズム

最初に剰余無しのFizzBuzzを書いたのは id:eel3:20100531:1275307408 の/bin/shシェルスクリプト)編だ。

/bin/shによるシェルスクリプトでは剰余演算に expr(1) を使うしかないのだが、expr(1) はシェルの組み込みコマンドではなく外部コマンドである為、実行する度にプロセスが生成される。

これがWindowsだとアンチウイルスソフトの類がプロセスの生成を監視していたりする訳で、その結果としてプロセス生成の時間的コストが高くなってしまう。つまり毎回 expr(1) を呼び出すと無茶苦茶遅いFizzBuzzになってしまうのだ。

そこで expr(1) を使わず且つ極力シェル組み込みのコマンドを使うように頑張った結果、こんなFizzBuzzが出来上がった。

#!/bin/sh

FIZVAL=`seq -s ' ' 3 3 100`
isfizz() {
    for i in $FIZVAL; do
        if [ $i = $1 ]; then
            FIZVAL=`echo $FIZVAL | sed -e 's/^[0-9]*[ ]*//'`
            return 0
        fi
        break
    done
    return 1
}

BUZVAL=`seq -s ' ' 5 5 100`
isbuzz() {
    for i in $BUZVAL; do
        if [ $i = $1 ]; then
            BUZVAL=`echo $BUZVAL | sed -e 's/^[0-9]*[ ]*//'`
            return 0
        fi
        break
    done
    return 1
}

isfizbuz() {
    for i in $BUZVAL; do
        if [ $i = $1 ]; then
            for j in $FIZVAL; do
                if [ $j = $1 ]; then
                    FIZVAL=`echo $FIZVAL | sed -e 's/^[0-9]*[ ]*//'`
                    BUZVAL=`echo $BUZVAL | sed -e 's/^[0-9]*[ ]*//'`
                    return 0
                fi
                break
            done
        fi
        break
    done
    return 1
}

pfizbuz() {
    if isfizbuz $1; then
        echo FizzBuzz
    elif isfizz $1; then
        echo Fizz
    elif isbuzz $1; then
        echo Buzz
    else
        echo $1
    fi
}

for i in `seq 1 100`; do
    pfizbuz $i
done

ここで考え出した方法は「事前に答えを用意しておく」とでもいうべき発想に基づくものだ。この後に書いた剰余無しFizzBuzzも同じようなアルゴリズムだったりする。

例えば id:eel3:20111031:1319989854 のbash編で書いた以下のFizzBuzzは、上記の方法をよりスッキリさせた実装だと言える。

#!/bin/bash

readonly MAXVAL=100

eval "
pfizbuz() {
    case \$1 in
        `seq -s " | " 15 15 $MAXVAL` )  echo FizzBuzz ;;
        `seq -s " | "  3  3 $MAXVAL` )  echo Fizz ;;
        `seq -s " | "  5  5 $MAXVAL` )  echo Buzz ;;
        * )                             echo \$1
    esac
}

for i in {1..$MAXVAL}; do pfizbuz \$i; done
"

ちなみにこのアルゴリズム、多分データ量が増えると無茶苦茶遅くなるはず。/bin/shで書いたパターンでは頻繁に文字列操作(とそれに伴うメモリ操作)が発生する可能性がある。bashで書いたパターンではパターンマッチングに古いパターン(もう2度と一致することがないパターン)が残ってしまう為、毎回膨大な数のパターンとのマッチングが発生する。100回程度ならともかく10000回のオーダーでは極端に遅くなるだろう。

既存ネタ:言語機能的に剰余無しでいけるもの

使用する言語によっては手軽に剰余無しでFizzBuzzできる言語機能を持っていたりする。最も顕著なのが id:eel3:20110924:1316791928 のmake編だろう。

# fizzbuzz2.mak
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

MAXVAL  ?= 100

all:    $(shell seq 1 1 $(MAXVAL))

$(shell seq 3 3 $(MAXVAL)):
	@echo 'Fizz'

$(shell seq 5 5 $(MAXVAL)):
	@echo 'Buzz'

$(shell seq 15 15 $(MAXVAL)):
	@echo 'FizzBuzz'

.DEFAULT:
	@echo $@

gmake(GNU make)用の実装。実行する際は標準エラーを読み捨てること。

make編で書いたFizzBuzzは全て剰余演算を使っていない。というか剰余を使う方が面倒ではないだろうか? ついでに言えばifやswitchなどの条件判定の構文もfor文のようなループ構文も使用していない。まあmakeですから。

Fortranも充実した配列操作機能のおかげで剰余無しでFizzBuzzできる。id:eel3:20111008:1318049202 のFortran 90〜95編より。

program fizzbuzz
  implicit none
  integer i
  integer :: tmp(100) = (/ (i, i = 1, 100) /)

  tmp( 3:size(tmp): 3) = -1
  tmp( 5:size(tmp): 5) = -2
  tmp(15:size(tmp):15) = -3

  do i = 1, size(tmp)
    call pfizbuz(tmp(i))
  end do
contains
  subroutine pfizbuz(n)
    integer, intent(in) :: n

    select case (n)
      case (-1)
        print '(A)', 'Fizz'
      case (-2)
        print '(A)', 'Buzz'
      case (-3)
        print '(A)', 'FizzBuzz'
      case default
        print '(I0)', n
    end select
  end subroutine pfizbuz
end program fizzbuzz

Fortranの仕様で1つの配列に複数のデータ型を含めることができないのだが、もしそれが可能で且つデータ型を気にせずにprintできるのなら、サブルーチンpfizbuzのようなまどろっこしい工夫が不要になってコードがスッキリするように思う。

新作ネタ:恐らく真っ当なアルゴリズム

剰余を使わないFizzBuzzアルゴリズムは他にも幾つか考えられるが、真っ当な方法としては「3の倍数や5の倍数を判定する為のカウンタを別途用意する」というものが考えられる。

#!/usr/bin/ruby -w

$fizz = 0
$buzz = 0

def fizzbuzz(n)
  val = ''
  $fizz = $fizz.next
  if $fizz == 3
    val << 'Fizz'
    $fizz = 0
  end
  $buzz = $buzz.next
  if $buzz == 5
    val << 'Buzz'
    $buzz = 0
  end
  val == '' ? n : val
end

(1..100).each {|n| puts fizzbuzz(n) }

この方法は「固定配列でリングバッファを作ったけど剰余演算でインデックスを計算したくない。だけど大きさが2のべき乗じゃないorz」という時に仕方なくif文を使う時の方法に似ている*3

カウンタではなく判定値を用意する方法もある。

#!/usr/bin/ruby -w

$fizz = 3
$buzz = 5

def fizzbuzz(n)
  val = ''
  if n == $fizz
    val << 'Fizz'
    $fizz += 3
  end
  if n == $buzz
    val << 'Buzz'
    $buzz += 5
  end
  val == '' ? n : val
end

(1..100).each {|n| puts fizzbuzz(n) }

アルゴリズムはカウンタを用意する方法と異なるが、この方法に至る発想自体は両者とも似通っているように思う。

ちなみに上記2つの実装は共に「1から開始する」という暗黙の前提がある。

新作ネタ:アフォコード

まあしかし真っ当に剰余無しでFizzBuzzしていても面白くないので一ひねりしてみた。

#!/usr/bin/ruby -w

$fizzmagic = ['3' ,'6', '9', '2', '5', '8', '1', '4', '7', '0']
$buzzmagic = ['5', '0']

def fizzbuzz(s)
  val = ''
  [ {magic: $fizzmagic, value: 'Fizz'},
    {magic: $buzzmagic, value: 'Buzz'}
  ].each do |prm|
    if s[-1] == prm[:magic][0]
      val << prm[:value]
      prm[:magic].rotate!
    end
  end
  val == '' ? s : val
end

(1..100).each {|n| puts fizzbuzz(n.to_s) }

何というか、うん、アフォコードだなあ。コード自体は比較的シンプルだけど、アルゴリズム自体が真っ当とは言えない。

*1:市場なのか……!

*2:定期的に小規模な祭りが起きているような印象を持っている。

*3:でも最近は剰余演算の命令を持つCPUを使うことが多いのでif文などで判定するコードは書かない。