Deux pointeurs : lent et rapide
Appliquez le schéma des pointeurs lent et rapide pour supprimer les doublons en place, déplacer les zéros et partitionner les tableaux autour d’une valeur pivot.
Deux pointeurs : lent et rapide est une leçon DSA Interview Prep gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DSA Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DSA Interview Prep comprend 4 leçons au total.
Explication des pointeurs lent et rapide
Le modèle des pointeurs lent-rapide, également appelé modèle de la tortue et du lièvre, utilise deux pointeurs qui se déplacent à des vitesses différentes dans la même séquence. Contrairement aux pointeurs placés aux extrémités opposées, ils commencent tous les deux au début. Le pointeur lent avance d’une étape à la fois ; le pointeur rapide en avance de deux, voire davantage. Leur différence de vitesse crée des invariants utiles : le pointeur lent suit un « préfixe valide », tandis que le pointeur rapide parcourt les éléments suivants à la recherche de certaines conditions.
# Slow pointer marks the write position;
# Fast pointer scans for next non-duplicate.
def remove_duplicates(nums):
if not nums: return 0
slow = 0 # next position to write a unique value
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1 # new length
nums = [1, 1, 2, 3, 3, 3, 4]
k = remove_duplicates(nums)
print(nums[:k]) # [1, 2, 3, 4]Supprimer les doublons d’un tableau trié
Dans un tableau trié, les doublons sont adjacents. Le pointeur lent suit la dernière valeur unique écrite ; le pointeur rapide parcourt les éléments suivants. Lorsque le pointeur rapide atteint une valeur différente de nums[slow], avancez le pointeur lent et copiez la nouvelle valeur. Cet algorithme en place s’exécute en O(n) avec un espace supplémentaire de O(1) : c’est une question d’entretien classique qui vérifie votre maîtrise du modèle des pointeurs de lecture-écriture.
def remove_duplicates_v2(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
# Allow at most 2 occurrences
def remove_duplicates_k2(nums):
slow = 0
for fast in range(len(nums)):
if slow < 2 or nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
return slow
print(remove_duplicates_k2([1,1,1,2,2,3]))
# Result: 5, nums[:5] = [1,1,2,2,3]Déplacer les zéros avec des pointeurs lent-rapide
Déplacez tous les zéros à la fin tout en préservant l’ordre relatif des éléments non nuls. Le pointeur lent indique la prochaine position destinée à un élément non nul. Le pointeur rapide recherche les valeurs non nulles. Lorsque le pointeur rapide en trouve une, copiez-la à la position du pointeur lent, puis avancez les deux pointeurs. Après le parcours, remplissez de zéros les positions allant du pointeur lent à la fin. Complexité : O(n) en temps et O(1) en espace.
def move_zeroes(nums):
slow = 0 # next position for a non-zero
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
# Fill rest with zeroes
while slow < len(nums):
nums[slow] = 0
slow += 1
nums = [0, 1, 0, 3, 12]
move_zeroes(nums)
print(nums) # [1, 3, 12, 0, 0]Partitionner un tableau autour d’un pivot
L’étape de partition du tri rapide réorganise les éléments en place afin que toutes les valeurs < au pivot précèdent les valeurs >= au pivot. Le schéma de Lomuto utilise un pointeur lent, qui indique la dernière position d’un petit élément, et un pointeur rapide, qui parcourt le tableau vers l’avant. Lorsque le pointeur rapide trouve un petit élément, incrémentez le pointeur lent et échangez les deux éléments. Cette opération s’exécute en O(n) avec un espace supplémentaire de O(1).
def lomuto_partition(nums, low, high):
pivot = nums[high]
slow = low - 1 # last position of small element
for fast in range(low, high):
if nums[fast] <= pivot:
slow += 1
nums[slow], nums[fast] = nums[fast], nums[slow]
# Place pivot in final position
nums[slow+1], nums[high] = nums[high], nums[slow+1]
return slow + 1 # pivot's final index
arr = [3, 1, 4, 1, 5, 9, 2, 6]
p = lomuto_partition(arr, 0, len(arr)-1)
print(arr) # elements before p are <= pivotTrouver le milieu d’une liste chaînée
Avec des pointeurs lent-rapide sur une liste chaînée, le pointeur rapide avance de deux nœuds par étape et le pointeur lent d’un seul. Lorsque le pointeur rapide atteint la fin, le pointeur lent se trouve au milieu. Cette approche en un seul parcours, en O(n), est bien plus simple que de compter les nœuds puis de parcourir la moitié de la liste. Elle est utilisée comme sous-étape du tri fusion de listes chaînées et de la détection de palindromes dans les listes chaînées.
class Node:
def __init__(self, val, nxt=None):
self.val = val
self.next = nxt
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is at middle
# Build 1->2->3->4->5
h = Node(1, Node(2, Node(3, Node(4, Node(5)))))
mid = find_middle(h)
print(mid.val) # 3 (middle of 5 nodes)Détection de cycle : la tortue et le lièvre de Floyd
La détection de cycle de Floyd place les pointeurs lent et rapide au début d’une liste chaînée. Le pointeur lent avance d’un nœud ; le pointeur rapide de deux. Si un cycle existe, le pointeur rapide finira par rattraper le pointeur lent et ils se rencontreront à l’intérieur du cycle. Si fast atteint None, il n’y a pas de cycle. La rencontre est garantie, car fast gagne une étape sur slow à chaque itération : dans un cycle de longueur k, ils se rencontrent au plus k étapes après l’entrée de slow dans le cycle.
class ListNode:
def __init__(self, val=0, nxt=None):
self.val = val
self.next = nxt
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: # identity check (same object)
return True
return False
# 1->2->3->4->2 (cycle at node 2)
n1 = ListNode(1)
n2 = ListNode(2)
n3 = ListNode(3)
n4 = ListNode(4)
n1.next=n2; n2.next=n3; n3.next=n4; n4.next=n2
print(has_cycle(n1)) # TrueTrouver le point d’entrée d’un cycle
Après avoir détecté un cycle (slow == fast), replacez l’un des pointeurs au début. Avancez maintenant les deux pointeurs d’une étape à la fois. Ils se rencontreront au point d’entrée du cycle. Cette méthode utilise la propriété mathématique selon laquelle la distance entre le début et l’entrée du cycle est égale à la distance entre le point de rencontre et l’entrée du cycle, modulo la longueur du cycle. C’est un résultat mathématique élégant qui apparaît fréquemment dans les problèmes d’entretien difficiles.
def detect_cycle(head):
slow = fast = head
# Phase 1: detect
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
break
else:
return None # no cycle
# Phase 2: find entry
slow = head
while slow is not fast:
slow = slow.next
fast = fast.next
return slow # cycle entry node
# Using same cycled list as previous scene
print(detect_cycle(n1).val) # 2 (cycle entry)Pointeurs lent-rapide pour les nombres heureux
Les pointeurs lent-rapide s’appliquent au-delà des listes chaînées, à tout processus cyclique. Un « nombre heureux » parcourt un cycle de sommes des carrés de ses chiffres ; si n n’est pas heureux, la séquence finit par boucler. Détectez la boucle avec slow, qui avance d’une étape correspondant à un carré de chiffre, et fast, qui avance de deux étapes. S’ils se rencontrent en 1, n est heureux ; sinon, il est piégé dans un cycle qui ne contient pas 1. Il s’agit de l’algorithme de Floyd appliqué à une liste chaînée virtuelle de valeurs.
def is_happy(n):
def next_val(x):
total = 0
while x:
x, d = divmod(x, 10)
total += d * d
return total
slow = n
fast = next_val(n)
while fast != 1 and slow != fast:
slow = next_val(slow)
fast = next_val(next_val(fast))
return fast == 1
print(is_happy(19)) # True (1->9->...->1)
print(is_happy(2)) # False (enters a cycle)Nième nœud depuis la fin d’une liste
Trouvez le nième nœud depuis la fin d’une liste chaînée en un seul parcours, à l’aide de deux pointeurs. Avancez le pointeur rapide de n étapes. Avancez ensuite les deux pointeurs ensemble jusqu’à ce que le pointeur rapide atteigne la fin : le pointeur lent se trouve alors au nième nœud depuis la fin. Pour supprimer ce nœud, conservez un pointeur prev juste derrière le pointeur lent. Il s’agit d’un problème classique sur les listes chaînées en un seul parcours, qui évite de compter d’abord la longueur totale.
def remove_nth_from_end(head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# Advance fast n+1 steps
for _ in range(n + 1):
fast = fast.next
# Advance together
while fast:
slow = slow.next
fast = fast.next
# slow.next is the nth from end
slow.next = slow.next.next
return dummy.next
# Build 1->2->3->4->5, remove 2nd from end
h2 = ListNode(1,ListNode(2,ListNode(3,ListNode(4,ListNode(5)))))
result = remove_nth_from_end(h2, 2)
# Should give 1->2->3->5Pointeurs lent-rapide dans les problèmes de chaînes
Le raisonnement lent-rapide s’applique également aux problèmes sur les tableaux et les chaînes. Lors de la compression d’une chaîne codée par longueurs de séquences, le pointeur lent indique la position d’écriture et le pointeur rapide parcourt les éléments jusqu’à la fin de chaque séquence. Lorsque tous les caractères de la séquence sont égaux au caractère suivi par slow, avancez fast ; sinon, enregistrez la séquence et mettez à jour slow. Cette méthode s’exécute en O(n) en un seul parcours, avec un espace de O(1).
def compress(chars):
slow = fast = 0
while fast < len(chars):
char = chars[fast]
count = 0
# Count the run
while fast < len(chars) and chars[fast] == char:
fast += 1
count += 1
chars[slow] = char
slow += 1
if count > 1:
for c in str(count):
chars[slow] = c
slow += 1
return slow
chars = list('aabcccccaa')
print(compress(chars)) # 6
print(chars[:6]) # ['a','2','b','c','5','a']... wait
# Actually: ['a','2','b','c','5','a','2']Choisir entre les pointeurs lent-rapide et les pointeurs aux extrémités opposées
Utilisez des pointeurs aux extrémités opposées lorsque le problème porte sur des paires dont la somme atteint une cible, sur des vérifications de palindrome ou sur la réduction d’une fenêtre par les deux côtés. Utilisez des pointeurs lent-rapide lorsque vous avez besoin d’un pointeur d’écriture, pour supprimer ou déplacer des éléments, lorsque vous traitez la structure d’une liste chaînée, par exemple pour trouver son milieu ou un cycle, ou lorsque vous détectez des cycles dans une séquence de valeurs. Les deux modèles éliminent les boucles imbriquées et atteignent O(n) ; le facteur déterminant est la structure du parcours.
# Pattern matcher:
# 1. Sorted array, target sum -> OPPOSITE ENDS
# 2. Remove/filter elements in-place -> SLOW-FAST (read-write)
# 3. Linked list middle/cycle -> SLOW-FAST (1x vs 2x speed)
# 4. Detect cycle in value sequence -> SLOW-FAST (Floyd)
# Example: given sorted array, remove val in-place
def remove_sorted(nums, val):
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
nums = [0,1,2,2,3,0,4,2]
print(remove_sorted(nums, 2)) # 5Vérification rapide
Testez votre compréhension des concepts de Structures de données & algorithmes — Préparation aux entretiens de programmation présentés dans cette leçon.
Récapitulatif de la leçon
Dans cette leçon, vous avez appris que le modèle lent-rapide, ou lecture-écriture, maintient un pointeur d’écriture sur la prochaine position valide tandis qu’un pointeur rapide parcourt les éléments vers l’avant : c’est le fondement de la suppression, de la déduplication et du déplacement des zéros en place, que la tortue et le lièvre de Floyd détectent les cycles en O(n) avec un espace de O(1) en exploitant la différence de vitesse entre deux pointeurs, et qu’après la détection d’un cycle, replacer un pointeur au début puis avancer les deux pointeurs à la même vitesse permet de trouver l’entrée du cycle grâce à une égalité démontrable des distances. Nous allons maintenant étudier l’API des chaînes Python pour les entretiens.
Questions Fréquemment Posées
La leçon « Deux pointeurs : lent et rapide » est-elle gratuite ?
Oui — le texte complet de « Deux pointeurs : lent et rapide » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DSA Interview Prep, passe à CoddyKit PRO. Le cours DSA Interview Prep comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Deux pointeurs : lent et rapide » ?
Appliquez le schéma des pointeurs lent et rapide pour supprimer les doublons en place, déplacer les zéros et partitionner les tableaux autour d’une valeur pivot. Tu pratiques DSA Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer DSA Interview Prep ?
Aucune expérience préalable n'est requise. DSA Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.
Combien de temps prend la leçon « Deux pointeurs : lent et rapide » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon DSA Interview Prep ?
Oui. Chaque leçon DSA Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Bases des tableaux et opérations en place
- Sommes préfixes et totaux cumulés
- Deux pointeurs : extrémités opposées
- Deux pointeurs : lent et rapide