アルゴリズム概要

💡 この問題を一言で言うと:「配列の中でペアを組んでいない、1回だけ登場する数字を見つける問題」

配列 nums の要素はほとんどが2回ずつ登場し、1つだけ1回しか登場しない要素が混ざっています。それを効率よく見つけ出すのがゴールです。

⚠️ なぜ単純な方法では解けないのか

  • ハッシュテーブル(=キーと値をセットで記録できる辞書のような構造)で出現回数を数えると、配列の要素数に比例した追加メモリ(O(n))を使ってしまい、「定数空間(O(1))」という制約に違反します。
  • 配列をソートしてから隣同士を比較する方法は、ソート自体にO(n log n)の時間がかかり、「線形時間(O(n))」という制約に違反します。
O(n)
時間計算量
O(1)
空間計算量
Easy
難易度
XOR
キーとなる演算

nums = [2, 2, 1]

出力: 1

2は2回登場して打ち消し合うため、1回しか登場しない1が答えになります。

nums = [4, 1, 2, 1, 2]

出力: 4

1と2はそれぞれ2回登場して打ち消し合うため、1回しか登場しない4が答えになります。このページのステップ解説では、この例を使って動作を追跡します。

ステップバイステップ解説

入力例 nums = [4, 1, 2, 1, 2] を使って、result 変数がどう変化していくかを1ステップずつ確認しましょう。

Python実装

📋 このコードの構造(先に全体像を把握しよう)

  1. nums が list かどうか、要素がすべて int かどうかを検証する
  2. 配列が空でないことを確認する(制約上は保証されているが、防御的に確認する)
  3. result を 0 で初期化し、1回のループで各要素を result にXORで重ねる
  4. 走査が終わったら result を返す
from __future__ import annotations

from typing import List


class Solution:
    """
    136. Single Number 解決クラス

    配列内で1回だけ登場する数値を、XOR(排他的論理和)を使って
    O(n)時間・O(1)空間で見つける。
    """

    def singleNumber(self, nums: List[int]) -> int:
        """
        配列内で1回だけ登場する数値を返す。

        Args:
            nums: 1回だけ登場する要素が1つ含まれる整数配列。
                  それ以外の要素はすべてちょうど2回登場する。

        Returns:
            1回だけ登場する整数。

        Raises:
            TypeError: nums がリストでない、または要素に int 以外が含まれる場合。
            ValueError: nums が空の場合。
        """
        # isinstance() で型チェックする。
        # Python は動的型付け(=実行するまで型が確定しない仕組み)のため、
        # 呼び出し元が誤った型を渡しても実行時まで気づけない。
        if not isinstance(nums, list):
            raise TypeError("Input must be a list")

        # all() + ジェネレータ式で全要素が int かどうかを1行でチェックする。
        if not all(isinstance(x, int) for x in nums):
            raise TypeError("All elements must be integers")

        # 制約上は長さ1以上が保証されているが、防御的プログラミングとして
        # 空リストを明示的に弾いておく。
        if len(nums) == 0:
            raise ValueError("Input list must not be empty")

        # result を「これまでXORを重ねた累積値」として使う。
        # 初期値を 0 にするのは、0 ^ a = a という性質があるため。
        result: int = 0

        # 配列を1回だけ走査し、各要素を result に XOR で重ねていく。
        # 同じ数字が2回現れると a ^ a = 0 で打ち消し合い、
        # 最終的にペアのない「はぐれ者」の数字だけが result に残る。
        for num in nums:
            result ^= num

        return result

▶ 入力例 nums = [4, 1, 2, 1, 2] での動作トレース

初期状態: result = 0

num = 4 → result = 0 ^ 4 = 4
num = 1 → result = 4 ^ 1 = 5
num = 2 → result = 5 ^ 2 = 7
num = 1 → result = 7 ^ 1 = 6   (1が2回目 → 打ち消し合いの途中経過)
num = 2 → result = 6 ^ 2 = 4   (2が2回目 → 打ち消し合い完了)

走査終了 → result = 4 を返す

処理フローチャート

🗺️ フローチャートの読み方

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
緑=はい/成功 赤=いいえ
開始 nums を受け取る 入力を検証 型・空をチェック いいえ 検証エラー 例外を送出 はい 初期化 result = 0 走査ループ 要素は残っているか はい XOR適用 result^=num 次の要素へ(ループ) いいえ(走査完了) 終了 result を返す

🔎 入力例 nums = [4, 1, 2, 1, 2] でのフロー追跡

  1. 「開始」ノード → nums = [4, 1, 2, 1, 2] を受け取る
  2. 「入力を検証」ひし形 → list型・全要素int・空でないため「はい」の経路へ
  3. 「初期化」ノード → result = 0 にセット
  4. 「走査ループ」ひし形 → 要素が残っているので「はい」の経路へ、XOR適用を5回繰り返す(result は 4 → 5 → 7 → 6 → 4 と変化)
  5. 「走査ループ」ひし形 → すべての要素を処理し終えたので「いいえ(走査完了)」の経路へ
  6. 「終了」ノード → result = 4 を返す

フローの説明:
このアルゴリズムは「検証 → 初期化 → ループ」というシンプルな3段構成です。ループ部分だけが配列の要素数に応じて繰り返され、それ以外は1回きりの処理です。ループが要素数分だけ回ることが、時間計算量がO(n)になる直接の理由です。

計算量分析

📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
nより少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
アプローチ 時間 空間 備考
Counterで出現回数を数える O(n) O(n) 追加メモリが制約に違反
sortedで隣同士を比較 O(n log n) O(n) ソート自体が線形時間の制約に違反
XOR + forループ(採用) O(n) O(1) 制約に完全一致・可読性も高い
XOR + functools.reduce O(n) O(1) 競技プログラミング向けの簡潔版

🔍 なぜこの計算量になるのか

配列を1回だけ走査するため、時間計算量はO(n)になります。XORの結果を保持する result という変数1つしか使わないため、空間計算量は入力サイズに関係なく常にO(1)です。ハッシュテーブルを使う方法はO(n)の追加メモリを必要とし、ソートを使う方法はO(n log n)の時間がかかるため、この問題の制約(線形時間・定数空間)を同時に満たせるのはXOR方式だけです。

📖 用語集

このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。

インデックス
配列の中で各要素の位置を表す番号のこと。多くのプログラミング言語では0から数え始める。
XOR(排他的論理和)
2つのビットを比較し、違っていれば1、同じなら0を返す演算。同じ値同士をXORすると必ず0になる性質があり、この問題ではこの性質を使ってペアの数字を打ち消し合わせている。
型ヒント
関数の引数や戻り値に型を注釈として書く仕組み。def f(x: int) -> str: のように書く。pylanceのような静的解析ツールがバグを実行前に検出できるようになる。
交換法則・結合法則
計算の順番を入れ替えたり、まとめる位置を変えても結果が変わらない性質。XORはこの2つの法則が成り立つため、配列の要素をどの順番でXORしても最終結果は同じになる。
定数空間
入力サイズに関係なく一定量のメモリしか使わないこと。O(1)の空間計算量とも呼ばれる。
動的型付け
実行するまで変数の型が確定しない仕組み。Pythonが採用している方式で、誤った型を渡しても実行時まで気づけないという特徴がある。
ハッシュテーブル
キーと値をセットで記録できる辞書のようなデータ構造。図書館の索引カードのように、キーからすぐに値を探せる。Pythonでは dictCounter がこれに相当する。
ビット
0か1で表される情報の最小単位。コンピュータ内部のすべての数値はビットの並びとして表現される。
pylance
VSCodeで使えるPythonの静的型チェックツール。型ヒントをもとに、実行前にコードの誤りを検出してくれる。
防御的プログラミング
想定外の入力にも備えて、プログラムが壊れないようにあらかじめ対処を書いておく考え方。