JavaのTreeMapガイド

JavaでのTreeMapのガイド

1. 概要

この記事では、Javaコレクションフレームワーク(JCF)からのMapインターフェースのTreeMap実装について説明します。

TreeMapは、キーの自然な順序に従ってエントリを並べ替えたままにするマップ実装です。構築時にユーザーから提供された場合は、コンパレータを使用することをお勧めします。

以前、HashMapLinkedHashMapの実装について説明しましたが、これらのクラスがどのように機能するかについて、同様の情報がかなりあることに気付くでしょう。

前述の記事は、この記事を読む前に読むことを強くお勧めします。

2. デフォルトのソートはTreeMap

デフォルトでは、TreeMapはすべてのエントリを自然な順序に従って並べ替えます。 整数の場合、これは昇順、文字列の場合はアルファベット順を意味します。

テストで自然順序を見てみましょう。

@Test
public void givenTreeMap_whenOrdersEntriesNaturally_thenCorrect() {
    TreeMap map = new TreeMap<>();
    map.put(3, "val");
    map.put(2, "val");
    map.put(1, "val");
    map.put(5, "val");
    map.put(4, "val");

    assertEquals("[1, 2, 3, 4, 5]", map.keySet().toString());
}

整数キーを不規則な方法で配置しましたが、キーセットを取得すると、それらが実際に昇順で保持されていることを確認します。 これは整数の自然な順序です。

同様に、文字列を使用すると、自然な順序で並べ替えられます。 アルファベット順:

@Test
public void givenTreeMap_whenOrdersEntriesNaturally_thenCorrect2() {
    TreeMap map = new TreeMap<>();
    map.put("c", "val");
    map.put("b", "val");
    map.put("a", "val");
    map.put("e", "val");
    map.put("d", "val");

    assertEquals("[a, b, c, d, e]", map.keySet().toString());
}

TreeMapは、ハッシュマップやリンクされたハッシュマップとは異なり、エントリを格納するために配列を使用しないため、どこにもハッシュ原理を採用していません。

3. TreeMapでのカスタムソート

TreeMapの自然な順序に満足できない場合は、ツリーマップの作成中にコンパレータを使用して順序付けの独自のルールを定義することもできます。

以下の例では、整数キーを降順に並べる必要があります。

@Test
public void givenTreeMap_whenOrdersEntriesByComparator_thenCorrect() {
    TreeMap map =
      new TreeMap<>(Comparator.reverseOrder());
    map.put(3, "val");
    map.put(2, "val");
    map.put(1, "val");
    map.put(5, "val");
    map.put(4, "val");

    assertEquals("[5, 4, 3, 2, 1]", map.keySet().toString());
}

ハッシュマップは、格納されたキーの順序を保証せず、特にこの順序が時間とともに変わらないことを保証しませんが、ツリーマップは、キーが常に指定された順序に従ってソートされることを保証します。

4. TreeMapの並べ替えの重要性

これで、TreeMapがすべてのエントリをソートされた順序で格納することがわかりました。 ツリーマップのこの属性により、次のようなクエリを実行できます。 「最大」を見つける、「最小」を見つける、特定の値より小さいまたは大きいすべてのキーを見つけるなど。

以下のコードは、これらのケースのほんの一部をカバーしています。

@Test
public void givenTreeMap_whenPerformsQueries_thenCorrect() {
    TreeMap map = new TreeMap<>();
    map.put(3, "val");
    map.put(2, "val");
    map.put(1, "val");
    map.put(5, "val");
    map.put(4, "val");

    Integer highestKey = map.lastKey();
    Integer lowestKey = map.firstKey();
    Set keysLessThan3 = map.headMap(3).keySet();
    Set keysGreaterThanEqTo3 = map.tailMap(3).keySet();

    assertEquals(new Integer(5), highestKey);
    assertEquals(new Integer(1), lowestKey);
    assertEquals("[1, 2]", keysLessThan3.toString());
    assertEquals("[3, 4, 5]", keysGreaterThanEqTo3.toString());
}

5. TreeMapの内部実装

TreeMapは、NavigableMapインターフェースを実装し、赤黒木の原則に基づいて内部作業を行います。

public class TreeMap extends AbstractMap
  implements NavigableMap, Cloneable, java.io.Serializable

赤黒木の原理はこの記事の範囲を超えていますが、それらがTreeMapにどのように適合するかを理解するために覚えておくべき重要なことがあります。

First of all、赤黒木はノードで構成されるデータ構造です。根が空にあり、枝が下に伸びている倒立マンゴーの木を想像してみてください。 ルートには、ツリーに追加された最初の要素が含まれます。

ルールは、ルートから開始して、ノードの左ブランチの要素は常にノード自体の要素よりも小さいということです。 右側のものは常に大きくなります。 より大きいかより小さいかを定義するものは、前に見たように、要素の自然な順序または構築時に定義されたコンパレータによって決まります。

このルールは、ツリーマップのエントリが常にソートされ、予測可能な順序になることを保証します。

Secondly、赤黒木は自己平衡二分探索木です。 この属性と上記は、検索、取得、配置、削除などの基本的な操作に対数時間O(log n)かかることを保証します。

ここでは、自己バランスを保つことが重要です。 エントリの挿入と削除を続けながら、ツリーの一方の端が長くなり、もう一方の端が短くなることを想像してください。

これは、短いブランチでは操作にかかる時間が短く、ルートから最も遠いブランチでは長い時間がかかることを意味します。

したがって、これは赤黒木の設計で考慮されます。 挿入と削除のたびに、任意のエッジのツリーの最大高さがO(log n)に維持されます。 ツリーは継続的にバランスを取ります。

ハッシュマップとリンクハッシュマップのように、ツリーマップは同期されないため、マルチスレッド環境で使用するためのルールは他の2つのマップ実装のルールと同様です。

6. 適切なマップの選択

以前はHashMapLinkedHashMapの実装を、現在はTreeMapを見てきたので、3つを簡単に比較して、どちらがどこに適合するかをガイドすることが重要です。

A hash mapは、迅速な保存および取得操作を提供する汎用マップの実装として適しています。 ただし、エントリが無秩序で不規則に配置されているため、不十分です。

これにより、基礎となる配列の容量全体がエントリの数以外のトラバースに影響するため、反復が多いシナリオではパフォーマンスが低下します。

A linked hash mapは、ハッシュマップの優れた属性を備えており、エントリに順序を追加します。 容量に関係なくエントリ数のみが考慮されるため、反復が多い場所でパフォーマンスが向上します。

A tree mapは、キーの並べ替え方法を完全に制御することにより、順序付けを次のレベルに引き上げます。 一方、他の2つの選択肢よりも一般的なパフォーマンスが低下します。

linked hash map reduces the chaos in the ordering of a hash map without incurring the performance penalty of a tree mapと言えます。

7. 結論

この記事では、JavaTreeMapクラスとその内部実装について説明しました。 一連の一般的なMapインターフェース実装の最後であるため、他の2つのインターフェースとの関連で最適な場所についても簡単に説明しました。

この記事で使用されているすべての例の完全なソースコードは、GitHub projectにあります。