コンピュータ演習II 第11回 アルゴリズム入門¶

今回の主役は アルゴリズム という考え方です。アルゴリズムとは何か、どう読んでコードに翻訳するかを、リストを小さい順に並べ替える 整列(ソート) を題材にして学びます。次の3 つを学びます。

  1. アルゴリズムの読み方(日本語の番号付きの手順を読んで、for 文と if 文に翻訳する)
  2. 2 つの整列アルゴリズム(バブルソートと選択ソート。同じ問題を違う戦略で解く)
  3. アルゴリズムの良し悪し(比較回数と交換回数を数えて比べる)

新しい文法は1 つも出てきません。これまでに学んだ for 文/if 文/入れ替え(swap)だけで、代表的なソーティングアルゴリズムが書けます。

1. アルゴリズムとは¶

アルゴリズム とは、問題を解くための手順を、順番に並べて書いたものです。この資料では、バブルソートと選択ソートをコードで示す前に、まず日本語の番号付きの手順(アルゴリズム)として示します。手順は 上から順に実行され、繰り返したいところは「手順◯へ戻る」と書きます。

これまで学んできたように、同じ処理は3 つの形で表せます。

  1. 手順(アルゴリズム): 日本語の文章で「何をどの順にするか」を書いたもの
  2. フローチャート: 手順を図(記号と矢印)で表したもの
  3. Python コード: 手順をコンピュータが実行できる形で書いたもの

3 つは 同じものの表現違い です。ただし、手順の「手順◯へ戻る」という書き方は、for 文とそのままの形では対応しません。そこでこの章では、手順を Python コードに翻訳するための 規則 を先に覚えてしまいます。

1.1 翻訳の規則: 繰り返しの「3 点セット」→ for 文¶

「手順◯へ戻る」で繰り返しを書くと、手順の中に必ず次の 3 点セット が現れます。

  1. はじめの値: 「i を 0 とする」
  2. 進め方: 「i を1 増やす」
  3. 続ける条件: 「i が n - 1 より小さければ、手順◯へ戻る」

この3 点セットを見つけたら、まとめて1 つの for 文 for i in range(0, n - 1): に落とし込んでください。これが翻訳の規則です。はじめの値が range の開始に、続ける条件が range の終わりに対応し、「1 増やして戻る」は for 文が自動でやってくれます。つまり for 文は、手順ではばらばらの場所に書かれる3 点セットを1 行にまとめた 省略記法 だと考えてください。

あわせて、次の2 つも覚えておいてください。

  • 「◯回に達していなければ、手順△へ戻る」のように回数で書かれた戻りも、同じ規則で for ... in range(0, ◯): になります。
  • 戻り以外の「〜ならば」は if 文になります。

1.2 同じものを3 つの表現で見る¶

規則を確かめるために、「0 から 4 までの数を順に出力する」という1 つの処理を、3 つの表現で縦に並べます。まず手順です。3 点セット(手順1 の「0 とする」、手順3 の「1 増やす」と「5 より小さければ戻る」)を探しながら読んでください。

  1. カウンタ i を 0 とする。
  2. i を出力する。
  3. i を1 増やす。i が 5 より小さければ、手順2 へ戻る。

この手順をそのまま図にしたのが、次のフローチャートです。手順3 の「手順2 へ戻る」が、図では 下から左へ回ってひし形へ戻る矢印 になっています。3 点セットは、i = 0 の箱と、i = i + 1 の箱と、i < 5 のひし形+戻り矢印です。

0 から 4 までの数を順に出力するループのフローチャート

そして、この3 点セット(箱2 つと、ひし形+戻り矢印)をまとめて1 行の for 文に落とし込んだのが、次の Python コードです。

for i in range(0, 5):
    print(i)

3 つの表現はどれも「0, 1, 2, 3, 4 と1 行ずつ出力する」という 同じ動作 を表しています。手順の「戻る」=図の戻り矢印=for 文、という対応を頭に入れておいてください。

この読み方は、次の2 章の復習で最初の練習をし、その後の2 つのソートで繰り返し練習します。また、授業後の課題の発展問題では、資料で扱っていないアルゴリズムを、コードの例を示さず 手順だけから実装 します。手順とコードの対応を意識しながら読み進めてください。

2. 復習: 入れ替え(swap)と最大値移動¶

2.1 tmp を使った入れ替え¶

第09回の復習です。リストの隣り合う2 つの要素 numbers[i] と numbers[i + 1] を入れ替えるには、片方の値を一時変数 tmp に 逃がして から、3 ステップで行うのでした。いきなり numbers[i] = numbers[i + 1] とすると、もとの値が消えてしまうからです。

今回はこの3 ステップの入れ替えを何度も書きます。手が覚えるまで、まずは1 回書いて動かしてください。

In [1]:
numbers = [6, 1, 2, 5, 0]
i = 0                         # 入れ替えたい位置(ここでは 0 番目と 1 番目)

tmp = numbers[i]              # numbers[0](=6)を tmp に逃がす
numbers[i] = numbers[i + 1]   # numbers[0] に numbers[1](=1)を入れる
numbers[i + 1] = tmp          # numbers[1] に、逃がしておいた 6 を入れる

print(numbers)
[1, 6, 2, 5, 0]

実行結果

[1, 6, 2, 5, 0]

2.2 最大値移動(最大値を後ろへ運ぶ操作)¶

これも第09回の復習です。隣り合うペアを 先頭から末尾まで順に 比べ、前のほうが大きければ入れ替える、という処理を 1 パス と呼ぶことにします。ループ変数は j を使います(あとで外側のループに i を使うためです)。

In [ ]:
numbers = [6, 1, 2, 5, 0]

# 1 パス: 先頭から末尾まで、前のほうが大きければ入れ替える
for j in range(0, len(numbers) - 1):  # j = 0, 1, 2, 3
    if numbers[j] > numbers[j + 1]:   # 前のほうが大きければ
        tmp = numbers[j]              # tmp を使って入れ替える
        numbers[j] = numbers[j + 1]
        numbers[j + 1] = tmp

print(numbers)
[1, 2, 5, 0, 6]

実行結果

[1, 2, 5, 0, 6]

1 パスが終わると、いちばん大きい要素(ここでは 6)が、いちばん後ろまで運ばれます。ただし、それ以外の部分([1, 2, 5, 0])はまだ並んでいません。

ここで考えてください。1 パスで「最大の要素が1 つ、後ろに確定する」なら、パスを何度も繰り返せば、大きい要素から順に後ろへ確定していき、最後にはリスト全体が並ぶはずです。この「1 パスを繰り返す」が、1 つ目の整列アルゴリズム バブルソート です。

2.3 1 パスをアルゴリズムとして読む¶

2.2 でやったばかりの「1 パス」を、1 章の規則でさっそく読んでみます。手順の形で書くと次のようになります。

  1. 数を入れたリスト numbers を用意する。位置 j を 0 とする。
  2. numbers[j] が numbers[j + 1] より大きいならば、互いの値を tmp を使って入れ替える。
  3. j を1 増やす。j が「末尾の1 つ手前の位置」を超えていなければ、手順2 へ戻る。

たった3 つの手順ですが、1 章の3 点セットの規則で読むと構造が見えてきます。

手順 見つかる言葉 コードでの形
1 (変数の用意) numbers = [6, 1, 2, 5, 0]
1 & 3 j を 0 とし、1 増やして「手順2 へ戻る」 for j in range(0, len(numbers) - 1):
2 「〜ならば」 if numbers[j] > numbers[j + 1]: と tmp を使った入れ替え3 行

2.2 のコードと見比べてください。「j を 0 とする」「j を1 増やす」「手順2 へ戻る」の3 点セット(手順1 と手順3)が for 文1 つに畳まれ、手順2 が if 文+入れ替えに、そのまま対応しています。逆に言えば、2.2 のコードを書けた人は、この手順を(無意識に)読み取って実装していたことになります。

3. バブルソート¶

まず、バブルソートを日本語の手順(アルゴリズム)として書きます。

  1. 数を入れたリスト numbers を用意し、その長さを n とする。パスの番号 i を 0 とする。
  2. 位置 j を 0 とする。
  3. numbers[j] が numbers[j + 1] より大きいならば、2 つを tmp を使って入れ替える。
  4. j を1 増やす。j が n - 1 より小さければ、手順3 へ戻る。
  5. i を1 増やす。i が n - 1 より小さければ、手順2 へ戻る。

※ 手順2〜4 が1 パス。全部で n - 1 パス。1 パスが終わると、いちばん大きい要素がいちばん後ろに確定する

1 章の3 点セットの規則で読むと、3 点セットが2 組(j の手順2 & 4 と、i の手順1 & 5)、「〜ならば」が1 か所(手順3)あるので、for 文2 つと if 文1 つでできる、と見当が付きます。この後のコードとは、次のように対応します。

手順 コードの対応する行
1. リスト numbers を用意し、長さを n とする numbers = [6, 1, 2, 5, 0] と n = len(numbers)
1 & 5. i を 0 とし、1 増やして手順2 へ戻る 外側の for i in range(0, n - 1):
2 & 4. j を 0 とし、1 増やして手順3 へ戻る 内側の for j in ...:
3. 大きいならば、入れ替える if numbers[j] > numbers[j + 1]: と、tmp を使った3 行

「戻る」が2 つあるときは、戻り先に注目 してください。手順4 は近く(手順3)へ、手順5 は遠く(手順2)へ戻ります。近くへ戻る手順4 の繰り返しが、遠くへ戻る手順5 の繰り返しの 中にすっぽり入っている ので、手順4 が内側の for 文、手順5 がいちばん外側の for 文になります。

3.1 1 パスを外側の for 文で繰り返す¶

2.2 の1 パスを、外側の for 文で繰り返します。第10回で学んだ2重ループの形そのままで、外側 i がパスの番号、内側 j がパスの中の位置 という役割分担です。

パスは何回繰り返せばよいでしょうか。1 パスごとに要素が1 つ後ろに確定するので、要素数を n とすると n - 1 回 で十分です(最後の1 個は、残りが確定すれば自動的に決まります)。

各パスの終わりに print(numbers) を入れて、リストが少しずつ並んでいく様子を観察します。

In [ ]:
numbers = [6, 1, 2, 5, 0]
n = len(numbers)                          # 要素数(5)

for i in range(0, n - 1):                 # 外側: パスを n - 1 回(i = 0, 1, 2, 3)
    for j in range(0, n - 1):             # 内側: 2.2 の 1 パスそのまま
        if numbers[j] > numbers[j + 1]:   # 前のほうが大きければ
            tmp = numbers[j]              # tmp を使って入れ替える
            numbers[j] = numbers[j + 1]
            numbers[j + 1] = tmp
    print(f"パス {i + 1} 後の配列: {numbers}")        # パスが 1 回終わるごとに出力

実行結果

パス 1 後の配列: [1, 2, 5, 0, 6]
パス 2 後の配列: [1, 2, 0, 5, 6]
パス 3 後の配列: [1, 0, 2, 5, 6]
パス 4 後の配列: [0, 1, 2, 5, 6]

1 パス目で 6 が、2 パス目で 5 が、3 パス目で 2 が……と、大きい要素から順に、後ろへ確定していく のが分かります。小さい要素が泡(bubble)のように前へ浮かび上がっていく様子から、この方法を バブルソート と呼びます。

これで並べ替えは完成です。内側の for 文は2.2 の1 パスとまったく同じで、外側の for 文を1 行かぶせただけ、という点を確かめてください。

3.2 無駄な作業を減らして効率化¶

3.1 のプログラムには、少しむだがあります。1 パス目が終わった時点で最後の1 個は確定しているのに、2 パス目でも内側は最後のペアまで比べています。すでに確定した後ろの部分は、もう比べる必要がありません。

パス i が始まる時点で、後ろの i 個は確定しています。そこで内側を range(0, n - 1 - i) にすると、パスが進むごとに比べる範囲が1 つずつ狭くなります。第10回3 章の「内側の range を外側の変数で変える」書き方が、ここで役に立ちます。

In [4]:
numbers = [6, 1, 2, 5, 0]
n = len(numbers)

for i in range(0, n - 1):                 # 外側: パスを n - 1 回
    for j in range(0, n - 1 - i):         # 内側: 確定した後ろの i 個は比べない
        if numbers[j] > numbers[j + 1]:
            tmp = numbers[j]
            numbers[j] = numbers[j + 1]
            numbers[j + 1] = tmp

print(numbers)                            # 整列の完成形を出力
[0, 1, 2, 5, 6]

実行結果

[0, 1, 2, 5, 6]

結果は3.1 と同じですが、比べる回数が減っています(どれだけ減るかは5 章で数えます)。この

for i in range(0, n - 1):
    for j in range(0, n - 1 - i):
        if numbers[j] > numbers[j + 1]:
            (入れ替え)

という形が、バブルソートの完成形です。下の図は、この流れをフローチャートで表したものです。ひし形が3 つ(外側の繰り返し/内側の繰り返し/大小の比較)現れる点と、内側のループが終わってから外側が1 つ進む点を確かめてください。

バブルソートの流れ

4. 選択ソート¶

4.1 最小値の位置(min_index)を探す¶

並べ替えにはバブルソート以外にもさまざまな方法があります。ここでは、同じ問題を解く2 つ目のアルゴリズム として 選択ソート を学びます。2重ループの骨格はバブルソートと同じですが、戦略が違います。考え方を説明するために、リストを 並べ終わった部分(先頭側) と まだ並べ替えていない部分(それより後ろ) の2 つに分けて考えます。最初は、リスト全体が「まだ並べ替えていない部分」です。選択ソートは、「まだ並べ替えていない部分の中からいちばん小さい要素を選んで(選択して)、その部分の先頭に置く」を繰り返します。置いた要素は「並べ終わった部分」の仲間入りをするので、並べ終わった部分が先頭から1 つずつ伸びていき、最後にはリスト全体が並びます。

その準備として、まず「リストの中で最小値が どの位置にあるか」を探します。第06回では最大値そのものを求めましたが、今回は入れ替えに使うので、値ではなく 位置(インデックス) を覚えておく必要があります。変数 min_index に「これまでで最小だった位置」を入れておき、より小さい要素を見つけるたびに更新します。

これも、まずアルゴリズムとして書きます。

  1. 数を入れたリスト numbers を用意する。最小値の位置 min_index を 0 とし、位置 j を 1 とする。
  2. numbers[j] が numbers[min_index] より小さいならば、min_index を j にする。
  3. j を1 増やす。j がリストの長さより小さければ、手順2 へ戻る。

3 点セット(手順1 & 3)が1 組なので for 文1 つ、「〜ならば」(手順2)が1 か所なので if 文1 つでできる、と見当が付きます。下のコードと見比べてください。

In [ ]:
numbers = [6, 3, 8, 1, 5]

min_index = 0                             # 最小値の位置。まず 0 番目とみなす
for j in range(1, len(numbers)):          # 1 番目から末尾まで順に見る(0 番目をmin_index としているため1 から始める)
    if numbers[j] < numbers[min_index]:   # いままでの最小より小さければ
        min_index = j                     # 最小値の位置を更新する

print(f"最小値の位置: {min_index}")
print(f"最小値: {numbers[min_index]}")

実行結果

最小値の位置: 3
最小値: 1

最小値 1 は3 番目にある、と分かりました。min_index には位置(3)が、numbers[min_index] にはその値(1)が入っている、という区別に注意してください。

4.2 選択ソートを完成させる¶

4.1 の「最小値の位置を探す」を組み込んで、選択ソート全体をアルゴリズムとして書きます。3 章のバブルソートと同じく、繰り返しが2 つある完全な形です。

  1. 数を入れたリスト numbers を用意し、その長さを n とする。位置 i を 0 とする。
  2. 最小値の位置 min_index を i とし、位置 j を i + 1 とする。
  3. numbers[j] が numbers[min_index] より小さいならば、min_index を j にする。
  4. j を1 増やす。j が n より小さければ、手順3 へ戻る。
  5. min_index 番目の要素と i 番目の要素を、tmp を使って交換する(これで i 番目までが「並べ終わった部分」になる)。
  6. i を1 増やす。i が n - 1 より小さければ、手順2 へ戻る。

※ 手順2〜5 が1 パス

1 章の3 点セットの規則で読むと、3 点セットが2 組(j の手順2 & 4 と、i の手順1 & 6)あるので、バブルソートと同じ2重ループだと分かります。手順2〜4 は、4.1 の「最小値の位置を探す」そのものです(探しはじめが 0 番目から i 番目に変わっただけ)。下のコードとは、次のように対応します。

手順 コードの対応する行
1. リスト numbers を用意し、長さを n とする numbers = [6, 3, 8, 1, 5] と n = len(numbers)
1 & 6. i を 0 とし、1 増やして手順2 へ戻る 外側の for i in range(0, n - 1):
2 & 4. min_index と j を用意し、j を1 増やして手順3 へ戻る min_index = i と、内側の for j in range(i + 1, n):
3. より小さいならば min_index を j にする if numbers[j] < numbers[min_index]: と min_index = j
5. min_index 番目と i 番目を交換する 内側の for 文を抜けたあとの、tmp を使った3 行

外側の i が「まだ並べ替えていない部分の先頭(=次に埋めたい位置)」、内側の j が「その部分を探索するときの位置」です。内側の探索は i 番目から後ろだけを見ればよいので、min_index = i から始めて range(i + 1, n) を調べます。第10回4 章の range(i + 1, ...) と同じ形です。

なお、手順2〜4 をまとめて「i 番目から末尾までの中から、最小値の位置 min_index を探す」と1 行で書く 粗い書き方 もあります。粗く書かれた手順を見たら、中身の3 点セット(この手順2〜4)を自分で組み立ててください。

下のコードでは、動きを観察するために、各パスの終わりに min_index と交換後の numbers を出力する行を足しています(print(min_index, numbers)。並べ替えそのものには不要な、観察用の行です)。

In [ ]:
numbers = [6, 3, 8, 1, 5]
n = len(numbers)

for i in range(0, n - 1):                     # 外側: 確定させたい位置(i = 0, 1, 2, 3)
    min_index = i                             # まず i 番目を最小とみなす
    for j in range(i + 1, n):                 # 内側: i より後ろから最小値の位置を探す
        if numbers[j] < numbers[min_index]:
            min_index = j                     # 最小値の位置を更新する
    tmp = numbers[i]                          # i 番目と最小値の位置を交換する
    numbers[i] = numbers[min_index]
    numbers[min_index] = tmp
    print("最小値の位置:", min_index, "移動後のリスト:", numbers)   # パスごとに位置とリストを出力(観察用)

実行結果

最小値の位置: 3 移動後のリスト: [1, 3, 8, 6, 5]
最小値の位置: 1 移動後のリスト: [1, 3, 8, 6, 5]
最小値の位置: 4 移動後のリスト: [1, 3, 5, 6, 8]
最小値の位置: 3 移動後のリスト: [1, 3, 5, 6, 8]

1 パス目で最小値 1(3 番目)が先頭に来て、以降は前から順に確定していきます。2 パス目の min_index は 1、つまり i と同じ位置なので、自分自身との交換になりリストは変わりません(間違いではありません)。

注意してほしいのは、交換(swap)が内側のループの外にある ことです。バブルソートは比べるたびに交換しましたが、選択ソートは 探し終わってから1 パスに1 回だけ 交換します。下の図は、この流れをフローチャートで表したものです。

選択ソートの流れ

4.3 バブルソートとの対比¶

2 つのソートを並べると、同じ2重ループの骨格 でできていることが分かります。

バブルソート 選択ソート
外側の i 何パス目か 確定させたい位置
内側の j range(0, n - 1 - i) range(i + 1, n)
内側ですること 隣同士を比べ、そのたびに交換 最小値の位置を探すだけ
交換の場所 内側のループの 中(比べるたび) 内側のループの 外(1 パスに1 回)
確定していく場所 後ろ から(最大値から) 前 から(最小値から)

比較回数はどちらも 4 + 3 + 2 + 1 = 10 回で同じです(選択ソートの内側 range(i + 1, n) も、回数を数えると 4, 3, 2, 1 回です)。一方、交換回数は選択ソートでは 1 パスに1 回、合計 n - 1 回まで に抑えられます。この違いは、次の5 章で count 変数を入れて実際に数えて確かめます。

5. アルゴリズムの良し悪し: 比較回数と交換回数で比べる¶

5.1 count 変数で数える¶

バブルソートと選択ソートは、同じ問題を解く2 つのアルゴリズムでした。では、どちらが「良い」のでしょうか。アルゴリズムの良し悪しを比べる物差しの1 つが 操作の回数、つまり 比較した回数 と 実際に入れ替えた回数 です。まずバブルソートで数えます。第06回で学んだ「count = count + 1 で数える」書き方を2 か所に入れるだけです。

  • 比較回数 compare_count: if 文で比べるたびに1 増やす(if 文の直前に置きます)
  • 交換回数 swap_count: 実際に入れ替えたときだけ1 増やす(if 文の中に置きます)

2 つの count を増やす場所の違い(if の外か中か)に注意してください。

In [ ]:
# バブルソートの比較回数と交換回数を数える
numbers = [6, 1, 2, 5, 0]
n = len(numbers)
compare_count = 0                         # 比較した回数
swap_count = 0                            # 交換した回数

for i in range(0, n - 1):
    for j in range(0, n - 1 - i):
        compare_count = compare_count + 1     # 比べるたびに数える
        if numbers[j] > numbers[j + 1]:
            tmp = numbers[j]
            numbers[j] = numbers[j + 1]
            numbers[j + 1] = tmp
            swap_count = swap_count + 1       # 交換したときだけ数える

print(numbers)
print("比較回数:", compare_count)
print("交換回数:", swap_count)

実行結果

[0, 1, 2, 5, 6]
比較回数: 10
交換回数: 7

比較回数の 10 は、内側の回数を足したもの、つまり 4 + 3 + 2 + 1 = 10 回です(3.1 の素朴版なら 4 × 4 = 16 回でした。n - 1 - i の短縮で 6 回減っています)。比較回数はリストの中身によらず一定ですが、交換回数はリストの並び方で変わります。最初から並んでいれば交換は0 回、逆順に並んでいれば比較のたびに交換が起こります。

いろいろなリストに変えて実行し、比較回数が変わらないこと/交換回数が変わることを確かめてください。

5.2 選択ソートと比べる¶

同じ数え方を4.2 の選択ソートに入れると、比較回数はバブルソートと同じ 10 回(内側の回数はどちらも 4 + 3 + 2 + 1)ですが、交換は 内側のループの外で1 パスに1 回だけ なので、交換回数は多くても n - 1 回に抑えられます(このリストにおいて、選択ソートなら 4 回、バブルソートなら 7 回)。

同じ問題を解いても、アルゴリズムによって操作の回数(=手間)が違う。これが「アルゴリズムの良し悪し」の最初の物差しです。

6. アルゴリズムを改造する: 降順に並べ替える¶

最後に、アルゴリズムを少しだけ 改造 します。ここまでは小さい順(昇順)に並べてきました。大きい順(降順)に並べたいときは、どこを変えればよいでしょうか。

答えは 比較の向きを変えるだけ です。バブルソートなら、numbers[j] > numbers[j + 1] の > を < に変えると、「前のほうが 小さければ 入れ替える」になり、大きい要素が前に集まります。変更はこの1 文字だけで、ループの形は一切変わりません。

In [8]:
numbers = [6, 1, 2, 5, 0]
n = len(numbers)

for i in range(0, n - 1):
    for j in range(0, n - 1 - i):
        if numbers[j] < numbers[j + 1]:   # 向きを変えた(> を < に)
            tmp = numbers[j]
            numbers[j] = numbers[j + 1]
            numbers[j + 1] = tmp

print(numbers)
[6, 5, 2, 1, 0]

実行結果

[6, 5, 2, 1, 0]

選択ソートでも同じで、numbers[j] < numbers[min_index] の < を > に変えれば「最大値の位置を探して前へ置く」降順ソートになります(そのときは変数名も max_index にすると読みやすくなります)。アルゴリズムの骨格はそのままで、比較の条件だけで並ぶ向きが決まる、という点を覚えておいてください。

7. フローチャートで流れをつかむ¶

この資料に載せたフローチャートの図は、ソートの流れを目で確かめるための ヒント です。今回の課題では、この流れを自分でフローチャートに組んで提出します(ツールの使い方は、課題の説明と別紙のクイックリファレンスを参照してください)。1 章で見たとおり、フローチャートは手順・コードと並ぶ 同じアルゴリズムの3 つ目の表現 です。

読み方のポイントは次の2 つです。3 章と4 章の図で確かめてください。

  • ソートのフローチャートには ひし形が3 つ 現れます。外側の繰り返し/内側の繰り返し/大小の比較(if)の3 つです。第10回の2重ループの図に、if のひし形が1 つ増えた形です。
  • バブルソートでは 入れ替えの3 ステップが if の「True」の先(内側のループの中)にあり、選択ソートでは 内側のループを抜けたあと にあります。4.3 の表の「交換の場所」の違いが、図では位置の違いとしてはっきり見えます。

コードを読んでいて流れが分からなくなったら、図の矢印を指でたどり、3.1 や4.2 の「パスごとの出力」と見比べてください。

まとめ¶

  • アルゴリズム は、問題を解くための手順を順番に書いたもの。手順/フローチャート/コードは 同じものの3 つの表現。「はじめの値/進め方/続ける条件」の 3 点セット をまとめて for 文に畳む、戻り以外の「〜ならば」→ if 文、が翻訳の規則。
  • バブルソート は、第09回の「1 パス」を外側の for 文で n - 1 回繰り返したもの。1 パスごとに最大の要素が 後ろ に確定する。内側を range(0, n - 1 - i) にすると、確定した部分を比べ直すむだが省ける。
  • 選択ソート は、まだ並べ替えていない部分(i 番目から末尾)の中から 最小値の位置 min_index を探し、その部分の先頭(i 番目)と交換する。交換は1 パスに1 回だけで、先頭 から順に確定していく。
  • バブルソートも選択ソートも 同じ2重ループの骨格。違いは「内側ですること」と「交換の場所」。
  • アルゴリズムの良し悪しは 操作の回数 で比べられる。比較回数 は if の直前で、交換回数 は if の中で count = count + 1 すると数えられる。比較回数は2 つのソートで同じだが、交換回数は選択ソートのほうが少ない。
  • 降順 にしたいときは、比較の向き(> か <)を変えるだけでよい(アルゴリズムの小さな改造)。

今回いちばん身につけてほしいのは、個々のソートの暗記ではなく、アルゴリズム(手順)を読んで、for 文と if 文のコードに翻訳する 力です。授業後の課題で、この力を確かめてください。