Tengo la siguiente pregunta de tarea:
Implemente los métodos de pila push (x) y pop () usando dos colas.
Esto me parece extraño porque:
- Una pila es una cola (LIFO)
- No veo por qué necesitarías dos colas para implementarlo
Busqué alrededor:
y encontré un par de soluciones. Esto es lo que terminé con:
public class Stack<T> {
LinkedList<T> q1 = new LinkedList<T>();
LinkedList<T> q2 = new LinkedList<T>();
public void push(T t) {
q1.addFirst(t);
}
public T pop() {
if (q1.isEmpty()) {
throw new RuntimeException(
"Can't pop from an empty stack!");
}
while(q1.size() > 1) {
q2.addFirst( q1.removeLast() );
}
T popped = q1.pop();
LinkedList<T> tempQ = q1;
q1 = q2;
q2 = tempQ;
return popped;
}
}
Pero no entiendo cuál es la ventaja sobre el uso de una sola cola; la versión de dos colas parece inútilmente complicada.
Digamos que elegimos que los empujes sean los más eficientes de los 2 (como hice anteriormente), push
seguirían siendo los mismos y pop
simplemente requerirían iterar hasta el último elemento y devolverlo. En ambos casos, el push
sería O(1)
y el pop
sería O(n)
; pero la versión de cola única sería drásticamente más simple. Solo debería requerir un solo bucle for.
¿Me estoy perdiendo de algo? Cualquier idea aquí sería apreciada.
Respuestas:
No hay ventaja: este es un ejercicio puramente académico.
Una muy hace mucho tiempo, cuando yo era un estudiante de primer año en la universidad tuve un ejercicio similar 1 . El objetivo era enseñar a los estudiantes cómo usar la programación orientada a objetos para implementar algoritmos en lugar de escribir soluciones iterativas usando
for
bucles con contadores de bucles. En su lugar, combine y reutilice las estructuras de datos existentes para lograr sus objetivos.Nunca usará este código en Real World TM . Lo que debe eliminar de este ejercicio es cómo "pensar fuera de la caja" y reutilizar el código.
Tenga en cuenta que debe usar la interfaz java.util.Queue en su código en lugar de usar la implementación directamente:
Esto le permite usar otras
Queue
implementaciones si lo desea, así como también ocultar 2 métodosLinkedList
que pueden activar el espíritu de laQueue
interfaz. Esto incluyeget(int)
ypop()
(mientras su código se compila, hay un error lógico dado las restricciones de su asignación. Declarar sus variables como enQueue
lugar deLinkedList
lo revelará). Lectura relacionada: Comprender la "programación en una interfaz" y ¿Por qué son útiles las interfaces?1 Todavía recuerdo: el ejercicio consistía en revertir una pila utilizando solo métodos en la interfaz de pila y sin métodos de utilidad en
java.util.Collections
otras clases de utilidad "estáticas". La solución correcta implica el uso de otras estructuras de datos como objetos temporales de retención: debe conocer las diferentes estructuras de datos, sus propiedades y cómo combinarlas para hacerlo. Sorprendió a la mayoría de mi clase CS101 que nunca había programado antes.2 Los métodos todavía están allí, pero no puede acceder a ellos sin tipos de conversión o reflexión. Por lo tanto, no es fácil usar esos métodos sin cola.
fuente
No hay ventaja Te has dado cuenta correctamente de que usar Colas para implementar una Pila conduce a una complejidad de tiempo horrible. Ningún programador (competente) haría algo como esto en la "vida real".
Pero es posible. Puede usar una abstracción para implementar otra, y viceversa. Una pila se puede implementar en términos de dos colas, y de la misma manera usted podría implementar una cola en términos de dos pilas. La ventaja de este ejercicio es:
En realidad, este es un gran ejercicio. Debería hacerlo yo mismo ahora :)
fuente
push
,peek
y laspop
operaciones están en O (1). Lo mismo para una pila basada en matriz redimensionable, excepto quepush
está en O (1) amortizado, con O (n) en el peor de los casos. En comparación con eso, una pila basada en la cola es muy inferior con O (n) push, O (1) pop and peek, o alternativamente O (1) push, O (n) pop and peek.Definitivamente hay un propósito real para hacer una cola con dos pilas. Si usa estructuras de datos inmutables de un lenguaje funcional, puede ingresar a una pila de elementos que se pueden empujar y extraer de una lista de elementos emergentes. Los elementos poppables se crean cuando todos los elementos se han desplegado, y la nueva pila poppable es el reverso de la pila empujable, donde la nueva pila empujable ahora está vacía. Es eficiente
¿En cuanto a una pila hecha de dos colas? Eso podría tener sentido en un contexto en el que tiene un montón de colas grandes y rápidas disponibles. Definitivamente es inútil como este tipo de ejercicio Java. Pero podría tener sentido si esos son canales o colas de mensajes. (es decir: N mensajes en cola, con una operación O (1) para mover elementos (N-1) en el frente a una nueva cola.)
fuente
El ejercicio se ideó innecesariamente desde un punto de vista práctico. El punto es forzarlo a usar la interfaz de la cola de una manera inteligente para implementar la pila. Por ejemplo, su solución "Una cola" requiere que itere sobre la cola para obtener el último valor de entrada para la operación "pop" de la pila. Sin embargo, una estructura de datos de cola no permite iterar sobre los valores, está restringido para acceder a ellos en el primero en entrar, primero en salir (FIFO).
fuente
Como otros ya notaron: no hay una ventaja en el mundo real.
De todos modos, una respuesta a la segunda parte de su pregunta, por qué no simplemente usar una cola, está más allá de Java.
En Java, incluso la
Queue
interfaz tiene unsize()
método y todas las implementaciones estándar de ese método son O (1).Eso no es necesariamente cierto para la lista ingenua / canónica vinculada que implementaría un programador de C / C ++, que solo mantendría punteros al primer y último elemento y cada elemento un puntero al siguiente elemento.
En ese caso
size()
es O (n) y debe evitarse en bucles. O la implementación es opaca y solo proporciona el mínimoadd()
y mínimoremove()
.Con tal implementación, primero tendría que contar el número de elementos transfiriéndolos a la segunda cola, transferir los
n-1
elementos nuevamente a la primera cola y devolver el elemento restante.Dicho esto, probablemente no inventarías algo como esto si vives en tierra de Java.
fuente
size()
método. Sin embargo, en ausencia de unsize()
método O (1) , es trivial que la pila realice un seguimiento de su tamaño actual. Nada para detener una implementación de una cola.Es difícil imaginar un uso para tal implementación, es cierto. Pero la mayor parte del punto es demostrar que se puede hacer .
En términos de usos reales de estas cosas, sin embargo, puedo pensar en dos. Un uso para esto es implementar sistemas en entornos restringidos que no fueron diseñados para ello : por ejemplo, los bloques redstone de Minecraft representan un sistema completo de Turing, que la gente ha utilizado para implementar circuitos lógicos e incluso CPUs completas. En los primeros días de los juegos guiados por guiones, muchos de los primeros bots de juegos también se implementaron de esta manera.
Pero también puede aplicar este principio a la inversa, asegurando que algo no sea posible en un sistema cuando no lo desee . Esto puede surgir en contextos de seguridad: por ejemplo, los sistemas de configuración potentes pueden ser una ventaja, pero todavía hay grados de potencia que preferiría no dar a los usuarios. Esto restringe lo que puede permitir que haga el lenguaje de configuración, para que no sea subvertido por un atacante, pero en este caso, eso es lo que desea.
fuente