タートルグラフィックスで学ぶ Python のキホン 第 17 章
教材コード
第 17 章

リストとアルゴリズム

探す,並べ替える.手順を自分で組み立てる.

前の章,たくさんのデータを 1 つの名前でまとめて持つリストを習いました. 今回は,そのリストに対して「何かをする手順」を自分で組み立てます. 探す.いちばん小さいものを見つける.小さい順に並べ替える. どれも,やりたいことは一言で言えるのに, コンピュータにやらせようとすると手順に分解しなければなりません.

この「手順」のことをアルゴリズムといいます. プログラミングの本当のおもしろさは,ここから始まります. 命令の書き方を覚えることではなく, 「どうやったらできるか」を自分で考えて組み立てること. 今回はそのための最初の一歩です.

並べ替えは目に見えない処理なので,途中で何が起きているのか分かりにくいものです. そこで今回は,データの大きさを棒の高さで描いて, 並べ替わっていく様子をタートルに見せてもらいます.

この章でできるようになること
  • 「アルゴリズム」という言葉の意味を説明でき,同じ結果を出す手順が何通りもあることがわかる
  • 線形探索(先頭から順に見る)を自分で書ける
  • in・index() と,自分で書いた探索との違いがわかる
  • リストの中の最小値と,その場所を見つけられる
  • a, b = b, a で 2 つの値を入れ替えられる
  • 選択ソート・挿入ソート・バブルソートの考え方を,図で説明できる
  • sorted()・sort() を使える.そのうえで,自分で書けることの意味がわかる
  • データが増えたとき,手間がどれくらい増えるかを見積もれる

17.1 手順を決めるということ

アルゴリズムとは,何かを実現するための手順のことです. もう少しきちんと言うと, 小さな命令を組み合わせ,順序立てて実行することで, より大きな目的を達成する手順を指します.

料理のレシピを思い浮かべてください.「カレーを作る」は目的です. 「玉ねぎを切る」「炒める」「水を加える」……という順番が手順です. レシピを見ずに作れる人には当たり前でも, まったく料理を知らない相手に伝えるには,手順に分解して書き下さなければなりません. コンピュータは,まさにその「まったく知らない相手」です.

17.1.1 同じ結果を出す手順は 1 つではない

たとえば「1 から 100 までの合計を求めよ」という目的を考えます. 手順はすぐに 2 通り思いつきます.

手順やり方たし算の回数
ぜんぶ足すs = 0 から始めて,1 から 100 まで順に足していく100 回
公式を使うs = 100 * 101 // 2 (ガウスが子どものころ見つけた式)0 回(かけ算 1 回とわり算 1 回)
two_ways.py
# 1 から 100 までの合計を,2 通りの手順で求める

# 手順 A:ぜんぶ足す
s = 0
for i in range(1, 101):
    s = s + i
print("手順 A の答え:", s)

# 手順 B:公式を使う
t = 100 * 101 // 2
print("手順 B の答え:", t)

答えはどちらも 5050 です. 結果が同じでも,手順は別ものだということが大事な点です. プログラムを書くというのは, 「どんな手順にするか」をあなたが決めるということなのです.

17.1.2 手順の良し悪し

手順には,良し悪しがあります.評価の物差しはおもに 3 つです.

  1. 正しいか.どんなデータを与えても正しい答えになるか.たまたまうまくいっただけ,では困ります.
  2. 速いか.手間(計算の回数)が少ないか.データが 10 個なら差は出ませんが,10 万個になると,1 秒で終わるか 3 時間かかるかの差になります(17.10 節).
  3. 読めるか.人間が読んで分かるか.直せるか.プログラムは書く時間より,読まれる時間のほうがずっと長いものです.
まず正しく,それから速く 1 番目の「正しいか」が最優先です. 速いけれど時々間違える手順より,遅くても必ず正しい手順のほうが,はるかに価値があります. 速くするのは,正しく動くものができた後の話です.

17.1.3 手順を先に書き出す

いきなり Python で書き始めると,たいてい途中で迷子になります. 先に日本語で手順を書き出すと,見通しがよくなります.

日本語で書いた手順(擬似コード)
# 「リストの中からいちばん小さい数を見つける」手順

1. いまのところの最小値を,先頭の数だと思っておく
2. 2 つめ(添字 1)から最後まで,1 つずつ順に見ていく
3.   見ている数が「いまのところの最小値」より小さければ,
4.     「いまのところの最小値」をその数に取り替える
5. 全部見終わったら,「いまのところの最小値」が答え

このように,プログラムの形を借りながら日本語で書いたものを 擬似(ぎじ)コードといいます. 古くからある方法としては,処理を箱と矢印でつないだ フローチャート(流れ図)もよく使われます. どちらでもかまいません.大事なのは 「書く前に手順を決める」という順番です.

コラム:設計と実装 設計(どんな手順にするか決めること)に対して, それを実際に動くプログラムにすることを実装(じっそう)といいます. ソフトウェアの仕事の現場でよく使う言葉です. 「設計はできているが実装がまだ」「実装は終わったがテストがまだ」のように使います. 設計と実装を分けて考える癖がつくと,行きづまったときに 「手順が間違っているのか,書き方を間違えているのか」を切り分けられるようになります.

17.2 カードを描く道具をつくる

並べ替えの話をする前に,リストの中身を目で見られるようにしておきます. 数字を書いたカードを横一列に並べて描く関数を作りましょう. トランプを机に並べるところを想像してください.

cards.py
# リストの中身をカードの列として描く

from turtle import *

def card(x, y, n):
    # (x, y) を左下のかどとして,数 n の書かれたカードを 1 枚描く
    pu()
    goto(x, y)
    seth(0)
    pd()
    pencolor("saddlebrown")
    fillcolor("lightyellow")
    begin_fill()
    fd(40)
    lt(90)
    fd(56)
    lt(90)
    fd(40)
    lt(90)
    fd(56)
    lt(90)
    end_fill()
    pu()
    goto(x + 9, y + 18)
    pencolor("black")
    write(n, font=("Arial", 20, "normal"))

def show(a, y):
    # リスト a を横一列に並べて描く
    x = -23 * len(a)
    for n in a:
        card(x, y, n)
        x = x + 46

ht()
speed(0)
pensize(2)

a = [5, 2, 9, 1, 7, 3]
show(a, 0)

done()
529173
図 17.1 リスト [5, 2, 9, 1, 7, 3] をカードの列として描いたもの.

show(a, y) は,リストをそのまま受け取って, 中身を左から順に並べて描きます. リストの中身が変わってから呼び直せば,変わった後の姿が描けます. これから何度も使うので,覚えておいてください.

無い部品は自分で作る 「カードを描く部品」は,はじめから用意されているわけではありません. 無ければ自分で作ります. 無ければ作れる,というのが第 13 章で関数を習った意味です.

17.2.1 入れ物から入れ物へ 1 枚ずつ移す

並べ替えの前に,カードを 1 枚ずつ別の入れ物に移す練習をします. 並べ替えのアルゴリズムは,どれも結局「カードをどこへ移すか」の話だからです.

リストの先頭から 1 枚抜き取るには pop(0), 末尾に 1 枚加えるには append() を使います.

movecards.py
# 6 枚のカードを,入れ物 1 から入れ物 2 へ 1 枚ずつ移す

from turtle import *

def card(x, y, n):
    pu()
    goto(x, y)
    seth(0)
    pd()
    pencolor("saddlebrown")
    fillcolor("lightyellow")
    begin_fill()
    fd(40)
    lt(90)
    fd(56)
    lt(90)
    fd(40)
    lt(90)
    fd(56)
    lt(90)
    end_fill()
    pu()
    goto(x + 5, y + 18)
    pencolor("black")
    write(n, font=("Arial", 18, "normal"))

def show(a, y):
    x = -23 * len(a)
    for n in a:
        card(x, y, n)
        x = x + 46

ht()
speed(0)
pensize(2)

list1 = [0, 10, 20, 30, 40, 50]
list2 = []

show(list1, 60)         # はじめの入れ物 1

while len(list1) > 0:   # 入れ物 1 が空になるまで
    n = list1.pop(0)    # 先頭を 1 枚抜く
    list2.append(n)     # 末尾に加える
    print("移した:", n, "/残り", len(list1), "枚")

show(list2, -80)        # 移し終わった入れ物 2

done()
0102030405001020304050
図 17.2 上が移す前の入れ物 1,下が移し終わった入れ物 2.

while len(list1) > 0: は「入れ物 1 に 1 枚でも残っているあいだ繰り返す」 という意味です.中で pop(0) するたびにリストは 1 枚ずつ減るので, いつかは 0 枚になり,繰り返しが終わります.

while は終わり方を先に決める while を書くときは, 「いつ終わるのか」を必ず自分で言えるようにしてください. 上の例なら「pop するたびに 1 枚減るから,いつか 0 になる」です. もし pop(0) を書き忘れると,条件がいつまでも成り立ち続けて, プログラムは止まらなくなります(無限ループ). この教材では while True: は使えません.ブラウザが固まるからです.
0 回で終わることもある while は先に条件を調べるので, 条件が最初から成り立たなければ1 回も実行されません. 「必ず 1 回はやりたい」ときは,while の前にその 1 回を書くか, while True: の中の最後で break します (ただしこの教材では while True: は使いません).
  • while … 先に条件を調べる.0 回で終わることもある
  • break … 繰り返しの途中で,その場ですぐ抜け出す(17.3.2 節)

17.3 探す — 線形探索

いちばん基本のアルゴリズムは探すことです. 「このリストの中に 25 はあるか.あるならどの添字か」. 人間なら一目で分かりますが,コンピュータには手順を教えなければなりません.

17.3.1 先頭から順に見る

いちばん素直な手順は,先頭から 1 つずつ見ていって, 目当てのものと同じかどうか調べるというものです. これを線形探索(リニアサーチ,linear search)といいます. 「線のように一直線に見ていく」という意味です.

先頭から順に,1 つずつ見ていく127253198見たいま見ているまだ見ていない
図 17.3 線形探索.左から順に見ていき,見つかったところで止まる.
linear.py
# リストの中から 25 を探す

a = [12, 7, 25, 3, 19, 8]
target = 25

found = -1                    # 見つかった場所.-1 は「まだ見つかっていない」印

for i in range(len(a)):
    print("添字", i, "を見る:", a[i])
    if a[i] == target:
        found = i

if found >= 0:
    print(target, "は添字", found, "にありました")
else:
    print(target, "は入っていません")

コンソールを見ると,6 つ全部を見ていることがわかります. 25 は添字 2 で見つかっているのに,そのあとも添字 3・4・5 を見ています.無駄です.

「まだ無い」を表す値 found = -1 のように, 「まだ何もない」ことを表す値をあらかじめ入れておくやり方を覚えてください. リストの番号は 0 から始まるので,0 は「見つからなかった」印には使えません (添字 0 で見つかったのか,見つからなかったのか区別できない). そこで,絶対にあり得ない -1 を使います.

17.3.2 見つかったら止める — break

見つかったらそこで探すのをやめれば,無駄がなくなります. 繰り返しの途中で抜け出す命令が break です.

linear_break.py
# 見つかったところで探すのをやめる

a = [12, 7, 25, 3, 19, 8]
target = 25

found = -1

for i in range(len(a)):
    print("添字", i, "を見る:", a[i])
    if a[i] == target:
        found = i
        break              # ← ここで for から抜け出す

if found >= 0:
    print(target, "は添字", found, "にありました")
else:
    print(target, "は入っていません")

今度は「添字 0 を見る」「添字 1 を見る」「添字 2 を見る」で止まります. break は,それが入っているいちばん内側の繰り返しだけを抜けます. 二重の繰り返しの内側で break しても,外側の繰り返しは続きます.

コラム:break は使ってよいのか break は,「入り口が 1 つ,出口が 1 つ」というきれいな処理の形 (構造化プログラミング)を崩すものなので, 昔は「行儀が悪い」と言われることもありました. 実際,break を使わなくても,変数と条件で同じ動きは書けます.
python
i = 0
while i < len(a) and a[i] != target:   # 「まだ範囲内」かつ「まだ違う」あいだ
    i = i + 1
ただし今の Python では,探索のように 「見つけたらもう用は無い」という場面で break を使うのは, むしろ読みやすい書き方だと考えられています. 大事なのは,どちらが読みやすいかを自分で判断することです.

17.3.3 できあいの道具 — in と index()

実は Python には,同じことをする道具がはじめから用意されています.

in_index.py
a = [12, 7, 25, 3, 19, 8]

print(25 in a)          # 入っているか? → True
print(100 in a)         # → False

print(a.index(25))      # 添字は? → 2

# 入っていないものを index() で探すとエラーになる
# print(a.index(100))   # ValueError: 100 is not in list

# だから,先に in で確かめてから使う
if 100 in a:
    print("100 は添字", a.index(100))
else:
    print("100 は入っていません")
書き方返すもの入っていないとき
x in aTrue / FalseFalse が返る
a.index(x)見つかった場所(0 から数えた番号)エラー(ValueError)で止まる
a.count(x)いくつ入っているかの個数0 が返る

in は「あるか / ないか」だけ.index() は「どの添字か」まで. 目的に合ったほうを選びます. そして,どちらも中では線形探索をしています. 先頭から順に 1 つずつ比べているのです.書く手間が省けているだけで,手順は同じです.

自分で書けることと,道具を使うこと 「できあいの道具があるなら,自分で書く意味は無いのでは」と思ったかもしれません. 意味は 2 つあります.1 つは, 道具の中で何が起きているか分かるようになること (速いのか遅いのか,いつ使ってよいのかが判断できる). もう 1 つは,ぴったりの道具が無いときに自分で作れることです. たとえば「30 以上の数が最初に出てくるのはどの添字か」を求める道具は,Python にはありません. 自分で書くしかないのです.
虫食い虫食い 17-A 線形探索

リスト a の中から,入力された数を探して 「どの添字にあるか」を表示するプログラムです. ____ を埋めて完成させてください. 見つかったらすぐに探すのをやめること.

ヒント コンソールの入力行には 19 が入れてあります. 別の数(たとえば 100)に書き換えて, 「入っていません」になることも確かめてください.
fill16_linear.py
a = [12, 7, 25, 3, 19, 8]

target = int(input("探す数:"))

found = -1

for i in range(____(a)):
    if a[i] == ____:
        found = ____
        ____

if found >= 0:
    print(target, "は添字", found, "にありました")
else:
    print(target, "は入っていません")

17.4 いちばん小さいものを探す

並べ替えの土台になるのが最小値を探す手順です. ここが分かれば,並べ替えは半分できたようなものです.

17.4.1 「いまのところの最小値」を覚えておく

コンピュータは,人間のように全体を一目で見ることができません. 一度に 1 つずつしか見られないのです. ではどうするか.「いまのところ,いちばん小さかった数」を覚えておくのです.

  1. まず,先頭の数を「いまのところの最小値」だと思っておく.
  2. 2 つめ(添字 1)から順に見ていく.
  3. 見ている数が「いまのところの最小値」より小さければ,取り替える.
  4. 最後まで見終わったら,「いまのところの最小値」が本当の最小値になっている.
はじめ:先頭を最小値だと思う529173min = 5
図 17.4 まず先頭の 5 を「いまのところの最小値」にする.
2 は 5 より小さい → 取り替える529173min = 2
図 17.5 2 のほうが小さいので,最小値を 2 に取り替える.
9 は 2 より大きい → そのまま529173min = 2
図 17.6 9 は大きいので何もしない.
1 は 2 より小さい → 取り替える529173min = 1
図 17.7 1 に取り替える.以降 7・3 は大きいので変わらない.
minimum.py
# リストの中でいちばん小さい数を見つける

a = [5, 2, 9, 1, 7, 3]

m = a[0]                      # まず先頭を「いまのところの最小値」にする

for i in range(1, len(a)):    # 添字 1 から最後まで
    print("添字", i, "=", a[i], "/いまのところの最小値 =", m)
    if a[i] < m:
        m = a[i]              # 取り替える

print("最小値は", m)

range(1, len(a)) になっているところに注目してください. 先頭(添字 0)はすでに m に入れたので,添字 1 から調べれば十分です. range(len(a)) と書いても答えは同じですが, 添字 0 を自分自身と比べることになり,1 回分だけ無駄になります.

初めの値を,決め打ちの数にしない m の初めの値を a[0] ではなく, 0 や 100 のような決め打ちの数にする 書き方を見かけます.これは危ういやり方です.
  • 最大値を m = 0 から探すと,気温のようにマイナスがあるデータでは,答えがいつも 0 になってしまいます.
  • 最小値を m = 100 から探すと,101 点満点のテストが出てきたとたんに壊れます.
先頭の要素を初めの値にすると決めておけば, データの中身がどうであっても正しく動きます. 「あり得ない大きな数」を探して悩むより,こちらのほうが確実です.

17.4.2 「場所」を覚えておく

並べ替えのためには,最小値そのものよりも 「それがどの添字にあったか」のほうが必要です. 入れ替えるには場所が分からないといけないからです. 覚えておく変数を,値ではなく番号にします.

argmin.py
# いちばん小さい数が「どの添字」にあるかを見つける

a = [5, 2, 9, 1, 7, 3]

k = 0                         # いまのところ最小なのは添字 0

for i in range(1, len(a)):
    if a[i] < a[k]:           # 「値」ではなく a[k] と比べるのがポイント
        k = i                 # 覚えるのは番号

print("最小値", a[k], "は添字", k, "にあります")
番号と値を取り違えない if a[i] < a[k]: の a[k] を k と 書き間違えるのが定番の失敗です. k は番号,a[k] はその場所に入っている値. 比べたいのは値のほうです.エラーにはならず,答えだけが静かに狂います.

なお,最小値・最大値を求めるだけなら min(a)・max(a) という道具があります. 「どの添字か」まで知りたいときは a.index(min(a)) と書けますが, これはリストを2 回なぞることになります. 上の書き方なら 1 回で済みます.

虫食い虫食い 17-B 最大値の場所

今度はいちばん大きい数がどの添字にあるかを探し, その棒だけを赤くして描くプログラムです. ____ を埋めてください. 最小値のときと,どこが変わるでしょうか.

fill16_argmax.py
from turtle import *

def bars(vals, cols, y):
    x = -15 * len(vals)
    for i in range(len(vals)):
        fillcolor(cols[i])
        pu()
        goto(x, y)
        seth(0)
        pd()
        begin_fill()
        fd(24)
        lt(90)
        fd(vals[i] * 5)
        lt(90)
        fd(24)
        lt(90)
        fd(vals[i] * 5)
        lt(90)
        end_fill()
        x = x + 30

ht()
speed(0)

a = [5, 2, 9, 1, 7, 3]

k = 0
for i in range(1, len(a)):
    if a[i] ____ a[k]:
        k = ____

cols = []
for i in range(len(a)):
    if i == ____:
        cols.append("crimson")
    else:
        cols.append("skyblue")

bars(a, cols, 0)
print("最大値", a[k], "は添字", k)

done()

17.5 2 つの値を入れ替える

並べ替えのもう 1 つの部品が交換(スワップ)です. a[0] と a[3] の中身を入れ替えたい. 素直に書くと,こうしたくなります.

やってはいけない交換
a[0] = a[3]      # ← これをやると a[0] にあった値が消える
a[3] = a[0]      # ← もう a[0] は a[3] と同じ値になっている

両方とも a[3] の値になってしまいます. コップの水を入れ替えるのに,空のコップがもう 1 つ要るのと同じで, 一時的に値を置いておく場所が必要です.

正しい交換(多くの言語ではこう書く)
tmp = a[0]       # まず a[0] を避難させる
a[0] = a[3]
a[3] = tmp       # 避難させておいた値を入れる

この tmp のように,値を一時的にあずかるためだけに用意する変数を 退避用変数(たいひようへんすう)といいます. tmp(temporary の略)や temp という名前がよく使われます.

ところが Python では,これを1 行で書けます.

swap.py
# 2 つの値を入れ替える

a = [5, 2, 9, 1, 7, 3]
print("前:", a)

a[0], a[3] = a[3], a[0]      # 添字 0 と添字 3 を入れ替える

print("後:", a)

# ふつうの変数でも同じことができる
x = 100
y = 200
x, y = y, x
print("x =", x, "/ y =", y)
なぜ 1 行で書けるのか a, b = b, a は,右側の b, a を 先にまとめて読み取ってから,左側へ配ります. だから避難用の変数が要りません. Python の書き方のなかでも,とくに気持ちのよいものの 1 つです.
虫食い虫食い 17-C 両端から入れ替えて逆順にする

リストを逆順にします. 両端どうし(添字 0 と最後,添字 1 と最後から 2 つめ,……)を入れ替えていけば, 真ん中まで進んだところで逆順になります. ____ を埋めてください.

ヒント n // 2 回だけ繰り返せば十分です. 全部繰り返すと,入れ替えたものをもう一度入れ替えて元に戻ってしまいます. できあがったら a.reverse() や a[::-1] でも 同じことができると知っておいてください.
fill16_reverse.py
a = [5, 2, 9, 1, 7, 3, 6, 4]
print("前:", a)

n = len(a)

for i in range(n ____ 2):
    a[i], a[____] = a[____], a[i]

print("後:", a)

17.6 選択ソート — 最小値を選んで前に置く

材料がそろいました.最小値を探す手順と, 入れ替える手順です.この 2 つを組み合わせると,並べ替えができます.

考え方はこうです.

  1. 全体からいちばん小さいものを探し,添字 0 と入れ替える.これで添字 0 は確定.
  2. 添字 1 から後ろでいちばん小さいものを探し,添字 1 と入れ替える.これで添字 1 も確定.
  3. 添字 2 から後ろで……と,これを最後まで繰り返す.

この方法を選択ソート(セレクションソート,selection sort), 日本語では最小値選択法といいます. トランプを机に広げて並べ替えるとき,多くの人が自然にやる方法です.

17.6.1 1 ステップずつ見る

[5, 2, 9, 1, 7, 3] を並べ替えてみましょう. 各図,上の段が入れ替える前,下の段が入れ替えた後です. オレンジは確定したところ,赤は見つけた最小値, 黄は入れ替える相手(その回の先頭)です.

1 回目:添字 0 から後ろの最小は 1(添字 3)529173→ 添字 0 と添字 3 を入れ替える129573
図 17.8 1 回目.最小の 1 を先頭に持ってくる.添字 0 が確定した.
2 回目:添字 1 から後ろの最小は 2(添字 1)129573→ 自分自身なので入れ替えなし129573
図 17.9 2 回目.すでに正しい場所にあるので,入れ替えは起きない.
3 回目:添字 2 から後ろの最小は 3(添字 5)129573→ 添字 2 と添字 5 を入れ替える123579
図 17.10 3 回目.たまたまこれで全部そろったが,機械は最後まで確かめる.
機械は「もう並んでいる」ことに気づかない 図 17.10 で見た目はもう完成していますが,プログラムはそれを知りません. 人間には一目で分かることが,コンピュータには分からない. だから 4 回目・5 回目も同じ手順をきちんと実行します. ここが,手順を機械に渡すということの実感できるところです.

17.6.2 プログラムにする

selection.py
# 選択ソート(最小値選択法)

a = [5, 2, 9, 1, 7, 3]
print("はじめ:", a)

for i in range(len(a) - 1):       # 添字 0 から,最後の 1 つ手前まで
    k = i                         # 添字 i から後ろの最小値の場所を探す
    for j in range(i + 1, len(a)):
        if a[j] < a[k]:
            k = j
    a[i], a[k] = a[k], a[i]       # 見つけた最小値を添字 i に持ってくる
    print(i + 1, "回目:", a)

print("できあがり:", a)

繰り返しが二重になっています. 外側の i が「いまどの添字の場所を確定させようとしているか」, 内側の j が「そこから後ろを見て回る係」です.

場所意味
for i in range(len(a) - 1)確定させる場所を前から順に決める.最後の 1 つは自動的に決まるので - 1 でよい
k = i「いまのところ最小なのは添字 i」と思っておく(17.4.2 節と同じ)
for j in range(i + 1, len(a))i より後ろだけを見て回る.前は確定済みなので見なくてよい
a[i], a[k] = a[k], a[i]見つけた最小値を添字 i へ.i == k のときは何も変わらない
場所を覚えず,そのつど入れ替える書き方 同じ「前から確定させる」考え方でも,書き方はもう 1 つあります. 最小値の場所を覚えておくのをやめて, 自分より小さいものを見つけるたびに,その場で入れ替えてしまうやり方です.
python
for i in range(len(a) - 1):
    for j in range(i + 1, len(a)):
        if a[j] < a[i]:
            a[i], a[j] = a[j], a[i]    # 見つけるたびに入れ替える
結果はまったく同じです.ただし,入れ替えの回数が増えます. 上の書き方は 1 回の外回りにつき入れ替えが 1 回きりですが, こちらは小さいものが見つかるたびに入れ替えるからです. 17.1.1 節で見た「同じ結果を出す手順は 1 つではない」のよい例です. どちらを選ぶかは,読みやすさと手間のどちらを重く見るかで決まります.
1 ステップずつ,自分の目で追う 手元の VS Code なら,デバッガで 1 行ずつ止めながら値の変化を見られます (第 1 章 1.13 節). a[i], a[k] = a[k], a[i] の行にブレークポイントを置いて F5 で止め,あとは F10 を押していけば, i・k・a が 1 手ずつ変わっていく様子が, 17.6.1 節の図とそのままの順番で「変数」に出てきます.

17.6.3 並べ替えの途中を目で見る

さて,本題です.並べ替わっていく様子をタートルに描かせます. 1 回終わるごとに,そのときのリストを棒グラフにして 1 段ずつ下に描いていきます. いちばん上が並べ替える前,いちばん下ができあがりです.

絵に描かせるのは,あとから全体を見くらべるためです. これに対して,手元の VS Code のデバッガ(第 1 章 1.13 節)は, その場で 1 行ずつ止めて中を覗くためのものです. どちらも「途中を見る」道具ですが,向いている場面が違います.

selection_vis.py
# 選択ソートの途中経過を棒グラフで見る

from turtle import *

def bars(vals, fixed, y):
    # vals を棒グラフにして描く.先頭から fixed 個はオレンジ(確定済み)
    x = -13 * len(vals)
    for i in range(len(vals)):
        if i < fixed:
            fillcolor("orange")
        else:
            fillcolor("skyblue")
        pu()
        goto(x, y)
        seth(0)
        pd()
        begin_fill()
        fd(20)
        lt(90)
        fd(vals[i] * 4)
        lt(90)
        fd(20)
        lt(90)
        fd(vals[i] * 4)
        lt(90)
        end_fill()
        x = x + 26

ht()
speed(0)
pensize(1)
pencolor("dimgray")

a = [5, 2, 9, 1, 7, 3, 6, 4]

y = 180
bars(a, 0, y)                     # 並べ替える前の姿

for i in range(len(a) - 1):
    k = i
    for j in range(i + 1, len(a)):
        if a[j] < a[k]:
            k = j
    a[i], a[k] = a[k], a[i]
    y = y - 50                    # 1 段下へ
    bars(a, i + 1, y)             # そのときの姿を描く

done()
図 17.11 上から下へ,1 回ごとに棒が並び替わっていく様子.オレンジが確定した部分.

オレンジが左から 1 本ずつ増えていくのが見えます. これが選択ソートの動きです. 速さを「とてもゆっくり」にして実行すると,棒が 1 本ずつ描かれていくので, さらによく分かります.

やってみよう データを変えて試してください.a = [1, 2, 3, 4, 5, 6, 7, 8](すでに並んでいる)や a = [8, 7, 6, 5, 4, 3, 2, 1](逆順)だとどうなるでしょうか. 選択ソートは,すでに並んでいても手を抜きません. 毎回きちんと後ろ全部を見て回ります.この性質は 17.7 節の挿入ソートと対照的です.
虫食い虫食い 17-D 大きい順に並べ替える

選択ソートを書き換えて,大きい順(降順)に並べ替えます. ____ を埋めてください. 変えるところは,たった 1 か所です.

fill16_desc.py
a = [5, 2, 9, 1, 7, 3]
print("はじめ:", a)

for i in range(len(a) - 1):
    k = i
    for j in range(____, len(a)):
        if a[j] ____ a[k]:
            k = j
    a[i], a[k] = ____, ____
    print(i + 1, "回目:", a)

print("できあがり:", a)

17.7 挿入ソート — 手札を並べる要領

並べ替えの手順は 1 つではありません(17.1.1 節). まったく別の考え方を見てみましょう.

トランプの手札を配られたところを想像してください. 多くの人は,1 枚引いては,すでに並べてある札の正しい場所に差し込む という並べ方をします.これが挿入ソート(insertion sort)です. 日本語では挿入法といいます.

  1. 左端の 1 枚は,それだけで「並んでいる」とみなす.
  2. 次の 1 枚を取り上げる.これを手に持っている札とする.
  3. 並んでいる札を右から左へ見ていき,手札より大きい札は1 つ右へずらす.
  4. ずらせなくなったところ(=手札より小さい札に出会ったところ)に手札を置く.
  5. これを最後の 1 枚まで繰り返す.
1 と 9 の列に 3 を差し込む1939 を右へずらし,3 を置く139
図 17.12 挿入ソートの 1 ステップ.大きい札をずらして場所を空ける.
insertion.py
# 挿入ソート(挿入法)

a = [5, 2, 9, 1, 7, 3]
print("はじめ:", a)

for i in range(1, len(a)):
    v = a[i]                   # 手に持つ札
    j = i - 1                  # そのすぐ左から見ていく
    while j >= 0 and a[j] > v:
        a[j + 1] = a[j]        # 大きい札を 1 つ右へずらす
        j = j - 1
    a[j + 1] = v               # 空いた場所に手札を置く
    print(i, "回目:", a)

print("できあがり:", a)

while j >= 0 and a[j] > v: の条件は 2 つの意味を持っています. j >= 0 は「まだ左に札が残っている」, a[j] > v は「その札は手札より大きい」. どちらかが成り立たなくなったら,そこが置き場所です.

条件を書く順番には意味がある 条件の順番を入れ替えて while a[j] > v and j >= 0: と書くと, j が -1 になったときに a[-1] (=リストの最後の要素)を見てしまい,おかしな結果になります. Python の and は左から順に調べ,左が偽なら右を調べません. この性質を使って「安全を確かめてから中を見る」と書くのは,よくある作法です.
選択ソート挿入ソート
考え方後ろから最小値を選んで前に置く1 つずつ取り上げて正しい場所に差し込む
途中の見え方左から確定していく.右側はばらばらのまま左側は常に並んでいる.右側は手つかずのまま
入れ替えの回数多くても n 回(毎回 1 回だけ)ずらす回数はデータ次第.最悪だと多い
すでに並んでいるとき変わらず全部調べるほとんど何もしないで終わる(速い)
虫食い虫食い 17-E 挿入ソートの途中を見る

挿入ソートの途中経過を棒グラフで描きます. ____ を埋めて完成させてください. できあがったら選択ソート(図 17.11)と見比べて, 並び方の進み方がどう違うかを確かめてください.

fill16_insvis.py
from turtle import *

def bars(vals, fixed, y):
    x = -13 * len(vals)
    for i in range(len(vals)):
        if i < fixed:
            fillcolor("mediumseagreen")
        else:
            fillcolor("skyblue")
        pu()
        goto(x, y)
        seth(0)
        pd()
        begin_fill()
        fd(20)
        lt(90)
        fd(vals[i] * 4)
        lt(90)
        fd(20)
        lt(90)
        fd(vals[i] * 4)
        lt(90)
        end_fill()
        x = x + 26

ht()
speed(0)
pensize(1)
pencolor("dimgray")

a = [5, 2, 9, 1, 7, 3, 6, 4]

y = 180
bars(a, 1, y)

for i in range(1, len(a)):
    v = a[i]
    j = i - 1
    while j >= 0 and a[j] ____ v:
        a[j + 1] = ____
        j = j - 1
    a[____] = v
    y = y - 50
    bars(a, i + 1, y)

done()

17.8 バブルソート — となりどうしを比べる

3 つめの考え方を見ます. 選択ソートは「後ろ全部を見わたして最小値を選ぶ」, 挿入ソートは「正しい場所を探して差し込む」でした. バブルソート(bubble sort)は,もっと視野が狭くなります. 見るのはとなりどうしの 2 つだけです.

  1. 左端から順に,となりあう 2 つを比べる.
  2. 左のほうが大きければ,入れ替える.
  3. これを右端まで続けると,いちばん大きいものが右端に落ち着く.
  4. 右端は確定したので,次はその手前までで同じことを繰り返す.
1 回目:左から順に,となりどうしを比べる529173→ ひと回りすると,最大の 9 が右端へ送られる251739
図 17.13 バブルソートの 1 回目.となりどうしの比べっこを右端まで続けると,いちばん大きいものが右端に届く.

確定していく場所が,選択ソートとは逆だということに注目してください. 選択ソートは左から確定しましたが,バブルソートは右から確定していきます.

bubble.py
# バブルソート(隣接交換法)

a = [5, 2, 9, 1, 7, 3]
n = len(a)
print("はじめ:", a)

for i in range(n - 1):             # ひと回りを n - 1 回くりかえす
    for j in range(n - 1 - i):     # 右端の i 個は確定済みなので見ない
        if a[j] > a[j + 1]:        # となりどうしを比べて
            a[j], a[j + 1] = a[j + 1], a[j]    # 左が大きければ入れ替える
    print(i + 1, "回目:", a)

print("できあがり:", a)

内側の range(n - 1 - i) がこの手順のかなめです. j は a[j] と a[j + 1] の 2 つを見るので, j は最後の 1 つ手前までしか進めません(だから n - 1). さらに,ひと回り終わるごとに右端が 1 つずつ確定していくので, 見る範囲を i だけ狭められます.

なぜ「泡」なのか 「バブル(泡)」という名前は, 小さい値が,水の中の泡のように少しずつ上(左)へ上がっていく ように見えることから付きました. 大きい値がすとんと右端まで落ちるのに対して, 小さい値はひと回りにつき 1 つ分しか左へ進めません. 上の実行結果で,1 がどう動いていくかを目で追ってみてください.
選択ソート挿入ソートバブルソート
見るもの後ろ全部左どなりから順に左へとなりの 1 つだけ
確定する側左から左から右から
入れ替えの回数多くても n 回ずらす回数はデータ次第データ次第(挿入ソートのずらす回数と同じ).選択ソートより多い
すでに並んでいるとき変わらず全部調べるほとんど何もしない工夫すればひと回りで終われる
書きやすさふつうやや難しいいちばんやさしい

バブルソートは,3 つのなかでいちばん書くのがやさしく, いちばん手間がかかる方法です. 実用のために使うことはまずありませんが, 「となりを比べて入れ替えるだけで並べ替えができてしまう」という驚きがあるので, アルゴリズムの入門では必ず出てきます.

ひと回りして何も起きなかったら,終わってよい バブルソートには,簡単で効果の大きい工夫があります. ひと回りのあいだに 1 回も入れ替えが起きなかったら, もう全部並んでいるということです.そこでやめてしまえばよいのです.
python
for i in range(n - 1):
    kokan = 0                      # このひと回りで入れ替えた回数
    for j in range(n - 1 - i):
        if a[j] > a[j + 1]:
            a[j], a[j + 1] = a[j + 1], a[j]
            kokan = kokan + 1
    if kokan == 0:                 # 1 回も入れ替えなかった
        break                      # = もう並んでいるので終わり
17.3.2 節で覚えた break が,ここで効いてきます. こうすると,すでに並んでいるデータはひと回りで終わります. 選択ソートには,この手が使えません (毎回きちんと後ろ全部を見ないと,最小値が分からないからです). 手順を少し変えるだけで性質が変わる.これがアルゴリズムの面白さです.
虫食い虫食い 17-F バブルソートの途中を見る

バブルソートの途中経過を,1 回ごとに 1 段ずつ棒グラフで描きます. ____ を埋めてください. 確定するのは右側なので,色を塗る場所も右側になります.

ヒント できあがったら,選択ソート(図 17.11)と見くらべてください. オレンジが増えていく向きが逆になっているはずです.
fill16_bubble.py
from turtle import *

def bars(vals, kakutei, y):
    # 右端から kakutei 本ぶんをオレンジ(確定済み)にして描く
    n = len(vals)
    x = -13 * n
    for i in range(n):
        if i >= n - kakutei:
            fillcolor("orange")
        else:
            fillcolor("skyblue")
        pu()
        goto(x, y)
        seth(0)
        pd()
        begin_fill()
        fd(20)
        lt(90)
        fd(vals[i] * 4)
        lt(90)
        fd(20)
        lt(90)
        fd(vals[i] * 4)
        lt(90)
        end_fill()
        x = x + 26

ht()
speed(0)
pensize(1)
pencolor("dimgray")

a = [5, 2, 9, 1, 7, 3, 6, 4]
n = len(a)

y = 180
bars(a, 0, y)                      # 並べ替える前の姿

for i in range(n - 1):
    for j in range(n - 1 - ____):
        if a[j] ____ a[j + 1]:
            a[j], a[j + 1] = ____, ____
    y = y - 50
    bars(a, ____ + 1, y)           # 右から i + 1 本が確定した

print("できあがり:", a)

done()

17.9 できあいの道具 — sorted() と sort()

ここまで自分で並べ替えを書いてきました. しかし実際の仕事では,Python が用意した道具を使います. 速くて,正しくて,短いからです.

sorted.py
a = [5, 2, 9, 1, 7, 3]

# sorted(a) … 並べ替えた「新しいリスト」を返す.a はそのまま
b = sorted(a)
print("a =", a)
print("b =", b)

# a.sort() … a 自身を並べ替える.返り値は無い
a.sort()
print("a =", a)

# 大きい順
a.sort(reverse=True)
print("大きい順:", a)

# 文字列も並べ替えられる
w = ["ringo", "ichigo", "suika", "momo"]
print(sorted(w))                 # アルファベット順
print(sorted(w, key=len))        # 文字数の少ない順
書き方元のリスト返すもの使いどころ
sorted(a)変わらない新しい並べ替え済みリスト元の順番も残しておきたいとき
a.sort()並べ替わるNone(無し)元の順番はもう要らないとき
b = a.sort() は失敗する b = a.sort() と書くと,b には None が入ります. sort() はその場で並べ替えるだけで,何も返さないからです. 新しいリストが欲しいときは sorted() を使ってください. とてもよくある間違いです.
コラム:それでも自分で書けることが大事 では,自分で書く練習に意味はなかったのでしょうか.とんでもない.
  • 中で何が起きているか分かる.「データが 10 万件になったら時間がかかるかも」と想像できるようになります.
  • 道具が無い場面に出会う.「点数の高い順,同点なら名前の五十音順」のような並べ替えは,条件を自分で組み立てなければ書けません.
  • 手順を組み立てる力そのものが身につく.並べ替えはその練習台にすぎません.本当に鍛えているのは,問題を手順に分解する力です.
プロの料理人も,既製のソースを使います. それでも,一から作れる人と作れない人とでは, 出来合いの使い方も,味の直し方も,まったく違います.

17.10 計算の手間 — データが 2 倍になると

選択ソートは,データが n 個のとき何回比べているでしょうか. 数えてみます.1 回目は n−1 回,2 回目は n−2 回,……,最後は 1 回. 全部足すと n(n−1)/2 回です.

データの個数 n比べる回数(およそ)n が 10 倍になると
1045 回—
1004,950 回約 100 倍
1,000499,500 回約 100 倍
10,00049,995,000 回約 100 倍
100,000約 50 億回約 100 倍

データが 2 倍になると手間は約 4 倍, 10 倍になると約 100 倍です.n の 2 乗で増えるので, このような手順を「n の 2 乗に比例するアルゴリズム」といいます.

挿入ソートもバブルソートも,比べる回数は同じく n の 2 乗に比例します. 今回覚えた 3 つは,どれも同じ「n の 2 乗」の仲間なのです. 細かい速さの差はあっても,データが増えたときの苦しさは変わりません. 本当に速くしたければ,まったく別の手順に取り替える必要があります.

これは思ったより深刻です. 1 万件で 1 秒かかるプログラムは,100 万件では 1 秒 × 100 × 100 = 10000 秒,つまり約 3 時間かかります. パソコンを速いものに買い替えても,せいぜい数倍にしかなりません. 手順を変えるほうが,機械を変えるよりずっと効きます.

もっと速い手順がある 世の中には,もっと賢い並べ替えの手順があります. マージソートやクイックソートと呼ばれるもので, 手間は「n × log n に比例する」程度です. 100 万件でも数秒で終わります. sort() が中で使っているのも,その仲間です. くわしくは 2 年次の「データ構造とアルゴリズム」で習います. 今回は,「手順によって手間がまるで違う」ということだけ 実感しておいてください.

ところで,探すほうはどうでしょうか. 線形探索は,最悪の場合 n 回比べます(n に比例). しかしすでに小さい順に並んでいるなら, まん中を見て「探すものはこれより前か後ろか」を判断し, 毎回範囲を半分にしていく方法が使えます(二分探索).

1000 件なら 10 回ほどで見つかります. 並べ替えておくと,探すのが速くなる. これが,並べ替えがこれほど重要視される理由の 1 つです.

17.11 この章で出てきた書き方

書き方意味
len(a)リストの要素数
a[i]添字 i の要素.添字は 0 から
a.append(x)末尾に x を加える
a.pop()末尾を取り出して取り除く
a.pop(0)先頭を取り出して取り除く
x in ax が入っていれば True
a.index(x)x の添字(最初に見つかった場所).無ければエラー
a.count(x)x がいくつ入っているか
min(a) / max(a)最小値 / 最大値
sum(a)合計
a, b = b, a2 つの値を入れ替える
sorted(a)並べ替えた新しいリストを返す(a はそのまま)
a.sort()a 自身を並べ替える
a.sort(reverse=True)大きい順に並べ替える
a.reverse()a 自身を逆順にする
break繰り返しの途中で抜け出す
while 条件:条件が成り立つあいだ繰り返す

演習

出発点のプログラムを「▶ この演習を開く」で右側に読み込み, 各演習の「評価の目安」の必須を満たしてから,発展させて作品にしてください. できあがったら「⤓ 保存」でファイルに保存して,手元に残しておきましょう.

書く前に手順を決める まず日本語で手順を書いてからプログラムにしてください(17.1.3 節). 行きづまったら,紙にカードの絵を描いて,自分の手で動かしてみるのがいちばん速い近道です.
演習演習 17-1 10 枚のカードで遊ぶ

ウォーミングアップです.次の 4 つを,1 つのファイルの中で順に作ってください. カードの列は出発点の show(a, y) で描き,上から y = 170・85・0・-85・-170 の 5 段に並べます (出発点の show(a, 80) は show(a, 170) に変える).

  1. 1 から 10 までの数が書かれたカードを 10 枚作り,横一列に並べて描く(1 段目,y = 170).
  2. 1 枚ずつ別の入れ物 b へ移し(while と pop(0)・append),移すたびにコンソールに「○ を移しました.残り △ 枚」と表示する.最初は「1 を移しました.残り 9 枚」,最後は「10 を移しました.残り 0 枚」になる.移し終えた b を 2 段目(y = 85)に描く.
  3. b をかきまぜて(random.shuffle(b))3 段目(y = 0)に描き,いちばん小さいカードの添字を 17.4.2 節の方法で探して「最小の ○ は添字 △ にありました」と表示する.見つけたカードを先頭(添字 0)と入れ替えて,4 段目(y = -85)に描き直す.
  4. これもやりましょう:同じ方法で,いちばん大きいカードの添字を探して「最大の ○ は添字 △ にありました」と表示し,末尾(添字 len(b) - 1)と入れ替えて,5 段目(y = -170)に描き直す.先頭は 1,末尾は 10 になっているはず.
ヒント min()・max() や index() を使えば 1 行ですが,ここでは使わずに, 「いまのところ最小なのは添字 k」を覚えながら for で探してください. 入れ替えは b[0], b[k] = b[k], b[0] です. 最大を探すときは,if の不等号の向きを逆にします.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. while と pop(0)・append でカードを 1 枚ずつ別の入れ物へ移し,移すたびに「○ を移しました.残り △ 枚」と表示している
  2. かきまぜたあと,min()・index() を使わずに,最小のカードの添字を for と if で探して表示している
  3. 見つけた最小のカードを先頭と入れ替え,入れ替える前と後の両方を描いている
  4. 最大のカードの添字も同じように探して表示し,末尾と入れ替えた列を 5 段目に描いている(先頭 1・末尾 10)

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 入れ替えた 2 枚だけ色を変えて描いている
  • 最小と最大の入れ替えをくり返して,全部を小さい順に並べている(選択ソートの考え方)
  • かきまぜる前と後で,同じ位置に同じ数が残ったカードの枚数を数えて表示している

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 17-2 探すプログラム

カードの列を描き,入力された数がどこにあるかを探して, そのカードだけ色を変えて描き直すプログラムを作ってください.

  • カードは y = 0 の 1 段に,x = -23 * len(a) から 46 ずつ右へずらしてcard(x, 0, a[i], 色) で並べる.
  • 見つかったときは,そのカードを赤("tomato")く塗り,コンソールに「○ は添字 △ にありました」と表示する.たとえば 19 なら「19 は添字 4 にありました」.
  • 見つからなかったときは,「○ は入っていません」と表示する(たとえば 5).
  • 見つかったらそこで探すのをやめること(break).
  • これもやりましょう:探した道のりが分かるように,カードを 3 色で塗り分ける.見たけれど違ったカード(見つかった場所より前)は "lightgray",見つかったカードは "tomato",見ずに済んだカード(見つかった場所より後)は "lightyellow".見つからなかったときは全部見たので,全部 "lightgray".あわせて「○ 枚見ました」と表示する(19 なら 5 枚,5 なら 8 枚).
ヒント 出発点の card() は,4 つめの引数 col で塗る色を受け取れるようにしてあります. in や index() は使わずに,17.3 節の線形探索を自分で書いてください. コンソールの入力行には 19 を入れてあります.書き換えて試してください. 見つかった場所 found と添字 i を比べれば(i < found など), 3 色のどれにするかが決まります.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. in・index() を使わずに,for と if で線形探索を書き,見つかったら break で止めている
  2. 見つかったカードを赤く塗ってカードの列を描き,「○ は添字 △ にありました」と表示している(19 なら添字 4)
  3. 入っていない数を入れたときは「○ は入っていません」と表示している
  4. 見たカード・見つかったカード・見ずに済んだカードを 3 色で塗り分け,見た枚数を正しく表示している

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 同じ数が 2 枚以上あるリストにして,break せずに全部の場所を表示している
  • 17.10 節の二分探索で同じデータを探し(並べ替えてから),見た枚数を線形探索と比べている

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 17-3 バブルソートを改良する

17.8 節のバブルソートは,すでに並んでいるデータでも最後まで回り続けます. これを「ひと回りで 1 回も入れ替えなかったら終わる」ように改良し, データによって何回りで終わるかを比べてください.

  1. まず,日本語で手順を書き出す(どこで入れ替えた回数を 0 にし,どこで 1 増やし,どこで調べるか).
  2. ひと回りのあいだの入れ替えの回数を数え,0 回なら break で終える.
  3. 並べ替えを関数 bubble(a) にまとめ,何回りで終わったかを return で返す(第 14 章).
  4. ばらばら [6, 3, 8, 2, 9, 1, 5, 4]・すでに並んでいる [1, 2, 3, 4, 5, 6, 7, 8]・逆順 [8, 7, 6, 5, 4, 3, 2, 1] の 3 つのデータで bubble を呼び,回りの数を表示する.正しければ,ばらばらは 6 回り,すでに並んでいるものは 1 回り,逆順は 7 回りで終わる.3 つの回りの数が違う理由を,コメントで 1 行以上説明する.
  5. これもやりましょう:bubble の中で比べた回数(if a[j] > a[j + 1]: を調べた回数)も数え,return する前に「比較 ○ 回(改良前は 28 回)」と表示する.8 個のデータなら改良前は n(n−1)/2 = 28 回.改良後は,ばらばら 27 回・すでに並んでいる 7 回・逆順 28 回になる.
ヒント まず print で 1 回りごとのリストを表示して,正しく並べ替えられることを確かめてから, 回りの数を数える処理を足してください. break は,それが入っているいちばん内側の繰り返しだけを抜けます(17.3.2 節). どちらの for の中に書けばよいか,考えてから書きましょう.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. ひと回りで入れ替えた回数を数え,0 回なら break で外側の繰り返しを終えている
  2. 関数 bubble(a) が並べ替えを行い,何回りで終わったかを return で返している
  3. ばらばら・すでに並んでいる・逆順の 3 つのデータで正しく並び,それぞれの回りの数を表示している
  4. 3 つの回りの数(6・1・7)が違う理由を,コメントで 1 行以上説明している
  5. 比べた回数を数えて表示し,改良前の 28 回と比べている(27・7・28 回)

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 途中経過を棒グラフで 1 段ずつ描き,右から確定していく様子を見せている
  • データを 16 個にして,すでに並んでいるときと逆順のときの比較回数の差を確かめている
  • 左から右・右から左と交互に往復するシェーカーソートに作り変え,回りの数を比べている

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 17-4 アルゴリズムを改良しよう

17.6 節の選択ソートを,より良い手順に改良してください. 次の 3 つをすべて行います.

  1. 入れ替えを減らす.i == k のとき(すでに正しい場所にあるとき)は,入れ替えを行わない.実際に入れ替えた回数 swap_count と,省けた入れ替えの回数(len(a) - 1 - swap_count)を表示する.
  2. これもやりましょう:比べた回数を数える.comp_count を,if a[j] < a[k]: で比べるたびに 1 増やす.17.10 節の n(n−1)/2 と一致するか,両方を表示して確かめる(8 個なら 28 回).
  3. これもやりましょう:いろいろなデータで確かめる.出発点のデータに加えて,すでに並んでいる [1, 2, 3, 4, 5, 6, 7, 8],逆順 [8, 7, 6, 5, 4, 3, 2, 1],同じ数を含む [3, 1, 3, 2, 1, 2, 3, 1] の 4 つをリストのリスト datas(16.9 節)にまとめ,for a in datas: で回して,どれも小さい順に並ぶことを確かめる.

正しければ,入れ替えの回数は 4 つのデータの順に 6 回・0 回・4 回・4 回(省けたのは 1・7・3・3 回), 比べた回数はどれも 28 回になります.

ヒント 改良したら,必ず元の手順と同じ答えになるかを確かめてください. 速くしたつもりで壊してしまう,というのはよくあることです. for a in datas: の中で,swap_count と comp_count を データごとに 0 に戻すのを忘れないでください.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. i == k のときは入れ替えず,実際の入れ替え回数 swap_count と,省けた入れ替えの回数(len(a) - 1 - swap_count)を表示している
  2. 比べるたびに comp_count を 1 増やし,n(n−1)/2 と一致することを表示して確かめている
  3. 4 つのデータを datas にまとめて for で回し,どれも正しく小さい順に並んでいる
  4. 入れ替えの回数が 6・0・4・4 回,比べた回数がどれも 28 回と表示されている

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 両端から同時に:1 回のなかで最小値と最大値を同時に探し,最小値は前へ,最大値は後ろへ置いて,正しく並ぶことを確かめている
  • 比べた回数をデータの個数 n = 4, 8, 16 で表にし,n が 2 倍で約 4 倍になることを示している

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 17-5 身長の順に並ぶ

8 人の身長(cm)のリストがあります.これを小さい順に並べ替えて, 棒グラフで並べ替え前と後を見比べられるようにしてください.

  • 並べ替えは,自分で書いた選択ソートか挿入ソートを使うこと(sort()・sorted() は使わない).
  • 上段(y = 30)に並べ替え前,下段(y = -180)に並べ替え後を,出発点の bars で描く(並べ替え後は色を変える).
  • いちばん低い人・いちばん高い人の身長を,min()・max() を使わずに,並べ替えたあとの先頭と末尾(h[0]・h[len(h) - 1])から取り出して表示し,平均(sum(h) / len(h))も表示する.正しければ,低い人 149 cm・高い人 180 cm・平均 164.875 cm.
  • これもやりましょう:並べ替えたあとの h を先頭から順に見て,160 cm 以上の人が最初に現れる添字を探す(17.3 節の線形探索.見つかったら break).小さい順に並んでいるので,その添字から後ろは全員 160 cm 以上になる.人数を len(h) - 添字 で求め,「160 cm 以上は添字 3 から,5 人」の形で表示する.
ヒント 身長は 150〜180 くらいの数なので,そのまま棒の高さにすると画面からはみ出ます. (h - 140) * 3 のように, 基準を引いてから倍率をかけると,違いが見やすくなります.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. 選択ソートか挿入ソートを自分で書いて並べ替えている(sort()・sorted() は使わない)
  2. 上段に並べ替え前,下段に並べ替え後の棒グラフを描いている
  3. 並べ替えた結果の先頭と末尾(h[0]・h[len(h) - 1])からいちばん低い人(149)・高い人(180)を取り出し,平均(164.875)とともに表示している(min()・max() は使わない)
  4. 並べ替えたあとの h で 160 cm 以上が最初に現れる添字を for・if・break で探し,len(h) - 添字 で人数を求めて「160 cm 以上は添字 3 から,5 人」と表示している

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 名前のリストも作り,身長と一緒に入れ替えて,並べ替え後も名前と身長が対応している
  • 平均の高さに横線を引き,平均より高い人の棒だけ色を変えている

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

演習演習 17-6 名簿から探す

名前のリスト(名簿)から,キーボードで打ち込んだ文字を 名前の一部に含む人を全員探し出すプログラムを作ってください. ここまでの探索は「ぴったり同じ値」を探すものでしたが, 今度は一部だけ一致すればよいという探索です.

  • 文字列 b の中に文字列 a が含まれているかは,if a in b: で調べられる.
  • 見つかった人を全員コンソールに表示し,何人見つかったかを数えて「○ 件見つかりました」と出す.
  • 1 人も見つからなければ「見つかりませんでした」と出す.
  • 名簿をタートルで縦に並べて write し(x = -230,y = 170 から 46 ずつ下へ),見つかった人だけ色を変える.
  • たとえば「た」なら「たかだ ともこ」「たかはし たかし」の 2 件,「み」なら「ふじもと みつこ」「いまい みう」「まつおか みき」の 3 件,「ん」なら「きくち じゅん」の 1 件,「ぬ」なら「見つかりませんでした」.
  • これもやりましょう:見つかった人を新しいリスト mitsukatta = [] に append していき,探し終わったあとで「見つかった人:」に続けてfor で 1 人ずつ表示する.件数は数える箱 kensu と len(mitsukatta) の両方で求め,同じになることを確かめる.
ヒント 今回は全員ぶん調べ切る必要があるので, break は使いません. 「見つかったらやめる」探索(17.3.2 節)と, 「全部数え上げる」探索の違いに気づいてください. コンソールの入力行には た を入れてあります.書き換えて試してください.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. 名簿を for で全員ぶん調べ,in で部分一致した人を全員コンソールに表示している(break しない)
  2. 見つかった人数を数える箱で数え,「○ 件見つかりました」,0 件なら「見つかりませんでした」と表示している
  3. 名簿をタートルで縦に write し,見つかった人だけ色を変えている
  4. 見つかった人を mitsukatta に append し,探し終わったあとでまとめて表示している(件数が len(mitsukatta) と一致する)

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 「名字だけで探す」「名前の先頭が一致する人だけ」など,探し方を選べるようにしている
  • 見つかった文字の場所が分かるように,名前の横に印や件数の番号を書き添えている

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

自由演習 17-7 手順を見せる作品

自由制作です.アルゴリズムの動きが目で分かる作品を作ってください. テーマは自由ですが,たとえば次のようなものが考えられます.

  • 3 つのソート(選択・挿入・バブル)の途中経過を1 つの画面に並べて描き,進み方の違いを見くらべる.
  • シェーカーソートを自分で調べて実装する(バブルソートを,左から右・右から左と交互に往復させる方法です).
  • 棒グラフではなく,色の濃さや円の大きさでデータを表して並べ替える.
  • random で作ったばらばらのデータを並べ替え,毎回ちがう模様ができるようにする.
  • 二分探索(17.10 節)を実装し,見ている範囲がどんどん狭くなる様子を描く.
  • 並べ替えの途中を ontimer でアニメーションにする(第 10 章の復習).
ヒント 先頭にコメントで,題名・氏名・日付・ 「どんな手順を,どう見せようとしたか」を書いてください. 絵の美しさよりも,手順が伝わるかを採点します.
評価の目安(10 段階)

必須(すべて満たして正しく動けば 8)

  1. 並べ替えか探索を,sort()・sorted()・index() を使わずに自分で書いている
  2. 途中経過を何段(何コマ)か描くかアニメーションにして,アルゴリズムの動きが目で分かる
  3. 先頭のコメントに題名・氏名・日付を書いている
  4. 先頭のコメントに「どんな手順を,どう見せようとしたか」を日本語で書いている

発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)

  • 入れ替えた場所・見ている場所・確定した場所などを色で区別している
  • 2 つ以上の手順を同じデータで並べて描き,進み方の違いを見くらべられる
  • シェーカーソートや二分探索など,本文にない手順を自分で調べて実装している

必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6

まとめ

  • アルゴリズムとは,目的を達成するための手順.同じ結果を出す手順は 1 つではなく,良し悪しがある.
  • 線形探索は先頭から順に見ていく方法.見つかったら break で止める.
  • in は「あるか」,index() は「どの添字か」.どちらも中身は線形探索.
  • 最小値は「いまのところの最小値」を覚えながら 1 つずつ比べて探す.並べ替えには値ではなく場所(番号)を覚える.
  • a, b = b, a で 2 つの値を入れ替えられる.
  • 選択ソートは,最小値を選んで前から確定させる方法.挿入ソートは,1 つずつ正しい場所に差し込む方法.バブルソートは,となりどうしを比べて入れ替え,後ろから確定させる方法.
  • sorted() は新しいリストを返し,sort() はその場で並べ替える.道具は使う.それでも中身が分かっていることが大事.
  • データが 2 倍になると,選択ソートの手間は約 4 倍になる.手順を変えるほうが,機械を速くするより効く.

次の回では,辞書を習います. リストは「添字 0・添字 1」と番号で引く入れ物でした. 辞書は「りんご・みかん」のように名前で引ける入れ物です. データに意味を持たせられるようになると,作れるものが一気に広がります.