Sur la page wikipedia Sentinel Node , il est indiqué que les avantages d'un nœud sentinelle par rapport à NULL sont les suivants:
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:
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.
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 ...
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.
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;
}