コンテンツにスキップ

第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) ← 先頭から取る

それだけで取り出される順番が完全に逆になった。この2つの動きにはそれぞれ名前がついている。


スタック(Stack)

後入れ先出しの挙動をする構造。LIFO(Last In, First Out)。

  • 概念的な操作:
    • push(追加): 末尾に置く
    • pop(取り出し): 末尾から取る
  • 入れる場所も出す場所も同じ端

スタックの動き

push(1)   → [1]
push(2)   → [1, 2]
push(3)   → [1, 2, 3]
pop() → 3 → [1, 2]
pop() → 2 → [1]

最後に積んだ「3」が最初に出てくる。

演習4-1

空のスタックに以下の操作を順に行ったとき、最終的なスタックの中身は?

push(A), push(B), pop(), push(C), pop(), push(D)

ヒント: 左を底、右を上として書く。


キュー(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

空のキューに以下の操作を順に行ったとき、最終的なキューの中身は?

enqueue(A), enqueue(B), dequeue(), enqueue(C), dequeue(), enqueue(D)

ヒント: 左を先頭、右を末尾として書く。


スタック vs キュー

並べ方は同じ。取り出すルールが違うだけ。

スタック キュー
追加する端 末尾 末尾
取り出す端 末尾 先頭
ルール LIFO FIFO
追加操作 push enqueue
取り出し操作 pop dequeue

Pythonでの実装

スタック → list でOK

list は末尾の操作(append / pop)が O(1)。スタックの「同じ端」にしかアクセスしないので相性がいい。

stack = []
stack.append(1)   # push
stack.append(2)
stack.pop()       # 2 が返る

キュー → 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 終了

スタックの中身も時間とともに伸び縮みする:

時刻 →
[ ]   [f]   [f,g]   [f,g,h]   [f,g]   [f]   [ ]
 開始  f呼ぶ  g呼ぶ   h呼ぶ    h終了  g終了 f終了

「最後に呼ばれた関数が、最初に終わる」 → LIFO → スタック。これをコールスタックという。

プログラムの実行そのものがスタックの上で動いている

ふだん意識しないが、関数呼び出しの裏側ではスタックが動いている。次回(再帰と分割統治法)で重要になる。

演習4-4

以下の用途で、スタック・キューどちらが適切か? 理由も1文で書いてください。

  1. 文書編集ソフトの「Ctrl+Z」で直前の操作を取り消す
  2. ATMで並んでいる人を順に呼び出す
  3. ((a + b) * c) のようなカッコの対応をチェックする
  4. 図書館で予約された本を、予約された順に渡す