You are on page 1of 9

Universidad Tecnológica del Perú

Ingeniería de Sistemas

Algoritmos y Estructuras de Datos II

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.

1.1 Los TADs Pila y Cola


Una pila es un conjunto dinámico que obedece la propiedad LIFO, mientras que una cola es
un conjunto dinámico que obedece la propiedad FIFO. Esto significa que el orden temporal
de inserciones en estos conjuntos determina completamente el orden en el cual los
elementos son obtenidos a partir de ellos.
Presentamos ahora la colección de operaciones que definen los TADs PILA y COLA.
Aunque estas operaciones son extremadamente simples, se muestran bastante útiles en un
gran número de aplicaciones. Con el fin de de mostrar este hecho, presentamos cierto
número de ejemplos. Un ejemplo de uso de las pilas es para evaluar expresiones
matemáticas, asimismo, las pilas también se utilizan para eliminar llamadas a funciones
recursivas. Tendremos la oportunidad de usar el TAD COLA en la simulación del sistema de
colas de un banco.

1.1.1 Operaciones de las pilas


Dado que una pila es sólo un tipo especial de conjunto dinámico, las operaciones asociadas
con el TAD LISTA pueden utilizarse para implementar una pila. Sin embargo, estas
operaciones reciben nombres especiales cuando se aplican a pilas. En cada una de las
operaciones del TAD PILA que se dan a continuación, x representa a un elemento y S a una
pila arbitraria.
1. Apilar(S, x). Inserta x en S.
2. Cima(S). Devuelve el elemento que fue insertado más recientemente en S.
3. Desopilar(S). Elimina el elemento que fue insertado más recientemente en S.

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.

1.1.2 Operaciones de las colas


Las operaciones ofrecidas por una implementación del TAD LISTA también pueden ser
utilizadas para implementar el TAD COLA. Los nombres especiales de las rutinas de acceso
para COLA se ofrecen más abajo. En cada operación, x de nuevo representa a un elemento y
Q a una cola arbitraria.

1. Añadir(Q, x). Inserta x en Q.


2. Primero(Q). Devuelve el elemento que lleva más tiempo en Q.
3. Avanzar(Q). Elimina el elemento que lleva más tiempo en Q.

En la práctica, la operación Avanzar a menudo se implementa de tal forma que devuelve un


elemento antes de eliminarlo. Asumiremos que realizar una operación Primero o Avanzar en
una cola vacía produce un error, y nos referiremos a los elementos que llevan el mayor y el
menor período de tiempo en la cola como el primero y el último de la cola, respectivamente.
Implementar el TAD COLA supone añadir elementos en un extremo de la lista durante una
operación Añadir, y quitarlos del otro extremo de la lista durante una operación Avanzar.
Esto garantiza que los elementos son accedidos en orden FIFO. Esta estrategia puede ser
implementada eficientemente utilizando una lista enlazada, lo mismo que añadir un
elemento a la cola de una lista enlazada (asumiendo que mantenemos un puntero al nodo
cola como se exponía).
Implementar la estrategia anteriormente mencionada utilizando una lista con disposición
secuencial es, sin embargo, bastante ineficiente. Si el primero de la cola se fija en la cabeza

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.

1.2.1 Expresiones postfijas


En primer lugar consideremos la evaluación de expresiones matemáticas dadas en forma
postfija (ejemplo, notación polaca inversa). En esta notación, los operadores matemáticos
van a continuación de sus operandos. Por ejemplo, la expresión postfija
ab + c *
es equivalente a la expresión estándar (ejemplo, infija)
(a + b) * c
Téngase en cuenta que los paréntesis no son necesarios en ¡a flotación postfija, mientras que
a menudo se necesitan para asegurar el orden correcto de evaluación con e xpresiones infijas.
Si evaluamos una expresión postfija leyéndola de izquierda a derecha, los operandos se
encuentran antes que sus operadores asociados. De este modo, cuando se lee un operador,
debemos ser capaces de volver atrás para encontrar los operandos apropiados. La forma más
fácil de hacer esto es emplear una pila: La expresión postfija es sondeada de izquierda a
derecha, y cada vez que se encuentra un operando, éste es apilado en la pila. Cuando se
encuentra un operador, se aplica a los dos operandos más recientemente apilados en la pila.
Por ejemplo, la expresión postfija.
4873 +*2/+
se evalúa corno sigue: Los primeros cuatro operandos son apilados en la pila en el orden en
el que son encontrados... Esto produce

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

A continuación se apila un 2 en la pila:

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:

Finalmente, se lee un ‘+’ y se desapilan 40 y 4 de la pila, y el resultado final 40 ÷ 4 = 44 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,

1.2.2 Conversión de infija a postfija


El proceso de conversión que vamos a exponer acepta una expresión infija como entrada y
produce una expresión postfija como salida. La idea general es utilizar una pila para
almacenar los operandos conforme son encontrados, para más tarde desapilar estos
operandos de acuerdo a su precedencia. Concretamente, la expresión infija es sondeada de
izquierda a derecha y procesada de acuerdo a las siguientes reglas:
1. Cuando se encuentra un operando se lleva a la salida.
2. Cada vez que se lee un operador la pila es desapilada repetidamente y los operandos se
llevan a la salida, hasta que se encuentre un operador que tenga una precedencia menor
que la del operador más recientemente leído. Entonces se apila el operador más
recientemente leído.
3. Cuando se alcanza el final de la expresión infija todos los símbolos restantes de la pila
son desapilados y llevados a la salida.
4. Como se pueden usar paréntesis para cambiar el orden de evaluación en las expresiones
infijas, debemos incorporarlos en el proceso de conversión, Esto se consigue tratando

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 salida final es la expresión postfija correcta.

1.3 Pilas y Recursión


Existe una aplicación de manejo de programas, mediante la cual se utiliza una pila para
almacenar los puntos de ejecución, de tal forma que se puede volver hacia atrás hasta el
último registro de activación situado en la pila de ejecución, permitiendo al control del
programa volver a la posición adecuada después de completar cada llamada recursiva.
Una técnica habitual utilizada para mejorar el rendimiento en tiempo de ejecución de un
programa consiste en la eliminación de llamadas a procedimientos. Esto es posible gracias
al hecho de que una cantidad significativa de información está almacenada en los registros
de activación producidos por una llamada a procedimiento. En el caso de las llamadas
recursivas, en vez de confiar en la pila de ejecución del sistema, podemos crear una pila en
nuestro programa y realizar manualmente la administración asociada con cada llamada
recursiva. La eliminación de los costes adicionales asociados con estas llamadas recursivas
puede reducir el tiempo de ejecución, así como la cantidad de memoria requerida por el
programa. En esta sección presentaremos una técnica que utiliza una pila para convertir
sistemáticamente un procedimiento recursivo en uno no recursivo (ejemplo, iterativo).
En primer lugar debería mencionarse, no obstante, que una llamada recursiva que aparece en
la última línea de un procedimiento puede eliminarse sin utilizar una pil a. Concretamente,
este tipo de recursión, denominada recursión final, puede ser reemplazada con un bucle. La
recursión final es tan fácil de identificar que a menudo es eliminada automáticamente por
los compiladores. A pesar de este hecho, es una práctica de buena programación eliminarla
manualmente de nuestro código. Considérese, por ejemplo, la rutina OrdenarRápida() :
OrdenarRápida(elemento A[], entero a, entero b)
1 si a < b entonces
2 k  Partición(A, a, b)
3 OrdenarRápida(A, a, k - 1)
4 OrdenarRápida(A, k + l, b)  recursión final

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

You might also like