Simulazione reciproca di stack e coda
Implementi una coda usando due stack e uno stack usando due code, spiegando il costo ammortizzato di ciascun approccio
Simulazione reciproca di stack e coda è 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.
Perché simulare una struttura con l'altra?
Implementare una coda usando due pile e una pila usando due code sono classiche domande di progettazione nei colloqui tecnici. Verificano la comprensione degli invarianti di entrambe le strutture dati e la capacità di mantenere la garanzia di una struttura usando le primitive dell'altra. Sono anche un punto di partenza per discutere la complessità ammortizzata.
L'idea fondamentale è questa: le pile sono LIFO e le code sono FIFO. Per effettuare la conversione è necessario invertire l'ordine; trasferire una pila in un'altra pila ripristina l'ordine di inserimento originale, che è FIFO.
Coda usando due pile (approccio lazy)
L'approccio lazy utilizza una pila inbox per le operazioni di inserimento e una pila outbox per le operazioni di rimozione. Quando viene chiamato dequeue, se outbox è vuota, trasferisca tutti gli elementi da inbox a outbox: questa inversione ripristina l'ordine FIFO. Se outbox non è vuota, estragga direttamente da essa. I trasferimenti avvengono lazy, ammortizzando il costo O(n) del trasferimento su molte operazioni.
class MyQueue:
def __init__(self):
self.inbox = []
self.outbox = []
def push(self, x):
self.inbox.append(x)
def _transfer(self):
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop())
def pop(self):
self._transfer()
return self.outbox.pop()
def peek(self):
self._transfer()
return self.outbox[-1]
def empty(self):
return not self.inbox and not self.outbox
q = MyQueue()
q.push(1); q.push(2); q.push(3)
print(q.peek()) # 1
print(q.pop()) # 1
print(q.pop()) # 2
q.push(4)
print(q.pop()) # 3Analisi ammortizzata O(1) per una coda ottenuta da pile
Ogni elemento viene trasferito da inbox a outbox al massimo una volta. Quando l'estrazione da outbox è O(1) e i trasferimenti avvengono solo quando outbox è vuota, il lavoro totale per n inserimenti e n rimozioni è al massimo di 2n operazioni sulle pile: O(n) complessivo, ovvero O(1) ammortizzato per operazione. Ciò significa che nel caso peggiore le singole operazioni possono essere O(n), ma la media è O(1).
# Trace transfer costs for 10 push/pop interleaved
class TrackedQueue:
def __init__(self):
self.inbox = []; self.outbox = []; self.transfers = 0
def push(self, x): self.inbox.append(x)
def pop(self):
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop())
self.transfers += 1
return self.outbox.pop()
q = TrackedQueue()
for i in range(5):
q.push(i)
for _ in range(5):
q.pop()
q.push(10); q.push(20)
q.pop()
print('Total transfer operations:', q.transfers) # at most nPila usando due code (rimozione lazy)
Implementare una pila con due code è meno naturale, perché le code sono FIFO. L'approccio lazy per la rimozione mantiene una coda principale e una coda temporanea. Durante push, inserisca l'elemento nella coda principale (O(1)). Durante pop o peek, rimuova dalla coda tutti gli elementi tranne l'ultimo e li inserisca nella coda temporanea, salvi l'ultimo elemento, quindi scambi le due code. Il costo è O(n) per ogni rimozione, ma O(1) per ogni inserimento.
from collections import deque
class MyStack:
def __init__(self):
self.main = deque()
self.temp = deque()
def push(self, x):
self.main.append(x) # O(1)
def pop(self):
# Move all but last element to temp
while len(self.main) > 1:
self.temp.append(self.main.popleft())
val = self.main.popleft() # the 'top'
self.main, self.temp = self.temp, self.main # swap
return val
def top(self):
while len(self.main) > 1:
self.temp.append(self.main.popleft())
val = self.main[0]
self.temp.append(self.main.popleft())
self.main, self.temp = self.temp, self.main
return val
def empty(self):
return len(self.main) == 0
s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top()) # 3
print(s.pop()) # 3
print(s.pop()) # 2Pila usando una coda (rotazione durante l'inserimento)
Un'implementazione elegante con una sola coda: durante push, inserisca il nuovo elemento, quindi ruoti la coda in modo che il nuovo elemento si trovi all'inizio. Ruotare significa rimuovere e reinserire tutti gli elementi presenti prima dell'inserimento. In questo modo pop e peek hanno costo O(1) (basta rimuovere o consultare l'elemento iniziale). push ha costo O(n): è il compromesso opposto rispetto alla versione con due code.
from collections import deque
class MyStackOneQueue:
def __init__(self):
self.q = deque()
def push(self, x):
self.q.append(x)
# Rotate: move all preceding elements behind x
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self):
return self.q.popleft()
def top(self):
return self.q[0]
def empty(self):
return len(self.q) == 0
s = MyStackOneQueue()
s.push(1); s.push(2); s.push(3)
print(s.top()) # 3
print(s.pop()) # 3
print(s.top()) # 2Riepilogo dei compromessi: quale variante scegliere?
Per una coda ottenuta da due pile: push O(1), pop/peek O(1) ammortizzato; la preferisca quando le operazioni di pop sono frequenti. Per una pila ottenuta da due code: push O(1), pop O(n); la preferisca quando le operazioni di push sono molto più frequenti delle operazioni di pop. Per una pila ottenuta da una sola coda: push O(n), pop O(1); la preferisca quando prevalgono le operazioni di pop. Esponga chiaramente questi compromessi durante un colloquio per dimostrare di considerare più del semplice fatto che la soluzione funzioni.
print('Queue from 2 stacks: push O(1), pop O(1) amortised')
print('Stack from 2 queues: push O(1), pop O(n)')
print('Stack from 1 queue: push O(n), pop O(1)')Perché invertire l'ordine ripristina il FIFO?
Quando gli elementi 1, 2, 3 vengono inseriti in una pila (inbox), si dispongono dal fondo verso la cima nell'ordine 1, 2, 3. Estraendoli tutti e inserendoli in una seconda pila (outbox), l'ordine si inverte: outbox contiene 3 in fondo e 1 in cima. Estraendo gli elementi da outbox si ottengono 1, poi 2, poi 3, esattamente nell'ordine di inserimento FIFO. Ecco perché esattamente due inversioni (due pile) ripristinano il FIFO, mentre una sola pila produrrebbe il comportamento LIFO.
# Demonstrate double-reversal = FIFO
inbox = [1, 2, 3] # pushed in this order
outbox = []
while inbox:
outbox.append(inbox.pop())
print('outbox (one reversal):', outbox) # [3, 2, 1] top-to-bottom
# Pop from outbox gives FIFO
result = []
while outbox:
result.append(outbox.pop())
print('dequeued:', result) # [1, 2, 3] — FIFO!LeetCode 232: implementare una coda usando pile
LeetCode 232 è il problema diretto della «coda ottenuta da due pile». La soluzione prevista è il trasferimento lazy in outbox. Durante un colloquio, dichiari che ogni elemento si sposta da inbox a outbox al massimo una volta, rendendo tutte le operazioni O(1) ammortizzato. Precisi che le singole chiamate a pop possono avere costo O(n) nel caso peggiore, quando outbox è vuota, ma la media su n operazioni è O(1).
class MyQueue:
def __init__(self):
self.inbox = []
self.outbox = []
def push(self, x):
self.inbox.append(x)
def pop(self):
self.peek() # ensure outbox is populated
return self.outbox.pop()
def peek(self):
if not self.outbox:
while self.inbox: # transfer lazily
self.outbox.append(self.inbox.pop())
return self.outbox[-1]
def empty(self):
return not self.inbox and not self.outbox
# Simulation
q = MyQueue()
q.push(1); q.push(2)
print(q.peek()) # 1
print(q.pop()) # 1
print(q.empty()) # FalseLeetCode 225: implementare una pila usando code
LeetCode 225 è il problema della «pila ottenuta da code». La soluzione più chiara è quella con una sola coda e rotazione durante l'inserimento. Dopo aver inserito l'elemento x, ruoti la coda spostando dietro x tutti gli elementi che erano già presenti. Questa operazione costa O(n) per ogni push, ma rende top e pop O(1). Esponga il compromesso e verifichi che sia compatibile con i vincoli, ad esempio un carico di lavoro con poche operazioni di push o con molte operazioni di pop.
from collections import deque
class MyStack:
def __init__(self):
self.q = deque()
def push(self, x): # O(n)
self.q.append(x)
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self): # O(1)
return self.q.popleft()
def top(self): # O(1)
return self.q[0]
def empty(self):
return len(self.q) == 0
s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top()) # 3
print(s.pop()) # 3
print(s.top()) # 2
print(s.empty()) # FalseEstendere a tre pile in un unico array
Una sfida progettuale correlata consiste nell'implementare tre pile usando un singolo array. Un approccio divide l'array in tre sezioni fisse di uguali dimensioni. Un approccio più flessibile usa una memorizzazione intercalata con puntatori: ogni pila cresce nella propria area e gli elementi vengono copiati quando i confini si incontrano. Questo mette alla prova la gestione dinamica degli array ed è un argomento proposto nei colloqui per posizioni senior. L'approccio con sezioni fisse è più semplice, ma spreca spazio se le pile crescono in modo non uniforme.
class ThreeStacks:
def __init__(self, size):
self.data = [0] * (3 * size)
self.tops = [-1, -1, -1] # relative top of each stack
self.size = size
def push(self, stack_num, val):
self.tops[stack_num] += 1
if self.tops[stack_num] >= self.size:
raise OverflowError('stack full')
self.data[stack_num * self.size + self.tops[stack_num]] = val
def pop(self, stack_num):
if self.tops[stack_num] < 0:
raise IndexError('stack empty')
val = self.data[stack_num * self.size + self.tops[stack_num]]
self.tops[stack_num] -= 1
return val
ts = ThreeStacks(5)
ts.push(0, 10); ts.push(1, 20); ts.push(2, 30)
print(ts.pop(0), ts.pop(1), ts.pop(2)) # 10 20 30Concetti chiave: schemi di simulazione
I problemi di simulazione reciproca insegnano un principio più generale: con una quantità sufficiente di memoria intermedia e di inversioni, è possibile costruire qualsiasi struttura dati a partire da un'altra. Il costo della simulazione dipende dalle operazioni che si ottimizzano: è sempre possibile rendere push O(1) oppure pop O(1), ma per rendere entrambe O(1) sono necessarie l'ammortizzazione o più strutture ausiliarie.
Durante un colloquio, chieda sempre: «Quali operazioni vengono eseguite più spesso?». Questa domanda guida la scelta della variante di implementazione e dimostra una mentalità da senior nei confronti dei requisiti operativi.
Verifica rapida
Verifichi la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep presentati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: una coda costruita con due pile raggiunge un'operazione pop ammortizzata in O(1) trasferendo gli elementi da inbox a outbox solo quando necessario, una pila costruita con una coda raggiunge un'operazione pop in O(1) ruotando la coda a ogni push (push in O(n)) e la scelta dell'operazione da rendere O(1) dipende dal modello di utilizzo. Nella prossima lezione esploreremo i meccanismi interni delle hash map e la gestione delle collisioni.
Domande Frequenti
La lezione «Simulazione reciproca di stack e coda» è gratuita?
Sì — il testo completo di «Simulazione reciproca di stack e coda» è 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 «Simulazione reciproca di stack e coda»?
Implementi una coda usando due stack e uno stack usando due code, spiegando il costo ammortizzato di ciascun approccio 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 «Simulazione reciproca di stack e coda»?
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
- Implementazione e applicazioni dello stack
- Implementazione della coda e deque
- Schema dello stack monotono
- Simulazione reciproca di stack e coda