第4回:スタックとキュー¶
実験1:ブラウザの「戻る」を作ってみる¶
何が出力されるか、走らせる前に予想してから実行してみよう。
# 訪問履歴
history = []
# 3つのページを訪問
history.append("ホーム")
history.append("ニュース")
history.append("天気")
# 「戻る」を押す(直前のページを取り出す)
print(history.pop())
# もう一度「戻る」
print(history.pop())
最後に訪問したページから順に戻ってくる。「最後に入れたもの」から取り出される。
実験2:プリンタの「印刷待ち」を作ってみる¶
同じく予想してから実行してみよう。
# 印刷待ち
queue = []
# 3つの印刷リクエスト
queue.append("レポート")
queue.append("写真")
queue.append("論文")
# プリンタが空いたので印刷(先に頼まれたものから)
print(queue.pop(0))
# 次のジョブ
print(queue.pop(0))
先に頼んだものから順に処理される。「最初に入れたもの」から取り出される。
観察:何が違った?¶
- 同じ
listを使った - 同じ
appendで追加した - 違いは取り出し方だけ
- 実験1:
pop()← 末尾から取る - 実験2:
pop(0)← 先頭から取る
- 実験1:
それだけで取り出される順番が完全に逆になった。この2つの動きにはそれぞれ名前がついている。
スタック(Stack)¶
後入れ先出しの挙動をする構造。LIFO(Last In, First Out)。
- 概念的な操作:
- push(追加): 末尾に置く
- pop(取り出し): 末尾から取る
- 入れる場所も出す場所も同じ端
スタックの動き¶
最後に積んだ「3」が最初に出てくる。
演習4-1¶
空のスタックに以下の操作を順に行ったとき、最終的なスタックの中身は?
ヒント: 左を底、右を上として書く。
キュー(Queue)¶
先入れ先出しの挙動をする構造。FIFO(First In, First Out)。
- 概念的な操作:
- enqueue(追加): 末尾に置く
- dequeue(取り出し): 先頭から取る
- 入れる側と出す側が逆の端
キューの動き¶
enqueue(1) → [1]
enqueue(2) → [1, 2]
enqueue(3) → [1, 2, 3]
dequeue() → 1 → [2, 3]
dequeue() → 2 → [3]
演習4-2¶
空のキューに以下の操作を順に行ったとき、最終的なキューの中身は?
ヒント: 左を先頭、右を末尾として書く。
スタック vs キュー¶
並べ方は同じ。取り出すルールが違うだけ。
| スタック | キュー | |
|---|---|---|
| 追加する端 | 末尾 | 末尾 |
| 取り出す端 | 末尾 | 先頭 |
| ルール | LIFO | FIFO |
| 追加操作 | push | enqueue |
| 取り出し操作 | pop | dequeue |
Pythonでの実装¶
スタック → list でOK¶
list は末尾の操作(append / pop)が O(1)。スタックの「同じ端」にしかアクセスしないので相性がいい。
キュー → collections.deque を使う¶
list でも pop(0) でキューを実装できるが、これは O(n)(先頭を抜くと残り全部を前にずらす必要がある、第3回参照)。要素が多いと急激に遅くなる。
collections.deque は両端の操作が O(1) なのでキュー向き。
from collections import deque
queue = deque()
queue.append(1) # enqueue
queue.append(2)
queue.popleft() # 1 が返る(dequeue)
list でキューを作らないこと
lst.pop(0) は O(n)。キューとして使うなら deque を選ぶ。
演習4-3¶
ブラウザの「戻る」と「進む」をスタックを使って用意するためには?
コールスタック(Call stack)¶
関数の呼び出しもスタックになっている。
関数 f の中で g を呼び、g の中で h を呼ぶ場合の流れ:
sequenceDiagram
participant Main as 実行
participant f
participant g
participant h
Main->>+f: f を呼ぶ
f->>+g: g を呼ぶ
g->>+h: h を呼ぶ
h-->>-g: h 終了
g-->>-f: g 終了
f-->>-Main: f 終了
スタックの中身も時間とともに伸び縮みする:
「最後に呼ばれた関数が、最初に終わる」 → LIFO → スタック。これをコールスタックという。
プログラムの実行そのものがスタックの上で動いている
ふだん意識しないが、関数呼び出しの裏側ではスタックが動いている。次回(再帰と分割統治法)で重要になる。
演習4-4¶
以下の用途で、スタック・キューどちらが適切か? 理由も1文で書いてください。
- 文書編集ソフトの「Ctrl+Z」で直前の操作を取り消す
- ATMで並んでいる人を順に呼び出す
((a + b) * c)のようなカッコの対応をチェックする- 図書館で予約された本を、予約された順に渡す