ESTRUCTURA DE DATOS: Repaso e Introduccion
ESTRUCTURA DE DATOS: Repaso e Introduccion
ESTRUCTURA DE DATOS: Repaso e Introduccion
Apunte 1 - Introducción
Índice general
1. Introducción 6
1.1. ¾Qué es una estructura de datos? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2. Clasicación de las estructuras de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3. Denición e implementación de un TDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4. Eciencia de algoritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.4.1. Análisis del tiempo de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5. Recursividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5.1. Traza de algoritmos recursivos con una sola invocación . . . . . . . . . . . . . . . . . . . . 15
1.5.2. Traza de algoritmos recursivos con dos invocaciones . . . . . . . . . . . . . . . . . . . . . 17
2
Índice de guras
3
Algoritmos
4
Índice de ejercicios
5
Apunte 1
Introducción
Actualizado: 12 de febrero de 2021
En este apunte nos enfocaremos en deniciones que serán centrales a lo largo de esta materia y repasaremos
algunos conceptos vistos en materias anteriores.
El tipo de dato de una variable es el conjunto de valores que la variable puede tomar. Por ejemplo, una
variable de tipo boolean sólo puede tomar un valor verdadero (true) o falso (false).
Una estructura de datos es una colección de variables, posiblemente de diferentes tipos de datos,
conectados de varias formas. El contenido básico de una estructura de datos es la celda o nodo,
que es capaz de almacenar un dato de un tipo determinado.
Un tipo de datos abstracto (TDA) es un modelo matemático, junto con varias operaciones denidas sobre
ese modelo. Es decir, un TDA dene un modelo de un tipo de datos, indicando su posible contenido
y funcionalidad (operaciones), pero no dice cómo debe implementarse. Como programadores, lo ideal es
diseñar algoritmos en términos de TDAs. Luego, al momento de implementar un algoritmo en un lenguaje
de programación particular, será necesario encontrar la manera de representar los TDAs con los tipos de
datos y operadores de dicho lenguaje de programación.
La estructura básica en la mayoría de los lenguajes de programación es el arreglo unidimensional (array), que
es una sucesión de celdas que almacenan un dato cada una, donde todos los datos son del mismo tipo. Además,
es posible acceder a cada celda indicando su posición dentro de la sucesión (por ejemplo, en Java se accede con
la sintaxis a[i] ). La posibilidad de acceder a una celda particular, para observar o modicar su contenido, es
parte de la funcionalidad que la estructura array permite. Cuando se ha utilizado un arreglo en algún lenguaje,
logramos entender cómo funciona en otros lenguajes, dado que en realidad hemos aprendido los principios del
TDA arreglo. Al usar otro lenguaje de programación sólo necesitamos saber con qué sentencias se crea un arreglo
y qué operaciones tiene disponibles, pero "qué es un arrelgo" ya lo hemos aprendido.
Entonces podemos decir que una estructura de datos dene y organiza la interrelación de los datos y las
operaciones que se pueden realizar sobre ellos. Las operaciones sobre las estructuras pueden ser básicas como
alta (agregar un nuevo valor a la estructura), baja (eliminar un valor de la estructura), y búsqueda (encontrar
un valor determinado dentro de la estructura); u operaciones más complejas como ordenar los valores, crear una
copia, etc.
1 Aunque algunas estructuras pueden mantener datos de diferentes tipos, en esta materia nos independizaremos de ese enfoque.
6
APUNTE 1. INTRODUCCIÓN 7
tiene propósitos diferentes y la elección de las estructuras de datos adecuadas a un problema es uno de los
puntos clave de un buen desarrollo de software.
Estructuras lineales : Estas son las estructuras de datos más sencillas. Guardan los datos (a los que
llamaremos elementos) como si estuvieran en una la (uno detrás de otro). Visualmente las estructuras
lineales tienen la forma que se muestra en la Figura 1.1. En general pueden almacenar elementos repetidos.
Tienen operaciones para añadir y quitar elementos de la estructura y también para preguntar por algún
elemento en particular. El arreglo es una estructura lineal que tiene un tamaño jo (una vez creado no
puede crecer ni decrecer). En la materia veremos tres estructuras lineales: Pila, Cola y Lista.
Estructuras jerárquicas : Los árboles son estructuras de datos jerarquizadas. Cada árbol está constituido
por una colección de elementos llamados nodos, uno de los cuales se denomina raíz. Cada nodo tiene una
relación de parentesco dentro de la estructura jerárquica (de padres a hijos). Visualmente los árboles tienen
la forma que se muestra en la Figura 1.2. Hay distintos tipos de árboles según la cantidad máxima de
hijos que puede tener un nodo, si sus elementos se mantienen ordenados o no, etc. En la materia veremos
varios tipos de árboles: binarios, genéricos, heap, binario de búsqueda y AVL.
Grafos : son estructuras de datos en las que se pueden expresar relaciones de conexión sin restricción entre
los diversos elementos. Estos elementos son denominados vértices o nodos. Los vértices están unidos por
líneas llamadas aristas o arcos. Visualmente los grafos tienen una forma similar a la que se muestra en
la Figura 1.3. Los grafos son muy utilizados en problemas reales de conectividad (circuitos analógicos y
digitales, caminos o rutas entre localidades o aeropuertos, redes de computadoras, redes sociales, etc).
Uno de los problemas más comunes en los grafos es encontrar el camino de costo mínimo o vericar si
dos nodos tienen un camino entre sí (por ej., en una web que vende pasajes de avión, un grafo puede ser
apropiado para buscar maneras de viajar entre dos ciudades que no tienen vuelos directos).
Un Tipo de Dato Abstracto (TDA en castellano o ADT en inglés, de Abstract Data Type), representa
un conjunto de datos que cumplen ciertas condiciones especicadas por el mismo TDA, más un conjunto de
operaciones primitivas que representan el comportamiento del mismo. Es decir que el TDA tiene una identidad
(información) y un comportamiento (operaciones), con el cual permite la interacción o comunicación con sí
mismo.
Algo muy importante es que todo TDA es independiente del lenguaje en el que se pueda implementar: una
vez denido puede implementarse en PASCAL, C, C ++, C#, JAVA, etc. y debería funcionar de igual manera.
El TDA facilita el trabajo con tipos de datos, haciendo abstracción de la implementación al momento de denirlo
y también cuando éste es utilizado:
Abstracción al utilizar un TDA: Al usar un TDA, sólo se debe saber de qué se trata dicho TDA, es decir
qué tipos de datos involucra y cuáles son las operaciones que ofrece. La parte que se da a conocer se llama
la INTERFAZ, y está conformada por los nombres, parámetros y resultados de todas sus operaciones o
funciones. Cómo dichas operaciones han sido construidas y con qué variables, constantes o estructuras
internas cuenta, conforman la IMPLEMENTACIÓN del TDA y siempre son transparentes al usuario. Por
ejemplo: la clase String de Java es un TDA que se sabe que representa a una cadena de caracteres, que
tiene un conjunto de rutinas (funciones/procedimientos) que permiten manipularla (como length() que
devuelve un int que representa la longitud, charAt(int) que devuelve un char que está en la posición dada,
indexOf(String) que devuelve un int que es la posición del substring dado, etc). Sin embargo, los atributos,
constantes e implementaciones de los métodos públicos se mantienen ocultos y de forma transparente al
usuario nal.
Abstracción al denir e implementar un TDA: Un TDA es ante todo un producto software, cuyo usuario
será otro programador. Al igual que un programa común, al archivo o archivos necesarios para que el TDA
funcione (IMPLEMENTACION) debe adjuntarse la documentación apropiada (INTERFAZ + comentarios
de uso). Además, es muy importante que al construir un TDA se comience por tener un buen diseño previo.
Para ello se recomienda lo siguiente:
2. Hacer un diagrama analizando la identidad (valores variables y/o constantes, tipos de datos de cada
una) y sus operaciones (funciones, procedimientos)
5. Volver a rever y ajustar el producto de los pasos anteriores mientras sea necesario
Un TDA tiene que modelar o estar enfocado en resolver un sólo problema cada uno, nunca varios a la vez.
Las operaciones de un TDA (primitivas) deben ser implementadas con códigos cortos. Siempre se debe
modularizar con operaciones ocultas (privadas del TDA).
Todo TDA tiene que tener una primitiva para su construcción y, si el lenguaje lo requiere, otra para su
destrucción
Todo TDA que tenga clave unívoca no puede tener el constructor vacío. Debe existir al menos un
constructor a partir de su clave. Luego pueden agregarse otros constructores con la clave y otros datos.
Ejemplo: Alumno es identicado por su clave legajo. Debe existir un constructor Alumno(legajo) y
opcionalmente pueden haber otros constructores que además del legajo agreguen datos básicos como
nombre, apellido, tipo y nro de DNI, fecha de nacimiento, etc.
En Java no es necesaria una operación de destrucción dado que utiliza un recolector de basura
(Garbage Collector) que se dispara automáticamente y no tiene primitivas para devolver el espacio
liberado a la memoria heap.
La información de la entidad (datos) debe estar siempre oculta, excepto por medio de las operaciones
que correspondan. Para ello, al implementar en Java, los atributos deben ser siempre privados (private).
Además, las operaciones de recuperación (get) y modicación (set) de dichos atributos deben ser diseñadas
sin ir en contra de la encapsulación y de la naturaleza del TDA que representan:
APUNTE 1. INTRODUCCIÓN 9
No debe haber método set para los campos clave de un TDA: Por ejemplo, una instancia de Alumno
puede cambiar su nombre y apellido, género, domicilio o teléfono, pero nunca se puede cambiar su
número de legajo porque podría ocurrir que en una estructura que los mantiene ordenados por clave,
nunca se lo volviera a encontrar. En caso que un campo clave sea erróneo, será necesario quitarlo de
la estructura y volver a insertar el dato correcto.
De forma similar, no debería haber métodos set para cualquier campo considerado inmutable (es decir
aquellos que no debería ser posible modicar libremente) aún cuando no sean clave. Por ejemplo, el
campo número de DNI no debería ser modicado libremente, aún cuando no sea la clave del dato
Alumno. Esto debe ser analizado para cada clase del dominio particular.
Hay campos para los cuales la operación obtener (get) debería estar vedada. Por ejemplo, el
campo contraseña de un usuario de Facebook o cualquier sistema similar nunca debería ser
recuperado mediante un método get. En cambio, podría existir una operación en el TDA llamada
esPasswordCorrecto(clave) que devolviera true o false según corresponda.
Las primitivas del TDA tienen que permitirle al usuario manipularlo sin dar información sobre la
implementación del TDA (ocultamiento de la Información). El ocultamiento de información al implementar
las operaciones primitivas, se denomina encapsulamiento y es fundamental.
1. TDA Alumno
a ) Diseñar el TDA considerando campo clave legajo y como información asociada los datos nombre,
apellido, tipo y número de DNI, domicilio (calle, número y ciudad), teléfono, usuario y clave en el
sistema SIU. Especique las operaciones primitivas de acuerdo a las recomendaciones anteriores.
2. TDA Fecha
a ) Diseñe el TDA Fecha con clave compuesta día, mes y año. Analice y especique las operaciones
básicas del TDA.
b ) Implemente el TDA Fecha especicado en 2a) con tres atributos enteros que representen dia, mes y
año por separado.
c ) Implemente el TDA Fecha especicado en 2a) con un único atributo interno String con el formato
AAAAMMDD donde AAAA es el año expresado con cuatro dígitos, MM y DD son mes y día
expresados con dos dígitos cada uno.
Para calcular el tiempo de ejecución siempre partimos de la base que el algoritmo es correcto, es decir,
que produce el resultado deseado en un tiempo nito. Para comparar dos algoritmos correctos que resuelven el
mismo problema, se determina su eciencia de acuerdo a una medida de tamaño N, que representa la cantidad
de datos que manipulan dichos algoritmos.
Para comparar los algoritmos se puede utilizar una estrategia empírica o teórica:
La estrategia empírica consiste en programar los algoritmos y ejecutarlos en una computadora sobre varios
casos de prueba, y comparar los tiempos reales de ejecución.
La estrategia que utilizaremos en esta materia es la teórica. Para ello, el tiempo de ejecución de un algoritmo
se expresará como una función de la longitud de entrada en relación con un número de pasos a realizar. Así,
el tiempo de ejecución de un programa será una función de una cantidad n de elementos, que se la denomina
T(n). Estas estimaciones se analizan para un tamaño n muy grande, generalizando decimos que estudiamos a
n cuando tiende a innito.
En las sentencias condicionales (alternativas y repetitivas de tipo mientras o hasta) los algoritmos dependen
de valores concretos, y generalmente se pueden diferenciar el mejor caso, el peor caso y el caso promedio.
Mejor caso : Tmejor (n), es el menor de los tiempos de ejecución sobre todas las entradas de tamaño n.
Puede dar lugar a errores en algoritmos lentos que trabajan rápido sobre unas pocas entradas. Por ejemplo,
en el método de ordenamiento burbuja mejorado, el mejor caso es cuando el arreglo ya está ordenado y
en una sola pasada termina sin hacer ningún intercambio.
Caso promedio : Tpromedio (n), es el promedio de los tiempos de ejecución sobre todas las entradas de
tamaño n. Es el más el, pero puede ser muy difícil de determinar. En el ejemplo del método burbuja
mejorado, el caso promedio es cuando el arreglo está aleatoriamente desordenado.
Peor caso : Tpeor (n) , es el máximo de los tiempos de ejecución sobre todas las entradas de tamaño n,
puede no ser muy el, pero consideramos que es una cota superior del tiempo promedio. En el mismo
ejemplo del método burbuja mejorado, el peor caso es cuando el arreglo está ordenado de manera inversa
a la deseada. En este caso se produce la mayor cantidad de comparaciones e intercambios.
Como el análisis debe realizarse con independencia de los valores que determinan las sentencias condicionales, en
general se preere el cálculo más pesimista, es decir el peor caso. Para ello se evalúa la estructura del algoritmo
bajo la suposición de una memoria innita (es decir que no habrá demoras por intercambio de información entre
memoria primaria y secundaria).
Ejemplo:
(1) a = 1 T1 = 1 asig = 1
(2) b = 2*y + 3 T2 = 1 asig + 2 op.mát = 3
(3) c = arr[i] * 9 T3 = 1 asig + 1 op.mát + 1 acc.arreglo = 3
Secuencia s: Sea un algoritmo donde hay una sentencia S1 seguida por una o más sentencias S2, el tiempo
del bloque completo es la suma de los tiempos de cada bloque que lo compone.
Ejemplo
Algoritmo x
Tsecuencia = T1 + T2 + T3
(1) a = 1
(2) b = 2*y + 3 T1 , T2 y T3 ya fueron calculados en el ejemplo anterior, luego:
(3) c = arr[i] * 9
Fin algoritmo Tsecuencia = T1 + T2 + T3 = 1 + 3 + 3 = 7
APUNTE 1. INTRODUCCIÓN 11
Alternativas : El tiempo de ejecución de la alternativa nunca es más grande que el tiempo empleado por
la evaluación de la condición más el mayor de los tiempos de S1 y S2.
Denición
si cond hacer
S1
sino Talternativa = Tcond + max(T1 , T2 )
S2
fin si
Ejemplo
Denición
para var=min hasta max Pmax
Tf or = Tini + min (Tcond + Tint + Tincr ) + Tcond
bloque interno Tf or = Tini + (max − min + 1) ∗ (Tcond + Tint + Tincr ) + Tcond
fin para
Ejemplo en Java
Tini = 1 asig = 1
for (i=3; i<9; i++){ Tcond = 1 op.lóg = 1
a[i]=a[i+1]; Tincr = 1 op.mát = 1
Tint = 1 asig +
P2max
acc.arr + 1 op.mát = 4
} P8
Tf or = Tini + min (Tcond +Tint +Tincr )+Tcond = 1+ 3 (1+4+1)+1 =
= 1 + (8 − 3 + 1) ∗ (1 + 4 + 1) + 1 = 1 + 6 ∗ 6 + 1 = 38
Repetitiva MIENTRAS: consideramos la cantidad de repeticiones (las veces que la condición da true);
por cada repetición sumamos el tiempo para evaluar la condición (Tcond ) y el tiempo del cuerpo de
sentencias interno del bucle (Tint ), y consideramos que la evaluación de la condición (Tcond ) se ejecuta
una vez más que las que se ejecuta el bloque interno (cuando la condición es falsa).
Denición
mientras cond
bloque interno
Tmientras = cantrep (Tcond + Tint ) + Tcond
fin mientras
Ejemplo en Java
while (i<n){ Tcond = 1op.lóg = 1
a[i]=a[i+1]; Tint1 = 1asig + 2acc.arr + 1op.mát = 4
i=i+1; Tint2 = 1asig + 1op.mát = 2
Tint = Tint1 + Tint2 = 4 + 2 = 6
}
Tmientras = cantrep (Tcond + Tint ) + Tcond = n ∗ (1 + 6) + 1 = 7n + 1
Repetitiva HASTA: consideramos la cantidad de repeticiones (las veces que la condición da true);
por cada repetición sumamos el tiempo para evaluar la condición (Tcond ) y el tiempo del cuerpo
APUNTE 1. INTRODUCCIÓN 12
de sentencias interno del bucle (Tint ). A diferencia del mientras y el para, esta sentencia evalúa la
condición sólo la cantidad de veces que se ejecuta el cuerpo interno.
Denición
repetir
bloque interno
Thasta = cantrep (Tcond + Tint )
hasta cond
Ejemplo en Java
do {
a[i]=a[i+1]; Tcond = 1 op.lóg = 1
i=i+1; Tint = Tint1 + Tint2 = 4 + 2 = 6
Thasta = cantrep (Tcond + Tint ) = n ∗ (1 + 6) = 7n
} while i<=n;
A partir de la función de tiempo de ejecución : cuando se conoce la función del tiempo de ejecución,
se aísla el término que crece más rápido, despreciando las constantes que lo multiplican. Ejemplo, si
T (n) = 4n² + 5n + 8 su orden es O(n²).
A partir del código (o pseudocódigo), simplicando las sentencias de tiempo constante (orden O(1)) y
aplicando las siguientes reglas: Sean T1 (n) y T2 (n) las funciones que expresan los tiempos de ejecución de
dos fragmentos de un programa, y sus órdenes T1 (n)O(f1 (n)) y T2 (n)O(f2 (n))
Regla de la suma : El orden de la suma de dos bloques de código secuenciales es el máximo de sus
órdenes.
Regla del producto : El orden del producto de ambos bloques es el producto de sus órdenes.
O(n ∗ logn)
O(n²): orden cuadrático
Los algoritmos de complejidad O(n) y O(n ∗ log n) son los que muestran un comportamiento más natural,
prácticamente a doble cantidad de datos procesados, doble de tiempo necesario. Los algoritmos de complejidad
logarítmica son excelentes pues para resolver un problema el doble de grande sólo hace falta un poco más
de tiempo. Sobre los algoritmos de tipo polinómico, mientras complejidades del orden O(n²) y O(n³) suelen
ser aceptables, nadie acepta algoritmos de orden mayor. Finalmente, cualquier algoritmo por encima de una
complejidad polinómica se dice "intratable" y sólo será aplicable a problemas muy pequeños.
Conclusión
Antes de encarar la codicación de un programa se debe analizar y elegir un buen algoritmo. La elección de
las estructuras de datos también inuyen en la eciencia de los programas que desarrollamos, por ello debemos
ser capaces de elegir la estructura más adecuada para cada problema. Por bueno o adecuado entendemos que
utilicen el mínimo de recursos; siendo los más importantes el tiempo de ejecución y la cantidad de memoria
necesaria. En el análisis de algoritmos consideraremos siempre el peor caso o trataremos de estimar un caso
promedio. En el caso de las estructuras analizaremos el tipo de acceso que proveen y la cantidad de accesos
necesarios para cada tarea, así como la memoria que utilizan. En general, analizaremos en forma conjunta tanto
la estructura de datos a utilizar como los algoritmos relacionados. Para independizarse de factores tales como
el lenguaje de programación, la habilidad del codicador, la máquina soporte, etc. trabajaremos con el cálculo
asintótico que indica como se comporta el algoritmo para datos muy grandes o mucha cantidad de información.
Para problemas pequeños es cierto que casi todos los algoritmos son "más o menos iguales", primando otros
aspectos como esfuerzo de codicación, legibilidad, etc.
APUNTE 1. INTRODUCCIÓN 14
2. Calcular el orden de ejecución de los mismos métodos, utilizando las reglas de la suma y del producto, y
vericar que concuerdan con el cálculo a partir de la función del tiempo de ejecución.
1.5. Recursividad
La recursividad es un método usual de simplicación de un problema complejo, mediante la división de este
en subproblemas del mismo tipo. Esta técnica de programación, conocida como divide y vencerás, es el núcleo
del diseño de algoritmos muy ecientes, como el de búsqueda binaria, y los métodos de ordenamiento quicksort
y mergesort, entre otros.
En las estructuras de datos jerárquicas y de tipo grafo, la manera más adecuada de recorrerlas (y a veces la
única manera posible) es utilizando procedimientos recursivos.
A continuación repasaremos los conceptos más importantes:
Un algoritmo recursivo es un algoritmo que expresa la solución de un problema en términos de una llamada
a sí mismo. La llamada a sí mismo se conoce como llamada recursiva o recurrente.
tamaño menor que N. De esta forma, al ir reduciendo progresivamente la complejidad del problema a
resolver, llega un momento en que su resolución es más o menos trivial (o, al menos, sucientemente
manejable como para resolverlo de forma no recursiva). En esa situación diremos que estamos ante un
caso base de la recursividad.
A veces, los algoritmos recursivos pueden ser más inecientes en cuanto al tiempo de ejecución que los
iterativos, pero por lo general son mucho más sencillos (cortos en líneas de código).
Descomposición : Cada llamada recursiva se debe denir sobre un problema de menor complejidad (más
fácil de resolver).
Composición : Denir cómo se combinan las soluciones de él o los problemas más pequeños, para lograr la
solución del problema original.
Caso base : Debe existir al menos un caso base para evitar que la recurrencia sea innita.
El ejemplo del cálculo recursivo del factorial de un número es el ejemplo más habitual de resolución recursiva:
Si n <=1 devolver 1
En la Figura 1.4 se muestra el algoritmo codicado en Java, indicando las partes claves del mismo: en el
caso recursivo detectamos la descomposición del problema en un problema más pequeño, que se combina al
multiplicarlo por n a la vuelta de la recursión. Luego, es preciso que exista el caso base que permitirá terminar
la sucesión de llamadas recursivas.
Figura 1.4: Análisis del algoritmo recursivo para cálculo del factorial(n)
Un aspecto fundamental al programar algoritmos recursivos, es entender paso a paso la ejecución de los
mismos y ser capaces de gracar la pila de ejecución. En la Figura 1.5 se observa la traza para la invocación
fact(5). En la parte superior de la gura se observa el crecimiento de la pila de ejecución, indicando en un
piso nuevo cada llamada recursiva. Con el símbolo ? se indican los puntos donde ha quedado pendiente la
combinación del resultado esperado. En la parte inferior de la gura se observa la acción a la vuelta de la
recursión, donde el valor retornado por cada piso de la pila es utilizado en el piso inferior para resolver la
combinación pendiente y emitir el nuevo resultado.
APUNTE 1. INTRODUCCIÓN 16
Para realizar la traza con pila de ejecución, se dibuja un nuevo piso al comenzar la ejecución de la llamada
recursiva, luego se agrega una columna para cada parámetro y cada variable local de tipo simple. Para las
variables de tipo objeto (como arreglo en Java) se lo dibuja fuera de la pila, dado que el contenido de un objeto
es visible de igual manera en todos los pisos que lo referencian.
Como ejemplo, veamos en la Figura 1.6 un método recursivo que busca un elemento en un arreglo y devuelve
su posición. Si no lo encuentra, devuelve -1 (que es una posición inválida). Si analizamos el método podemos
reconocer dos casos base: (1) cuando encuentra el elemento en el arreglo y (2) cuando buscándolo llega más allá
del límite, es decir, cuando el elemento no está; luego existe una llamada recursiva cuando el elemento visitado
no es el buscado. La composición consiste en la asignación del valor de la llamada recursiva a la variable de
salida.
En la Figura 1.7 se muestra la traza para la invocación buscarElem(tres, arr, 0). En la parte superior de
la gura se observa paso a paso el crecimiento de la pila de ejecución. Como se puede ver, se ha dibujado una
columna para cada parámetro (buscado, arr, pos) y variable local (salida). Para el arreglo, al ser de tipo objeto,
se indica que la variable apunta o referencia al objeto arr que todos los pisos comparten. De esta manera, un
cambio del contenido de arr en cualquiera de los pisos de la pila, será visible en todos los otros que referencien
al mismo objeto. En la parte inferior de la gura se observa la acción a la vuelta de la recursión, donde el
APUNTE 1. INTRODUCCIÓN 17
valor retornado por cada piso de la pila es utilizado en el piso inferior para actualizar la propia variable local
salida y luego emitir el nuevo resultado.
Figura 1.7: Traza del algoritmo recursivo que busca un elemento en un arreglo
f(0) = 0
f(1) = 1
Generar la sucesión de Fibonacci es un típico ejemplo de recursividad con más de una invocación. El código en
Java se muestra en la Figura 1.8. Este ejemplo tiene la particularidad que para realizar la traza es necesario
saber claramente desde qué punto del módulo se ha invocado cada vez. Para ello se ha nombrado a la primera
invocación con la letra A y a la segunda con la letra B.
En la Figura 1.9 se observa la traza para la invocación b(4). De manera similar, en la la (1) se observa
el crecimiento de la pila de ejecución, indicando en un piso nuevo cada llamada recursiva. Con el símbolo ?
se indican los puntos donde ha quedado esperando, pendiente del resultado de la invocación. Las letras A y
B se ponen a la izuierda de cada piso de la pila para marcar desde dónde se realizó la llamada, para volver
exáctamente al punto del programa que debe resolverse. En el caso del piso de más abajo, que representa la
invocación original, no se le pone letra porque se sabe que es desde un módulo externo. A partir de la la (2) en
adelante, cuando termina de calcular la primera invocación (A), llama desde la segunda (B), produciendo que
la pila crezca y decrezca hasta cumplir con todas las llamadas necesarias.
APUNTE 1. INTRODUCCIÓN 18
(1)
(2)
(3)
(4)
(5)
(6)
(7)
1. Siguiendo el código de la Figura 1.4 realizar una traza con pila para calcular el valor del factorial de 4.
2. Siguiendo el código de la Figura 1.6 realizar una traza con pila para buscar el elemento seis en el arreglo.
3. Siguiendo el código de la Figura 1.8 realizar una traza con pila para calcular el valor del término 5 de la
sucesión de Fibonacci.
4. Escriba una subrutina recursiva para cada inciso siguiente, considerando que todas reciben un String por
parámetro:
5. Diseñe un lote de pruebas apropiado y haga trazas con pilas para asegurar la correctitud de cada subrutina
recursiva del ejercicio anterior.
6. Dado un arreglo de números enteros, implementar un método recursivo que en un único recorrido imprima
los elementos menores que el promedio de todos los números cargados. Por ejemplo: dado el arreglo
[10,5,3,7,11,1] el promedio es (10+5+3+7+11+1)/6=37/6=6.166, luego debe imprimir 1, 3, 5
(*) Pista : calcular el promedio a la ida de la recursión e imprimir los elementos menores a la vuelta.
7. Diseñe un lote de pruebas apropiado y haga trazas con pila para asegurar la correctitud del ejercicio
anterior.
a ) Escribir un método recursivo explotar, que dado un número n y un número bomba b, imprima
los pedazos que quedan al explotar n usando b. (En el caso que muestra la gura debe imprimir
1,1,3,3,2,1,1,3)