Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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
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.
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.
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 ]:
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:
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];
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:
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.
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):
Búsqueda binaria
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
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
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...
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.
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:
El siguiente esquema, muestra un recorrido en orden de columna creciente, para leer por
teclado una matriz a de n filas y m columnas: