コンピュータ演習II 第10回 繰り返し処理(2重ループ)¶

第06回で繰り返し(for 文)、第09回で range の応用を学びました。今回は、for 文の中にもう1 つの for 文を入れる 2重ループ を学びます。次の3 つを学びます。

  1. 2重ループの実行順序(外側が1 回進む間に、内側は最後まで回る)
  2. 代表的なパターン(すべての組み合わせを作る/図形を表示する/リストの全ペアを調べる)
  3. 内側の 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 にする点に注意してください)。

In [ ]:
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 つずつ追うことを トレース といいます)。

In [ ]:
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. すべての組み合わせを作る¶

2.1 2 つのリストの全組み合わせ¶

2重ループの代表的な使いみちの1 つが、2 つのリストのすべての組み合わせ を作ることです。外側で色のリスト colors を、内側でサイズのリスト sizes をたどると、「色とサイズの全組み合わせ」が出力できます。

ここでいう組み合わせとは、1 つ目のリストから1 個、2 つ目のリストから1 個を選ぶ選び方 のことです。全部で(1 つ目の個数)×(2 つ目の個数)通りあります。4 章で扱う「1 つのリストから2 個を選ぶペア」とは別のものなので、区別してください。

In [ ]:
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 回だけ 行います。

In [ ]:
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 の九九と同じ考え方で、今度はリストの要素どうしを掛けます。

In [ ]:
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 から始めて、内側の中身で足していきます。

In [ ]:
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) で出力します。

In [ ]:
line = ""              # 空の文字列から始める
for j in range(0, 5):
    line = line + "*"  # "*" を 1 つずつ後ろにつなげる

print(line)            # できあがった 1 行を出力

実行結果

*****

3.2 長方形を表示する¶

3.1 の「1 行を作る」処理を、外側の for 文で 行の数だけ くり返すと、長方形が表示できます。外側が行、内側が列 という役割分担です。

注意: line = "" は 外側のループの中(行のはじめ)に書き、行のたびに作り直します。これを外側のループの前に1 回だけ書いてしまうと、前の行の * が残ったまま次の行がつながってしまい、行がどんどん長くなります。よくあるバグなので気をつけてください。

In [ ]:
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 か所だけですが、これで「行ごとに長さの違う行」が作れるようになります。

In [ ]:
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 となり、重複も自分自身とのペアもないペアだけが取り出せます。この、順序を区別しない「重複のないペア」の数え方が 組み合わせ です。

In [ ]:
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 です。

In [ ]:
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回の整列アルゴリズムで使う。