Está en la página 1de 17

Programación- Apunte 03

Estructuras de datos en Java

Introducción al concepto general de Estructura de Datos.

Hasta aquí se estudió la forma de resolver problemas de diversas características usando


instrucciones de programación variadas: algunos problemas requerían sólo del uso de
condiciones y secuencias de instrucciones simples, en otros se hizo muy necesario el
desarrollo de métodos y una fuerte división en subproblemas, y otros requerían ya el uso
de ciclos...

Si observamos con detalle los últimos problemas que se han estado planteando (donde se
utilizan instrucciones repetitivas) y se los compara con los primeros problemas presentados
en el curso (en los que los ciclos ni siquiera se conocían), podremos notar que las
instrucciones de repetición se hicieron imperativamente necesarias para poder manejar un
gran volumen de datos (o sea, una gran cantidad de datos). En los primeros problemas del
curso, los datos que el programa necesitaba se cargaban todos a la vez en cierto conjunto
de variables, y luego esas variables se procesaban usando instrucciones simples y
condiciones. Pero en los últimos problemas analizados, o bien era muy grande la cantidad
de datos que pedía cada problema, o bien se desconocía en forma exacta la cantidad de
datos que se presentarían. Ante esto, no resultaba ni práctico ni posible cargar todos los
datos a la vez, dado que esto conduciría a declarar un conjunto demasiado grande de
variables que luego sería muy difícil de procesar. El uso de ciclos brindó una buena
solución: se declara un conjunto de variables necesario sólo para cargar un lote de datos,
se carga un lote, se procesa el mismo, y se repite el esquema usando un ciclo, hasta
terminar con todos los lotes de datos.

Si bien pudiera parecer que con esto quedan solucionados todos nuestros problemas, en
realidad estamos muy lejos de tal situación. Sólo piense el lector lo que pasaría si el
conjunto de datos tuviera que ser procesado varias veces en un programa. Con el esquema
de “un ciclo procesando lotes”, cada vez que el ciclo termina una vuelta se carga un nuevo
lote, y se pierde con ello el lote anterior. Si como sugerimos, el programa requiriera volver
a procesar un lote deberíamos volver a cargarlo, con la consecuente pérdida de tiempo y
eficiencia general. El re-procesamiento de un lote de datos es muy común en situaciones
que piden ordenar datos y/o resultados de un programa, o en situaciones en las que un
mismo conjunto de datos debe ser consultado varias veces a lo largo de una corrida (como

Ing. Felipe Steffolani Programación – Apunte 03 1


el caso de los datos contenidos en una agenda, o los datos de la lista de productos de un
comercio, por ejemplo).

Resulta claro que en casos como los planteados en el párrafo anterior, no es suficiente con
usar un ciclo para cargar de a un lote por vez, perdiendo el lote anterior en cada repetición.
Ahora se requiere que los datos estén todos juntos en la memoria, y poder accederlos de
alguna forma en el momento que se desee, sin perder ningún lote.

¿Cómo mantener en memoria un volumen grande de lotes de datos, pero de forma que su
posterior acceso y procesamiento sea relativamente sencillo? La respuesta a esta cuestión
se encuentra en el uso de las llamadas estructuras de datos.

Esencialmente, una estructura de datos es una variable que puede contener varios valores
a la vez. A diferencia de las variables de tipo primitivo (que sólo pueden contener un único
valor al mismo tiempo, y cualquier nuevo valor que se asigne provoca la pérdida del
anterior), una estructura de datos puede pensarse como un conjunto de varios valores
almacenados en una misma y única variable. En sentido, por ejemplo, un String es una
estructura de datos pues está formado por varios caracteres simples y todo el conjunto de
caracteres se referencia a través de una misma variable.

Las estructuras de datos son útiles cuando se trata de desarrollar programas en los cuales
se maneja un volumen elevado de datos y/o resultados, o bien cuando la organización de
estos datos o resultados resulta compleja. Piense el alumno en lo complicado que resultaría
plantear un programa que maneje los datos de todos los alumnos de la Facultad, usando
solamente variables sueltas como se hizo hasta ahora... Las estructuras de datos permiten
agrupar en forma conveniente y ordenada todos los datos que un programa requiera,
brindando además, muchas facilidades para acceder a cada uno de esos datos por
separado.

Tan importantes resultan las estructuras de datos, que en muchos casos de problemas
complejos resulta totalmente impracticable plantear un algoritmo para resolverlos sin utilizar
alguna estructura de datos, o varias combinadas.

Ing. Felipe Steffolani Programación – Apunte 03 2


Arreglos unidimensionales

Una estructura de datos muy importante es la que se conoce como arreglo unidimensional.
Se trata de una colección de valores que deben ser del mismo tipo y que se organiza de tal
forma que cada valor o componente individual es identificado automáticamente por un
número designado como índice. El uso de los índices permite el acceso y posterior uso de
cada componente en forma individual.

La cantidad de índices que se requieren para acceder a un elemento individual, se llama


dimensión del arreglo. Los arreglos unidimensionales se denominan así porque solo
requieren un índice para acceder a un componente. Por otra parte, dada la similitud que
existe entre el concepto de arreglo unidimensional y el concepto de vector en Algebra, se
suele llamar también vectores a los arreglos unidimensionales.

El siguiente gráfico muestra la forma conceptual de entender un arreglo unidimensional. Se


supone que la variable arreglo se denomina v y que la misma está dividida en seis casilleros,
de forma que en cada casillero puede guardarse un valor. Se supone también que el tipo
de valor que puede guardarse en el arreglo v del ejemplo, es int.

Observar que cada casillero es automáticamente numerado con índices, los cuales en Java
comienzan siempre a partir del cero: la primera casilla del arreglo siempre es subindicada
con el valor cero, en forma automática. A partir del índice, cada elemento del arreglo v
puede accederse en forma individual usando el identificador del componente: se escribe el
nombre del arreglo, luego un par de corchetes, y entre los corchetes el valor del índice de
la casilla que se quiere acceder. En ese sentido, el identificador del componente cuyo índice
es 2, resulta ser v[ 2 ]:

Ing. Felipe Steffolani Programación – Apunte 03 3


Para declarar una variable de tipo arreglo en Java, hay que recordar primero que en Java
los arreglos de cualquier dimensión son objetos, y por lo tanto deben ser creados con el
operador new. Lo primero es declarar entonces una referencia con la cual se va a apuntar
al arreglo que se quiere crear. En Java, esto se hace escribiendo el tipo de valor que se
almacenará en el arreglo (ese tipo se conoce como el tipo base del arreglo), luego el nombre
del arreglo y finalmente un par de corchetes vacíos:

int v [];

Note que en Java, el par de corchetes puede ir a la derecha o a la izquierda del nombre de
la variable. La declaración anterior es equivalente a esta otra:

int [] v;

Al declarar una referencia de tipo arreglo, lo que se está haciendo es declarar una variable
que luego será capaz de contener la dirección de memoria de un arreglo. El valor inicial de
esa referencia es null, y el arreglo aún no existe en memoria:

v null

Luego se usa el operador new para crear el objeto arreglo: se escribe new, seguido
nuevamente del tipo base del arreglo, y otra vez el par de corchetes pero de forma que esta
vez, se escribe dentro de ellos el tamaño del arreglo que se quiere crear:

v = new int [6];

La instrucción anterior crea un arreglo de seis componentes capaces de almacenar cada


uno un valor int, inicializa en cero cada casilla de ese arreglo, y retorna la dirección del
mismo (que en este caso se almacena en la referencia v que declaramos antes):

0 0 0 0 0 0

0 1 2 3 4 5

Obviamente se puede lograr lo mismo en una sola instrucción: int v [] = new int
[6];

Ing. Felipe Steffolani Programación – Apunte 03 4


Observar que si el tamaño de un arreglo es 6, entonces la última casilla del mismo lleva el
índice 5 debido a que los índices comienzan siempre desde el 0. En un arreglo en Java, no
existe una casilla cuyo índice coincida con el tamaño del arreglo.

Una vez que se creó el arreglo con new, se usa la referencia que lo apunta para acceder a
sus componentes, colocando a la derecha de ella un par de corchetes y el índice del valor
que se quiere acceder. Los siguientes son ejemplos de las operaciones que pueden
hacerse con los componentes de un arreglo (tomamos como modelo el arreglo v
anteriormente creado):

Si se desea procesar un arreglo de forma que la misma operación se efectúe sobre cada
uno de sus componentes, es normal usar un ciclo for, de forma se aproveche la variable de
control del ciclo como índice para entrar a cada componente. Los siguientes esquemas
muestran la forma de hacer una carga por teclado y una visualización por pantalla de un
arreglo de seis componentes de tipo int:

Ing. Felipe Steffolani Programación – Apunte 03 5


Un detalle interesante es que todo objeto arreglo en Java provee un atributo llamado length,
que contiene el tamaño del arreglo tal como fue declarado al crear ese arreglo con new.
Ese atributo es de naturaleza pública (public), por lo que puede accederse directamente
mediante el identificador de la variable referencia que apunta al arreglo. Los dos ciclos
anteriores, podrían escribirse así:

A continuación se desarrolla un ejercicio para poner en práctica lo visto hasta ahora sobre
arreglos unidimensionales.

i.) Cargar por teclado un arreglo de n componentes y multiplicarlo por el valor k que también
se ingresa por teclado.

La solución a este problema se muestra a continuación (corresponde al proyecto Ejemplo04


que acompaña a esta ficha de clase):

Ing. Felipe Steffolani Programación – Apunte 03 6


Observaciones:
La lógica del programa es directa: la carga por teclado el arreglo se realiza usando un ciclo for ajustado de
forma que la variable de control i varíe desde 0 hasta (v.length – 1) . En cada vuelta del ciclo, se carga el
componente v [ i ] con lo que, al finalizar el ciclo, el arreglo queda cargado completamente. Para multiplicar

Ing. Felipe Steffolani Programación – Apunte 03 7


también recorre el arreglo usando un for, pero en cada vuelta multiplica en forma acumulativa el componente v
[ i ] por el valor k. Finalmente el para mostrar aplica los mismos principios que para cargar, pero ahora
mostrando el componente v [ i ].

Búsqueda secuencial en un arreglo unidimensional.

Un problema común en programación es el de determinar si cierto valor x está contenido o


no en un arreglo v de n componentes. La convención suele ser que si se encuentra se
informa en qué posición, y si no se encuentra se informa con un mensaje.

Si el arreglo está desordenado, o no se sabe nada acerca de su estado, la única forma


inmediata de buscar un valor en él consiste en hacer una búsqueda secuencial: con un ciclo
for se comienza con el primer elemento del arreglo. Si allí se encuentra el valor buscado,
se detiene el proceso y se retorna el índice del componente que contenía al valor. Si allí no
se encuentra el valor, se salta al componente siguiente y se repite el esquema. Si se llega
al final del arreglo sin encontrar el valor buscado, lo cual ocurrirá cuando i (la variable de
control del ciclo) sea igual al tamaño del arreglo, el programa pone un mensaje avisando
que la búsqueda fue infructuosa. El caso de la búsqueda secuencial en un arreglo se
resuelve en el Ejemplo05:

Ordenamiento de un arreglo unidimensional

Una operación muy común cuando se trabaja con arreglos unidimensionales es la de


ordenar sus elementos, ya sea en secuencia de menor a mayor o de mayor a menor. Entre
otras ventajas, la importancia del ordenamiento de un arreglo (y en general, de cualquier
estructura de datos que admita algún tipo de ordenamiento) está en relación directa con la
operación de buscar valores dentro de ella. Como se verá, pueden plantearse algoritmos
que resultan más eficientes para buscar un valor en un arreglo ordenado que para buscarlo
en uno desordenado.

Ing. Felipe Steffolani Programación – Apunte 03 8


Existen muchísimos métodos diferentes para proceder al ordenamiento de un arreglo de n
componentes. El estudio de todos y cada uno de ellos, sería una tarea ajena al propósito
introductivo de estas notas de clase, y por lo tanto nos concentraremos en comprender el
modo de funcionamiento de sólo uno de ellos, de naturaleza sencilla, conocido como
Ordenamiento de Selección Directa. No obstante, aclaramos que incluso este sencillo
método ofrece variantes y mejoras que no serán analizadas aquí.
Si se desea ordenar el arreglo de menor a mayor usando selección directa, la idea central
es la adaptación y repetición sistemática de este simple algoritmo (suponemos que el
arreglo a ordenar es v y que ya está cargado con valores de tipo int):

Un rápido análisis muestra que el algoritmo anterior recorre el arreglo buscando el valor
menor del mismo, y al terminar lo deja en la casilla indicada por la variable i (que en este
caso vale cero). Se comienza asumiendo que el menor está en casilla la i = 0, y se recorre
con j desde el valor i + 1. Cada valor en la casilla j se compara con el valor en la casilla i. Si
el valor en v[i] resulta mayor que v[j] entonces los valores se intercambian, y así se prosigue
hasta que el ciclo controlado por j termina.
El algoritmo no ordena el arreglo: sólo garantiza que el menor valor será colocado en la
casilla con índice i = 0. Pero entonces sólo basta un pequeño cambio para que se ordene
todo el arreglo: hacer que la variable i modique su valor con otro ciclo, comenzando desde
cero y terminando antes de llegar a la última casilla (para evitar que j comience valiendo un
índice fuera de rango):

Ing. Felipe Steffolani Programación – Apunte 03 9


El algoritmo mostrado está implementado en la clase Ejemplo01 del proyecto que compaña
a esta Lección.

Búsqueda binaria

Si el arreglo estuviera ordenado (por ejemplo de menor a mayor), se puede aplicar un


método de búsqueda mucho más eficiente que la búsqueda secuencial ya vista, conocido
como Búsqueda Binaria. La idea básica es la siguiente: se usan dos índices auxiliares iz y
de cuya función es la de marcar dentro del arreglo los límites del intervalo en donde se
buscará el valor. Como al principio la búsqueda se hace en todo el vector, originalmente iz
comienza valiendo 0 y de comienza valiendo n -1 (siendo n la cantidad de componentes del
arreglo). Dentro del intervalo marcado por iz y de, se toma el elemento central, cuyo índice
c es:
c = (iz + de) / 2;

o sea, el promedio de los valores de iz y de. Luego de esto, se verifica si el valor contenido
en v[ c ] coincide o no con el número buscado. Si coincide, se termina la búsqueda, y se
retorna el valor de c para indicar la posición del número dentro del arreglo. Si no coincide,
es entonces cuando se aprovecha que el arreglo está ordenado de menor a mayor: si el
valor buscado x es menor que v[ c ], entonces si x está en el arreglo, debe estar a la
izquierda de v[ c ] y por lo tanto, el nuevo intervalo de búsqueda debe ir desde el valor de
iz hasta el valor de c - 1. Entonces, se ajusta de para que valga el valor c - 1, y se repite
el proceso descripto. Si el valor x es mayor que v[ c ], entonces la situación es la misma
pero simétrica hacia la derecha, y debe ajustarse iz para valer c + 1. El proceso continúa
hasta que se encuentre el valor (en cuyo caso se retorna el último c calculado), o hasta que
el valor de iz se haga mayor que el de de (es decir, hasta que los índices se crucen), lo cual
indicará que ya no quedan intervalos en los que buscar, y por lo tanto el valor x no estaba
en el arreglo (y en este caso, la búsqueda retornará el valor –1).
El proceso que se describió, se denomina búsqueda binaria porque "parte" al arreglo en
dos intervalos a partir del valor central, y a cada intervalo en otros dos, hasta dar con el

Ing. Felipe Steffolani Programación – Apunte 03 10


valor o no poder generar nuevos intervalos. La eficiencia del método se basa en que se
toma solo uno de los dos intervalos para buscar al valor, y el otro se desecha por completo.
En la primera partición, la mitad de los elementos del arreglo se desechan, en la segunda
se desecha la cuarta parte (o sea, la mitad de la mitad), y así sucesivamente. Con muy
pocas comparaciones, el valor es encontrado (si existe).
La desventaja obvia, es que el arreglo debe estar ordenado. Si se trabaja en un contexto
donde el arreglo sufre cambios permanentes de contenido y debe ser ordenado
periódicamente para facilitar la búsqueda, entonces se pierde la ventaja de la rapidez del
método, pues se supera largamente con el tiempo que se pierde ordenando. En todo caso,
la enseñanza final de toda esta cuestión es que existen herramientas algorítmicas de tipos
y condiciones diversas, algunas muy buenas en ciertas condiciones; pero depende de la
capacidad de análisis del programador el poder determinar en qué casos usar una u otra
técnica. El caso de la búsqueda binaria se incluye en la clase Ejemplo02 del Proyecto que
acompaña esta Lección.

Conteo por acceso directo

Los arreglos son estructuras de datos especialmente útiles, puesto que brindan la
posibilidad del acceso directo a cada componente. Es decir: si se necesita acceder
directamente a un elemento, sólo se requiere conocer el índice del mismo y no es necesario
pasar en forma secuencial por todos los elementos anteriores. Existen situaciones en las

Ing. Felipe Steffolani Programación – Apunte 03 11


cuales esta propiedad permite resolver problemas en forma notablemente sencilla. Por
ejemplo, considérese el siguiente ejercicio:
i.) Cargar por teclado un conjunto de valores tales que todos ellos estén
comprendidos entre 0 y 99. Se indica el fin de datos con el número -1.
Determinar cuántas veces apareció cada número.

A primera vista, el problema parece de solución obvia: se usa un ciclo para cargar de a un
valor (num) por vez, de forma que el ciclo sólo se detenga cuando num == -1. Como se
pide determinar cuantas veces apareció cada valor posible de num, se usa un contador
distinto por cada valor diferente que num pueda asumir. Así, para contar cuántas veces
apareció el 1, se puede usar el contador c1, para el 2 se podrá usar c2, y así sucesivamente.
En cada repetición del ciclo, se usa un if encadenado (o un switch) para determinar qué
número se cargó en esa vuelta, y en función del número se elije el contador
correspondiente. Al finalizar el ciclo, se muestran todos los contadores, y asunto
terminado...
Sin embargo, esa solución dista mucho de ser buena y sobre todo en casos como éste, en
que la variable num puede asumir valores en un rango muy amplio (0 a 99). El programador
debería declarar y poner en cero cien contadores, y luego, dentro del ciclo, usar un switch
con ¡cien ramas! para determinar qué contador debería usar de acuerdo al número
ingresado. Además del gran esfuerzo de codificación que el programador deberá realizar,
se tendrá el problema de la enorme redundancia de instrucciones semejantes, y por si esto
fuera poco, el programa resultante será muy poco flexible en la práctica: si luego de
plantearlo, nos cambian las condiciones supuestas (por ejemplo: si los valores que puede
asumir num pasaran a ser entre 0 y 150) entonces el programa original ya no serviría de
nada...

La solución correcta es usar un arreglo unidimensional de cien componentes de tipo int, de


forma que cada componente se use como uno de los contadores que se están necesitando.
Se pone inicialmente en cero cada componente del vector (dado que esos componentes
serán contadores). Luego comienza el ciclo para cargar los valores num. La idea central,
es que si num vale 0, entonces debe usarse v[0] para contarlo. Si num vale 1, se usa v[1],
y así sucesivamente. En general, para cada valor de la variable num tal que 0 <= num <=
99, debe usarse v[num] para contarlo... En otras palabras: dentro del ciclo no es necesario
usar el switch para determinar qué casillero del arreglo usar para contar cada número: se
accede directamente al casillero cuyo índice es num, y se incrementa el mismo en uno... Al
cortar el ciclo, se muestra el contenido del arreglo, y ahora sí el problema queda resuelto
de manera convincente.
Cuando un arreglo unidimensional se usa de esta forma, se lo suele designar como vector
de conteos o simplemente como vector de contadores. Si el arreglo se usara para acumular

Ing. Felipe Steffolani Programación – Apunte 03 12


valores (en vez de contarlos como se hizo en el ejemplo), se hablaría entonces de un vector
de acumulación. Se muestra a continuación el programa que aplica lo dicho:

Ing. Felipe Steffolani Programación – Apunte 03 13


Creación y uso de arreglos bidimensionales en Java

Básicamente, un arreglo bidimensional o matriz es un arreglo cuyos elementos están


dispuestos en forma de tabla, con varias filas y columnas. Aquí llamamos filas a las
disposiciones horizontales del arreglo, y columnas a las disposiciones verticales.
Para entrar a un componente, debe darse el índice de la fila del mismo y también el índice
de la columna. Como los índices requeridos son dos, el arreglo es de dimensión dos. El
siguiente esquema ilustra la manera de declarar y crear un arreglo bidimensional de
componentes int en Java, la forma conceptual de representarlo, y la manera de acceder a
sus componentes:

int a [][]; // se declara una referencia al arreglo, con valor inicial null.
a = new int [ 6 ][ 4 ]; // se crea el arreglo, con 6 filas y 4 columnas.
a[2][3] = 5; // se accede a una casilla y se asigna un valor en ella.

Ing. Felipe Steffolani Programación – Apunte 03 14


a

0 1 2 3
0 a[2][3]=5
1
2 5
filas
3 se accede al elemento en fila 2 y columna 3, y se
asigna un 5 en ese casillero
4
5

columnas

Para declarar la referencia al arreglo se usan ahora dos pares de corchetes vacíos. Y para
crear el arreglo con new, en el primer par de corchetes se escribe la cantidad de filas que
se necesitan y en el segundo par se escribe la cantidad de columnas.
Observar que para acceder a un elemento, se coloca el nombre de la referencia al arreglo,
luego el número de la fila del elemento que se quiere acceder, pero encerrado entre
corchetes, y por último el número de la columna de ese elemento, también encerrado entre
corchetes. Notar además, que en el lenguaje Java los arreglos de cualquier dimensión
están basados en cero, lo cual significa que el primer índice de cada dimensión es siempre
cero. En la figura anterior, puede verse que el arreglo tiene seis filas, pero numeradas del
0 (cero) al 5 (cinco), y cuatro columnas, numeradas del 0 (cero) al 3 (tres). No hay
excepciones a esta regla, por lo cual debe tenerse cuidado de ajustar correctamente los
ciclos para procesamiento de arreglos.
Para cargar por teclado una matriz a de n filas y m columnas (y en general, para procesar
secuencialmente una matriz), se pueden usar dos ciclos for anidados, de forma que el
primero recorra las filas de la matriz, y el segundo las columnas:
La idea básica del proceso aquí definido es que la variable i del ciclo más externo se usa
para indicar qué fila se está procesando en cada vuelta. Dado un valor de i, se dispara otro
ciclo controlado por j, cuyo objetivo es el de recorrer todas las columnas de la fila indicada
por i. Notar que mientras avanza el ciclo controlado por j permanece fijo el valor de i. Sólo
cuando corta el ciclo en j, se retorna al ciclo en i, cambiando ésta de valor y comenzando
por ello con una nueva fila. El proceso de recorrer secuencialmente una matriz avanzando
fila por fila empezando desde la cero, como aquí se describe, se denomina recorrido en
orden de fila creciente.
Se pueden hacer recorridos de otros tipos si fuera necesario, simplemente cambiando el
orden de los ciclos. Por ejemplo, el siguiente esquema realiza un recorrido en orden de fila
decreciente: comienza con la última fila, y barre cada fila hacia atrás hasta llegar a la fila
cero:

Ing. Felipe Steffolani Programación – Apunte 03 15


Notar que el cambio sólo consistió en hacer que la variable i (usada para barrer las filas),
comience en el valor n – 1 (que es el índice de la última fila), y se decremente hasta llegar
a cero. El ciclo en j se dejó como estaba.
Si se desea un recorrido en orden de columna creciente (o decreciente), sólo deben
invertirse los ciclos: el ciclo en j debe ir por fuera, y el ciclo en i debe ir por dentro. De esta
forma, el valor de j no cambia hasta que el ciclo en i termine todo su recorrido. Sin embargo,
no debe olvidarse que si queremos que j indique una columna, entonces j debe usarse en
el segundo par de corchetes al acceder a la matriz. Y si la variable i va a indicar filas,
entonces debe usarse en el primer par de corchetes. Esto es independiente del orden en
que se presenten los ciclos para hacer cada recorrido:

La variable que indica la fila va en el primer par de corchetes, y la variable que


indica la columna va en el segundo, sin importar cuál ciclo va por fuera y cuál
por dentro.

El siguiente esquema, muestra un recorrido en orden de columna creciente, para leer por
teclado una matriz a de n filas y m columnas:

Se muestra a continuación un ejemplo completo de matrices.

Ing. Felipe Steffolani Programación – Apunte 03 16


Ing. Felipe Steffolani Programación – Apunte 03 17

También podría gustarte