コンピュータ演習II 第10回 繰り返し処理(2重ループ)¶
第06回で繰り返し(for 文)、第09回で range の応用を学びました。今回は、for 文の中にもう1 つの for 文を入れる 2重ループ を学びます。次の3 つを学びます。
- 2重ループの実行順序(外側が1 回進む間に、内側は最後まで回る)
- 代表的なパターン(すべての組み合わせを作る/図形を表示する/リストの全ペアを調べる)
- 内側の range を外側の変数で変える方法(三角形の表示や重複のないペアの列挙で使う)
1. 2重ループの基本¶
1.1 九九の表を作る¶
for 文の中身には、print() や if だけでなく、もう1 つの for 文 を入れることができます。これを 2重ループ と呼びます。
例として九九の表を作ります。外側の for 文で 段(1 の段〜9 の段)を、内側の for 文で 1〜9 の掛ける数 をたどり、i * j を出力します。外側の for 文と内側の for 文で、インデント(字下げ)が 2 段 になる点に注目してください。print(...) は内側の for 文の中身なので、2 段(半角スペース8 つ分)下げて書きます。
九九は 1 から 9 までなので、range は range(1, 10) にします(開始を 0 ではなく 1 にする点に注意してください)。
for i in range(1, 10): # 外側: 1 の段から 9 の段まで(i = 1, 2, ..., 9)
for j in range(1, 10): # 内側: 1 倍から 9 倍まで(j = 1, 2, ..., 9)
print(i, "×", j, "=", i * j) # 段 × 数 = 積 を出力
実行結果
1 × 1 = 1
1 × 2 = 2
1 × 3 = 3
1 × 4 = 4
1 × 5 = 5
1 × 6 = 6
1 × 7 = 7
1 × 8 = 8
1 × 9 = 9
……(省略)……
9 × 9 = 81
全部で 9 × 9 = 81 行が出力されます(ここでは 1 の段と最後の行だけ示しています)。外側の i が 1 のまま内側の j が 1〜9 と動き、次に i が 2 になって……という順に進みます。
1.2 実行の順番をたどる(トレース)¶
九九は81 行あって全部は追いきれないので、もっと小さな例で順番を確かめます。次のプログラムで、i と j の値がどう変わるかを1 つずつ順にたどります(このように値の変化を1 つずつ追うことを トレース といいます)。
for i in range(0, 3): # 外側の for 文(i = 0, 1, 2)
for j in range(0, 2): # 内側の for 文(j = 0, 1)
print(i, j) # i と j の組を出力
実行結果
0 0
0 1
1 0
1 1
2 0
2 1
全部で 3 × 2 = 6 行が出力されます。
上のプログラムの i と j の値を表にして、順にたどります。
| 順番 | i |
j |
出力 |
|---|---|---|---|
| 1 | 0 | 0 | 0 0 |
| 2 | 0 | 1 | 0 1 |
| 3 | 1 | 0 | 1 0 |
| 4 | 1 | 1 | 1 1 |
| 5 | 2 | 0 | 2 0 |
| 6 | 2 | 1 | 2 1 |
大事なのは、外側が1 回進む間に、内側は最後まで回る ということです。外側の i が 0 から 1 に進むのは、内側の j が 0, 1 と 最後まで回り終わったあと です。2重ループの動きが分からなくなったら、この表のように i と j の値を書き出してたどってください。
1.3 外側と内側で違う変数名を使う¶
外側と内側のループ変数には、必ず違う名前 を付けます。慣習にならって、外側を i、内側を j とするのが基本です(3 重になったら k を使います)。
注意: 外側と内側を同じ名前(たとえば両方 i)にすると、内側のループが外側の i の値を書きかえてしまい、思いどおりに動きません。エラーにはならず、間違った結果が黙って出てくる ので気づきにくいバグになります。
下の図は、1.2 のプログラム(外側 for i in range(0, 3):、内側 for j in range(0, 2): で i, j を出力する)の流れをフローチャートで表したものです。
2. すべての組み合わせを作る¶
2.1 2 つのリストの全組み合わせ¶
2重ループの代表的な使いみちの1 つが、2 つのリストのすべての組み合わせ を作ることです。外側で色のリスト colors を、内側でサイズのリスト sizes をたどると、「色とサイズの全組み合わせ」が出力できます。
ここでいう組み合わせとは、1 つ目のリストから1 個、2 つ目のリストから1 個を選ぶ選び方 のことです。全部で(1 つ目の個数)×(2 つ目の個数)通りあります。4 章で扱う「1 つのリストから2 個を選ぶペア」とは別のものなので、区別してください。
colors = ["red", "blue", "green"]
sizes = ["S", "M"]
for i in range(0, len(colors)): # 外側: 色を順にたどる(i = 0, 1, 2)
for j in range(0, len(sizes)): # 内側: サイズを順にたどる(j = 0, 1)
print(colors[i], sizes[j]) # 色とサイズの組み合わせを出力
実行結果
red S
red M
blue S
blue M
green S
green M
外側の色が1 つ決まるごとに、内側のサイズが S、M と最後まで回るので、どの色にもすべてのサイズが組み合わされます。
2.2 組み合わせの数を数える¶
組み合わせが全部で何組あるかを、変数 count で数えます。第06回で学んだ「count = count + 1 で数える」書き方を、そのまま2重ループの中で使うだけです。出力は、ループがすべて終わったあとに 1 回だけ 行います。
colors = ["red", "blue", "green"]
sizes = ["S", "M"]
count = 0 # 組み合わせの個数
for i in range(0, len(colors)):
for j in range(0, len(sizes)):
count = count + 1 # 組み合わせを 1 つ作るたびに 1 増やす
print(count) # ループが全部終わってから 1 回だけ出力
実行結果
6
外側が 3 回、内側が 2 回なので、内側の中身は全部で 3 × 2 = 6 回実行されます。2重ループの実行回数は「外側の回数 × 内側の回数」になる、と覚えてください。
2.3 計算の表を作る¶
2 つの整数リストのすべての組み合わせについて、積 を出力します。1.1 の九九と同じ考え方で、今度はリストの要素どうしを掛けます。
numbers1 = [2, 4, 6]
numbers2 = [1, 2, 3]
for i in range(0, len(numbers1)):
for j in range(0, len(numbers2)):
print(numbers1[i], "×", numbers2[j], "=", numbers1[i] * numbers2[j]) # 積の式を出力
実行結果
2 × 1 = 2
2 × 2 = 4
2 × 3 = 6
4 × 1 = 4
4 × 2 = 8
4 × 3 = 12
6 × 1 = 6
6 × 2 = 12
6 × 3 = 18
出力する代わりに、変数 total にすべての積を 足し込む こともできます。第06回で学んだ「合計を求める」書き方と同じで、total = 0 から始めて、内側の中身で足していきます。
numbers1 = [2, 4, 6]
numbers2 = [1, 2, 3]
total = 0 # 積の合計
for i in range(0, len(numbers1)):
for j in range(0, len(numbers2)):
total = total + numbers1[i] * numbers2[j] # すべての積を合計に足し込む
print(total)
実行結果
72
3. 行を作って図形を表示する¶
3.1 文字列の連結で1 行を作る¶
2重ループのもう1 つの代表的な使いみちが、* を並べて 図形を表示する ことです。その準備として、まず * を5 個並べた1 行を作ります。
第02回で学んだ 文字列の連結(+)だけで書けます。空の文字列 line = "" から始めて、for 文で "*" を1 つずつつなげ、最後に print(line) で出力します。
line = "" # 空の文字列から始める
for j in range(0, 5):
line = line + "*" # "*" を 1 つずつ後ろにつなげる
print(line) # できあがった 1 行を出力
実行結果
*****
3.2 長方形を表示する¶
3.1 の「1 行を作る」処理を、外側の for 文で 行の数だけ くり返すと、長方形が表示できます。外側が行、内側が列 という役割分担です。
注意: line = "" は 外側のループの中(行のはじめ)に書き、行のたびに作り直します。これを外側のループの前に1 回だけ書いてしまうと、前の行の * が残ったまま次の行がつながってしまい、行がどんどん長くなります。よくあるバグなので気をつけてください。
for i in range(0, 3): # 外側: 行(3 行)
line = "" # 行のはじめに、毎回空の文字列から作り直す
for j in range(0, 5): # 内側: 列("*" を 5 個)
line = line + "*"
print(line) # できあがった 1 行を出力
実行結果
*****
*****
*****
3.3 三角形を表示する¶
長方形では、内側の繰り返しの回数はどの行でも 5 回で同じでした。これを 行ごとに変える と、三角形が表示できます。
内側を range(0, i + 1) にすると、i 行目には * が i + 1 個並びます。i = 0 の行は1 個、i = 1 の行は2 個、……、i = 4 の行は5 個です。
このように 内側の range を外側の変数で変える のが、今回の山場です。長方形との違いは range(0, 5) が range(0, i + 1) になった1 か所だけですが、これで「行ごとに長さの違う行」が作れるようになります。
for i in range(0, 5): # 外側: 行(i = 0, 1, 2, 3, 4)
line = "" # 行のはじめに、毎回作り直す
for j in range(0, i + 1): # 内側: "*" を i + 1 個並べる
line = line + "*"
print(line)
実行結果
*
**
***
****
*****
下の図は、この三角形を作る流れをフローチャートで表したものです。内側の繰り返しの回数が、外側の i によって変わっていることを確かめてください。
4. リストの全ペアを調べる¶
4.1 重複のないペア(range(i + 1, ...) を使う)¶
「内側の range を外側の変数で変える」書き方は、1 つのリストから2 つの要素を選ぶ組(ペア)をすべて調べる ときにも使います。2 章の「2 つのリストから1 個ずつ選ぶ」とは違う数え方なので、区別してください。
リスト numbers = [3, 8, 5, 9, 2] から2 つの要素を選ぶことを考えます。内側も range(0, len(numbers)) にしてしまうと、(numbers[i], numbers[j]) と (numbers[j], numbers[i]) を 別々に数えてしまい、(numbers[0], numbers[0]) のような 自分自身とのペア まで出てきてしまいます。このように取り出す順序まで区別する数え方を 順列 といいます。
そこで内側を range(i + 1, len(numbers)) にします。j が i より後ろだけ を動くので、常に i < j となり、重複も自分自身とのペアもないペアだけが取り出せます。この、順序を区別しない「重複のないペア」の数え方が 組み合わせ です。
numbers = [3, 8, 5, 9, 2]
for i in range(0, len(numbers)): # i = 0, 1, 2, 3, 4
for j in range(i + 1, len(numbers)): # j は i より後ろだけを動く(i < j)
print(numbers[i], numbers[j]) # ペアを出力
実行結果
3 8
3 5
3 9
3 2
8 5
8 9
8 2
5 9
5 2
9 2
i = 0(numbers[0] は 3)のとき j は 1, 2, 3, 4、i = 1(numbers[1] は 8)のとき j は 2, 3, 4、……と、内側の範囲が少しずつ狭くなり、全部で 10 組のペアが出力されます。
4.2 条件を満たすペアを数える¶
4.1 の全ペアに if を組み合わせると、条件を満たすペアだけ を数えられます。ここでは「合計が10 以上になるペア」を数えます。数え方は 2.2 と同じ count = count + 1 です。
numbers = [3, 8, 5, 9, 2]
count = 0 # 条件を満たすペアの個数
for i in range(0, len(numbers)):
for j in range(i + 1, len(numbers)):
if numbers[i] + numbers[j] >= 10: # 合計が 10 以上なら
count = count + 1 # 数える
print(count)
実行結果
7
10 組のペアのうち、合計が10 以上になるのは 7 組です。下の図は、このペアを数える流れをフローチャートで表したものです。
第11回では 整列アルゴリズム(並べ替え)を扱いますが、そこではこの「全ペアや隣同士を順に比較していく」考え方が骨格になります。今回のうちに、2重ループでペアをたどる動きを確実に身につけておきましょう。
5. フローチャートで流れをつかむ¶
この資料に載せたフローチャートの図は、2重ループの流れを目で確かめるための ヒント です。今回の課題では、この流れを自分でフローチャートに組んで提出します(ツールの使い方は、課題の説明と別紙のクイックリファレンスを参照してください)。
読み方のポイントは次の2 つです。1 章、3 章、4 章に載せた図で確かめてください。
- 2重ループのフローチャートには、繰り返しの続行を判断する ひし形が2 つ 現れます。外側のひし形の「次あり」の先に、内側のループがまるごと入っている形です。
- 内側のループが最後まで終わってから、外側が1 つ進みます。内側のひし形で「次なし」(内側の繰り返しの終わり)になると、流れは外側に戻り、外側の変数が1 つ進んで、また内側のループが最初から始まります。
Python のコードを読んでいて流れが分からなくなったら、図の矢印を指でたどって、1.2 のトレースの表と見比べてください。
まとめ¶
- for 文の中に for 文を入れたものが 2重ループ。インデントは2 段になり、外側と内側で違う変数名(
iとj)を使う。 - 2重ループでは、外側が1 回進む間に、内側は最後まで回る。内側の中身が実行される回数は「外側の回数 × 内側の回数」。
- 2重ループで作る「選び方」には次の3 つがある。いま自分がどれを作っているのかを、いつも意識する。
- 2 つのリストから1 個ずつ選ぶ(2 章): 外側と内側で別々のリストをたどる。全部で n × m 通り。
- 1 つのリストから2 個選び、順序を区別する(順列): 内側も
range(0, len(numbers))にして、i != jのときだけ処理する。(i, j)と(j, i)を別々に数える。 - 1 つのリストから2 個選び、順序を区別しない(組み合わせ。4 章): 内側を
range(i + 1, len(numbers))にして、i < jのペアだけを取り出す。
count = count + 1で数えたり、totalに足し込んだりする書き方は、1重の for 文のときと同じ。- 図形の表示は 外側が行、内側が列。1 行分の文字列は
line = ""から連結で組み立て、行のはじめに毎回作り直す(作り直し忘れはよくあるバグ)。 - 内側の range を外側の変数で変える と、三角形(
range(0, i + 1))や重複のないペア(range(i + 1, len(numbers)))が作れる。この考え方は第11回の整列アルゴリズムで使う。