フリーキーズ | 独学プログラミング

スタックとキューの使いどころを初心者向けに解説!データ構造の基本を理解しよう

リンドくん

リンドくん

たなべ先生、データ構造の勉強で「スタック」と「キュー」って出てくるんですけど、これって何が違うんですか?どっちも同じようなものに見えて...

たなべ

たなべ

確かに似ているように見えるけど、実はデータの取り出し方が真逆なんだ。
身近な例で言うと、スタックは本の積み重ね、キューはレジの待ち列みたいなものなんだよ。

プログラミングを学び始めると、必ず出会うのが「データ構造」という概念です。
その中でもスタック(Stack)とキュー(Queue)は、最も基本的かつ重要なデータ構造の2つです。

「なんとなく聞いたことはあるけど、実際にどう使うの?」「配列と何が違うの?」という疑問を持つ方も多いのではないでしょうか?

この記事では、スタックとキューの基本概念から実際の使いどころまで、プログラミング初心者の方でも理解できるように、できるだけわかりやすく解説していきます。

オンラインコミュニティ運営しています

HackATAは、IT技術を習得する人のために広く開かれたオンラインコミュニティです。 現在、無料コミュニティとして開放していますので、ご気軽に参加してください。

✓ 再立ち上げ

✓ コミュニティの方向性について意見募集中

HackATA公式Webサイト

スタックとキューの基本概念を理解しよう

リンドくん

リンドくん

そもそもデータ構造って何なんですか?普通に変数に入れるのと何が違うんですか?

たなべ

たなべ

データ構造っていうのは、データを効率よく管理するための入れ物の形みたいなものなんだ。
料理で例えると、材料をどう並べるかで取り出しやすさが変わるよね?それと同じなんだよ。

データ構造とは何か

データ構造とは、プログラム内でデータを効率的に管理・操作するための仕組みのことです。
単純に変数に値を入れるだけでなく、複数のデータをどのように整理し、どのような順序で取り出すかを定めたものです。

適切なデータ構造を選ぶことで、以下のようなメリットがあります。

  • 処理速度の向上 - 必要なデータに素早くアクセスできる
  • メモリの効率化 - 無駄なメモリ使用を避けられる
  • コードの可読性向上 - プログラムの意図が明確になる
  • バグの削減 - データの管理方法が明確になる

スタック(Stack)の基本

スタックはLIFO(Last In, First Out)、つまり「後から入れたものが先に出る」というルールを持つデータ構造です。

身近な例で言えば以下のようなものです。

  • 本の積み重ね - 一番上に置いた本から取る
  • お皿の重ね - 上から順番に取っていく
  • ブラウザの戻るボタン - 最後に見たページから戻る

スタックには主に2つの基本操作があります。

  • push(プッシュ) - データを一番上に追加する
  • pop(ポップ) - 一番上のデータを取り出す

キュー(Queue)の基本

キューはFIFO(First In, First Out)、つまり「先に入れたものが先に出る」というルールを持つデータ構造です。

身近な例で言えば以下のようなものです。

  • レジの待ち列 - 先に並んだ人から順番に進む
  • プリンタの印刷待ち - 先に送った印刷から順番に処理
  • タスク処理 - 受け取った順番に仕事をこなす

キューには主に2つの基本操作があります。

  • enqueue(エンキュー) - データを列の最後に追加する
  • dequeue(デキュー) - 列の先頭からデータを取り出す

この2つの違いを理解することが、適切なデータ構造を選ぶ第一歩になります。
次のセクションでは、実際のコードで動きを確認していきましょう。

Pythonで実装するスタックの基本

リンドくん

リンドくん

実際にコードで書くとどうなるんですか?難しそうですね...

たなべ

たなべ

Pythonならリストを使って簡単に実装できるんだ!
標準的な機能だけで十分スタックの動作が再現できるよ。

Pythonでのスタック実装

Pythonでは、組み込みのlistを使ってスタックを簡単に実装できます。

# スタックの作成
stack = []

# push操作(要素を追加)
stack.append("ページA")
stack.append("ページB")
stack.append("ページC")

print("現在のスタック:", stack)
# 出力: ['ページA', 'ページB', 'ページC']

# pop操作(要素を取り出す)
last_page = stack.pop()
print(f"取り出したページ: {last_page}")
# 出力: 取り出したページ: ページC

print("pop後のスタック:", stack)
# 出力: ['ページA', 'ページB']

このように、append()でデータを追加し、pop()で取り出すことで、スタックの動作を実現できます。

スタックの使用例

ブラウザの「戻る」機能をシミュレートしてみましょう。

class BrowserHistory:
    def __init__(self):
        self.history = []  # 閲覧履歴を保存するスタック
        
    def visit(self, url):
        """新しいページを訪問"""
        self.history.append(url)
        print(f"訪問: {url}")
        
    def back(self):
        """前のページに戻る"""
        if len(self.history) > 1:
            current = self.history.pop()
            previous = self.history[-1]  # 最後の要素を確認(削除しない)
            print(f"{current} から {previous} に戻りました")
            return previous
        else:
            print("これ以上戻れません")
            return None
            
    def current_page(self):
        """現在のページを表示"""
        if self.history:
            return self.history[-1]
        return None

# 使用例
browser = BrowserHistory()
browser.visit("https://A.com")
browser.visit("https://A.com/about")
browser.visit("https://A.com/contact")

print(f"現在のページ: {browser.current_page()}")
browser.back()
browser.back()

実行結果は以下のようになります。

訪問: https://A.com
訪問: https://A.com/about
訪問: https://A.com/contact
現在のページ: https://A.com/contact
https://A.com/contact から https://A.com/about に戻りました
https://A.com/about から https://A.com に戻りました

スタックの便利なメソッド

スタックを扱う際に便利なメソッドをいくつか紹介します。

stack = []

# 空かどうか確認
def is_empty(stack):
    return len(stack) == 0

# 先頭の要素を見る(取り出さない)
def peek(stack):
    if not is_empty(stack):
        return stack[-1]
    return None

# スタックのサイズを取得
def size(stack):
    return len(stack)

# 使用例
stack.append(10)
stack.append(20)
stack.append(30)

print(f"スタックのサイズ: {size(stack)}")  # 出力: 3
print(f"先頭の要素: {peek(stack)}")  # 出力: 30
print(f"空ですか?: {is_empty(stack)}")  # 出力: False

このように、Pythonのリストを使えば簡単にスタックを実装できます。
特別なライブラリをインストールする必要もなく、標準機能だけで十分なのが嬉しいですよね。

Pythonで実装するキューの基本

リンドくん

リンドくん

キューもスタックと同じようにリストで作れるんですか?

たなべ

たなべ

作れるけど、実はキューには専用のモジュールを使う方が効率的なんだ。
Pythonにはcollections.dequeという便利な機能があるんだよ。

Pythonでのキュー実装

キューもリストで実装できますが、collections.dequeを使う方が効率的です。

from collections import deque

# キューの作成
queue = deque()

# enqueue操作(要素を追加)
queue.append("タスク1")
queue.append("タスク2")
queue.append("タスク3")

print("現在のキュー:", list(queue))
# 出力: ['タスク1', 'タスク2', 'タスク3']

# dequeue操作(要素を取り出す)
first_task = queue.popleft()
print(f"処理したタスク: {first_task}")
# 出力: 処理したタスク: タスク1

print("dequeue後のキュー:", list(queue))
# 出力: ['タスク2', 'タスク3']

dequeを使う理由は、先頭からの要素削除が高速だからです。
通常のリストで先頭を削除すると、すべての要素をずらす必要がありますが、dequeならその必要がありません。

キューの使用例

印刷ジョブの管理システムを作ってみましょう。

from collections import deque

class PrintQueue:
    def __init__(self):
        self.queue = deque()
        
    def add_job(self, document_name):
        """印刷ジョブを追加"""
        self.queue.append(document_name)
        print(f"印刷待ちに追加: {document_name}")
        print(f"待ち件数: {len(self.queue)}件")
        
    def process_job(self):
        """次の印刷ジョブを処理"""
        if self.queue:
            document = self.queue.popleft()
            print(f"印刷中: {document}")
            return document
        else:
            print("印刷待ちのジョブがありません")
            return None
            
    def show_queue(self):
        """待ち状態のジョブを表示"""
        if self.queue:
            print("印刷待ちリスト:")
            for i, doc in enumerate(self.queue, 1):
                print(f"  {i}. {doc}")
        else:
            print("待ちジョブはありません")

# 使用例
printer = PrintQueue()
printer.add_job("レポート.pdf")
printer.add_job("請求書.xlsx")
printer.add_job("プレゼン資料.pptx")

print("\n--- 現在の待ち状態 ---")
printer.show_queue()

print("\n--- 印刷処理開始 ---")
printer.process_job()
printer.process_job()

print("\n--- 残りの待ち状態 ---")
printer.show_queue()

実行結果は以下のようになります。

印刷待ちに追加: レポート.pdf
待ち件数: 1件
印刷待ちに追加: 請求書.xlsx
待ち件数: 2件
印刷待ちに追加: プレゼン資料.pptx
待ち件数: 3件

--- 現在の待ち状態 ---
印刷待ちリスト:
  1. レポート.pdf
  2. 請求書.xlsx
  3. プレゼン資料.pptx

--- 印刷処理開始 ---
印刷中: レポート.pdf
印刷中: 請求書.xlsx

--- 残りの待ち状態 ---
印刷待ちリスト:
  1. プレゼン資料.pptx

キューの便利なメソッド

キューを扱う際に便利なメソッドも確認しておきましょう。

from collections import deque

queue = deque()

# 空かどうか確認
def is_empty(queue):
    return len(queue) == 0

# 先頭の要素を見る(取り出さない)
def peek(queue):
    if not is_empty(queue):
        return queue[0]
    return None

# キューのサイズを取得
def size(queue):
    return len(queue)

# 使用例
queue.append("A")
queue.append("B")
queue.append("C")

print(f"キューのサイズ: {size(queue)}")  # 出力: 3
print(f"先頭の要素: {peek(queue)}")  # 出力: A
print(f"空ですか?: {is_empty(queue)}")  # 出力: False

このように、キューも簡単に実装できます。
特にdequeを使うことで、効率的なキュー操作が可能になります。

スタックとキューの実用的な使いどころ

リンドくん

リンドくん

実際のプログラミングでは、どんな場面でスタックやキューを使うんですか?

たなべ

たなべ

すごくたくさんの場面で使われているんだよ!
みんなが日常的に使っているアプリやシステムの裏側で、必ずと言っていいほど活躍しているんだ。

スタックの実用例

スタックは以下のような場面で活用されています。

関数の呼び出し管理(コールスタック)

プログラムが関数を呼び出すとき、その情報はスタックに積まれます。

def function_a():
    print("A開始")
    function_b()
    print("A終了")

def function_b():
    print("B開始")
    function_c()
    print("B終了")

def function_c():
    print("C実行")

function_a()
# 出力:
# A開始
# B開始
# C実行
# B終了
# A終了

これは、関数呼び出しがスタックで管理されているため、後から呼ばれた関数が先に終了するという動作になります。

括弧の対応チェック

プログラムのコードが正しいかをチェックする際にもスタックが使われます。

def check_brackets(text):
    """括弧の対応をチェック"""
    stack = []
    pairs = {')': '(', '}': '{', ']': '['}
    
    for char in text:
        if char in '({[':
            stack.append(char)
        elif char in ')}]':
            if not stack or stack[-1] != pairs[char]:
                return False
            stack.pop()
    
    return len(stack) == 0

# テスト
print(check_brackets("(a + b) * {c - [d / e]}"))  # True
print(check_brackets("(a + b] * {c - d}"))  # False

Undo(元に戻す)機能

テキストエディタなどの「元に戻す」機能もスタックで実装されています。

class TextEditor:
    def __init__(self):
        self.text = ""
        self.history = []  # 変更履歴のスタック
        
    def type_text(self, new_text):
        """テキストを追加"""
        self.history.append(self.text)  # 現在の状態を保存
        self.text += new_text
        
    def undo(self):
        """元に戻す"""
        if self.history:
            self.text = self.history.pop()
            print(f"元に戻しました: '{self.text}'")
        else:
            print("これ以上戻せません")

# 使用例
editor = TextEditor()
editor.type_text("Hello")
editor.type_text(" World")
print(f"現在のテキスト: '{editor.text}'")  # Hello World
editor.undo()  # Hello
editor.undo()  # (空)

キューの実用例

キューは以下のような場面で活用されています。

タスク処理システム

Webサーバーのリクエスト処理やバックグラウンドジョブの管理に使われます。

from collections import deque
import time

class TaskProcessor:
    def __init__(self):
        self.task_queue = deque()
        
    def add_task(self, task_name):
        """タスクを追加"""
        self.task_queue.append(task_name)
        print(f"タスク追加: {task_name}")
        
    def process_tasks(self):
        """すべてのタスクを処理"""
        print("\n--- タスク処理開始 ---")
        while self.task_queue:
            task = self.task_queue.popleft()
            print(f"処理中: {task}")
            time.sleep(0.5)  # 処理のシミュレーション
        print("--- すべて完了 ---")

# 使用例
processor = TaskProcessor()
processor.add_task("メール送信")
processor.add_task("データベース更新")
processor.add_task("レポート生成")
processor.process_tasks()
幅優先探索(BFS)

グラフやツリー構造の探索にキューが使われます。

from collections import deque

def bfs_search(graph, start, target):
    """幅優先探索でターゲットを探す"""
    queue = deque([start])
    visited = {start}
    
    while queue:
        node = queue.popleft()
        print(f"訪問: {node}")
        
        if node == target:
            print(f"見つかりました: {target}")
            return True
            
        # 隣接ノードをキューに追加
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    
    print(f"{target}は見つかりませんでした")
    return False

# グラフの例(友達関係など)
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

bfs_search(graph, 'A', 'F')

このように、スタックとキューは様々な実用的な場面で使われています。
データの処理順序が重要な場面では、必ずと言っていいほどこれらのデータ構造が活用されているのです。

スタックとキューの使い分けのポイント

リンドくん

リンドくん

スタックとキュー、どっちを使えばいいか迷ったときはどうすればいいですか?

たなべ

たなべ

データを取り出す順序を考えれば、自然と答えが見えてくるんだ。
「最後に入れたものを先に使いたい」ならスタック、「最初に入れたものから使いたい」ならキューだよ。

判断基準の基本

スタックとキューを選ぶときの基本的な判断基準は以下の通りです。

スタックを選ぶべき場面

  • 逆順処理が必要なとき - 後から入れたデータを先に処理したい
  • 一時的な保存と復元 - 状態を保存して後で元に戻す必要がある
  • ネストした構造の処理 - 括弧のチェックや関数呼び出しなど

キューを選ぶべき場面

  • 順番通りの処理が必要なとき - 先に入れたデータから順番に処理したい
  • 公平な処理が求められるとき - 先着順で処理する必要がある
  • バッファリングが必要なとき - データを一時的にためておく

パフォーマンスの考慮

データ構造を選ぶ際は、パフォーマンスも重要な判断材料です。

import time
from collections import deque

# リストをキューとして使った場合(遅い)
def test_list_as_queue(n):
    queue = []
    start = time.time()
    
    for i in range(n):
        queue.append(i)
    for i in range(n):
        queue.pop(0)  # 先頭削除は遅い
        
    return time.time() - start

# dequeをキューとして使った場合(速い)
def test_deque_as_queue(n):
    queue = deque()
    start = time.time()
    
    for i in range(n):
        queue.append(i)
    for i in range(n):
        queue.popleft()  # 高速
        
    return time.time() - start

n = 10000
print(f"リスト: {test_list_as_queue(n):.4f}秒")
print(f"deque: {test_deque_as_queue(n):.4f}秒")

このように、使いどころを理解することで、より効率的なプログラムを書けるようになります。
最初は迷うかもしれませんが、データの流れを意識することで自然と適切な選択ができるようになりますよ。

まとめ

リンドくん

リンドくん

スタックとキューの違いと使いどころがよくわかりました!思ったより身近なところで使われているんですね。

たなべ

たなべ

これからプログラムを書くときは、ぜひデータの流れを意識してみてね。きっと効率的なコードが書けるようになるよ!

この記事では、プログラミングの基本となるデータ構造、スタックとキューについて解説してきました。

重要なポイントをおさらいしましょう。

  • スタック(Stack) - 「後から入れたものが先に出る」LIFO構造で、ブラウザの戻るボタンやUndo機能などに使われる
  • キュー(Queue) - 「先に入れたものが先に出る」FIFO構造で、タスク処理や印刷待ちなどに使われる
  • Pythonでの実装 - リストやdequeを使って簡単に実装できる
  • 使い分けのポイント - データの取り出し順序を考えれば、自然と適切な選択ができる

スタックとキューは一見シンプルなデータ構造ですが、その応用範囲は非常に広く、実際のソフトウェア開発では欠かせない存在です。
ブラウザの履歴管理からサーバーのリクエスト処理まで、私たちが日常的に使うほとんどのアプリケーションの裏側で、これらのデータ構造が活躍しているのです。

プログラミングでは、適切なデータ構造を選ぶ能力が、優れたエンジニアとそうでないエンジニアを分ける大きなポイントの一つです。
今日学んだスタックとキューの知識は、これからのプログラミング学習において必ず役立つはずです。

この記事をシェア

関連するコンテンツ