Java | Java 標準ライブラリ:TreeSet

Java Java
スポンサーリンク

TreeSet をざっくりイメージする

まず全体像からいきます。

TreeSet

「重複を許さない集合(Set)でありつつ、
 中身を“常にソートされた順番”で持ってくれるクラス」

です。

前回の HashSet は「順番は一切気にしない代わりに、高速な集合」でした。
TreeSet は、

  • Set なので「重複なし」
  • さらに「常に小さい順/辞書順に整っている」
  • その代わりに HashSet より少し重い

という性質を持っています。

コードではたいていこう書きます。

import java.util.Set;
import java.util.TreeSet;

Set<Integer> set = new TreeSet<>();
Java

左側を Set(インターフェース)にしておくのは、常に良い習慣です。


HashSet と TreeSet の違いを直感で押さえる

HashSet:順番はどうでもいいけど、とにかく速く「存在チェック」したい

Set<String> set = new HashSet<>();
set.add("banana");
set.add("apple");
set.add("orange");

System.out.println(set);  // [orange, banana, apple] など順番バラバラ
Java

HashSet の特徴は、

  • 「順番の概念がない」
  • contains がとても速い(多くの場合 O(1))

です。

順番に意味がない「単なる集合」として使うなら、HashSet が一番シンプルです。

TreeSet:常に「小さい順/辞書順」で並べて持ってくれる

import java.util.Set;
import java.util.TreeSet;

Set<String> set = new TreeSet<>();
set.add("banana");
set.add("apple");
set.add("orange");

System.out.println(set);  // [apple, banana, orange] (常にソート順)
Java

TreeSet の特徴は、

  • 要素が常に「ソートされた順」に並んでいる
  • contains や追加・削除も比較的速い(内部は「木構造」)

です。

「集合として重複なしで持ちたい」かつ「順番も意味がある(ソートしたい)」
というときに TreeSet がかなり気持ちよくハマります。


TreeSet の基本操作(add / remove / contains / size)

要素を追加する add(重複は禁止)

Set<Integer> set = new TreeSet<>();

set.add(30);
set.add(10);
set.add(20);
set.add(10);  // 重複

System.out.println(set);        // [10, 20, 30]
System.out.println(set.size()); // 3
Java

Set なので、同じ値を何度 add しても、入っているのは一つだけです。

内部的には「二分探索木(赤黒木)」という構造で管理されていて、
追加した時点で「適切な位置」に挿入され、
結果として常に小さい順になっています。

contains で「あるかどうか」を調べる

if (set.contains(20)) {
    System.out.println("20 は含まれている");
}
Java

HashSet ほどではありませんが、
ほとんどの用途では十分に速く動きます。

「重複なし・ソート済み・存在チェックも速い」の三拍子というイメージでいて大丈夫です。

remove で削除

set.remove(20);
System.out.println(set);  // [10, 30]
Java

存在しないものを remove しても例外は出ず、何も起きません(戻り値が false)。


「常にソートされている」ことを活かす周辺メソッド

TreeSet は、SortedSet / NavigableSet というインターフェースも実装しています。
つまり、「順序付きならでは」の便利メソッドが使えます。

ここが HashSet との決定的な違いです。少し深掘りします。

最小値/最大値を簡単に取れる(first / last)

TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(30);
set.add(20);

System.out.println(set.first()); // 10(最小値)
System.out.println(set.last());  // 30(最大値)
Java

最小値・最大値がすぐ取れるのは、「常にソート済み」の TreeSet ならではです。
HashSet ではこうはいきません。

ある値より「小さい最大」「大きい最小」を取る(lower / higher)

TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);

System.out.println(set.lower(25));   // 20(25より小さい中で最大)
System.out.println(set.higher(25));  // 30(25より大きい中で最小)
Java

「ある値のすぐ前/すぐ後ろ」を知りたいときに使えます。

例えば、「あるスコアに一番近い、上のランクを知りたい」など、
ランキングや閾値の判定に便利です。

以下/以上の範囲を取る(floor / ceiling)

System.out.println(set.floor(25));   // 20(25以下で最大)
System.out.println(set.ceiling(25)); // 30(25以上で最小)

System.out.println(set.floor(20));   // 20(ちょうどあるのでそれ)
System.out.println(set.ceiling(20)); // 20(同上)
Java

lower / higher は「より小さい」「より大きい」だけですが、
floor / ceiling は「同じ値を含むかどうか」がポイントです。

この違いは、境界判定や「しきい値」の処理で効いてきます。

部分集合を切り出す(headSet / tailSet / subSet)

TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);
set.add(50);

System.out.println(set.headSet(30));       // [10, 20]     (30より小さい要素)
System.out.println(set.tailSet(30));       // [30, 40, 50] (30以上の要素)
System.out.println(set.subSet(20, 50));    // [20, 30, 40] (20以上50未満)
Java

「ある範囲のデータだけ欲しい」というときに、
一発で「範囲ビュー」が取れるのが TreeSet の強みです。

ここまで来ると少し高度ですが、
「TreeSet はソートされているからこそ“範囲操作”が得意なんだ」
という感覚だけ持っておくと、後で効いてきます。


String / 自作クラスを TreeSet に入れるときの「順序」の話

String を入れると辞書順に並ぶ

TreeSet<String> set = new TreeSet<>();
set.add("banana");
set.add("apple");
set.add("orange");

System.out.println(set);  // [apple, banana, orange]
Java

String は「自然な順序(自然順序)」として辞書順が定義されているので、
TreeSet に入れるだけで勝手に辞書順に並びます。

自作クラスを入れる場合は「どう並べるか」を決める必要がある

TreeSet は内部で「小さい順」を判断するために、
compareToComparator を使います。

自作クラスを TreeSet に入れたいときは、
少なくともどちらかが必要になります。

1. クラス自体に「自然順序」を実装する(Comparable)

class User implements Comparable<User> {
    String name;
    User(String name) { this.name = name; }

    @Override
    public int compareTo(User other) {
        return this.name.compareTo(other.name); // 名前の辞書順で比較
    }

    @Override
    public String toString() {
        return name;
    }
}
Java

このようにしておくと、

TreeSet<User> users = new TreeSet<>();
users.add(new User("Bob"));
users.add(new User("Alice"));
users.add(new User("Carol"));

System.out.println(users); // [Alice, Bob, Carol]
Java

のように、name の辞書順に並んでくれます。

2. TreeSet を作るときに Comparator を渡す

クラス自体に自然順序を持たせたくない場合は、
TreeSet を new するときに「並べ方」を渡せます。

TreeSet<User> users = new TreeSet<>(
        (u1, u2) -> u1.name.compareTo(u2.name)  // ラムダでComparator
);
Java

このようにすると、「この TreeSet では name の辞書順で並べる」
というルールになります。

ここで重要なのは、

  • TreeSet は「順序が定義できない型」は扱えない
  • 自作クラスを入れるときは「何を基準に並べるか」をはっきり決める必要がある

という点です。


「HashSet ではなく TreeSet を選ぶべき場面」と「逆の場面」

TreeSet を選ぶ場面

次のような条件のとき、TreeSet が向いています。

  • 重複なしで良い(Set でよい)
  • 順番に意味がある(小さい順や辞書順で扱いたい)
  • 最小・最大、範囲検索、しきい値の判定など「順序を使う」ロジックがある

例えば:

  • ソート済みのユニークなタグ一覧を持ちたい
  • スコアの集合を持っておいて、「何点以上」「何点未満」のような判定をしたい
  • ユニークなユーザー名を辞書順で列挙したい

こういうときは、HashSet + 別途ソート でもできますが、
「ソートされた集合」として最初から TreeSet にしておくとコードが素直になります。

HashSet を選ぶ場面

逆に、次のようなときは HashSet が向いています。

  • 重複なしで良い
  • 順番はほんとうにどうでもいい
  • とにかく「存在チェック」が高速であってほしい

例えば:

  • ログイン中ユーザーIDの集合(いるかいないかだけ重要)
  • 使用済みトークンの集合
  • 一度見たことがある単語かどうかのチェック

こういう用途で TreeSet を使うと、
「順序機能にコストを払っているのに使っていない」状態になります。


まとめ:TreeSet を自分の中でこう位置づける

初心者向けに TreeSet の本質をまとめると、こうなります。

TreeSet は「重複なし」+「常にソートされた順」を両立した Set。
HashSet との違いは、「順番を持つかどうか」と「そのために木構造を使っているかどうか」。
first, last, lower, higher, floor, ceiling, headSet, tailSet, subSet など、順序ベースの便利メソッドがある。
String や Integer など「自然順序」がある型はそのまま使える。自作クラスを入れるなら Comparable 実装や Comparator が必要。
「順番に意味があるユニーク集合」を扱うときに選ぶ。単なる存在チェックだけなら HashSet のほうが素直。

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