Dos punteros: lento y rápido
Aplique el patrón de punteros lento y rápido para eliminar duplicados in-place, mover ceros y particionar arrays alrededor de un valor pivote.
Dos punteros: lento y rápido es una lección gratuita de DSA Interview Prep en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DSA Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DSA Interview Prep incluye 4 lecciones en total.
Explicación de los punteros lento y rápido
El patrón de punteros lento-rápido, también llamado tortuga y liebre, utiliza dos punteros que avanzan a distintas velocidades por la misma secuencia. A diferencia de los punteros en extremos opuestos, ambos comienzan al principio. El puntero lento avanza un paso cada vez; el rápido avanza dos pasos, o más. Esta diferencia de velocidad crea invariantes útiles: el puntero lento sigue el rastro de un «prefijo válido», mientras el rápido busca condiciones más adelante.
# 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]Eliminar duplicados de un arreglo ordenado
En un arreglo ordenado, los duplicados son adyacentes. El puntero lento sigue el último valor único escrito; el puntero rápido explora los elementos siguientes. Cuando el puntero rápido llega a un valor diferente de nums[slow], incremente slow y copie el nuevo valor. Este algoritmo sobre el mismo arreglo se ejecuta en O(n) con O(1) de espacio adicional; es una pregunta habitual de entrevistas que evalúa el dominio del patrón de punteros de lectura y escritura.
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]Mover ceros con punteros lento-rápido
Mueva todos los ceros al final y conserve el orden relativo de los elementos distintos de cero. El puntero lento indica la siguiente posición para un elemento distinto de cero. El puntero rápido busca valores distintos de cero. Cuando fast encuentra uno, cópielo en la posición de slow y haga avanzar ambos punteros. Después del recorrido, rellene con ceros las posiciones desde slow hasta el final. Tiempo O(n) y espacio O(1).
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]Particionar un arreglo alrededor de un pivote
El subpaso de partición de quicksort reorganiza los elementos en el mismo arreglo, de modo que todos los valores < pivot queden antes que los valores >= pivot. El esquema de Lomuto utiliza un puntero lento, que marca la última posición de un elemento pequeño, y un puntero rápido, que recorre el arreglo hacia delante. Cuando fast encuentra un elemento pequeño, incremente slow e intercambie los elementos. Se ejecuta en O(n) con O(1) de espacio adicional.
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 <= pivotEncontrar el centro de una lista enlazada
Con punteros lento-rápido en una lista enlazada, el puntero rápido avanza dos nodos por paso y el lento avanza uno. Cuando fast llega al final, slow se encuentra en el centro. Este enfoque de una sola pasada y O(n) es mucho más limpio que contar los nodos y después recorrer la mitad de la lista. Se utiliza como subpaso en el ordenamiento por mezcla de listas enlazadas y en la detección de palíndromos en listas enlazadas.
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)Detección de ciclos: la tortuga y la liebre de Floyd
La detección de ciclos de Floyd coloca los punteros lento y rápido en el inicio de una lista enlazada. El lento avanza un nodo; el rápido, dos. Si existe un ciclo, el puntero rápido acabará alcanzando al lento y ambos se encontrarán dentro del ciclo. Si fast llega a None, no existe ningún ciclo. El encuentro está garantizado porque fast gana un paso a slow en cada iteración: en un ciclo de longitud k, se encuentran en un máximo de k pasos desde que slow entra en el ciclo.
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)) # TrueEncontrar el punto de entrada del ciclo
Después de detectar un ciclo (slow == fast), restablezca uno de los punteros al inicio. Ahora haga avanzar ambos punteros un paso cada vez. Se encontrarán en el punto de entrada del ciclo. Esto utiliza la propiedad matemática de que la distancia desde el inicio hasta la entrada del ciclo es igual a la distancia desde el punto de encuentro hasta la entrada del ciclo, módulo la longitud del ciclo. Es un elegante resultado matemático que aparece con frecuencia en problemas difíciles de entrevistas.
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)Punteros lento-rápido para números felices
Los punteros lento-rápido también se aplican fuera de las listas enlazadas, a cualquier proceso que forme ciclos. Un «número feliz» recorre una secuencia de sumas de cuadrados de sus dígitos; si n no es feliz, la secuencia acaba entrando en un bucle. Detecte el bucle con slow (un paso = un cuadrado de dígito) y fast (dos pasos). Si se encuentran en 1, n es feliz; de lo contrario, queda atrapado en un ciclo que no contiene 1. Este es el algoritmo de Floyd aplicado a una lista enlazada virtual de valores.
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)Nodo n desde el final de una lista
Encuentre el nodo n desde el final de una lista enlazada en una sola pasada utilizando dos punteros. Haga avanzar el puntero rápido n pasos. Después, haga avanzar ambos punteros juntos hasta que fast llegue al final; slow estará entonces en el nodo n desde el final. Para eliminar este nodo, mantenga un puntero «prev» un paso detrás de slow. Este es un problema clásico de listas enlazadas de una sola pasada que evita tener que contar primero la longitud total.
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->5Punteros lento-rápido en problemas de cadenas
El razonamiento de lento-rápido también se aplica a problemas de arreglos y cadenas. Al comprimir una cadena codificada mediante longitudes de ejecución, el puntero lento marca la posición de escritura y el rápido recorre hasta el final de cada ejecución. Mientras todos los caracteres de la ejecución sean iguales al carácter de slow, haga avanzar fast; de lo contrario, registre la ejecución y actualice slow. Esto logra O(n) en una sola pasada con O(1) de espacio.
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']Elegir entre punteros lento-rápido y extremos opuestos
Utilice punteros en extremos opuestos cuando el problema incluya pares cuya suma sea un objetivo, comprobaciones de palíndromos o una ventana que se estrecha desde ambos lados. Utilice punteros lento-rápido cuando necesite un puntero de escritura (para eliminar o mover elementos), al procesar la estructura de una lista enlazada (centro o ciclo) o al detectar ciclos en cualquier secuencia de valores. Ambos eliminan los bucles anidados y logran O(n); el factor decisivo es la estructura del recorrido.
# 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)) # 5Comprobación rápida
Compruebe su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.
Resumen de la lección
En esta lección aprendió que: el patrón lento-rápido (de lectura y escritura) mantiene un puntero de escritura en la siguiente posición válida mientras un puntero rápido recorre el arreglo hacia delante; es la base de las operaciones sobre el mismo arreglo para eliminar, deduplicar y mover ceros; la tortuga y la liebre de Floyd detectan ciclos en O(n) de tiempo y O(1) de espacio aprovechando la diferencia de velocidad entre dos punteros; y después de detectar un ciclo, restablecer un puntero al inicio y hacer avanzar ambos a la misma velocidad encuentra la entrada del ciclo gracias a una igualdad de distancias demostrable. A continuación, exploraremos la API de cadenas de Python para entrevistas.
Preguntas frecuentes
¿La lección «Dos punteros: lento y rápido» es gratis?
Sí — el texto completo de «Dos punteros: lento y rápido» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DSA Interview Prep, actualiza a CoddyKit PRO. El curso de DSA Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Dos punteros: lento y rápido»?
Aplique el patrón de punteros lento y rápido para eliminar duplicados in-place, mover ceros y particionar arrays alrededor de un valor pivote. Practicas DSA Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar DSA Interview Prep?
No se requiere experiencia previa. DSA Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.
¿Cuánto tiempo toma la lección «Dos punteros: lento y rápido»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de DSA Interview Prep?
Sí. Cada lección de DSA Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Fundamentos de arrays y operaciones in-place
- Sumas de prefijos y totales acumulados
- Dos punteros: extremos opuestos
- Dos punteros: lento y rápido