0Pricing
Coding Interview Prep · Lezione

Due puntatori: lento e veloce

Applichi lo schema dei puntatori lento e veloce per rimuovere duplicati in-place, spostare gli zeri e partizionare gli array attorno a un valore pivot

Due puntatori: lento e veloce è una lezione Coding Interview Prep gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.

Spiegazione dei puntatori slow-fast

Lo schema slow-fast (chiamato anche schema della tartaruga e della lepre) usa due puntatori che si muovono a velocità diverse nella stessa sequenza. A differenza dei puntatori alle estremità opposte, entrambi partono dall'inizio. Il puntatore slow avanza di un passo alla volta, mentre il puntatore fast ne percorre due (o più). La differenza di velocità crea invarianti utili: slow tiene traccia di un prefisso valido, mentre fast scansiona in avanti alla ricerca di condizioni.

# 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]

Rimozione dei duplicati da un array ordinato

In un array ordinato, i duplicati sono adiacenti. Il puntatore slow tiene traccia dell'ultimo valore unico scritto, mentre fast scansiona in avanti. Ogni volta che fast raggiunge un valore diverso da nums[slow], faccia avanzare slow e copi il nuovo valore. Questo algoritmo in-place richiede tempo O(n) e spazio aggiuntivo O(1): è una domanda tipica dei colloqui tecnici, che verifica la padronanza del modello del puntatore di lettura-scrittura.

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]

Spostamento degli zeri con slow-fast

Sposti tutti gli zeri alla fine mantenendo l'ordine relativo degli elementi diversi da zero. Il puntatore slow indica la posizione successiva destinata a un elemento diverso da zero. Il puntatore fast scansiona alla ricerca di valori diversi da zero. Quando fast ne trova uno, lo copi nella posizione di slow e faccia avanzare entrambi. Al termine della scansione, riempia di zeri le posizioni da slow alla fine. Tempo O(n), spazio 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]

Partizionamento di un array intorno a un pivot

La fase di partizionamento di quicksort riorganizza gli elementi in-place in modo che tutti i valori < pivot precedano i valori >= pivot. Lo schema di Lomuto usa un puntatore slow, che indica l'ultima posizione di un elemento piccolo, e un puntatore fast, che scansiona in avanti. Quando fast trova un elemento piccolo, incrementi slow ed esegua uno scambio. Questo richiede tempo O(n) e spazio aggiuntivo 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 <= pivot

Ricerca del nodo centrale di una lista concatenata

Con i puntatori slow-fast su una lista concatenata, il puntatore fast avanza di due nodi per passo, mentre slow avanza di uno. Quando fast raggiunge la fine, slow si trova al centro. Questo approccio in un'unica scansione, in O(n), è molto più semplice che contare i nodi e poi percorrere metà della lista. Viene usato come fase intermedia nel merge sort per liste concatenate e nel rilevamento di palindromi in liste concatenate.

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)

Rilevamento dei cicli: tartaruga e lepre di Floyd

Il rilevamento dei cicli di Floyd colloca i puntatori slow e fast all'inizio di una lista concatenata. Slow avanza di un nodo, mentre fast ne avanza due. Se esiste un ciclo, il puntatore fast raggiungerà prima o poi slow e i due si incontreranno all'interno del ciclo. Se fast raggiunge None, il ciclo non esiste. L'incontro è garantito perché fast guadagna un passo su slow a ogni iterazione: in un ciclo di lunghezza k, i due si incontrano entro k passi dall'ingresso di slow nel 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))  # True

Ricerca del punto di ingresso del ciclo

Dopo aver rilevato un ciclo (slow == fast), reimposti uno dei puntatori su head. A questo punto faccia avanzare entrambi i puntatori di un passo alla volta. Si incontreranno nel punto di ingresso del ciclo. Questo sfrutta la proprietà matematica secondo cui la distanza da head all'ingresso del ciclo è uguale alla distanza dal punto d'incontro all'ingresso del ciclo (modulo la lunghezza del ciclo). È un elegante risultato matematico che compare spesso nei problemi difficili dei colloqui tecnici.

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)

Slow-fast per i numeri felici

I puntatori slow-fast si applicano, oltre che alle liste concatenate, a qualsiasi processo che presenti cicli. Un «numero felice» attraversa una sequenza di somme dei quadrati delle cifre: se n non è felice, la sequenza finisce per ripetersi in un ciclo. Rilevi il ciclo con slow (un passo = una somma dei quadrati delle cifre) e fast (due passi). Se si incontrano in 1, n è felice; altrimenti è intrappolato in un ciclo che non contiene 1. Si tratta dell'algoritmo di Floyd applicato a una lista concatenata virtuale di valori.

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)

N-esimo nodo dalla fine della lista

Trovi l'n-esimo nodo dalla fine di una lista concatenata in un'unica scansione usando due puntatori. Faccia avanzare il puntatore fast di n passi. Poi faccia avanzare entrambi i puntatori insieme finché fast raggiunge la fine: slow si trova ora sull'n-esimo nodo dalla fine. Per eliminare questo nodo, mantenga un puntatore 'prev' un passo indietro rispetto a slow. È un classico problema sulle liste concatenate in un'unica scansione, che evita di dover contare prima la lunghezza 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->5

Slow-fast nei problemi sulle stringhe

Il ragionamento slow-fast si applica anche ai problemi su array e stringhe. Quando comprime una stringa con codifica run-length, il puntatore slow indica la posizione di scrittura e fast scansiona fino alla fine di ogni sequenza. Quando tutti i caratteri della sequenza sono uguali al carattere di slow, faccia avanzare fast; altrimenti registri la sequenza e aggiorni slow. Si ottiene O(n) in un'unica scansione con spazio 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']

Scelta tra slow-fast ed estremità opposte

Usi i puntatori alle estremità opposte quando il problema riguarda coppie la cui somma è uguale a un valore target, la verifica di palindromi o la riduzione di una finestra da entrambi i lati. Usi i puntatori slow-fast quando serve un puntatore di scrittura (per rimuovere o spostare elementi), quando si lavora sulla struttura di una lista concatenata (nodo centrale, ciclo) o quando si rilevano cicli in una sequenza di valori qualsiasi. Entrambi eliminano i cicli annidati e raggiungono O(n): il fattore decisivo è la struttura dell'attraversamento.

# 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))  # 5

Verifica rapida

Verifichi la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha appreso che: il modello slow-fast (lettura-scrittura) mantiene un puntatore di scrittura sulla posizione valida successiva mentre un puntatore fast scansiona in avanti: è la base della rimozione in-place, della deduplicazione e dello spostamento degli zeri, il metodo della tartaruga e della lepre di Floyd rileva i cicli in tempo O(n) e spazio O(1) sfruttando la differenza di velocità tra due puntatori e dopo aver rilevato un ciclo, reimpostare un puntatore su head e far avanzare entrambi alla stessa velocità permette di trovare l'ingresso del ciclo grazie a una dimostrabile uguaglianza delle distanze. Ora esamineremo l'API delle stringhe Python per i colloqui tecnici.

Domande Frequenti

La lezione «Due puntatori: lento e veloce» è gratuita?

Sì — il testo completo di «Due puntatori: lento e veloce» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Due puntatori: lento e veloce»?

Applichi lo schema dei puntatori lento e veloce per rimuovere duplicati in-place, spostare gli zeri e partizionare gli array attorno a un valore pivot Eserciti Coding Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Coding Interview Prep?

Non è richiesta alcuna esperienza precedente. Coding Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «Due puntatori: lento e veloce»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Coding Interview Prep?

Sì. Ogni lezione Coding Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Basi degli array e operazioni in-place
  2. Somme prefisse e totali progressivi
  3. Due puntatori: estremità opposte
  4. Due puntatori: lento e veloce
← Torna a Coding Interview Prep