0Pricing
DSA Interview Prep · Lección

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 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.

¿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())   # 3

Aná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 n

Pila 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())  # 2

Pila 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())  # 2

Resumen 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()) # False

LeetCode 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()) # False

Extender 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 30

Conclusiones 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 DSA Interview Prep, actualiza a CoddyKit PRO. El curso de DSA 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 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 «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 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

  1. Implementación y aplicaciones de pilas
  2. Implementación de colas y deque
  3. Patrón de pila monótona
  4. Simulación mutua de pilas y colas
← Volver a DSA Interview Prep