web-dev-qa-db-fra.com

quelle est la différence entre set et unordered_set en C++?

Vous êtes tombé sur cette bonne question, qui est similaire mais pas du tout puisqu'elle parle de Java, qui a une implémentation différente des tables de hachage, du fait que les accesseurs/mutateurs sont synchronisés Différences entre HashMap et Hashtable?

Alors, quelle est la différence dans l'implémentation C++ de set et de unordered_set? Cette question peut évidemment être étendue à map vs unordered_map et ainsi de suite pour d’autres conteneurs C++.

Voici mon évaluation initiale

set: Bien que standard ne lui demande pas explicitement d'être implémenté sous forme d'arborescence, la contrainte de complexité temporelle demandée pour ses opérations sur find/insert signifie qu'elle sera toujours implémentée sous forme d'arborescence . Généralement sous forme d'arborescence RB (comme voir dans GCC 4.8), qui est équilibré en hauteur . Comme ils sont équilibrés en hauteur, ils ont une complexité temporelle prévisible pour find ()

Avantages: compact (comparé à d'autres DS en comparaison)

Contre: la complexité du temps d'accès est O (lg n)

unordered_set: Tandis que le standard ne lui demande pas explicitement d'être implémenté sous forme d'arborescence, la contrainte de complexité temporelle demandée pour ses opérations sur find/insert, signifie qu'elle sera toujours implémentée sous forme de table de hachage.

Avantages :

  1. Plus rapide (promesses amorties O(1) pour la recherche)
  2. Facile à convertir les primitives de base en thread-safe, par rapport à tree-DS

Les inconvénients :

  1. Il n’est pas garanti que la recherche soit O(1). Le cas le plus défavorable pour Therotical est O (n).
  2. Pas aussi compact que l'arbre. (pour des raisons pratiques, le facteur de charge n'est jamais 1)

Note: Le O (1), pour hashtable vient de l’hypothèse qu’il n’ya pas de collision. Même avec un facteur de charge de 0,5, une insertion de variable sur deux entraîne une collision ... Il est à noter que le facteur de charge de la table de hachage est inversement proportionnel au nombre d'opérations nécessaires pour accéder à un élément de celle-ci. Plus nous réduisons le nombre d'opérations, la table de hachage moins dense. Lorsque la taille de l’élément stocké est comparable à celle du pointeur, la surcharge est très importante.

Edit: Puisque la plupart des gens disent que la question contient une réponse suffisante, je change la question en "Ai-je oublié une différence entre map/set pour l'analyse des performances qu'il faut savoir ??"

51
Ajeet Ganga

Je pense que vous avez généralement répondu à votre propre question, cependant, ceci:

Pas aussi compact que l'arbre. (pour des raisons pratiques, le facteur de charge n'est jamais 1)

n'est pas nécessairement vrai. Chaque nœud d'un arbre (nous supposerons qu'il s'agit d'un arbre rouge-noir) pour un type T utilise un espace égal à au moins 2 * pointer_size + sizeof(T) + sizeof(bool). Cela peut être 3 * pointer size selon que l’arbre contient ou non un pointeur parent pour chaque nœud de l’arbre.

Comparez ceci à une carte de hachage: il y aura un espace de tableau gaspillé pour chaque carte de hachage en raison du fait que load factor < 1, comme vous l'avez dit. Cependant, en supposant que la carte de hachage utilise des listes liées individuellement pour le chaînage (et en réalité, il n'y a aucune raison de ne pas le faire), chaque élément inséré ne prend que sizeof(T) + pointer size

Notez que cette analyse ignore toute surcharge pouvant provenir de l'espace supplémentaire utilisé par l'alignement.

Pour tout élément T qui a une petite taille (donc, tout type de base), la taille des pointeurs et des autres frais généraux est dominante. Avec un facteur de charge de > 0.5 (par exemple), le std::unordered_set peut en effet utiliser moins de mémoire que son équivalent std::set.

L'autre grand point manquant est le fait qu'itérer à travers un std::set est garanti pour produire un ordre du plus petit au plus grand, en fonction de la fonction de comparaison donnée, tandis qu'itérer à travers un std::unordered_set retournera les valeurs dans un ordre "aléatoire". 

26
Yuushi

Une autre différence (bien que non liée aux performances) réside dans le fait que l'insertion set n'invalide pas les itérateurs, tandis que l'insertion unordered_set peut le faire si elle déclenche une modification. En pratique, c'est une préoccupation assez mineure, car les références aux éléments actuels restent valables.

11
dhaffey

Dans certains cas, set est plus pratique.

Par exemple, en utilisant vector comme clé:

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 

La raison pour laquelle vector<int> peut être dans set parce que vector a priorité sur operator<.

Mais si vous utilisez unordered_set<vector<int>>, vous devez créer une fonction de hachage pour vector<int>, car vector n’a pas de fonction de hachage. Vous devez donc en définir une comme:

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
}

vous pouvez voir que dans certains cas, unordered_set est plus compliqué.

Principalement cité dans: https://stackoverflow.com/a/29855973/6329006

Plus de différence entre unordered_set et set, voyez ceci: https://stackoverflow.com/a/52203931/6329006

0
Jayhello