はじめに
G.ポリアの「いかにして問題をとくか」という本があります。
主に数学の問題に対する解き方という内容ですが、一般的な問題に対しても使うことが可能ということで名著と呼ばれるくらいにいろいろな分野で読まれている本です。
ちなみに実践活用編というのもあるみたいですが、こっちは未読です。
今回は「FizzBuzz問題」というプログラムでよく使われる問題に対して、これを適用してみます。
言語はJavaScriptで書きますが(楽だから)、コードよりも解決にいたるプロセスを追うことのほうが重要ですので、あまり関係ないです。
前提
「いかにして問題をとくか」
この本で紹介されている問題解決のプロセスは以下の4段階に分かれます。
各プロセスの内容は今回特に使うものを抜粋しています。
- 問題を理解する
- 既知のものは何か。
- 未知のものは何か。
- 条件は何か。
- 条件を分離できるか。
- 図をかけるか。
- 計画を立てる・補助問題を考える
- 似た問題を知っているか。
- 問題を言い換えることができるか。
- 与えられた問題が解けないとき、問題の条件を変えて解くことはできるか。
- データをすべて使ったか。
- 条件をすべて使ったか。
- 計画を実行する
- 計画を実行するときに各段階を検証し、各段階が正しいと認められるか。
- 答えを検証する
- 結果を試すことができるか。
- 結果を違った方法で試すことができるか。
「FizzBuzz問題」
1~任意の数までの数字を順番に表示する。
ただし、3の倍数のときは「Fizz」、5の倍数のときは「Buzz」、3と5の公倍数のときは「FizzBuzz」と表示する。
出力例(25まで)
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz
問題を解く
問題を理解する
問題を解くにあたり、一番初めにするべきは問題を理解することです。
問題の中から既知の値、未知の値、条件を取り出します。
- 既知の値
- 開始の値:1
- 未知の値
- 終了の値:変数 end
- 条件
- 3の倍数の時は「Fizz」と表示する。
- 5の倍数の時は「Buzz」と表示する。
- 3と5の公倍数の時は「FizzBuzz」と表示する。
- 3の倍数でも5の倍数でもない場合は現在の数字を表示する。
- 上記の処理を開始の値(1)から終了の値(end)まで繰り返す。
プログラムの場合、未知の値があると実行できないので問題上未知の値となっている部分はその処理の外部(引数や別関数の戻り値)から持ってくることになります。
作ろうとしている処理の中では変数となるので、適切な変数名をこの段階でつけておきます。
今回の場合、終了の値にendという変数名を付けました。
フローチャート、というほどきっちりでなくてもいいですが図にしてみるとなお理解がしやすいです。
計画を立てる・補助問題を考える
問題を理解したら問題を解くための計画を立てます。
与えられた問題自体と似た問題がないかどうか。あるならば、似た問題の解が今回の問題に適用できないかどうかを考えます。
プログラムの場合で言えば、すでにそれに近い機能を提供するライブラリがあれば自分で作らなくても、そのライブラリを使って実現できればそれでよいということになります。
すでにある機能を自作するのは「車輪の再発明」になってしまいますので、開発の段階ではやらないほうがいいです。
似た問題がない場合、条件を限定したりして補助問題を作っていきます。
例えば、「Fizz」「Buzz」「FizzBuzz」と表示する部分の条件を一度無視し、開始の値から終了の値まで表示するという部分だけを補助問題として、それを解くことを考えます。
さらに、終了の値も任意ではなく100に固定してしまうなどして、問題の条件を解けるまで簡単にしていきます。
与えられた問題をより簡単な問題群へと分割していき、それぞれの問題を解いてから組み合わせることで最終的な解を得ます。
FizzBuzzを分割すると、以下のような木構造になります。
これを葉(末端)から順に解いて、どんどん根のほうの問題を解決していきます。
今回はだいぶ細かく分けてますが、分割するのは自分が解けると思ったレベルまでで十分です。
計画を実行する
計画を立てたらそれを実行していきます。
1つの問題が複数の補助問題に分割されているので、それを1つ1つ解決していきます。
ここで、必ず1つの補助問題が解けるたびにその解が正しいかどうかを実行して、検証します。
補助問題の時点で誤りがあれば、最終的な解も誤りとなってしまいます。
補助問題は与えられた問題よりも簡単になっているはずなので、全体を検証するよりも誤りを見つけやすいです。
1の表示
単純に1を表示するだけです。
console.log(1);
数字の表示
任意の数字を表示する処理です。
関数化して、引数で値を受け取るようにしてみます。
「1を表示する」で数字の表示処理ができているので、そこに引数の変数を入れればその値を表示するようになります。
function printNumber(number) {
console.log(number);
}
// 確認用
printNumber(6);
printNumber(99);
printNumber(-10);
printNumber(0);
1~100までの繰り返し
1~100まで、100回繰り返す処理を作ります。
繰り返し処理を作るとき、表示がないと正しい回数が繰り返されているか不明なので、今回はtestと表示する処理を1~100まで繰り返すようにしました。
for(let i = 1; i <= 100; i++) {
console.log('test');
}
Chromeの開発者ツール(F12)で開くコンソールだと、同じ出力はこのように回数でまとめられるので、100回繰り返されていることが確認できます。
ここでの本質は終了値の決まった繰り返し処理を作ることですので、100回繰り返しの確認が大変であれば数を少なくして試すのもよいでしょう。
1~100までの数字を表示
繰り返しの中で数字を表示するようにすればよいので、「1~100までの繰り返し」で表示していた部分に「数字の表示」で作った関数で数字を表示する処理を入れてあげます。
function printNumber(number) {
console.log(number);
}
for(let i = 1; i <= 100; i++) {
printNumber(i);
}
中略
3の倍数の時Fizz
3の倍数の時にFizzと表示する処理を作ります。
数字は引数から与えるようにします。
3の倍数かどうかは3との剰余が0であれば3の倍数と判定します。
function printFizz(number) {
if(number % 3 == 0) {
console.log('Fizz');
}
}
// 確認用
console.log(3);
printFizz(3);
console.log(4);
printFizz(4);
console.log(5);
printFizz(5);
console.log(6);
printFizz(6);
3と6の時にFizzと表示されているのがわかります。
5の倍数の時Buzz
条件が3から5、表示する文字がBuzzになっただけで「3の倍数の時Fizz」と同じです。
function printBuzz(number) {
if(number % 5 == 0) {
console.log('Buzz');
}
}
// 確認用
console.log(5);
printBuzz(5);
console.log(6);
printBuzz(6);
console.log(9);
printBuzz(9);
console.log(10);
printBuzz(10);
3と5の公倍数の時FizzBuzz
3と5の公倍数ということは、3の倍数かつ5の倍数ということになります。
条件と表示する文字が変わった以外はFizz、Buzzの表示処理と同じです。
function printFizzBuzz(number) {
if(number % 3 == 0 && number % 5 == 0) {
console.log('FizzBuzz');
}
}
// 確認用
console.log(15);
printFizzBuzz(15);
console.log(18);
printFizzBuzz(18);
console.log(20);
printFizzBuzz(20);
console.log(30);
printFizzBuzz(30);
1~100までのFizzBuzz
さて、ここまで作ってきた処理を組み合わせて1~100までのFizzBuzzを作ってみます。
まず、単純に組み合わせてみます。
function printNumber(number) {
console.log(number);
}
function printFizz(number) {
if(number % 3 == 0) {
console.log('Fizz');
}
}
function printBuzz(number) {
if(number % 5 == 0) {
console.log('Buzz');
}
}
function printFizzBuzz(number) {
if(number % 3 == 0 && number % 5 == 0) {
console.log('FizzBuzz');
}
}
for(let i = 1; i <= 100; i++) {
printFizzBuzz(i);
printFizz(i);
printBuzz(i);
printNumber(i);
}
出力結果を見ると、例えば15のところでFizzBuzz、Fizz、Buzz、15と4種類の表示がすべて出力されてしまっています。
FizzBuzz問題では15の時はFizzBuzzという表示だけされてほしいため、上記のコードは誤りということになります。。
先ほどのコードは出力処理をすべて並べてしまい、それぞれの条件が排他的ではなかったため複数の条件に一致する処理で一致したすべての出力をしてしまったことが原因です。
修正の方法はいくつかありますが、一番単純なのは条件文を展開してelseを使うことです。
function printNumber(number) {
console.log(number);
}
function printFizz(number) {
console.log('Fizz');
}
function printBuzz(number) {
console.log('Buzz');
}
function printFizzBuzz(number) {
console.log('FizzBuzz');
}
for(let i = 1; i <= 100; i++) {
if(i % 3 == 0 && i % 5 == 0) {
printFizzBuzz(i);
} else if(i % 3 == 0) {
printFizz(i);
} else if(i % 5 == 0) {
printBuzz(i);
} else {
printNumber(i);
}
}
elseを使うことで、各条件が排他的であることを表現できます。
ここで注意するのは、条件が厳しいものから先に記述する必要があるというところです。
例えば、以下のようにしてしまうと3の倍数であれば5の倍数かどうかにかかわらずprintFizzの処理に入ってしまいます。
...
if(i % 3 == 0) {
printFizz(i);
} else if(i % 3 == 0 && i % 5 == 0) {
printFizzBuzz(i);
}
...
1~endまでの繰り返し
1~endまでの繰り返しは「1~100までの繰り返し」で100だった部分がendになればいいので以下のようになります。
function repeat(end) {
for(let i = 1; i <= end; i++) {
console.log('test');
}
}
// 確認用
console.log(8);
repeat(8);
console.log(100);
repeat(100);
console.log(-1);
repeat(-1);
console.log(0);
repeat(0);
回数が負や0の時は繰り返しません。
1~endまでのFIzzBuzz
いよいよ、最終的な目標の処理となります。
「1~100までのFizzBuzz」と「1~endまでの繰り返し」を組み合わせます。
function printNumber(number) {
console.log(number);
}
function printFizz(number) {
console.log('Fizz');
}
function printBuzz(number) {
console.log('Buzz');
}
function printFizzBuzz(number) {
console.log('FizzBuzz');
}
function fizzbuzz(end) {
for(let i = 1; i <= end; i++) {
if(i % 3 == 0 && i % 5 == 0) {
printFizzBuzz(i);
} else if(i % 3 == 0) {
printFizz(i);
} else if(i % 5 == 0) {
printBuzz(i);
} else {
printNumber(i);
}
}
}
問題に出力例として25の場合があるので、それに合わせてfizzbuzz(25)
を実行すると以下のように正しく出力されているのがわかります。
答えを検証する
最終的に得られた解を検証します。
いわゆるソフトウェアテストです。
テスト自動化ツールなどでテストコードを書いて、正しく動くかどうかを検証します。
テスト駆動開発であれば、「計画を実行する」で実際のコードを書く前にテストコードを作ってしまうかと思います。
検証では、問題として考慮されていない部分に関するテストも必要です。
今回の場合であれば、終了の値が0や負の場合にどうなるか、というところなど様々な条件でテストしていきます。
また、改めて最終的にできたコードを見てみるともっと効率的な方法が浮かぶこともあります。
そのような場合にリファクタリングを安全に行うためにもテストコードは重要になります。
まとめ
FizzBuzzを例にして問題を解くまでのプロセスを丁寧に追ってきました。
実際の開発ではこれらのプロセスを頭の中であまり意識せずにやっていることが多いかと思いますが、難しい問題に突き当たった時に意識してみるとうまくいくこともあります。
また、初学者の方だと与えられた問題を一気に解こうとして混乱する方がいます。
実際、未経験の新人に問題解かせようとすると何をしていいかわからないという人が結構います。
そういう時にはまず落ち着いて問題を理解するところから始めて、このプロセスに沿うことで考えが整理され、実際に扱う問題も簡単になるので試してみてください。