Simulación mutua de pilas y colas
Implemente una cola usando dos pilas y una pila usando dos colas, y explique el coste amortizado de cada enfoque.
Simulación mutua de pilas y colas es una lección gratuita de Coding 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 Coding Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Coding Interview Prep incluye 4 lecciones en total.
¿Por qué simular una estructura con la otra?
Implementar una cola utilizando dos pilas y una pila utilizando dos colas son preguntas clásicas de diseño en entrevistas. Evalúan su comprensión de los invariantes de ambas estructuras de datos y su capacidad para mantener la garantía de una estructura utilizando las operaciones básicas de otra. Los entrevistadores también utilizan estos problemas como introducción a la complejidad amortizada.
La idea clave es que las pilas son LIFO y las colas son FIFO. Para convertir una en otra debe invertir el orden; pasar los elementos de una pila a otra produce el orden de inserción original, que es FIFO.
Cola utilizando dos pilas (enfoque diferido)
El enfoque diferido utiliza una pila inbox para las operaciones push y una pila outbox para las operaciones pop. Cuando se llama a dequeue, si outbox está vacía, transfiera todos los elementos de inbox a outbox; esta inversión restaura el orden FIFO. Si outbox no está vacía, extraiga directamente de ella. Las transferencias se realizan de forma diferida, amortizando el coste de transferencia O(n) entre muchas operaciones.
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()) # 3Análisis amortizado O(1) de una cola a partir de pilas
Cada elemento se transfiere de inbox a outbox como máximo una vez. Extraer de outbox cuesta O(1) y las transferencias solo se realizan cuando outbox está vacía; por tanto, el trabajo total de n operaciones push y n operaciones pop es como máximo de 2n operaciones de pila: O(n) en total y O(1) amortizado por operación. Esto significa que algunas operaciones individuales pueden costar O(n) en el peor caso, pero el coste medio es 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 utilizando dos colas (extracción diferida)
Implementar una pila con dos colas resulta menos natural porque las colas son FIFO. El enfoque de extracción diferida mantiene una cola principal y una cola temporal. En push, encole el elemento en la cola principal (O(1)). En pop o peek, desencole todos los elementos salvo el último en la cola temporal, guarde el último elemento y, después, intercambie las colas. Esto cuesta O(n) por operación pop, pero O(1) por operación push.
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 utilizando una cola (rotación al insertar)
Una implementación elegante con una sola cola consiste en que, al realizar push, se encole el elemento nuevo y después se rote la cola para colocar dicho elemento al frente. Rotar significa desencolar y volver a encolar todos los elementos que ya estaban allí antes de la operación push. Así, pop y peek cuestan O(1) (simplemente desencolan o consultan el elemento del frente). Push cuesta O(n): el intercambio contrario al de la versión con dos colas.
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()) # 2Resumen de las alternativas: ¿cuál elegir?
Para una cola a partir de dos pilas: push O(1), pop/peek O(1) amortizado; prefiera esta opción cuando las operaciones pop sean frecuentes. Para una pila a partir de dos colas: push O(1), pop O(n); prefiera esta opción cuando las operaciones push sean mucho más frecuentes que las operaciones pop. Para una pila a partir de una cola: push O(n), pop O(1); prefiera esta opción cuando predominen las operaciones pop. Exponga explícitamente estas ventajas y desventajas en una entrevista para demostrar que piensa más allá de «funciona».
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)')¿Por qué invertir restaura FIFO?
Cuando los elementos 1, 2 y 3 se insertan en una pila (inbox), quedan ordenados de abajo arriba como 1, 2 y 3. Al extraerlos todos en una segunda pila (outbox), se invierte el orden: outbox tiene el 3 abajo y el 1 arriba. Extraer elementos de outbox devuelve primero el 1, después el 2 y finalmente el 3, exactamente en el orden de inserción FIFO. Por eso exactamente dos inversiones (dos pilas) restauran FIFO, mientras que una sola pila produciría 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: implementar una cola utilizando pilas
LeetCode 232 es el problema directo de «cola a partir de dos pilas». La solución esperada es la transferencia diferida a outbox. En una entrevista, indique que cada elemento se mueve de inbox a outbox como máximo una vez, por lo que todas las operaciones tienen un coste amortizado O(1). Mencione que las llamadas individuales a pop pueden costar O(n) en el peor caso (cuando outbox está vacía), pero que el promedio entre n operaciones es 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: implementar una pila utilizando colas
LeetCode 225 es el problema de la «pila a partir de colas». La solución más clara es rotar la cola al realizar push. Después de insertar el elemento x, rote la cola moviendo detrás de x todos los elementos que ya estaban allí. Esto cuesta O(n) por operación push, pero hace que top y pop cuesten O(1). Exponga esta ventaja y desventaja, y confirme que se ajusta a las restricciones (por ejemplo, una carga de trabajo con pocas operaciones push o muchas operaciones 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()) # FalseExtender a tres pilas en un solo array
Un reto de diseño relacionado consiste en implementar tres pilas usando un solo array. Un enfoque divide el array en tres secciones fijas del mismo tamaño. Un enfoque más flexible utiliza almacenamiento intercalado con punteros: hace crecer cada pila dentro de su región y copia los elementos cuando las fronteras colisionan. Esto pone a prueba la gestión dinámica de arrays y se plantea en entrevistas para puestos sénior. El enfoque de secciones fijas es más sencillo, pero desperdicia espacio si las pilas crecen de forma desigual.
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 30Conclusiones clave: patrones de simulación
Los problemas de simulación mutua enseñan un principio más general: cualquier estructura de datos puede construirse a partir de otra si se cuenta con suficiente almacenamiento intermedio y operaciones de inversión. El coste de la simulación depende de las operaciones que se optimicen: siempre se puede hacer que push sea O(1) o que pop sea O(1), pero conseguir que ambas sean O(1) requiere amortización o varias estructuras auxiliares.
En una entrevista, pregunte siempre: «¿Qué operaciones son más frecuentes?». Esto guía la elección de la variante de implementación y demuestra una forma de pensar propia de un nivel sénior sobre los requisitos operativos.
Comprobación rápida
Ponga a prueba 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: una cola implementada con dos pilas consigue un pop amortizado O(1) al transferir elementos de inbox a outbox de forma diferida, una pila implementada con una cola consigue un pop O(1) al rotar la cola en cada push (push O(n)) y la elección de qué operación hacer O(1) depende del patrón de uso. A continuación explorará los componentes internos de los mapas hash y la gestión de colisiones.
Preguntas frecuentes
¿La lección «Simulación mutua de pilas y colas» es gratis?
Sí — el texto completo de «Simulación mutua de pilas y colas» 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 Coding Interview Prep, actualiza a CoddyKit PRO. El curso de Coding Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Simulación mutua de pilas y colas»?
Implemente una cola usando dos pilas y una pila usando dos colas, y explique el coste amortizado de cada enfoque. Practicas Coding 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 Coding Interview Prep?
No se requiere experiencia previa. Coding 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 «Simulación mutua de pilas y colas»?
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 Coding Interview Prep?
Sí. Cada lección de Coding 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
- Implementación y aplicaciones de pilas
- Implementación de colas y deque
- Patrón de pila monótona
- Simulación mutua de pilas y colas