LoginSignup
0
0

More than 1 year has passed since last update.

【Ruby のまずいコード】素数判定

Posted at

お題

引数として与えられた正の整数が素数かどうか判定するメソッドを書いてください。
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


  1. この記事で伝えたい以外のことをできるだけシンプルにしたかったわけ。 

  2. 浮動小数点演算の結果は環境によって変わりうるので,全ての環境でこうなるとは限りませんが,一例は示せました。もう少し小さい数だと,手元の環境では Math.sqrt(9904578032905937 ** 2)9904578032905937 よりも小さくなりました。 

  3. それが目的ならもっとシンプルな例題があるわけですが,素数判定の話を採用したのは,この記事のようなコードが多数投稿されているのを見たからです。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0