ハッシュセットとツリーセット


496

私はいつも木を愛してきましO(n*log(n))た。ただし、私がこれまでに知っているすべてのソフトウェアエンジニアから、なぜを使用するのかがはっきりと尋ねられましたTreeSet。CSのバックグラウンドから、私はあなたが何を使うかはそれほど重要ではないと思います、そして私はハッシュ関数とバケット(の場合Java)をいじくり回す必要はありません。

どの場合に私はHashSetオーバーを使用する必要がありTreeSetますか?

回答:


860

HashSetはTreeSetよりもはるかに高速です(追加、削除、包含などのほとんどの操作での一定時間とログ時間)。TreeSetのような順序の保証はありません。

ハッシュセット

  • このクラスは、基本的な操作(追加、削除、包含、サイズ)に対して一定の時間パフォーマンスを提供します。
  • 要素の順序が長期にわたって一定であることを保証するものではありません
  • 反復のパフォーマンスは、初期容量とHashSetの負荷係数に依存します。
    • デフォルトの負荷係数を受け入れることは非常に安全ですが、セットが大きくなると予想されるサイズの約2倍の初期容量を指定することもできます。

TreeSet

  • 基本操作(追加、削除、包含)のlog(n)時間コストを保証します
  • セットの要素がソートされることを保証します(昇順、自然、またはコンストラクターによって指定されたもの)(実装SortedSet
  • 反復パフォーマンスのための調整パラメーターを提供していません
  • 申し出のような順序付きセットに対処するためのいくつかの便利な方法first()last()headSet()、およびtailSet()など

重要なポイント:

  • どちらも重複のない要素のコレクションを保証します
  • 一般に、要素をHashSetに追加してから、コレクションをTreeSetに変換して、重複のないソートされた走査を行う方が高速です。
  • これらの実装は同期されていません。つまり、複数のスレッドがセットに同時にアクセスし、少なくとも1つのスレッドがセットを変更する場合は、外部で同期する必要があります。
  • LinkedHashSetのは、ある意味での間の中間であるHashSetTreeSet。リンクリストが実行されるハッシュテーブルとして実装されますが、TreeSetで保証されているソート済みトラバーサルとは異なる挿入順の反復を提供します

したがって、使用方法の選択は完全にニーズに依存しますが、順序付けられたコレクションが必要な場合でも、HashSetを使用してセットを作成し、それをTreeSetに変換することをお勧めします。

  • 例えば SortedSet<String> s = new TreeSet<String>(hashSet);

38
「HashSetはTreeSetよりもずっと速い(一定時間対ログ時間...)」という断定を明らかに間違っているのは私だけですか?まず、これは絶対時間ではなく時間の複雑さに関するものであり、O(1)は多くの場合O(f(N))よりも遅くなる可能性があります。次に、O(logN)は「ほぼ」O(1)です。多くの一般的なケースで、TreeSetがHashSetよりも優れていたとしても、私は驚かないでしょう。
lvella 2012年

22
Ivellaのコメントを2番目にしたいと思います。時間複雑性はありませ時間を実行するのと同じこと、およびO(1)は、常により良いOよりも(2 ^ n)のではありません。不可解な例が要点を示しています。10要素に対して、1兆のマシン命令を実行するハッシュアルゴリズムを使用したハッシュセット(O(1))と一般的なバブルソートの実装(O(N ^ 2)avg / worst)を考えてください。 。バブルソートは毎回勝ちます。ポイントは、アルゴリズムのクラスは、誰もが時間の複雑さを利用して近似を考えることが、現実の世界で一定の要因が教えるある問題で頻繁に。
Peter Oehlert、2007

17
多分それは私だけですが、最初にすべてをハッシュセットに追加し、それをツリーセットに変換するのは恐ろしいものではありませんか?1)ハッシュセットへの挿入は、事前にデータセットのサイズがわかっている場合にのみ高速です。それ以外の場合は、O(n)の再ハッシュを(場合によっては複数回)実行します。2)セットを変換するときに、とにかくTreeSet挿入の料金を支払います。(復讐を伴う、なぜならハッシュセットを介した反復はひどく効率的ではないからです)
TinkerTank

5
このアドバイスは、セットの場合、アイテムを追加する前に、アイテムが重複しているかどうかを確認する必要があるという事実に基づいています。したがって、ツリーセットでハッシュセットを使用している場合は、重複を排除する時間を節約できます。ただし、非重複の2番目のセットを作成するために支払う代償を考えると、重複の割合は、この代価を克服して時間を節約するために本当に大きいはずです。そしてもちろん、これは中規模および大規模のセットの場合です。これは、小規模なセットの場合、ツリーセットはハッシュセットよりも高速であるためです。
SylvainL、

5
@PeterOehlert:そのためのベンチマークを提供してください。ポイントを理解しましたが、コレクションサイズが小さい場合、両方のセットの違いはほとんど問題になりません。そして、セットが大きくなり、実装が重要になるポイントになるとすぐに、log(n)が問題になっています。一般に、リーフを検索/アクセス/追加/変更するためのハッシュ関数(複雑なものでも)の大きさは、いくつかのキャッシュミス(ほとんどすべてのアクセスレベルの巨大なツリーにあります)よりも高速です。少なくとも、Javaでのこれら2つのセットの私の経験です。
Bouncner 2013年

38

aについてまだ言及されていない利点の1つTreeSetは、その「局所性」が大きいことTreeSetです。(2)この配置は、類似性のデータが類似の頻度でアプリケーションによってアクセスされることが多いと述べている局所性の原理を利用しています。

これはHashSet、キーが何であれ、エントリをメモリ全体に分散するとは対照的です。

ハードドライブからの読み取りのレイテンシコストがキャッシュまたはRAMからの読み取りのコストの数千倍であり、データが局所性で実際にアクセスさTreeSetれる場合、これははるかに優れた選択肢です。


3
2つのエントリが順序の近くにある場合、TreeSetはそれらをデータ構造内、したがってメモリ内で互いに近くに配置することを実証できますか?
David Soroko、2015年

6
Javaにはまったく関係ありません。セットの要素はとにかくオブジェクトであり、どこか他の場所を指しているので、何も保存していません。
Andrew Gallasch 2015

Javaにおける局所性の欠如について行われた他のコメントに加えて、OpenJDKのTreeSet/ の実装はTreeMap局所性が最適化されていません。赤黒木を表すために4次のbツリーを使用して、局所性とキャッシュパフォーマンスを向上させることは可能ですが、それは実装が機能する方法ではありません。代わりに、各ノードは、独自のキー、独自の値、その親、およびその左右の子ノードへのポインターを格納します。これは、TreeMap.EntryのJDK 8ソースコードで明らかです。
kbolino

25

HashSet要素にアクセスするためのO(1)なので、それは確かに重要です。ただし、セット内のオブジェクトの順序を維持することはできません。

TreeSet順序を維持することが重要な場合(挿入順序ではなく値に関して)は便利です。ただし、すでに述べたように、要素にアクセスするためのより遅い時間で注文を交換しています。基本的な操作ではO(log n)です。

javadocsからTreeSet

この実装は、基本的な操作に保証されたlog(n)時間コストを提供します(addremoveおよびcontains)の。


22

1.HashSetはnullオブジェクトを許可します。

2.TreeSetはnullオブジェクトを許可しません。null値を追加しようとすると、NullPointerExceptionがスローされます。

3.HashSetはTreeSetよりもはるかに高速です。

例えば

 TreeSet<String> ts = new TreeSet<String>();
 ts.add(null); // throws NullPointerException

 HashSet<String> hs = new HashSet<String>();
 hs.add(null); // runs fine

3
ts.add(null)TreeSetの最初のオブジェクトとしてnullが追加された場合、TreeSetの場合は正常に機能します。そして、その後追加されたオブジェクトは、コンパレータのcompareToメソッドでNullPointerExceptionを与えます。
Shoaib Chikate、2015年

2
あなたは本当にnullどちらにしてもあなたのセットに本当に追加すべきではありません。
ふわふわ

TreeSet<String> badassTreeSet = new TreeSet<String>(new Comparator<String>() { public int compare(String string1, String string2) { if (string1 == null) { return (string2 == null) ? 0 : -1; } else if (string2 == null) { return 1; } else { return string1.compareTo(string2); } } }); badassTreeSet.add("tree"); badassTreeSet.add("asdf"); badassTreeSet.add(null); badassTreeSet.add(null); badassTreeSet.add("set"); badassTreeSet.add("tree"); System.out.println(badassTreeSet);
デヴィッド・Horvathの

21

@shevchykによるマップでの素敵な視覚的回答に基づくここに私の見解があります:

╔══════════════╦═════════════════════╦═══════════════════╦═════════════════════╗
   Property          HashSet             TreeSet           LinkedHashSet   
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                no guarantee order  sorted according                       
   Order       will remain constant to the natural        insertion-order  
                    over time          ordering                            
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
 Add/remove           O(1)              O(log(n))             O(1)         
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                                      NavigableSet                         
  Interfaces           Set                Set                  Set         
                                       SortedSet                           
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                                       not allowed                         
  Null values        allowed        1st element only        allowed        
                                        in Java 7                          
╠══════════════╬═════════════════════╩═══════════════════╩═════════════════════╣
                 Fail-fast behavior of an iterator cannot be guaranteed      
   Fail-fast   impossible to make any hard guarantees in the presence of     
   behavior              unsynchronized concurrent modification              
╠══════════════╬═══════════════════════════════════════════════════════════════╣
      Is                                                                     
 synchronized               implementation is not synchronized               
╚══════════════╩═══════════════════════════════════════════════════════════════╝

13

ほとんどの理由HashSetは、操作がO(log n)ではなく(平均して)O(1)であるためです。セットに標準アイテムが含まれている場合、「ハッシュ関数をいじる」ことはありません。セットにカスタムクラスが含まれている場合は、hashCode使用するために実装する必要がありますHashSet(ただし、Effective Javaで方法が示されています)。ただし、使用するTreeSet場合は、作成するComparableか、を指定する必要がありますComparator。クラスに特定の順序がない場合、これは問題になる可能性があります。

非常に小さなセット/マップ(<10アイテム)を使用するTreeSet(または実際にTreeMap)こともありますが、実際に利益があるかどうかは確認していません。大きなセットの場合、その差はかなり大きくなる可能性があります。

ソートが必要な場合TreeSetは適切ですが、更新が頻繁でソート結果の必要性が低い場合でも、コンテンツをリストまたは配列にコピーしてソートする方が高速な場合があります。


10K以上などのこれらの大きな要素のデータポイント
クハジェヤン2015年

11

頻繁な再ハッシュ(またはHashSetがサイズ変更できない場合は衝突)を引き起こすのに十分な要素を挿入していない場合、HashSetは常に一定の時間でアクセスできるという利点があります。しかし、多くの成長または縮小があるセットでは、実装によっては、実際にツリーセットを使用するとパフォーマンスが向上する場合があります。

メモリが私に役立つ場合、償却時間は機能的な赤黒木でO(1)に近くなる可能性があります。岡崎の本は私が引き出すよりも良い説明があるでしょう。(または彼の出版物リストを参照してください)


7

もちろん、HashSetの実装ははるかに高速です-順序付けがないため、オーバーヘッドは少なくなります。JavaのさまざまなSet実装の適切な分析は、http://java.sun.com/docs/books/tutorial/collections/implementations/set.htmlで提供されています。ます。

そこでの議論は、ツリー対ハッシュの質問に対する興味深い「中間的な」アプローチも指摘しています。JavaはLinkedHashSetを提供します。これは、「挿入指向」のリンクリストが実行されているHashSetです。つまり、リンクリストの最後の要素も最後にハッシュに挿入されます。これにより、ツリーセットのコストの増加を招くことなく、順序付けられていないハッシュの無秩序さを回避できます。


4

TreeSetのは、 2つのソートコレクション(他のビーイングのTreeMap)の一つです。これは赤黒ツリー構造を使用します(ただし、ご存知のとおり)。要素が自然順序に従って昇順であることを保証します。必要に応じて、ComparableまたはComparatorを使用して、コレクションが(要素のクラスで定義された順序に依存するのではなく)順序をどうするかについての独自のルールをコレクションに与えることができるコンストラクターでTreeSetを構築できます。

そしてA LinkedHashSetのは、すべての要素間の二重リンクリストを維持HashSetの順序付けられたバージョンです。反復順序を気にする場合は、HashSetの代わりにこのクラスを使用してください。HashSetを反復するときの順序は予測できませんが、LinkedHashSetを使用すると、要素が挿入された順序で要素を反復できます。


3

特にパフォーマンスに関する技術的な考慮事項に基づいて、多くの答えが出されました。私によると、間の選択TreeSetHashSet問題。

しかし、私はむしろ選択は最初に概念的な考慮事項によって駆動されるべきであると言いたいです。

操作する必要があるオブジェクトについて、自然な順序付けが意味をなさない場合は、を使用しないでくださいTreeSet
を実装してSortedSetいるため、ソートされたセットです。つまりcompareTo、関数をオーバーライドする必要があるということですequals。これは、関数を返すものと一致している必要があります。たとえば、Studentというクラスのオブジェクトのセットがある場合、私はTreeSet、生徒間で自然な順序付けがないため、 returnが意味をなす。あなたはそれらを平均グレードで注文することができますが、これは「自然な注文」ではありません。関数compareTo、2つのオブジェクトが同じ生徒を表す場合だけでなく、2つの異なる生徒が同じ成績を持っている場合も、 0が返さ。2番目のケースでは、equalsはfalseを返します(2人の異なる学生が同じ成績を持っている場合に後者をtrueにすると、equals機能が誤解を招くような意味になり、間違った意味を持たない場合を除きます)。との
間のこの一貫性はオプションですが、強く注意してくださいお勧めします。そうしないと、インターフェイスの規約が破られて、コードが他の人に誤解を与え、予期しない動作を引き起こす可能性もあります。equalscompareToSet

このリンクは、この質問に関する良い情報源になるかもしれません。


3

オレンジが作れるのに、なぜリンゴが作られるの?

真剣に考えている人やギャル-コレクションが膨大で、何億回も読み書きされ、CPUサイクルにお金を払っている場合、パフォーマンスの向上が必要な場合にのみ、コレクションの選択が関係します。ただし、ほとんどの場合、これは実際には問題になりません-数ミリ秒が人間の言葉で気づかれることはありません。それが本当に重要なのであれば、なぜアセンブラまたはCでコードを作成しないのですか?[別の議論の手がかり]。つまり、重要なのは、選択した任意のコレクションを使用して満足している場合であり、それが問題を解決します(たとえそれがタスクに特に最適なタイプのコレクションでなくても)、自分をノックアウトします。ソフトウェアは順応性があります。必要に応じてコードを最適化します。ボブおじさんは、早期最適化はすべての悪の根源だと言います。ボブおじさんはそう言っています


1

メッセージ編集(完全書き換え)順序を問わない場合はその時。どちらもLog(n)を与えるはずです-どちらかが他方よりも5%以上速いかどうかを確認することは実用的です。HashSetは、O(1)テストをループで実行して、そうであるかどうかを明らかにする必要があります。


-3
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class HashTreeSetCompare {

    //It is generally faster to add elements to the HashSet and then
    //convert the collection to a TreeSet for a duplicate-free sorted
    //Traversal.

    //really? 
    O(Hash + tree set) > O(tree set) ??
    Really???? Why?



    public static void main(String args[]) {

        int size = 80000;
        useHashThenTreeSet(size);
        useTreeSetOnly(size);

    }

    private static void useTreeSetOnly(int size) {

        System.out.println("useTreeSetOnly: ");
        long start = System.currentTimeMillis();
        Set<String> sortedSet = new TreeSet<String>();

        for (int i = 0; i < size; i++) {
            sortedSet.add(i + "");
        }

        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useTreeSetOnly: " + (end - start));
    }

    private static void useHashThenTreeSet(int size) {

        System.out.println("useHashThenTreeSet: ");
        long start = System.currentTimeMillis();
        Set<String> set = new HashSet<String>();

        for (int i = 0; i < size; i++) {
            set.add(i + "");
        }

        Set<String> sortedSet = new TreeSet<String>(set);
        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useHashThenTreeSet: " + (end - start));
    }
}

1
投稿によると、通常、要素をHashSetに追加してから、コレクションをTreeSetに変換して、重複のないソート済みトラバーサルを行う方が高速です。Set <String> s = new TreeSet <String>(hashSet); ソートされた反復に使用されることがわかっている場合、Set <String> s = new TreeSet <String>()を直接使用しないのはなぜでしょうか。
gli00001 2012

「どの場合に、TreeSetよりもHashSetを使用しますか?」
オースティンヘンリー

1
私の要点は、順序付けが必要な場合は、すべてをHashSetに入れて、そのHashSetに基づいてTreeSetを作成するよりも、TreeSetを単独で使用するほうがよいということです。元の投稿からHashSet + TreeSetの値がまったくわかりません。
gli00001

@ gli00001:ポイントを逃しました。要素のセットを常にソートする必要はないが、頻繁に操作する場合は、ハッシュセットを使用して、ほとんどの場合より高速な操作のメリットを享受することは価値があります。のために時折あなたは順序で要素を処理する必要がある回、そしてちょうどTreeSetので包みます。それはあなたのユースケースに依存しますが、それはそれほど一般的なユースケースではありません(そしてそれはおそらく、あまりにも多くの要素を含まず、複雑な順序ルールを持つセットを想定しています)。
ヘイレム2012年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.