TL;DR
- Le Problème: Optimisation de la complexité temporelle et spatiale des structures de données.
- L'Approche: Probleme style CTCI 2.3: supprimer un noeud du milieu d'une liste chainee simple quand on n'a qu'un pointeur vers ce noeud. Copie la valeur suivante, saute le suivant, et explique pourquoi le dernier noeud echoue.
- Complexité: Compromis optimal entre temps et mémoire avec gestion des cas limites.
Tu es dans une conga. Quelqu'un te tape sur l'épaule et dit: sors de la file. Tu ne peux pas atteindre la personne derrière toi, donc tu ne peux pas lui demander de te sauter. Le seul coup qui marche est bizarre: tu deviens la personne devant toi. Tu copies son costume et son badge, tu la fais sortir de la file, puis tu refermes le trou. Le reste de la chaine a l'air intact. C'est delete middle node sur une liste chainee simple.
Ce billet est un enseignement original pour debutants en Java. Meme famille de problemes que les classiques de listes en entretien, pas une copie de livre. Fait partie de la serie CTCI en Java.
1. Analogie du quotidien
Une liste chainee simple est une conga a sens unique. Chaque personne ne connait que la suivante. On ne te donne pas la tete de la file. On te donne seulement un pointeur vers quelqu'un au milieu, et la tache est de le retirer.
Le unlink normal a besoin du noeud precedent:
prev.next = node.next
Ici tu n'as pas prev. Donc tu triches:
- Tu voles l'identite de la personne suivante (copie
next.datadans le noeud courant). - Tu sautes la personne suivante (
current.next = next.next).
Le "slot" du milieu existe encore comme objet, mais il porte maintenant la valeur suivante et pointe ou pointait le suivant. De l'exterieur, cette valeur a disparu de la sequence.
2. Enonce en mots simples
Entree: une reference Node vers un noeud qui n'est ni le premier ni le dernier d'une liste chainee simple. Tu ne recois pas la tete.
Sortie: muter la liste pour que la valeur qui etait en node n'apparaisse plus dans la sequence. La liste doit ressembler a une suppression de ce noeud du milieu.
Forme du noeud:
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
Exemple:
| Avant | Supprimer celui-ci | Apres | Pourquoi |
|---|---|---|---|
a → b → c → d → e |
noeud avec c |
a → b → d → e |
c devient d, puis on saute l'ancien d |
1 → 2 → 3 → 4 |
noeud avec 2 |
1 → 3 → 4 |
on copie 3 dans le slot de 2, on saute l'ancien 3 |
1 → 2 → 3 → 4 |
noeud avec 3 |
1 → 2 → 4 |
la meme idee un cran plus loin |
Clarifie avant de coder (dis-le a voix haute):
- Le noeud est-il garanti non dernier? (Enonce classique: oui, ou "n'importe quel noeud sauf le dernier".)
- Est-il garanti non tete? (Souvent oui; supprimer la tete demande un autre contrat.)
- Peut-on ecraser
data? (Oui. C'est tout le truc.) - Simple ou double chainage? (Ici: simple.)
- Liste a un seul noeud? (Hors perimetre; pas de suivant a copier.)
Pour cet article: noeud du milieu avec next non null, entiers, mutation en place, retour succes ou void.
3. Reflechir d'abord
Ce que tu ne peux pas faire
- Partir de la tete pour trouver
prev. Tu n'as pas la tete. - Faire
node = node.next. Cela ne rebind qu'une variable locale. Lenextdu precedent pointe encore vers l'ancien objet. - Liberer le noeud sans rebrancher. La chaine l'inclut toujours.
Le seul truc pratique
Si node.next existe:
node.data = node.next.data
node.next = node.next.next
Tu supprimes physiquement le noeud suivant apres avoir copie son payload dans le courant. En effet, la valeur qui vivait en node a disparu. Les valeurs plus loin se decalent d'un cran logique vers la gauche.
Pourquoi le dernier noeud echoue
Si node.next == null, il n'y a ni identite a voler ni noeud a sauter. Tu ne peux pas retirer la derniere valeur sans le pointeur precedent (ou un design sentinel). En entretien, dis-le clairement: cet algorithme ne supprime pas un vrai dernier noeud.
Certains interviewers acceptent "marquer dummy / lever / renvoyer false". Choisis un contrat clair et tiens-le.
4. Solution Java
/**
* Deletes a middle node from a singly linked list given only that node.
* Copies the next node's data into this node, then skips the next node.
* Does not work for the last node (no next to copy from).
*
* @return true if deleted, false if node is null or is the last node
*/
boolean deleteMiddleNode(Node node) {
if (node == null || node.next == null) {
// Cannot delete last node (or a null reference) this way.
return false;
}
Node next = node.next;
node.data = next.data;
node.next = next.next;
return true;
}
Deroulement pour a → b → c → d → e, supprimer le noeud avec c:
| Etape | node.data |
node.next pointe vers |
Liste vue depuis la tete |
|---|---|---|---|
| Debut | c |
d |
a → b → c → d → e |
| Copier data | d |
d (meme objet) |
a → b → d → d → e (deux noeuds avec d un instant) |
| Sauter next | d |
e |
a → b → d → e |
L'ancien noeud d est delie et eligible au GC. Qui tenait encore un pointeur vers l'objet qui etait c voit maintenant d dans cet objet. C'est le tradeoff habituel: l'identite de l'objet n'est pas celle de la valeur dans la sequence.
Petit driver pour tester mentalement:
Node build(int... vals) {
Node dummy = new Node(0);
Node t = dummy;
for (int v : vals) {
t.next = new Node(v);
t = t.next;
}
return dummy.next;
}
// head: 1 → 2 → 3 → 4 → 5
// delete the node with value 3 (must look it up only for the demo)
Node head = build(1, 2, 3, 4, 5);
Node target = head.next.next; // the 3
deleteMiddleNode(target);
// list is now 1 → 2 → 4 → 5
Dans l'appel reel du probleme, l'interviewer te donne target directement. Tu ne cherches jamais depuis la tete.
5. Tableau de complexite
| Approche | Temps | Espace extra | Notes |
|---|---|---|---|
| Copier next + sauter | O(1) | O(1) | Seulement des pointeurs en temps constant |
| Marcher depuis la tete jusqu'a prev | O(N) | O(1) | Besoin de la tete; interdit par l'enonce |
| Copier toute la liste sans cette valeur | O(N) | O(N) | Trop lourd, et il faut encore la tete |
C'est l'un des rares problemes de listes vraiment en O(1) quand les contraintes tiennent.
6. Cas limites et erreurs frequentes
Les interviewers poussent ici:
- Dernier noeud → renvoyer false, lever, ou documenter "non supporte". Pas de NPE sur
node.next.data. - Noeud null → proteger d'abord.
- Liste a deux noeuds, supprimer le premier des deux → ca marche: le premier devient le second, puis on saute le second. La liste devient un seul noeud. Si "premier de deux" compte comme milieu depend du wording; l'algo tourne quand meme.
- Tete avec longueur > 2 → l'algo "marche" techniquement (tu ecrases la data de la tete et tu sautes l'ancien second). Beaucoup d'enonces disent quand meme "pas le premier ni le dernier". Suis la contrainte donnee.
- Valeurs en double → ok. Tu retires une occurrence a cette position, pas "tous les egaux".
- References externes vers l'ancien noeud de la valeur → elles pointent maintenant vers l'objet qui porte la valeur suivante. Dis-le si la liste est partagee.
Erreurs frequentes:
- Seulement
node = node.next. Rebind local ne delie rien. - Oublier de copier data. Si tu sautes seulement next, tu gardes la valeur du milieu et tu perds la suivante. C'est l'inverse de supprimer le milieu.
- Croire pouvoir liberer le dernier noeud. Impossible avec ce seul pointeur sur une liste simple.
- Renvoyer void et ignorer l'echec. Prefere un boolean ou une exception claire pour le cas dernier noeud.
- Penser que l'objet noeud disparait. L'objet en
nodereste; son payload change. La "suppression" est logique pour la sequence, pas toujours physique pour cet objet Java.
7. Recap a raconter a un ami
Delete middle node demande: retire une valeur d'une liste chainee simple quand tu ne tiens que ce noeud, pas la tete.
- Tu ne peux pas rebrancher le pointeur precedent. Tu ne l'as pas.
- Copie la data du noeud suivant dans le courant.
- Fais pointer le courant au-dela du suivant.
- Le dernier noeud n'a pas de next, donc le truc echoue. Dis-le d'emblee.
Si tu ecris le corps en trois lignes et expliques la limite du dernier noeud en trente secondes, tu maitrises le 2.3.
Serie
- Guide: Guide de la serie CTCI
- Precedent: Return Kth to Last
- Suivant: Partition
