プログラミング演習・14章版 第12章 イテレータ・再帰・計算量
教材コード

イテレータ・再帰・計算量

同じ仕事を繰り返すとき,何を順番に取り出すか,いつ止めるか,入力が増えたときに処理がどれほど増えるかを区別して考えます.この章では,既習の for と while を出発点に,イテレータ,再帰,計算量まで進みます.

enumerate で番号と要素を取り出す

知識

enumerate は反復可能な対象から,連番と要素の組を順に返します.既定の番号は0から始まり,start=1 で1から始められます.for i, name in ... の二つの変数は,組の左側と右側をそれぞれ受け取ります.

身近な例として,出席簿に名前だけ並んでいるとき,読み上げながら「1番,春」「2番,夏」と番号を付ける仕事です.番号を自分で増やす変数を別に用意しなくてよいでしょう.

enumerate は「数え上げる」に由来します.関連語は number(番号)と enumeration(列挙)です.発表では “enumerate adds an index to each item” といいます.

そのまま演習知識 · そのまま演習
chapter-12_code_1.py
for i, name in enumerate(['春', '夏'], start=1):
    print(i, name)
ヒントを見る

enumerate は要素と番号を組にします。start を変更すると番号だけが変わり、要素の並びは変わりません。

演習の解説を見る

最初の組は (1, '春'),次の組は (2, '夏') です.start=0 に変えると番号は0と1になります.

実行結果の例
1 春
2 夏
虫食い知識 · 虫食い
chapter-12_code_3.py
for i, name in ___(['春', '夏'], start=1):
    print(i, name)
ヒントを見る

春と夏を取り出すたびに、その番号も一緒に受け取ります。start=1 は番号の開始値を指定する引数です。

演習の解説を見る

enumerate を補います.range だけでは要素の名前まで一緒には得られません.

結果を読み解く

enumerate(['春', '夏'], start=1) が返す一組目は番号1と「春」,二組目は番号2と「夏」です.for i, name はそれぞれの組を二つの変数へ分ける.したがって二行表示されます.

書き方 得られる値
for name in names 要素だけ
for i, name in enumerate(names) 0からの番号と要素
for i, name in enumerate(names, 1) 1からの番号と要素

よくある誤りは for i in enumerate(names) として i に整数だけが入ると思うことです.実際には番号と要素の組が入ります.両方を使うなら for i, name in enumerate(names) と書きます.

繰り返しの流れを制御する

知識

for は反復可能な対象から要素を順に受け取ります.break は現在のループを終了し,continue は現在の回の残りを飛ばして次の回へ進みます.どちらも最も内側のループだけに作用します.

身近な例として,箱が並ぶベルトコンベヤーを想像しましょう.for は箱を一つずつ調べます.赤い箱で作業そのものを終えるのが break,空箱だけを飛ばして次へ進むのが continue です.

for 文の処理の流れ
for 文の処理の流れ

用語:反復(iteration)は「再び行うこと」であり,英語の iterate に由来します.関連語はイテレータ(iterator)です.発表では “iterate over the values”(値を順に調べる)といえます.

そのまま演習知識 · そのまま演習

次を実行し,表示される数を予想してから確かめよ.

chapter-12_code_4.py
for x in [2, 0, 3, -1, 4]:
    if x == 0:
        continue
    if x < 0:
        break
    print(x)
ヒントを見る

continue は残りの処理を飛ばして次の反復へ進みます。break は繰り返し自体を終えるので、どこまで表示が続くかを区別します。

演習の解説を見る

2 は表示されます.0 では continue により表示を飛ばす.3 は表示されます.-1 では break により終了するので 4 には到達しません.

実行結果の例
2
3
虫食い知識 · 虫食い

最初の負数で止まり,0 を表示しないように空欄を埋めよ.

chapter-12_code_6.py
for x in [2, 0, -1, 4]:
    if x < 0:
        ___
    if x == 0:
        ___
    print(x)
ヒントを見る

負数に出会ったら反復全体を終え、0に出会った回だけ表示を飛ばします。二つの空欄は異なる働きの文です。

演習の解説を見る

上から break,continue を入れます.順序も大切です.負数を見た時点でループ全体を止める.0 のときだけ次の回へ進みます.

break と continue の違い

演習コードの一行目で値を一つ受け取ります.x == 0 なら continue により以降の行を実行せず次の値へ進みます.x < 0 なら break により次の値も取らず終了します.どちらにも当てはまらないときだけ print(x) に到達します.

命令 今回の残り 次の要素
continue 実行しない 調べる
break 実行しない 調べない
何も書かない 実行する 調べる

continue を「ループ終了」と取り違えると,0 の後の 3 が表示される理由を説明できません.continue は一回だけ飛ばす.また print(x) を二つの条件より上へ移すと,飛ばすはずの0や負数まで表示されます.

イテラブルとイテレータ

知識

イテラブル(iterable)は iter に渡すとイテレータを得られる対象です.イテレータ(iterator)は next ごとに要素を一つ返し,取り尽くすと StopIteration を送出します.リストはイテラブルであり,iter で得た値は取り出し位置を持つイテレータです.

身近な例として,本棚の本の列がイテラブル,本を次々手渡す係がイテレータです.係は今どこまで渡したかを覚えています.最後の本の後でもう一冊頼むと,「もうない」という合図が返る.

iter と next の流れ
iter と next の流れ

語の覚え方:iterable は iterate(繰り返す)に可能を表す able が付いた語です.iterator の or は「行うもの」を表します.関連語は iteration です.発表では “an iterator yields one item at a time”(イテレータは一度に一項目ずつ返す)といいます.

イテレータが要素を一つずつ取り出す流れ
そのまま演習知識 · そのまま演習
chapter-12_code_7.py
it = iter([10, 20])
print(next(it))
print(next(it))
ヒントを見る

このコードでは it の値を追ってください。next は呼び出すたびに次の要素へ進みます。要素を取り尽くしたあとには、通常の返り値ではなく例外が発生します。

演習の解説を見る

最初の next が 10,次が 20 を返します.もう一度 next(it) を呼ぶと StopIteration が起こる.これは空の値を返すこととは異なります.

実行結果の例
10
20

同じイテレータを二度使っても,位置は最初へ戻りません.最初から調べるには iter([10, 20]) を再度作ります.

虫食い知識 · 虫食い
chapter-12_code_9.py
it = ___([1, 2, 3])
print(___(it))
ヒントを見る

最初の空欄はリストから反復の状態を持つオブジェクトを作り、次の空欄はそこから一つの要素を取り出します。

演習の解説を見る

最初は iter,次は next です.表示は 1.for は,この「次を取る,終わったら止まる」を内部で扱う.

取り出し位置を確かめる

演習の一行目で [10, 20] からイテレータを作ります.最初の next(it) で10を返し,内部の位置が一つ進みます.次は20を返します.その後に三回目を呼ぶと残りがないため StopIteration になります.

対象 特徴
リスト 値を保持し,何度も先頭から調べられます.
リストから作ったイテレータ 現在の取り出し位置を持つ.
for 次の値を取り,終了の合図を内部で扱う.

next(it) を三回呼んで「空文字列が返る」と予想しません.終了は例外として通知されます.for ではこの終了を通常のループ終了として扱うため,普段は例外が画面に出ません.

再帰関数と停止条件

知識

再帰(recursion)は関数が自分自身を呼ぶ構成です.問題を小さくする再帰ステップと,それ以上呼ばない停止条件を対にします.階乗は 0! = 1,n! = n(n − 1)!(n ≧ 1)と定義します.

身近な例として,階段を一段ずつ下り,地面に着いたら戻ると考えます.下りる指示だけでは終わりません.「地面に着いたら止まる」という約束が必要です.戻るときに各段で計算を完成させる.

階乗の再帰呼び出し
階乗の再帰呼び出し

recursion はラテン語の「戻る」に由来します.関連語は recursive call(再帰呼び出し)と base case(停止条件)です.発表では “The base case stops the recursion” といいます.

再帰呼び出しと値が戻る順序
再帰計算を考えるための白紙図
再帰計算を書き込んだ図
そのまま演習知識 · そのまま演習
chapter-12_code_10.py
def fact(n):
    if n <= 1:
        return 1
    return n * fact(n - 1)

print(fact(4))
ヒントを見る

再帰では、停止条件に届くまで引数を小さくします。停止条件の返り値から順に戻りながら、各呼び出しの掛け算を確かめます。

演習の解説を見る

fact(4) は 4 × fact(3) を求めます.次に 3 × fact(2),2 × fact(1) と小さくなり,fact(1) が 1 を返します.戻りながら 2,6,24 となります.上の関数は負数にも 1 を返すため,実務で使うなら入力を非負整数に限定する確認が要る.

実行結果の例
24
虫食い知識 · 虫食い
chapter-12_code_12.py
def fact(n):
    if n <= 1:
        return ___
    return n * fact(___)
ヒントを見る

停止した場合の積の初期値と、停止に近づく次の引数を分けて考えます。再帰するたびに引数を減らします。

演習の解説を見る

順に 1,n - 1 です.二つ目を n にすると問題が小さくならず,通常は RecursionError になります.Python では呼び出し回数に上限があるため,大きな n の階乗には反復版が適します.

呼び出しと戻りを分ける

fact(4) の if n <= 1 は偽なので 4 * fact(3) を計算する必要があります.同様に fact(3) と fact(2) も次の呼び出しを待つ.fact(1) は条件が真で1を返します.そこから fact(2) は2,fact(3) は6,fact(4) は24を返します.

呼び出し 保留する計算 戻り値
fact(4) 4 × fact(3) 24
fact(3) 3 × fact(2) 6
fact(2) 2 × fact(1) 2
fact(1) 停止条件 1

停止条件だけあっても,次の呼び出しがそこへ近づかなければ終わりません.fact(n) の中で fact(n) を呼ぶのは誤りであり,fact(n - 1) とします.

処理回数から計算量を考える

知識

計算量(computational complexity)は入力の大きさ n に対し,処理時間や記憶領域の増え方を表します.一回走査はおおむね n 回で O(n),縦横それぞれ n 回の二重ループは n2 回で O(n2) です.ここでは実行時間そのものではなく,処理回数を比べます.

身近な例として,40人の出席を一人ずつ確認するなら40回ほどです.全員が全員と一回ずつ照合するような表を作ると,縦40行と横40列をたどり1600回になります.人数が倍なら前者は約2倍,後者は約4倍になります.

処理回数の比較
処理回数の比較

complexity は複雑さを表す語で,関連語は time complexity(時間計算量)と space complexity(空間計算量)です.O は order の頭文字で「増え方の階級」を表します.

そのまま演習知識 · そのまま演習
chapter-12_code_13.py
n = 4
count = 0
for i in range(n):
    for j in range(n):
        count += 1
print(count)
ヒントを見る

このコードでは n、count の値を追ってください。外側の1回に対して内側が何回実行されるかを数えます。総回数は外側と内側の回数を組み合わせて考えてください。

演習の解説を見る

外側の各回につき内側が4回動き,外側も4回なので 4 × 4 = 16 回です.n = 8 にすると 8 × 8 = 64 回になります.

実行結果の例
16
虫食い知識 · 虫食い
chapter-12_code_15.py
for i in range(n):
    for j in ___(n):
        count += 1
ヒントを見る

内側でも0から n の直前まで繰り返します。外側と同じ反復範囲を作る関数を使います。

演習の解説を見る

range を入れます.内側の回数が外側の回数ごとに繰り返されるので掛け算になります.ただし O(n2) は「どの入力でも実測秒数が厳密に n2 倍」という意味ではありません.

処理回数を表で比べる

n = 4 なら外側の for が4回動く.その一回ごとに内側が4回動く.count += 1 は合計16回実行されます.n = 8 なら8回を8組で64回です.

入力件数 n 一重ループ 二重ループ
4 4 16
8 8 64
100 100 10000

二重ループを常に O(n2) と決めつけません.内側の回数が一定なら全体は n に比例します.実際に「外側は何回か」「一回の外側に対して内側は何回か」を数えてから掛ける.

図で理解する

イテレータの進み方
イテレータの進み方
再帰の戻り
再帰の戻り
再帰の計算:考える前
再帰の計算:考える前
再帰の計算:書き込んだ後
再帰の計算:書き込んだ後

練習して確かめる

enumerate と添字

知識

enumerate は要素と0から始まる番号を同時に取り出します。start を指定すると番号の開始値を変えられます。

chapter-12_code_16.py
for i, name in enumerate(['春', '夏'], start=1):
    print(i, name)
そのまま演習enumerate と添字 · そのまま演習

例を実行し、start=0 に変えて比較します。

ヒントを見る

番号の開始値だけを変更します。春と夏の順番はそのままで、対応する二つの番号が変わります。

演習の解説

start=1 では 1 春、2 夏。start=0 では 0 春、1 夏。

虫食いenumerate と添字 · 虫食い

空欄を埋め、コードを実行してください。

chapter-12_code_17.py
for i, name in ___(['春', '夏'], start=1):
    print(i, name)
ヒントを見る

反復ごとに番号と名前の組を受け取る関数です。名前だけを取り出す通常の for と比べてください。

虫食いの解説
chapter-12_code_18.py
for i, name in enumerate(['春', '夏'], start=1):
    print(i, name)

enumerate を補います。

break と continue

知識

break はループを終え、continue はその回の残りを飛ばして次の回へ進みます。条件判定の位置によって結果が変わります。

chapter-12_code_19.py
for n in range(5):
    if n == 3: break
    print(n)
そのまま演習break と continue · そのまま演習

break を continue に変え、出力の差を記録します。

ヒントを見る

3になった回だけ表示を飛ばし、その後の4では反復を続けます。反復全体を終了する文との違いを確認します。

演習の解説

break なら 0,1,2 で終了。continue なら 3 だけ飛ばし 4 まで続く。

虫食いbreak と continue · 虫食い

空欄を埋め、コードを実行してください。

chapter-12_code_20.py
for n in range(5):
    if n == 3: ___
    print(n)
ヒントを見る

n が3になった時点で、その回だけでなく繰り返し全体を終える文を置きます。

虫食いの解説
chapter-12_code_21.py
for n in range(5):
    if n == 3: break
    print(n)

break を補います。

イテラブルとイテレータ

知識

for は反復可能な値から順に要素を取る。iter でイテレータを作り、next で次の要素を1つずつ取り出せます。尽きると StopIteration になります。

chapter-12_code_22.py
it = iter([10, 20])
print(next(it), next(it))
そのまま演習イテラブルとイテレータ · そのまま演習

3回目の next で発生する StopIteration を try/except で捕まえ、例外名を1行に表示してください。

ヒントを見る

二つの要素を取り出したあと、さらに進めると例外になります。三度目の呼び出しを例外処理の内側へ置きます。

演習の解説

要素は2つなので3回目に StopIteration。

虫食いイテラブルとイテレータ · 虫食い

空欄を埋め、コードを実行してください。

chapter-12_code_23.py
it = ___([10, 20])
print(next(it))
ヒントを見る

リストから順に取り出す状態を持つオブジェクトを作ります。次の行にある取り出し用の関数とは別の役割です。

虫食いの解説
chapter-12_code_24.py
it = iter([10, 20])
print(next(it))

iter を補います。

再帰関数

知識

再帰関数は自分自身を呼ぶ。停止条件を先に決め、各呼び出しで停止条件に近づける。深すぎる再帰には上限があります。

chapter-12_code_25.py
def factorial(n):
    if n <= 1: return 1
    return n * factorial(n - 1)
print(factorial(4))
そのまま演習再帰関数 · そのまま演習

factorial(5) の返値を1行で表示してください。

ヒントを見る

関数の停止条件と計算式を残し、呼び出し時の引数を5にします。小さい引数へ進む順と、戻るときの掛け算を追います。

演習の解説

factorial(5) は5 × 4 × 3 × 2 × 1を計算します。再帰が停止したあとに積が返るため、120を1行で表示します。

虫食い再帰関数 · 虫食い

空欄を埋め、コードを実行してください。

chapter-12_code_26.py
def factorial(n):
    if n <= 1: return 1
    return n * factorial(n - ___)
ヒントを見る

各呼び出しで次の小さい整数へ進めるようにします。減らす量が0だと停止条件へ近づきません。

虫食いの解説
chapter-12_code_27.py
def factorial(n):
    if n <= 1: return 1
    return n * factorial(n - 1)

1 を補います。引数が減るので停止条件に達します。

計算量を比べる

知識

入力件数 n が増えるときの処理回数を考えます。1重ループは概ね n 回、2重ループは概ね n² 回。大きなデータほど差が大きくなります。

chapter-12_code_28.py
n = 4
count = 0
for i in range(n):
    for j in range(n): count += 1
print(count)
そのまま演習計算量を比べる · そのまま演習

n を8に変え、処理回数の変化を比べます。

ヒントを見る

外側が8回進むそれぞれで、内側も8回進みます。count を増やす行がどちらの反復の内側かを確認します。

演習の解説

n=4 で16回、n=8 で64回。入力が2倍で回数は4倍。

虫食い計算量を比べる · 虫食い

空欄を埋め、コードを実行してください。

chapter-12_code_29.py
for i in range(n):
    for j in ___(n): count += 1
ヒントを見る

外側の各回に対して内側が n 回進むようにします。空欄は整数を順番に作る組み込み関数名です。

虫食いの解説
chapter-12_code_30.py
for i in range(n):
    for j in range(n): count += 1

range を補います。

章末演習(10問)

ここには解答を載せていません。

  1. 章末演習章末演習 1

    【基本】enumerate を使い,名簿に1から始まる出席番号を付けて表示し,番号が偶数の人だけを別のリストに集めてください。

    入力

    キーボード入力なし。名簿はプログラムの中で names = ["佐藤", "鈴木", "高橋", "田中"] とします。

    処理条件

    • for 文で enumerate(names, start=1) を回し,番号と名前を同時に受け取ります。range(len(names)) や names.index で番号を作りません。
    • 同じ for 文の中で,番号が偶数(2で割った余りが0)の名前を空リストに追加していく。スライス names[1::2] で代用しません。

    出力

    • 5行を出力します。1〜4行目は「番号: 名前」(半角コロンの直後に半角空白1つ),5行目は偶数番号の名前のリストを print したもの。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    1: 佐藤
    2: 鈴木
    3: 高橋
    4: 田中
    ['鈴木', '田中']
    
    ヒントを見る

    enumerate は何も指定しないと0から数え始めます。開始番号を変えると,偶数・奇数の判定結果がどう変わるかも考えましょう。

  2. 章末演習章末演習 2

    【基本】得点データを先頭から読み,負の値は入力ミスとして読み飛ばし,999 はデータの終わりの印として読み取りを打ち切って,有効な得点の合計と件数を求める関数 valid_total(data) を作ってください。

    入力

    キーボード入力なし。次の3つのリストを,この順に valid_total に渡す。

    [72, -1, 85, 90, 0, 999, 60]
    [999, 50]
    [10, -5, 20]
    

    処理条件

    • for 文で data を先頭から1つずつ調べます。999 なら break で打ち切り,負の値なら continue で次へ進みます。0 は有効な得点として数える。
    • 999 より後ろの値は数えません。999 がなければ最後まで調べます。
    • 合計と件数は for 文の中で変数に足し込んで求めます。sum・len・filter・リスト内包表記は使いません。
    • 関数は (合計, 件数) のタプルを返し,表示は呼び出し側で行います。

    出力

    • 3行を出力します。各行は「合計 値 件数 値」(各語と値の間は半角空白1つ)。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    合計 247 件数 4
    合計 0 件数 0
    合計 30 件数 2
    
    ヒントを見る

    1つの値について「終わりの印か」「入力ミスか」「有効か」を順に判定します。どの判定を先に置けば,3つのデータすべてで意図どおりになるかを考えましょう。

  3. 章末演習章末演習 3

    【基本】リストから iter でイテレータを作り,next で最初の要素だけを取り出したあと,for 文で残りを取り出してください。取り尽くしたイテレータに既定値付きの next を使った結果と,元のリストが何度でも使えることも確かめます。

    入力

    キーボード入力なし。データはプログラムの中で queue = ["受付", "検温", "診察"] とします。

    処理条件

    • it = iter(queue) でイテレータを作り,next(it) で最初の要素を取り出します。
    • 続けて for 変数 in it: で残りの要素を空リストに追加します。スライス queue[1:] で代用しません。
    • その後 next(it, "終わり") を呼び,戻り値を表示します。
    • 最後に元のリスト queue を for 文で回して要素数を数える。len は使いません。

    出力

    • 4行を出力します。順に「最初: 値」「残り: リスト」「追加の next: 値」「元のリストの要素数: 値」。コロンは半角で,その直後に半角空白1つ。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    最初: 受付
    残り: ['検温', '診察']
    追加の next: 終わり
    元のリストの要素数: 3
    
    ヒントを見る

    イテレータは「どこまで進んだか」を覚えていますが,リストそのものは位置を持ちません。for 文に渡したものがどちらなのかを意識しましょう。

  4. 章末演習章末演習 4

    【基本】0以上の整数 n の各桁の和を返す再帰関数 digit_sum(n) を作ってください。

    入力

    キーボード入力なし。次の5つの値を,この順に digit_sum に渡す。

    9045, 7, 0, 1000, 99999
    

    処理条件

    • 停止条件は「n が1桁(0〜9)のとき n を返す」とします。
    • それ以外は「一の位」と「一の位を除いた残りの数に対する digit_sum の結果」の和を返します。一の位と残りの数は % と // で求めます。
    • 関数の中では str への変換,for 文・while 文,sum を使わない(呼び出し側で5つの値を順に渡す for 文は可)。負の数は与えません。

    出力

    • 5行を出力します。各行は「n -> 各桁の和」(-> の前後に半角空白1つ。-> は半角のハイフンと不等号)。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    9045 -> 18
    7 -> 7
    0 -> 0
    1000 -> 1
    99999 -> 45
    
    ヒントを見る

    呼び出すたびに n の桁数が1つずつ減ることを確かめましょう。1桁になったら,それ以上分ける必要はありません。

  5. 章末演習章末演習 5

    【応用】リスト nums の中から k の倍数を先頭から探し,最初に見つかった値とその位置を返す関数 find_multiple(nums, k) を作ってください。

    入力

    キーボード入力なし。次の4組の (nums, k) を,この順に find_multiple に渡す。k は1以上の整数とします。

    ([12, 15, 22, 35, 40], 7)
    ([1, 2, 3], 7)
    ([14, 21], 7)
    ([], 7)
    

    処理条件

    • for 文で enumerate(nums, start=1) を回し,位置(1から数える)と値を受け取ります。
    • k で割った余りが0の値が見つかったら結果の文字列を変数に入れ,break で探索を打ち切る。ループの途中で return しません。
    • 見つからなかった場合の結果は,for 文の else 節で "なし" に設定します。フラグ変数や index で代用しません。
    • 見つかったときの結果は「値 は 位置 番目」(各語の間は半角空白1つ)の文字列とします。関数は結果の文字列を返し,表示は呼び出し側で行います。

    出力

    • 4行を出力する(空リストのときも「なし」)。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    35 は 4 番目
    なし
    14 は 1 番目
    なし
    
    ヒントを見る

    for 文の else 節がどんなときに実行され,どんなときに実行されないかを確かめましょう。空のリストでは繰り返しが1回も起きないことにも注意します。

  6. 章末演習章末演習 6

    【応用】昇順に並んだデータから値を探すとき,先頭から順に調べる線形探索と,範囲を半分ずつ狭める二分探索とで,要素を比べた回数を比べてください。

    入力

    キーボード入力なし。データはプログラムの中で data = list(range(0, 1000, 2))(0から998までの偶数500個)とします。目標値は 998 と 500 の2つで,この順に調べます。目標値は必ず data の中にあります。

    処理条件

    • 関数 linear_count(data, target) は先頭から1つずつ比べ,見つかったら break し,比べた要素の個数を返します。
    • 関数 binary_count(data, target) は lo = 0,hi = len(data) - 1 から始め,while lo <= hi: の中で mid = (lo + hi) // 2 の値と目標値を比べて範囲を狭める(data[mid] が目標値より小さければ lo を mid + 1 に,大きければ hi を mid - 1 にする)。見つかったら break し,while の繰り返し回数を返します。
    • 回数は「1回比べる(または1回 mid を調べる)たびに1増やす」ように数え,見つかった回も含める。in 演算子による存在確認,index,bisect モジュールは使いません。

    出力

    • 2行を出力します。各行は「目標 値: 線形探索 回数 回,二分探索 回数 回」。コロンは半角,読点は全角の「,」,それ以外の区切りは半角空白1つ。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    目標 998: 線形探索 500 回,二分探索 9 回
    目標 500: 線形探索 251 回,二分探索 8 回
    
    ヒントを見る

    回数を数える変数を増やす位置が,比較の前か後か,break の前か後かで結果が変わります。まず要素数の少ないデータで,手で数えた回数と一致するかを確かめましょう。

  7. 章末演習章末演習 7

    【応用】リストの中にリストが何重にも入ったデータを,1段のリストに平らにする再帰関数 flatten(items) を作ってください。

    入力

    キーボード入力なし。データはプログラムの中で data = [1, [2, [3, 4]], 5, [[6]], []] とします。要素はリストか整数のどちらかとします。

    処理条件

    • flatten は新しい空リストを用意し,items の要素を for 文で順に調べます。
    • 要素がリストかどうかは type(x) == list で判定します。リストなら flatten を呼び直した結果を extend でつなげ,そうでなければ append で加える。
    • 元の要素の並び順を保つ。itertools,文字列への変換,sum(..., []) は使いません。

    出力

    • 2行を出力します。1行目は flatten(data) の結果,2行目は flatten([]) の結果をそれぞれ print したもの。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    [1, 2, 3, 4, 5, 6]
    []
    
    ヒントを見る

    この関数では,中身が空のリストを受け取ると繰り返しが1回も起きずに終わります。それが停止条件の役割を果たすことを確かめましょう。

  8. 章末演習章末演習 8

    【複合】和が target になる2つの数の組を2通りの方法で求め,調べた回数を比べてください。

    入力

    キーボード入力なし。データはプログラムの中で nums = [3, 8, 1, 6, 4, 5],target = 9 とします。nums の値はすべて異なります。

    処理条件

    • 方法(1):for i, a in enumerate(nums): で位置 i と値 a を取り,内側の for 文で nums[i + 1:] の値 b と組にします。a + b を1回調べるたびに比較回数を1増やし,和が target なら (a, b) をリストに加える。
    • 方法(2):空の集合を用意して nums を先頭から1回ずつ調べ,1つ調べるたびに回数を1増やす。今の値を x としたとき,target - x がすでに集合にあれば (target - x, x) をリストに加え,その後 x を集合に加える。
    • 組の並び順はどちらの方法も見つかった順とします。itertools は使いません。

    出力

    • 2行を出力します。1行目は「二重ループ: 組のリスト 比較 回数 回」,2行目は「集合を使う: 組のリスト 調べた回数 回数 回」。コロンは半角,各要素の区切りは半角空白1つ。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    二重ループ: [(3, 6), (8, 1), (4, 5)] 比較 15 回
    集合を使う: [(8, 1), (3, 6), (4, 5)] 調べた回数 6 回
    
    ヒントを見る

    方法(2)では,今の値と組になる相手の値を先に計算し,それを「これまでに見た値」の中から探します。2つの方法で組の並び順が違って見える理由も考えましょう。

  9. 章末演習章末演習 9

    【複合】フィボナッチ数を求める再帰関数を2通り作り,fib(20) を求めるまでに関数が呼ばれた回数を比べてください。

    入力

    キーボード入力なし。求める値は n = 20 とします。

    処理条件

    • (1) 関数 fib(n) は,n < 2 なら n を返し,それ以外は fib(n - 1) + fib(n - 2) を返します。
    • (2) 関数 fib_memo(n) は,計算済みの値を関数の外で用意した辞書 memo = {} に覚えます。n < 2 なら n を返し,n が memo になければ fib_memo(n - 1) + fib_memo(n - 2) を計算して memo に入れ,memo[n] を返します。
    • 呼び出し回数は,関数の外で用意した辞書 calls = {"naive": 0, "memo": 0} を使い,各関数の先頭(停止条件の判定より前)で該当する値を1増やして数える。
    • fib(20) を先に,fib_memo(20) を後に,それぞれ1回だけ呼ぶ。functools.lru_cache や for 文による反復計算で代用しません。

    出力

    • 2行を出力します。各行は「関数名(20) = 値 呼び出し 回数 回」(= の前後と各要素の区切りは半角空白1つ)。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    fib(20) = 6765 呼び出し 21891 回
    fib_memo(20) = 6765 呼び出し 39 回
    
    ヒントを見る

    まず小さな n(たとえば 4)で呼び出しの木を紙に描き,同じ引数で何度も呼ばれている箇所を探しましょう。memo はその重複をどこで止めるのかを考えます。

  10. 章末演習章末演習 10

    【複合】CSV 形式の行を集めたリストを読み,見出しを分けて取り出したあと,行番号を付けながら得点を集計してください。不正な行は記録して読み飛ばし,終わりの印で読み取りを打ち切ります。

    入力

    キーボード入力なし。データはプログラムの中で次のように用意します。

    rows = ["name,score", "A,80", "B,abc", "C,95", "END", "D,70"]
    

    処理条件

    • it = iter(rows) でイテレータを作り,next(it) で1行目を取り出してカンマで分け,見出しのリストにします。
    • 残りの行は enumerate(it, start=2) で行番号(rows の中で1から数えた番号)を付けて for 文で処理します。
    • 行が "END" なら break で打ち切る。それ以外の行はカンマで名前と得点に分け,得点が isdigit() で数字だけと判定できなければ行番号をリストに記録して continue します。数字だけなら int に変換して辞書に「名前→得点」で加える。
    • csv モジュールは使いません。"END" より後ろの行は処理しません。

    出力

    • 3行を出力します。順に「見出し: リスト」「得点: 辞書」「不正な行: 行番号のリスト」。コロンは半角で,その直後に半角空白1つ。
    • 上記以外の行(見出し,入力の確認表示,デバッグ用の print など)は出力しません。

    実行例

    見出し: ['name', 'score']
    得点: {'A': 80, 'C': 95}
    不正な行: [3]
    
    ヒントを見る

    見出しを next で取り出した後のイテレータは,2行目から始まります。enumerate に渡す開始番号がそれと合っているかを確かめましょう。