Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pythonによるアルゴリズムとデータ構造

Avatar for Atsushi Atsushi
October 06, 2026

 Pythonによるアルゴリズムとデータ構造

Pythonを使って「アルゴリズムとデータ構造」の講義をやるとどうなるか?というのを考えて作成したスライドです。
型ヒントやdataclassを使って必要な要素は入れることができたかと思っています。

スライドはMarpを使って書いており、GitHubで公開しています。

Avatar for Atsushi

Atsushi

October 06, 2026

Other Decks in Education

Transcript

  1. Pythonのバージョン 応用Python 本資料は、Python 3.10以上を想定しています --version でバージョンを確認してください % python3 --version Python

    3.14.5 3.10未満の時は、新しいPythonをインストールしてください 3.10〜3.13では、後述の自己参照型( Node など)のために、次の行をファイルの 先頭に追加してください from __future__ import annotations 3
  2. 型 / type 応用Python データの種類や性質を表すものを「型(データ型)」と呼びます a = 10 / 3

    print(a) 整数どうしの / は浮動小数点数 float を返します。 a には float 型の値が入り、 3.3333333333333335 と表示されます a = 10 // 3 print(a) は商を切り下げます。この例は整数どうしなので、 int 型の 3 を返します ( 10.0 // 3 は float 型の 3.0 ) // 4
  3. よく使う型 組み込まれている型には以下のようなものがあります 型 意味 int 整数 float 浮動小数点数(実数の近似表現) str 文字列

    list リスト(本資料では配列として使用) dict 連想配列 None 値がないことを表す特別な値(型ヒントにも使う) 応用Python 5
  4. 型ヒント / Type Hints Pythonは「動的型付け」言語で、変数の型を事前に宣言する必要はありません。 型ヒントを記述すると、想定する型を読み手や型チェックツールに伝えられます a: int = 10

    // 3 int 2つを引数として float を返す関数 def division(a: int, b: int) -> float: return a / b 型チェック機能を備えたエディタや mypy などで、型の不整合を検出できます。 ただし、型ヒント自体が実行時に型を強制するわけではありません。 応用Python 6
  5. Union型 応用Python 複数の型を許容する時は | で併記します。 int | float は int

    または float を表し ます。 例:除数が0の時は None を返す関数 def division(a: int, b: int) -> float | None: if b == 0: return None else: return a / b 7
  6. 型のエラー 応用Python a: float | None = division(1,2) b =

    a * 0.2 # aがNoneの時、乗算できないのでチェックされます は float か None のどちらかです 2行目で乗算を行う時、 float の時は問題ないですが、 None の時は乗算ができないの で、型チェックで警告が出ます 型ヒントと型チェックを組み合わせると、実行前にエラーの可能性を見つけられます a 8
  7. isinstanceによる型絞り込み 前述のような場合、 isinstance で型を絞ることができます if isinstance(a, float): b = a

    * 0.2 # aはfloatと確定しています assert でも型を絞れます(最適化オプション -O では検査が省略されます) assert a is not None # aがNoneならAssertionErrorになります b = a * 0.2 応用Python 9
  8. フォーマット付き文字リテラル 応用Python Python 3.6で追加された f"" を使うと print() に渡す文字列を書きやすくなります。 { }

    内の変数が展開して表示されます total = 300 print(f"total = {total}") 小数点以下の桁数の指定など、各種フォーマットも指定できます pi = 3.1415926535 print(f"pi = {pi:.2f}") 10
  9. おまじない? 応用Python #! /usr/bin/env python3 Unix系の環境で、ファイルを直接実行する際に使う処理系を指定します。ファイルの 先頭に置き、実行権限を付けて使います。 def main(): print("Hello

    World") if __name__ == "__main__": main() このファイルをスクリプトとして実行した時だけ main() を呼び出し、インポートした 時には呼び出さないための記述です。 どこからプログラムがスタートするのかが分かりやすいため、このように記述するこ とを推奨します。 11
  10. 範囲について 範囲について 13 たとえば range(1, 5) とあった時、これは 1, 2, 3,

    4 を順に生成する範囲で、5は含みません 開始位置を含み、終了位置を含まない範囲(半開区間)です。リストの範囲では、下 図のように境界を考えると分かりやすくなります 0 1 0 2 1 3 2 4 3 range(1,5) 5 4 6 5 7 6 7
  11. 範囲について 範囲について 14 本資料では、これに則して「範囲」を示します。 たとえば、 left = 1 , right

    = 5 の時、対象は添字1〜4で、添字5は含みません 0 1 0 2 1 3 2 4 3 5 4 6 5 range(1,5) 具体的には以下のアルゴリズムの説明で範囲を使います クイックソート, マージソート, 二分探索 7 6 7
  12. 連除法の実装 実際に実装すると以下のようになる def gcd_short_division(a: int, b: int) -> int: d

    : int = 2 ans : int = 1 while d <= a and d <= b: if a % d == 0 and b % d == 0: a = a // d b = b // d ans = ans * d else: d = d + 1 return ans アルゴリズムとデータ構造 19
  13. 別解 : ユークリッドの互除法 最大公約数を求めるには、もっと効率の良い方法がある def gcd_euclidean(a: int, b: int) ->

    int: while b != 0: a, b = b, a % b return a アルゴリズムを知ると効率のよいプログラムが書けるようになる アルゴリズムとデータ構造 20
  14. 計算量 入力の大きさに対する、処理回数や必要なメモリの増え方を見積もる データ数などを 、基本操作の実行回数を とする ある正の定数 と が存在し、すべての で なら、

    と表す の例: 、 、 、 以下では、比較や代入などの基本操作を定数時間として数える 必要なメモリの増え方は「空間計算量」と呼ぶ オーダー計算 22
  15. 計算量の例(1) 1 def main(): n : int = int(input("n =

    ")) 1 n n s : int = 0 for i in range(1, n + 1): s = s + i 1 print(f"s = {s}") よって計算量は (ループ終了時の判定などは省略) より . オーダー計算 23
  16. オーダー計算 計算量の例(2) 1 1 log_2 n log_2 n log_2 n

    1 24 def main(): n : int = int(input("n = ")) s : int = 0 while n > 1 : n = n // 2 s = s + n print(f"s = {s}") 正の整数 に対し、繰り返し回数は 回になる ループ終了時の判定などを省けば ※ オーダー表記では の基数も無視して表記する 。よって計算量は
  17. ソート ソート 27 複数のデータを一定の規則に従って順番通りに並べ替えること。 5 2 6 3 1 10

    8 7 4 9 1 2 3 4 5 6 7 8 9 10 Sort 「どうやって並べ替えるか?」について、多くのアルゴリズムが提案されている
  18. アルゴリズムに書き下してみる 要素数 N の配列 data を昇順に並べ替える。添字は 0 から N −

    1 とする。 確定させる位置 i を N − 1 から 1 まで、1 ずつ減らしながら繰り返す 比較する位置 j を 0 から i − 1 まで、1 ずつ増やしながら繰り返す data[j] > data[j + 1] なら、両者を入れ替える この時点で、位置 i の値が確定する ソート | バブルソート 30
  19. ソースコードで書いてみる def bubble_sort(data: list[int]): n: int = len(data) for i

    in reversed(range(1, n)): for j in range(0, i): if data[j] > data[j + 1]: data[j], data[j + 1] = data[j + 1], data[j] ソート | バブルソート 31
  20. ソースコードの補足 len(data) range(1, n) reversed() a, b = b, a

    配列dataの要素数 1からn - 1までの整数列 整数列を逆転させる aとbをスワップ(入れ替え)する ソート | バブルソート 32
  21. ソート | バブルソート 計算量 計算量は 1 n - 1 n(n-1)/2

    n(n-1)/2 n(n-1)/2 33 (下の回数は概算。交換回数は入力に依存する) def bubble_sort(data: list[int]): n: int = len(data) for i in reversed(range(1, n)): for j in range(0, i): if data[j] > data[j + 1]: data[j], data[j + 1] = data[j + 1], data[j]
  22. ソースコードで書いてみる def insertion_sort(data: list[int]): n: int = len(data) for i

    in range(1, n): j: int = i - 1 while j >= 0 and data[j] > data[j + 1]: data[j], data[j + 1] = data[j + 1], data[j] j -= 1 ソート | 挿入ソート 36
  23. ソート | 挿入ソート 計算量 37 平均・最悪の計算量は (下の回数は逆順の入力での概算)(下の回数は概算。交 換回数は入力に依存する) 1 n-1

    n-1 n(n-1)/2 n(n-1)/2 n(n-1)/2 def insertion_sort(data: list[int]): n: int = len(data) for i in range(1, n): j: int = i - 1 while j >= 0 and data[j] > data[j + 1]: data[j], data[j + 1] = data[j + 1], data[j] j -= 1 の回数は、 data に依存する 最初からソートされている場合は、 while で終わる
  24. 再帰関数の例 1からnまでの自然数の和を求める関数 def sum_recursive(n: int): if n <= 0: return

    0 return n + sum_recursive(n - 1) 以下のような漸化式をイメージすると分かりやすいかもしれない 再帰関数 40
  25. 再帰関数 関数が呼び出される様子 の中で sum_recursive(2) が呼び出されて...とい うことを繰り返す 関数を繰り返し呼び出すので、スタックメ モリを沢山使う ※ 再帰の深さには上限があり、

    sys.getrecursionlimit() で確認できる (通常は1000程度) 41 sum_recursive(3) sum_recursive(3) 6 3+ sum_recursive(2) 3 2+ sum_recursive(1) 1 1+ sum_recursive(0) 0 0
  26. フィボナッチ数を再帰関数で実装してみる def fib(n: int) -> int: match n: case 0:

    return 0 case 1: return 1 case _: return fib(n - 1) + fib(n - 2) 再帰関数 43
  27. ソースコードの補足 はPython 3.10から追加されたパターンマッチングの構文 上から順番に評価される case 0 で n が0の時 case

    _ は全部マッチングする(それ以外の意味で最後につかう) ガード条件を使ってこんな風にも書けます match def fib(n: int) -> int: match n: case x if x <= 1: return x case _: return fib(n - 1) + fib(n - 2) 再帰関数 44
  28. 再帰関数 関数が呼び出される様子 45 fib(5) fib(2) + fib(3) fib(1) + fib(2)

    1 fib(1) + fib(2) 0 1 fib(2) + fib(3) fib(1) + fib(2) 0 1 fib(1) + fib(2) 1 fib(1) + fib(2) 0 1
  29. 計算量 再帰関数 フィボナッチ数を再帰関数で実装した時の計算量は n 関数呼出回数 5 15 10 177 20

    21,891 30 2,692,537 n = 40では約3億3千万回の関数呼び出しが発生し、実行に時間がかかる 46
  30. ループで実装する この場合の計算量は def fib_loop(n: int) -> int: if n ==

    0: return 0 fib: list[int] = [0 for _ in range(0, n + 1)] fib[1] = 1 for i in range(2, n + 1): fib[i] = fib[i - 2] + fib[i - 1] return fib[n] この解き方は「動的計画法」という名前がついているが、この資料のスコープ外 再帰関数 47
  31. [発展] メモ化再帰 / Memoization 関数の返り値をキャッシュしておき、同じ引数で呼び出された時、その値を返す Pythonの場合 @cache をつけるだけでOK from functools

    import cache @cache def fib(n: int) -> int: match n: case x if x <= 1: return x case _: return fib(n - 1) + fib(n - 2) この時の計算量は になる 再帰関数 49
  32. ソート | クイックソート クイックソートの動作例 52 2 17 39 6 23

    1 5 24 11 2 6 1 5 11 17 39 23 24 2 6 1 5 17 39 23 24 2 1 5 6 17 23 24 39 2 1 6 17 23 39 1 2 6 17 23 39 1 2 17 23 5 6 11 24 39
  33. ソート | クイックソート クイックソートの実装 53 pivotを決め、配列を前半と後半に分ける関数 partition() を使うと以下のように書 ける def

    quick_sort(data: list[int], left: int, right: int): if right - left <= 1: return pivot_index: int = partition(data, left, right) quick_sort(data, left, pivot_index) quick_sort(data, pivot_index + 1, right) left pivot_index right left pivot_index + 1 pivot_index right
  34. ソート | クイックソート partition関数の動き 54 partition(data:list[int], left: int, right: int)

    -> int 対象範囲は空でないことを前提とする( quick_sort では要素が2個以上の時だけ呼 ぶ) 1. pivotを決める(今回は一番右側の要素) 2. pivotとの大小で要素を並べ替え、pivotを左右の境界に置く 3. pivotの位置を返す 8 15 43 35 12 57 2 40 24 [0] [1] [2] [3] [4] [5] [6] [7] [8] 2 8 12 15 24 35 40 43 57 [0] [1] [2] [3] [4] [5] [6] [7] [8]
  35. ソート | クイックソート partition関数の動き | 並び替えの詳細 1. rightを1減らす(pivotの分) 2. data[left]

    < pivot ならleftを増やし、 data[right - 1] >= pivot ならrightを減らす (いずれもleft < rightの間) 3. left < rightであればleftの要素とright - 1の要素 を入れ替える 4. 2, 3を left < rightの間繰り返す 5. pivotとleftの位置の要素を入れ替える n個の要素を1回ずつ参照するので計算量は 55 left left 8 15 right right right 43 35 12 57 left 8 15 15 40 24 right 2 left 8 2 35 12 left 2 57 right 35 12 left 57 43 40 24 right 43 40 24 right 8 15 2 12 35 right left 57 43 40 24 8 15 2 12 35 57 43 40 24 24 57 43 40 35 left 8 15 2 12
  36. partition関数の実装 ソート | クイックソート def partition(data: list[int], left:int, right:int) ->

    int: pivot_index: int = right - 1 pivot: int = data[pivot_index] right = right - 1 while left < right: while left < right and data[left] < pivot: left += 1 while left < right and data[right - 1] >= pivot: right -= 1 if left < right: data[left], data[right - 1] = data[right - 1], data[left] data[pivot_index], data[left] = data[left], data[pivot_index] return left 56
  37. partition関数の変更 ソート | クイックソート partition関数の最初を少し変えれば残りはそのまま使える def partition(data: list[int], left: int,

    right: int) -> int: pivot, pivot_index = median3( data, left, right - 1, (left + right - 1) // 2 ) data[pivot_index], data[right - 1] = data[right - 1], data[pivot_index] pivot_index = right - 1 right -= 1 while left < right: # 以下は元のpartition関数と同じ(省略) ... 60
  38. ソート | マージソート マージソートの概要 62 1 8 55 80 2

    10 24 72 8 55 80 2 10 24 72 55 80 2 10 24 72 1 既にソートしてあるリストが2つあったとき、先頭か ら小さい方を順番に選んでいけばソートできる 2つのリストをどうやってソートするか? → マージソート (再帰関数) 1 1 1 2 8 1 2 8
  39. マージソートのソース data[left] から data[right - 1] までをソートする関数 def merge_sort(data: list[int],

    left: int, right: int): if right - left <= 1: return mid: int = (left + right) // 2 merge_sort(data, left, mid) merge_sort(data, mid, right) merge_list(data, left, mid, right) ソート | マージソート 63
  40. リストをマージする部分 (前半) ソート | マージソート 前半と後半のどちらかの要素をすべて取り出すまでループする( p 、 q は各範囲の現

    在位置) def merge_list(data: list[int], left: int, mid: int, right: int): p : int = left q : int = mid working : list[int] = [0] * (right - left) s : int = 0 while p < mid and q < right: if data[p] < data[q]: working[s] = data[p] p += 1 else: working[s] = data[q] q += 1 s += 1 64
  41. リストをマージする部分 (後半) 余った分を working に入れていく while p < mid: working[s]

    = data[p] s += 1 p += 1 while q < right: working[s] = data[q] s += 1 q += 1 data[left:right] = working ソート | マージソート 65
  42. ソースコードの補足 [0] * (right - left) data[left:right] = working ソート

    | マージソート 長さ (right - left) の値が全部0のリストを作る data[left] から data[right - 1] に working を代入す る。 working の長さが right - left なので丁度おさま る 66
  43. Pythonのソートは何なのか? 最近の言語ではソートは用意されているので実装することは少ない b: list[int] = sorted([73, 13, 30, 95]) CPython(Pythonのリファレンス実装)で使われているアルゴリズムは?

    Timsortを基礎とする安定ソート 整列済みの区間を利用し、挿入ソートとマージを組み合わせる 3.11以降は、マージ順序の決定にPowersortを採用 計算量の平均は , 最善値は ソート | まとめ 70
  44. 探索 | 二分探索 二分探索の動作 78 1. lo = 0, hi

    = n とする 2. lo < hiなら、mid = (lo + hi) // 2 とする(そうで なければ5へ) 3. midの位置の要素とキーを比較する 等しければmidを返す キーが大きければ lo = mid + 1 キーが小さければ hi = mid 4. lo < hi の間2, 3を繰り返す 5. 見つからなければNoneを返す mid lo 2 4 5 2 2 4 4 4 8 12 15 18 22 24 28 32 mid lo 2 7 5 7 7 Since 15 > 8, hi = mid hi 8 12 15 18 22 24 28 32 lo 5 hi Since 7 < 8, lo = mid + 1 mid hi 8 12 15 18 22 24 28 32 Since 12 > 8, hi = mid mid lo hi 5 7 8 12 15 18 22 24 28 32 Since 8 < 8, I found it
  45. 二分探索の実装 def binary_search(data: list[int], key:int) -> int | None: lo

    : int = 0 hi : int = len(data) while lo < hi: mid : int = (lo + hi) // 2 if data[mid] == key: return mid elif data[mid] > key: hi = mid else: lo = mid + 1 return None 探索 | 二分探索 79
  46. 探索 | ハッシュ探索 ハッシュ探索 もしキーが 1〜5の整数値だけだったら? 5要素のリストを用意し、添字 キー - 1

    で 有無を管理 キーの範囲が大きい場合 ハッシュ関数を用意してインデックスを決 める キーと合っているかは確認する 81 1 key 4 2 3 4 5 key 14876 hash function hash() hash code 4 1 2 3 4 14876 5
  47. ハッシュ関数 ハッシュ関数には以下のような事が求められる 低コスト : 計算コストが十分小さい 決定性 : 同じキーに対するハッシュ値は等しい 一様性 :

    キーに対してハッシュ値がばらけている 今回は、以下のようなハッシュ関数を使う HASH_SIZE: int = 190979 HASH_COEFF: int = 67 def hash_func(key:int) -> int: return (key * HASH_COEFF) % HASH_SIZE 探索 | ハッシュ探索 82
  48. ハッシュテーブルへの挿入 ハッシュ値を計算して、テーブルの該当する場所にキーを代入する もし、すでに値が入っていれば False を返す def insert(hash_table: list[int|None], key:int) ->

    bool: hash_value : int = hash_func(key) if hash_table[hash_value] is None: hash_table[hash_value] = key return True else: return False 探索 | ハッシュ探索 84
  49. ハッシュテーブルからの削除 ハッシュ値を計算して、テーブルの該当する場所を None にする def remove(hash_table: list[int|None], key:int) -> bool:

    hash_value : int = hash_func(key) if hash_table[hash_value] == key: hash_table[hash_value] = None return True else: return False 探索 | ハッシュ探索 86
  50. 探索 | ハッシュ探索 衝突 / Collision 87 異なるキーから同じハッシュ値が得られる現象を「衝突」と呼ぶ ここまでの単純な実装では、衝突したキーを追加できない 衝突の解決方法として、チェイン法やオープンアドレス法などがある

    チェイン法 / Separate Chaining ハッシュテーブルからリストなど他のデータ構造につなげる オープンアドレス法 / Open Addressing 衝突した場合、次のエントリなどハッシュテーブルの他の場所に保存する Separate Chaining 1 1 2 1456 3 Collision 4 5 Open Addressing 1 2 1456 852 2 1456 3 4 5 852 1456 3 4 852 5 1456
  51. オープンアドレス法 探索 | ハッシュ探索 ハッシュテーブルは、キーの他に「空き」と「削除済」の2つの値を持つ from enum import Enum class

    Marker(Enum): EMPTY = "empty" DELETED = "deleted" def main(): hash_table : list[int|Marker]= [Marker.EMPTY for _ in range(0, HASH_SIZE)] 88
  52. オープンアドレス法 | 挿入 def insert(hash_table: list[int|Marker], key:int) -> bool: index

    : int = hash_func(key) insert_index : int | None = None for _ in range(HASH_SIZE): match hash_table[index]: case value if value == key: return True case Marker.EMPTY: if insert_index is None: insert_index = index break case Marker.DELETED: if insert_index is None: insert_index = index index = (index + 1) % HASH_SIZE if insert_index is None: return False else: hash_table[insert_index] = key return True 探索 | ハッシュ探索 89
  53. オープンアドレス法 | 検索 def search(hash_table: list[int|Marker], key:int) -> bool: index

    : int = hash_func(key) for _ in range(HASH_SIZE): match hash_table[index]: case value if value == key: return True case Marker.EMPTY: return False index = (index + 1) % HASH_SIZE return False 探索 | ハッシュ探索 90
  54. オープンアドレス法 | 削除 def remove(hash_table: list[int|Marker], key:int) -> bool: index

    : int = hash_func(key) for _ in range(HASH_SIZE): match hash_table[index]: case value if value == key: hash_table[index] = Marker.DELETED return True case Marker.EMPTY: return False index = (index + 1) % HASH_SIZE return False 探索 | ハッシュ探索 91
  55. Pythonでの実装 多数のデータを扱うときは、計算量を考えて選択する必要がある list の探索 : 線形探索 a : list[int] =

    [ 1, 3, 5, 7 ] if 3 in a: print("True") set , dict の探索 : ハッシュ探索 a : set[int] = set([ 1, 3, 5, 7 ]) if 3 in a: print("True") 探索 | まとめ 94
  56. 構造体を使わないプログラム例 import math def main(): x1: float = float(input("x1 =

    ")) y1: float = float(input("y1 = ")) x2: float = float(input("x2 = ")) y2: float = float(input("y2 = ")) d: float = math.sqrt( math.pow(x1 - x2, 2) + math.pow(y1 - y2, 2) ) print(f"distance = {d:.3f}") if __name__ == "__main__": main() 構造体 98
  57. 構造体 構造体 99 プログラムを書く人が、x1, y1 がセットで1つの座標を示していることを常に意識する 必要がある x1: float =

    float(input("x1 = ")) y1: float = float(input("y1 = ")) 座標を表していることがわかるように (x, y) のセットにPointという名前をつけたい Point x y
  58. 構造体の定義 という標準ライブラリから dataclass 機能を読み込む class の下に構造体のメンバ変数をならべる 以下の Point という構造体は x

    と y の2つの float の値を持つ dataclasses from dataclasses import dataclass @dataclass class Point: x: float y: float Pythonに構造体 (struct) はないのでクラスを使う。そのため「クラス」と呼ぶのが正しいが、本資料では、複 数のデータをまとめる目的にしか使わないので、構造体とクラスを区別せず「構造体」と呼ぶ。 構造体 100
  59. 構造体 | インスタンス化 新しく Point というデータ型があるように扱える。 変数を作るときは定義した順番に引数を渡す。 p : Point

    = Point(1, 2) print で表示も可能 print(p) Point(x=1, y=2) メンバ変数名を指定するキーワード引数なら、定義順と異なっていても渡せる p: Point = Point(y=2, x=1) 構造体 101
  60. 構造体 | メンバ変数へのアクセス メンバ変数には .x , .y のようにしてメンバ変数名を使ってアクセスする p :

    Point = Point(5, 3) print(f"Position ({p.x}, {p.y})") 書き込みも可能 p.x = 4 p.y = 2 構造体 102
  61. 構造体の定義 | 初期値 定義の右側に = をつけることで初期値を設定できる @dataclass class Point: x:

    float = 0 y: float = 0 初期値を設定したメンバ変数は、引数を省略できる p : Point = Point() 構造体 103
  62. 構造体の定義 | 初期値の注意点 定義の時に必ず初期値がないメンバ変数 → 初期値があるメンバ変数の順にならべる 以下のコードはエラーになる @dataclass class Point:

    x: float = 0 y: float リストなどの変更可能な値を初期値にする場合は、`field(default_factory=list)`などを使う。詳細は本資料の 対象外とする 構造体 104
  63. 構造体を引数とする関数 | 値が変更される例 ただし、構造体を引数とした時、関数内で値を変更すると反映されるので注意 def set_x_five(p: Point): p.x = 5

    def main(): p: Point = Point(0, 0) set_x_five(p) print(f"({p.x}, {p.y})") (5, 0) Pythonでは、list や dictも関数内で変更すると関数外にも反映される。 構造体 106
  64. [参考] intやfloatでは値が変更されない や float の値自体は変更できない。引数 a への再代入は、呼び出し元の変数 x には影響しない int

    def set_five(a: int): a = 5 def main(): x: int = 0 set_five(x) print(x) # 0が表示される 0 引数への再代入と、参照先のオブジェクトの変更を区別する。再代入は`list`や`dict`でも、呼び出し元の変数 には影響しない 構造体 107
  65. 構造体 構造体を引数とする関数 | 値が変更されないようにする 108 元の Point の値を変更したくない時は、コピーして渡す import copy

    def set_x_five(p: Point): p.x = 5 def main(): p: Point = Point(0, 0) set_x_five(copy.copy(p)) print(f"({p.x}, {p.y})") (0, 0) ただし、コピーの処理コストがかかる。 copy.copy は浅いコピーなので、メンバがリ ストなどの場合、そのリストは共有される
  66. 構造体を戻り値とする関数 例: 2つの値をユーザから入力してもらい、 Point 構造体を返す関数 def input_point() -> Point: x:

    float = float(input("x = ")) y: float = float(input("y = ")) p: Point = Point(x, y) return p 構造体 109
  67. 構造体を使って書き直す これまでの構造体, 関数を使うと以下のように書ける def main(): print("p1:") p1: Point = input_point()

    print("p2:") p2: Point = input_point() d = distance(p1, p2) print(f"distance = {d:.3f}") 構造体 110
  68. スタックの実装 定義 top : 次に格納する位置(格納済みの要素数)を示す。初期値は0 data : データを保存するスタックの実体 @dataclass class

    Stack: data: list[int] top: int = 0 宣言時にはデータを保存するリストを渡す def main(): stack : Stack = Stack([0] * 10) データ構造 | スタック 113
  69. Push の位置に値を入れる。 もし、dataがいっぱいなら False を返す top def push(stack: Stack, value:

    int) -> bool: if stack.top >= len(stack.data): return False stack.data[stack.top] = value stack.top += 1 return True データ構造 | スタック 114
  70. Pop を1減らし、その位置の値を取り出して返す スタックが空なら None を返す top def pop(stack: Stack) ->

    int | None: if stack.top <= 0: return None stack.top -= 1 value : int = stack.data[stack.top] return value データ構造 | スタック 115
  71. データ構造 | キュー キュー / Queue 待ち行列のようなデータ構造 enqueue キューの一番後ろにデータを入れる dequeue

    キューの先頭のデータを取り出す FIFO (First In, First Out) とも呼ぶ 117 8 dequeue 8 5 9 8 5 9 8 enqueue
  72. データ構造 | キュー リングバッファ / Ring Buffer tail [7] [0]

    [4] [3] [2] [5] [6] [1] キューの実装方法の1つ head キューの先頭 次に取り出されるデータの位置 tail キューの末尾 次にデータが挿入される位置 count キューに入っているデータの数 119 head
  73. キューの実装 定義 @dataclass class Queue: data: list[int] head: int =

    0 tail: int = 0 count: int = 0 宣言時にはデータを保存するリストを渡す def main(): queue : Queue = Queue([0] * 10) データ構造 | キュー 120
  74. enqueue の位置にデータを入れる もし、キューがいっぱいなら False を返す tail def enqueue(queue: Queue, value:

    int) -> bool: n : int = len(queue.data) if queue.count >= n: return False queue.data[queue.tail] = value queue.tail = (queue.tail + 1) % n queue.count += 1 return True データ構造 | キュー 121
  75. dequeue の位置のデータを取り出して返す もし、データがないなら None を返す head def dequeue(queue: Queue) ->

    int | None: n : int = len(queue.data) if queue.count <= 0: return None value : int = queue.data[queue.head] queue.head = (queue.head + 1) % n queue.count -= 1 return value データ構造 | キュー 122
  76. オブジェクト識別子 オブジェクト識別子 / Object Identifier 124 Pythonの変数は、オブジェクトに付けた名前と考えられる。 id() で得られる「オブ ジェクト識別子」は、そのオブジェクトを識別する整数である。

    インタラクティブモードで確認するのがわかりやすいので確認してみる % python3 >>> a = 3 >>> id(a) 4353024928 図は参照関係を番号で表した模式図(識別子の数値は実行ごとに異なる) a 453024928 453024928 3
  77. オブジェクト識別子 変数の値の変更 125 変数の中身を変えるとどうなるのか? 指し示すオブジェクトが変わる >>> a = 5 >>>

    id(a) 4349043616 図示するとこんなイメージである a 4349043616 453024928 3 4349043616 5 、 float 、 bool は値自体を変更できません。この例では、再代入により別のオ ブジェクトを指すため、識別子が変わります int
  78. オブジェクト識別子 オブジェクトの変更 list 126 などの中の値を変えても指し示すオブジェクトは変わらない >>> b = [1, 2,

    3, 4] >>> id(b) 4336093440 >>> b[0] = 10 >>> id(b) 4336093440 b[0] = 10 b 4421998848 4336093440 10 [ 1 , 2, 3, 4]
  79. オブジェクト識別子 同じオブジェクトを指す変数 リストを別の変数に代入すると同じオブジェクトを 指し示すようになる >>> b = [1, 2, 3,

    4] >>> c = b >>> id(b), id(c) (4421998848, 4421998848) このため、変更が両方に影響する >>> c[0] = 10 >>> b [10, 2, 3, 4] 127 b 4421998848 c 4421998848 [1, 2, 3, 4] c[0] = 0 b 4421998848 c 4421998848 [ 10 , 2, 3, 4]
  80. immutableとmutable オブジェクト識別子 この振る舞いの違いはオブジェクトがimmutable(変更不可)かmutable(変更可) かによって変わる mutable immutable list, dict, set, 構造体

    など int, float, str, tuple など 同じmutableなオブジェクトを共有すると、その変更が関数外にも見える。引数への 再代入は、呼び出し元の変数には影響しない 128
  81. オブジェクト識別子 is と == 129 : 同じオブジェクトを指しているか。つまりオブジェクト識別子が同じか == : オブジェクトの中身が同じか

    is a 4336283504 a is b b 4338023040 "abc" a == b "abc" は唯一のオブジェクト(シングルトン)なので、判定には x is None または x is not None を使う。 None
  82. リストの繰り返し / List Repetition オブジェクト識別子 a = [0] * 5

    は、元のリストの要素への参照をn回繰り返したリストを作る。要素自体は コピーしないため、下の例では3つの要素が同じ内側のリストを指す list * n >>> a = [[1, 2]] * 3 >>> a [[1, 2], [1, 2], [1, 2]] >>> a[0][0] = 5 >>> a [[5, 2], [5, 2], [5, 2]] 内包表記で内側のリストを毎回作れば、別々に変更できる a = [[1, 2] for _ in range(3)] 130
  83. 定義 各ノードは値と次のノードへの参照を持つ リストは最初と最後のノードへの参照を持つ @dataclass class Node: value: int nextNode: Node

    | None = None @dataclass class LinkedList: head: Node | None = None tail: Node | None = None データ構造 | 線形リスト 133
  84. データ構造 | 線形リスト 最初のノードの追加 134 ノードが1つだけなので、 head と tail 両方が同じノードを指す

    def init_list(lst: LinkedList, value: int): new_node: Node = Node(value) lst.head = new_node lst.tail = new_node head tail 5
  85. データ構造 | 線形リスト append 135 リストの最後にノードを追加する tail ノードの次のノードを new_node にする

    tail ノードを new_node にする def append(lst: LinkedList, value: int): if lst.tail is None: init_list(lst, value) else: new_node: Node = Node(value) lst.tail.nextNode = new_node lst.tail = new_node head tail head new_node tail new_node
  86. データ構造 | 線形リスト prepend 136 リストの最初にノードを追加する new_node の次のノードを head ノードにする

    head ノードを new_node にする def prepend(lst: LinkedList, value: int): if lst.head is None: init_list(lst, value) else: new_node : Node = Node(value) new_node.nextNode = lst.head lst.head = new_node head new_node tail head new_node tail
  87. データ構造 | 線形リスト pop_front 137 先頭の要素を取り出して返す head ノードを target にする

    target ノードの次のノードを head ノードにする def pop_front(lst: LinkedList) -> int | None: if lst.head is None: return None target = lst.head lst.head = target.nextNode if lst.head is None: lst.tail = None return target.value head target tail head target tail
  88. node_at 番目のノードを返す(先頭は0番目) 順番に index 回、ノードを辿っていく必要がある index def node_at(lst: LinkedList, index:

    int) -> Node | None: if index < 0: return None node : Node | None = lst.head for _ in range(index): if node is None: return None node = node.nextNode return node データ構造 | 線形リスト 138
  89. データ構造 | 線形リスト insert 139 番目にノードを追加する node_at() を使って index -

    1 番目のノードを得る new_node の次を index - 1 番目のノードの次のノードにする index - 1 番目のノードの次のノードを new_node にする index new_node head new_node tail index - 1 index head tail index - 1 index
  90. insert def insert(lst: LinkedList, index: int, value: int) -> bool:

    if index == 0: prepend(lst, value) return True previous : Node | None = node_at(lst, index - 1) if previous is None: return False new_node: Node = Node(value) new_node.nextNode = previous.nextNode previous.nextNode = new_node if new_node.nextNode is None: lst.tail = new_node return True データ構造 | 線形リスト 140
  91. データ構造 | 線形リスト remove 141 番目のノードを削除する node_at() を使って index -

    1 番目のノードを得る index - 1 番目のノードの次のノードを target にする index - 1 番目のノードの次のノードを target の次のノードにする index head tail head tail target index - 1 target index + 1 index - 1 index + 1
  92. remove def remove(lst: LinkedList, index: int) -> bool: if index

    == 0: return pop_front(lst) is not None previous : Node | None = node_at(lst, index - 1) if previous is None: return False target : Node | None = previous.nextNode if target is None: return False previous.nextNode = target.nextNode if target.nextNode is None: lst.tail = previous return True データ構造 | 線形リスト 142
  93. 計算量 , remove の処理自体は append prepend insert データ構造 | 線形リスト

    143 で完了するが、 index 番目を得るのに 必要 pop_front insert remove
  94. [参考] 償却計算量(Amortized)とは 一連の操作の合計コストを、操作回数で割って評価する考え方 例:空の動的配列( list )への末尾追加 ※ 満杯になると容量を2倍に増やす場合 状態 必要な処理

    空きがある 新しい要素を書き込む 満杯 容量を増やし、既存の要素をコピーする 回の追加で、コピーする要素数の合計は 書き込みも含めた合計は なので、1回あたり Amortized データ構造 | 線形リスト 147 1回の計算量
  95. 定義 ノードは値と、左右の子ノードへの参照を持ちます(子がなければ None ) @dataclass class TreeNode: value: int left:

    TreeNode | None = None right: TreeNode | None = None 一番大元のノードを根(root)と呼びます @dataclass class BinarySearchTree: root: TreeNode | None = None データ構造 | 二分木 150
  96. 探索 / search def search(tree: BinarySearchTree, value: int) -> TreeNode

    | None: node: TreeNode | None = tree.root while node is not None: if node.value == value: return node elif node.value > value: node = node.left else: node = node.right return None データ構造 | 二分木 152
  97. 挿入 / insert 挿入は再帰で書いた方が書きやすい def insert(tree: BinarySearchTree , value: int):

    tree.root = insert_impl(tree.root, value) def insert_impl(node: TreeNode | None, value: int) -> TreeNode: if node is None: return TreeNode(value) elif node.value == value: return node elif node.value > value: node.left = insert_impl(node.left, value) return node else: node.right = insert_impl(node.right, value) return node データ構造 | 二分木 153
  98. 削除 / remove def remove_impl(node: TreeNode | None, value: int)

    -> TreeNode | None: if node is None: return None elif node.value == value: if node.left is None and node.right is None: return None elif node.left is None: return node.right elif node.right is None: return node.left else: max_value: int = search_max(node.left) node.left = remove_impl(node.left, max_value) node.value = max_value return node elif node.value > value: node.left = remove_impl(node.left, value) return node else: node.right = remove_impl(node.right, value) return node データ構造 | 二分木 155
  99. 削除 / remove def search_max(node: TreeNode) -> int: pos: TreeNode

    = node while pos.right is not None: pos = pos.right return pos.value def remove(tree: BinarySearchTree, value: int): tree.root = remove_impl(tree.root, value) データ構造 | 二分木 156
  100. 計算量 データ構造 | 二分木 各操作の計算量は木の高さ に対して 。木が平衡なら になる 挿入 探索

    削除 平均 最悪値 平均値は挿入順がランダムな場合などを想定する。AVL木などの平衡二分探索木で は、高さを に保つ 157
  101. クイックソート | partitionの仕様 コメント 「rightは含まない」に統一しているので、 right - 1を使うなどちょっとわかりに くいかも。 whileで条件を書くより、無限ループにしてしまって

    breakする方が自然? 無限ループの是非はあるので... 「pivotは前半後半どっちにしても良い」とした方が綺麗に書ける ただ、分かりにくい? 161
  102. マージソート | ワーキングメモリの確保 リストに append で追加していけば、 s が不要になるのだが以下の理由で不採用 オブジェクト指向的な書き方になる スタック・キューとソートの学習順をどうするか

    def merge_list(data: list[int], left: int, mid: int, right: int): p : int = left q : int = mid working : list[int] = list() while p < mid and q < right: if data[p] < data[q]: working.append(data[p]) p += 1 else: working.append(data[q]) q += 1 # 余った要素の追加とdataへの書き戻しは省略 コメント 162
  103. マージソート | マージ結果の戻し方 コメント data[left:right] = working この記述手法はちょっと特殊ではあるので for 文で書いた方がいいかも

    for i in range(left, right): data[i] = working[i - left] ちなみに、以下の記述でも結果は同じであるが、右辺の working[0:s] が新しいリス トを作るため、要素への参照のコピーが追加で発生する data[left:right] = working[0:s] 163
  104. 構造体 | 構造体とクラスを同一化していいのか? コメント 「オブジェクト指向」の前に学習する「アルゴリズムとデータ構造」という講義 を想定しているので、「構造体」としてクラスを使っている class と struct は別ものなのではないか?

    講義なのでちゃんと「定義」にしたがって教えるべきである 個人的には、以下の理由から同一化していいのでは?と考えている 構造体とクラスを区別している言語がそんなにないのではないか? 本来の構造体の役割はrecordではないのか? オブジェクト指向への繋ぎを考えると最初からクラスでいいのでは そもそもデータの操作はメンバ関数として定義した方が分かりやすい... 164
  105. 構造体 | 構造体 (struct) とクラス (class) 言語によって定義は異なっている struct class C

    ◦ × C++ ◦ ◦ C# ◦ ◦ Java × ◦ Rust ◦ × Swift ◦ ◦ Python × ◦ 備考 デフォルトが publicかprivateか 値型と参照型の違い 構造体に近いrecordがある 機能的にはクラス 値型と参照型の違い コメント 165
  106. 構造体 | immutableとmutable コメント 166 引数にはオブジェクトへの参照が渡り、mutableかimmutableかで渡し方は変わらな い mutable(変更可) immutable(変更不可) オブジェクト自体の変

    可能。呼び出し元にも見える 不可 更 呼び出し元の変数には影響しな 引数への再代入 同左 い 例 list, dict, set, 本資料の構造体 int, float, str, tuple ここの部分は既にソートでは使っているので、関数内での変更が外に影響するかどう か?は最初に説明してもいいかもしれない。