お題
引数として与えられた正の整数が素数かどうか判定するメソッドを書いてください。
prime ライブラリーは用いず,2 以上の整数で順に割り切れるかを調べるごくごく素朴な実装としてください。
コード
def prime?(n)
2.upto(Math.sqrt(n)) do |k|
return false if n % k == 0
end
true
end
2, 3, 4, 5, 6, ... で割った剰余を求め,割り切れたら false
を返しています。
どれでも割り切れなかったとき true
を返しています。
4 以上の偶数は試す必要がありませんが,お題ではそういった工夫もせず素朴に実装することを求めているのでそうしています1。
ただし,効率に関わる大きな工夫が一つあります。
除数として n
までの整数を試すのではなく n
の平方根までの整数を試すようにしているのです。
というのは,$n$ がもし $\sqrt n$ を超える整数で割り切れるならその商は $\sqrt n$ 未満であるはずだからです。$\sqrt n$ までの整数で割り切れないならそれより大きな整数で割り切れるはずがありません。
問題点
危ういのは,繰り返しの上限を Math.sqrt(n)
としていることです。
理論上は n
の平方根でいいのですが,問題は Math.sqrt
が誤差を含むということです。
一般に浮動小数点演算は誤差を含み得ます。
Math.sqrt(n)
が n
の真の(つまり数学上の)平方根以上であればいいのですが,真の平方根未満の場合もあります。その場合,テストすべき除数を一つ省いてしまうことになります。
例えば,数学的には $\sqrt{10^{46}} = 10^{23}$ のはずですが,
p Math.sqrt(10**46) < 10**23 # => true
で分かるとおり,Math.sqrt
で求めた $\sqrt{10^{46}}$ の値は真の値より小さくなります2。
これでは誤判定が起こり得ますね。
ただ,実際に誤判定が起こるのはかなり大きな n
の場合です。
おそらくこのコードでまともな時間内に判定できる限界を遥かに超えているでしょう。
それでもこれはまずいコードと言うべきです。正しく動作するかどうかがコードを見ても直ちには分からないからです。
改善
もし「数学的に厳密な平方根を超えない最大の整数」を簡単に得ることができれば解決ですね。
Ruby にはそのためのメソッド Integer.sqrt があります:
(7..10).each{ |n| p [n, Integer.sqrt(n)] }
# => [7, 2]
# [8, 2]
# [9, 3]
# [10, 3]
これを使えば
def prime?(n)
2.upto(Integer.sqrt(n)) do |k|
return false if n % k == 0
end
true
end
と書けます。
まあ,多くの Ruby ユーザーは(このような素数判定の練習問題を除けば)Integer.sqrt
を使う機会は無いでしょう。私も実際の仕事で使ったことはありません。
この記事で伝えたかったことは,「繰り返しの上限に浮動小数点演算の結果を用いる際にはよく気をつけよう」ということです。もう少し一般化すると,「浮動小数点数の大小関係の判定には気をつけよう」となりますね3。