Java Tips | 文字列処理:回文判定

Java Java
スポンサーリンク

回文判定は「左右対称かどうか」を見るシンプルなアルゴリズム

回文(かいぶん)は、「前から読んでも後ろから読んでも同じ文字列」のことです。
英語なら level, noon、日本語なら「たけやぶやけた」「しんぶんし」などが有名ですね。

回文判定は、一見お遊びのようですが、アルゴリズムの練習としてとても優秀です。
なぜなら、「文字列」「インデックス」「ループ」「条件分岐」「境界条件」といった、
プログラミングの基礎が全部詰まっているからです。

ここでは、まずは一番簡単な実装から入り、
そのあとで「効率の良い書き方」「実務で使える形」まで一気に持っていきます。


一番簡単な回文判定:反転して同じかどうか比べる

文字列反転ユーティリティをそのまま使う

すでに「文字列反転」のユーティリティがあるなら、
回文判定は驚くほど簡単に書けます。

public final class PalindromeUtil {

    private PalindromeUtil() {}

    public static boolean isPalindromeSimple(String text) {
        if (text == null) {
            return false;
        }
        String reversed = new StringBuilder(text).reverse().toString();
        return text.equals(reversed);
    }
}
Java

使い方はこうなります。

System.out.println(PalindromeUtil.isPalindromeSimple("level")); // true
System.out.println(PalindromeUtil.isPalindromeSimple("noon"));  // true
System.out.println(PalindromeUtil.isPalindromeSimple("abc"));   // false
System.out.println(PalindromeUtil.isPalindromeSimple("あいいあ")); // true
System.out.println(PalindromeUtil.isPalindromeSimple(null));    // false
Java

ここで押さえておきたいポイントは二つです。

一つ目は、「反転して同じなら回文」という、定義そのものをコードにしていることです。
回文の定義をそのまま実装しているので、直感的で分かりやすい書き方になっています。

二つ目は、「null をどう扱うかを決めている」ことです。
ここでは「null は回文ではない」として false を返しています。
このあたりのポリシーを自分で決めて、コードに反映させる感覚が大事です。


もう一歩踏み込む:左右から内側に向かって比較する

反転用の文字列を作らずに判定する方法

さきほどの実装は分かりやすい反面、
「反転した文字列を丸ごと1つ作る」というコストがかかります。

回文判定だけが目的なら、
左右から1文字ずつ比較していくだけで十分です。

public final class PalindromeUtil {

    private PalindromeUtil() {}

    public static boolean isPalindrome(String text) {
        if (text == null) {
            return false;
        }
        int left = 0;
        int right = text.length() - 1;

        while (left < right) {
            char cLeft = text.charAt(left);
            char cRight = text.charAt(right);
            if (cLeft != cRight) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
Java

使い方は同じです。

System.out.println(PalindromeUtil.isPalindrome("level")); // true
System.out.println(PalindromeUtil.isPalindrome("noon"));  // true
System.out.println(PalindromeUtil.isPalindrome("abc"));   // false
System.out.println(PalindromeUtil.isPalindrome("あいいあ")); // true
Java

ここで重要なポイントをしっかり噛み砕きます。

一つ目は、「left と right という“両端のインデックス”を使っている」ことです。
left は先頭から、right は末尾からスタートし、
毎回1つずつ内側に進めていきます。

二つ目は、「while (left < right) という条件の意味」です。
左右が交差した時点(left >= right)で、
すべてのペアをチェックし終わっているので、ループを終えてよい、ということです。
ここを <= にしてしまうと、中央の1文字を自分自身と比較することになり、
動作としては問題ないものの、無駄な1回が増えます。

三つ目は、「途中で1箇所でも違えば即座に false を返している」ことです。
回文ではないと分かった瞬間に処理を打ち切ることで、
無駄な比較をしない、という効率の良さも手に入れています。

この「左右から内側に向かって比較する」パターンは、
回文判定だけでなく、配列やリストの対称性チェックなど、
いろいろな場面で使える強力な基本パターンです。


実務寄りの回文判定:空白や大文字小文字を無視する

「見た目として回文かどうか」を判定したい場合

現実のテキストでは、空白や記号、大文字小文字の違いを無視して
「見た目として回文かどうか」を判定したいことがあります。

例えば、英語の有名な回文にこんなものがあります。

Never odd or even

空白と大文字小文字を無視すると、
neveroddoreven になり、これは回文です。

これを判定するユーティリティを書いてみます。

public final class PalindromeUtil {

    private PalindromeUtil() {}

    public static boolean isPalindromeIgnoringCaseAndSpaces(String text) {
        if (text == null) {
            return false;
        }
        String normalized = text
                .replace(" ", "")      // 空白を除去
                .toLowerCase();        // 小文字に統一

        int left = 0;
        int right = normalized.length() - 1;

        while (left < right) {
            char cLeft = normalized.charAt(left);
            char cRight = normalized.charAt(right);
            if (cLeft != cRight) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
Java

使い方はこうです。

System.out.println(
    PalindromeUtil.isPalindromeIgnoringCaseAndSpaces("Never odd or even")
); // true

System.out.println(
    PalindromeUtil.isPalindromeIgnoringCaseAndSpaces("A man a plan a canal Panama")
); // true っぽく扱いたい例(実際には記号もあるので、さらに正規化が必要)
Java

ここでの重要ポイントは、「正規化(normalize)」という考え方です。

判定ロジックそのものは、先ほどと同じ「左右から比較」です。
違うのは、その前に

空白を消す
大文字小文字を揃える

という「前処理」を挟んでいることです。

実務の文字列処理では、この「前処理(正規化)」がとても大事になります。
回文に限らず、検索、比較、バリデーションなど、
「何を無視して、何を区別するか」を決めてから処理を書く、という感覚を持っておくと、
設計が一気にきれいになります。


Unicodeと回文:深追いしすぎないための知識

絵文字や結合文字をどう扱うか

文字列反転のときと同じく、
「1文字=1char ではない」問題は、回文判定にも関わってきます。

例えば、絵文字や一部の漢字は、内部的には2つの char(サロゲートペア)で表現されます。
また、「文字+濁点」「文字+結合記号」のようなケースもあります。

本気で「見た目の1文字単位で回文かどうか」を判定しようとすると、
codePoint 単位で扱う必要があります。

public static boolean isPalindromeByCodePoint(String text) {
    if (text == null) {
        return false;
    }
    int[] cps = text.codePoints().toArray();
    int left = 0;
    int right = cps.length - 1;
    while (left < right) {
        if (cps[left] != cps[right]) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}
Java

ただし、ここまでやるかどうかは要件次第です。
多くの業務システムでは、
「回文判定をそこまで厳密にやる」場面はほとんどありません。

大事なのは、「char ベースの処理には限界がある」ということを知っておくこと。
知らないまま「完璧だ」と思い込むのが一番危険です。
「必要になったら codePoint で考える」という引き出しを持っておけば十分です。


回文判定の実務での使いどころと、学習としての価値

正直に言うと、「回文判定そのもの」が業務要件として出てくることはあまり多くありません。
ただし、次のような場面では役に立ちます。

テキスト処理や検索機能のテストデータとして
アルゴリズムの練習問題として(新人教育など)
左右対称性を使うロジックのサンプルとして

そして何より、回文判定をちゃんと書けるようになると、

文字列の長さとインデックスの関係
左右から中央に向かうループの書き方
途中で打ち切る条件の考え方
前処理(正規化)と本処理の分離

といった、実務でガンガン使う思考パターンが身につきます。


まとめ:回文判定ユーティリティで鍛えたい感覚

回文判定は、「前から読んでも後ろから読んでも同じか」を見るだけのシンプルな処理ですが、
その裏側には、文字列処理のエッセンスが詰まっています。

まずは

文字列を反転して equals で比べるシンプル版
左右から内側に向かって比較する効率の良い版

の二つを書いてみてください。

そのうえで、

空白や大文字小文字を無視するための正規化
必要に応じて codePoint を意識する

といった発展形にも触れてみると、
「文字列を自在に扱える感覚」が一気に育ちます。

もし今、あなたのコードのどこかに
「左右対称かどうか」「前後が一致しているか」を見る処理があれば、
一度この回文判定のパターンに当てはめて書き直してみてください。

その小さな練習が、アルゴリズムに強いエンジニアへの、確かな一歩になります。

タイトルとURLをコピーしました