アルゴリズム概要

💡 この問題を一言で言うと:「文字列から記号やスペースを除き、大文字を小文字に揃えたとき、前から読んでも後ろから読んでも同じかどうかを判定する問題」です。

英語の文章では、大文字・小文字の違いやカンマ・コロン・スペースといった記号が自由に混ざります。この問題では、それらの「見た目のノイズ」を無視して、純粋に英数字(アルファベットと数字)の並びだけに注目したときに回文(前から読んでも後ろから読んでも同じ並び)になっているかを判定します。

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

  • 記号やスペースが文字列のどこに何個出現するか分からないため、s == s[::-1] のような一括比較では正しく判定できません。
  • 「記号を除いて小文字化した新しい文字列を作ってから比較する」という素直な方法は正解にはなりますが、文字列の長さが最大 2×10^5 になりうるため、新しい文字列を作るたびに余分なメモリを消費してしまいます。
O(n)
時間計算量
O(1)
空間計算量
isalnum
使用メソッド
Easy
難易度

s = "A man, a plan, a canal: Panama"

→ True

記号とスペースを除いて小文字化すると "amanaplanacanalpanama" となり、前から読んでも後ろから読んでも同じになるため True です。

s = "race a car"

→ False

クリーニングすると "raceacar" となり、逆から読むと "racaecar" で一致しないため False です。

s = " "(半角スペース1文字)

→ True

英数字が1つも無いため、クリーニング後は空文字列になります。空文字列は前からも後ろからも「何もない」ので、回文として扱われ True になります。

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

入力例 s = "A man, a plan, a canal: Panama" を使って、二方向ポインタが実際にどう動くかを1ステップずつ確認しましょう。文字の下の数字はインデックス(何番目の文字かを示す位置番号)です。

Python実装

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

  1. 入力が str 型かどうかを検証する(エッジケース処理を含む)
  2. 左ポインタ left を先頭、右ポインタ right を末尾に初期化する
  3. left < right の間、非英数字の読み飛ばし・小文字化しての比較・ポインタの移動を繰り返す
  4. 不一致が見つかれば即座に False を、ループを抜けたら True を返す
from __future__ import annotations
from typing import Final


class Solution:
    """125. Valid Palindrome を解くクラス"""

    _MAX_LENGTH: Final[int] = 2 * 10 ** 5

    def isPalindrome(self, s: str) -> bool:
        """
        文字列 s が英数字だけ・小文字統一で回文かどうかを判定する

        Args:
            s: 判定対象の文字列

        Returns:
            回文であれば True、そうでなければ False
        """
        if not isinstance(s, str):
            raise TypeError("Input must be a string")
        if len(s) > self._MAX_LENGTH:
            raise ValueError("Input length exceeds the constraint")

        left: int = 0
        right: int = len(s) - 1

        while left < right:
            if not s[left].isalnum():
                left += 1
                continue
            if not s[right].isalnum():
                right -= 1
                continue
            if s[left].lower() != s[right].lower():
                return False
            left += 1
            right -= 1

        return True

▶ 入力例 s = "A man, a plan, a canal: Panama" での動作トレース

入力: s = "A man, a plan, a canal: Panama"(長さ30)
left=0, right=29
 0,29: 'A' と 'a' → 小文字化して比較 → 'a' == 'a' → 一致 → left=1, right=28
 1,28: s[1]=' ' は非英数字 → left=2
 2,28: 'm' と 'm' → 一致 → left=3, right=27
 3,27: 'a' と 'a' → 一致 → left=4, right=26
 4,26: 'n' と 'n' → 一致 → left=5, right=25
 5,25: s[5]=',' は非英数字 → left=6
 6,25: s[6]=' ' は非英数字 → left=7
 7,25: 'a' と 'a' → 一致 → left=8, right=24
 8,24: s[8]=' ' は非英数字 → left=9
 9,24: 'p' と 'P' → 小文字化 'p' == 'p' → 一致 → left=10, right=23
10,23: s[23]=' ' は非英数字 → right=22
10,22: s[22]=':' は非英数字 → right=21
10,21: 'l' と 'l' → 一致 → left=11, right=20
11,20: 'a' と 'a' → 一致 → left=12, right=19
12,19: 'n' と 'n' → 一致 → left=13, right=18
13,18: s[13]=',' は非英数字 → left=14
14,18: s[14]=' ' は非英数字 → left=15
15,18: 'a' と 'a' → 一致 → left=16, right=17
16,17: s[16]=' ' は非英数字 → left=17
left(17) < right(17) は成立しない → ループ終了
出力: True ✅

処理フローチャート

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

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
はい いいえ ループ
開始 初期化 left = 0, right = length - 1 left < right ? はい 非英数字ならポインタを移動 left は右へ 1 つ, right は左へ 1 つ 小文字化して比較 s[left] == s[right] ? はい left += 1, right -= 1 戻る いいえ いいえ False を返す True を返す

🔎 入力例 s = "A man, a plan, a canal: Panama" でのフロー追跡

  1. 「開始」ノード → 入力 s(長さ30)を受け取る
  2. 「初期化」ノード → left=0, right=29 に設定
  3. 「left < right ?」ノード → 0 < 29 なので「はい」の経路へ
  4. 「非英数字ならポインタを移動」ノード → s[0]='A' も s[29]='a' も英数字なのでそのまま次へ
  5. 「小文字化して比較」ノード → 'a' と 'a' が一致するので「はい」の経路へ
  6. 「left += 1, right -= 1」ノード → left=1, right=28 に更新し、再び「left < right ?」へ戻る(紫の矢印)
  7. (中略)この後、left 側では ',' や ' ' が、right 側では ' ' や ':' がそれぞれスキップされながら、ポインタが中央へ収束していきます
  8. 最終的に left=17, right=17 となり「left < right ?」が「いいえ」になるため、「True を返す」ノードに到達し、結果 True が返されます

フローの説明:
このフローチャートは、二方向ポインタが「ループ条件のチェック」→「非英数字の読み飛ばし」→「小文字化しての比較」→「ポインタの移動」という4つの処理を繰り返しながら中央へ収束していく様子を表しています。途中で不一致が見つかれば紫のループを抜けて赤い経路で「False を返す」に到達し、一度も不一致がなければ緑の経路で「True を返す」に到達します。

計算量分析

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

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
n より少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
アプローチ 時間計算量 空間計算量 備考
二方向ポインタ(採用) O(n) O(1) 新しい文字列を作らないため最省メモリ
フィルタ+反転比較 O(n) O(n) 実装は直感的だが新しい文字列を2つ生成する

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

時間計算量が O(n) になるのは、left と right という2つのポインタがそれぞれ最大でも文字列の長さ分しか動かず、各文字を高々1回ずつしか読まないためです。空間計算量が O(1) になるのは、追加で使うメモリが left と right という2つの整数変数だけであり、入力文字列の長さが変わってもこの追加メモリの量は変化しないためです。

📖 用語集

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

インデックス
文字列や配列の中で「何番目の要素か」を示す位置番号のことです。多くのプログラミング言語(Pythonを含む)では 0 から数え始めるため、先頭の文字はインデックス 0、2番目の文字はインデックス 1 になります。
エッジケース
空の入力・要素が1つだけ・制約の上限に近い巨大な入力など、通常とは異なる境界的な条件のことです。普通の入力ではうまく動くのに、エッジケースだけでバグが発生することがあるため、必ず確認すべき対象です。
回文
前から読んでも後ろから読んでも同じ並びになる文字列や文のことです。例えば "level" や "racecar" が回文です。この問題では、記号やスペースを除き大文字小文字を無視した上で回文かどうかを判定します。
空間計算量
アルゴリズムの処理中に使うメモリ量が、入力サイズに対してどのくらい増えるかの目安です。O(1) であれば、入力がどれだけ大きくなっても追加で使うメモリは一定のままであることを意味します。
時間計算量
入力サイズが大きくなるにつれて、処理にかかる時間がどのくらい増えるかの目安です。O(n) であれば、入力が2倍になると処理時間もおよそ2倍になることを意味します。
早期リターン
条件を満たした時点ですぐに関数を終了させることです。この問題では、不一致が見つかった瞬間に残りの処理を続けず即座に False を返しており、これが早期リターンにあたります。
二方向ポインタ
配列や文字列の両端から中央に向かって、2つの指し位置(インデックス)を同時に動かしていく手法です。新しいデータ構造を作らずに1回の走査だけで処理を終えられるため、メモリ効率と実行速度の両方に優れています。
不変条件
アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のことです。この問題では「left より前の英数字の並びと、right より後ろの英数字の並びを逆にしたものが常に一致している」という条件が不変条件にあたります。