Computing">
Apuntes Progra 2
Apuntes Progra 2
Apuntes Progra 2
Apuntes de cátedra
El objetivo de este apunte es el de ser una guía en el dictado de la materia, junto a las
explicaciones de los docentes en las clases. Se encuentra en su primera versión y es por
eso que agradeceremos todas las observaciones, comentarios y correcciones que nos
puedan hacer llegar los alumnos para ir mejorándolo.
Resumen
• boolean: los valores posibles que pueden tomar las variables de este tipo son
true o false únicamente
1.2 Variables
Otro aspecto para considerar es cómo se lleva a cabo la declaración de variables en Java,
y las mismas pueden ser de la siguiente manera:
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 7
Como se vio en algunos de los ejemplos anteriores para la asignación de valores a una
variable se utiliza el operador = (por ejemplo: variable = 5).
El alcance de la variable está dado por el bloque en el que se declara. Una variable es
visible dentro del bloque en el que fue declarada y dentro de todos los sub-bloques que
se definan dentro de éste.
1.3 Operadores
1.3.1 Aritméticos
Las operaciones aritméticas básicas sobre números son las siguientes:
• +: operador de suma.
• *: operador de multiplicación.
• /: operador de división.
1.3.2 Relacionales
Las operaciones de comparación básicas sobre números son las siguientes:
1.3.3 Lógicos
Además, de las operaciones sobre números están las operaciones sobre valores
lógicos:
1
Tener especial cuidado de no omitir un signo =, ya que el lenguaje lo tomaría como una asignación
y no daría error, pero cambiaría el significado o comportamiento de la operación
2
valor debe ser una expresión de tipo booleano
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 9
1.4.1 Bloques
En Java, los bloques de sentencias se delimitan con llaves ({ y }). Se comienza un
bloque con una llave de apertura y se finaliza con una llave de cierre. En muchos de
los casos, si el bloque contiene una única sentencia, se pueden omitir las llaves,
aunque para evitar errores y malos entendidos, se recomienda utilizarlas siempre. La
indentación se utilizará a modo simbólico para facilitar la lectura del código, sin
embargo, el compilador lo ignora totalmente.
1.4.2 Secuencia
Para la separación de secuencias de instrucciones se utiliza ;.
1.4.5 Ciclos
Para la ejecuciónde ciclos vamos a ver tres instrucciones, while, fory do..while. Las
dos primeras se pueden ejecutar como mínimo cero veces, a diferencia del do..while
que se ejecuta como mínimo una vez, debido a que primero hace la ejecución y luego
evaluá la condición.
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 11
La variable que se inicializa y utiliza dentro del for puede ser declarada previo al for o
dentro del mismo (como en el ejemplo que sigue). De acuerdo a dónde se declare
será el alcance que tenga, es decir, si se declara en el mismo for sólo se podrá utilizar
en las instrucciones dentro del ciclo del for. La variación de la variable puede ser un
incremento o un decremento. Un ejemplo de uso del for podría ser para sumar los
números del 1 al 10, con lo cual se haría:
En este caso tenemos declarada la variable i del for en el mismo, ya que no se requiere
fuera del él.
Ahora supongamos que estamos buscando un valor en un arreglo de enteros de 20
elementos y queremos conocer no sólo si existe el valor, sino en qué posición del
mismo, en cuyo caso haríamos lo siguiente:
El do..while, a diferencia del las otros dos sentencias de ciclos, lo que hace primero
es ejecutar las instrucciones y luego evaluá la condición, con lo cual, al menos una vez
se ejecutan todas las instrucciones independientemente del valor de la condición.
1.5 Arreglos
Luego de haber descripto las diferentes sentencias de ciclos vamos a ver como se
declaran y usan los arreglos en Java.
Declaración
La forma de declarar un arreglo es la siguiente:
<tipo_datos_arreglos>[] <nombre_arreglo>; por ejemplo, si queremos declarar un
arreglo de enteros haríamos lo siguiente:
int[] valores;.
Dimensionamiento
Los arreglos, por tratarse de una estructura de tamaño fijo, antes de poder utilizarlos
se le debe asignar la cantidad de elementos que va a contener, es decir,
dimensionarlo:
<nombre_arreglo> = new <tipo_datos_arreglos>[<dimension>]
La palabra reservada new, se utilizará en el lenguaje, cada vez que se quiera reservar
memoria para una variable que no sea simple. Para los enteros, reales y caracteres,
como se ha visto, no es necesario hacer la reserva de memoria, ya que la declaración
de la variable lleva inmersa la reserva de memoria.
Por ejemplo, si al arreglo valores utilizado para ejemplificar en la parte de declaración,
le queremos decir que va a contener 100 elementos, entonces lo que haríamos es:
valores = new int[100];
Otra forma de dimensionar un arreglo, y a su vez inicializarlo con valores es la
siguiente: int[] valores = {5,8,15,20};
en este caso el arreglo toma una dimensión 4, con los valores 5, 8, 15 y 20 en las
posiciones 0, 1, 2 y 3 respectivamente.
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 13
Utilización
Debemos tener en cuenta que los arreglos en Java tienen su primer elemento en la
posición cero, es decir, si declaramos un arreglo de dimensión 100, lo vamos a poder
recorrer desde 0 a 99. A continuación se ve un ejemplo de recorrido del arreglo
valores para inicializarlo con valores:
1.7 Métodos
Por último, vamos a ver dos conceptos particulares de los lenguajes orientados a
objetos como es el caso de Java, que son los métodos y clases. El concepto de
métodos y clases que daremos es básico y no tiene por objetivo dar un conocimiento
del paradigma de Orientación a Objetos, sino dar las herramientas básicas para poder
trabajar a lo largo del curso.
En Java todas las operaciones se llevan a cabo a través de métodos (se puede hacer
una similitud a funciones y procedimientos de los lenguajes estructurados) los cuales
se declaran de la siguiente manera:
<tipo_retorno_me´todo><nombre_metodo>(<tipo_parametro>< parametro1>
,...,<tipo_parametro> <parámetro_n>){
instruccion1;
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 14
En el caso de que el método no devuelva valor se lo declara del tipo void. Por otra
parte, el método puede no contener parámetros, en cuyo caso es igualmente
obligatorio colocar los paréntesis sin ningún parámetro, es decir, se colocaría:
Esto se puede escribir de la siguiente manera que da el mismo resultado, es decir, son
formas equivalentes de decir los mismo:
1.8 Clases
Luego de haber hablado de los métodos, nos resta describir el concepto de clases. En
los lenguajes Orientados a Objetos siempre vamos a estructurar nuestros programas
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 15
o sistemas mediante clases. Las clases las podríamos comparar a las estructuras de
los lenguajes estructurados, aunque la definición correcta de las clases es que son la
definición de un objeto, en donde un objeto representa a alguna entidad de la vida
real (por ejemplo, un objeto puede ser una persona, una casa, un auto, una formula,
etc.). A continuación, veremos cómo es la declaración y el uso de una clase.
Declaración
Vamos a declarar una clase con sus componentes, esos componentes son variables
de la clase y sus métodos. Para indicar que es una clase se utiliza la palabra reservada
del lenguaje class.
Como ejemplo, vamos a declarar una clase punto, en donde tenemos dos
componentes que representan las coordenadas x e y del punto:
Por último, nos resta ver como podemos acceder a los componentes de las clases, es
decir, las variables3 y métodos. Para ello se utiliza el punto “.” de la siguiente manera:
<nombre_variable>.<nombre_componente>
Es decir, si queremos asignarles valores a las coordenadas x e y del punto p lo que
haríamos es lo siguiente:
p.x = 5; p.y = 10;
En lo referido a métodos y variables que hemos venido trabajando, existe el concepto
de alcance o visibilidad de los métodos y variables, y es lo que nos permite indicar si
una método o variable podrá ser utilizado por quién usa esa clase o no. Es decir, si un
método o variables es privado solo se podrá utilizar dentro de la clase, en cambio si es
público podrá ser utilizado tanto dentro de la clase como por quién usa esa clase. Para
indicar que un método o variable es público se utiliza la palabra reservada del leguaje
public, y para indicar si es privado la palabra private (por defecto si no se indica nada el
lenguaje considera que la variable o método es privada). Supongamos el siguiente
ejemplo para mostrar lo mencionado anteriormente:
public class Persona {
String nombre;
String apellido;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getApellido() {
return apellido;
}
public void setApellido(String apellido) {
this.apellido = apellido;
}
public String getCalle() {
return calle;
}
public void setCalle(String calle) {
this.calle = calle;
}
}
Tenemos una clase que representa una persona, que tiene un nombre, apellido, la
calle, número y ciudad en donde vive. Para asignarle el nombre y el apellido a la
persona tenemos un método público y para asignarle la calle, número y ciudad otro.
3
Debemos tener en cuenta que no hemos introducido el concepto de m´etodos privados y
pu´blicos hasta el momento, ya que so´lo podríamos acceder desde afuera si el m´etodo es pu´blico.
Este concepto se va a ver en los próximos p´arrafos. No se ver´a en el curso el concepto de variables o
m´etodos protegidos
UNIDAD 1 INTRODUCCIÓN AL LENGUAJE JAVA 17
Estos dos métodos serán públicos y se deberán ser utilizados por quién usa la clase,
pero las variables en donde guardamos esos valores no podrán ser accedidas por
quién usa la clase con lo cual serán privadas. Para el ejemplo se utilizará un tipo de
dato provisto por Java que es String que simboliza una cadena de caracteres y se puede
imprimir como vimos anteriormente.
UNIDAD 2 TDA: CONCEPTOS Y ESPECIFICACION´ 18
hecho, para una misma definición de TDA puede haber diferentes maneras de realizar
lo definido por ese TDA.
A la definición del TDA la llamaremos especificación y a la forma de llevar a cabo lo
definido por un TDA lo denominaremos implementación. Por lo tanto, para un mismo
TDA vamos a tener diferentes implementaciones.
La notación de Java que utilizaremos para especificar TDA es la notación de interfaces.
Para especificar el TDA Ejemplo, notaremos:
• PilaVacía: indica si la pila contiene elementos o no. Se supone que la pila está
inicializada.
Luego de listar las operaciones del TDA pila vamos a describir el mismo con Java:
• ColaVacía: indica si la cola contiene elementos o no. Se supone que la cola está
inicializada.
De la misma manera que utilizamos como ejemplo pasar los elementos de una pila a
otra, ahora vamos a escribir un método que nos permita pasar los elementos de una
cola a otra. En este caso particular, a diferencia de las pilas, por el comportamiento
particular de las colas, los elementos en la cola destino quedarán en el mismo orden
que en la cola origen. origen (TP1, ejercicio 4.a)
Podemos establecer las siguientes conclusiones. Si acolamos todos los elementos con
la misma prioridad, la cola con prioridad se comporta como una cola común. Si
acolamos todos los elementos con la prioridad igual a su valor, la cola con prioridad
puede ordenar automáticamente los elementos.
2.3.4 Conjunto
El siguiente tipo de dato abstracto que vamos a definir será el conjunto. El conjunto
es una estructura que nos permite guardar elementos sin que los mismos se repitan
y en donde no se tiene un orden, y además nos permite conocer si un elemento dado
se encuentra o pertenece a la estructura. Debido a que la estructura no tiene un
UNIDAD 2. TDA: CONCEPTOS Y ESPECIFICACION´ 24
orden, cuando recuperamos un dato la estructura nos devuelve uno cualquiera que
pertenece a ella, y por otro lado cuando queremos eliminar una valor debemos
indicarle cuál es. A partir de esta descripción de comportamiento del conjunto,
podemos decir que la lista de operaciones que define el TDA de conjunto es la
siguiente:
• Agregar: permite agregar un elemento al conjunto. Se supone que el conjunto
está inicializado.
• Sacar: permite eliminar del conjunto un elemento dado. Se supone como
precondición que el conjunto no está vacío.
• Elegir: devuelve un elemento cualquiera del conjunto. Se supone como
precondición que el conjunto no está vacío.
Como ejemplo de uso del ConjuntoTDA, supongamos que queremos indicar si dos
conjuntos son iguales, entendiendo que dos conjuntos son iguales si tienen los
mismos elementos.
public static boolean sonIguales(ConjuntoTDA a, ConjuntoTDA b) {
ConjuntoTA aux1= new ConjuntoTA();aux1.inicializarConjunto();
ConjuntoTA aux2= new ConjuntoTA();aux2.inicializarConjunto();
copiar(a,aux1);copiar(b,aux2);
boolean iguales=true;
int elemento;
while (!aux1.conjuntoVacio() && !aux2.conjuntoVacio() && iguales ) {
elemento=aux1.elegir();
if (!aux2.pertenece(elemento))
iguales=false;
else {
aux1.saca r(elemento);
aux2.sacar(elemento);
}
}
return (aux1.conjuntoVacio() && aux2.conjuntoVacio());
}
2.3.5 Diccionarios
El último tipo de dato abstracto que vamos a definir en esta unidad será el diccionario.
La estructura de datos diccionario se caracteriza porque cada valor ingresa a la
estructura asociado a una clave, y estás claves existen siempre que tengan valor
asociado y son únicas. En el presente curso se van a analizar dos comportamientos
diferentes de la estructura diccionario, el primero de ellos será en donde cada clave
puede tener asociado un único valor, estructura que denominaremos
DiccionarioSimple, y el segundo en donde cada clave tiene asociado un conjunto de
valores, estructura que denominaremos DiccionarioMultiple.
• Eliminar: dada una clave elimina el valor asociado a la clave, y por consiguiente
la clave ya que el diccionario no puede contener claves sin valores asociados. Si
la clave no existe no se hace nada. Se supone que el diccionario esta
inicializado.
UNIDAD 2. TDA: CONCEPTOS Y ESPECIFICACION´ 26
• Recuperar: dada una clave devuelve el valor asociado a la clave. La clave dada
debe pertenecer al diccionario. Se supone que el diccionario esta inicializado.
2.4 Resumen
Durante esta unidad hemos visto el concepto de tipo de dato abstracto, y la definición
y uso de cinco tipos de datos abstractos como son Pila, Cola, Cola con Prioridad,
Conjunto y Diccionario (en sus dos variantes, simple y múltiple). Como mencionamos
cuando definimos un TDA, en donde dijimos que el TDA especifica el comportamiento
de una estructura independientemente de cómo sea la implementación, y que por
otra parte, íbamos a tener diferentes implementaciones para un mismo TDA, estas
diferentes implementaciones es lo que vamos a ver en las unidades siguientes.
UNIDAD 3 IMPLEMENTACION 29
Unidad 3: Implementación
El objetivo de esta unidad es entender el concepto de implementación y su clara
diferencia con la especificación de un TDA. Para eso vamos a ver diferentes
implementaciones de los TDA definidos anteriormente, utilizando como estrategia de
implementación estructuras de tamaño fijo, es decir, arreglos. Luego de haber
realizado las diferentes implementaciones veremos el costo asociado a cada una de
las operaciones de cada TDA de acuerdo a la implementación realizada.
3.1 Implementación de TDAs
Durante esta sección veremos diferentes implementacionesde PilaTDA, ColaTDA,
ColaPrioridadTDA y ConjuntoTDA.
Cuando hablamos de implementación nos estamos refiriendo a que debemos definir
los siguientes puntos:
• Escribir / implementar cada una de las operaciones del TDA que se esta
implementando
En esta sección las estructuras utilizadas para guardar la información van a ser
arreglos, lo cual va a tener una restricción que es que tienen tamaño fijo. En secciones
posteriores veremos otra forma de guardar la información.
Para implementar un TDA en Java, vamos a escribir una clase, a la que le indicaremos
que es la implementación de un TDA con la palabra reservada implements. Luego
definiremos la estructura de datos, y por último, codificaremos cada uno de los
métodos especificados. Si fuera necesario, se podrán agregar métodos auxiliares, que
serán internos a la clase.
0 1 2 3 4 5
0 1 2 3 4 5 6 7 8 9
}
UNIDAD 3 IMPLEMENTACION 32
int []a;
int indice;
Estrategia 3: De la misma manera que para Pilas, podemos mencionar otra estrategia
de implementación en donde variamos la estructura utilizada para guardar la
información, es decir, también utilizamos un arreglo, pero para guardar la cantidad
de elementos que tenemos en vez de hacerlo en una variable utilizamos la posición 0
del arreglo.
UNIDAD 3 IMPLEMENTACION 34
class Elemento{
int valor;
int prioridad;
}
}
elementos[j] = new Elemento();
elementos[j].valor = x;
elementos[j].prioridad = prioridad;
indice++;
0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9
UNIDAD 3 IMPLEMENTACION 38
Diccionario Simple
public class DicSimpleA implements DiccionarioSimpleTDA {
class Elemento{
int clave;
int valor;
}
Elemento[] elementos;
int cant;
Diccionario Múltiple
class Elemento{
int clave;
int[] valores;
int cantValores;
}
Elemento[] elementos;
int cantClaves;
return i;
}
return i;
}
}
InicializarPila C C InicializarCola C C
Apilar L C Acolar L C
Desapilar L C Desacolar C L
Tope C C Primero C C
ColaVacia C C
PilaVacia C C
UNIDAD 3 IMPLEMENTACION 42
4.1.1 Pila
Vamos ahora a implementar el PilaTDA con una estructura dinámica. Para ello,
recordemos la especificación del PilaTDA:
4.1.2 Cola
Recordemos la especificación del ColaTDA:
Con la misma estrategia que para la pila, ahora vamos implementar la cola, esta vez,
manteniendo un puntero al primer elemento y uno al último ya que por el
comportamiento de este TDA, se deberá agregar por un extremo y eliminar por el
otro. El orden en que estarán los elementos será el primero apunta al segundo, el
segundo al tercero, así siguiendo, llegando al último elemento de la cadena que será
el último elemento agregado a la Cola.
UNIDAD 4 IMPLEMENTACION CON ESTRUCTURAS DINAMICAS 46
Aquí la estrategia será modificar el nodo, para que además del valor tenga la
prioridad incluida:
nuevo.sig = aux.sig;
aux.sig = nuevo;
}
}
UNIDAD 4 IMPLEMENTACION CON ESTRUCTURAS DINAMICAS 49
4.1.4 Conjuntos
Para implementar el conjunto, a diferencia de las otras estructuras, no hay orden,
por lo tanto, la estrategia es simplemente mantener la cadena de nodos y siempre
se deberá recorrerla para buscar un elemento o eliminarlo. Aquí presentamos una
implementación, existen otras que mantienen la cadena ordenada. Con esto se
mejoran algunos costos y se empeoran otros.
Recordemos entonces la especificación del ConjuntoTDA:
c = null;
}
4.1.5 Diccionarios
4.1.5.1 Diccionario Simple
La implementación propuesta es la siguiente:
}
}
}
UNIDAD 4 IMPLEMENTACION CON ESTRUCTURAS DINAMICAS 52
i f (aux== null) {
NodoValor nv = new NodoValor();
nv.valor = valor;
nv.sigValor = nc.valores;
nc.valores = nv;
}
}
InicializarCola C C C
AcolarPrioridad L (2 n) L (2 n) L (n)
Desacolar C C C
Primero C C C
ColaVacia C C C
Prioridad C C C
InicializarPila C C C
Apilar L C C
Desapilar L C C
Tope C C C
PilaVacia C C C
InicializarCola C C C
Acolar L C C
Desacolar C L C
Primero C C C
ColaVacia C C C
InicializarConjunto C C
Agregar L L
Sacar L L
Pertenece L L
Elegir C C
UNIDAD 4 IMPLEMENTACION CON ESTRUCTURAS DINAMICAS 56
InicializarDiccionario C C C C
Agregar L L L L
Eliminar L L L L
EliminarValor - L - L
Recuperar L L L L
Claves L L L L
UNIDAD 5: Árboles 57
Unidad 5: Arboles
Durante esta sección vamos a describir el concepto de la estructura de árboles y
definiremos en particular el tipo de dato abstracto de los árboles binarios de búsqueda
(ABB). Se presentarán los algoritmos de recorridos de árboles. Para la implementación y
resolución de ejercicios con estas estructuras, será necesario introducir el tema de
recursividad.
5.1 Conceptos
Un árbol representa una estructura jerárquica sobre una colección de elementos. Se
puede definir como una colección de elementos llamados nodos sobre la que se define
una relación de paternidad que impone una estructura jerárquica sobre estos nodos. Esta
relación entre dos elementos define dos roles, un nodo es el padre y el otro es el hijo. En
un árbol todo nodo tiene uno y sólo un padre, excepto un nodo particular al que se
denomina la raíz del árbol y que no tiene ningún padre.
Más formalmente podemos decir que un árbol es vacío o es un nodo raíz y un conjunto
de árboles que dependen de esa raíz. Cada uno de estos árboles, se denomina subárbol.
Más adelante, veremos que esta definición es especialmente útil al momento de resolver
problemas con árboles.
Se pueden definir más términos que se utilizarán con los árboles. Un nodo sin hijos se
denomina hoja. Todos los nodos que no son ni la raíz ni las hojas se denominan nodos
internos. Se denominan descendientes de un nodo al conjunto de elementos de todos
sus subárboles. Se denominan ancestros de un nodo al conjunto de elementos formados
por su padre, el padre de su padre y así siguiendo hasta llegar a la raíz del árbol.
La profundidad de un nodo es la longitud del camino único desde la raíz a ese nodo. La
altura del árbol es la mayor de las profundidades de sus nodos. Un solo nodo es por sí
mismo un árbol, y ese nodo será a su vez la raíz del árbol. También el árbol nulo, es decir,
aquel que no contiene nodos es considerado un
árbol.
Por ejemplo, una estructura de directorios en una PC puede ser representada por un
árbol como se ve en la figura 5.1.
UNIDAD 5: Árboles 58
Siguiendo con la metodología de toda la materia, se trabajará con árboles de enteros. Por
ejemplo, un árbol de enteros como el que se ve en la figura 5.2.
En el árbol de ejemplo, 2 es la raíz. Las hojas son 45, 1, 54, 98, 43, 7, 1 y 32. Los
descendientes de 21 son 1, 4, y 54. Los ancestros de 43 son 7, 87 y 2. La profundidad de
8 es 2. La altura del árbol es 3.
• ElimHijoMConDesc: Elimina el subárbol más a la izquierda del árbol dado con todos
sus descendientes.
• ElimHermSConDesc: Elimina el hermano siguiente del árbol dado con todos sus
descendientes.
• AgregarHijoM: Agrega el valor dado como raíz del subárbol del árbol dado.
Un caso particular de árboles son los árboles binarios, los cuales cumplen con todas las
definiciones dadas previamente, sólo que cada nodo puede tener a lo sumo dos hijos. Es
decir, cada nodo tiene cero, uno o dos hijos.
En estos árboles podemos hablar de hijo izquierdo, hijo derecho, subárbol izquierdo y
subárbol derecho respectivamente. Se puede ver un árbol binario en la figura 5.4.
5.4 Recursividad
Debido a la forma en que vamos a representar los árboles, se requiere para la mayoría de
sus operaciones la utilización de operaciones recursivas. Vamos a recordar el concepto
de recursividad.
Cuando hablamos de un método recursivo hablamos de un método que se define en
función de sí mismo. Siempre nos vamos a encontrar que dentro de ese método va a
haber una llamada o invocación a sí mismo. Esto, a simple vista llevaría a un método que
UNIDAD 5 ARBOLES 61
nunca acabaría su ejecución, por lo tanto, vamos a tener que buscar una forma de cortar
esta recursión. Entonces, en todo método recursivo vamos a tener:
Debemos tener especial cuidado en que la ejecución del algoritmo alcance siempre la
condición de corte, entonces cuando se invoca al método en forma recursiva se debe
hacer con los parámetros que tiendan a alcanzar en un punto al caso base que corte la
recursividad.
Un ejemplo simple de recursividad es el método que calcula el factorial de un número:
Como vemos, en este ejemplo, la llamada recursiva se hace con un número menos al
ingresado, con lo cual, suponiendo que el número era positivo, en algún momento se va
a alcanzar la condición de corte, cuando el número sea 0.
5.5.1 Especificación
Las operaciones necesarias para utilizar un ABB son:
• Raiz: recupera el valor de la raíz de un ABB siempre que el mismo esté inicializado
y no sea vacío.
• HijoIzq: devuelve el subárbol izquierdo siempre que el árbol esté inicializado y no
esté vacío.
• HijoDer: devuelve el subárbol derecho siempre que el árbol esté inicializado y no
esté vacío.
• ArbolVacio: indica si el árbol está vacío o no siempre que el árbol esté inicializado.
• InicializarABB: inicializa el árbol.
• AgregarElem: agrega el elemento dado manteniendo la propiedad de ABB,
siempre que el árbol esté inicializado.
• EliminarElem: elimina el elementodado manteniendo la propiedad de ABB,
siempre que el árbol esté inicializado.
A continuación, vamos a presentar la definición del TDA para los árboles ABB:
public interface ABBTDA {
// siempre que el árbol esté inicializado y no esté vacío
int raiz();
// siempre que el árbol esté inicializado y no esté vacío
ABBTDA hijoIzq();
// siempre que el árbol esté inicializado y no esté vacío
ABBTDA hijoDer();
// siempre que el árbol esté inicializado
boolean arbolVacio();
void inicializarArbol();
// siempre que el árbol esté inicializado
void agregarElem(int x);
// siempre que el árbol esté inicializado
void eliminar( int x );}
UNIDAD 5 ARBOLES 63
5.5.2 Implementación
Existen diversas formas de implementar los ABB. Vamos a presentar una con estructura
dinámica, pero se debe saber que existen otras implementaciones, incluso algunas con
estructuras estáticas.
Para esta implementación, vamos a definir un nodo, como un valor y sus dos subárboles.
El nodo queda como sigue:
NodoABB nodo;
public int raiz() {
return nodo.info;
}
public ABBTDA hijoIzq() {
return nodo.HijoIzq;
}
public ABBTDA hijoDer() {
return nodo.HijoDer;
}
public boolean arbolVacio() {
return nodo == null;
}
public void inicializarArbol() {
nodo = null;
}
public void agregarElem(int x) {
if(nodo ==null) {
nodo = new NodoABB();
nodo.info = x;
nodo.HijoIzq = new ABB();
nodo.HijoIzq.inicializarArbol();
nodo.HijoDer = new ABB();
nodo.HijoDer.inicializarArbol();
}
else
if(nodo.info > x)
nodo.HijoIzq.agregarElem(x);
else
if(nodo.info < x)
nodo.HijoDer.agregarElem(x);
}
UNIDAD 5 ARBOLES 64
Pre-order
Este recorrido propone visitar primero los padres y luego cada hijo en el mismo orden,
es decir se visita el nodo raíz, y luego cada subárbol en pre-order. En esta definición,
UNIDAD 5 ARBOLES 65
visitar puede significar mostrar o cualquier otra acción que se desee realizar. El algoritmo
que hace este recorrido es el siguiente:
El resultado de recorrer en pre-order el árbol de ejemplo de la figura 5.5 sería: 20, 10, 1,
18, 14, 12, 15, 35, 26, 23, 30, 40.
In-order
Este recorrido propone visitar in-order el subárbol izquierdo, luego la raíz y por último
in-order el subárbol derecho. En esta definición, visitar puede significar mostrar o
cualquier otra acción que se desee realizar. Este recorrido es el que en un ABB muestra
los elementos ordenados de menor a mayor. El algoritmo que hace este recorrido es el
siguiente:
El
resultado de recorrer in-order el árbol de ejemplo de la figura 5.5 sería: 1, 10, 12, 14, 15,
18, 20, 23, 26, 30, 35, 40.
Post-order
Este recorrido propone visitar primero los hijos en post-orden y luego la raiz. En esta
definición, visitar puede significar mostrar o cualquier otra acción que se desee realizar.
El algoritmo que hace este recorrido es el siguiente:
5.5.4 Utilización
Vamos a resolver algunos ejercicios en donde se trabaja con árboles binarios de
búsqueda.
Ejercicio 1: Contar la cantidad de nodos de un ABB.(TP4.3k)
Ejercicio 4: Dado un árbol ABBTDA a, devolver todos los nodos pares del mismo.
Los árboles binarios de búsqueda que hemos analizado en la sección anterior, adolecen del
problema de que en el peor de los casos pueden tender parcialmente hacia un árbol similar
a una lista, de manera que la búsqueda de un elemento cualquiera puede ser de un orden
superior a O(log n), y tender a O(n). Este problema se resuelve con los árboles AVL debido
UNIDAD 5 ARBOLES 68
a que estos árboles aseguran una serie de propiedades que permiten que la búsqueda de
cualquier elemento quede acotada por una complejidad de orden O(log n).. El orden de las
operaciones de inserción y eliminación sigue siendo O(log n), debido a la forma que se
mantienen balanceados y que se verá a continuación. Por tanto, las aplicaciones de estos
árboles son las mismas que las de un ABB, pero con la consideración de que las búsquedas
son más eficientes.
• Un AVL es un ABB
5.6.1 Rotaciones
Para mantener un árbol AVL balanceado se debe tener en cuenta que luego de cada inserción o
eliminación no se pierda la propiedad que la diferencia entre las alturas de los subárboles derecho
e izquierdo no debe excederse en más de 1 para cada nodo del árbol, por lo cual se debe verificar
esta condición y corregirla en caso que suceda. Para llevar a cabo la corrección, en la diferencia de
las alturas de los subarboles, existe un concepto de rotaciones en donde por medio de a lo sumo
dos rotaciones entre los nodos se puede volver a balancear el árbol. A continuación, se describen
UNIDAD 5 ARBOLES 69
los casos que se pueden presentar de rotaciones, y la forma de realizarlas para devolver al árbol la
condición de balanceo.
A continuación, se describen los casos que se pueden presentar y la forma de rotación para
resolver el problema del balanceo.
5.6.2 Implementación
package impl;
import apis.ABBTDA;
/**
* AVL=ABB + BALANCEO
*
*/
public class AVL extends ABB {
public AVL() {
}
private AVL(NodoABB n) {
this.nodo=n;
}
// TODO Auto-generated constructor stub
public void agregarElem(int x) {
super.agregarElem(x);
balancear();
}
/**
* Metodo para rotar a la derecha
*
*/
private ABBTDA rotarDer(ABBTDA a) {
NodoABB NodoDer=nuevoNodo(a.raiz(), a.hijoIzq().hijoDer(),a.hijoDer());
return new AVL( nuevoNodo(a.hijoIzq().raiz(), a.hijoIzq().hijoIzq(),new AVL(NodoDer))
}
private void setearNodo(NodoABB nodo, int info, ABBTDA hi, ABBTDA hd) {
nodo.info=info;
nodo.HijoIzq=hi;
nodo.HijoDer=hd;
}
}
UNIDAD 5 ARBOLES 74
Un árbol de búsqueda es un tipo especial de árbol que sirve para guiar la búsqueda
de un valor. Un árbol de búsqueda de orden q=2*t+1 es un árbol tal que cada nodo
contiene, cuando más, 2*t valores de búsqueda y 2*t+1 referencias a nodos en el
orden <P1, k1, P2, k2, …, Pq-1, Kq-1, Pq> , donde q <= 2*t+1, cada Pi es una referencia a
un nodo hijo (puede ser vacío) y cada Ki es un valor de búsqueda proveniente de
algún conjunto ordenado de valores. Se supone que todos los valores de búsqueda
son únicos. Un árbol de búsqueda debe cumplir, en todo momento, las siguientes
restricciones:
.
Para insertar valores de búsqueda en el árbol y eliminarlos, sin violar las
restricciones anteriores, se utilizan algoritmos que no garantizan que el árbol de
búsqueda esté equilibrado (que todas las hojas estén al mismo nivel).
Es importante mantener equilibrados los árboles de búsqueda porque esto
garantiza que no habrá nodos en niveles muy profundos que requieran muchas
comparaciones durante una búsqueda. Además, las eliminaciones de valores
pueden hacer que queden nodos casi vacíos, con lo que hay un desperdicio de
UNIDAD 5 ARBOLES 75
5.7.1 Árbol B
“En busca del equilibrio en el árbol y un mayor aprovechamiento del espacio”
El árbol B es un árbol de búsqueda, con algunas restricciones adicionales, que
resuelve hasta cierto punto los dos problemas anteriores. Estas restricciones
adicionales garantizan que el árbol siempre estará equilibrado y que el espacio
desperdiciado por la eliminación, si lo hay, nunca será excesivo. Los algoritmos para
insertar y eliminar se hacen más complejos para poder mantener estas
restricciones. No obstante, la mayor parte de las inserciones y eliminaciones son
procesos simples, se complican sólo en circunstancias especiales.
1. Cada nodo interno del árbol B tiene la forma general <P 1, <k1, Pr1>, P2, <k2,
Pr2> , …, <kq-1,
Prq-1>, Pq> (Ver Fig. 2) donde q <= 2*t+1. Cada Pi es un referencia a otro
árbol. Cada Pri es una referencia de datos: una referencia a información
adicional asociada al valor de búsqueda Ki (similar a lo visto en el TPO)
La siguiente figura nos muestra la cantidad de nodos por cada nivel del árbol.
UNIDAD 5 ARBOLES 77
Obtenemos que:
Las operaciones básicas que se pueden realizar en un árbol B son básicamente tres:
1. Buscar un valor
2. Insertar un valor
3. Eliminar un valor
Búsqueda
La búsqueda de un valor Y se realiza de manera análoga a la búsqueda en un árbol
binario de búsqueda. Se comienza buscando por el nodo raíz y se compara el valor
UNIDAD 5 ARBOLES 78
Y con las llaves Ki que se encuentran en ese nodo. Si Y es igual a algún Ki, termina la
búsqueda satisfactoriamente, si no, se debe seguir la referencia adecuada hacia un
nodo hijo en el que se continuará la búsqueda. La referencia seleccionada será P1,
si Y < K1, Pq si Y > Kq-1 y Pi, si Ki-1 < X < Ki, para 1 < i < q. En caso de que el Pi
correspondiente sea nulo, la búsqueda termina insatisfactoriamente.
Inserción
Para realizar la inserción, lo primero que debe hacerse es un proceso de búsqueda,
para localizar el nodo en el que se va a insertar. Si este proceso de búsqueda da por
resultado que el elemento ya existe, no se realizará ninguna operación, pues el
árbol B no permite elementos repetidos.
Es importante hacer notar la curiosa manera de crecer en altura que tienen los
árboles B, que lo hacen hacia la raíz, a diferencia de los comúnmente utilizados,
cuyas inserciones se realizan en forma de hojas y por tanto crecen en sentido
UNIDAD 5 ARBOLES 79
No hay espacio para almacenar el valor en nodo, por lo tanto, este nodo debe
dividirse, promocionando el valor que queda en el medio, en este caso, el 65:
Ej. 2 Veamos otro ejemplo que implicará que aumente la altura del árbol.
Supongamos que tenemos un árbol con esta estructura, y queremos insertar el
valor 68:
UNIDAD 5 ARBOLES 80
Y otra vez repetimos el proceso, al tratar de insertar el 65 en el nodo padre del nodo
que se dividió, vemos que este no existe, porque el nodo que se acaba de dividir
era la raíz. En este caso, se crea un nuevo nodo en el que se inserta el valor
promovido y se actualizan las referencias. Note la variación en la altura del árbol y
su crecimiento hacia la raíz.
UNIDAD 5 ARBOLES 81
Eliminación
El proceso de eliminación puede llegar a ser algo más complejo que la inserción. La
eliminación siempre debe realizarse en una hoja. Si después de realizarla búsqueda,
el nodo a borrar no estuviese en una hoja, de la misma manera que se procede en
un árbol binario de búsqueda, el nodo a borrar se sustituiría por su antecesor o
sucesor, que sí debe estar en una hoja.
El caso más sencillo se produce cuando al eliminar un valor de un nodo hoja, bien
porque sea el nodo a eliminar, bien porque sea un elemento que sustituyó al nodo
a eliminar, el tamaño del nodo sigue siendo al menos t. De esta forma, la estructura
del árbol se sigue cumpliendo y no hay que realizar ninguna reestructuración.
Esta “donación”, debe realizarse con participación del nodo padre. El elemento
donado, debe intercambiarse con el valor Ki correspondiente en el nodo padre y es
este valor Ki quien iría a suplir la falta en el hermano afectado. En este caso, pudiera
adoptarse la variante de chequear primero si el hermano derecho tiene la
posibilidad de donar un valor y en caso negativo, pasar a chequear si el hermano
izquierdo puede, pero esto provocaría un doble costo y por tanto, es preferible
chequear solamente a uno de los dos hermanos.
UNIDAD 5 ARBOLES 82
Aún resta un caso más. Este caso es idéntico al anterior, pero ahora el nodo vecino
tiene el número mínimo de nodos y por tanto, no existe la posibilidad de suplir la
falta de llave tomando un valor del hermano En esta situación se produce el efecto
inverso a la división, los dos nodos se agrupan en uno solo, formando un nuevo
nodo en la que todavía existe capacidad para otro valor. A este nodo se le añade el
valor Ki que estaba situado en el nodo padre, entre ambos nodos. Si al fundirse los
dos nodos y tomar el valor Ki de su padre, este se queda con una cantidad de llaves
menor que t, el proceso se repite, propagándose hasta la raíz.
En caso de ser la raíz el nodo al que se le quitará el Ki y esta sólo tenía un valor,
queda automáticamente eliminada, reduciéndose en 1 la altura del árbol. Note que,
del mismo modo que el árbol B crece hacia la raíz, este tipo de árbol se acorta
también desde la raíz, garantizándose así que todas las hojas, estén al mismo nivel.
Ej. 1 Eliminemos la clave 65. Es uno de los casos más sencillos, el valor está en un
nodo hoja, y el número de valores del nodo después de eliminarlo es mayor o igual
al mínimo.
Ahora estamos en el mismo caso que antes, sólo hay que borrar el valor 18 del nodo
hoja:
El resultado es que sólo queda una clave en el nodo que tiene el 12, y debe haber
al menos dos claves en un nodo de capacidad para cuatro (debe cumplirse que
q<=2t+1).
Según el algoritmo, buscaremos algún hermano que le pueda “prestar una clave”
(empezando por preguntar al de la izquierda por convención) y este es el hermano
derecho. Si el número de valores en el nodo derecho es mayor que el mínimo,
pasaremos un valor del nodo derecho al padre y de éste al nodo hoja, en este caso,
el 25 pasa al nodo padre y el 18 pasa al nodo que tenía el déficit.
Ej. 4 Vamos a ver el caso en que el nodo derecho no tiene valores suficientes para
transferir uno al nodo hoja. Eliminaremos el valor 5:
La primera parte es igual que el caso anterior, eliminamos el valor 5. Pero ahora el
nodo derecho tiene el número mínimo de valores.
UNIDAD 5 ARBOLES 84
El nodo hermano (en este caso el derecho, compuesto por las claves 12 y 18) tiene
justo el número mínimo de claves, por lo tanto, no podemos aplicar “donación”. En
estos casos se aplica una operación denominada “fusión" donde debemos fundir
en un solo nodo las claves de los nodos hijo izquierdo (9) e hijo derecho (12 y 18),
más la clave del padre(10). Luego se debe eliminar el nodo derecho que queda
vacío.
La cosa no termina ahí, ahora debemos comprobar el nodo padre, ya que también
ha podido quedar con menos claves que el mínimo exigido. En este caso sucede
eso, pero es un caso especial, ya que ese nodo es raíz y puede contener menos
claves que número mínimo. Ej. 5 Veamos cómo sería el proceso en un caso general,
que implica la reducción de altura del árbol.
Borraremos la clave 69 de este árbol:
El primer paso es sustituir el valor 69, en este caso; por el mayor de los menores,
(ya que hay menores): es decir, el 67:
Debemos fusionar la hoja con clave 66 con el único hermano (con claves 70 y 72)
más la clave del padre, eliminando el nodo derecho que queda vacío.
Ahora el nodo que contiene al 73, tiene una cantidad de claves menor al mínimo.
Vemos que el hermano izquierdo tiene la mínima cantidad de claves y no existe
hermano derecho para intentar donar. Al no poder aplicar donación, debemos
fusionar el nodo que contiene al 73 con el único hermano (el izquierdo) más la clave
del padre. Eliminamos el nodo derecho que queda vacío (el que contenía el 73).
Debido a que el nodo padre es la raíz y ha quedado vacía, lo eliminamos también
quedando como nodo raíz el nodo el izquierdo fusionado:
Unidad 6
6.Grafos
En esta unidad se introducirán los conceptos principales de grafos. Se especificará el TDA
grafo y sus posibles implementaciones.
Se dice que un vértice es alcanzable desde otro si existe un camino que una al segundo
con el primero.
A los efectos del curso, vamos a trabajar con: grafos dirigidos, sin aristas paralelas ni
bucles, en donde los nodos son números enteros, los pesos son enteros positivos.
Dado el grafo de la figura 6.3 los vértices son 1, 2, 3, 4, 5, 6, 7, 8 y 10. Las aristas son (1,2),
(2,1), (1,3), (3,4), (3,5), (4,6), (5,6), (6,5) y (8,10). El peso de cada una es 12, 10, 21, 32, 9,
10, 10, 12, 87 y 10 respectivamente. El nodo 3 tiene una arista entrante y dos salientes.
El nodo 7 no tiene ninguna arista entrante ni saliente, por ende es un nodo aislado. Existe
un camino entre el nodo 1 y el 4, su costo es 53. Además el nodo 4 es alcanzable desde
1.
• EliminarArista: elimina la arista entre los vértices dados, siempre que esta arista
exista.
UNIDAD 6 GRAFOS 89
• ExisteArista: indica si el grafo contiene una arista entre los vértices dados, siempre
que los vértices existan.
• PesoArista: devuelve el peso de la arista entre los vértices dados, siempre que la
arista exista.
}
cantNodos++;
}
referencia a la lista de aristas que tienen a ese nodo como origen. Una arista (a la que
llamaremos NodoArista), tiene una referencia al nodo destino de esta arista, el peso y
una referencia a la próxima arista de esa lista.
Entonces para el grafo del ejemplo de la figura 6.3 tenemos la la representación de la
figura 6.6 donde los círculos son los NodoGrafo y los rectángulos los NodoArista.
origen = origen.sigNodo;
}
NodoGrafo aux = origen;
while (aux != null ){
//remueve de aux todas las aristas hacia v
this .EliminarAristaNodo(aux, v);
i f (aux.sigNodo!= null && aux.sigNodo.nodo == v) {
//Si el siguiente nodo de aux es v, lo elimina
aux.sigNodo = aux.sigNodo.sigNodo;
}
aux = aux.sigNodo;
}
}
/*
* Si en las aristas del nodo existe
* una arista hacia v, la elimina
*/
private void EliminarAristaNodo(NodoGrafo nodo, int v){
NodoArista aux = nodo.arista;
i f (aux!= null) {
//Si la arista a eliminar es la primera en
//la lista de nodos adyacentes
i f (aux.nodoDestino.nodo == v){
nodo.arista = aux.sigArista;
}
else
{
while (aux.sigArista!= null && aux.sigArista.nodoDestino.nodo != v){
aux = aux.sigArista;
}
i f (aux.sigArista!= null) {
//Quita la referencia a la arista hacia v
aux.sigArista = aux.sigArista.sigArista;
}
}
}
}
public ConjuntoTDA Vertices(){
ConjuntoTDA c = new ConjuntoLD();
UNIDAD 6 GRAFOS 95
Unidad 6: GRAFOS 96
Bibliografía
[AA98] J. Hopcroft Y J. Ullman A. Aho. Estructuras de Datos y Algoritmos. Addison Wesley,
Ubicación en biblioteca UADE: 681.3.01/A292, 1998.
[Wei04] Mark Allen Weiss. Estructuras de datos en JAVA. Addison Wesley, Ubicación en
biblioteca UADE: 004.43 WEI est [1a] 2004, 2004.