Professional Documents
Culture Documents
Ingeniería de Sistemas
Pilas y Colas
Docente:
Ing. Walter A. Carpio
Arequipa, Perú
2010
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
1 Pilas y Colas
Las pilas y las colas son dos tipos especiales de conjuntos dinámicos que se encuentran
frecuentemente en las aplicaciones informáticas. Antes de exponerlas, resultará útil definir
los términos FIFO Y LIFO. FIFO es una estructura en la que el primero que entra, es el
primero que sale; lo que significa que el primer elemento almacenado en este tipo de esta
estructura será el primer elemento obtenido de ella. Un ejemplo de estructura FIFO es la
cola que se forma en una caja de un supermercado a la hora de pagar. La primera persona
que esté en la cola será la primera persona atendida. Por el contrario, LIFO es una
estructura en la que el último que entra, es el primero que sale. En este caso, el último
elemento que es almacenado en la estructura será el primer elemento obtenido de ella. Un
ejemplo de estructura LIFO es la pila de bandejas de una cafetería-autoservicio.
Habitualmente, las bandejas limpias se colocan encima de esta pila, y de la cima son
tomadas. Así, la última bandeja colocada en la pila será la primera bandeja recogida por un
cliente.
Pág 2
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
En la práctica la operación Desapilar a menudo se implementa de tal forma que devuelve el
elemento insertado más recientemente antes de eliminarlo. Asumiremos que realizar una
operación Cima o Desapilar en una pila vacía produce un error.
A menudo resulta útil pensar en una pila usando la analogía de la cafetería-autoservicio
comentada previamente. Por ejemplo, la Figura 6.1 muestra cómo sería representada una
pila de números enteros. El único elemento al que tenemos acceso es el que ha sido
insertado más recientemente, al que nos referiremos como cima de la pila. En la Figura 6.1,
el elemento insertado más recientemente es 8, y el elemento que lleva más tiempo en la pila
es 2.
No es difícil implementar las operaciones Apilar y Desapilar utilizando tanto una lista con
disposición secuencial, como una lista enlazada. Para ambas formas de lista tenemos dos
opciones básicas: podemos mantener la cima de la pila en la cabeza o en la cola de la lista.
Con una lista con disposición secuencial es más eficiente mantener la cima de la pila en la
cola de la lista. En este caso, en una operación Apilar simplemente añadimos un nuevo
elemento a la cola de la lista, y en una operación Desapilar eliminamos un elemento de la
cola de la lista.
Mantener la cima de la pila en la cabeza de una lista con disposición secuencial no permite
implementar tan eficientemente las operaciones del TAD PILA. Para ver por qué,
recuérdese que insertar un nuevo elemento en la cabeza de una lista requiere que los
elementos de las posiciones 1 hasta n sean ascendidos a las posiciones 2 hasta n + 1.
Además, la eliminación de la cabeza de la lista implica que los elementos de las posiciones
2 hasta n deben ser descendidos. Por tanto, ambas operaciones Apilar y Desapilar
requerirían barrer toda la colección de elementos.
En una implementación con listas enlazadas, los elementos pueden ser almacenados tanto en
la cabeza como en la cola de la lista (asumiendo que almacenamos un puntero a la cola), y
ambas operaciones Apilar y Desapilar aumentan su rendimiento.
Pág 3
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
de la lista, entonces cada operación Avanzar conllevaría descender una posición en la lista a
n - 1 elementos. Similarmente, fijando el último de la cola en la cola de la lista provoca que
cada operación Añadir realice n - 1 desplazamientos.
Una forma de solventar estos problemas es permitir que el primero y el último de la cola se
muevan a través de la lista. Esto es, tratar el primero y el último de la cola como variables
que pueden apuntar a cualquier posición de la lista. Como veremos, esto esencialmente
convierte la cola en una estructura circular que es capaz de «enroscarse» en los extremos de
la lista. Lo más sencillo es demostrar esta estrategia con un ejemplo. Considérese la
implementación de una cola en el array de cuatro elementos Q inicialmente vacío mostrado
a continuación.
Obsérvese que hemos inicializado dos variables, denominadas primero y último, de tal
forma que apuntan a las posiciones del array 0 y 3, respectivamente. Asumiendo que se
añaden a la cola tres elementos con claves 8, 3 y 5 en el orden dado (ejemplo, insertadas
utilizando Añadir), la cola tendría ahora este aspecto:
Antes de cada operación Añadir, la variable último fue incrementada módulo el tamaño del
array antes de insertar el nuevo elemento en la posición apuntada por esta variable.
Una operación Avanzar devolvería el elemento con clave 8, y modificaría la estructura de
datos como sigue:
Realicemos ahora tres operaciones más Añadir(Q, 7), Avanzar(Q) y Añadir(Q, 2). Después
de estas operaciones, la cola tendría este aspecto:
La cuestión interesante a observar aquí es que la cola se enrosca ahora en los extremos del
array.
Pág 4
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
1.2 Pilas y Evaluación de Expresiones
Consideramos ahora el empleo del TAD PILA en la evaluación de expresiones matemáticas
encontradas en el código fuente. Esta sección pretende ofrecer sólo un atisbo de las técnicas
utilizadas por los compiladores para realizar esta tarea.
A continuación se lee el operando ‘+’, así que los dos elementos superiores son desapilados
de la pila y su suma, l0, es apilada de nuevo en la pila tal y como se muestra a continuación:
El siguiente símbolo leído es el operando ‘*’, así que 10 y 8 son desapilados de la pila y su
producto, 80, es apilado de nuevo en la pila:
Pág 5
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
Entonces se encuentra el operando ‘/‘, con lo que 80 y 2 son desapilados de la pila y 80/2 =
40 es apilado en la pila:
Téngase en cuenta que las expresiones postfijas son fáciles de evaluar debido a que no se
tienen que aplicar reglas de precedencia durante la evaluación —no se puede decir lo mismo
de las expresiones infijas. Existe, no obstante, una manera directa de convertir una
expresión infija en una expresión postfija. La técnica que acabamos de presentar puede
entonces ser utilizada para evaluar la expresión postfija resultante,
Pág 6
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
los paréntesis como operadores que tienen una precedencia superior a la de cualquier
otro operador. Además, no permitimos que los paréntesis derechos sean apilados en la
pila, y sólo permitimos que un paréntesis izquierdo sea desapilado después de que haya
sido leído un paréntesis derecho. Téngase en cuenta, no obstante, que los paréntesis no
deben ser llevados a la salida cuando son desapilados de la pila dado que no aparecen en
las expresiones postfijas.
Como demostración de este proceso de conversión, considérese la expresión infija
a * (b + c) + d/e
El primer símbolo leído es a; como es un operando se lleva a la salida. A continuación se lee
el operador ‘*’. Debido a que la pila está vacía en el momento actual, no se desapila ningún
operador y se apila ‘*’, De este modo tenemos
A continuación se lee un ‘(‘, y como todos los operadores tiene una precedencia inferior a la
del paréntesis izquierdo, es inmediatamente apilado en la pila. Entonces se lee ‘ b’ y se lleva
a la salida. Esto produce
El siguiente símbolo leído es el operador ‘+’. Aunque ‘C tiene una precedencia superior que
la de este operador, no puede ser desapilado de la pila hasta que haya sido leído un ‘)’. De
este modo, nada se desapila y el operador ‘+’ es apilado. Después de esto, se lee ‘e’ y se
lleva a la salida:
A continuación se lee ‘)’. Como ‘+’ tiene una precedencia inferior a la de este operador, no
se desapila nada. El símbolo ‘)’ es descartado; sin embargo, cuando se produzca el
momento, seremos capaces de desapilar el paréntesis izquierdo, dado que su paréntesis
derecho correspondiente ha sido leído. El siguiente símbolo que se encuentra es el operador
‘+’. Al leer este símbolo se desapilan todos los símbolos almacenados en ese momento
(ninguno de estos operadores tiene una precedencia menor que ‘+‘), pero sólo los
operadores ‘+’ y ‘*’ son llevados a la salida. El operador más recientemente leído, ‘+’, es
entonces apilado:
A continuación se lee ‘d’ (y se lleva a la salida) seguido por ‘/’. Como la suma tiene una
precedencia inferior a la de la división, el operador ‘/’ es apilado:
Pág 7
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
El símbolo final es ‘e’, que es llevado a la salida. Entonces los restantes operadores
almacenados en la pila son desapilados y llevados a la salida como se muestra a
continuación:
La sentencia de la última línea de este procedimiento es recursiva final dado que no la sigue
ninguna instrucción. Así podemos rescribirlo utilizando un bucle:
OrdenarRápida1(elemento A[] , entero a, entero b)
1 k (Partición(A, a, b)
2 mientras a < b hacer
3 OrdenarRápidal(A, a, k-l)
4 a k + l
Pág 8
Universidad Tecnológica del Perú Ingeniería de Sistemas
Algoritmos y Estructuras de Datos II Pilas y Colas
5 k b + l
En cada iteración del bucle, OrdenarRápidal() es primero invocada remcursivamente sobre
la parte inferior del subarray. En la siguiente iteración, las posiciones índice son
modificadas de tal forma que OrdenarRápidal() es invocada recursivamente sobre la parte
superior del mismo subarray.
Es fácil verificar (ejemplo, dibujando árboles de recursión) que OrdenarRápida() y
OrdenarRápidal() producen exactamente la misma sec uencia de llamadas recursivas. De este
modo, no se ahorra nada en términos del número de llamadas recursivas, a no ser que
también podamos eliminar la otra llamada recursiva que aparece en la línea 3. Esto puede
conseguirse empleando una pila suministrada por el usuario tal y como se muestra a
continuación:
OrdenarRápida2(elemento A[], entero a, entero b)
1 PILA S
2 Apilar(a, S); Apilar(b, S)
3 hacer
4 b Desapilar(S); a Desapilar(S)
5 mientras a < b hacer
6 k Partición(A, a, b)
7 Apilar(k+l, S); Apilar(b, S)
8 b k - l
9 mientras Vacía(S) = falso
En este procedimiento, después de cada llamada a Partición() las posiciones índice
correspondientes a la parte superior del subarray actual son apiladas en la pila. En la
siguiente iteración, el procedimiento Partición() es invocado con entradas correspondientes
a las posiciones índice de la parte inferior del subarray. De este modo, podemos pensar que
la pila de este procedimiento guarda para su tratamiento subsiguiente las partes superiores
de todos los subarrays, mientras las partes inferiores de los subarrays están siendo
particionadas.
Para el procedimiento recursivo inicial, el número de registros de activación almacenados en
la pila de ejecución en cualquier momento puede ser tan grande recorrer toda la colección.
El tamaño de la pila utilizada en el procedimiento final no recursivo sería también tan
grande como recorrer todo en el caso peor de partición. Sin embargo, en OrdenarRápida2()
el tamaño máximo de la pila puede ser mejorado si tenemos cuidado acerca de cuál subarray
es tratado primero. Concretamente, en lugar de colocar siempre las posiciones índices de la
parte superior de un subarray en la pila, colocar en la pila las posiciones índice del mayor
subarray. El menor subarray es entonces particionado en cada iteración.
2 Bibliografía
GREGORY HEILEMAN : “Estructuras de Datos, Algoritmos y POO”
Mc Graw Hill; 1997.
Pág 9