C# Tips | 文字列処理:文字頻度カウント

C# C#
スポンサーリンク

はじめに 「文字頻度カウント」は“テキストのクセを数字で見る”道具

文字頻度カウントは、「この文字列の中で、どの文字が何回出てくるか」を数える処理です。
一見お勉強っぽいテーマですが、業務でもログ解析、入力チェック、簡易統計、レポート生成などで普通に出番があります。

例えば、
「このログメッセージで ‘E’(Error)がどれくらい出ているか知りたい」
「ユーザー入力に、特定の記号が多すぎないかチェックしたい」
「テキストの傾向をざっくり見たい」
といったときに、文字頻度カウントのユーティリティがあると便利です。

ここでは、初心者向けに、
基本的な実装パターン、Dictionary<char,int> を使う理由、LINQ版との違い、Unicodeをどう考えるか、
そして業務ユーティリティとしてどうまとめるかを、例題付きでかみ砕いて説明します。


基本の考え方 「1文字ずつ見て、カウンタを増やす」

文字頻度カウントの考え方はとてもシンプルです。

文字列を先頭から1文字ずつ見る。
その文字の「カウンタ」を1増やす。
最後に「文字 → 出現回数」の一覧ができている。

この「文字 → 回数」の対応を持つ入れ物として、C#では Dictionary<char, int> を使うのが定番です。
キーが文字、値がその文字の出現回数、というイメージです。


基本実装:Dictionary<char,int> を使った文字頻度カウント

一番素直なユーティリティメソッド

まずは、string を受け取って Dictionary<char,int> を返す、基本形を作ってみます。

using System;
using System.Collections.Generic;

public static class CharFrequencyUtil
{
    public static IReadOnlyDictionary<char, int> CountChars(string? text)
    {
        var result = new Dictionary<char, int>();

        if (string.IsNullOrEmpty(text))
        {
            return result;
        }

        foreach (char c in text)
        {
            if (result.ContainsKey(c))
            {
                result[c]++;
            }
            else
            {
                result[c] = 1;
            }
        }

        return result;
    }
}
C#

ここでやっていることを言葉で整理すると、こうなります。

まず、結果を入れるための Dictionary<char,int> を用意する。
文字列が null や空なら、そのまま空の辞書を返す(呼び出し側を楽にするための方針)。
文字列を1文字ずつループする。
その文字が辞書にすでにあれば、カウンタを1増やす。
なければ、新しくキーを追加して 1 を入れる。

これだけで、「どの文字が何回出たか」が全部集計できます。

動作例でイメージを固める

var freq = CharFrequencyUtil.CountChars("ABCAA");

foreach (var pair in freq)
{
    Console.WriteLine($"'{pair.Key}' : {pair.Value}");
}
C#

出力イメージは次のようになります。

'A' : 3
'B' : 1
'C' : 1

辞書は順序を保証しないので、出力順は変わることがありますが、
「文字と回数の対応」が取れていればOKです。


少しだけ洗練させる:TryGetValue を使う書き方

ContainsKey とインデクサを2回使う代わりに、TryGetValue を使うと、
辞書へのアクセス回数を少し減らせます。

public static IReadOnlyDictionary<char, int> CountChars(string? text)
{
    var result = new Dictionary<char, int>();

    if (string.IsNullOrEmpty(text))
    {
        return result;
    }

    foreach (char c in text)
    {
        if (result.TryGetValue(c, out int count))
        {
            result[c] = count + 1;
        }
        else
        {
            result[c] = 1;
        }
    }

    return result;
}
C#

やっていることは同じですが、
「キーがあるかどうかのチェック」と「値の取得」を1回で済ませているのがポイントです。
文字列が長くなってくると、こういう小さな工夫が積み重なって効いてきます。


LINQ を使った書き方との比較

GroupBy を使った“宣言的”な書き方

LINQを使うと、次のような書き方もできます。

using System.Linq;

public static IReadOnlyDictionary<char, int> CountCharsLinq(string? text)
{
    if (string.IsNullOrEmpty(text))
    {
        return new Dictionary<char, int>();
    }

    return text
        .GroupBy(c => c)
        .ToDictionary(g => g.Key, g => g.Count());
}
C#

ここでは、

GroupBy(c => c) で「同じ文字ごとにグループ化」
ToDictionary で「キー=文字、値=グループの件数」という辞書に変換

という流れになっています。

コードは短くて読みやすいですが、
内部的にはイテレータやグループオブジェクトが生成されるので、
パフォーマンス的には手書きループより少し重くなります。

業務で普通の長さの文字列を扱う分には問題ありませんが、
「大量のテキストを何度も集計する」ような場面では、
手書きループ版のほうが有利になることもあります。


Unicode をどう考えるか:「char単位」でいいのか問題

C#の char は「UTF-16 の1単位」

C#の char は「UTF-16 の1コード単位」です。
多くの日本語や英数字は char 1つで1文字ですが、
絵文字や一部の漢字・記号は「サロゲートペア」として char 2つで1文字になることがあります。

文字頻度カウントを char 単位で行うと、
サロゲートペアは「2つの別々の文字」としてカウントされます。

例えば、絵文字 😊 が含まれる文字列をカウントすると、
内部的には「上位サロゲート」「下位サロゲート」が別々にカウントされる形になります。

多くの業務システムで「日本語+英数字中心」のテキストを扱うなら、
char 単位のカウントで困ることはほとんどありません。
ただし、「絵文字を1文字として数えたい」「見た目の1文字単位で頻度を見たい」といった要件があるなら、
もう一段階踏み込んだ実装が必要になります。

見た目の1文字単位で数えたい場合の入口

厳密にやるなら、「テキスト要素(グラフェムクラスター)」単位で数える必要があります。
これは、前の回答でも触れた StringInfo を使うアプローチです。

イメージだけ示すと、次のようになります。

using System.Collections.Generic;
using System.Globalization;

public static IReadOnlyDictionary<string, int> CountTextElements(string? text)
{
    var result = new Dictionary<string, int>();

    if (string.IsNullOrEmpty(text))
    {
        return result;
    }

    var enumerator = StringInfo.GetTextElementEnumerator(text);
    while (enumerator.MoveNext())
    {
        string element = (string)enumerator.Current!;

        if (result.TryGetValue(element, out int count))
        {
            result[element] = count + 1;
        }
        else
        {
            result[element] = 1;
        }
    }

    return result;
}
C#

この場合、キーは char ではなく string(見た目の1文字)になります。
絵文字や結合文字も「1要素」としてカウントされるようになります。


業務ユーティリティとしてどうまとめるか

「用途」と「粒度」を決めておく

実務でユーティリティ化するなら、
まず「何を数えたいのか」をはっきりさせるのが大事です。

ログやID、コード、英数字中心のテキストの傾向を見たいなら、
Dictionary<char,int> ベースの CountChars で十分です。

ユーザー名やメッセージに絵文字が多用されるサービスで、
「見た目の1文字単位で頻度を見たい」なら、
CountTextElements のような“テキスト要素単位”のカウントを用意しておく価値があります。

例えば、こんなラッパーを用意しておくと、呼び出し側の意図が分かりやすくなります。

public static class TextAnalysis
{
    public static IReadOnlyDictionary<char, int> CountCharsRaw(string? text)
        => CharFrequencyUtil.CountChars(text);

    public static IReadOnlyDictionary<string, int> CountCharsVisual(string? text)
        => CountTextElements(text);
}
C#

呼び出し側は、「生のchar単位でいいのか」「見た目の1文字単位がいいのか」を選んで呼び分けられます。

null や空文字の扱いをユーティリティ側で固定する

どの実装でも共通しているのが、
「null や空文字なら、空の辞書を返す」という方針です。

これをユーティリティ側で決めておくことで、
呼び出し側は「とりあえず結果を foreach すればいい」という書き方ができます。
毎回 null チェックを書く必要がなくなり、業務コードがすっきりします。


まとめ 「文字頻度カウントユーティリティ」は“テキストの中身を数字で眺めるためのルーペ”

文字頻度カウントは、単なる練習問題ではなく、
テキストのクセや傾向を数字で把握するための、実務でも使える道具です。

押さえておきたいポイントは、
Dictionary<char,int> で「文字 → 回数」を持つのが基本形であること。
手書きループ版と LINQ版があり、パフォーマンスと読みやすさのバランスで選べること。
Unicode的には「1文字=1char」とは限らず、必要なら StringInfo で“見た目の1文字”単位に踏み込めること。
ユーティリティとしては、「nullは空の結果」「用途ごとに粒度を分ける」といったルールを決めておくと、呼び出し側が楽になること。

ここまで理解できれば、「なんとなく数えている」段階から一歩進んで、
“テキストの中身を意識的に分析できる文字列ユーティリティ”として、
文字頻度カウントを自分のC#コードに自然に組み込めるようになっていきます。

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