map で出現回数を数える

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

map で出現回数を数える

このレッスンで分かること

  • 配列の各要素が何回現れたかを数えるという作業は、データ分析の現場でも実装の現場でも頻出します
  • 素朴に二重ループで書くと O(n^2) になりますが、map を使えば 1 パスで完了します
  • 関数 frequency(arr) は整数配列を受け取り、出現する値の最大頻度 (= もっとも多く現れた値の出現回数) を整数で返します

map で出現回数を数える とは

ハッシュマップ (dict / Map) を使って、整数配列の各値の出現回数を 1 パスで集計する。本レッスンでは、map で出現回数を数える の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

配列の各要素が何回現れたかを数えるという作業は、データ分析の現場でも実装の現場でも頻出します。アンケート集計、ログのエラーコード分類、テキスト中の単語頻度、これらは全部「出現回数を数える」という同じ操作です。本レッスンでは map (ハッシュマップ) を使って、O(n) で頻度集計を行う基本パターンを身につけます。

素朴に二重ループで書くと O(n^2) になりますが、map を使えば 1 パスで完了します。これは第 4 章のうち、最もよく実戦で書くコード の 1 つです。

配列の頻度集計は map (dict / HashMap) の代表例。O(n^2) から O(n) に落とすための基本パターン。

仕様

関数 frequency(arr) は整数配列を受け取り、出現する値の最大頻度 (= もっとも多く現れた値の出現回数) を整数で返します。空配列のときは 0 を返します。たとえば [1, 2, 2, 3, 3, 3] の最大頻度は 3 です。

戻り値を「値」ではなく「頻度 (回数)」にする理由は、戻り値の型が整数 1 つで済み、テストが言語依存しないからです。中で使う map のキーが整数でも文字列でもよく、純粋に「頻度を数える」操作だけに集中できます。

アルゴリズム

diagram (will load when visible)

手順は次のとおりです。

  1. 空の map を 1 つ用意する (キーは値、バリューは出現回数)
  2. 配列を 1 度走査し、各要素 v に対し map[v] += 1 を行う
  3. ループ中 (または後で) 最大頻度を求めて返す

ループ中で max を更新すれば、2 周目を回す必要はなく O(n) で終わります。

Python での実装

Python

def frequency(arr): if not arr: return 0 counts = {} best = 0 for v in arr: counts[v] = counts.get(v, 0) + 1 if counts[v] > best: best = counts[v] return best

counts.get(v, 0)キーが無ければ 0 を返す安全なアクセスです。counts[v] += 1 だけ書くと、初回アクセス時に KeyError になります。get(v, 0)defaultdict(int) を使う癖 をつけましょう。Python なら collections.Counter を使えば 1 行ですが、ここでは原理を学ぶために自力で書きます。

JavaScript での実装

JavaScript

function frequency(arr) { if (arr.length === 0) return 0; const counts = new Map(); let best = 0; for (const v of arr) { const c = (counts.get(v) || 0) + 1; counts.set(v, c); if (c > best) best = c; } return best; }

MapObject と違ってキーの型を保ったまま保存できます。整数キーをそのまま使えるので、競技プログラミング系の問題と相性が良いです。(counts.get(v) || 0) + 1 という書き方で、未登録キーを 0 扱いにします。

1 パスで集計しながら最大値も同時に更新できる点が、map パターンの真価。2 周しなくて良いのが嬉しい。

Java / Go での実装

Java

import java.util.*; public class Solution { public static int frequency(int[] arr) { if (arr.length == 0) return 0; Map<Integer, Integer> counts = new HashMap<>(); int best = 0; for (int v : arr) { int c = counts.getOrDefault(v, 0) + 1; counts.put(v, c); if (c > best) best = c; } return best; } }

Java の Map.getOrDefault(key, default)Pythondict.get(key, default) と同じ感覚で使えます。getOrDefault がなかった時代は if (map.containsKey(k)) を毎回書いていましたが、今は楽になりました。

計算量

ハッシュマップへの getset は平均 O(1)。配列の長さを n とすると、ループは n 回で、各イテレーションは O(1)。よって全体は O(n) です。これを二重ループの O(n^2) で書くと、要素数が 10000 のとき 1 億回 の比較が走り、現実的なアプリでは耐えられない速度になります。

ハッシュマップの操作は平均 O(1)。配列の頻度集計は O(n) で終わる、これが基本中の基本。

よくある間違い

1 つ目は 未登録キーへの加算 です。counts[v] += 1 を最初に書くと、未登録キーで KeyErrorNaN が出ます。get(v, 0) で初期値を補う こと。

2 つ目は 空配列の戻り値 を考えていないケース。最大頻度を聞かれているのに、best = 0 のまま空配列で何も走らないと 0 を返せて結果的に OK ですが、best の初期値を Infinity 系にしてしまうと壊れます。最初に空チェックを入れるのが安全です。

3 つ目は 2 周走らせてしまう こと。1 周目で集計、2 周目で max を取る書き方でも正解は出ますが、ループ内で同時に best を更新すれば 1 周で済みます。O(n) 自体は変わりませんが、定数倍が半分になります。

やってみよう

  • [1, 1, 2, 3, 3, 3, 4] の最大頻度が 3 (値 3 が 3 回) になることを手で追う。
  • 同じ map を使って 最頻値そのもの (頻度ではなく値) を返すバージョンも書いてみる。
  • collections.Counter (Python) や _.countBy (lodash) のような既製ライブラリを使ったバージョンと比較する。
  • map のキーを「数値そのもの」ではなく「v % 10 のような派生キー」にすると、グルーピングと頻度集計が同時にできる。試してみる。

よくある質問

Q. dict と Counter どちらを使うべきですか?

A. 原理を学ぶ段階では dict + get(v, 0) で自力実装するのがおすすめです。実務コードでは collections.Counter が簡潔で可読性も高いため積極的に使って問題ありません。Counter は dict のサブクラスなので、習得後に切り替えてもロジックの変更はほぼ不要です。

Q. キーが文字列でも整数でも同じように動きますか?

A. はい、ハッシュマップのキーはハッシュ可能なオブジェクトであれば何でも使えます。整数でも文字列でもタプルでも同じ操作で集計できます。JavaScript の Map はキーの型を保ったまま保存できるため、文字列 "1" と整数 1 が別キーとして扱われる点に注意してください。

Q. defaultdict と get() の使い分けは?

A. get(v, 0) はその場で初期値を指定するスタイルで、特別なインポートが不要です。defaultdict(int) はキーが存在しない場合に自動で 0 を補填するため、counts[v] += 1 とシンプルに書けます。どちらも正解ですが、初学者は get(v, 0) の方が何が起きているかを意識しやすいです。

Q. 最大頻度ではなく最頻値そのものが欲しいときはどうしますか?

A. 集計後に max(counts, key=counts.get) (Python) や [...counts.entries()].reduce((a, b) => b[1] > a[1] ? b : a)[0] (JavaScript) のように、バリューで比較してキーを取り出す方法が一般的です。Python では Counter.most_common(1)[0][0] が最もシンプルです。

次のレッスン

次は 二数の和 (map で O(n)) に進みましょう。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. 出現回数 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 出現回数 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. 整数を返す。最大頻度 (もっとも多く出た値の回数)
  2. map / HashMap / dict を使い、1 パスの O(n) で集計する
  3. 空配列のときは 0 を返す

入出力例

test-cases.txt

frequency([1,2,2,3,3,3])3 frequency([1,2,3,4])1 frequency([5,5,5,5,5])5 frequency([7])1 frequency([1,1,2,2])2 frequency([-1,-1,-1,0,1])3

ヒント

main.py
main.py
学習モード

メモ

map で出現回数を数える

⌘S で保存