Set で重複を排除
Set で重複を排除 とは
Java の Set を使って、コレクションから重複した要素を取り除く方法を学びます。HashSet と LinkedHashSet と TreeSet の違い、Set ならではの API を整理しよう。
Set とは ─ 重複を許さない集合
Java のコレクションフレームワークには大きく分けて 3 つの仲間がいます。順序を持つ List、キーと値のペアを管理する Map、そして今回学ぶ「重複を許さない集合」である Set です。List が「並んだ箱の列」なら、Set は「同じものを 2 個入れられない袋」と覚えるとイメージしやすいです。
例えば、アンケートに答えてくれたユーザー ID のリストを想像してみてください。[101, 102, 101, 103, 102, 104] のように、同じ ID が複数回現れることがあります。ここで知りたいのが「実際に何人が回答したか」だけなら、重複を除いた {101, 102, 103, 104} の 4 件がわかれば十分です。こういうときに活躍するのが Set です。
Setは「中身に何が入っているか」だけを管理し、「何回入れたか」「どの順番で入れたか」は基本的に気にしません。重複を意識せずに add してよい、というのは実装するうえで非常に楽です。
Set は数学の集合 (set) とほぼ同じ概念で、Set 同士を合体させる和集合、共通要素だけ残す積集合、片方から片方を引く差集合といった操作も addAll retainAll removeAll といったメソッドで素直に書けます。重複が許されない代わりに、こうした集合演算が得意というわけです。
「
ListかSetか迷ったら、要素の出現回数や順序が大事ならList、含まれているかどうかだけが大事ならSetを選ぶ」と覚えておくとほぼ外しません。
HashSet / LinkedHashSet / TreeSet の違い
Set は interface なので、実装クラスを選ぶ必要があります。代表的なのは 3 つで、それぞれ得意分野が違います。
HashSet— 最もよく使う実装。内部はハッシュテーブルで、追加・削除・検索が平均O(1)と高速。ただし反復順序は保証されないLinkedHashSet—HashSetに「挿入された順番」を覚える連結リストをくっつけたもの。速度は少し落ちるが、追加した順に取り出せるTreeSet— 内部がRed-Black Tree(赤黒木) になっていて、要素が常にソートされた状態で保たれる。検索はO(log n)
用途の目安は次の通りです。ただ重複を排除したいだけなら HashSet、入れた順を保ちたいなら LinkedHashSet、ソート済みで取り出したいなら TreeSet を選びます。今回の課題では「重複しない要素数」を数えるだけなので HashSet が最適です。
図のとおり、どの
Set実装に変換しても、要素そのものは1, 2, 3の 3 つにまとまります。違うのは「中での並び方」だけです。後で順序を活かしたくなったらLinkedHashSetやTreeSetに差し替えれば OK で、本体のロジックはほぼ変えなくて済みます。
add / contains / remove / size
Set で覚えるメソッドはとてもシンプルです。よく使う 4 つを並べると次のとおりです。
add(E e)— 要素を追加する。すでに含まれていれば何もせずfalseを返す。新しく追加できればtruecontains(Object o)— 含まれているか判定する。HashSetなら平均O(1)で爆速remove(Object o)— 要素を削除する。含まれていなければfalsesize()— 現在の要素数を返す
コードで触ってみましょう。int を入れたいときは、Set<int> とは書けずラッパークラスの Integer を使います (int は基本型なのでジェネリクスに直接入れられない、というのが Java の制約です)。
Java
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
Set<Integer> ids = new HashSet<>();
ids.add(101);
ids.add(102);
ids.add(101); // 重複しているので無視される
ids.add(103);
System.out.println(ids.size()); // 3
System.out.println(ids.contains(102)); // true
ids.remove(102);
System.out.println(ids.contains(102)); // false
}
}配列から重複を除いた個数を数える、という今回の課題で使うパターンは次の通りです。for で 1 件ずつ add していくだけで、Set が自動的に重複をはじいてくれます。
Java
import java.util.HashSet;
import java.util.Set;
public class Solution {
public static int uniqueCount(int[] arr) {
Set<Integer> seen = new HashSet<>();
for (int n : arr) {
seen.add(n);
}
return seen.size();
}
}空配列 [] を渡しても、for のループ自体が回らないので seen.size() は 0 になり、自然と正しく動きます。場合分けを書かなくて済むのも Set のいいところです。
List との違い
List と Set の違いを表で整理すると次の通りです。
- 重複の扱い —
Listは許す、Setは許さない - 順序 —
Listは挿入順を保つ、HashSetは保たない - インデックスアクセス —
Listはget(i)で取れる、Setは取れない - 検索 —
ListはO(n)、HashSetは平均O(1) - 主な用途 —
Listは並び順が大事なデータ、Setは集合演算や重複排除
「あるか / ないか」を頻繁にチェックするなら、たとえ重複が無いデータでも
Setを選んだほうが速いです。List.containsは要素数に比例して遅くなりますが、HashSet.containsは要素数が増えてもほぼ一定の速さです。
よくある間違い
Set を使い始めたばかりの人がハマりがちな落とし穴を 3 つ紹介します。
HashSetの順序を期待してしまう —HashSetは要素の並びを保証しません。forで回したときの順番がプログラムを再起動したら変わる、ということも普通に起きます。順序が大事ならLinkedHashSetかTreeSetを使いましょうequals/hashCodeの実装を忘れる — 自作クラスをHashSetに入れるときは、equalsとhashCodeを両方ともオーバーライドする必要があります。片方だけだと「同じはずの 2 つが別物扱いされる」事故が起きますnullの扱いを誤解する —HashSetとLinkedHashSetはnullを 1 個だけ入れられます。TreeSetはnullを入れようとするとNullPointerExceptionで落ちます。Setならどれでも同じ、と思い込まないようにしましょう
自作クラスの
equalsだけ書いてhashCodeを書き忘れる、というのは本当によくあるバグです。IDEの「自動生成」機能で必ずペアで作るクセを付けておくと安全です。
配列を Set に変える便利な書き方として Set.of(...) や new HashSet<>(Arrays.asList(arr)) もありますが、Set.of は不変 (immutable) な集合を作るので、あとから add しようとすると例外が出ます。「読むだけ」なら Set.of、「足したり消したり」したいなら new HashSet<>() と使い分けましょう。
やってみよう
それでは実際に今回の課題に挑戦してみましょう。
- 右側のエディタを開く
import java.util.HashSet;とimport java.util.Set;を一番上に書くuniqueCountメソッドの中でSet<Integer>を作って、forループで配列の各要素をaddする- 最後に
return seen.size();する - 「実行」ボタンを押して、テストが全部緑になることを確認する
動いたら、ぜひ HashSet を LinkedHashSet や TreeSet に置き換えても同じ結果になることを試してみてください。実装を切り替えても呼び出し側のコードが一切変わらない、というのが Java のコレクションフレームワークの美しい設計です。さらに余裕があれば、String[] を受け取るバージョンに書き換えてみたり、重複している要素そのものをリストアップする版を自作してみたりすると、Set への理解が一段深まります。
よくある質問
Q. set とリストはどう違いますか?
A. set は重複を持たない集合で、要素の追加と存在確認が O(1) と高速です。リストは順序付きで重複を許し、要素の前後関係が意味を持つ場面で使います。重複除去や「含まれているか」判定の高速化が必要なら set が圧倒的に有利です。
Q. set の順序は保証されますか?
A. set は順序が保証されません。順序を保ったまま重複を除去したい場合は dict.fromkeys(items) を使う(Python)か、Set + 配列の組み合わせで管理してください。順序付きで集合演算したい場面は意外と多いので覚えておくと便利です。
Q. set 同士の積集合・和集合はどう書きますか?
A. a & b で積集合、a | b で和集合、a - b で差集合になります。リスト同士の共通要素を求めたいときは set(a) & set(b) で O(n) に高速化できます。文字列もイテラブルなので set('abc') & set('bcd') → {'b', 'c'} のように使えます。
次のレッスン
次は Collections クラスの便利メソッド で、Collections クラスの便利メソッド を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- Set の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Set とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- クラス名は
Solution、メソッド名はuniqueCount、戻り値の型はint、引数はint[] arrの 1 つ - 重複を排除した要素数を返すこと。
Set(HashSet推奨) を使って実装する - 空配列を渡されたときは
0を返すこと
入出力例
test-cases.txt
uniqueCount([1,2,2,3,3,3]) → 3
uniqueCount([]) → 0
uniqueCount([5,5,5,5]) → 1
uniqueCount([1,2,3,4]) → 4
uniqueCount([-1,0,-1,0,7]) → 3