web-dev-qa-db-fra.com

Comment un nœud sentinelle offre-t-il des avantages par rapport à NULL?

Sur la page wikipedia Sentinel Node , il est indiqué que les avantages d'un nœud sentinelle par rapport à NULL sont les suivants:

  • Augmentation de la vitesse des opérations
  • Taille de code algorithmique réduite
  • Augmentation de la robustesse de la structure de données (sans doute).

Je ne comprends pas vraiment comment les contrôles sur un nœud sentinelle seraient plus rapides (ni comment les implémenter correctement dans une liste chaînée ou une arborescence), alors je suppose que la question est plus double:

  1. Qu'est-ce qui fait que le nœud sentinelle a une meilleure conception que NULL?
  2. Comment implémenteriez-vous un nœud sentinelle dans (par exemple) une liste?
42
spbots

Les sentinelles ne présentent aucun avantage si vous ne faites que de simples itérations et que vous ne regardez pas les données contenues dans les éléments.

Cependant, son utilisation pour des algorithmes de type "trouver" présente un réel avantage. Par exemple, imaginez une liste de liste liée std::list dans laquelle vous souhaitez rechercher une valeur spécifique x.

Ce que vous feriez sans sentinelles, c'est:

for (iterator i=list.begin(); i!=list.end(); ++i) // first branch here
{
  if (*i == x) // second branch here
    return i;
}
return list.end();

Mais avec les sentinelles (bien entendu, la fin doit en réalité être un véritable nœud pour cela ...):

iterator i=list.begin();
*list.end() = x;

while (*i != x) // just this branch!
  ++i;

return i;

Vous voyez que la branche supplémentaire n'a pas besoin de tester la fin de la liste - la valeur est toujours garantie; vous retournerez automatiquement end() si x ne peut pas être trouvé dans vos éléments "valides".

Pour une autre application intéressante et réellement utile des sentinelles, voir "intro-sort", qui est l'algorithme de tri utilisé dans la plupart des implémentations de std::sort. Il a une variante intéressante de l'algorithme de partition qui utilise des sentinelles pour supprimer quelques branches.

25
ltjax

Je pense qu'un petit exemple de code serait une meilleure explication qu'une discussion théorique.

Vous trouverez ci-dessous le code de suppression de nœud dans une liste de nœuds doublement liés, où NULL est utilisé pour marquer la fin de la liste et où deux pointeurs first et last sont utilisés pour contenir l'adresse du premier et du dernier nœud:

// Using NULL and pointers for first and last
if (n->prev) n->prev->next = n->next;
        else first = n->next;
if (n->next) n->next->prev = n->prev;
        else last = n->prev;

et c'est le même code où à la place il y a un noeud factice spécial pour marquer la fin de la liste et où l'adresse du premier noeud de la liste est stockée dans le champ next du noeud spécial et où le dernier noeud de la liste est stocké dans le champ prev du noeud factice spécial:

// Using the dummy node
n->prev->next = n->next;
n->next->prev = n->prev;

Le même type de simplification est également présent pour l'insertion de nœud; Par exemple, pour insérer le noeud n avant le noeud x (ayant x == NULL ou x == &dummy, ce qui signifie insertion en dernière position), le code serait:

// Using NULL and pointers for first and last
n->next = x;
n->prev = x ? x->prev : last;
if (n->prev) n->prev->next = n;
        else first = n;
if (n->next) n->next->prev = n;
        else last = n;

et

// Using the dummy node
n->next = x;
n->prev = x->prev;
n->next->prev = n;
n->prev->next = n;

Comme vous pouvez le voir, l'approche des nœuds factices a été supprimée pour une liste à double liaison, tous les cas spéciaux et toutes les conditions.

L'image suivante représente les deux approches pour la même liste en mémoire ...

NULL/dummy node alternatives for a doubly-linked list

58
6502

La réponse à votre question (1) se trouve dans la dernière phrase de l'entrée Wikipedia liée: "En tant que nœuds qui seraient normalement liés à NULL sont maintenant liés à" nil "(y compris nil lui-même), il n'est plus nécessaire de créer une branche coûteuse. opération pour vérifier la valeur NULL. "

Normalement, vous devez tester un nœud pour la valeur NULL avant d'y accéder. Si vous avez plutôt un nœud nil valide, vous n'avez pas besoin de faire ce premier test, en enregistrant une comparaison et une branche conditionnelle, ce qui peut coûter cher sur les CPU superscalaires modernes lorsque la branche est mal prédite.

8
Paul R

Je vais essayer de répondre dans le contexte de la bibliothèque de modèles standard:

1) Dans un appel à "next ()", NULL n'indique pas nécessairement la fin de la liste. Et si une erreur de mémoire se produisait? Le renvoi d'un nœud sentinelle est un moyen définitif d'indiquer qu'une fin de liste s'est produite et non un autre résultat. En d'autres termes, NULL pourrait indiquer une variété de choses, pas seulement la fin de liste.

2) Ce n'est qu'une méthode possible: lorsque vous créez votre liste, créez un nœud privé qui n'est pas partagé en dehors de la classe (appelé "lastNode" par exemple). Après avoir détecté que vous avez itéré à la fin de la liste, "next ()" renvoie une référence à "lastNode". De plus, une méthode appelée "end ()" renvoie une référence à "lastNode". Enfin, selon la manière dont vous implémentez votre classe, vous devrez peut-être remplacer l'opérateur de comparaison pour que cela fonctionne correctement.

Exemple:

class MyNode{

};

class MyList{

public:
    MyList () : lastNode();

    MyNode * next(){

       if (isLastNode) return &lastNode;
       else return //whatever comes next
    }

    MyNode * end() {

       return &lastNode;
    }

    //comparison operator
    friend bool operator == (MyNode &n1, MyNode &n2){

        return (&n1 == &n2); //check that both operands point to same memory
    }


private:
    MyNode lastNode;
};


int main(){

  MyList list;
  MyNode * node = list.next();

  while ( node != list.end() ){ 

     //do stuff!

     node = list.next();
  }

  return 0; 
}
0
J T