UNIDAD 2. ESTRUCTURAS DE DATOS SECUENCIALES 1. Pilas (Stacks) Una pila es una coleccion de elementos en la que sólo se pueden insertar y eliminar datos por uno de los extremos de la lista. Al igual que toda estructura de datos, se consideran algunas operaciones básicas como la inserción y el borrado, que se denominan comúnmente como operación "push" y "pop" respectivamente. Los elementos de una pila se eliminan en orden inverso al que fueron insertados. Es decir, el último elemento que entra a la pila es el primer elemento que se saca. Esta estructura se le conoce como estructura LIFO (Last-In, First-Out / último en entrar - primero en salir). Existen numerosos casos prácticos en los que se utiliza el concepto pila: pila de platos (cuando se necesita un plato limpio siempre se toma el primero de la pila), pila de monedas, pila de latas, etc. Las pilas se pueden representar con arreglos y con listas. En el caso de los arreglos es necesario definir el tamaño máximo, y una variable auxiliar que indique el tope de la pila, en el caso de las listas se manejan espacios dinámicos denominados nodos. Representación de una pila Las pilas se pueden representar en un lenguaje de programación por medio de listas enlazadas o con arreglos, para los cuales se debe definir el tamaño máximo de la pila y una variable auxiliar que debe servir como apuntador al último elemento de la pila llamado Tope. Representación en Memoria Las pilas no son estructuras de datos fundamentales, es decir, no están definidas como tales en los lenguajes de programación, ya que tienen que ser implementadas. Las pilas pueden representarse e implementarse mediante el uso de: Arreglos. Listas enlazadas. 1 A las Pilas que se implementan con arreglos se les denominan Pilas Estáticas, debido a que su tamaño difícilmente será modificado, a no ser que así se implemente. Por lo tanto es importante definir el tamaño máximo de la pila antes de su uso, además es necesario considerar un apuntador al último elemento insertado en la pila (tope) el cual denominaremos Sp (Stak pointer). La representación gráfica de una pila es la siguiente: Como utilizamos arreglos para implementar pilas, tenemos la limitante de espacio de memoria reservada. Una vez establecido un máximo de capacidad para la pila, ya no es posible insertar más elementos. Una posible solución a este problema es el uso de espacios compartidos de memoria. Supóngase que se necesitan dos pilas , cada una con un tamaño máximo de n elementos. En este caso se definirá un solo arreglo de 2*n elementos, en lugar que dos arreglos de n elementos. En este caso utilizaremos dos apuntadores: SP1 para apuntar al último elemento insertado en la pila 1 y SP2 para apuntar al último elemento insertado en la pila 2. Cada una de las pilas insertará sus elementos por los extremos opuestos, es decir, la pila 1 iniciará a partir de la localidad 1 del arreglo y la pila 2 iniciará en la localidad 2n. De este modo si la pila 1 necesita más de n espacios (hay que recordar que a cada pila se le asignaron n localidades) y la pila 2 no tiene ocupados sus n lugares, entonces se podrán seguir insertando elementos en la pila 1 sin caer en un error de desbordamiento Operaciones con pilas Las operaciones principales son poner y quitar elementos: ALGORITMO INSERTAR ELEMENTOS (PUSH) Este algoritmo pone el DATO en PILA. Actualiza el valor de TOPE. MAX es una variable que indica el máximo de elementos que puede almacenar la pila. SI TOPE < MAX TOPE = TOPE + 1 PILA[TOPE] = DATO SI NO “LA PILA ESTA LLENA” FIN SI 2 ALGORITMO ELIMINAR ELEMENTOS (POP) Este algoritmo saca el último elemento de la PILA. El elemento lo guarda en la variable DATO. Actualiza el valor de TOPE. SI TIPE > 0 DATO = PILA[TOPE] TOPE = TOPE – 1 SI NO “PILA VACIA” Aplicaciones con pilas (postfix, prefix, infix) Las pilas son útiles en el tratamiento de recursividad, en llamadas a subprogramas, ordenación y en el manejo de expresiones aritméticas. En este último caso se utilizan para convertir expresiones en notación infija a su equivalente en postfijo o prefijo: Dada la expresión A+B, se dice que esta en notación infija y su nombre se debe a que el operador + se encuentra entre los operandos A y B. Dada la expresión AB+, se dice que esta en notación postfija y su nombre se debe a que el operador + se encuentra después de los operandos A y B. Dada la expresión +AB, se dice que esta en notación prefija y su nombre se debe a que el operador + se encuentra antes de los operandos A y B. La transformación de expresiones de infijo a postfijo y prefijo sigue las mismas reglas de precedencia para los signos de operación, es decir la potencia ($) tiene la mayor precedencia, después el producto y la división, y al final la suma y la resta. Además es importante recordar que las expresiones encerradas entre paréntesis, deberán ser resueltas anticipadamente. Recuerde que las operaciones como la suma, resta, división y producto son asociativas por la izquierda, a diferencia de la potencia que es asociativa por la derecha. A continuación observamos una tabla con ejemplos de conversión de operaciones infijo a postfijo/prefijo: Infijo A+B A+B-C (A+B)*(C-D) A$B*C-D+E/F/(G+H) ((A+B)*C-(D-E))$(F+G) A-B/(C*D$E) Postfijo AB+ AB+CAB+CD-* AB$C*D-EF/GH+/+ AB+C*DE--FG+$ ABCDE$*/- Prefijo +AB -+ABC *+AB-CD +-*$ABCD//EF+GH $-*+ABC-DE+FG -A/B*C$DE Tome en cuenta que la forma prefija de una expresión compleja no es la imagen espejo de la forma postfija, como puede ser visto en el segundo ejemplo de la tabla. La ventaja de utilizar expresiones en notación polaca postfija y prefija radica en que no son necesarios los paréntesis para indicar el orden de operación, ya que este queda establecido por la ubicación de los operadores con respecto a los operando. 3 Las pilas son estructuras de datos muy usadas para la solución de diversos tipos de problemas. Pero tal vez el principal uso de estas estructuras es el tratamiento de expresiones matemáticas representadas en prefijo y postfijo, que es la manera en como las computadoras pueden interpretar las mismas. Los procesos para la conversión de expresiones de infijos a postfijo y prefijo pueden ser fácilmente mecanizados a través de algoritmos computacionales. Conversión de Expresiones en Infijo a Postfijo Considere dos expresiones A+B*C y (A+B)*C, y sus respectivas expresiones postfijas ABC*+ y AB+C*. En cada caso existe un orden especifico en el cual se respetan las precedencias de las operaciones que se tienen que realizar, resulta obvio observar que la precedencia de los operadores es un aspecto muy importante a considerar en el proceso de conversión, por ello asumiremos la existencia hipotética de una función precede( op1, op2 ), donde op1 y op2 son los caracteres que representan los operadores. Esta función regresa un valor verdadero si op1 tiene precedencia sobre op2, y falso para el caso contrario. Por lo que precede( ‘*’, ‘+’ ) regresa verdadero, precede( ‘+’, ‘+’ ) regresa verdadero y precede( ‘+’, ‘*’ ) regresa falso. Además asumiremos las siguientes reglas de precedencia para los paréntesis, ya que las operaciones encerradas entre estos deberán ser realizadas previamente al resto de las operaciones, ya que su precedencia es mayor: precede( precede( precede( precede( ‘(’, op ) = falso op, ‘(’ ) = falso op, ‘)’ ) = verdadero ‘)’, op ) = error // // // // para para para para cualquier cualquier cualquier cualquier operador operador operador operador op op menos ‘)’ op menos ‘(’ op, se considera error A continuación mostraremos el algoritmo correspondiente para convertir expresiones en infijo a postfijo: stackOp = una pila vacía while ( no sea fin de cadena de entrada infija ) { simbolo = siguiente carácter de entrada if ( simbolo es un operando ) agregar simbolo a la cadena postfija else { while ( !vacia(stackOp) && precede(tope(stackOP),simbolo) ) { topeSimbolo = extrae(stackOp) agregar topeSimbolo a la cadena postfija } if ( vacia(stackOp) || simbolo != ')' ) inserta(stackOp, simbolo) else topeSimbolo = extrae(stackOp) } } while ( !vacia(stackOp) ) { topeSimbolo = extrae(stackOp) agregar topeSimbolo a la cadena postfija } Consideremos ahora la evaluación de las dos expresiones mencionadas al principio. 4 Ejemplo 1: cadena infija A + B * C símbolo 1 2 3 4 5 6 7 A + B * C Cadena postfija stackOp A A A A A A A + + +* +* + B B BC BC* BC*+ Las líneas 1,3 y 5 corresponden al deletreo de los operándoos por lo que símbolo es inmediatamente colocado en la cadena postfija. En la línea 2 un operador es deletreado y colocado en la pila que esta vacío. En la línea 4 la precedencia del símbolo * es mayor que la del símbolo colocado al tope de la pila (+); por lo que el nuevo símbolo es colocado en la pila. En la línea 6 y 7 la cadena de entrada es vacía, y la pila es vaciada extrayendo su contenido y colocándolo en la cadena postfija. La colocación de los elementos en la pila stackOp es en dirección derecha izquierda, por lo que los elementos van siendo incrustados del lado derecho. Ejemplo 2: cadena infija (A + B) * C símbolo 1 2 3 4 5 6 7 8 ( A + B ) * C Cadena postfija A A A A A A A B B B B B stackOp ( ( (+ (+ + + +C +C* * * En este ejemplo, cuando el paréntesis del lado derecho es encontrado los elementos de la pila son extraídos hasta que el paréntesis izquierdo es encontrado, en este punto ambos paréntesis son descartados. De esta manera los paréntesis forzan a un orden de precedencia diferente. Evaluación de Expresiones en Postfijo Cada operador en una cadena postfija se refiere a los dos operados previos en la cadena. Suponga que cada vez que leemos un operando colocamos a éste en la pila. Cuando alcanzamos un operador, sus operándoos estarán al tope de la pila. Nosotros podemos extraer estos dos elementos, realizar la operación indicada en ellos y colocar el resultado en la pila de manera que quede disponible para ser usado como operando en la siguiente operación. A continuación presentamos un algoritmo que es capaz de evaluar expresiones postfijas: stackOp = una pila vacía while ( no sea fin de cadena de entrada postfija ) { simbolo = siguiente carácter de entrada if ( simbolo es un operando ) 5 inserta(stackOp, simbolo) else { // el simbolo es un operador op1 = extrae(stackOp) op2 = extrae(stackOp) valor = el resultado de aplicar símbolo a op1 y op2 inserta(stackOp, valor) } } return extrae(stackOp) Suponga que queremos evaluar la siguiente expresión en postfijo: 6 2 3 + - 3 8 2 / + *. La siguiente tabla muestra los valores que se van generando en la evaluación de cada elemento de la cadena. Cada operando es colocado en una pila de operándoos. De tal manera que el numero máximo de elementos de la pila que es el numero de operándoos que aparecen en la entrada de la expresión. Sin embargo el máximo de de elementos en la pila es menor al numero teóricamente necesario, ya que el operador los remueve de la pila. En el ejemplo veremos que la pila nunca contiene mas de cuatro elementos, a pesar de que aparecen seis operándoos en el ejemplo. Tome en cuenta que el ejemplo supone la entrada de la cadena del tipo postfijo. Símbolo 6 2 3 + 3 8 2 / + * op1 2 6 6 6 6 8 3 1 op2 3 5 5 5 5 2 4 7 valor 5 1 1 1 1 4 7 7 stackOp 6 6, 6, 6, 1 1, 1, 1, 1, 1, 7 2 2, 3 5 3 3, 8 3, 8, 2 3, 4 7 2. Colas Una cola es una lista de elementos en la que éstos se introducen por un extremo y se eliminan por otro. Los elementos se eliminan en el mismo orden en el que insertaron. El primer elemento que entra en la cola, es el primer elemento en salir. Las colas también reciben el nombre de estructuras FIFO (First-In, First-Out) Ejemplos, una cola de personas esperando usar el teléfono público, comprar tortillas, una fila de autos en un auto lavado, etc. Las colas son una estructura con muy pocas operaciones disponibles ya que solo permiten añadir y leer elementos y al igual que las pilas, se pueden representar con arreglos y listas. En el caso de los arreglos es necesario definir el tamaño máximo para la cola y dos variables auxiliares; una de ellas para que guarde la posición del primer elemento de la cola (Frente) y otra para que guarde la posición del ultimo elemento (Final). 111 1 ↑ Frente 222 333 2 444 3 ...... 4 ↑ Final Máximo 6 ALGORITMO INSERTAR ELEMENTOS Este algoritmo inserta el elemento Dato al Final de la cola. Frente y Final son variables que indican el inicio y fin de la cola, respectivamente. MAX es el máximo de elementos que puede almacenar la cola. Frente y Final inician en 0. if ( Final < MAX ) { Final = Final + 1 Cola [ Final ] = Dato if ( Final igual a 1 ) { Frente = 1 } } else { “Cola llena” } ALGORITMO ELIMINAR ELEMENTOS Este algoritmo elimina el primer elemento de la cola y lo guarda en la variable Dato. if ( Frente no es igual a 0 ) { Dato = Cola [ Frente ] if ( Frente igual a Final ) { Frente = 0 Final = 0 } else { Frente = Frente + 1 } } else { “COLA VACIA” } Colas circulares Para hacer uso mas eficiente de la posiciones disponible se trata a las colas como una estructura circular. Para ello es necesario realizar varios cambios al esquema inicial, en primer lugar los valores para Frente y Final deberán iniciar ambos en la posición máxima de la cola, debido a que el ultimo elemento de la cola precede inmediatamente al primero al primero dentro de la cola bajo esta representación. En segundo lugar es necesario sacrificar un elemento de la cola para no tener casos absurdos en el que Final y Frente sean la misma posición aún cuando la cola este casi vacía. El algoritmo de inserción deberá contemplar un caso especial donde el Frente es incrementado en una unidad y paso seguido se verifica que Final es igual Frente, en ese caso el valor de Final ya fue incrementado y como se verifico que la cola esta llena, es necesario manipular el valor de Final para que sea reubicado en su posición anterior próxima. 7 ALGORITMO INSERTAR ELEMENTOS Este algoritmo inserta el elemento Dato al final de la cola. Frente y Final son variables que indican el inicio y fin de la cola. MAX es el máximo de elementos que puede almacenar la cola. if ( Final igual a MAX ) Final = 1 else Final = Final + 1 if ( Final igual a Frente ) “Cola llena” else Cola [ Final ] = Dato ALGORITMO ELIMINAR ELEMENTOS Este algoritmo elimina el primer elemento de la cola y lo guarda en la variable DATO. if ( Frente = Final ) “COLA VACIA” else { if ( Frente igual a MAX ) Frente = 1 else { Frente = Frente + 1 Dato = Cola [ Frente ] } } 8