Son commentaire:
[...] Inutile de se demander pourquoi il n'y a pas de déclaration if ici. Il est important de regarder le problème sous un angle différent et de le réécrire afin que le cas spécial disparaisse et devienne un cas normal, et c'est un bon code . - L. Torvalds
Linus montre un pseudocode de style C assez simple à titre d'exemple. Mais ne fournit pas d'explication conceptuelle. Par conséquent, il n'est pas immédiatement clair comment fonctionne un pointeur indirect.
Regardons de plus près cette solution et ses avantages. En prime, non seulement la suppression est représentée, mais également l'insertion d'un élément via un adressage indirect.
Le code
La structure de données de base pour une liste d'entiers liés à un seul lien est illustrée à la Fig. 1.
Fig. 1. Une seule liste d'entiers liés
Les nombres sont des valeurs entières sélectionnées aléatoirement, et les flèches correspondent à des pointeurs:
head
- c'est un pointeur de type
IntListItem*
, tous les blocs sont des instances d'une structure
IntListItem
, chacun avec sa propre variable (
next
dans le code) du type
IntListItem*
, qui pointe vers l'élément suivant.
Implémentation de la structure de données en C:
struct IntListItem {
int value;
struct IntListItem* next;
};
typedef struct IntListItem IntListItem;
struct IntList {
IntListItem* head;
};
typedef struct IntList IntList;
Aussi l'API (minimale):
/* The textbook version */
void remove_cs101(IntList* l, IntListItem* target);
/* A more elegant solution */
void remove_elegant(IntList* l, IntListItem* target);
Regardons maintenant les implémentations
remove_cs101()
et
remove_elegant()
. Le code des exemples ne contredit pas le pseudocode de l'exemple de Linus, mais il se compile et s'exécute.
Version de base
Figure: 2. Modèle conceptuel de la structure des données de liste dans l'algorithme de base
void remove_cs101(IntList *l, IntListItem *target)
{
IntListItem *cur = l->head, *prev = NULL;
while (cur != target) {
prev = cur;
cur = cur->next;
}
if (prev) {
prev->next = cur->next;
} else {
l->head = cur->next;
}
}
Dans l'approche standard, deux pointeurs de parcours
cur
et
prev
, qui marquent respectivement la position de parcours actuelle et précédente dans la liste.
cur
commence en tête de liste
head
et avance jusqu'à ce que la cible soit trouvée. À son tour, il
prev
commence par
NULL
et est ensuite mis à jour à la valeur précédente
cur
chaque fois qu'il avance. Lorsque la cible est trouvée, l'algorithme vérifie si elle n'est pas
prev
nulle. Si tel est le cas, il
cur
pointe vers le premier élément de la liste, auquel cas la suppression signifie faire avancer la tête de la liste.
Une solution plus élégante
La version la plus élégante a moins de code et ne nécessite pas de branche séparée pour supprimer le premier élément de la liste.
void remove_elegant(IntList *l, IntListItem *target)
{
IntListItem **p = &l->head;
while ((*p) != target) {
p = &(*p)->next;
}
*p = target->next;
}
Le code utilise un pointeur indirect
p
qui contient l'adresse du pointeur vers l'élément de liste, en commençant à l'adresse
head
. À chaque itération, ce pointeur est développé pour inclure l'adresse du pointeur vers l'élément suivant dans la liste, c'est-à-dire l'adresse de l'élément
next
dans l' élément actuel
IntListItem
. Lorsque le pointeur vers l'élément de liste
(*p)
est égal
target
, nous quittons la boucle de recherche et supprimons l'élément de la liste.
Comment ça fonctionne?
Un pointeur indirect présente
p
deux avantages conceptuels:
- Vous permet d'interpréter une liste chaînée de manière à ce que le pointeur
head
fasse partie intégrante de la structure de données. Cela élimine le besoin d'un cas spécial pour supprimer le premier élément.
- Il vous permet également d'évaluer l'état de la boucle
while
sans avoir à relâcher le pointeur pointant verstarget
. Cela vous permet de changer le pointeurtarget
et de vous déplacer avec un seul itérateur, contrairement àprev
etcur
.
Jetons un coup d'œil à chaque élément à son tour.
Intégration du pointeur de tête
Le modèle standard interprète une liste chaînée comme une séquence d'instances
IntListItem
. Le début de la séquence peut être obtenu à l'aide d'un pointeur
head
. Cela conduit au modèle conceptuel illustré à la Fig. 2. Le pointeur est
head
simplement traité comme une poignée pour accéder au début de la liste.
prev
et
cur
sont des pointeurs de type
IntListItem*
et pointent toujours vers ou
NULL
.
L'implémentation élégante utilise un schéma d'adressage indirect, qui donne une vue différente de la structure des données:
Fig. 3. Modèle conceptuel de la structure de données de liste dans une solution plus élégante Représente
ici
p
le type
IntListItem**
et contient l'adresse du pointeur vers l'élément de liste actuel. Lorsque le pointeur avance, son adresse passe à l'élément suivant.
Dans le code, cela ressemble à
p = &(*p)->next
:
(*p)
: déréférencer l'adresse du pointeur vers l'élément de liste actuel.
->next
: déréférencer à nouveau ce pointeur et sélectionner le champ avec l'adresse de l'élément suivant.
&
: prenez l'adresse de ce champ (c'est-à-dire, obtenez un pointeur vers celui-ci).
Cela correspond à l'interprétation d'une structure de données, où une liste est une séquence de pointeurs vers des éléments
IntListItem
(Figure 3).
La touche finale
Un avantage supplémentaire de cette interprétation particulière est qu'elle permet au pointeur
next
d' être édité pour le prédécesseur de l'élément courant tout au long du parcours .
Si
p
contient l'adresse d'un pointeur vers un élément de la liste, la comparaison dans la boucle de recherche devient:
while ((*p) != target) {
...
}
Le cycle de recherche se terminera s'il
(*p)
est égal
target
, et dès que cela se produit, nous pouvons encore changer
(*p)
, puisque nous tenons son adresse
p
. Ainsi, même si la boucle est itérée jusqu'à la fin, un descripteur (adresse de champ
next
ou pointeur
head
) est stocké qui peut être utilisé pour changer directement le pointeur vers un élément.
C'est pourquoi nous pouvons modifier l'entrée du pointeur d'élément pour pointer vers un emplacement différent, en utilisant
*p = target->next
, et par conséquent, nous n'avons pas besoin de contourner les pointeurs
prev
et
cur
de supprimer l'élément.
Nouvelle application
Il s'avère que la même idée peut être appliquée à une implémentation extrêmement laconique d'une autre fonction dans des listes chaînées:,
insert_before()
c'est-à-dire insérer cet élément avant un autre.
Insertion avant l'élément existant
Tout d'abord, ajoutons la déclaration suivante à
list.h
:
void insert_before(IntList *l, IntListItem *before, IntListItem *item);
La fonction prendra un pointeur vers la liste l, un pointeur avant un élément de cette liste et un pointeur vers un nouvel élément de liste que la fonction insérera avant lui.
Refactoring rapide
Avant de continuer, faisons de la boucle de recherche une fonction distincte:
static inline IntListItem **find_indirect(IntList *l, IntListItem *target)
{
IntListItem **p = &l->head;
while ((*p) && (*p) != target) {
p = &(*p)->next;
}
return p;
}
et utilisez-le dans
remove_elegant()
:
void remove_elegant(IntList *l, IntListItem *target)
{
IntListItem **p = find_indirect(l, target);
*p = target->next;
}
Implémentation Insert_before ()
Son utilisation
find_indirect()
est facile à mettre en œuvre
insert_before()
:
void insert_before(IntList *l, IntListItem *before, IntListItem *item)
{
IntListItem **p = find_indirect(l, before);
*p = item;
item->next = before;
}
La sémantique solide pour les cas de bord est particulièrement agréable: si elle
before
pointe vers la tête de la liste, un nouvel élément sera inséré au début, s'il
before
est nul ou invalide (c'est-à-dire qu'il n'existe pas dans
l
), un nouvel élément sera ajouté à la fin.
Conclusion
La condition préalable à une solution plus élégante pour supprimer des éléments est un changement simple: un pointeur indirect
IntListItem**
pour itérer sur des pointeurs vers des éléments de liste. Tout le reste découle de là: il n'y a pas besoin de cas particuliers ou de branches. Un itérateur suffit pour trouver et supprimer l'élément cible. Et il s'avère que la même approche fournit une solution élégante pour l'insertion en général et l'insertion devant un élément existant en particulier.
Donc, pour revenir à la remarque initiale de Linus, est-ce un indicateur de bon goût? Dur à dire. Mais il existe clairement une solution créative et très élégante à un problème bien connu.