Your SlideShare is downloading. ×
Tipos de Datos Abstractos.
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×

Saving this for later?

Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime - even offline.

Text the download link to your phone

Standard text messaging rates apply

Tipos de Datos Abstractos.

3,239
views

Published on


0 Comments
2 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
3,239
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
92
Comments
0
Likes
2
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. Capítulo 4: Tipos de Datos Abstractos.1.1 IntroducciónProceso de Programación:Fase de modelación: se expresan ciertos aspectos de unproblema a través de un modelo formal. En esta etapa, lasolución del problema será un algoritmo expresado de maneramuy informal.Fase TDA: Aplicando refinamientos por pasos llegamos a unprograma en pseudolenguaje . Se crean los tipos de datosabstractos para cada tipo de datos (a excepción de los datos detipo elemental). La solución al problema será un algoritmo enpseudolenguaje definido sobre tipos de datos abstractos y susoperaciones.Fase Estructuras de datos: Para cada tipo de datos abstracto seelija una representación y se reemplace por sentencias en C. Elresultado será un programa ejecutable Modelo TDAs Estructuras de Matemático datos algoritmo algoritmo Programa en Informal pseudolenguaje C
  • 2. 1.2 Concepto de TDAIntroducción al concepto de TDAComparación Procedimiento-TDA:• Los procedimientos generalizan el concepto de operador. Evitan al programador limitarse a los operadores incorporados en un lenguaje de programación. El programador es libre de definir sus propios operadores y aplicarlos a operandos que no tienen por qué ser de tipo fundamental. Ej. : multiplicación de matrices.• Pueden utilizarse para encapsular partes de un algoritmo, localizando en una sección de un programa todas las sentencias que incumben a un aspecto del programa en concreto. Un ejemplo de encapsulación es el uso de un procedimiento para leer todas las entradas y verificar su validez.Definición TDA: Es un modelo matemático con una serie deoperaciones definidas sobre ese modelo. Un ejemplo de TDA son los conjuntos de números enteros conlas operaciones de unión, intersección y diferencia.Las operaciones de un TDA pueden tener como operandos nosolo los del TDA que se define, sino también otros tipos deoperandos, como enteros o de otros TDA, y el resultado de unaoperación puede no ser un caso de ese TDA. 2
  • 3. Las propiedades de generalización y encapsulación, sonigualmente aplicables a los tipos de datos abstractos:• Los TDA son generalizaciones de los tipos de datos primitivos (enteros, caracteres,...), al igual que los procedimientos son generalizaciones de operaciones primitivas (suma, resta,...).• Un TDA encapsula cierto tipo de datos pues es posible localizar la definición del tipo y todas sus operaciones en una sección del programa. De esta forma, si se desea cambiar la forma de implementar un TDA, se sabe hacia dónde dirigirse.Una vez definido un TDA, éste se puede utilizar como si fueseun tipo de dato primitivo, sin preocuparse por cual sea suimplementación.Metodología para la definición de un TDA• Definir el dominio del TDA en donde tomará valores una entidad que pertenezca al modelo matemático del TDA.• Definir los efectos que producen en el dominio del TDA cada una de las operaciones definidas. Especificando sintácticamente las operaciones, indicando las reglas que hay que seguir para hacer referencia a una operación. Especificando semánticamente para conocer que significado o consecuencia tiene cada operación. 3
  • 4. Dominio de un TDAFormas para describir el dominio de un TDA:• Si el dominio es finito y pequeño, éste puede ser enumerado. Por ejemplo, el dominio del tipo booleano es {true, false}.• Se puede hacer referencia a un dominio conocido de objetos matemáticos. Por ejemplo, el conjunto de los números negativos.• Se puede definir constructivamente. Enumerando unos cuantos miembros básicos del dominio y proporcionando reglas para generar o construir los miembros restantes a partir de los enumerados. Ejemplo. Dominio de las cadenas de caracteres : 1. Cualquier letra es una cadena. 2. Cualquier cadena seguida de una letra es una cadena.Este tipo de definiciones se llama también definiciones recursivas,ya que hacen referencia a los elementos del dominio definidopara crear nuevos elementos.Especificación sintáctica de un TDAConsiste en determinar como hay que escribir las operacionesde un TDA, dando el tipo de operandos y el resultado. 4
  • 5. El tipo entero se puede especificar sintácticamente,enumerando las siguientes operaciones:+ : entero x entero entero- : entero x entero entero> : entero x entero booleanabs: entero enteroOtra forma más relacionada con la forma de escribir en unlenguaje de programación es: int ‘+’ (int a,b) int ‘-‘ (int a,b) unsigned char ‘>’ (int a,b) int abs (int a)Tomamos esta última como notación para la especificaciónsintáctica de los TDA. La sintaxis de las operaciones vendrádescrita por las cabeceras de las funciones correspondientes acada una de las operaciones.Especificación semántica de un TDA1. Una forma de especificar el significado de las operaciones sería mediante el lenguaje natural. Sin embargo, el uso del lenguaje natural puede dar lugar a ambigüedades.2. Notación algebraica: Consiste en dar un conjunto de axiomas verificados por las operaciones del TDA. 5
  • 6. Ejemplo: Definición del TDA sucesión de forma algebraica• Sintaxis: Sucesión nula: sucesión ‘<>’() Sucesión de un solo elemento: ‘<*>’ (D b) {El significado del asterisco como nombre de una función es que se escribe sustituyendo el asterisco por el valor a que se aplica: <b>} Composición: sucesión ‘ο’ (sucesión a, b) Ultimo: D last (sucesión a) Cabecera: sucesión leader (sucesión a) Primero: D first (sucesión a) Cola: sucesión trailer (sucesión a) Longitud: int length (sucesión a)• Semántica a) last (x ο <d>) = d b) leader (x ο <d>) = x c) x ο (y ο z) = (x ο y) ο z d) first (<d> ο x) = d e) trailer (<d> ο x) = x f) length (<>) = 0 g) length (x ο <d>) = 1 + length (x) h) <> ο x = x ο <> = x i) Si x ≠ <>, y ≠ <>, first (x) = first(y), trailer(x) = trailer(y), entonces x = y j) first (<>) = last (<>) = leader (<>) = trailer(<>) = ERROR 6
  • 7. En las definiciones axiomáticas no se dice nada acerca deldominio del TDA. Los objetos que pertenecen al dominio sonlos que se pueden obtener a través de las operaciones, cuyoresultado se determina a partir de los axiomas.Un elemento de este dominio tendría la forma: <d1> ο<d2>ο....ο<dn>que de forma abreviada escribiremos como <d1,d2, . . ,dn>3. Modelos abstractos: Este método se basa en el hecho de que podemos describir el dominio de un tipo en términos de otro tipo y, entonces, usar las operaciones del segundo tipo para describir las operaciones del que estamos definiendo. Este tipo de especificación se conoce también comoespecificación operacional.Ejemplo: Tipo String.Podemos definir el tipo String como: {a| a=<a1, . . , an>, ai es carácter, n ≤ N}Especificación sintáctica y semánticaVamos a usar una sintaxis similar a la de Java 7
  • 8. En la especificación semántica de cada operación se usará: un lenguaje matemático (basado en este caso en el conocimiento de las sucesiones finitas). precondiciones y postcondiciones. La precondición es la condición previa para poder aplicar una operación y la postcondición especifica el resultado de la operación. String concat (String a, b) pre a=<a1,a2, .. ,an>, b=<b1,b2, .. ,bm> length(a)+length(b)=n+m ≤ N post concat = <a1,a2, . . ,an,b1,b2, . . ,bm> String substr (String a,int f,t) pre a=<a1,a2, . . ,an> f ≥1,t ≤ length(a), f ≤ t post substr= <af, . . ,at>Bajo el cumplimiento de las precondiciones, se podrá ejecutar laoperación y tras su ejecución se debe cumplir la postcondición.No se afirma nada en el caso de que no se cumplan lasprecondiciones, puede dar un resultado indeterminado o error.Lo ideal es que diese error advirtiéndose de esta forma el maluso de la operación. Vamos a utilizar el nombre de la operación para denotar suresultado en la postcondición. 8
  • 9. Uso de los TDA en ProgramaciónLa información, los datos con los que se puede trabajar en unlenguaje de programación están organizados por tipos.El diseñador del lenguaje en cuestión: Comunica estos tipos al usuario de forma abstracta: identificando los objetos y sus operaciones. Diseña un compilador en el que para cada tipo se determina como hay que organizar, interpretar y operar con la información representada en la memoria, de forma que se imite el comportamiento del tipo correspondiente.Con todo esto el programador no tiene que preocuparse decómo se representa un tipo particular de datos y sólo tiene quetrabajar con ellos en base a las especificaciones sintácticas ysemánticas del diseñador.El programador debe realizar una tarea similar a la que hemosdescrito para el constructor del compilador. El proceso sería elsiguiente:1. Determinar, ante un problema, los objetos candidatos a tipos de datos, estén o no estén en el lenguaje de partida.2. Identificar las operaciones básicas, primitivas entre dichos objetos.3. Especificar dichas operaciones 9
  • 10. 4. Construir un programa, que usando estos tipos y estas operaciones resuelva el problema original.5. Implementar los tipos y operaciones que no estén en el lenguaje de partida, con los elementos de dicho lenguaje6. Determinar la eficiencia de la implementación.Con esta metodología se logra disminuir la complejidadinherente a la tarea de construir programas, separando dosproblemas, que se resuelven de forma independiente: Construir un programa a partir de unos objetos adecuados a las características particulares del problema que se quiere resolver. Implementar estos objetos en base a los elementos del lenguaje y razonar sobre ellos.Se debe llevar de forma estricta esta separación: construir el programa, no haciendo ninguna referencia a la implementación usada para los objetos diseñar una implementación para estos sin tener que conocer todos los detalles del problema en el que se vaya a usar.Esta separación nos permite especificar las operaciones yconstruir el programa original sin llegar a implementarlas hastael final. 10
  • 11. Esto nos permite posponer la decisión acerca de laimplementación final hasta una etapa posterior en el desarrollode la solución, en la cual pueda resultar más fácil realizar laelección de la estructura de datos más adecuada.Esta separación en la construcción de los programas se conocecon el nombre de abstracción-realización.La abstracción consiste en la descripción de un determinadosistema, teniendo en cuenta solo las características importantesdel mismo, y no considerando los detalles irrelevantes.La realización consiste en completar los detalles que antes se handejado sin especificar.Para el caso que nos ocupa, la realización de un programa, enbase a unos determinados TDA sería una descripción abstractadel mismo. La implementación de los TDA correspondería a latarea de realización.La construcción de software siguiendo esta metodología nos vaa permitir:1. Construir la solución despreciando detalles de implementación. Esto nos permite obtener la solución en menos tiempo. Para obviar el tipo de implementación que se ha usado necesitamos realizar ocultamiento de la información. 11
  • 12. 2. Los TDA se pueden diseñar en módulos software independientes como pueden ser librerías adicionales con las que finalmente enlazaremos los programas que hacen uso de estos nuevos tipos. De esta manera, disponemos de un nuevo tipo de dato para posibles problemas futuros. Para garantizar la reutilización de los nuevos tipos las operaciones que definamos sobre ellos deben ser completas, es decir, que nos permitan poder realizar sobre ellos todas las operaciones que en el futuro puedan ser necesarias. Sin exceder en el número de operaciones que necesitamos programar realmente. A veces añadir una nueva primitiva puede ser redundante pues tal vez sea posible programarla en base a las demás. Aunque en otras ocasiones puede ser interesante añadir una primitiva por cuestión de eficiencia, pues tal vez accediendo a la implementación de forma directa el resultado acabe siendo más eficiente.Ejemplo de un TDA: TDA PolinomioPara el TDA polinomio podemos enumerar las siguientesoperaciones primitivas:1. crearPolinomio: obtiene los recursos necesarios y devuelve el polinomio nulo. 12
  • 13. 2. grado: devuelve el grado del polinomio.3. coeficiente: devuelve un coeficiente del polinomio.4. asigCoeficiente: asigna un coeficiente del polinomio5. destruirPolinomio: libera los recursos del tipo obtenido.Estas operaciones las podemos clasificar como sigue: • De creación: crearPolinomio • De destrucción: destruirPolinomio • De acceso y actualización: grado, coeficiente, asigCoeficiente.Especificación del TDA polinomioDominio del TDA PolinomioUna instancia P del tipo de dato abstracto polinomio es unelemento del conjunto de polinomios de cualquier grado en unavariable x con coeficiente reales.El polinomio P (x) = 0 es llamado polinomio nulo. P (x) = a0 + a1 x1 + a2 x2 + …+ anxnDonde n es un número natural y ai es un número real. 13
  • 14. EspecificaciónPolinomio crearPolinomio (int maxGrado) pre maxGrado ≥ 0. post devuelve el polinomio nulo. El objetivo de esta función es reservar los recursos necesarios, inicializar con el valor nulo y devolver un tipo polinomio.int grado () pre Polinomio p está inicializado post devuelve el grado del polinomio indicado por p.double coeficiente (natural n) pre Polinomio p está inicializado. n ≤ maxGrado. post devuelve el coeficiente correspondiente al monomio de grado n del polinomio p.void asigCoeficiente (natural n, double c) pre Polinomio p está inicializado. n ≤ maxGrado. post asigna al monomio de grado n el coeficiente c 14
  • 15. Los pasos a seguir para hacer uso de una instancia p de este tiposon:1. Declaración del tipo. Declaramos a p de tipo polinomio (Polinomio p).2. Creación de la estructura.3. Uso de las funciones de acceso y actualización sobre p.4. Destrucción de los recursos usados por p. (Esta operación en Java se realiza de forma automática por el recolector de basura)Ejemplo del uso del TDA Polinomio en una aplicación: Evaluarun PolinomioEn la solución de la evaluación de un polinomio se aprecian dosaspectos importantes de los TDA:1. Construir la solución despreciando detalles de implementación. El TDA Polinomio puede representarse en la forma en que se desee.2. Inicialmente se ha diseñado el TDA polinomio independientemente del problema a resolver. De esta manera, disponemos de un nuevo tipo reutilizable en posibles problemas futuros. 15
  • 16. public double evaluarPolinomio (Polinomio p, double valor)/*Función que dado un polinomio p lo evalúa para un valor x*/ { int i,grado; double resultado,coeficiente; grado=p.grado(); resultado=0.0; for (i=0;i<=grado;i++) { coeficiente= p.coeficiente (i); resultado=resultado+coeficiente*(pow(valor,i)); } return (resultado); }Implementación del TDA polinomio Una vez especificado el TDA Polinomio queda elegir unaimplementación y realizarla.La implementación de un TDA conlleva elegir una estructurade datos para representar el TDA y diseñar en base a laestructura de datos elegida un procedimiento por cadaoperación del TDA.Posibles implementaciones del tipo polinomio:1. Un vector en el que se guarden los distintos coeficientes, de tal forma que el coeficiente i-ésimo se guardará en la posición i del vector. 16
  • 17. Esta implementación tiene el inconveniente de que para determinar el grado el polinomio será necesario recorrer ese vector desde el final hasta el inicio buscando el primer coeficiente distinto de cero, esta operación tendrá un O (n).2. Un registro con dos campos: uno para guardar el grado del polinomio y otro correspondiente a un vector en el que se guardan los coeficientes con el mismo criterio que en la implementación anterior. En este caso la implementación de la operación para determinar el grado será O (1).3. Un registro con tres campos: los dos de la implementación anterior más un campo (pos_min) en el que se indica la posición del polinomio en la que existe el primer coeficiente, comenzando por el término independiente, distinto de cero. Así, el coeficiente que aparece en la posición i del vector corresponde al coeficiente i+pos-min-ésimo del polinomio. Tiene la ventaja de que polinomios del tipo x545 se representen sin desperdiciar el espacio para guardar los primeros 544 coeficientes cero.4. Un registro con dos campos: uno para guardar el grado y otro correspondiente a una referencia a una serie de celdas enlazadas una por cada coeficiente distinto de cero que posea el polinomio, cada una contiene un par coeficiente-grado. 17
  • 18. De esta forma no es necesario limitar a priori el número máximo de coeficientes que posee el polinomio y se utiliza sólo la memoria necesaria para guardar los coeficientes distintos de cero. Como inconvenientes tenemos que operaciones como asigCoeficiente, coeficiente y destruirPolinomio dejan de tener un orden de eficiencia constante.Criterios que suelen prevalecer en la elección de larepresentación:a) Cuáles son las operaciones que se van a realizar con más frecuencia. Con el fin de elegir aquella representación en la que estas operaciones sean más eficientesb) Conocer o no de antemano el tamaño aproximado de los objetos del TDA. Pues las representaciones basadas en estructuras de datos estáticas (como las representaciones 1, 2 y 3 del TDA polinomio definidas anteriormente) limitan el tamaño de los objetos del TDA. En aquellos casos en que no se conozca a priori el tamaño de los objetos del TDA con los que se va a trabajar debemos rechazar este tipo de representaciones.Consideraciones generales para la elección de primitivasEl conjunto de primitivas de un TDA no es único pues esposible optar por un conjunto distinto teniendo en cuenta lossiguientes puntos: 18
  • 19. 1. El conjunto de primitivas debe ser suficiente pero no obligatoriamente mínimo. Puede ser conveniente añadir nuevas funciones si existen motivos: a) La función va a ser probablemente muy usada. b) La función va a ser usada con cierta asiduidad y su implementación haciendo uso de las demás funciones primitivas empeora considerablemente la eficiencia de la operación.2. Puede ser necesario rehacer el conjunto de primitivas atendiendo a razones referentes a una eficiente utilización de los recursos hardware. Este el caso de la función crearPolinomio, la cual en ciertas implementaciones es altamente probable que sea transformada en una función de creación complementada con otra de destrucción.: a) destruir: cuando un polinomio ya no va a ser utilizado, se usa esta primitiva para liberar los recursos de memoria que mantienen la estructura. b) crear: antes de usar un nuevo polinomio, se utiliza esta primitiva para reservar y asociar la memoria necesaria para mantener la estructura del polinomio en memoria. 19
  • 20. 3. Las cabeceras de las funciones pueden necesitar ser modificadas para hacer viable su implementación. Es el caso por ejemplo de una función que no pueda devolver un tipo de dato o que el tipo de dato sea muy complejo y pasarlo por valor o devolverlo como salida de una función pueda convertirse en algo ineficiente debido a su tamaño. Es pues aconsejable no pasar estructuras directamente sino un referencia a ellas, y que una función no devuelva un valor sino que lo devuelva mediante los parámetros a través del paso por referencia.4. Un tipo de dato abstracto es un producto software y como tal es algo dinámico y está sujeto a mantenimiento. Por tanto, el conjunto de primitivas de un TDA es algo extensible. Es conveniente retrasar la incorporación de ciertas primitivas cuya necesidad sea dudosa. Pues desde el punto de vista del mantenimiento del software, es mucho menos costoso la adición de nuevas primitivas que la supresión de algunas ya existentes.1.3 Tipo de datos, estructura de datos y tipo de datos abstracto.Características del Concepto de Tipo1. Un tipo determina la clase de valores que pueden tomar las variables y expresiones.2. Todo valor pertenece a uno y sólo un tipo. 20
  • 21. 3. El tipo de un valor denotado por una constante, variable o expresión puede deducirse de su forma o contexto.4. Cada operador está definido para operandos de varios tipos, y calcula el resultado obteniendo un tipo que esta determinado por el de éstos (usualmente el mismo sí son iguales).5. Las propiedades de los valores de un tipo y de sus operaciones primitivas se especifican formalmente.6. La información del tipo en un lenguaje de programación se usa, por una parte para prevenir errores, y por otra, para determinar el método de representar y manipular los datos en un ordenador.En la mayoría de los lenguajes suelen aparecer como tiposbásicos los proporcionados por el ordenador: enteros, reales,carácter y lógicos.A partir de estos tipos básicos se pueden generar nuevos tiposde datos, aplicando mecanismos de estructuración del lenguajede programación.A estos nuevos tipos se les denomina tipos de datoscompuestos o estructurados (estructuras de datos) 21
  • 22. Estas estructuras de datos pueden servir de base para diseñarestructuras de datos más complejas, y de esta forma construirverdaderas jerarquías de estructuras, pero los componentesúltimos deben ser de tipo básico.Los mecanismos de estructuración que forman parte de lamayor parte de los lenguajes son: array, cadena de caracteres yregistro.Estas estructuras de datos se caracterizan por conocer sutamaño en tiempo de compilación y no variarlo durante laejecución del programa. De ahí que reciban el nombre deestructuras de datos estáticasHay ocasiones en que es deseable aprovechar mejor la memoriasolicitándola conforme sea necesario y liberándola cuando yano haga falta, sin necesidad de reservar una cantidad fija einvariable.Las estructuras diseñadas de este modo reciben el nombre deestructuras de datos dinámicas.Para generar este tipo de estructuras es imprescindible disponerde un método para adquirir posiciones adicionales de memoriaa medida que se necesiten durante la ejecución del programa yliberarlas posteriormente cuando ya no sean necesarias.Este método se conoce como asignación dinámica de memoria ytiene como elemento base al tipo de dato referencia. 22
  • 23. TDA y Estructuras de datosEl diseño de un TDA conlleva dos fases: • una de especificación • y otra de realización.La fase de realización consistirá en representar los TDA enfunción de los tipos de datos y los operadores manejados porese lenguaje, es decir emplearán estructuras de datos. Un TDA se podrá implementar bajo diferentes estructuras dedatos, a la hora de elegir una estructura frente a otras se debentener en cuenta los siguientes principios:1. La eficiencia en tiempo de las operaciones que se utilizarán con mayor frecuencia.2. Las posibles restricciones de espacio en memoria derivadas del uso de la estructura. En el caso de estructuras estáticas se asigna un tamaño fijo para almacenar los objetos del TDA, con ello se limita el tamaño máximo de los mismos y en muchas ocasiones se desaprovecha espacio en memoria. En aquellos casos en que no se conozca de antemano el tamaño aproximado de los objetos del TDA con los que se va a trabajar, no es conveniente una estructura de datos estática. 23
  • 24. No se deben confundir los conceptos de TDA y estructura dedatos. A través de una estructura de datos y sus operacionespodemos definir un tipo de dato abstracto.Existen un conjunto de estructuras de datos que sonampliamente usadas junto con sus operaciones más frecuentes,es el caso de: • Listas • Árboles binarios • Árboles binarios de búsqueda (ABB) • Árboles equilibrados (AVL), • Árboles binarios parcialmente ordenados (APO) • Tablas hash • Grafos.Algunas de estas estructuras de datos (listas y árboles) vamos aestudiarlas y presentarlas como tipos de datos abstractos. 24
  • 25. Cuando en un programa necesitemos manejar objetosestructurados según alguna de las estructuras de datos de quedisponemos, no tendremos más que hacer uso de ellas: • añadiendo el módulo correspondiente a nuestro programa • usando la estructura en base a las especificaciones como tipo de dato abstracto sin necesidad de considerar detalles de implementación.1.4 Tipos de datos abstractos lineales• Listas: que son secuencias de elementos• Pilas: listas donde los elementos se insertan y eliminan solo en un extremo• Colas: listas donde los elementos se insertan por un extremo y se eliminan por el otro.El tipo de datos abstracto “Lista” Las listas constituyen una estructura flexible en particular,porque pueden crecer y acortarse según se requiera.Los elementos son accesibles y se pueden insertar y suprimir encualquier posición de la lista.Las listas también pueden concatenarse entre sí o dividirse ensublistas. 25
  • 26. Suelen ser bastante utilizadas en gran variedad de aplicaciones;por ejemplo en recuperación de información, traducción delenguajes de programación y simulación, …Matemáticamente, una lista es una sucesión de cero o máselementos de un tipo determinado ( que por lo general sedenominará tipo_elemento). Una lista se suele representar de laforma: <a1,a2,…, an>donde n ≥ 0 y cada ai es del tipo tipo_elemento.A n se le llama longitud de la lista. Al suponer que n ≥ 1, sedice que a1 es el primer elemento y an el último elemento. Sin=0, se tiene una lista vacía.En una lista se dice que ai precede a ai+1 para i=1,2,…,n-1, y queai sucede a ai-1 para i =2,3,…,n. Se dice que elemento ai ocupa laposición i.Si la lista tiene n elementos, no existe ningún elemento queocupe la posición n+1. Sin embargo, a esta posición se le llamaposición que sucede al último elemento de la lista, e indicará elfinal de la lista.La posición final de la lista está a una distancia que varíaconforme la lista crece o se reduce con respecto a la primeraposición de la lista, mientras que las demás posiciones guardanuna distancia fija con respecto al principio de la lista. 26
  • 27. Vamos a notar al conjunto de las listas (es decir, al tipo lista)como TLista, al conjunto de los elementos básicos comoTElemento, y al tipo posición como TPosicion.El tipo posición no siempre lo vamos a representar por enteros,y su implementación cambiará con aquella que se haya elegidopara las listas.Especificación de las operaciones primitivas del TDA ListaLas operaciones primitivas propuestas para el tipo de datoabstracto TLista son las siguientes, teniendo en cuenta que listadenota una instancia de TLista. void anula () post (lista) = <> TElemento primero () pre lista está inicializada. post primero = 0 {Si lista está vacía, la posición que se devuelve es fin () } TElemento fin () pre lista está inicializada. post fin = n+1 27
  • 28. {posición detrás de la última}void insertar (TElemento x,TElemento ap) pre lista = <a1,a2,…,ap,...,an> 1 ≤ p ≤ n+1 post lista = <a1,…,ap-1,x,ap,…,an> Si (p= fin (lista)) entonces lista = <a1,a2,…,an,x>{resulta una lista de longitud n+1, en la que x ocupa laposición p. Si la lista lista no tiene posición p, el resultadoes indefinido}void borrar (TElemento ap) pre lista = <a1,a2,… ap,...an> 1≤p≤n post lista = <a1,…,ap-1,ap+1,…,an>{se elimina el elemento que ocupaba la posición p. Ahorala posición p la ocupa el elemento que se encontraba en laposición p+1. El resultado no está definido si lista no tieneposición p o si p = fin (lista)}TElemento elemento (TPosicion p) pre lista = <a1,a2,…,an> 28
  • 29. 1≤p≤n post elemento = ap{devuelve el elemento que está en la posición p de la listalista. El resultado no está definido si p = fin (lista) o si listano tiene posición p}TElemento siguiente (TElemento ap) pre lista = <a1,a2,... ,ap, ap+1,...,an> 1≤p≤n post siguiente = ap+1{devuelve la posición siguiente a p en la lista lista. Si p es laúltima posición de lista, siguiente (p, lista) = fin (lista). Elresultado no está definido si p = fin (lista), o si la lista listano tiene posición p}TElemento anterior (TElemento ap) pre lista = <a1,a2, ... ,ap-1 ,ap …,an> 2 ≤ p ≤ n+1 post anterior = ap-1{devuelve la posición anterior al elemento ap en lista. Elresultado no está definido si p=1 , o si lista no tieneposición p} 29
  • 30. Tposicion posicion (TElemento x) pre lista está inicializada post Si ∃ j ∈ {1,2,…,n}, tal que aj = x, entonces posicion = i, donde i verifica que: 1. ai = x 2. Si aj = x, entonces j ≥ i. Si no existe j ∈ {1,2,…,n}, tal que aj = x, entonces posicion = n+1 {devuelve la posición de la primera aparición de x en lista. Si x no figura en la lista entonces se devuelve fin (lista)}Basándonos en la especificación realizada, consideraremos unaaplicación típica: eliminar todos los elementos repetidos de unalista.Los elementos de la lista son de tipo TElemento y existe unafunción lógica, igual (x,y), que nos dice cuando son iguales doselementos de este tipo.Con estas consideraciones, el procedimiento para eliminar lasrepeticiones de una lista sería: void elimina (TLista lista) { TPosicion p,q; for (p = lista.primero ();p!= lista.fin ();p= lista.siguiente (p)) { q=lista.siguiente(p); 30
  • 31. while (q!=lista.fin ()) if (igual (lista.elemento (p),lista.elemento (q))) lista.borrar (q); else q=lista.siguiente(q); } }Las variables p y q se usan para representar dos posiciones en lalista.Dado el elemento de la posición p se eliminan todos loselementos iguales a él que se encuentren a la derecha de suposición, usaremos la posición q para recorrer los elementos ala derecha de p.Cuando se elimine el elemento de la posición q los elementosque estaban en las posiciones siguientes retroceden unaposición en la lista, en estos casos no será necesario pasar a laposición siguiente de q, nos quedaremos en la misma posiciónq.Implementación de las listasImplementación de las listas mediante vectoresEn la realización de una lista mediante un vector, los elementosde ésta se almacenan en celdas contiguas de un vector. 31
  • 32. Como las listas tienen longitud variable y los vectores longitudfija, debemos tomar vectores de tamaño igual a la longitudmáxima de la lista y utilizar un entero que nos indique laposición del último elemento de la lista.Definiremos el tipo TLista como un objeto con dos variablesmiembro:• La primera un vector que tiene la longitud adecuada para contener la lista de mayor tamaño que se pueda presentar.• El segundo campo es un entero que indica la posición del último elemento de la lista en el vector.El i-ésimo elemento de la lista está en la (i-1)-ésima celda delvector. Las posiciones en la lista se representan medianteenteros.Para esta realización basada en vectores, el tipo TLista se va adefinir con la ayuda de un interfaz que marca como debe sercualquier objeto que se utilice como una lista.El TElemento será la clase general Object puesto que todos losobjetos en Java pertenecen a la clase Object.El TPosicion será una variable de tipo Object que en la prácticapodrá ser un Integer, que nos representa perfectamente laposición de un objeto dentro de una lista.El interfaz TLista define como sigue: public interface TLista{ 32
  • 33. Object primero(); Object fin(); boolean insertar(Object x, Object ap); boolean borrar (Object ap); Object elemento(Object p); Object siguiente(Object ap); Object anterior (Object ap) Object posicion (Object ap) } La implementación de un TLista utilizando una estructuradel tipo java.util.Vector está en la clase Lista, definida acontinuación: public class Lista implements TLista{ public static final int LMAX=100; Vector vector = new Vector(LMAX); int n=-1; ... } 0 Primer Elemento 1 . Segundo Elemento . .. . n . Último Elemento . tamMax-1 Elementos 33
  • 34. La implementación de la mayoría de los métodos es inmediata.Los métodos más simples son: Object primero (){ if (n>=0) return (new Integer(0)); else return null; } Object fin (){ return (new Integer(n+1)); } Object siguiente (Object ap){; int i=((Integer)ap).intValue(); if (i<n) return (new Integer (i+1)); else return null; } Object anterior (Object ap){ int i=((Integer)ap).intValue(); if (i>0) return (new Integer(i-1)); else return null; } 34
  • 35. Object elemento (Object ap){ int i=((Integer)ap).intValue(); if ((i < 0) || (i > n)){ //La posición no está en la lista return null; } return vector.elementAt(i); }La función posicion tiene que realizar una búsqueda secuencialen un vector. En el caso de que el elemento no esté en el vectorla función devolverá la siguiente posición a insertar. Object posicion (Object p){ int index=0; while (index<=n){ if (vector.elementAt(index).equals(p)) return (new Integer(index)); index++; } return (new Integer(n+1)); }Para insertar un elemento en la posición p de la lista se debedesplazar una posición dentro del vector a todos los elementosque siguen al elemento de la posición p, con el fin de hacerpreviamente un hueco donde realizar la inserción. boolean insertar (Object x, Object p){ if (n== Lista.LMAX){ return false; //La lista está llena } 35
  • 36. else { /*Desplaza los elementos en p,p+1, … una posición hacia abajo*/ int posP = ((Integer)posicion(p)).intValue(); for (int q=n;q>=posP;q--) vector.add(q+1, vector.elementAt(q)); vector.add(posP,x); return true; } }La eliminación de un elemento, excepto en el caso del último,requiere desplazamientos de elementos para llenar el vacíoformado. boolean borrar (Object p){ int posP = ((Integer)posicion(p)).intValue(); if(posP==n+1){ return false; //La posición no está en la lista } else { /*Desplaza los elementos en p+1,p+2,… una posición hacia arriba*/ for (int q=posP;q<n-1;q++) lista.add(q,lista.elementAt(q+1)); lista.add(n, null); return true; } }Estas tres últimas operaciones tienen una eficiencia del ordendel tamaño de la lista, pues en el peor de los casos tendrían querecorrer la lista por completo para realizar la operación encuestión. 36
  • 37. Inconveniente de esta implementación:• las listas tienen un tamaño máximo del que no se puede pasar en contra de la especificación en la que no se le impone ningún límite al tamaño de las listas.• Siempre hay una cantidad de espacio reservada para los elementos de la lista desperdiciada al ser el tamaño de la lista, en un momento dado, menor que el tamaño máximo. Este problema se acentúa si las distintas listas que se representan son de un tamaño muy dispar, en este caso no es conveniente el uso de esta representación.Para mejorar esta implementación incorporamos un parámetroen el constructor, que va a determinar el tamaño máximo de lalista.La versión optimizada sería: public class Lista{ public int LMax; Vector vector; int n; public Lista (int tamMax) { // Constructor LMax = tamMax; vector = new Vector(tamMax); 37
  • 38. n = -1; } ... }Las demás primitivas quedarían de la misma formasustituyendo LMAX por LMax. En esta nueva implementaciónconseguimos resolver con éxito dos cosas:1. Tamaños variables. Ahora a la primitiva encargada de crear el objeto (constructor) se le pasa un parámetro indicando el tamaño máximo que tendrá la lista. Se mejora con respecto a la versión anterior en la que el tamaño máximo tenía que ser superior a la más grande de las listas que se manejan y por consiguiente para pequeñas listas habría una gran cantidad de memoria desperdiciada.2. Creación y destrucción. En esta versión se ofrece el constructor y el destructor del tipo de dato permitiendo de esta forma recuperar los recursos ocupados por las listas que no se volverán a usar. El uso de la constructor (new) sobre una lista ya creada provocará una pérdida de los recursos de memoria ocupados por esta lista y la actualización de su valor a la lista vacía. Tras la destrucción de una lista, se podrá usar de nuevo la misma variable en la creación, uso y destrucción de una nueva lista. 38
  • 39. En JAVA no es necesario destruir los objetos ya que, cuando salen de su ámbito, son marcados para ser eliminados, de lo cual se encarga el “garbage collector” (recolector de basura). En caso de que fuera necesario realizar alguna tarea en el momento de la destrucción de un objeto, deberá especificarse en el método void finalize()Una vez implementado el TDA Lista estará disponible para serusado siguiendo los siguientes pasos: 1. Declaración de la variable de tipo Lista. 2. Creación de la lista mediante su constructor. 3. Uso de la lista mediante primitivas distintas a la de creación y destrucción. 4. Destrucción de la lista mediante su constructor (opcional).Al escribir los programas en función de TDAs, es posiblemodificar las estructuras de datos de los programas másfácilmente con sólo aplicar de nuevo las operaciones.La flexibilidad que se obtiene con esto, puede ser especialmenteimportante en proyectos grandes, aunque no sea tan evidenteen los ejemplos pequeños. 39
  • 40. Implementación de listas mediante celdas enlazadas porreferenciasEn esta implementación se utilizan referencias para enlazarelementos consecutivos, evitando el empleo de memoriacontigua para almacenar una lista y eludiendo losdesplazamientos de elementos para hacer inserciones yeliminaciones.No obstante, hay que pagar el precio de un espacio adicionalpara las referencias.En esta representación, una lista está formada por celdas; cadaelemento ai de una lista <a1,a2,…,an>, se representa por unacelda dividida en dos partes:• un primer campo que contiene al elemento ai de la lista• un segundo campo donde se almacena una referencia a la celda que contiene al siguiente elemento ai+1.La celda que contiene a an posee un referencia a NULL(referencia nulo), para indicar el final de la lista. a1 a2 anPara realizar más fácilmente las operaciones es convenienteconsiderar una celda inicial, llamada cabecera donde no sealmacena ningún elemento de la lista. 40
  • 41. La lista vendrá representada por un referencia que indique ladirección de la cabecera En el caso de una lista vacía, elreferencia cabecera será NULL. Cabecera a1 an LLa posición i será un referencia pero no a la celda que contieneal elemento ai , sino a la celda donde está el elemento anterior ai.Con esto se puede acceder al elemento y también se facilita eldiseño de las operaciones de inserción y borrado.La posición del primer elemento será una referencia a la celdacabecera o inicial, y la posición fin() es un referencia a la últimacelda de la estructura.La definición de Celda es como sigue:public class Celda{ public Celda sig; public Object elemento;}La definición de la clase TDA que implementa el TLista sería:public class Lista implements TLista{ Celda cabecera;...} 41
  • 42. Los métodos se detallan a continuación: public Lista (){ // Constructor de lista cabecera = new Celda(); cabecera.sig = null; } Object fin (){ Celda pos = cabecera; while (pos.sig!=null) pos=pos.sig; return pos; }Este método es ineficiente pues se requiere recorrer toda la listapara devolver el referencia a la última celda de la lista. Sueficiencia es pues del orden de la longitud de la lista.Si en las aplicaciones que utilizan el tipo lista se va a utilizarcon frecuencia esta operación puede optarse por cualquiera delas opciones siguientes:1. Sustituir el uso de fin() donde sea posible. Por ejemplo, en un ciclo while con una condición del tipo p !=fin () se debería sustituir por: q= fin(); while (p!=q) .... donde q es de tipo Celda y fin() no se ve afecta de ninguna forma en el interior del ciclo. 42
  • 43. 2. Considerar listas con un referencia a la cabecera y otra a la posición fin(), aunque esto complicaría ligeramente algunas operaciones, pero se ganaría mucha eficiencia en esta función.void insertar (Object x, Object ap){ Celda c=new Celda(); c.elemento=x; c.sig=ap.sig; ap.sig= c;}Mecánica del manejo de referencias en la función inserta: ai ai+1 ( a ) Situación inicial ap ( b ) Asignaciones de x ai ai+1 x ap x ( c ) Asignación de ap ai ai+1 x ap x 43
  • 44. Sobre este método cabe señalar varias cosas:1. Tarda siempre un tiempo constante frente a la implementación vectorial en que se tardaba un tiempo proporcional a la longitud de la lista.2. No se comprueba la precondición pues se perdería mucho tiempo en la comprobación, y dejaría de ser tan eficiente la operación. Es responsabilidad del programador utilizarla siempre con posiciones de esta lista. Si no se hace así, se puede dar lugar a incongruencias.3. Gracias al uso de la celda cabecera no es necesario distinguir en que posición se realiza la inserción. El procedimiento funciona bien en los casos extremos de la primera posición y fin () posición. En listas sin cabecera estos casos habrían sido necesarios considerarlos aparte, complicando la realización de la operación Object siguiente (Object ap){ if (ap.sig==null){ return null; //No hay siguiente al fin de la lista } return (ap.sig); } 44
  • 45. Celda primero (){ return (cabecera); } Object posicion (Object x){ Celda p; boolean encontrado; p=primero (); encontrado=false; while ((p.sig!=null)&&(!encontrado)) if (p.sig.equals(x)) encontrado=true; else p=p.sig; return (p); }Respecto a esta función es conveniente destacar:a) La función cumple la postcondición en los dos casos posibles: cuando éste y no éste el elemento buscado en la lista.b) La complejidad es la misma que para la implementación mediante vectores.c) En la condición del bucle aparece la comparación (p.sig!=NULL). Esta es equivalente a (p!=fin()), pero no se utiliza esta última pues aumentaría mucho la complejidad debido a la ineficiencia ya comentada de la función fin (). 45
  • 46. No se debe sustituir en cualquier programa esta última condición por la que hemos usado aquí, pues iría en contra de todo lo comentado sobre la construcción de programas haciendo uso de TDAs. Object elemento (Object p){ return p.sig.elemento; } boolean borrar (Object ap) { if (ap.sig==null){ return false; } else { ap.sig=ap.sig.sig; return true; } }Mecánica de referencias que se realizan en esta operación ai ai+1 ai+2 apVentajas de esta representación:• ya no se acota el tamaño máximo de las listas.• las operaciones de inserción y borrado que en la anterior representación tenían un O(n), donde n es la longitud de la 46
  • 47. lista, en esta representación requieren únicamente un tiempo constante.Inconvenientes de esta representación:• requiere un espacio adicional para la referencia de cada celda. Si conocemos el tamaño aproximado de las listas que se van manejar podría ser más conveniente representar el tipo lista mediante vectores, con el consiguiente ahorro de espacio en memoria.Comparación de los métodosAnte una aplicación que necesite el uso del TDA listas hay quedeterminar si es mejor usar una implementación de listasbasadas en celdas enlazadas o en vectores.La decisión dependerá de las operaciones que se deseenrealizar, o de las que se realizan más frecuentemente. Otrasveces la decisión es en base a la longitud de la lista.Los puntos principales a considerar son los siguientes:1. La implementación mediante vectores requiere especificar el tamaño máximo de una lista en el momento de la compilación. 47
  • 48. Si no es posible acotar la longitud de la lista, posiblemente deberíamos escoger una representación basada en referencias. Este problema ha sido parcialmente solucionado con la parametrización del tamaño máximo de la lista, pero aún así hay que delimitar el tamaño máximo para cada una de las listas.2. Ciertas operaciones son más lentas en una realización que en otra. Por ejemplo, insertar y borrar realizan un número constante de pasos para una lista enlazada, pero necesitan un tiempo proporcional al número de elementos de la lista para la representación basada en vectores. Inversamente, la ejecución de anterior y fin requiere un tiempo constante con la representación mediante vectores, pero un tiempo proporcional a la longitud de la lista si usamos la implementación por referencias simplemente- enlazadas3. La realización con vectores puede malgastar espacio, pues usa la cantidad máxima de espacio independientemente del número de elementos presentes en la lista en un momento dado. 48
  • 49. La implementación por referencias utiliza sólo el espacio necesario para los elementos que actualmente tienen la lista, pero necesita espacio adicional para los referencias de cada celda.4. En las listas enlazadas la posición de un elemento se determina con un referencia a la celda del elemento anterior por lo que hay que tener cuidado con la operación de borrado si se trabaja con varias posiciones consecutivas.Listas Doblemente-EnlazadasEn algunas aplicaciones puede ser deseable recorrereficientemente una lista, tanto hacia delante como hacia atrás.O, dado un elemento, podría desearse determinar con rapidezel siguiente y el anterior.En tales situaciones podríamos desear añadir a cada celda deuna lista un referencia a la anterior celda tal y como se muestra:Otra ventaja de las listas doblemente enlazadas es que permitenusar un referencia a la celda que contiene el i-ésimo elementode una lista para representar la posición i, en vez de usar elreferencia a la celda anterior, que es menos natural. 49
  • 50. El único precio que se paga por estas características es lapresencia de un referencia adicional en cada celda yconsecuentemente procedimientos algo más lentos para algunade las operaciones básicas de las listas.El tiempo para todas las operaciones es constante, excepto parala operación posición que requiere un tiempo proporcional a lalongitud de la lista.La declaración de una lista doblemente enlazada sería: public class Celda{ public Object elemento; public Celda sig, ant; }Un procedimiento para borrar un elemento en la posición p deuna lista doblemente-enlazada es: boolean borrar (Object p){ if (p.ant!=null) p.ant.sig=p.sig; if (p.sig!=null) p.sig.ant=p.ant; }Los cambios sufridos por los referencias en este procedimientoson: 50
  • 51. Para evitar las verificaciones sobre si la celda a suprimir es laprimera o la última, se suele hacer que la primera celda de lalista doblemente_enlazada sea una celda que efectivamentecierre el círculo, es decir, que el campo ant de esta celda apuntea la última celda y el campo sig de la última celda apunte a laprimera.PilasUna pila es un tipo especial de lista en la que todas lasinserciones y borrados tienen lugar en un extremo denominadotope.A las pilas se les llama también “listas LIFO” (last in first out) olistas “último en entrar, primero en salir”.El modelo intuitivo de una pila es precisamente un mazo decartas puesto sobre una mesa, o de platos en una estantería,situaciones todas en las que sólo es conveniente quitar oagregar un objeto del extremo superior de la pila, al que sedenominará en lo sucesivo tope.Especificación de las operaciones primitivas del TDA Pila Vamos a notar al tipo pila como TPila, al conjunto de loselementos básicos como TElemento .Un tipo de dato abstracto pila suele incluir a menudo lassiguientes operaciones: 51
  • 52. void anula () post (P)=<>{Esta operación es la misma que la de las listas generales}boolean vacia () pre P está inicializada post Si (P = <>) entonces vacia = true sino vacia = falseTElemento tope () pre no vacia (P) post tope = a1{Devuelve el elemento en la cabeza de la pila P. Estaoperación puede escribirse en términos de las operacionesde listas como elemento(primero(P),P) , pues la cabeza deuna pila se identifica con la posición 1}void push (TElemento x) pre P=<a1,a2,…,an> post P = <x,a1,a2,…,an>{inserta el elemento x en el tope de la pila P. En términosde primitivas de listas esta operación esinserta(x,primero(P),P)}void pop () pre no vacia (P) P=<a1,a2,…,an> post P = <a2,…,an> 52
  • 53. {Borra el elemento del tope de la pila P. En términos deprimitivas de listas Borra (primero (P),P). Algunas veces esconveniente implementar pop como una función que devuelveel elemento que acaba de borrar}Al igual que se hizo con la operación anula para las listas, en laimplementación del tipo pila esta función también serátransformada en una función de creación que se verácompletada con otra nueva de destrucción.Implementación de las pilasTodas las implementaciones de listas que hemos descrito sonválidas para las pilas ya que una pila junto con sus operacioneses un caso especial de una lista con sus operaciones.Sin embargo las operaciones de las pilas son más específicas ypor tanto la implementación puede ser mejorada especialmenteen el caso de la implementación basada en vectores.Implementación de pilas basadas en vectoresLa implementación basada en vectores para las listas, no esparticularmente buena para las pilas, porque cada push o poprequiere mover la lista entera hacia arriba o hacia abajo.Estas operaciones requieren un tiempo proporcional al númerode elementos en la pila. 53
  • 54. Un mejor criterio para usar un array es tener en cuenta que lasinserciones y las supresiones ocurren sólo en la parte superior.Se puede anclar la base de la pila a la base del array (el extremode índice más bajo) y dejar que la pila crezca hacia el últimoelemento del array.Un cursor llamado tope indica la posición actual del primerelemento de la pila. 0 . Último Elemento . . . . . Segundo Elemento Tope Primer Elemento tamMax-1 ElementosEl interfaz TPila se define como sigue: Public interface TPila{ boolean vacia (); Object tope(); boolean push (); boolean pop(); }La implementación de un TPila utilizando una estructura deltipo java.util.Vector está en la clase Pila, definida acontinuación: 54
  • 55. Public class Pila implements TPila{ public int LMax; Vector vector = new Vector (LMAX); int tope; public Pila (int tamMax){// Constructor Lmax =tamMax; vector= new Vector (tamMax); tope =-1; ... }La implementación de la mayoría de los métodos es inmediata: boolean vacia () { return (tope == -1); } Object tope () { if (vacia (){ // la pila está vacía return null; } return vector.elementAt(tope); } boolean pop () { if (vacia ()){ 55
  • 56. // la pila está vacía return false; } tope--; return true; } boolean push (Object x) { if (tope == Lmax-1){ // la pila está llena; return false; } tope++; vector.add(tope)=x; return true; }Implementación de Pilas mediante celdas enlazadasPara esta representación las operaciones push y pop operan sólosobre el primer elemento y no existe la noción de posición.La definición de Celda es como sigue: public class Celda{ public Celda sig; public Object elemento; } 56
  • 57. P a1 an Cabecera tope de la pila fondo de la pilaLa definición de la clase Pila que implementa el TPila sería: public class Pila implements Tpila{ Celda Cabecera; ... }Los métodos se detallan a continuación: public Pila (){ //Constructor de Pila cabecera=new Celda (); cabecera.sig=null; } bolean vacia () { return (Cabecera.sig==null); } boolean pop () { if (vacia ()){ // la pila está vacía return false; } Cabecera =Cabecera.sig; 57
  • 58. } Object tope () { if (vacia ()){ //la pila está vacía return null; } return (Cabecera.sig.elemento); } boolean push (Object x) { Celda c=new Celda (); c.elemento = x; c.sig=Cabecera.sig; Cabecera.sig=c; }ColasUna cola es otro tipo especial de lista en el cual los elementos seinsertan en un extremo (el posterior) y se suprimen en el otro(el anterior o frente).Las colas se conocen también como “listas FIFO” (first-in first-out) o listas “primero en entrar, primero en salir”.Las operaciones para una cola son análogas a las de las pilas; lasdiferencias sustanciales consisten en que las inserciones sehacen al final de la lista, y no al principio, y en que laterminología tradicional para colas y listas no es la misma. 58
  • 59. Especificación de las operaciones primitivas para el TDA ColaVamos a notar al tipo cola como TCola y al conjunto de loselementos básicos como TElemento. Las primitivas que vamos aconsiderar para las colas son las siguientes: void anula () post (C)=<> {Esta operación es la misma que la de las listas generales} boolean vacia () pre C está inicializada post Si (C = <>) entonces vacia = true sino vacia = false TElemento frente () pre no vacia () post frente = a1 {devuelve el valor del primer elemento de la cola C. Se puede escribir en función de las operaciones primitivas de las listas como: elemento (primero (C),C)} int poner_en_cola (TElemento x) pre C =<a1,a2,…,an> post C =<a1,a2,…,an,x> poner_en_cola True si consigue poner en cola {inserta el elemento x al final de la cola C. En función de las operaciones de las listas sería: inserta(x, fin (C),C)} 59
  • 60. int quitar_de_cola () pre no vacia () C =<a1,a2,…,an> post C =<a2,…,an> quitar_de_cola Trae si consigue quitar de cola {suprime el primer elemento de C. En función de las operaciones de las listas sería: borra (primero ( C),C)}Implementación de las colas basada en celdas enlazadasComo en el caso de las pilas, cualquier realización de listas eslícita para las colas.No obstante, para aumentar la eficiencia de pone_en_cola esposible aprovechar el hecho de que las inserciones se realizansólo en el extremo posterior y mantener un referencia al últimoelemento.Como en las listas de cualquier clase, también se mantiene unareferencia al frente de la lista; en las colas, esa referencia es útilpara ejecutar mandatos del tipo frente o quita_de_cola.Al igual que para las listas, utilizaremos una celda cabecera conel referencia frontal apuntándola. Esta convención permitemanejar convenientemente una cola vacía. ant post Cab a1 an C 60
  • 61. Para esta realización se va a definir el tipo TCola con la ayudade un interfaz que marca como debe ser cualquier objeto que seutilice como una cola. public interface TCola{ boolean vacia(); Object frente (); boolean poner_en_cola (Object p); boolean quitar_de_cola(); }Una cola es pues un referencia a una estructura compuesta pordos referencias, uno al extremo anterior de la cola y otro alextremo posterior.La primera celda es una celda cabecera cuyo campo elemento seignora. La definición de tipo es la siguiente: public class Celda{ Object elemento; Celda sig; } public class Cola implements TCola { Celda ant, post; //Entran por post y salen por ant … } 61
  • 62. Con esta definición del tipo cola, la implementación de lasprimitivas es la siguiente: public Cola () { //Constructor ant=new Celda(); ant.sig=null; post=ant; } boolean vacia (){ return (post.sig==null); } Object frente (){ //Elemento que entró primero (el más antiguo) if (vacia ()){ return null; } return (ant.sig.elemento); } boolean poner_en_cola (Object x){ Celda c=new Celda(); c.sig=null; c.elemento=x; post.sig=c; post=c; if (post.sig!=null) post.sig.sig=c; if (ant.sig==null) ant.sig=c; post.sig=c; return true; } void quitar_de_cola (){ 62
  • 63. if (ant.sig!=null){ ant.sig=ant.sig.sig; return true; } else return false; }El procedimiento quitar_de_cola suprime el primer elemento dela cola C deconectando la cabecera antigua de la cola, de formaque el primer elemento de la cola se convierte en la nuevacabecera. C ant post (a) C = crear ( ) (b) poner_en_cola (x,C) poner_en_cola (y,C) C ant post x y NULL (c) quitar_de_cola (C)C ant post x y NULL 63
  • 64. Implementación de las colas usando matrices circularesLa implementación de listas por medio de matrices puedeusarse para las colas, pero no es muy eficiente.Con un referencia al último elemento es posible ejecutarponer_en_cola en un tiempo constante.Pero quitar_de_cola, que suprime el primer elemento, requiereque la cola completa ascienda una posición en la matriz con loque tiene un orden de eficiencia lineal proporcional al tamañode la cola.Para evitarlo se puede adoptar un criterio diferente.Imaginemos a la matriz como un círculo en el que la primeraposición sigue a la última. tamMax-1 0 0 1 post ant ColaPara insertar un elemento en la cola se mueve el referencia postuna posición en el sentido de las agujas del reloj y se escribe elelemento en esa posición. 64
  • 65. Para suprimir un elemento simplemente se mueve ant unaposición en el sentido de las agujas del reloj.De esta forma, la cola se mueve en es e sentido conforme seinsertan y suprimen elementos.Utilizando este modelo los procedimientos poner_en_cola yquitar_de_cola se pueden implementar de manera que suejecución se realice en tiempo constante.Existe un problema y es que no hay forma de distinguir unacola vacía de una que llene el círculo completo salvo quemantengamos un bit que sea verdad si y solo si la cola estávacía.Si no deseamos mantener este bit debemos prevenir que la colallene alguna vez la matriz.Una cola vacía tiene post a una posición de ant en el sentido delas agujas del reloj, que es exactamente la misma posiciónrelativa que cuando la cola tenía tamMax elementos.Así cuando la matriz tenga tamMax casillas, no podemos hacercrecer la cola más allá de tamMax-1 casillas, a menos queintroduzcamos un mecanismo para distinguir si la cola estávacía o llena. 67
  • 66. post post ant ant Cola Llena Cola vacíaLas primitivas de las colas usando esta representación sedescriben a continuación: public class Cola implements TCola{ int LMax; Vector vector; int ant, post; public Cola(int tamMax){ //Constructor LMax = tamMax+1; vector = new Vector(LMax); ant=0; post=LMax-1; } boolean vacia () { return ((post+1)%(Lmax)==ant); } 68
  • 67. Object frente () { if (vacia())){ return null; } else return (vector.elementAt(ant)); } boolean poner_en_cola (Object x) { if ((post+2)%(LMax)==ant){ //La cola está llena return false; } else { post=(post+1)%(LMax); vector.add(post, x); return true; } } boolean quitar_de_cola () { if (vacia()){ return false; } else { ant= (ant+1)%(LMax); return true; } }En esta implementación podemos observar que se reserva unaposición más que la especificada en el parámetro de la funcióncrear. 69
  • 68. En el caso de la cola llena la posición siguiente a post no esusada y por lo tanto es necesario crear una matriz de untamaño n+1 para tener una capacidad de almacenar nelementos en una cola.1.5 El TDA ÁrbolIntroducción y terminología básicaEn esta sección vamos a considerar una estructuración de losdatos más compleja: los árboles.Este tipo de estructura es usual incluso fuera del campo de lainformática. Como es el caso de los árboles gramaticales paraanalizar oraciones, los árboles para analizar circuitos eléctricos,los árboles genealógicos, representación de jerarquías, etc …Para tratar esta estructura cambiaremos la notación:• Las listas tienen posiciones. Los árboles tienen nodos.• Las listas tienen un elemento en cada posición. Los árboles tienen una etiqueta en cada nodo algunos autores distinguen entre árboles con y sin etiquetas. Un árbol sin etiquetas tiene sentido aunque en la inmensa mayoría de los problemas necesitaremos etiquetar los nodos.Usando esta notación, un árbol es una colección de elementosllamados nodos, uno de los cuales se distingue como raíz, juntocon una relación de paternidad que impone una estructurajerárquica sobre los nodos. 70
  • 69. Formalmente, un árbol se puede definir de manera recursivacomo sigue:1. Un solo nodo es, por sí mismo, un árbol. Ese nodo es también la raíz de dicho árbol.2. Supóngase que n es un nodo y que A1,A2,…, Ak son árboles con raíces n1,n2, …,nk, respectivamente. Se puede construir un nuevo árbol haciendo que n se constituya en el padre de los nodos n1,n2,…,nk. En dicho árbol, n es la raíz y A1,A2,…,Ak son los subárboles de la raíz. Los nodos n1,n2,…,nk reciben el nombre de hijos del nodo n.A veces conviene incluir entre los árboles un árbol especial elárbol nulo, un árbol sin nodos que se representa mediante Λ.Ejemplo de un Árbol: C C1 C2 C3 s1.1 s1.2 s2.1 s2.2 s2.3 s4.1 s2.2.1 s2.2.2 s2.2.3Los árboles normalmente se presentan en forma descendente yse interpretan de la siguiente forma: C es la raíz del árbol. C1, C2, C3 son los hijos de C 71
  • 70. C1, s1.1, s1.2 componen un subárbol de la raíz. s1.1, s1.2, s2.1, s2.2.1, s2.2.2, s2.2.3, s2.3,s4.1 son las hojas del árbol.Además de los términos introducidos consideraremos lasiguiente terminología:a) Grado de salida o simplemente grado: se denomina grado de un nodo al número de hijos que tiene. Así el grado de un nodo hoja es cero. En la figura el nodo con etiqueta C tiene grado 3.b) Caminos: si n1,n2,…,nk es una sucesión de nodos en un árbol tal que ni es el padre de ni+1 para 1≤ i ≤ k-1, entonces esta sucesión se llama un camino del nodo n1 al nodo nk. La longitud de un camino es el número de nodos menos uno, que hay en el mismo. Existe un camino de longitud cero de cada nodo a sí mismo. Ejemplos sobre el árbol de la figura: • C,C2,s2.2,s2.3 es un camino de C a s2.2.3 ya que C es padre de s2, éste es padre de s2.2, etc. • C1,C,C2 no es un camino de C1 a C2 ya que C1 no es padre de C.c) Ancestros y descendientes: si existe un camino, del nodo a al nodo b, entonces a es un ancestro de b y b es un descendiente de a. 72
  • 71. En el ejemplo anterior los ancestros de s2.2 son s2.2, C2 y C y sus descendientes s2.2.1, s2.2.2, s2.2.4. (cualquier nodo es a la vez ancestro y descendiente de sí mismo). Un ancestro o descendiente de un nodo, distinto de sí mismo, se llama un ancestro propio o descendiente propio respectivamente. Podemos definir en términos de ancestros y descendientes los conceptos de raíz, hoja y subárbol: • En un árbol, la raíz es el único nodo que no tiene ancestros propios. • Una hoja es un nodo sin descendientes propios. • Un subárbol de un árbol es un nodo, junto con todos sus descendientes.d) Altura: la altura de un nodo es un árbol es la longitud del mayor de los caminos del nodo a cada hoja. La altura de un árbol es la altura de la raíz. Ejemplo: la altura de C2 es 2 y la del árbol es 3.e) Profundidad: la profundidad de un nodo es la longitud del único camino de la raíz a ese nodo. Ejemplo: la profundidad de C2 es 1.f) Niveles: dado un árbol de altura h se definen los niveles 0…h de manera que el nivel i está compuesto por todos los nodos de profundidad i. 73
  • 72. g) Orden de los nodos: los hijos de un nodo usualmente están ordenados de izquierda a derecha. Si deseamos explícitamente ignorar el orden de los hijos, nos referiremos a un árbol como un árbol no-ordenado. La ordenación izquierda-derecha de hermanos puede ser extendida para comparar cualesquiera dos nodos que no están relacionados por la relación ancestro-descendiente. La regla a usar es que si n1 y n2 son hermanos y n1 está a la izquierda de n2, entonces todos los descendientes de n1 están a la izquierda de todos los descendientes de n2. Ejemplo: en la figura el nodo s2.2.1 está a la derecha de los nodos C1, s1.1, s1.2, s2.1 y a la izquierda de los nodos s2.2.2, s2.2.3, s2.3, C3, s4.1.Recorridos de un árbolExisten distintos métodos útiles para recorrer sistemáticamenterecorrer todos los nodos de un árbol.Los tres recorridos más importantes se denominan preorden,inorden y postorden.Estos ordenamientos se definen recursivamente como sigue:• Si un árbol A es nulo, entonces la lista vacía es el listado de los nodos de A en los recorridos preorden, inorden y postorden. 74
  • 73. • Si A contiene un solo nodo, entonces ese nodo constituye el listado de los nodos de A en los recorridos preorden, inorden y postorden.Si ninguno de los anteriores es el caso, sea A un árbol con raíz ny subárboles A1,A2,…,Ak. n A1 A2 … Ak1. El listado en preorden de los nodos de A está formado por el nodo raíz de A, seguido de los nodos de A1 listados en preorden, luego por los de A2 en preorden y así sucesivamente hasta los nodos de Ak listados en preorden.2. El listado en inorden de los nodos de A está formado por los nodos de A1 listados en inorden, seguidos de n (nodo raíz) y luego por los nodos de los subárboles A2,…, Ak listados en inorden.3. El listado en postorden de los nodos de A está formado por los nodos de A1 listados en postorden, luego por los nodos de A2 en postorden y así sucesivamente hasta los nodos de Ak listados en postorden y por último la raíz n.Como ejemplo de listados veamos el resultado que se obtendríasobre el siguiente árbol. 75
  • 74. 1 2 3 4 5 6 7 8 9 10Los resultados de los listados en preorden, postorden e inordenson los siguientes: 1. Listado preorden: 1,2,3,5,8,9,6,10,4,7 2. Listado postorden: 2,8,9,5,10,6,3,7,4,1 3. Listado inorden: 2,1,8,5,9,3,10,6,7,4Un truco útil para producir los tres recorridos es el siguiente: sicaminamos por la periferia del árbol, partiendo de la raíz, yavanzando en el sentido contrario de las agujas del reloj.El recorrido en preorden se lista un nodo la primera vez que sepasa por él.En el caso del recorrido postorden, se lista un nodo la últimavez que se pasa por él, conforme se sube hacia su padre.Para el recorrido inorden, se lista una hoja la primera vez que sepasa por ella, y un nodo interior, la segunda vez que se pasapor él. 76
  • 75. El orden de las hojas en los tres recorridos corresponde almismo ordenamiento de izquierda a derecha de las hojas. Sóloel orden de los nodos interiores y su relación con las hojas varíaentre los tres ordenamientos. 1 2 3 4 5 6 7 8 9 10Un árbol no puede, en general, recuperarse con uno solo de susrecorridos. Será a partir de los recorridos preorden y postordencuando se pueda determinar unívocamente.Las posiciones en postorden de los nodos tienen la útilpropiedad de que los nodos de un subárbol con raíz n ocupanposiciones consecutivas de ord_post (n) – desc (n) a ord_post(n).Para determinar si un nodo x es descendiente de un nodo y,basta verificar que se cumple: ord_post(y)-desc(y) ≤ ord_post(x) ≤ ord_post(y)Una propiedad similar se cumple para el recorrido en preorden. 77
  • 76. Una aplicación: árboles de expresiónUna importante aplicación de los árboles en la informática es larepresentación de árboles sintácticos, es decir, árboles quecontienen las derivaciones de una gramática necesarias paraobtener una determinada frase de un lenguaje.Podemos etiquetar los nodos de un árbol con operandos yoperadores de manera que un árbol represente una expresión. * + - x z x yLa expresión aritmética de este árbol sería: (x+z)*(x-y). Lasreglas para que un árbol represente a una expresión son:1. Cada hoja está etiquetada con un operando y sólo consta de ese operando.2. Cada nodo interior está etiquetado con un operador.Con estas premisas si un nodo interior n está etiquetado por unoperador binario θ (como + ó *), el hijo izquierdo representa laexpresión E1 y el derecho la E2, entonces el árbol de raíz nrepresenta la expresión (E1) θ (E2).En general, cuando se recorra un árbol en preorden, inorden opostorden, se preferirá listar las etiquetas de los nodos, en lugarde sus nombres. 78
  • 77. En los árboles de expresión, el listado en preorden de lasetiquetas nos da lo que se conoce como la forma prefijo de unaexpresión, en la que el operador precede a su operandoizquierdo y derecho.Formalmente:• La forma prefija para un único operando x es el mismo.• La expresión prefija correspondiente a (E1) θ (E2), donde θ es un operador binario, es θP1 P2, donde P1 y P2 son las expresiones prefijas correspondientes a E1 y E2.En la expresión prefija no se necesitan paréntesis, dado que esposible revisar la expresión prefija θ P1 P2 e identificarunívocamente a P1 como el prefijo más corto (y único) de P1P2,que es además una expresión prefija válida.En el ejemplo el preorden de etiquetas del árbol es *+xy-xy.Como puede verse el prefijo válido más corto de esta expresiónes +xy que corresponde al hijo izquierdo del nodo raíz.De manera similar, el listado en postorden de las etiquetas deun árbol de expresión da lo que se conoce como representaciónpostfija (o polaca).Formalmente:• La expresión postfijo para un único operando x es el mismo. 79
  • 78. • La expresión postfijo para (E1) θ (E2), siendo θ un operador binario es Z1Z2θ , donde Z1 y Z2 son las representaciones postfijo de E1 y E2, respectivamente.Los paréntesis son innecesarios, porque se puede identificar aZ2 buscando el sufijo más corto de Z1Z2 que sea una expresiónpostfijo válida.En el ejemplo, la expresión postfija correspondiente es xy+xy-*,y si escribimos esta expresión como Z1Z2*, entonces Z2 es xy-, elsufijo más corto de xy+xy-*.Finalmente, el inorden de una expresión en un árbol deexpresión da la expresión infijo en sí mismo, pero sinparéntesis. En el ejemplo, la sucesión inorden del árbol esx+y*x-y.El TDA ÁrbolLa estructura de árbol puede ser tratada como un tipo de datoabstracto. Notaremos al tipo Árbol como TArbol, al tipo nodocomo TNodo y al tipo de las etiquetas TEtiqueta.Para notar el árbol vacío usaremos un valor especialARBOL_VACIO, al igual que en las listas existe el concepto delista vacía.Para expresar que un nodo no existe usaremos un valor especialNODO_NULO. Un ejemplo de su uso puede ser cuandointentemos extraer el nodo hijo a la izquierda de un nodo hoja. 80
  • 79. Especificación de las operaciones primitivas del TDA Árbol crear (TEtiqueta u) { Construye la raiz del árbol como un nuevo nodo r con etiqueta u y sin hijos. } TNodo padre () {Devuelve el padre del nodo n. Si n es la raíz, que no tiene padre, devuelve NODO_NULO. Como precondición n no es NODO_NULO} TNodo hizqda () {Devuelve el descendiente más a la izquierda en el siguiente nivel del nodo n, y devuelve NODO_NULO si n no tiene hijo a la izquierda. Como precondición n no es NODO_NULO. En el ejemplo, hijo_izda (n2) = n4, , hijo_izda (n5) = NODO_NULO} n1 n2 n3 n4 n5 n6 n7 TNodo herdrcha () {Devuelve un nodo m con el mismo padre p que n tal que m cae inmediatamente a la derecha de n en la ordenación de los hijos de p. Devuelve NODO_NULO si n no tiene hermano a la derecha. Como precondición n no es NODO_NULO. En el ejemplo, hermano_drcha (n4) = n5} 81
  • 80. TEtiqueta etiqueta (){Devuelve la etiqueta del nodo n en el árbol T}void reEtiqueta (TEtiqueta e){Asigna una nueva etiqueta e al nodo n}TNodo raiz (){Devuelve el nodo que está en la raíz del árbol T oNODO_NULO si T es ARBOL_VACIO}void insertar_hijo_izqda (TNodo n){Inserta el árbol Ti como hijo a la izquierda del nodo n.Como precondición n no es NODO_NULO y Ti no esARBOL_VACIO}void insertar_hermano_drcha (TNodo n){Inserta el árbol Td como hermano a la derecha del nodo n.Como precondición n no es NODO_NULO y Td no esARBOL_VACIO}TArbol podar_hijo_izqda (TNodo n){Devuelve el subárbol con raíz hijo a la izquierda de n elcual se ve privado de estos nodos. Como precondición nno es NODO_NULO}TArbol podar_hermano_drcha (TNodo n){Devuelve el subárbol con raíz hermano a la derecha de n,el cual se ve privado de estos nodos. Como precondición nno es NODO_NULO} 82
  • 81. Implementación de árboles basada en celdas enlazadasVamos a declarar un nodo de forma que la estructura del árbolpueda ir en aumento mediante la obtención de memoria deforma dinámica, haciendo una petición de memoria adicionalcada vez que se quiere crear un nuevo nodo.Bajo esta implementación cada nodo de un árbol contiene 3referencias: padre que apunta al padre, hizqda que apunta al hijoizquierdo y herdrcha que apunta al hermano a la derecha delnodo.La definición de la clase TNodo es como sigue, se ha incluido enella todas aquellas operaciones de los arboles que trabajanúnicamente con nodos:Public class TNodo { Public static final NODO_NULO NULL; Object etiqueta; TNodo padre,hizqda,herdrcha; Public TNodo (Object u){ //Constructor etiqueta=u; padre=hizqda=herdrcha=NULL; } TNodo padre () { return (padre); } 83
  • 82. TNodo hizqda () { return (hizqda); } TNodo herdrcha () { return (herdrcha); } TEtiqueta etiqueta () { return (etiqueta); } void reEtiqueta (TEtiqueta e) { etiqueta = e; }}// fin de la clase TNodoA continuación se detalla como quedaría la clase TArbol a partirde lo especificado en la clase TNodo:Public class TArbol{ Public static final ARBOL_VACIO NULL TNodo raiz; TNodo TArbol (Object u) { raiz=new TNodo(u); } 84
  • 83. TNodo raiz(){ return (raiz);}void insertar_hijo_izqda (TNodo n){ raiz.herdrcha=n.hizqda; raiz.padre=n; n.hizqda=raiz;}void insertar_hermano_dcha(TNodo n){ raiz.herdrcha=n.herdrcha; raiz.padre=n.padre; n.herdrcha=raiz;}TArbol podar_hijo_izqda (TNodo n){ TArbol Taux=new TArbol(); Taux.raiz = n.hizqda; if (Taux!=ARBOL_VACIO){ n.hizqda = Taux.raiz.herdrcha; Taux.raiz.padre = TNodo.NODO_NULO; Taux.raiz.herdrcha = TNodo.NODO_NULO; } return (Taux);}TArbol podar_hermano_drcha (TNodo n){ TArbol Taux=new TArbol(); 85
  • 84. Taux.raiz = n.herdrcha; if (Taux!=ARBOL_VACIO){ n.herdrcha = Taux.raiz.herdrcha; Taux.raiz.padre = NODO_NULO; Taux.raiz.herdrcha = NODO_NULO; } return (Taux);}private static void preorden (TNodo n){Escribir (n.etiqueta ());for(n=n.hizqda();n!=TNodo.NODO_NULO;n=herdrcha()) preorden (n);}public void preordenAr(){ preorden(raiz);}private static void inorden (TNodo n){TNodo c;c=n.hizqda ();if (c!=TNodo.NODO_NULO){ inorden (c); Escribir (n.etiqueta ()); for(c=c.herdrcha();c!=TNodo.NODO_NULO;c=c.herdrcha ()) inorden (c); 86
  • 85. } else Escribir (n.etiqueta ()); } public void inordenAr(){ inorden(raiz); } private void static postorden (TNodo n) { TNodo c; for (c=n.hizqda();c!=TNodo.NODO_NULO;c=c.herdrcha ()) postorden (c); Escribir (n.etiqueta ()); } public void postordenAr(){ postorden(raiz); }}//fin de la claseImplementación de los recorridos de un árbol• Preorden: los pasos a seguir son los siguientes: 1. Visitar la raíz. 2. Recorrer el subárbol más a la izquierda en preorden. 3. Recorrer el subárbol de la derecha en preorden. 87
  • 86. El procedimiento recursivo que, dado un nodo n, lista lasetiquetas en preorden del subárbol con raíz n se ha definido enla clase TArbol, se ha utilizado una rutina Escribir que tienecomo parámetro de entrada un valor de tipo TEtiqueta que seencarga de imprimir en la salida estándar.El recorrido preorden no recursivo , necesita una pila paraencontrar el camino alrededor del árbol. El tipo TPila esrealmente pila de nodos, es decir, pila de posiciones de nodos.La idea básica subyacente al algoritmo es que cuando estamosen un nodo p, la pila alojará el camino desde la raíz a p, con laraíz en el fondo de la pila y el nodo p en la cabeza.El programa tiene dos modos de operar:• En el primer modo desciende por el camino más a la izquierda en el árbol, escribiendo y apilando los nodos a lo largo del camino, hasta que encuentre una hoja.• En el segundo modo de operación en el cual vuelve hacia atrás por el camino apilado en la pila, extrayendo los nodos de la pila hasta que se encuentra un nodo en el camino con un hermano a la derecha.Entonces tras ello el programa vuelve al primer modo deoperación, comenzando el descenso desde el inexploradohermano de la derecha.El programa comienza en modo uno en la raíz y terminacuando la pila está vacía. Veamos otra versión de preordenmediante el uso de una Pila: 88
  • 87. void preordenAr () { Pila P; /*Pila de posiciones: TElemento de la pila es el tipo nodo*/ TNodo m; P = new Pila (); /*creación del TDA Pila*/ m=raiz (); do { if (m!=TNodo.NODO_NULO)){ Escribir (n.etiqueta ()); P.Push (m); m = m.hizqda (); } else if (!P.vacia ()){ m=(P.tope()).herdrcha (); P.pop (); } }while (!P.vacia ()); } • Inorden: los pasos a seguir son los siguientes: 1. Recorrer el subárbol más a la izquierda en inorden. 2. Visitar la raíz. 3. Recorrer el subárbol del siguiente hijo a la derecha en inorden.El procedimiento recursivo para listar las etiquetas de susnodos en inorden se ha definido en la clase TArbol. 89
  • 88. • Postorden: los pasos a seguir son: 1. Recorrer el subárbol más a la izquierda en postorden. 2. Recorrer el subárbol de la derecha en postorden. 3. Visitar la raíz.El procedimiento recursivo para listar las etiquetas de susnodos en postorden se ha definido en la clase TArbolEl TDA Árbol BinarioUn árbol binario puede definirse como un árbol que en cadanodo puede tener como mucho grado 2, es decir, a lo más 2hijos.Los hijos suelen denominarse hijo a la izquierda e hijo a laderecha, estableciéndose de esta forma un orden en elposicionamiento de los mismos.Todas las definiciones básicas que se dieron para árbolesgenerales permanecen inalteradas sin más que hacer lasparticularizaciones correspondientes.En los árboles binarios hay que tener en cuenta el orden izqda-drcha de los hijos. Vease los siguientes ejemplos: 1 1 1 2 2 23 4 3 4 3 4 90
  • 89. 5 5 5 (a) (b) (c)Si los árboles binarios (a) y (b) son diferentes, puesto quedifieren en el nodo 5.El árbol (c) es el correspondiente árbol general, por convenio sesupone igual al (b) y no al (a), aunque los árboles generales noson directamente comparables a los árboles binarios.Especificación del TDA Árbol BinarioVamos a notar al tipo árbol binario como TArbolB y al tipo nodocomo TNodoB. Las operaciones primitivas para el TDA árbolbinario son las siguientes: TArbolB (TEtiquetaB e, TArbolB ti,TArbolB td) {Devuelve un árbol cuya raíz contiene la etiqueta e, y como subárbol a la izquierda ti y como subárbol a la derecha td} TNodoB padreB () {Devuelve el padre del nodo n. Si n no tiene padre, devuelve NODO_NULO . Como precondición n no es NODO_NULO} TNodoB hizdaB () {Devuelve el hijo a la izquierda del nodo, y devuelve NODO_NULO si no tiene hijo a la izquierda.} 91
  • 90. TNodoB hdchaB (){Devuelve el hijo a la derecha del nodo y devuelveNODO_NULO si no tiene hijo a la derecha. }TEtiqueta etiquetaB (){Devuelve la etiqueta del nodo.}void reEtiquetaB (TEtiqueta e){Asigna una nueva etiqueta e al nodo.}TNodoB raizB (){Devuelve el nodo que está en la raíz del árbol oNODO_NULO si el árbol es BINARIO_VACIO }void insertar_hizdaB (TNodoB n){Inserta el árbol sobre el que se aplica el método como hijoa la izquierda del nodo n. Como precondiciones n no esNODO_NULO }void insertar_hdchaB (TNodoB n){Inserta el árbol sobre el que se aplica el método como hijoa la derecha del nodo n. Como precondiciones n no esNODO_NULO }TArbolB podar_hizdaB (TNodoB n){Devuelve el subárbol con raíz hijo a la izquierda de n, elcual se ve privado de estos nodos. Como precondición nno es NODO_NULO} 92
  • 91. TArbolB podar_hdchaB (TNodoB n) {Devuelve el subárbol con raíz hijo a la derecha de n, el cual se ve privado de estos nodos. Como precondición n no es NODO_NULO}Implementación del TDA Árbol BinarioVamos a realizar una implementación mediante referenciascomo sigue: public class TEtiquetaB { String cadena; } class TNodoB { public static final TNodoB NODO_NULO = null; TEtiquetaB etiqueta; TNodoB hizda, hdcha, padre; public TNodoB (TEtiquetaB etiqueta) { this.etiqueta=etiqueta; hizda=hdcha=padre=null; } public TNodoB padreB () { 93
  • 92. return padre; } public TNodoB hizdaB () { return hizda; } public TNodoB hdchaB () { return hdcha; } public TEtiquetaB etiquetaB () { return etiqueta; } public void reEtiquetaB (TEtiquetaB e) { etiqueta=e; }}public class TArbolB { public static final TArbolB BINARIO_VACIO = null; private TNodoB raiz; public TArbolB (TEtiquetaB etiqueta) { raiz=new TNodoB(etiqueta); } 94
  • 93. public TArbolB (TEtiquetaB etiqueta, TArbolB ti, TArbolB td) { raiz=new TNodoB(etiqueta); raiz.hizda=ti.raiz; if (ti!=BINARIO_VACIO) ti.raiz.padre=raiz; raiz.hdcha=td.raiz; if(td!=BINARIO_VACIO) td.raiz.padre=raiz; raiz.padre=null;}public TArbolB (TNodoB raiz) { this.raiz=raiz;}public TNodoB raizB () { return raiz;}public void insertar_hizdaB (TNodoB n) { // Insertar el arbol como hijo izda de nodo n n.hizda=raiz; if (raiz!=TNodoB.NODO_NULO) raiz.padre=n;}public void insertar_hdchaB (TNodoB n) { // Insertar el arbol como hijo dcha de nodo n n.hdcha=raiz; if (raiz!=TNodoB.NODO_NULO) raiz.padre=n;} 95
  • 94. public TArbolB podar_hizdaB (TNodoB n) { if (n!=TNodoB.NODO_NULO) { if (n.hizda!=TNodoB.NODO_NULO) { TArbolB t = new TArbolB(n.hizda); n.hizda=TNodoB.NODO_NULO; return t; } } return BINARIO_VACIO;}public TArbolB podar_hdchaB (TNodoB n) { if (n!=TNodoB.NODO_NULO) { if (n.hdcha!=TNodoB.NODO_NULO) { TArbolB t = new TArbolB(n.hdcha); n.hdcha=TNodoB.NODO_NULO; return t; } } return BINARIO_VACIO;}private void preorden (TNodoB n) { if (n!= TNodoB.NODO_NULO) { escribir (n.etiquetaB()); preordenArB (n.hizdaB()); preordenArB (n.hdchaB()); }} 96
  • 95. void preordenArB () { preorden(raiz);}private void postorden (TNodoB n) { if (n!= TNodoB.NODO_NULO) { postorden(n.hizdaB()); postorden(n.hdchaB()); escribir (n.etiquetaB()); }}void postordenArB () { postorden(raiz);}private void inorden (TNodoB n) { if (n!= TNodoB.NODO_NULO){ inorden (n.hizdaB()); escribir (n.etiquetaB()); inorden (n.hdchaB()); }}void inordenArB () { inorden(raiz); 97