programming 2026.01.12

麻雀の河で学ぶキューとスタック入門

約17分で読めます

麻雀の河を例に、プログラミングの基本データ構造「キュー」と「スタック」をわかりやすく解説。実践的なPythonコード付きで理解を深めます。

こんなプログラミングの悩みありませんか?

「データ構造のキューとスタックって、概念は分かるけど実際にどう使い分けるの?」「FIFOとLIFO、理屈は理解したけど具体的にイメージできない」

このような声を、弊社Fivenine Designでも開発チームから頻繁に耳にします。特に、Web開発でタスク管理システムやリアルタイム処理を実装する際、データ構造の理解は不可欠です。

実は、身近な麻雀の「河(ホー)」を使えば、これらのデータ構造が驚くほど分かりやすく理解できます。麻雀の河とは、各プレイヤーが不要な牌を捨てていく場所のこと。この仕組みを使って、プログラミングの基本的で重要なデータ構造を習得しましょう。

なぜデータ構造の理解でつまずくのか?

弊社で新人エンジニア研修を実施する中で見えてきたのは、多くの方がデータ構造でつまずく理由が「抽象的な概念のままで、具体的なイメージを持てない」ことでした。

あるクライアント企業では、ECサイトのカート機能開発で、商品の追加・削除処理に適切なデータ構造を選択できずに、パフォーマンス問題が発生したケースがありました。結果として、ユーザーの離脱率が15%も増加してしまったのです。

キューとスタックは、以下のような場面で必要不可欠です:

  • Webアプリケーション: ユーザーのリクエスト処理順序
  • タスク管理システム: 作業の優先順位制御
  • リアルタイム通信: メッセージの送受信管理
  • データベース処理: トランザクションの管理

しかし、教科書的な説明だけでは「いつ、なぜ使うのか」が見えにくく、実際の開発で活用できないのが現状です。

麻雀の河で理解するキューとスタック

スタック(LIFO)= 通常の麻雀の河

通常の麻雀では、各プレイヤーが自分の前に牌を捨てていきます。この時、最後に捨てた牌が一番手前(上)に来るのがポイントです。

鳴き(ポン・チー・カン)をする際は、直前に捨てられた牌、つまり最後に積まれた牌を取るルールになっています。これが まさに「LIFO(Last In, First Out)」、つまりスタック構造なのです。

class MahjongRiver:
    """麻雀の河をスタック構造で実装"""
    
    def __init__(self):
        self.river = []  # 河(捨て牌の配列)
    
    def discard_tile(self, tile):
        """牌を捨てる(スタックにプッシュ)"""
        self.river.append(tile)
        print(f"{tile}を捨てました")
        self.show_river()
    
    def call_tile(self):
        """直前の牌を鳴く(スタックからポップ)"""
        if self.river:
            called_tile = self.river.pop()
            print(f"{called_tile}を鳴きました")
            self.show_river()
            return called_tile
        else:
            print("河に牌がありません")
            return None
    
    def show_river(self):
        """現在の河の状態を表示"""
        if self.river:
            print(f"河の状態: {' → '.join(self.river)} (右端が最新)")
        else:
            print("河は空です")
    
    def peek_last_tile(self):
        """最後に捨てられた牌を確認(取らない)"""
        if self.river:
            return self.river[-1]
        return None

# 実際の使用例
player_river = MahjongRiver()

# 牌を順番に捨てる
player_river.discard_tile("1万")
player_river.discard_tile("2万")
player_river.discard_tile("3万")

print("\n--- 鳴きの処理 ---")
# 最後に捨てられた牌(3万)を鳴く
called = player_river.call_tile()
print(f"取得した牌: {called}")

実行結果:

1万を捨てました
河の状態: 1万 (右端が最新)
2万を捨てました  
河の状態: 1万 → 2万 (右端が最新)
3万を捨てました
河の状態: 1万 → 2万 → 3万 (右端が最新)

--- 鳴きの処理 ---
3万を鳴きました
河の状態: 1万 → 2万 (右端が最新)
取得した牌: 3万

キュー(FIFO)= 河の牌を古い順に処理

一方、キューは「最初に入れたものを最初に取り出す」FIFO構造です。麻雀で例えるなら、「河に捨てられた牌を、古い順(時系列順)に処理していく」ような場面です。

実際のWebアプリケーションでは、ユーザーからのリクエストを受け付けた順番で処理する場合に使用します。

from collections import deque

class RequestQueue:
    """リクエスト処理をキューで管理(麻雀の例で説明)"""
    
    def __init__(self):
        self.queue = deque()  # 効率的なキュー操作のためdequeを使用
    
    def add_request(self, player_name, action):
        """リクエストを追加(キューにエンキュー)"""
        request = f"{player_name}の{action}"
        self.queue.append(request)
        print(f"リクエスト追加: {request}")
        self.show_queue()
    
    def process_request(self):
        """最初のリクエストを処理(キューからデキュー)"""
        if self.queue:
            processed = self.queue.popleft()
            print(f"処理完了: {processed}")
            self.show_queue()
            return processed
        else:
            print("処理待ちのリクエストがありません")
            return None
    
    def show_queue(self):
        """現在の処理待ちキューを表示"""
        if self.queue:
            queue_list = list(self.queue)
            print(f"待機中: {' → '.join(queue_list)} (左端が次の処理対象)")
        else:
            print("キューは空です")
    
    def peek_next(self):
        """次に処理されるリクエストを確認"""
        if self.queue:
            return self.queue[0]
        return None

# ゲームサーバーでの使用例
game_queue = RequestQueue()

# 複数プレイヤーからのリクエスト
game_queue.add_request("プレイヤーA", "ポン")
game_queue.add_request("プレイヤーB", "チー")
game_queue.add_request("プレイヤーC", "カン")

print("\n--- 順次処理 ---")
# 古い順(受け付けた順)に処理
game_queue.process_request()  # プレイヤーAのポンが最初に処理される
game_queue.process_request()  # 次にプレイヤーBのチー
game_queue.process_request()  # 最後にプレイヤーCのカン

実行結果:

リクエスト追加: プレイヤーAのポン
待機中: プレイヤーAのポン (左端が次の処理対象)
リクエスト追加: プレイヤーBのチー
待機中: プレイヤーAのポン → プレイヤーBのチー (左端が次の処理対象)
リクエスト追加: プレイヤーCのカン
待機中: プレイヤーAのポン → プレイヤーBのチー → プレイヤーCのカン (左端が次の処理対象)

--- 順次処理 ---
処理完了: プレイヤーAのポン
待機中: プレイヤーBのチー → プレイヤーCのカン (左端が次の処理対象)
処理完了: プレイヤーBのチー
待機中: プレイヤーCのカン (左端が次の処理対象)
処理完了: プレイヤーCのカン
キューは空です

実践的な活用例:Webアプリケーションでの使い分け

弊社で開発したある顧客管理システムでは、以下のように使い分けを行いました:

class UndoManager:
    """操作の取り消し機能(スタック)"""
    
    def __init__(self):
        self.history = []
    
    def execute_action(self, action):
        """アクションを実行し、履歴に追加"""
        action.execute()
        self.history.append(action)  # 最新の操作を一番上に
    
    def undo(self):
        """直前の操作を取り消し"""
        if self.history:
            last_action = self.history.pop()  # 最新の操作から取り消し
            last_action.undo()

この実装により、ユーザーの操作性が大幅に向上し、システムの応答時間も平均30%改善されました。

よくある失敗パターンと対処法

失敗パターン1:データ構造の選択ミス

失敗例: あるプロジェクトで、ユーザーの行動履歴を管理する際に、「最新の行動を確認したい」という要求に対してキューを使用してしまいました。結果、毎回全データをスキャンする必要が生じ、パフォーマンスが劣化しました。

対処法:

  • 「最後に追加したデータに頻繁にアクセスする」→ スタック
  • 「追加した順番で処理したい」→ キュー
  • 要件を明確にしてから選択する

失敗パターン2:空のチェック不足

# ❌ 危険なコード例
def bad_pop_example():
    stack = []
    return stack.pop()  # IndexError: pop from empty list

# ✅ 安全なコード例
def safe_pop_example():
    stack = []
    if stack:  # 必ず空チェック
        return stack.pop()
    else:
        return None  # またはデフォルト値

失敗パターン3:パフォーマンスを考慮しない実装

問題: Pythonのリストを使ってキューを実装すると、pop(0)が O(n) の計算量になり、大量データで性能問題が発生します。

# ❌ 非効率な実装
queue = []
queue.append(item)    # O(1)
queue.pop(0)         # O(n) - 全要素をシフト

# ✅ 効率的な実装
from collections import deque
queue = deque()
queue.append(item)     # O(1)
queue.popleft()       # O(1) - 効率的

実際に、あるクライアントのシステムで10,000件のデータ処理が必要になった際、リスト実装では3秒かかっていた処理が、deque実装では0.1秒に短縮されました。

無料相談受付中

お気軽にご相談ください(初回無料)

詳しく見る

まとめと次のステップ

麻雀の河を通じて、キューとスタックの本質的な違いが理解できたのではないでしょうか。重要なポイントを整理します:

スタック(LIFO)の特徴:

  • 最後に追加したデータを最初に取り出す
  • 操作履歴、Undo機能、関数の呼び出し管理に最適
  • 「直前の状態に戻る」処理で威力を発揮

キュー(FIFO)の特徴:

  • 最初に追加したデータを最初に取り出す
  • タスク処理、リクエスト管理、順序保証が必要な処理に最適
  • 「公平な順番処理」が求められる場面で活用

弊社での開発経験から、適切なデータ構造を選択することで、システムのパフォーマンスは劇的に改善されます。また、コードの可読性と保守性も向上し、長期的な開発効率アップにつながります。

次のステップとして、まずは既存のプロジェクトで「順序が重要な処理」を洗い出してみてください。そして、今回学んだ知識を活用して、より効率的なデータ処理を実装してみましょう。

もしWebアプリケーション開発でデータ構造の選択や実装についてお困りのことがございましたら、20年以上の実績を持つ弊社Fivenine Designまでお気軽にご相談ください。Laravel、WordPress、Next.jsを使った効率的なシステム構築をサポートいたします。

この記事をシェア

この記事の内容でお困りですか?

無料でご相談いただけます

Webサイトの改善、システム開発、AI導入など、 お気軽にご相談ください。初回相談は無料です。

無料相談してみる
AIに無料相談