web-dev-qa-db-ja.com

C ++のsetとunordered_setの違いは何ですか?

この良い質問に出くわしましたが、アクセサ/ミューテータが同期されているため、ハッシュテーブルの実装が異なるJavaについて説明しているため、似ていますがまったく同じではありません HashMapとHashtableの違い?

それでは、setとunordered_setのC++実装の違いは何ですか?この質問はもちろん、他のC++コンテナのmap vs unordered_mapなどに拡張できます。

これが私の最初の評価です

set:標準は明示的にツリーとして実装することを要求していませんが、時間複雑性の制約は検索/挿入の操作を要求しているため、常にツリーとして実装されます。通常、RBツリーとして(GCC 4.8で見られるように)、高さのバランスが取れています。それらは高さのバランスが取れているため、find()の予測可能な時間の複雑さを持ちます。

長所:コンパクト(他のDSと比較して)

Con:アクセス時間の複雑さはO(lg n)です

unordered_set:標準は明示的にツリーとして実装することを要求していませんが、時間複雑性制約は検索/挿入の操作を要求しました。ハッシュテーブルとして実装されます。

長所:

  1. より高速(償却O(1)検索の場合)
  2. Tree-DSと比較して、基本プリミティブをスレッドセーフに簡単に変換できます

短所:

  1. ルックアップは、O(1)であることが保証されていません。理論上の最悪のケースはO(n)です
  2. ツリーほどコンパクトではありません。 (実際の目的では、負荷係数は1になりません)

注:ハッシュテーブルのO(1)は、衝突がないという仮定に基づいています。負荷係数が.5であっても、変数の挿入ごとに衝突が発生します。ハッシュテーブルの負荷係数は、その要素にアクセスするために必要な操作の数に反比例することが観察できます。さらに、#operationsを削減し、ハッシュテーブルを疎にします。格納されている要素のサイズがポインタに匹敵する場合、オーバーヘッドは非常に大きくなります。

編集:ほとんどの質問には十分な答えが含まれていると言われているので、質問を「知っておくべきパフォーマンス分析のマップ/セットの違いを見逃しましたか?」に変更しています。

53
Ajeet Ganga

あなたは一般的にあなた自身の質問に答えたと思いますが、これは:

ツリーほどコンパクトではありません。 (実際の目的では、負荷係数は1になりません)

必ずしも真実ではありません。タイプTのツリーの各ノード(赤黒ツリーと仮定します)は、少なくとも2 * pointer_size + sizeof(T) + sizeof(bool)に等しいスペースを使用します。これは、ツリーに各ツリーノードのparentポインターが含まれているかどうかに応じて、_3 * pointer size_になります。

これをハッシュマップと比較してください。あなたが言ったように_load factor < 1_であるという事実のために、各ハッシュマップに無駄な配列スペースがあります。ただし、ハッシュマップがチェーンに単一リンクリストを使用すると仮定すると(実際、そうしない本当の理由はありません)、挿入される各要素はsizeof(T) + pointer sizeのみを取ります。

この分析では、アライメントに使用される余分なスペースに起因するオーバーヘッドは無視されます。

サイズが小さい(つまり、すべての基本型)要素Tの場合、ポインターと他のオーバーヘッドのサイズが支配的です。 _> 0.5_の負荷係数では(たとえば)_std::unordered_set_は実際に同等の_std::set_よりも少ないメモリを使用する可能性があります。

もう1つの大きな欠落点は、_std::set_を反復処理すると、指定された比較関数に基づいて最小から最大への順序が生成されることが保証され、_std::unordered_set_を反復処理すると、 "順不同。

26
Yuushi

もう1つの違いは(パフォーマンスに関係しませんが)set挿入は反復子を無効にしませんが、unordered_set再ハッシュをトリガーする場合、挿入できます。実際には、実際の要素への参照は引き続き有効であるため、これはかなり小さな懸念事項です。

11
dhaffey

Yuushiは空間効率やその他のポイントに既に十分に取り組んでいます。私がコメントする質問の他のいくつかの部分...

ハッシュテーブルのO(1)は、衝突がないという仮定に基づいています。

それは真実ではない。 O(1)が意味するのは、最初のルックアップ試行が常に成功するということではなく、平均して、必要な試行の数ではなく、値が大きくなります。たとえば、unordered_setまたは..._mapmax_load_factor 構築時のデフォルトは1.0であり、負荷係数が適切なハッシュ関数でそれに近づくと、average任意の要素にハッシュする要素の数テーブル内の値の数に関係なく、1つのバケットは約2になります。

負荷係数が.5であっても、変数の挿入ごとに衝突が発生します。

確かに、それは直感的に予想されるほど悲惨ではありません。1.0の負荷係数で平均チェーン長が2であることは悪くありません。

ハッシュテーブルの負荷係数は、その要素にアクセスするために必要な操作の数に反比例することが観察できます。さらに、#operationsを削減し、ハッシュテーブルを疎にします。

必ず相関関係があります(逆ではありません)。

2
Tony Delroy

場合によっては、setの方が便利です。

たとえば、vectorをキーとして使用する場合:

set<vector<int>> s;
s.insert({1, 2});
s.insert({1, 3});
s.insert({1, 2});

for(const auto& vec:s)
    cout<<vec<<endl;   // I have override << for vector
// 1 2
// 1 3 

setvector<int>をオーバーライドするため、operator<vectorに含めることができる理由。

ただし、unordered_set<vector<int>>を使用する場合、vector<int>のハッシュ関数を作成する必要があります。ベクターにはハッシュ関数がないため、次のように定義する必要があります。

struct VectorHash {
    size_t operator()(const std::vector<int>& v) const {
        std::hash<int> hasher;
        size_t seed = 0;
        for (int i : v) {
            seed ^= hasher(i) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        }
        return seed;
    }
};

vector<vector<int>> two(){
    //unordered_set<vector<int>> s; // error vector<int> doesn't  have hash function
    unordered_set<vector<int>, VectorHash> s;
    s.insert({1, 2});
    s.insert({1, 3});
    s.insert({1, 2});

    for(const auto& vec:s)
        cout<<vec<<endl;
    // 1 2
    // 1 3
}

場合によってはunordered_setがより複雑であることがわかります。

主に引用元: https://stackoverflow.com/a/29855973/6329006

unordered_setsetのその他の違いは、これを参照してください: https://stackoverflow.com/a/52203931/6329006

0
Jayhello