(040) 繰り返し処理

Ippei Kishida

Last-modified:2018/03/29 22:55:11.

構造化プログラミングの最後の要素である「反復」を覚えよう。 これでかなりのことができるようになる。 ここまでを完全にマスターすることができれば、 コンピュータで解決できる問題は(理論上)ほとんど全て解決できるようになる筈だ。

最も基本となるループは while である。 これ 1つを覚えておけば(多少煩雑でも)ほとんどの問題に対応できるだろう。

1 条件付反復: while

ある条件を満たす限り、ループを繰り返す仕組み。

# while.rb
i = 0
while (i<10)
  print i, "\n"
  i += 1
end

条件文(上のコードで 「i<10」 の部分)は条件式であり、論理値を返すモノが入る。 上のコードでは i10 との大小を比較し、比較演算の結果真か偽が返るわけだ。 最初に while に突入したときには、while の頭で条件判定が行われ、 1回目のループを実行する。 printi の内容を書き出して、i に1を加えた数を新たに i とする。 1 何度かループを回ると、そのうち i\(10\) になる。 このとき i<10 の演算は偽を返すので、ループには入らず、 ループの外の次の処理に移動する。

1.1 無限ループ

条件式が常に真ならば、ループを抜け出せず延々と同じループを繰り返すことになる。

# while.rb
i = 0
while (true)
  print i, "\n"
  i += 1
end

一度このコードを試してみよう。 プログラムが止まらず、何となく不安な気持ちになるだろう。 プログラムを止めるには、キーボードで Ctrl を押しながら C を押せば良い。

本演習の範囲内では無限ループを使うことはあまりないだろうが、 多くのウィンドウアプリケーション、特にほぼ全てのゲームで、 無限ループが使われている。

2 ループの脱出: break, next

while ループは強力だが、時にはループ内で条件判定してループを抜け出したり したい場合がある。 そのような場面において有効な方法がある。

これらは主に if 文と供に使われる。 例を以下に挙げる。

#break.rb
i=0
while (i<100)
  print i, "\n"
  break if (12 == i)
  i += 1
end

3 多重ループ

多重ループを作ることができる。

#nestingLoop.rb
i = 0
while (i<3)
  j = 0
  while (j<4)
    print "#{i}-#{j} "
    j+=1
  end
  print "\n"
  i+=1
end

4 \(n\) 回繰り返し

whileだけ覚えておけばほとんどの問題に対応できる」と先に述べたが、 ループを適用する場面には幾つかのパターンがあり、 それにはまった時にはそれに適した書式で書いた方が便利だ。 ループ内で変化する条件に依存するのではなく、 ループに入る前に既にループの回数が決まっていることがある。 その場合には以下の書式が便利だ。

# repeatTimes.rb
10.times do |i|
  print i, "\n"
end

勿論 while を使って愚直に書くこともできるが、 ループの先頭で条件を、末尾でインクリメント演算を分けて記述するのは面倒だろう。 この形式は非常によく使うので、覚えてしまった方が良い。 2

10.times do |i| を大雑把に説明すると、10 個の数として 0〜9 を取り出し、 ループを回るごとに 0〜9を1つずつ i に代入していくという感じだ。 3 特に、0から始まって、(指定した数-1)で終わる点に注意しよう。

数字を直接書かずに、 整数クラスが格納された変数があれば、それから作用させることもできる。

# repeatTimes.rb
n = 12
n.times do |i|
  print i, "\n"
end

(n/3).times do |i|
  print i, "\n"
end

5 字下げ

本章のサンプルコードでループの中身が字下げされていることに気付いただろうか。 ループも頻出する構造で、離れた箇所の対応に注意する必要がある。 今後作成するコードは、必ず字下げを行ってコーディングすること。

6 課題

6.1

1から 100 までの整数の総和を求めるプログラムを作成し、ソースコードと答えを示せ。

6.2

円周率\(\pi\)を求める。 \(-\pi \le x < \pi\) で定義された関数 \(f(x) = |x|\) を周期\(2\pi\)で拡張した関数に関する フーリエ級数展開から、 \[\begin{aligned} \frac{\pi^2}{6} = \frac1{1^2} + \frac1{2^2} + \frac1{3^2} + \cdots + \frac1{n^2} + \cdots \end{aligned}\] の関係が得られる。 この式から以下の式が得られる。 \[\begin{aligned} \pi = \sqrt{6(\frac1{1^2} + \frac1{2^2} + \frac1{3^2} + \cdots + \frac1{n^2} + \cdots)} \end{aligned} \qquad(1)\]

式(eq. 1)を用いて円周率 \(\pi\)を求めるプログラムを作成せよ。 ただし、用いる項の数 \(n\) については、まず\(n=1\)について実行し、 次に\(n=10\)で実行, 次に\(n=100\) ……のように一桁ずつ増やしてプログラムを走らせ、 真の円周率 3.141592653589793…. に対する精度(合っている桁数)が どのように変化するかを評価せよ。 \(n\)\(10^9\) まで行うこと。 4 各段階での実行速度も計測し、先の精度と併せて議論せよ。 最初の幾つかはおそらく「瞬間的に終わる」(このように記述すれば良い)筈である。

6.2.1 ヒント

例えば、
項の数\(n=1\)の時は \(\sqrt{6(\frac11)} = 2.449\cdots\)
項の数\(n=2\)の時は \(\sqrt{6(\frac11 + \frac14)} = \sqrt{6(\frac54)} = \sqrt{\frac{15}2} = 2.738\cdots\)
項の数\(n=3\)の時は \(\sqrt{6(\frac11 + \frac14 + \frac19)} = \sqrt{6(\frac{49}{36})} = \sqrt{\frac{49}6} = 2.857\cdots\)

6.2.2 ヒント

平方根を求めるには、Math.sqrt() 関数を使えば良い。

  #sqrt.rb
  print Math.sqrt(2.0)

  1. この1加算する操作はインクリメントと呼ばれる。 逆に 1減算する操作はデクリメントと呼ばれる。

  2. このような「回数の決まったループ」は非常に多く使われるので、 ほとんどの言語で実装されている。 しかし、その表現方法は言語ごとに異なっているのが実情である。

  3. 厳密な動作としては、イテレータという概念を理解する必要があるが、 これは本演習の範囲を超える。

  4. 私のテストした環境では \(n=10^9\) で 90秒かかりました。 みなさんの環境では若干異なる結果になるかもしれません。