Vous êtes sur la page 1sur 824

1

Estructuras de Datos y Algoritmos.

Usando lenguaje C.

Sinopsis.

El texto está orientado a un segundo curso de programación en planes de estudios de ingenieros


eléctricos, electrónicos, telemáticos y de ciencias de computación.
Se asume que en un curso previo de programación de computadores, se han dominado las reglas
para la construcción de: expresiones, condiciones, alternativas y repeticiones; también la
correcta forma de diseñar funciones pasando los parámetros con los tipos adecuados, y la
elección de las variables locales y globales; así también la forma de comunicar los resultados de
las funciones; también se asume como prerrequisito el empleo de las bibliotecas de entrada-
salida y las matemáticas. Por el lado de los datos se asume que se conocen los tipos básicos de
datos, tanto en su forma de definir variables como en las de su manipulación.

El texto desarrolla en profundidad los mecanismos recursivos de agrupación de los tipos


básicos de datos en estructuras y arreglos, con los cuales se puede seguir elaborando estructuras
tan complejas como sea necesario; también mediante la vinculación, empleando cursores o
punteros, se pueden establecer relaciones entre componentes de la estructura de datos.

Mediante la agrupación y vinculación se describen estructuras abstractas de datos como:


listas, árboles, conjuntos, grafos. Usando estas estructuras abstractas de datos pueden modelarse
los datos de sistemas reales más complejos, como: sistemas operativos, la información que viaja
en los paquetes de datos de una red, o complejas relaciones entre componentes de datos en un
sistema de bases de datos, por mencionar algunos ejemplos.

El texto centra su atención en el diseño de algoritmos para realizar las acciones básicas de:
Ordenar, buscar, seleccionar y calcular, empleando diferentes estructuras de datos. Por
ejemplo, se pueden ordenar los elementos de una lista o de un arreglo; se puede buscar un valor
en un árbol, en una lista o en un arreglo.

La elección adecuada de la estructura de datos y del algoritmo empleado permite obtener un


diseño eficiente, tanto en recursos ocupados como en el tiempo de ejecución. Parte importante
del curso estará centrada en lograr una medida de la eficiencia de cada algoritmo y su
comportamiento a medida que aumenta el número de elementos que constituyen los datos; es
decir, de su complejidad.

Profesor Leopoldo Silva Bijit 17-07-2009


2 Estructuras de Datos y Algoritmos
Estos algoritmos eficientes tienen importantes aplicaciones: en el diseño de sistemas operativos,
en la programación de sistemas y de aplicaciones en red, en la elaboración de sistemas de bases
de datos, en la construcción de compiladores y aplicaciones multimediales, por nombrar las más
importantes.

La formación de un ingeniero debe contemplar un balance adecuado entre la teoría y la práctica.


Por un lado se requiere disponer de un marco conceptual que permita pensar y reflexionar sobre
un determinado problema; por otro, la capacidad de aplicar las ideas a situaciones reales.

En este curso, la capacidad de realización la entenderemos como el conjunto de diseñar


estructuras de datos y algoritmos eficientes, mediante el lenguaje de programación C; la
depuración de programas, la verificación de su funcionamiento correcto, a través de someterlos
a datos de prueba escogidos convenientemente. Todos los temas desarrollados en el texto,
contienen las definiciones de las estructuras de datos y las funciones que las manipulan, junto a
un programa principal que efectúa un test de las funciones.

Contenidos.

El texto está organizado en 5 secciones, 25 capítulos y 3 apéndices, con numerosos problemas


resueltos y ejercicios propuestos.

La sección de Conceptos básicos, es el núcleo del curso, y contiene las ideas principales del
texto.

La sección Buscar, ilustra algoritmos más evolucionados de búsqueda. De los cuales pueden
seleccionarse algunos capítulos para exponer en clases, otros pueden plantearse como proyectos
ha ser desarrollados en grupos de trabajo. Se incluyen los algoritmos clásicos deterministas y
también los aleatorizados y amortizados. El algoritmo Treaps, ilustra el diseño de algoritmos en
base a los desarrollados en Conceptos básicos, mostrando que éstos constituyen elementos de
diseño.

La sección Seleccionar muestra algoritmos más elaborados de selección. Algunos de ellos


ilustran el uso de listas doblemente enlazadas desarrolladas en Conceptos básicos.

En la sección Calcular se han seleccionado dos temas que son de interés en electrónica. Se
muestran los principales algoritmos empleados en los simuladores analógicos, y se desarrolla la
transformada rápida de Fourier.

Los Apéndices contienen algunos conceptos formales sobre lenguajes, una exposición básica del
lenguaje C, y una breve descripción de análisis léxico, con el objeto de facilitar el llenado de las
estructuras con datos provenientes de archivos de texto. Lo cual puede emplearse para probar
las funciones con una cantidad realista de datos.

Profesor Leopoldo Silva Bijit 17-07-2009


Prólogo y Contenidos 3

Conceptos básicos.

Capítulo 1 Introducción.
Capítulo 2 Diseño de estructuras de datos.
Capítulo 3 Administración de la memoria.
Capítulo 4 Complejidad temporal.
Capítulo 5 Listas, stacks, colas.
Capítulo 6 Árboles binarios de búsqueda.
Capítulo 7 Tablas de hash.
Capítulo 8 Colas de prioridad.
Capítulo 9 Ordenar.
Capítulo 10 Grafos.

Buscar.

Capítulo 11 Árboles Binarios Balanceados. AVL.


Capítulo 12 Árboles Coloreados
Capítulo 13 Árboles desplegados(splay trees)
Capítulo 14 Árboles enhebrados. Threaded Trees
Capítulo 15 Árboles AA
Capítulo 16 Treaps
Capítulo 25 Skip list
Capítulo 17 B-Trees

Seleccionar.

Capítulo 18 Pagodas
Capítulo 19 Leftist
Capítulo 20 Skewheap
Capítulo 21 Árboles binomiales
Capítulo 22 Pairing heaps

Calcular.

Capítulo 23 Algoritmos numéricos


Capítulo 24 Transformada rápida de Fourier

Profesor Leopoldo Silva Bijit 17-07-2009


4 Estructuras de Datos y Algoritmos
Apéndices.

Apéndice 1 Descripción formal de lenguajes.


Apéndice 2 Programación en C.
Apéndice 3 Analizadores léxicos.

Profesor Leopoldo Silva Bijit 17-07-2009


1

Capítulo 1.

Introducción a las Estructuras de Datos y


Algoritmos.

1.1. Estructura de los datos.

La información se representa mediante bits. Los bits se organizan en bytes y palabras de


memoria, mediante las cuales se representan los tipos básicos de datos.

Mediante los mecanismos de agrupación en estructuras y arreglos se pueden seguir elaborando


estructuras más complejas; mediante la vinculación, empleando cursores o punteros, se pueden
establecer relaciones entre componentes de la estructura de datos.

Con estos elementos pueden describirse estructuras abstractas de datos como: listas, árboles,
conjuntos, grafos. Usando estructuras abstractas de datos se pueden modelar los datos de
sistemas más complejos, como: sistemas operativos, la información que viaja en los paquetes de
datos de una red, o complejas relaciones entre componentes de datos en un sistema de bases de
datos, por mencionar algunos ejemplos.

Las relaciones de los elementos de datos, en sus diferentes niveles, pueden conceptualizarse en
el siguiente diagrama:

Datos en Sistemas operativos Paquetes en Redes


Bases de datos

Árboles, conjuntos, heaps Grafos


Listas, stacks, colas
Creación de nuevos tipos Datos en el heap
Datos en el stack
Arreglos, estructuras Ingresar y desplegar datos
punteros
int , unsigned int, long float, double
char, unsigned char
Byte, palabra

bit

Figura 1.1. Estructuras de datos.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Los dos niveles inferiores son cubiertos en asignaturas sobre sistemas digitales.
Los dos niveles siguientes deberían cubrirse en un curso básico de programación.
Los cuatro niveles inferiores también son tratados en cursos de estructuras de computadores, en
el cual se elaboran las estructuras de datos del lenguaje de alto nivel, mediante los mecanismos
de estructuración de datos y direccionamientos assembler; los que a su vez son explicados en
términos de los modos de direccionamiento de las instrucciones de máquina; en este nivel
también se describe el uso de los diferentes registros y segmentos de memoria, y la
configuración detallada de los frames en el stack.

Los niveles quinto y sexto son el tema de este texto.

1.2. Estructura de las acciones. Algoritmos.

En su forma más primitiva la agrupación de compuertas permite el desarrollo de acciones


combinacionales, entre ellas: la unidad aritmética y lógica, unidades de corrimiento, extensores,
muxes. Con la ayuda de elementos de memoria y conducidas por un reloj, se pueden elaborar
máquinas secuenciales que efectúen las acciones de contar, desplazar, multiplicar y dividir, así
también el generar las acciones de control que gobiernan las transferencias entre los recursos
básicos de un procesador. Esto se cubre en un curso de Sistemas Digitales.

Luego de esta estructuración, las acciones realizadas electrónicamente pueden ser abstraídas
como instrucciones de máquina. Mediante la ayuda de compiladores es posible traducir las
acciones o sentencias del lenguaje de alto nivel en instrucciones de máquina. En el nivel de los
lenguajes de programación, se dispone de operadores que permiten construir expresiones y
condiciones; agregando las acciones que implementan las funciones combinacionales se pueden
abstraer las acciones en términos de alternativas, condicionales y switches; la capacidad de
implementar máquinas de estados permite la realización de iteraciones y repeticiones. Las
organizaciones y arquitecturas se estudian en un curso de estructuras de computadores.

Sistemas operativos Aplicaciones en Redes


Sistemas de Bases de datos

Seleccionar Buscar, ordenar Calcular


funciones bibliotecas
Proyectos, módulos
Alternativas, condicionales Iteraciones, recursión
Expresiones, condiciones
Instrucciones de máquina

ALU, muxes, Buses, Control Máquinas secuenciales


Funciones combinacionales

Memorias primitivas, compuertas

Figura 1.2. Estructura de acciones.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 3
El siguiente nivel es el de un curso de programación, el que debería cubrir: las reglas para la
construcción de expresiones, condiciones, alternativas y repeticiones; también la correcta forma
de diseñar funciones pasando los parámetros con los tipos adecuados, y la elección de las
variables locales y globales; así también la forma de comunicar los resultados de las funciones.
En un curso básico de programación también se domina el empleo de las bibliotecas de entrada-
salida y las matemáticas.

El tema cubierto en este texto centra su atención en el diseño de algoritmos para realizar las
acciones básicas de: Ordenar, buscar, seleccionar y calcular, empleando diferentes
estructuras de datos. Por ejemplo, se pueden ordenar los elementos de una lista o de un arreglo;
se puede buscar un valor en un árbol, en una lista o en un arreglo.

La elección adecuada de la estructura de datos y del algoritmo empleado permite obtener un


diseño eficiente, tanto en recursos ocupados como en el tiempo de ejecución. Parte importante
del curso estará centrada en lograr una medida de la eficiencia de cada algoritmo y su
comportamiento a medida que aumenta el número de elementos que constituyen los datos; es
decir, de su complejidad.

Estos algoritmos eficientes tienen importantes aplicaciones: en el diseño de sistemas operativos,


en la programación de sistemas y de aplicaciones en red, en la elaboración de sistemas de bases
de datos, en la construcción de compiladores y aplicaciones multimediales, por nombrar las más
importantes.

1.3. Lenguajes.

En los diferentes niveles de datos y acciones se emplean diferentes lenguajes.

En los niveles físicos, se emplean Lenguajes de Descripción de Hardware (HDL). Para describir
los procesadores se emplea lenguaje assembler, cada procesador tiene un lenguaje assembler
propio.
En lenguajes de alto nivel ha existido una rápida evolución. Muchos de los que fueron más
universalmente usados han pasado a la historia: Fortran, Algol, APL, Pascal, Modula, Basic.

Los primeros lenguajes ofrecían al programador la posibilidad de efectuar saltos. El primer


avance evolutivo fundamentado fue impedir que el programador pudiese emplear los saltos, y
que aceptase formas estructuradas de organizar las acciones. Es decir que construyera
algoritmos organizando las acciones en secuencias, alternativas o iteraciones.

El siguiente modelo conceptual fue el desarrollo modular en el cual se definían estructuras de


datos y las funciones que las manipulaban; es decir la concepción de tipos abstractos de datos.

Los lenguajes como Pascal, C y otros tienen la posibilidad de programación de tipos abstractos.
El lenguaje C evolucionó rápidamente para convertirse en uno de los más usados. Su principio
de diseño es que compile eficientemente y que permita usar los recursos de hardware de los
procesadores. Por esta razón se lo seguirá empleando en cursos de estructuras de computadores
y en el diseño de microcontroladores. Sin embargo sus capacidades de manejo de memoria

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
dinámica y el empleo de punteros, que son sus principales ventajas, son sus principales fuentes
de errores en la elaboración de grandes programas.

El modelo actual es el diseño y la programación orientada a objetos, en el cual se impide al


programador emplear punteros, y el manejo de la memoria dinámica es responsabilidad del
lenguaje y no del programador. Los datos y las acciones conforman el objeto, no están
separadas. Existen varios de estos lenguajes, Java es uno de los que más se ha difundido.

Seguramente con los años aparecerán nuevos y mejores lenguajes, pero los principales
conceptos que aprendamos en este texto seguramente perseverarán, ya que son básicos.

1.4. Genialidades.

Los más importantes algoritmos que estudiaremos emplean ideas geniales y brillantes. Desde
que son descubiertos se los empieza a usar en las aplicaciones, debido a sus ventajas. Sin
embargo exponer ideas: a veces sofisticadas, otras decididamente rebuscadas, en algunos casos
aparentemente simplistas, y que han resuelto grandes problemas son parte de la dificultad de
este curso.

No se estudian cuestiones sencillas, la mayor parte de ellas son muy elaboradas, y algunas veces
difíciles de captar en su profundidad.

Debido a lo anterior, no se espera que después de estudiar este texto se esté en condiciones de
inventar algoritmos que pasen a la historia, pero si conocer las principales estructuras abstractas
de uso general, sus posibles aplicaciones, y los mejores algoritmos conocidos para esas
estructuras.

1.5. Teoría y práctica.

La formación de un ingeniero debe contemplar un balance adecuado entre la teoría y la práctica.


Por un lado se requiere disponer de un marco conceptual que permita pensar y reflexionar sobre
un determinado problema; por otro, la capacidad de aplicar las ideas a situaciones reales.

En el caso particular de este curso, la capacidad de realización la entenderemos como el


conjunto de diseñar estructuras de datos y algoritmos eficientes, mediante el lenguaje de
programación C; la depuración de programas, la verificación de su funcionamiento correcto, a
través de someterlos a datos de prueba escogidos convenientemente.

1.6. Definiciones.

1.6.1. Algoritmo.

Secuencia finita de operaciones, organizadas para realizar una tarea determinada.

Cada operación debe tener un significado preciso y debe ser realizada en un lapso finito de
tiempo y con un esfuerzo finito. Un algoritmo debe terminar después de ejecutar un número
finito de instrucciones.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 5

Algoritmos diferentes pueden completar la misma tarea con la ejecución de un conjunto de


instrucciones diferentes, en más o menos tiempo, y empleando más o menos memoria. Es decir,
pueden tener complejidad y costo diferentes.

Se pueden analizar algoritmos, en forma abstracta, es decir sin emplear un determinado lenguaje
de programación; el análisis se centra en los principios fundamentales del algoritmo y no en una
implementación en particular. En estos casos puede emplearse para su descripción un
pseudocódigo.

Existen métodos cuantitativos para determinar los recursos espaciales y temporales que requiere
un algoritmo. En particular se estudiarán métodos para calcular la complejidad temporal de un
algoritmo.

1.6.2. Heurística.

Un algoritmo que produce soluciones razonablemente rápidas y buenas, pero no necesariamente


la solución óptima.

1.6.3. Estructuras de datos.

La forma en que se organizan los datos para ser usados.

Es una colección de variables, posiblemente de diferentes tipos de datos, conectadas de un modo


determinado.

Una estructura de datos bien organizada debe permitir realizar un conjunto de acciones sobre los
datos de tal forma de minimizar el uso de los recursos y el tiempo empleado para efectuar la
operación.

1.7. Algoritmos clásicos.

Se describen algunos algoritmos, considerados clásicos, para ilustrar el camino que debe
recorrerse desde su diseño hasta traducirlo a un lenguaje de programación.

En opinión del Profesor Dijkstra, las computadoras manipulan símbolos y producen resultados;
y un programa, que describe un algoritmo, es un manipulador abstracto de símbolos.
Visto de este modo un algoritmo es una fórmula de algún sistema formal; sin embargo debido a
que estas fórmulas resultan mucho más largas y elaboradas que las usuales, no suele
reconocérselas como tales.

En primer lugar debe describirse lo que se desea realizar en lenguaje natural, desgraciadamente
esto puede producir más de alguna ambigüedad. Luego debe modelarse matemáticamente la
situación que interesa describir; es decir, apoyado en conceptos matemáticos y lógicos se
describe la función o fórmula que debe realizarse, empleando un pseudolenguaje; que es más
preciso y formal que el lenguaje común. Una vez obtenida una representación formal se la
traduce empleando un lenguaje de programación.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

No es sencillo derivar el algoritmo desde un programa que lo implementa. Lo cual puede


observarse intentando leer programas, sin disponer antes de la descripción del algoritmo.
Sin embargo el paso de la descripción en pseudocódigo a un lenguaje de programación suele ser
una actividad de menor complejidad; además, esto permite su codificación en diferentes
lenguajes de programación.

1.7.1. Algoritmo de Euclides.

Euclides fue un matemático griego que vivió alrededor del año 300 a.C. una de sus
contribuciones es el algoritmo para obtener el máximo común divisor de dos números enteros,
lo cual sería la descripción en lenguaje natural, del problema que se desea resolver.

Luego intentamos modelar matemáticamente la situación.

Si x e y son enteros, no ambos ceros, su máximo común divisor, que anotaremos mcd(x,y), es el
mayor entero que los divide a ambos exactamente; es decir, sin resto.

Ejemplo: mcd( 7, 11) = 1


mcd(16, 28) = 4

Puede comprobarse que:

mcd( x, 0) = |x|
mcd( x, y) = mcd(y, x)
mcd(-x, y) = mcd(x, y)

De lo cual se desprende que es de interés obtener un algoritmo para el mcd de enteros no


negativos, el conocimiento matemático que se necesita, para resolver este problema es el
siguiente:

Si los números son x e y:


a) Si x es igual a y; entonces x (ó y) es el resultado.
b) Si se reemplaza el número mayor por la diferencia del mayor menos el menor, no cambia el
máximo común divisor.

Lo anterior puede plantearse en términos matemáticos, según:


a) mcd(x, x) = x
b) Si x > y se tiene mcd(x, y) = mcd(x-y, y)

Entonces para enteros mayores que cero, tenemos el siguiente algoritmo, descrito en lenguaje
natural:

Mientras los números sean diferentes:


Deje el menor, y forme otro restando el menor al mayor.
Lo cual puede plantearse, empleando el lenguaje C:

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 7
while (x!=y) if (x>y) x=x-y; else y=y-x;

Como veremos, a través de los siguientes ejemplos, existen numerosas formas de describir un
algoritmo.

Otra forma de describir el Algoritmo de Euclides es la siguiente:

do
{
while (x>y) x=x-y;
while (y>x) y=y-x;
} while (x!=y);
// x es el mcd

Veremos otra forma del algoritmo.


Debido a que puede comprobarse que:

mcd(x, y) = mcd(y, x mod y)

La expresión muestra cómo reducir uno de los números, manteniendo en el primer lugar al
mayor. La transformación debe repetirse hasta que el menor de los números sea igual a cero.
El cambio de posición de los números requiere una variable temporal que denominaremos resto.

Entonces una variante del algoritmo de Euclides es la siguiente:

while (y!=0)
{ resto = x % y;
x=y;
y=resto;
}
// x es el mcd

La siguiente función iterativa resume las ideas anteriores:

int mcd(int x, int y)


{ int resto;
if (x==0) return (y);
if (y==0) return (x);
while (y!=0)
{ resto=x-y*(x/y); //resto=x % y;
x=y;
y=resto;
}
return (x);
}
Debido a puede definirse en forma recursiva la función máximo común divisor, según:

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
mcd( x, 0) = |x|,
mcd(x, y) = mcd(y, x mod y)

Se puede describir el algoritmo de Euclides, en forma recursiva:

int mcd(int x, int y)


{
if (y == 0) return x;
return mcd(y, x % y);
}

1.7.2. Algoritmo el colador de Erastótenes.

Erastótenes fue un matemático griego (275-194 A.C.) que desarrolló un algoritmo que se
conoce como la criba de Erastótenes (sieve en inglés), que permite determinar los números
primos menores que un número dado. El nombre deriva de un “colador” en que se colocan
todos los números, y sólo deja pasar los números primos.

Para desarrollar el modelo matemático que permita elaborar el algoritmo, pueden emplearse las
siguientes definiciones de un número primo:

Un número primo es un entero positivo que no es el producto de dos enteros positivos menores.

Un número primo es un entero positivo que tiene exactamente dos factores enteros positivos: 1
y sí mismo.

Entonces el algoritmo, puede describirse informalmente, según:


Se crea arreglo de números booleanos con el tamaño de los números que se desea probar si son
o no primos y se los marca como primos. El índice del arreglo está asociado al número entero.
Se marca el elemento con índice 1 como no primo. Ya que no existen dos enteros diferentes
menores que 1.
Se recorre el arreglo, en forma ascendente, dejando el siguiente número, previamente marcado
como primo al inicio, como primo; y todos los múltiplos de éste, se marcan como no primos.

Puede comprobarse que el primer múltiplo a eliminar es el cuadrado del número.

El siguiente programa ilustra el algoritmo, se ha agregado la función mostrar para efectuar un


listado de los números primos. Se listan los primos menores que la constante n.

#include <stdio.h>
#define n 40
#define primo 1
#define noprimo 0

int a[n];

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 9
void mostrar(void)
{ int i;
for (i= 2; i<=n; i++)
if (a[i]==primo) printf(" %d ", i);
}

void Eratostenes(void)
{ int i, p, j;
a[1] = noprimo; for (i= 2; i<=n; i++) a[i]= primo;
//Se inicia arreglo, marcando todos los números como primos, excepto el primero.
p = 2;
while (p*p <= n)
{
j = p*p; //El primer múltiplo de p no primo es su cuadrado.
while (j <= n)
{
a[j] = noprimo;
j = j+p; //siguiente múltiplo de p
}
//printf("Elimina múltiplos de %d: ", p); mostrar(); putchar('\n');
do p = p+1; while (a[p] == noprimo); //se salta los no primos.
}
}

int main(void)
{
Eratostenes();
mostrar();
return (0);
}

La criba de Atkin es un algoritmo moderno, más rápido, para encontrar todos los primos hasta
un entero dado

1.7.3. Permutaciones en orden lexicográfico. Algoritmo de Dijkstra (1930-2002).

La permutación f precede a la permutación g en orden lexicográfico si y sólo si para el mínimo


valor de k tal que f(k) != g(k), se tiene f(k) < g(k).

Si suponemos un arreglo de dígitos enteros, con índices desde 1 a n, la generación de la


permutación siguiente a la del arreglo inicial, está basada en encontrar el dígito menor que el
siguiente, a partir de la posición más significativa; luego se busca a partir del último el primer
dígito que sea mayor que el menor ya seleccionado, y se los intercambia. Luego se reordenan,
en orden creciente, los dígitos restantes a partir del siguiente a la posición donde se depositó el
mayor.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
Para entender el algoritmo suponemos un arreglo de enteros en el cual almacenamos los dígitos:
1, 3, 2, 5 y 4 y deseamos generar la permutación siguiente a la almacenada.

Si tenemos la permutación: 13254, el dígito dos es menor que 5, y el primero mayor que 2 es 4.
Resulta, luego del intercambio: 13452. Lo que resta es reordenar los dígitos siguientes a 4, hasta
la posición del último, es este ejemplo hay que intercambiar el 2 con el 5, resultando: 13425 que
es la permutación siguiente, en orden lexicográfico, a la inicial.

Veamos otro caso, a partir de: 15342. El primero menor que el dígito siguiente es el 3; y el
primero mayor que 3 es el dígito 4; luego del intercambio, se tiene: 15432. Y reordenando los
dígitos siguientes a 4, se obtiene la siguiente permutación: 15423.

Encontrar el dígito menor que el siguiente, a partir de la posición más significativa, puede
escribirse:
i = n-1; while (a[i] >= a[i+1]) i--;

Al salir del while se cumple a[i] < a[i+1]

Nótese que si i es menor que uno, al salir del while, la secuencia es monótonamente decreciente,
y no existe una permutación siguiente. Se asume que en la posición 0 del arreglo se ha
almacenado un dígito menor que los dígitos almacenados en las posiciones de 1 a n. Esto es
importante ya que debe asegurarse que el while termine, y también porque el lenguaje C no
verifica que el índice de un arreglo esté dentro del rango en que ha sido definido.

Si existe la próxima permutación: Se busca a partir del último, el primer dígito que sea mayor
que el menor ya seleccionado, lo cual puede traducirse según:

j = n; while (a[i] >= a[j]) j--;

Al salir del while se cumple a[i] < a[j]

Para el intercambio de los dígitos en posiciones i y j, se requiere una variable auxiliar temp, el
uso del siguiente macro simplifica la escritura del intercambio.

#define swap(i, j) temp=a[i], a[i] = a[j], a[j] = temp

El reordenamiento en orden creciente, está basado en la observación de que la secuencia de


números a reordenar es monótonamente decreciente, y por lo tanto basta intercambiar el primero
con el último de la subsecuencia y así sucesivamente. No importando si el número de
componentes es par o impar. El siguiente segmento reordena en forma ascendente, la secuencia
descendente, desde i+1 hasta n;
j = n;
i = i+1;
while (i<j)
{ swap(i, j);
j--; i++;
}

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 11

El reordenamiento ascendente de la subsecuencia descendente, puede codificarse, en forma más


compacta, empleando una sentencia for, en la que se emplea el operador secuencial coma:

for( i++, j = n; i<j ; i++, j--) swap(i, j);

La función getNext obtiene la siguiente permutación, de n dígitos, a partir de una dada,


almacenada en el arreglo a. El resultado queda en el arreglo a. Debe prevenirse invocarla
cuando el arreglo almacena la permutación mayor.

Si se decide que el retorno de la función sea verdadero si se generó la próxima permutación y un


retorno falso implica que no se puede generar la siguiente, porque es la última, se tiene:

int getNext(int a[], int n)


{ int i , j, temp;
a[0] = -1; //centinela
for( i = n-1; a[i] >= a[i+1]; i--); if( i<1) return ( 0);
for (j = n; a[j] <= a[i]; j--);
swap(i, j); // intercambia valores en posiciones (i) y (j)
for( i++, j = n; i<j ; i++, j--) swap(i, j); return(1); //reordena
}

La siguiente función calcula, en forma iterativa, el número de permutaciones.


//Calcula n factorial. Número de permutaciones
long int P(int n)
{ int j; long int per=1L;
for (j=1; j<=n; j++) per=per*j;
return(per);
}

Se ha usado un entero largo para el resultado, debido al rápido crecimiento de la función


factorial.

Problemas resueltos.

P1. Combinaciones en orden lexicográfico.

Una combinación es una colección no ordenada de elementos únicos. Si se tiene un conjunto C


de elementos únicos (no repetidos) una combinación es un subconjunto de C.

Si tenemos que el conjunto C está formado por los dígitos {1, 2, 3, 4, 5} las combinaciones
formadas con tres elementos del conjunto, en orden creciente numérico, son la 6 siguientes:
123
124
125
134
135

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
145

Se inspecciona la cifra más significativa de la combinación, y si es menor que el mayor número


del conjunto C, se incrementa la última cifra en uno, generando la combinación siguiente. Pero
si la última es igual al último del conjunto, debe inspeccionarse la cifra anterior.
Se inspecciona la cifra anterior de la combinación, hasta encontrar una cifra menor a la
máxima en esa posición. Una vez encontrada, ésta se incrementa en uno, y se completa la
combinación con los sucesores.

Podemos describir con más detalle: si la combinación tiene k elementos y el conjunto tiene n
elementos. En la posición k-ésima de la combinación el mayor número que puede escribirse es
el n-ésimo del conjunto. En la posición (k-1)-ésima de la combinación el mayor número que
puede escribirse es el (n-1)-ésimo del conjunto. La última combinación tiene en la posición 1 de
la combinación el número (n-k+1) del conjunto.

Con n=7 y k=5, si se tiene la combinación 12567, la segunda cifra de la combinación es menor
que el máximo en esa posición, que corresponde a 4. Entonces se incrementa el 2 en uno, y se
completa la combinación, generando: 13456. En este caso, la última combinación que puede
generarse es 34567.

El número de combinaciones de k elementos sobre un total de n, está dado por C(n, k) donde C
es el coeficiente binomial.

Se tiene la siguiente definición recursiva del coeficiente binomial.

C(n, 0) = 1 n >=0
C(n, k) = 0 n<k
C(n, k) = C(n-1, k) + C(n-1, k-1) 0<=k<=n

La siguiente función efectúa el cálculo en forma recursiva:

int Cr(int n, int k)


{
if (k==0) return (1); //asume n>=0
else
if (n<k) return (0);
else return(Cr(n-1, k) + Cr(n-1, k-1));
}

La siguiente definición del coeficiente binomial puede implementarse en forma iterativa.

C(n, k) = n! / k!(n-k)!

Que puede escribirse:

C(n, k) = (n-k+1)(n-k+2)..(n) / 1*2*3*..*k

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 13
La cual puede codificarse según:

int C(int n, int k)


{ int j; long int num, den;
num=1L; den=1L;
for (j=1; j<=k; j++)
{ num=num*(n+1-j);
den=den*j;
//printf("%ld %ld\n", num,den);
}
return(num/den);
}

Se emplean enteros largos para num y den que crecen rápidamente.

La siguiente función, muestra los elementos del arreglo, desde 1 a n.


void mostrarcomb(int a[], int n)
{ int i;
for (i=1; i<=n; i++) printf( " %2d", a[i] );
putchar('\n');
}

Si suponemos un arreglo de enteros únicos (no repetidos), con índices desde 1 a n, y valores
entre 1 y n, la generación de las combinaciones de k elementos, mayores en orden lexicográfico
que la inicial, pueden generarse mediante:

void combine(int a[], int n, int k)


{ int j,i=k,t;
mostrarcomb(a,k);
while(1)
{
t=n-k; while (a[i]==t+i && i>0 ) i--;
//La posición i marca una cifra menor a la máxima en esa posición.

if(i==0) break; //para asegurar el término del lazo while externo


t=a[i]-i+1; // completa la combinación con los sucesores.
for(j=i; j<=k; j++) a[j]=t+j;

mostrarcomb(a,k);
i=k; /revisa la siguiente.
}
}

La función que permite generar la siguiente combinación, a partir de una inicial, puede extraerse
del algoritmo anterior.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos

void getNextC(int a[], int n, int k)


{ int j, i=k, t=n-k;
while (a[i]==t+i && i>0 ) i--;
t=a[i]-i+1; for(j=i; j<=k; j++) a[j]=t+j;
}

Debe evitar invocarse a la función pasando como dato inicial, la última combinación.

El siguiente segmento, genera todas las combinaciones, invocando a getNextC.

for (i=0; i <= N; i++) a[i] = i;


mostrarcomb(a, k); //Muestra combinación inicial
j=Cr(N, k);
for (i=0; i < j-1; i++)
{getNextC(a, N, k); mostrarcomb(a, k);}

La siguiente fórmula genera todas las combinaciones de k elementos de un conjunto de n


elementos únicos. Obtener el algoritmo empleado, a partir del código no resulta una tarea
sencilla, como puede verificarse.

void combine(int a[], int n, int k)


{
int p, b;
mostrarcomb(a, k); //escribe la inicial
p=1;
while (p<=k)
{
if (a[k+1-p] < n+1-p) //si es menor genera la siguiente
{
b=a[k+1-p]; //en b recuerda la cifra que debe incrementarse en uno
while (p>=1)
{
a[k+1-p]=b+1; //produce secuencia de sucesores.
b++;
p--;
}
mostrarcomb(a, k);
p=1;
}
else p++; //si es igual aumenta p
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 15
P2. Cálculo del recíproco.

En los primeros computadores no estaba implementada la operación de división, en algunos sólo


se disponía de sumas y multiplicaciones. Se diseñaron algoritmos para implementar las
funciones para obtener el recíproco y la raíz cuadrada de un número real mediante sumas y
multiplicaciones.

Desarrollaremos el algoritmo para obtener el recíproco, de un número real positivo a, como la


raíz de f(x):
1
f ( x) a
x
Emplearemos la aproximación de Newton, que genera el valor (k+1)-ésimo mediante el punto
de intersección de la tangente a la función, en el valor k-ésimo de x, con el eje x.

f(x)=(1/x) -a

1/a x

xk xk+1

Figura 1.3. Secuencia de aproximación usando el método de la tangente.

Debe notarse que si el valor inicial de x se escoge muy grande, no se producirá el acercamiento
a la raíz. La pendiente en x = 2/a, intercepta al eje x en cero, y en este punto la función tiene
derivada con valor infinito; por lo tanto puede escogerse un valor inicial en el intervalo:
0 < x0 < 2/a, para garantizar la convergencia.

De la Figura 1.3, se obtiene para la tangente:


f ( xk 1 ) f ( xk ) f ( xk )
f '( xk )
xk 1 xk xk 1 xk
Que permite obtener, la recurrencia:
f ( xk )
xk 1 xk
f '( xk )
Reemplazando la función y su derivada, evaluadas en el punto actual, se obtiene:
1
a
xk
xk 1 xk 2 xk axk2
1
xk2

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Debe notarse que el nuevo punto de aproximación se obtiene empleando solamente sumas y
multiplicaciones.

Si definimos el error relativo de la aproximación, en la iteración k-ésima, mediante:


1
xk
a
ek 1 axk
1
a
Reemplazando la relación de recurrencia, se obtiene:

ek 1 a(2 xk 1 axk2 1 ) (1 axk 1 )2 ek2 1

Relación que muestra que el error disminuye cuadráticamente. Si el error es una etapa es
pequeño, en la etapa siguiente disminuye mucho más.

Expresando en términos del error relativo, se obtienen:

xk (1 1 axk 1 ) xk 1 (1 ek 1 ) xk 1
2
ek ek 1

Si el valor inicial se escoge, dentro del intervalo de convergencia, por ejemplo en x = 1, se


tienen los valores iniciales:
x0 1
e0 1 a
La codificación de un algoritmo, basado en relaciones de recurrencia, resulta sencilla
observando que las variables que figuran a la izquierda de las asignaciones corresponden a las
con subíndice (k-1) y las a la derecha de las asignaciones corresponden a las variables con
índice k en las relaciones de recurrencia. A la vez deben iniciarse con valores las variables
involucradas.

#define epsilon 1.0e-6

double reciproco(double a) // 0 < a <2


{ double x=1., err = fabs(1.-a);
while (err > epsilon)
{ x = (1+err)*x;
err = err*err;
}
return(x);
}

El valor de epsilon corresponde a la exactitud con que se manipulan números flotantes en


precisión simple. No pueden obtenerse más de seis cifras decimales significativas.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 17

Siempre que se diseña un lazo while, debe verificarse que éste termine. En este caso se requiere
verificar que el error va disminuyendo, cada vez que se ejecuta el bloque de repetición dentro
del lazo.

De la expresión para el error relativo, puede deducirse una relación para el error en la etapa
k-ésima en función del error inicial:

ek ek2 1 (ek2 2 ) 2 ((ek2 3 ) 2 ) 2 ...


2 3 k
ek ek2 1 ek2 2 ek2 3 ... e02

Es importante destacar el razonamiento inductivo que permite escribir el caso final. En este caso
basta observar que la suma del subíndice más el exponente de la potencia de dos, siempre
suman k.

Debido a que el exponente es par, para que el error disminuya al aumentar k, debe cumplirse:

e0 1

De la expresión del error relativo, el valor inicial debe cumplir:

1 ax0 1
La anterior implica:
1 1 ax0 1
Y también:
0 ax0 2

Finalmente se obtiene, la condición inicial para garantizar la convergencia:

2
0 x0
a

Que demuestra la expresión obtenida antes, a partir de la gráfica.

El código anterior, si bien resuelve la dificultad propuesta de no emplear divisiones, no permite


obtener respuesta para valores negativos del argumento, ni tampoco entrega resultados para
valores del argumento superiores a dos. También puede observarse que no puede tratarse el caso
con argumento igual a cero.

La primera modificación del algoritmo básico anterior es la elección de un valor inicial para
valores elevados del argumento. Para valores de a mayores que 2, el argumento real puede
expresarse:
a f 2e

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
Con:
1
f 1
2
El cálculo de e y f, puede lograrse con:

f=a; e=0;
while (f >= 1) {e++; f=f/2;}
while (f< 0.5) {e--; f=f*2;}

Debe notarse que sólo se efectúa uno de los lazos, y que después de ambos se tiene que se
cumple la condición para f.

El recíproco, en términos de e y f:
1 1 e
2
a f
Entonces el valor inicial debe cumplir:
2 2 e
0 x0 2
a f
Usando el mayor valor posible para f, se tiene:

e
0 x0 2 2

Entonces un valor seguro inicial que garantice la convergencia es:

e
x0 2
Y empleando la definición del error relativo, con el valor inicial anterior, se tiene:

e0 1 ax0 1 f 1 f

Entonces e0 1 , lo cual asegura que el algoritmo converge.

Cuando se trabaja con números flotantes es importante no efectuar comparaciones de igualdad


con un valor, dentro de un lazo. Suelen plantearse como la diferencia con el valor menor que
una tolerancia. Es decir en lugar de emplear (x == a) debe usarse: x a epsilon

Por esta razón se colocan cláusulas condicionales al ingresar a la función. En el diseño


evitaremos los casos: a=1, a=0 y a=inf.
La extensión para argumentos negativos la trataremos, almacenando el signo del argumento, y
calculando el recíproco de números positivos.

#define infinito 1./0.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 19
double reciproco(double a)
{ double x, err, f, p;
int e, sign;

if (a==1.) return (1.);


else if (a==-1.) return (-1.);
else if (a==0.) return (infinito);
else if (fabs(a)==infinito ) return (0.);

if (a<0.) {a = -a; sign=1;} else sign=0;

if (a<1.)
{x=1.; err=1.-a;}
else
{ // a > 1.
f=a; e=0; p=1; //mediante p se calcula 2^(-e)
while (f >= 1.) {e++; f=f/2; p=p/2;}
while (f< 0.5) {e--; f=f*2; p=p*2;}
x=p; err=1-f;
}

while (err>epsilon)
{ x = (1.+err)*x;
err=err*err;
}
if (sign) x=-x;
return(x);
}

Los valores iniciales, para a>1, pueden calcularse con funciones de la biblioteca (math.h), según
se ilustra a continuación.

Con: a f 2e , la función frexp, calcula mantisa (1/2 <= f < 1), y exponente e (de tipo entero).

err = 1- frexp(a, &e);


x = pow(2, -e); //valor inicial x= g*2^-e. Tal que: 1 < g <=2

Ejercicios propuestos.

E1. Diseñar una función que genera la próxima permutación.

Ahora el arreglo almacena los dígitos desde 0 hasta (n-1). De este modo no puede emplearse el
elemento 0 del arreglo como centinela y es preciso modificar el código propuesto antes.

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
E2. Codificar el siguiente algoritmo, para la raíz cuadrada.

Con: 0 a 2
x0 a
Para i=0
e0 1 a
e
xi (1 i 1 ) xi 1
Para i>0
2
3 ei 1
ei ei2 1 ( )
4 4

lim xi a
i
Debido a que el error relativo puede tener signo, la condición de término de la iteración, puede
lograrse con: fabs (ei ) epsilon . Que emplea la función valor absoluto para números flotantes.

Este algoritmo fue implementado en la biblioteca de EDSAC 1, uno de los primeros


computadores. El algoritmo sólo utiliza sumas y multiplicaciones.

E3. Codificar el siguiente algoritmo, para la raíz cuadrada.

Comprobar que empleando el método de Newton, se obtiene el algoritmo para calcular la raíz
cuadrada, según:
1 a
xk 1 ( xk )
2 xk
Converge con: x0 0

Referencias.

Edsger W. Dijkstra. “A Short Introduction to the Art of Programming.”, 1971.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción 21
Índice general.

CAPÍTULO 1. ............................................................................................................................................ 1
INTRODUCCIÓN A LAS ESTRUCTURAS DE DATOS Y ALGORITMOS. ................................... 1
1.1. ESTRUCTURA DE LOS DATOS. ............................................................................................................. 1
1.2. ESTRUCTURA DE LAS ACCIONES. ALGORITMOS. ................................................................................ 2
1.3. LENGUAJES. ....................................................................................................................................... 3
1.4. GENIALIDADES. ................................................................................................................................. 4
1.5. TEORÍA Y PRÁCTICA........................................................................................................................... 4
1.6. DEFINICIONES. ................................................................................................................................... 4
1.6.1. Algoritmo. .................................................................................................................................. 4
1.6.2. Heurística. ................................................................................................................................. 5
1.6.3. Estructuras de datos. ................................................................................................................. 5
1.7. ALGORITMOS CLÁSICOS. .................................................................................................................... 5
1.7.1. Algoritmo de Euclides. .............................................................................................................. 6
1.7.2. Algoritmo el colador de Erastótenes. ........................................................................................ 8
1.7.3. Permutaciones en orden lexicográfico. Algoritmo de Dijkstra (1930-2002). .......................... 9
PROBLEMAS RESUELTOS. ........................................................................................................................ 11
P1. Combinaciones en orden lexicográfico. ...................................................................................... 11
P2. Cálculo del recíproco. ................................................................................................................ 15
EJERCICIOS PROPUESTOS. ....................................................................................................................... 19
E1. Diseñar una función que genera la próxima permutación. ......................................................... 19
E2. Codificar el siguiente algoritmo, para la raíz cuadrada. ........................................................... 20
E3. Codificar el siguiente algoritmo, para la raíz cuadrada. ........................................................... 20
REFERENCIAS. ........................................................................................................................................ 20
ÍNDICE GENERAL. ................................................................................................................................... 21
ÍNDICE DE FIGURAS................................................................................................................................. 21

Índice de figuras.

FIGURA 1.1. ESTRUCTURAS DE DATOS. ......................................................................................................... 1


FIGURA 1.2. ESTRUCTURA DE ACCIONES....................................................................................................... 2
FIGURA 1.3. SECUENCIA DE APROXIMACIÓN USANDO EL MÉTODO DE LA TANGENTE. ................................. 15

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 2.

Definición de Estructuras de Datos en C.

Se desea conocer las técnicas para diseñar estructuras de datos. Se repasan los conceptos de
tipos básicos, para luego desarrollar las herramientas de creación de nuevas estructuras,
haciendo énfasis en los conceptos de agrupación y vinculación. Dando ejemplos que
posteriormente se emplearán en el texto.

2.1. Tipos primitivos.

En los lenguajes de programación suele disponerse de tipos básicos o primitivos: caracteres,


enteros con y sin signo, reales o flotantes de simple y doble precisión.

Se dispone la forma de definir e inicializar variables de esos tipos, y también la forma de


escribir constantes de esos tipos.

Para los tipos primitivos existen numerosos y variados operadores que permiten construir
expresiones con variables y constantes de dichos tipos. En determinados casos existen
mecanismos automáticos para convertir valores de un tipo en otro, sin embargo es preferible la
conversión explícita a través del molde o “cast”.

Mediante las bibliotecas de entrada-salida pueden ingresarse o desplegarse valores de esos


tipos.

2.2. Mecanismos de estructuración.

Existen dos mecanismos básicos de estructuración de datos: la agrupación y la vinculación.

2.2.1. Grupos básicos.

La estructura (struct) permite agrupar elementos de diferente tipo. El arreglo agrupa elementos
de igual tipo. El string agrupa caracteres.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos

Estructura Arreglo String

Figura 2.1. Agrupaciones de datos.

Se dispone de una forma de definir variables de esos grupos básicos, y los mecanismos de
acceso a las componentes: el discriminador del campo (un punto) para las estructuras; y los
paréntesis cuadrados o corchetes para delimitar el índice de la componente del arreglo. Las
operaciones que están definidas para estos grupos básicos son muy reducidas, el programador
debe crear sus propias funciones o métodos para efectuar operaciones más complejas sobre
estos grupos básicos. En el caso de strings existe una biblioteca estándar de funciones que los
manipulan (ver Apéndice 2).

Las agrupaciones pueden ser tan complejas como sea necesario: ya que es posible crear arreglos
de estructuras, y también estructuras que contengan arreglos y strings.

2.2.2. Vínculos.

Pueden establecerse relaciones entre elementos de datos. Los siguientes diagramas ilustran las
formas básicas de vinculación: vínculos de orden o secuencia en listas, relaciones jerárquicas en
árboles, e interconexiones complejas en grafos.

árboles

listas

grafos

Figura 2.2. Vínculos entre componentes de datos.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 3
El vínculo es el elemento de datos de una componente que permite accesar a otra componente.
Una componente puede contener información de uno o más vínculos.

El programador debe disponer de elementos del lenguaje que le permitan crear nuevos tipos de
datos, y mediante estos tipos crear funciones o métodos que realicen operaciones sobre las
componentes.

Cuando el número máximo de componentes es conocido de antemano, las componentes pueden


modelarse como una serie de arreglos agrupados en una estructura. En la cual se emplea uno o
varios arreglos para establecer el o los vínculos entre las componentes. Se suele definir como
cursor a una variable que contiene el valor del índice de una componente.

Otro mecanismo para establecer vínculos es el puntero, que es una variable que contiene la
dirección de otra componente. La vinculación a través de punteros suele emplearse
preferentemente en situaciones dinámicas, en las cuales el número de las componentes varía
durante la ejecución del programa; esto debido a que se crean, insertan y descartan
componentes.

2.3. Ejemplos basados en arreglos.

2.3.1. Acceso a componentes del arreglo.

El acceso a componentes se realiza vía indirección de un puntero constante que es el nombre del
arreglo, con un offset dado por el índice del elemento.

Se definen tipos de datos.


typedef int Tipo; /* tipo de item del arreglo */
typedef int Indice; /* tipo del índice */

Es práctica usual, definir mediante una constante el tamaño máximo del arreglo
#define MaxEntradas 10

Se definen e inician con valores dos arreglos


Tipo A[MaxEntradas]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Tipo B[MaxEntradas]={ 10,11,12,13,14,15,16,17,18,19};

A las funciones que manipulan arreglos se les suele pasar el arreglo por referencia.

Por ejemplo la función que imprime en una línea las componentes de un arreglo a.

void mostrar(Tipo *a, Indice inferior, Indice superior)


{
Indice i;
for (i = inferior; i <= superior; i++) printf(" %d ", a[i]);
putchar('\n');
}
Note que el primer argumento de la función también puede escribirse: Tipo a[].

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
Ejemplos de uso:
mostrar(A, 0, 4); imprime 0 1 2 3 4
mostrar(A+2, 0, 4); imprime 2 3 4 5 6

La manipulación de arreglos no suele verificar el rango del índice. En el caso de haberse


definido adyacentes los arreglos A y B, la referencia a A[12] está accesando efectivamente a
B[2], por eso se muestra un 12; pero es un error por acceso fuera de rango.

printf(" %d \n", A[12]); imprime 12


printf(" %d \n", B[-2]); imprime 8
printf(" %d \n", A[-1]); imprime cualquier cosa que estuviera almacenada antes de A
printf(" %d \n", 1[-1+B] ); imprime 10
printf(" %d \n", -1[B] ); imprime -11 (es una referencia correcta)

Similares errores pueden generarse accesando componentes indireccionando vía puntero:


printf(" %d \n", *(A+12)); imprime 12
printf(" %d \n", *(-2+B)); imprime 8

Los ejemplos anteriores ilustran que el acceso a componentes de un arreglo se efectúa mediante
indirección con el puntero que es el nombre del arreglo.
La expresión: A[c] se interpreta como el contenido de la dirección:
A + c*(tamaño del tipo del arreglo).
Donde A es la dirección de la primera componente del arreglo.

Empleando el lenguaje:
A[c] equivale a: *(A+c) y A es equivalente a &A[0].
Definición de matrices.
Puede definirse una matriz de caracteres, arr, de R renglones y C columnas mediante:

#define R 8 //renglones
#define C 10 //columnas
char arr[R][C];
arr

arr[3][5]

arr[3]

Figura 2.2.a. Matriz de caracteres

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 5
Entonces: arr[3] es el cuarto renglón de la matriz arr, y es un arreglo de C caracteres. Puede
considerarse que arr[3] es la dirección de inicio de ese arreglo; es decir, es un puntero constante
que indica la primera componente de ese arreglo. Puede escribirse, usando notación de punteros
como: *(arr+3).

La expresión: a[3][5], denota el sexto carácter del cuarto renglón. La cual puede escribirse,
usando notación con punteros, mediante: * ( *(arr+3) + 5 ).
Puede concluirse que arr es un puntero a un puntero a carácter.
Definición de arreglo de arreglos.
Empleando definición de tipos puede crearse el tipo renglón, como un arreglo de C caracteres.
Y mediante este nuevo tipo puede definirse una matriz ar, como un arreglo de renglones.
Definiciones que se muestran a continuación.

#define R 8 //renglones
#define C 10 //columnas
typedef char renglon[C];
renglon ar[R];

La expresión ar[3] es el cuarto renglón. La expresión: * ( *(ar+3) + 5 ) corresponde al sexto


carácter almacenado en el cuarto renglón, y es equivalente a: ar[3][5].

Con el mismo procedimiento se pueden estructurar organizaciones más complejas. Por ejemplo
una matriz cuyo contenido sea un arreglo.
Los arreglos aseguran que las componentes quedan adyacentes en memoria. En el caso de
matrices, el último elemento del primer renglón queda adyacente con el primer elemento del
segundo renglón.
Arreglo de punteros a renglones.
Un ejemplo de creación de estructuras de datos, empleando el lenguaje C, es diseñar un arreglo
de punteros pt, que apunten a los renglones de una matriz.
ar
pt

(*pt[2])[5]

Figura 2.2.b. Arreglo de punteros a arreglos de caracteres

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
Las siguientes declaraciones de tipos y definiciones de variables, crean el espacio que
almacenará a la estructura de datos.

typedef char renglon[C]; //definición de tipo renglón como un arreglo de C caracteres


typedef renglon * prenglon; //definición del tipo puntero a renglón
renglon ar[R]; //Matriz de caracteres como arreglo de renglones
prenglon pt[R]; //Arreglo de punteros a los renglones.

Es preciso vincular los punteros del arreglo con los renglones, lo cual se efectúa con:
for(i=0; i<R; i++) pt[i]=&ar[i]; //llena arreglo de punteros a renglones.

Entonces: pt[2] contiene un puntero al tercer renglón. Lo cual puede escribirse: *(pt +2 ).
La expresión: *(pt[2]) es el tercer renglón, que en este caso es un arreglo de C caracteres; es
decir: *( *(pt+2)) es el valor del puntero al inicio de ese renglón. Y consecuentemente:
*( *(pt+2)) +5 es el puntero al sexto carácter del tercer renglón.
Finalmente para accesar al elemento ubicado en el tercer renglón y en la sexta columna, debe
indireccionarse el puntero anterior, lo cual puede escribirse: *( *( *(pt+2)) +5), o empleando
notación de arreglos: (*pt[2])[5] .

Una alternativa a lo anterior es convertir el puntero a renglón en un puntero a carácter, esto se


logra mediante: (char *) pt[2]; entonces el acceso a un carácter, a través de punteros a renglones
se logra con: ((char*) pt[i])[j].
Arreglo de punteros a caracteres.
Puede lograrse una representación más simple, si se almacenan en un arreglo punteros a
caracteres, en lugar de punteros a renglones. Esto puede modelarse según:

typedef char renglon[C]; //definición de tipo renglon como un arreglo de C caracteres


renglon ar[R]; // Arreglo de R renglones
char * pch[R]; // Arreglo de punteros a caracteres

Los vínculos de la estructura se producen con:


for(i=0; i<R; i++) pch[i]=(char *) (&ar[i]); //llena arreglo de punteros a char

La expresión: pch[i][j] accesa al carácter ubicado en el renglón (i+1) y en la columna (j+1).

2.3.2. Lista simplemente enlazada en base a cursores.

#define MaxEntradas 4
#define fin_de_lista -1

Con los siguientes datos:


Tipo Valor[MaxEntradas] ={1, 2, 3, 4};
Indice Cursor[MaxEntradas] ={2, 3, 4, fin_de_lista };

Puede visualizarse la siguiente lista.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 7

1 2 3 4

Figura 2.3. Lista simplemente enlazada.

Se tiene, como ejemplo, que: Cursor[2] contiene el valor 4 de índice.

2.4. Ejemplos basados en estructuras.

2.4.1. Estructura para fecha.

Agrupamos mediante la estructura fecha los campos: día, mes, año.

//molde. Declaración.
struct fecha
{ int dia;
int mes;
int agno;
};

La declaración sólo establece un molde o patrón para la estructura.


Se denominan instancias o definiciones de variables cuando se asocia un tipo a una variable. La
definición considera una asignación de memoria para la variable.

//instancias.
struct fecha fecha1; //definición.
struct fecha fecha2={1,5,2004}; //definición e inicialización
Nótese que se requiere preceder con la palabra struct al nombre del molde.

También se puede establecer una definición e inicialización de un puntero a la variable fecha1,


mediante:
struct fecha *ptfecha1=&fecha1;

No puede accederse a los campos de una estructura no iniciada, para lectura.

Se pueden copiar estructuras completas, mediante asignación:


fecha1=fecha2;

Puede escribirse en un campo determinado:


fecha1.mes=11; // mediante el selector de campos
ptfecha1->dia=25; //o a través del puntero

La forma equivalente, vía indirección del puntero, no suele emplearse:


*(ptfecha1.dia) = 25;

Es preferible crear tipos, previo a la definición de variables de tipo estructura:

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
//Definiciones de tipos
typedef struct fecha Fecha; //hay diferencia entre mayúsculas y minúsculas.
typedef Fecha * pFecha; //define puntero a elemento de tipo Fecha.

En este ámbito, las definiciones de las instancias para las variables, quedan:
Fecha fecha3 = {1, 5, 2004}; //definición e inicialización.
pFecha pfecha3=&fecha3;
Observar que las definiciones no son precedidas por struct.

Es preciso crear métodos para desplegar variables que son estructuras:


Se ilustra pasar el argumento como una estructura. Lo cual implica: crear el espacio y copiar la
estructura en el frame de la función:

void printfecha(Fecha f) //paso por valor


{
printf(" Día = %d Mes = %d Año = %d \n", f.dia, f.mes, f.agno); //lectura de campos
}

Es preferible pasar una referencia a la estructura, ya que esto ocupa menos espacio en el frame.
Sólo es preciso copiar un puntero.
void printfecharef(pfecha p) //paso por referencia.
{
printf(" Día = %d Mes = %d Año = %d \n", p->dia, (*p).mes, p->agno);
}

Ejemplos de uso:
printfecha(fecha3);
printfecharef(pfecha3);

También es útil disponer de un constructor de la estructura.


Fecha setfecha (int dia, int mes, int agno) //retorno de estructura
{
Fecha f;
f.dia=dia; f.mes=mes; f.agno= agno; //escritura en campos
return(f);
}

Ejemplo de uso:
fecha3 = setfecha(15, 6, 2005);

También suele ser necesario desarrollar funciones para realizar operaciones de comparación
entre estructuras. El siguiente prototipo ilustra un operador de comparación “mayor o igual
que”, al cual se le pasan punteros a estructuras y devuelve un puntero a la estructura que es
mayor o igual que la otra, el diseño de la función se deja como tarea.

pFecha fechaGET(pFecha pf1, pFecha pf2);

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 9

Se pueden crear arreglos de estructuras de tipo Fecha:


Fecha ArrFecha[3]={ {1,1,2005},{30,6,2005},{31,12,2005}};

printf("mes %d \n", (ArrFecha+1)->mes); imprime 6


printf("dia %d \n", ArrFecha[2].dia); imprime 31

Es responsabilidad del programador no exceder los rangos del arreglo.

2.4.2. Lista simplemente enlazada en base a punteros.

Es posible declarar el molde y definir tipos simultáneamente.


En esta situación en lugar de moldenodo se podría haber puesto cualquier otro identificador.
Esta forma suele describirse como declaración anónima.

typedef struct moldenodo


{ int clave;
struct moldenodo *proximo;
} nodo, *pnodo;

//instancias. Definiciones.
nodo nodo1={1,NULL};
nodo nodo2={2,NULL};
nodo nodo3={3,NULL};
pnodo lista=&nodo1;

Las siguientes asignaciones crean los vínculos.


lista->proximo = &nodo2;
nodo2.proximo = &nodo3;

lista

1 2 3

nodo1 nodo2 nodo3

Figura 2.4. Lista simplemente enlazada, mediante punteros.

En caso de definir la lista en forma dinámica, sólo es preciso definir la variable lista. El espacio
para los nodos se solicita a través de llamados a malloc; debido a esto los nodos no tienen un
nombre y sólo se los puede accesar vía punteros.
En el caso del ejemplo, los nodos se han definido en forma estática, y tienen un nombre de
variable asociado.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
2.5. Estructuras más complejas.

2.5.1. Arreglo de listas.

Pueden concebirse estructuras más complejas.


Por ejemplo se desea disponer de un arreglo de punteros, donde cada elemento del arreglo es el
inicio de una lista simplemente enlazada.
Consideraremos que cada celda debe estar asociada a un string, donde los strings pueden ser de
diferente largo. Se decide que no es conveniente almacenar los strings como arreglos de
caracteres, esto implicaría un tamaño fijo para cada arreglo, con lo cual se ocuparía mal el
espacio cuando deba almacenarse un string de pequeño largo, y la estructura puede colapsar si el
largo del string es mayor que el tamaño máximo del arreglo. Es preferible asociar al nodo de la
lista un puntero a un string, lo cual ocupa un tamaño fijo (normalmente los bytes ocupados por
un entero).

Definimos entonces los siguientes tipos:

typedef struct moldecelda


{
char *nombre;
struct moldecelda *next;
} celda, *pcelda;

#define BALDES 10 /* 10 celdas */


static pcelda hashtable[BALDES]; /*tabla punteros */

La palabra static, que precede a la definición, explícitamente indica que la tabla debe
almacenarse en un segmento estático de la memoria. Es decir estará disponible, durante toda la
ejecución del programa.

Se ilustra un ejemplo de la estructura.

Índice Tabla String0

0 String i
1
2
….
B-1 String j

String k

Figura 2.5. Arreglo de listas.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 11
Veremos que se pueden desarrollar algoritmos eficientes para buscar strings almacenados en la
tabla, cuando estudiemos tablas de hash.

2.5.2. Arreglo de estructuras.

El siguiente ejemplo de diseño de una estructura de datos, contempla un arreglo en el cual se


califica el estado de cada celda. Suponemos que se almacena un entero, y para describir
lógicamente el estado empleamos la definición de un tipo enumerativo, que permite el mapeo de
nombres lógicos con enteros. Internamente se emplean enteros, pero el programador puede
referirse a ellos mediante sus nombres equivalentes.

typedef enum {vacio, ocupado, descartado} state;

typedef struct hcelda


{ int clave;
state estado;
} celda;

#define B 10 /* 10 celdas */
static celda hashtab[B]; /*arreglo de celdas */
static int ocupados; //ocupados de la tabla

El programador puede escribir expresiones en términos del tipo enumerativo:


(hashtab[i].estado != vacio && hashtab[i].estado != descartado)

2.5.3. Multiárboles.

La descripción de un nodo que pueda tener un número variable de punteros que apunten a sus
descendientes no es práctica. Ya que esto implica almacenar los vínculos en un arreglo, y si es
arreglo debe tener un tamaño fijo. El cual debe escogerse considerando el máximo número de
descendientes que soportará la estructura; mal empleando los recursos en el caso de un nodo sin
hijos, o de nodos con un pequeño número de descendientes. Además siempre estará el riesgo de
que aparezca un nodo con mayor número de descendientes que el tamaño máximo escogido.

2 3 4

5 6 7 8 9 10 11 12 13

Figura 2.6. Multiárbol.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
El esquema anterior ilustra los vínculos. Este ejemplo refuerza el concepto que un vínculo puede
ser un cursor o un puntero.

El número variable de los vínculos asociados a un nodo pueden reemplazarse por sólo dos
vínculos: uno que relaciona el nodo con el primer descendiente izquierdo, y otro que vincula a
cada nodo con su hermano derecho. Nótese que en cada nivel puede observarse una lista de
nodos que parte del nodo ubicado más a la izquierda y termina en un vínculo nulo después del
último descendiente de un nodo.

Figura 2.7. Primer descendiente izquierdo, hermano derecho.

Con esta visualización se requieren sólo dos vínculos por nodo.


2.5.3.1. Descripción mediante punteros.
Una representación mediante punteros requiere definir la estructura del nodo según:

typedef struct moldenodo


{
int valornodo;
struct moldenodo *hijoizquierdo;
struct moldenodo *hermanoderecho;
struct moldenodo *padre;
potros * datos_periféricos;
} nodo, *pnodo;

Donde se han definido los tipos asociados al nodo y al puntero a nodo.

Se ha considerado que se almacenará un entero como clave o valor distintivo del nodo. Se ha
agregado un puntero a una estructura que podría almacenar otros datos asociados al nodo, con el
campo datos_periféricos. También se agrega un puntero al padre, ya que esta información
reduce el tiempo de buscar al padre de un nodo determinado.
2.5.3.2. Descripción mediante arreglos de cursores.
Se describe a continuación los valores que describen el multiárbol, de la Figura 2.6, mediante
arreglos de cursores para el Hijo izquierdo y Hermano Derecho. Se ha considerado que un valor
cero indica que no hay hijo izquierdo o hermano derecho o padre; si se desea usar el nodo cero,
podría elegirse -1 como el cursor nulo.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 13
Índice Hijo izquierdo Hermano derecho Padre Clave Otros datos
0 0 0 0 -- --
1 2 0 0 1 --
2 5 3 1 2 --
3 7 4 1 3 --
4 10 0 1 4 --
5 0 6 2 6 --
6 0 0 2 6 --
7 0 8 3 7 --
8 0 9 3 8 --
9 0 0 3 9 --

Figura 2.8. Multiárbol mediante arreglo de cursores.

Se ha agregado un arreglo de cursores para especificar el padre del nodo. Esta información
facilita la búsqueda del padre de un nodo dado.

Un diseño de la estructura de datos considera al multiárbol como una estructura cuyos campos
son los diferentes arreglos.
#define MAXNODOS 13
struct moldemultiarbol
{
int Hijo_izquierdo[MAXNODOS];
int Hermano_derecho[MAXNODOS];
int Padre[MAXNODOS];
int clave[MAXNODOS];
int dato1[MAXNODOS];
} arbol;

arbol.Hijo_derecho[4] tiene valor 0; lo cual indica que es el último descendiente.

La estructura puede contener más o menos información dependiendo de costo de las


operaciones que se deseen implementar. Por ejemplo si sólo se desea conocer la conectividad de
los nodos del multiárbol, basta que la estructura aporte información para conocer quien es el
padre de cada nodo, con esta descripción si luego se desea conocer los hijos de cada nodo,
resultará un algoritmo costoso.
2.5.3.3. Descripción por arreglo de padres.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
0

1 2 3

4 5 6 7 8 9 10 11 12

Figura 2.9. Multiárbol mediante arreglo de padres.

En un ambiente de grafos, es de interés describir un árbol que interconecta todos los vértices sin
formar circuitos. Se ilustra un árbol de un grafo orientado, en el que se muestra el número
asociado a cada vértice; se ha considerado el vértice 0, esto debido a que los arreglos en C,
parten de índice cero.

La información que describe el árbol anterior es:

Vértice 0 1 2 3 4 5 6 7 8 9 10 11 12
Padre del vértice 0 0 0 0 1 1 2 2 2 3 3 3 3

Figura 2.10 Arreglo de padres.

Empleando lenguaje de programación, queda:

int PadreDelVertice[MAXNODOS]={0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3};

2.6. Un ejemplo real de estructuras.

La realidad suele ser más compleja que los casos idealizados que se exponen en los cursos
básicos.

El diseño de una estructura más realista, es la que describe los encabezados de los paquetes
ICMP e IP en un ambiente de red.

Primero se definen dos tipos básicos, con fines de compatibilidad. Se mapean los tipos del
lenguaje C, a tipos lógicos. Si los tamaños de los tipos básicos de un compilador para un
procesador específico son diferentes, basta cambiar el nombre de los tipos básicos. Toda la
codificación no es necesario modificarla, ya que queda en términos de u8_t y u16_t, que son los
tipos usados en el programa.

typedef unsigned char u8_t; //es el tipo byte.


typedef unsigned short u16_t; //el entero de 16 bits

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 15
/* The ICMP and IP headers. */
typedef struct {
/* IP header. */
u8_t vhl,
tos,
len[2],
ipid[2],
ipoffset[2],
ttl,
proto;
u16_t ipchksum;
u16_t srcipaddr[2],
destipaddr[2];
/* ICMP (echo) header. */
u8_t type,
icode;
u16_t icmpchksum;
u16_t id,
seqno;
} uip_icmpip_hdr;

Problemas resueltos.

P2.1. Se tiene el siguiente programa:

#include <stdlib.h>
#include <stdio.h>
#define MAX 5

typedef struct nn
{ int i1;
int i2;
struct nn * next;
} nodo, * pnodo;

pnodo lista1=NULL;
pnodo lista2=NULL;

pnodo getnodo(void)
{ pnodo p;
if( (p=(pnodo)malloc(sizeof(nodo)))==NULL) return(NULL); else return(p);
}

void Push(pnodo *ref, int dato1, int dato2)


{ pnodo newnodo;
if ( (newnodo=getnodo( )) == NULL) exit(1);
newnodo->i1 = dato1; newnodo->i2 = dato2; newnodo->next = *ref;

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
*ref = newnodo;
}

void crealista(void)
{ int i;
for(i=0; i<MAX; i++)
{ Push(&lista1, i, i+1); Push(&lista2, MAX-i, i-1); }
}

pnodo busca(pnodo pp)


{ pnodo qq=NULL;
while (pp !=NULL) {qq = pp; pp = pp->next;}
return (qq);
}

pnodo busca2(pnodo p, int j, pnodo q)


{
if(p !=NULL)
{ while( p->i1 != j ) p = p->next;
if (p != NULL)
while (q !=NULL) { if(p->i2 == q->i1) return(q); q = q->next; }
return (NULL);
}
return(NULL);
}

int main(void)
{ pnodo tt;
crealista();
printf(" %d \n", ( *(busca(lista1)) ).i1);
printf(" %d \n", busca(lista2)->i2);
if( (tt=busca2(lista1, 3, lista2)) !=NULL) printf(" %d \n", tt->i2);
return(0);
}

a) Explicar, empleando un diagrama, el paso por referencia de Push.


b) Diagrama de la estructura después de ejecutar crealista();
c) Explicar acción realizada por busca.
d) Explicar acción realizada por busca2.
e) Determinar qué imprime el programa.

Solución.
a) En la función crealista, se tiene un ejemplo de uso de Push.
Consideremos el llamado: Push(&lista1, 0, 1).

En la definición de Push, el primer argumento es un puntero a puntero a nodo.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 17
Dentro de la función, la ocurrencia de *ref, denota a la variable, cuya dirección es pasada en la
invocación a la función, en el caso del ejemplo referencia a variable global lista1.
Si *ref, aparece a la derecha, formando parte de una expresión, su valor es el almacenado en
lista1; si aparece a la izquierda, se escribe en la variable lista.

Luego de la invocación a la función, se tiene el siguiente esquema para las variables.

Datos Stack

Lista1
newnodo ?
ref
dato1 = 0
dato2 = 1

Figura P2.1.

Nótese que los argumentos son variables almacenadas en el stack, iniciadas con los valores de
los parámetros de la invocación. La variable local newnodo, al no estar inicializada en su
definición apunta a cualquier lado. Razón por la cual, conviene definirlas e inicializarlas
simultáneamente.

Después de un llamado exitoso a getnodo(); es decir, malloc asignó una estructura en el heap, un
diagrama de la situación es el siguiente:

Datos Stack heap

Lista1
newnodo i1 = ?
ref i2 = ?
dato1 = 0 next = ?
dato2 = 1

Figura P2.2.

El resto de la función, escribe en los campos de la estructura creada en el heap. Y además, en


último término, sobreescribe en la variable global lista1 (en el ejemplo que se analiza).

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos

Datos Stack heap

Lista1
newnodo i1 = 0
ref i2 = 1
dato1 = 0 next
dato2 = 1

Figura P2.3

Al salir de Push, desaparecen las variables automáticas, ubicadas en el stack, y la situación


queda:

Datos Stack heap

Lista1
i1 = 0
i2 = 1
next

Figura P2.4

Si se hubiera pasado solamente un puntero a nodo como referencia, se tendría el diseño:

void Push2(pnodo ref, int dato1, int dato2)


{
pnodo newnodo;
if ( (newnodo=getnodo()) == NULL) exit(1);
newnodo->i1 = dato1;
newnodo->i2 = dato2;
newnodo->next = ref;
ref = newnodo;
}
Y un ejemplo de uso, sería:
Push2(lista1, 0, 1);

Lo cual no es equivalente al diseño anterior. Puede comprobarse efectuando un diagrama.


Las listas con cabecera ( header ) permiten diseños alternativos.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 19
b) Después de crealista, el espacio queda:

Datos heap

Lista1 i1=4 i1 = 3 i1 = 2 i1 = 1 i1 = 0
i2 = 5 i2 = 4 i2 = 3 i2 = 2 i2 = 1
next next next next next

Lista2 i1 =1 i1 = 2 i1 = 3 i1 = 4 i1 = 5
i2 = 3 i2 = 2 i2 = 1 i2 = 0 i2 = -1
next next next next next

Figura P2.5.

c) A la función busca, se le pasa la dirección del primer nodo de la lista. Si la lista es vacía,
retorna un puntero nulo. Si no es vacía, con pp recorre la lista, dejando qq apuntado al nodo
corriente y con pp al próximo. Cuando se llega con pp, al final de la lista, qq apunta al último
nodo de la lista.

d) La función busca2 intenta encontrar el valor j en el campo i1 del nodo apuntado por el
argumento p. Si la lista es vacía retorna un puntero nulo; en caso de encontrar el valor j, busca a
partir del nodo apuntado por q, el valor del campo i1 que sea igual al valor del campo i2 del
nodo donde quedó apuntando p.
Si lo encuentra, retorna un puntero al nodo que cumple la condición anterior; en caso contrario,
retorna un puntero nulo.

La invocación: busca2(lista1, 3, lista2), después del primer while, deja p apuntando al segundo
nodo de la lista1, el segundo while deja q apuntando al cuarto nodo de la lista2; ya que busca en
ésta el valor 4 en el campo i1.
El diseño de la función incurre en un error frecuente, en este tipo de problemas. ¿Qué ocurre si
el valor j no se encuentra en ningún campo i1 de la lista apuntada por p?.
Cuando p tome valor nulo, intentará leer p->i1 en las primeras direcciones de memoria, las
cuales suelen pertenecer a un segmento del sistema operativo. Lo más seguro que la ejecución
del proceso aborte debido a un error de segmentación.
Se podría corregir, cambiando:
while( p->i1 != j ) p = p->next;

por: while ((p != NULL)&&( p->i1 != j )) p = p->next;

Lo cual funciona debido a que el and opera con cortocircuito.

e) En primer término imprime el campo i1 del último nodo de la lista1.


Luego el campo i2 del último nodo de la lista2.

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
Finalmente el campo i2 de la lista 2, cuyo campo i1 es 4; ya que en la lista1, el campo i2 es 4,
cuando el campo i1 es 3.

Imprimiría:
0
-1
0

P2.2. Escribir una función:

int btoi(char *str, int *pvalor) que convierta un string binario pasado en str (Ej. "110101") a un
entero a ser retornado en pvalor (Ej. 53). La función retorna 0 si no hay error y retorna -1 si el
string binario tiene un carácter que no es '0' o '1'.
Nota: 1101 (en binario) = 1x23+1x22+0x21+1x20 = 8 + 4 + 1 = 13 en decimal

/* Uso de la funcion */
#include <stdio.h>
#include <math.h>
#include <string.h>
int btoi(char *str, int *pvalor);

main()
{
int valor=0;
char str[10];

strcpy(str, "110101");
if( !btoi(str, &valor) ) printf("string binario: %s, int:%d\n", str, valor);
}

Solución.

int btoi(char *str, int *pvalor)


{ int i=0, largo=0, temp=0;
largo = strlen(str) - 1; //índice del dígito menos significativo

while(i <= largo)


{
if (*(str + i) == '1')
temp += (int) pow(2, largo - i);
//suma las potencias presentes de dos, desde la más significativa.
else if (*(str + i) == '0') ;
else return -1; //no es 1 ó 0
i++;
}
*pvalor = temp; //retorno por referencia
return 0;

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 21
}

Una alternativa es sumar las potencias presentes de dos, desde la menos significativa.

int btoi(char *str, int *pvalor)


{ int i=0, temp=0, pot=1;
i = strlen(str)-1; //índice del dígito menos significativo

while(i >=0)
{
if (*(str + i) == '1')
{ temp += pot; printf(" %d %d \n",temp, pot);}
else if (*(str + i) == '0') ;
else return -1; //no es 1 ó 0
i--;pot*=2;
}
*pvalor = temp;
return 0;
}

P2.3. Se tiene la siguiente función:

char *f1(char *s, int c)


{
while( *s )
{ if( *s == (char) c ) return (char *) s;
s++;
}
return (char *) 0;
}

a) Explicar los cast que se emplean y su necesidad.


b) Qué realiza la función.
c) Si se tiene char *string="12345678"; explicar que despliega:
printf("%s\n", f1(string, '3')) ;

Solución.
El cast (char) c se emplea, debido a que el segundo argumento de la función es de tipo entero,
para convertirlo a carácter y efectuar la comparación con un carácter del string. No es
necesario, ya que por defecto, los valores de tipo carácter son promovidos automáticamente a
enteros con signo. Por ejemplo se puede pasar equivalentemente el valor 65 ó 0x45 ó „A‟. Esto
permite pasar caracteres de control que no son imprimibles, como argumentos. La comparación
también se podía haber explicado según: ( (int) *s == c ). Es preferible usar cast, de tal modo
de comparar valores de igual tipo.
El cast en return (char *) s; no es necesario, ya que s es de tipo puntero a char.
El cast return (char *) 0; es necesario para retornar un puntero nulo a carácter.

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
b) Revisa y encuentra la primera ocurrencia del carácter (char) c, en el string apuntado por s;
retornado un puntero al carácter de s que es igual a (char) c. Si el string es nulo, retorna un
puntero nulo a carácter; también si no encuentra (char) c en el string.

c) Despliega el string: 345678, seguido de un terminador de línea, ya que el llamado


f1(string, '3') retorna un puntero al string “345678".

P2.4. Se tiene la siguiente función:

int f2(char *s1, char *s2)


{
while( *s1++ == *s2 )
if( *s2++ == '\0' ) return(1);
return(0);
}

a) Explicar que realiza la función. Indicando las condiciones en las que retorna un cero o un uno
b) Dar un ejemplo de uso. Definiendo las variables que sean necesarias.
Solución.
a) Compara dos strings. Retorna 1 sólo si los strings son iguales. Reconoce la igualdad de dos
strings nulos. Retorna cero, si los strings son diferentes. Si los caracteres, en la misma
posición, de s1 y s2 son iguales va recorriendo ambos strings.

Si los primeros caracteres de los strings fuesen iguales, retorna un cero si string s1 es más corto
que el string s2 (ya que no se cumple la condición del while); lo mismo sucede si string 1 es más
largo que el string s2.

b) char *s1="12345678";
char *s2="12345";

if ( f2(s1,s2)==0) printf("no son iguales\n"); else printf("son iguales\n");

P2.5. Se tiene la estructura para un nodo,

con clave de tipo entera y tres punteros a nodo.

clave
p1 p2 p3

a) Definir los tipos: nodo y pnodo (puntero a nodo).


b) Diseñar, empleando malloc, la función con prototipo: pnodo creanodo(int clave); que solicita
espacio e inicializa el nodo con punteros nulos y la clave del nodo con el valor del argumento.
c) Si se tiene la siguiente definición: pnodo pn; dibujar un diagrama con los datos, luego de
ejecutada la secuencia:
pn=creanodo(5);
pn->p3=pn;

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 23
pn->p1=creanodo(3);
pn->p1->p3=pn;
pn->p2=creanodo(8);
pn->p2->p3=pn->p1;

d) Escribir el segmento que forma el siguiente diagrama:


pn

4 6
p1 p2 p3 p1 p2 p3

Figura P2.6.

Solución.
a)
typedef struct moldenodo
{ int clave;
struct moldenodo *p1;
struct moldenodo *p2;
struct moldenodo *p3;
} nodo , *pnodo;

b)
pnodo creanodo(int clave)
{ pnodo p;
if ((p = (pnodo) malloc(sizeof(nodo))) == NULL) {
printf ("Memoria insuficiente para crear nodo\n");
exit(1);
}
p->clave=clave;p->p2=NULL;p->p1=NULL;p->p3=NULL;
return(p);
}

c) Los diagramas se ilustran después de ejecutada la instrucción.


pn=creanodo(5);

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
pn
5

Figura P2.7.

pn->p3=pn;
pn
5

Figura P2.8.
pn
pn->p1=creanodo(3);
5

Figura P2.9.

pn->p1->p3=pn;
pn
5

Figura P2.10.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 25
pn->p2=creanodo(8);
pn
5

3
8

Figura P2.11.

pn->p2->p3=pn->p1;
pn
5

3
8

Figura P2.12.

c)
pn= creanodo(6); pn->p2=pn; pn->p3=pn;
pn->p1=creanodo(4); pn->p1->p1=pn->p1;
pn->p1->p3=pn; pn->p1->p2=pn;

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos

pn

4 6
p1 p2 p3 p1 p2 p3

Figura P2.13.

P2.6. Se tiene el siguiente programa.

#include <stdlib.h>

typedef struct nn
{ int x;
struct nn * p;
struct nn * q;
int y;
} t, *pt;

t w, z;
pt px=&w;

void main(void)
{ px->p=&z;
w.q = px->p;
px->q->q=px;
z.p = w.p;
w.x = z.x = 2;
(*px).y = 8;
(*(w.q)).y =9;
px=(pt) malloc(sizeof(t));
px->p=px->q = (pt ) 0;
px->x=px->y=12;
}

Efectuar un diagrama de los datos después de ejecutar las instrucciones de main.

Solución.
Antes de main, el espacio de variables puede visualizarse según:

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 27

px

w
z
x
x
p q
p q
y
y

Figura P2.13.

px

p q

y
w.q=px->p;
px->p =&z

p q

Figura P2.14.

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos

px

p q

px->q->q=px
z

z.p=w.p p q

Figura P2.15.
px

x 2

p q

w.x=z.x=2 y 8 (*px).y=8;

x 2

p q
(*(w.q)).y=9
y 9

Figura P2.16.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 29

px =(pt)malloc(sizeof(t)); px

x 2

p q
x 12
y 8
p q
z
y 12
x 2
px ->p=px->q=(pt)0;
p q

y 9

px ->x=px->y=12;

Figura P2.17.

Ejercicios propuestos.

E1. Determinar qué imprimen los siguientes segmentos:

a ) printf(“\n%o %x %d”, 17, 017, 0x17) ;


b) n = 3 ; printf(“-05d %5d”, n, n) ;
c) x = 1234E-2 ; printf(“%6.3f”, x) ;
d) j =2; if ( ( '1' -1 ) == --j ) printf("verdad"); else printf("falso");

E2. Colocar paréntesis y evaluar las expresiones siguientes:

Si es preciso puede indicar los resultados de expresiones intermedias.


a) a != b && c + 1 == ! c + 2
con a1) a=2 ; b=3 ; c = 1;
a2) a=3 ; b=2 ; c=2 ;
b) 1 + 2 * ( n += 8) / 4 con n=3 ;
c) a < b ? a < b ? a+1 : a+2 : a+3 con a=2 ; b= 3 ;

E3. Se tiene el siguiente programa:

Qué efectúa la función, indicar tipo de parámetros.

Profesor Leopoldo Silva Bijit 26-05-2008


30 Estructuras de Datos y Algoritmos
Escribir el prototipo y dónde debe ubicarse en el programa.
Qué escribe el programa principal.
void main()
{ int i=11, j ;
j = funcion( i++, 5) ; printf(“\n%d %d”, j, i);
j = funcion( j+i, j-3) ; printf(“\n%d %d”, j, i);
}
int funcion( int x, int y)
{
int s,t ;
for( s=0 ; x != 0 ; x--) for ( t=0 ; t != y ; s++,t++) ;
return( s ) ;
}

E4. Escribir programa.

a) Que genere los primeros 20 números de la secuencia: 1, 4, 7, 10, 14, ....


b) Que genere b elementos de la secuencia a partir del a-ésimo. Leer a y b.

E5. Indicar que escribe el programa.

#include <stdio.h>
int func(int *, float *, char);
void show();

float f1=1, f2=2.2;


int i1=2, i2;
void main()
{
i2 = func(&i1, &f2, '1'); show();
i1 = func(&i2, &f1, '2'); show();
for(;;);
}

void show()
{ printf("\ni1= %d i2= %d" ,i1,i2);
printf("\nf1= %f f2= %f",f1, f2); }

int func(int *i, float *f, char ch)


{
*f = (float)(*i+2); *i = (int)(*f)+2; printf("\n%c ", ch);
return(*i+2);
}

E6. Indicar qué imprime el programa.

#include <stdio.h>

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 31
int a[5];
int i;
void arreglo(int j){ for(i=0;i<5; a[i]=j+i, i++); }
void show(void){for(i=0;i<5;i++) printf(" %3d" , a[i]); printf("\n");}

void main(void)
{ arreglo(1), show();
arreglo(2), show();
arreglo(a[2]+*a+3), show();
while(1);
}

E7. Indicar qué imprime el programa.

Indicar qué efectúa cada función.


#include <stdio.h>
void prt(int);
void f2(int&);
void f1(int&);
void main(void)
{ int i=1024;
prt(i);
i = 0x10; f1(i); prt(i);
i = -1; f2(i); prt(i);
}

void f1(int& i)
{ i = (i | 0x8000); }

void f2(int& i)
{ i = (i>>1); i = (i&0x7fff); }

void prt(int i)
{ int j;
for (j=15; j>=0; j--)(1<< j ) &i ? printf( '1') : printf( '0'); printf( '\n');
}

E8. Indicar qué imprime el programa.

#include <stdio.h>
int f1(int& , int);
int f2(int *, int);

void main()
{ int k, m =1;
k = f1(m,5); printf(“%d \n %d “, k, m);
k = 2; m = 3;

Profesor Leopoldo Silva Bijit 26-05-2008


32 Estructuras de Datos y Algoritmos
m= f2(&m, m); printf(“%d \n”, m );
k = 4; m = 6;
k = f1( m, f1(m,3)+5 ); printf(“%d \n %d \n”, k, m);
}

int f1(int&i , int j)


{ j = j+i ; i = i+j; return(i+j); }

int f2(int *i, int j)


{ *i = j+2+*i; return(j+*i); }

E9. Indicar qué escribe el programa.

#include <stdio.h>
struct punto{
int x;
int y; };

struct molde{
int a[2];
char c;
punto p; };

void main()
{
molde m1={{2,4},'p',{3,5}}, m2={{5,6},'q',{7,1}};
molde *pm=&m2;
int *pi=&(m1.p.y);
printf(“%d \n”, *pi ) ;
printf(“%d \n”, cout << m2.a[1] + m1.p.x );
printf(“%d \n”, pm->a[1] + pm->p.x );
printf(“%d \n “, *(pi-1)) ;
pi = &m2.a[0];
printf(%d \n “, *(pi+1));
}

E10. Determinar la salida.

#include <stdio.h>

int f2(int x, int y, int z)


{int i;
for(i=0; i<x; i++) y+=z;
return(y);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 33
int main(void)
{
printf("%d ", f2(5, 3, 2));
return(0);
}

E11. Determinar la salida.

#include <stdio.h>
int arr[10];

void f1(int x, int *y)


{ int i;
for(i=0; i<x; i++) *(y+i) = i;
}
void f2(int x, int *y)
{ int i;
for(i=0; i<x; i++) printf("%d, ", *(y+i));
putchar('\n');
}

int main(void)
{
f1(5, arr); f2(5, arr);
return(0);
}

E12. Determinar la salida.

#include <stdio.h>
int arr[10];
int *pi;
#define NULL ( (char *) 0)

void f1(int x, int *y)


{int i;
for(i=0; i<x; i++) *(y+i)=i;
}

int *f2(int x, int *y, int z)


{ int i;
for(i=0; i<x; i++) if(*(y+i)== z ) return(y+i);
return((int *) NULL);
}

int main(void)
{

Profesor Leopoldo Silva Bijit 26-05-2008


34 Estructuras de Datos y Algoritmos
f1(5, arr); pi=f2(5, arr, 8);
if(pi!=((int *) NULL)) printf("%d ", *pi); else printf("no se encuentra\n");
return(0);
}
#include <stdio.h>

E13. Explicar que realizan las funciones.

void prtlong(unsigned long int i)


{ int j; unsigned long int k; k=1L;
for (j=31; j>=0; j--) if((k<<j)&i) putchar(‟1‟); else putchar(‟0‟);
}

void prtint(int i)
{ int j, k=1;
for (j=15; j>=0; j--) if( (k<<j)&i ) putchar(‟1‟); else putchar(‟0‟);
}

void sp(int i) { int j; for(j=i; j>0;j--) putchar(‟ ‟); }

void lrs(int *p, int *q)


{
int k,m;
k=(*p)&0x0001; m=(*p)&0x8000;
(*p)>>=1; if(m) *p=(*p)&0x7fff;
(*q)>>=1; if(k) *q=(*q)|0x8000;else *q=(*q)&0x7fff;
}

void ars(int *p, int *q)


{ int k,m;
extern int s;
k=(*p)&0x0001; m=(*q)&0x0001;
(*p)>>=1;(*q)>>=1;if(k) *q=(*q)|0x8000; else *q=(*q)&0x7fff; s=m;
}

E14. Describir un multiárbol

Mediante un arreglo de listas de los hijos de cada nodo. El arreglo debe tener una entrada por
cada nodo, además considerar que la raíz pueda se cualquier nodo.

Profesor Leopoldo Silva Bijit 26-05-2008


Definición de Estructuras de Datos en C. 35
Índice general.

CAPÍTULO 2. ............................................................................................................................................ 1
DEFINICIÓN DE ESTRUCTURAS DE DATOS EN C......................................................................... 1
2.1. TIPOS PRIMITIVOS. ............................................................................................................................. 1
2.2. MECANISMOS DE ESTRUCTURACIÓN. ................................................................................................. 1
2.2.1. Grupos básicos. ......................................................................................................................... 1
2.2.2. Vínculos. .................................................................................................................................... 2
2.3. EJEMPLOS BASADOS EN ARREGLOS. ................................................................................................... 3
2.3.1. Acceso a componentes del arreglo. ........................................................................................... 3
Definición de matrices. .................................................................................................................................... 4
Definición de arreglo de arreglos. .................................................................................................................... 5
Arreglo de punteros a renglones. ..................................................................................................................... 5
Arreglo de punteros a caracteres. ..................................................................................................................... 6
2.3.2. Lista simplemente enlazada en base a cursores. ....................................................................... 6
2.4. EJEMPLOS BASADOS EN ESTRUCTURAS. ............................................................................................. 7
2.4.1. Estructura para fecha. ............................................................................................................... 7
2.4.2. Lista simplemente enlazada en base a punteros. ....................................................................... 9
2.5. ESTRUCTURAS MÁS COMPLEJAS....................................................................................................... 10
2.5.1. Arreglo de listas. ..................................................................................................................... 10
2.5.2. Arreglo de estructuras. ............................................................................................................ 11
2.5.3. Multiárboles. ........................................................................................................................... 11
2.5.3.1. Descripción mediante punteros. ........................................................................................................ 12
2.5.3.2. Descripción mediante arreglos de cursores. ...................................................................................... 12
2.5.3.3. Descripción por arreglo de padres. .................................................................................................... 13
2.6. UN EJEMPLO REAL DE ESTRUCTURAS. .............................................................................................. 14
PROBLEMAS RESUELTOS. ........................................................................................................................ 15
P2.1. Se tiene el siguiente programa: ............................................................................................... 15
P2.2. Escribir una función: ............................................................................................................... 20
P2.3. Se tiene la siguiente función: ................................................................................................... 21
P2.4. Se tiene la siguiente función: ................................................................................................... 22
P2.5. Se tiene la estructura para un nodo, ........................................................................................ 22
P2.6. Se tiene el siguiente programa. ................................................................................................ 26
EJERCICIOS PROPUESTOS. ....................................................................................................................... 29
E1. Determinar qué imprimen los siguientes segmentos: ................................................................ 29
E2. Colocar paréntesis y evaluar las expresiones siguientes: ........................................................... 29
E3. Se tiene el siguiente programa: .................................................................................................. 29
E4. Escribir programa. ..................................................................................................................... 30
E5. Indicar que escribe el programa. ................................................................................................ 30
E6. Indicar qué imprime el programa. ............................................................................................. 30
E7. Indicar qué imprime el programa. .............................................................................................. 31
E8. Indicar qué imprime el programa. .............................................................................................. 31
E9. Indicar qué escribe el programa. ................................................................................................ 32
E10. Determinar la salida. ................................................................................................................ 32
E11. Determinar la salida. ................................................................................................................ 33
E12. Determinar la salida. ................................................................................................................ 33
E13. Explicar que realizan las funciones. ......................................................................................... 34
E14. Describir un multiárbol ............................................................................................................ 34

Profesor Leopoldo Silva Bijit 26-05-2008


36 Estructuras de Datos y Algoritmos
ÍNDICE GENERAL. ....................................................................................................................................35
ÍNDICE DE FIGURAS. ................................................................................................................................36

Índice de figuras.

FIGURA 2.1. AGRUPACIONES DE DATOS. ........................................................................................................2


FIGURA 2.2. VÍNCULOS ENTRE COMPONENTES DE DATOS. ............................................................................2
FIGURA 2.2.A. MATRIZ DE CARACTERES ......................................................................................................4
FIGURA 2.2.B. ARREGLO DE PUNTEROS A ARREGLOS DE CARACTERES ........................................................5
FIGURA 2.3. LISTA SIMPLEMENTE ENLAZADA. ...............................................................................................7
FIGURA 2.4. LISTA SIMPLEMENTE ENLAZADA, MEDIANTE PUNTEROS. ...........................................................9
FIGURA 2.5. ARREGLO DE LISTAS. ...............................................................................................................10
FIGURA 2.6. MULTIÁRBOL. ..........................................................................................................................11
FIGURA 2.7. PRIMER DESCENDIENTE IZQUIERDO, HERMANO DERECHO. .......................................................12
FIGURA 2.8. MULTIÁRBOL MEDIANTE ARREGLO DE CURSORES. ..................................................................13
FIGURA 2.9. MULTIÁRBOL MEDIANTE ARREGLO DE PADRES........................................................................14
FIGURA 2.10 ARREGLO DE PADRES. .............................................................................................................14
FIGURA P2.1. ...............................................................................................................................................17
FIGURA P2.2. ...............................................................................................................................................17
FIGURA P2.3 ................................................................................................................................................18
FIGURA P2.4 ................................................................................................................................................18
FIGURA P2.5. ...............................................................................................................................................19
FIGURA P2.6. ...............................................................................................................................................23
FIGURA P2.7. ...............................................................................................................................................24
FIGURA P2.8. ...............................................................................................................................................24
FIGURA P2.9. ...............................................................................................................................................24
FIGURA P2.10. .............................................................................................................................................24
FIGURA P2.11. .............................................................................................................................................25
FIGURA P2.12. .............................................................................................................................................25
FIGURA P2.13. .............................................................................................................................................26
FIGURA P2.13. .............................................................................................................................................27
FIGURA P2.14. .............................................................................................................................................27
FIGURA P2.15. .............................................................................................................................................28
FIGURA P2.16. .............................................................................................................................................28
FIGURA P2.17. .............................................................................................................................................29

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 3

Administración de la memoria en C.

Los datos se almacenan en uno de los tres segmentos de memoria que el programador dispone.

La zona estática para datos, que permite almacenar variables globales durante la ejecución de un
programa.

El stack que permite almacenar los argumentos y variables locales durante la ejecución de las
funciones.

El heap que permite almacenar variables adquiridas dinámicamente durante la ejecución de un


programa.
Direcciones
bajas estática

heap

Direcciones
stack
altas

Figura 3.1. Segmentos de memoria.

Los segmentos son provistos por el sistema operativo.

3.1. Manejo estático de la memoria.

La zona estática para datos, permite almacenar variables globales y estáticas.

Si se encuentra una variable definida fuera de las funciones, se la considera global; el


compilador le asigna un espacio determinado y genera la referencia para accesarla en la zona
estática. El tamaño de las variables no puede cambiarse durante la ejecución del programa, es
asignado en forma estática.

El tiempo de vida de las variables de la zona estática es la duración del programa.

Estas variables son visibles para todas las funciones que estén definidas después de ellas.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Si se precede con la palabra static a una variable local a una función, ésta también es ubicada en
la zona estática, y existe durante la ejecución del programa; no desaparece al terminar la
ejecución de la función, y conserva su valor, entre llamados a la función.

3.2. Manejo automático de la memoria en C.

3.2.1. Asignación, Referencias y tiempo de vida.

El compilador asigna un espacio determinado para las variables y genera las referencias para
accesar a las variables del stack y de la zona estática. El tamaño de las variables de estas zonas
no puede cambiarse durante la ejecución del programa, es asignado en forma estática.

El tiempo de vida de las variables de la zona estática es la duración del programa; las variables
denominadas automáticas, o en la zona del stack, existen durante la ejecución de la función que
las referencia.

Los argumentos y variables locales, son asignados y desasignados en forma dinámica durante la
ejecución de las funciones; pero en forma automática, el programador no tiene responsabilidad
en ese proceso.

Esta organización permite direccionar eficientemente variables que serán usadas


frecuentemente; a la vez posibilita ahorrar espacio de direccionamiento ya que se puede
reutilizar el espacio de memoria dedicado a la función cuando ésta termina; y también posibilita
el diseño de funciones recursivas y reentrantes, asociando un espacio diferente para las variables
por cada invocación de la función.

Es importante notar que varias funciones pueden emplear los mismos nombres para las variables
locales y argumentos y esto no provoca confusión; existe independencia temporal de las
variables de una función. Si se emplea una global, con igual nombre que una local, dentro de la
función se ve la local; y fuera existe la global.

Por la misma razón, no pueden comunicarse los valores de variables locales de una función a
otra.

Cuando un programa se carga en la memoria, desde el disco, sólo se traen la zona de códigos y
los datos de la zona estática. Las zonas de stack y heap, son creadas en memoria antes de la
ejecución del programa.

3.2.2. Argumentos y variables locales.

Cada función al ser invocada crea un frame o registro de activación en el stack, en el cual se
almacenan los valores de los argumentos y de las variables locales. Los valores de los
argumentos son escritos en memoria, antes de dar inicio al código asociado a la función. Es
responsabilidad de la función escribir valores iniciales a las variables locales, antes de que éstas
sean utilizadas; es decir, que aparezcan en expresiones en donde se leen estas variables.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 3
Ejemplo 3.1. Función con dos argumentos de tipo valor, con dos locales, retorno de entero.
La siguiente definición de función, tiene argumentos, variables locales y un retorno de tipo
entero.

int función1(int arg1, int arg2)


{
int local1;
int local2=5;
/* no puede usarse local1 para lectura, por ejemplo: local2=local1+2; es un error */

local1=arg1 + arg2 + local2;


return ( local1+ 3);
}
A continuación un ejemplo de uso de la función.
Si una variable x, se define fuera de las funciones, se la considera global y se le asigna espacio
en la zona estática. Si en el texto escrito, su definición aparece antes de la función1, se dice que
es visible por ésta, o que está dentro del alcance de la función.

int x=7;
/*En este punto aún no existen las variables: local1, local2, arg1 y arg2. */

x = función1(4, 8);
/*Desde aquí en adelante no existe espacio asociado a los argumentos y variables locales de la
función 1 */

El diagrama de la Figura 3.1, ilustra el espacio de memoria asignado a las variables, después de
invocada la función y justo después de la definición de la variables local2. La variable local1, no
está iniciada y puede contener cualquier valor.

Zona estática Stack

x 7 local1
local2 5
Frame de función1
arg1 4
arg2 8

Figura 3.1a. Stack después de invocar a la función

Al salir de la función, el espacio de memoria asignado a las variables, puede visualizarse según:

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

Zona estática Stack

x 20

Figura 3.2. Stack al salir de la función.

3.2.3. Visión lógica del stack.

Los diagramas anteriores describen lógicamente el espacio asignado en la memoria para las
variables estáticas y automáticas. Cada compilador implementa físicamente la creación de estos
espacios de manera diferente; en el frame de la función se suelen almacenar: la dirección de
retorno, los valores de los registros que la función no debe alterar; además del espacio para
locales y argumentos. Adicionalmente cada compilador establece convenios para pasar los
valores y obtener el retorno de la función, en algunos casos lo hace a través de registros, en
otras a través del stack. El uso detallado del stack según lo requiere un programador assembler
es cubierto en cursos de estructuras de computadores.

Se describe aquí una visualización lógica del segmento del stack, que es la que requiere un
programador en lenguajes de alto nivel.
Ejemplo 3.2. Riesgos de la desaparición del frame.
La siguiente función plocal retorna un puntero a una variable local, lo cual es un gran error, ya
que al salir de la función, deja de existir la local; y el puntero retornado apunta a una dirección
del stack que no está asignada.

int* plocal(void)
{
int local;
// ****
return(&local); // retorna puntero a local
}

La función que invoca a plocal posiblemente produzca una falla seria del programa, o generará
un error de difícil depuración.

Tampoco puede tomarse la dirección de una variable local, declarada de tipo registro. Ya que
los registros no tienen asociada una dirección de la memoria. El calificar una variable local o a
un argumento de tipo register, es una indicación para que el compilador intente emplear
registros en su manipulación, con ventajas temporales de acceso.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 5
3.2.4. Copia de argumentos.

Puede llamarse a una función con los valores de los argumentos o variables locales (o más
generalmente mediante una expresión de éstas) de otra función. Sin embargo la función que es
llamada, crea una copia de los valores de sus argumentos, en su propio frame. Lo cual garantiza
la independencia de funcionamiento de las funciones, ya que una función que es invocada por
otra, puede cambiar sus argumentos sin alterar los argumentos de la que la invocó.

A su vez esta organización implica crear la copia, lo cual puede ser muy costoso en tiempo si el
tipo del argumento tiene un gran tamaño en bytes. Para solucionar el problema de la copia, se
puede pasar una referencia a un argumento de grandes dimensiones, esto se logra pasando el
valor de un puntero (se copia el valor del puntero, el cual ocupa normalmente el tamaño de un
entero). Esto se verá en pasos de argumentos por referencia.
Ejemplo 3.3. Una función f que invoca a otra función g.
Se tienen las siguientes definiciones de las funciones. Nótese que los nombres de los
argumentos y una de las locales tienen iguales nombres.

int g(int a, int b)


{ int c;
printf("Al entrar en g: a = %d b = %d \n", a, b);
a = a + b; /*cambia argumento a */
c = a + 4;
printf("Antes de salir de g: a = %d b = %d c = %d \n", a, b, c);
return( c );
}

int f(int a, int b)


{ int c;
int d=5;
c = a+b+d;
printf("Antes de g: a = %d b = %d c = %d d = %d \n", a, b, c, d);
d = g( a, b+c ); /*se copian los valores en el frame de g */
a = a + d;
b = b + a;
printf("Después de g: a = %d b = %d c = %d d = %d \n", a, b, c, d);
return( d + 2);
}

Si consideramos x definida en la zona estática, el siguiente segmento:


x=3;
x=f(5,6);
printf(" x = %d \n", x);

Genera la siguiente salida.


Antes de g: a = 5 b = 6 c = 16 d = 5
Al entrar en g: a = 5 b = 22

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
Antes de salir de g: a = 27 b = 22 c = 31
Después de g: a = 36 b = 42 c = 16 d = 31
x = 33

Se ilustran los frames, con el estado de las variables, después de los printf.

Antes de invocar a g, se tiene el esquema de la Figura 3.3:


Zona estática Stack

3 local1 c 16
x
local2 d 5
Frame de f
arg1 a 5
arg2 b 6

Figura 3.3. Stack antes de invocar a la función g.

Al entrar en g, se copian valores en los argumentos. Los frames se apilan hacia arriba, lo cual se
muestra en la Figura 3.4.

Zona estática Stack

Frame activo

x 3 local1 c ?
arg1 b 22 Frame de g
arg2 a 5

local1 c 16

local2 d 5
Frame de f
arg1 a 5

arg2 b 6

Figura 3.4. Al entrar en la función g.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 7
Antes de salir de la función g:

Zona estática Stack

Frame activo

x 3 local1 c 31
arg1 b 22 Frame de g
arg2 a 27

local1 c 16

local2 d 5
Frame de f
arg1 a 5

arg2 b 6

Figura 3.5. Stack justo antes de salir de la función g.

Al salir de la función g, ya no está disponible el frame de g, y el frame activo es el de la función


f:
Zona estática Stack

3 local1 c 16
x
local2 d 31
Frame de f
arg1 a 36
arg2 b 42

Figura 3.6. Stack al salir de la función g.

Al salir de la función f, se desvanece su frame, como se ilustra en la Figura 3.7:

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos

Zona estática Stack

x 33

Figura 3.7. Stack al salir de la función f.

Ejemplo 3.4. La misma función anterior invocada dos veces.


Veamos un caso en el cual se ejecuta dos veces la misma función1, del Ejemplo 3.1.

int x=4;
x = función1(4, función1(2,3)) + x;

Se ilustra un diagrama del espacio, cuando ha terminado la ejecución de la asignación a local1,


dentro de la ejecución de la función, pero antes de salir por primera vez de ésta. Los frames se
apilan hacia arriba, en la gráfica. Se ha marcado como activo, el frame de la segunda invocación
a la función1.

Si una función invoca a otra, en este caso invoca a la misma función, mantiene sus locales y
argumentos. Dichas variables existen hasta el término de la ejecución de la función.
Zona estática Stack

Frame activo

4 local1 10
x
local2 5 Frame de segunda
arg1 2 invocación a
función1
arg2 3

local1 ?

local2 ?
Frame de función1
arg1 4

arg2 ?

Figura 3.8. Stack después de la segunda invocación a f.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 9
Una vez que retorna, por primera vez de la función, se tiene el valor 13 de arg2, de la primera
invocación, y desaparece el frame que se creó en la segunda invocación. Se ilustra la zona
después de la asignación a local1.
Zona estática Stack

4 local1 22
x
local2 5
Frame de función1
arg1 4
arg2 13

Figura 3.9. Al salir de la segunda invocación.

Finalmente queda x con valor 29, y no está disponible el frame de la función en el stack.

3.2.4. Recursión.

La organización de las variables automáticas, a través de un stack, permite el desarrollo de


algoritmos recursivos.

Se define recursión como un proceso en el cual una función se llama a sí misma repetidamente,
hasta que se cumpla determinada condición.
Un algoritmo recursivo puede usarse para computaciones repetitivas, en las cuales cada acción
se plantea en términos de resultados previos. Dos condiciones deben tenerse en cuenta en estos
diseños: Una es que cada llamado a la función conduzca a acercarse a la solución del problema;
la otra, es que se tenga un criterio para detener el proceso.

En general los diseños recursivos requieren más espacio de memoria y ocupan mayor tiempo en
ejecutarse. Sin embargo generan diseños simples cuando las estructuras de datos quedan
naturalmente definidas en términos recursivos. Es el caso de los árboles y de algunas funciones
matemáticas.

La recursión es un método para resolver problemas en forma jerárquica (top-down), se parte


reduciendo el problema final (top) en partes más simples, hasta llegar a un caso (bottom), en el
cual se conoce la solución; y se devuelve ascendiendo hasta el tope, calculando con los valores
que se van obteniendo. Cada vez que se activa una invocación de una función recursiva, se crea
espacio para sus variables automáticas en un frame; es decir cada una tiene sus propias
variables. El cambio de las locales de una invocación no afecta a las locales de otra invocación
que esté pendiente (que tenga aún su frame en el stack). Las diferentes encarnaciones de las
funciones se comunican los resultados a través de los retornos.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
En un procedimiento iterativo, también denominado bottom-up, se parte de la base conocida y
se construye la solución paso a paso, hasta llegar al caso final.

Más adelante veremos una estructura básica de datos, denominada stack de usuario (no
confundir con el stack que maneja las variables automáticas). Se puede demostrar que un
algoritmo recursivo siempre se puede plantear en forma iterativa con la ayuda del stack de
usuario. Y un programa iterativo, que requiera de un stack de usuario, se puede plantear en
forma recursiva (sin stack).

Si existe una forma recursiva de resolver un problema, entonces existe también una forma
iterativa de hacerlo; y viceversa.

Consideremos la función matemática factorial, que tradicionalmente está definida en forma


recursiva (a través de sí misma).

factorial( 0 ) = 1
factorial( n ) = n * factorial( n-1 )

La condición para detener la recursión, el caso base, es que factorial de cero es uno. También se
puede detener con factorial(1) = 1.
Ejemplo 3.5. Diseño recursivo.
El siguiente diseño recursivo, condiciona la re-invocación de la función cuando se llega al caso
base:

unsigned int factorial( unsigned int n)


{
if ( n==0) return (1);
else return n*factorial(n-1);
}

El diseño está restringido a valores de n positivos y pequeños; ya que existe un máximo entero
representable, y la función factorial crece rápidamente.

Si se invoca: factorial(4), se producen cinco frames en el stack.


El último frame es producido por la invocación de factorial(0), hasta ese momento ninguna de
las funciones ha retornado (todas están ejecutando la acción asociada al else, pero no pueden
retornar ya que requieren para calcular el producto, el valor de retorno de la función).
Existen cinco argumentos, de nombre n, con valores diferentes. La ejecución, del caso base
(n=0), no invoca nuevamente a la función, ya que ésta está condicionada, y retorna el valor 1; lo
cual desactiva el frame con n=0, y pasa a completar la ejecución del llamado factorial(1) que
estaba pendiente. En este momento: conoce n, que es uno, y el valor retornado por factorial(0),
que también es uno; entonces retorna 1, y elimina el frame.
Sigue la ejecución de factorial(2) del mismo modo, hasta eliminar el último frame, retornando el
valor 24.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 11
Ejemplo 3.6. Diseño iterativo.
Similar cálculo se puede realizar en forma iterativa.

unsigned int factorial(unsigned int n)


{ int i;
unsigned int producto = 1;

for (i = 1; i <= n; i++) producto *= i;


return (producto);
}

3.2.6. Parámetros pasados por referencia y valor único de retorno.

La metodología empleada en C de pasar los argumentos de las funciones por valor, logra la
independencia temporal de las variables, lo cual obliga a copiar los valores de los argumentos en
el frame de la función, antes de ejecutar las acciones asociadas a ésta.

Otra limitación del diseño es que una función sólo puede retornar un valor, de esta forma la
función sólo se comunica con una única variable mediante la asignación del valor retornado.
Ejemplo 3.7. Comunicación del valor retornado por una función.
Veamos un ejemplo simple:
Se tiene visible una variable x. Se modifica el valor almacenado en x, con el resultado de la
invocación a una función f, de dos argumentos, que retorna un entero:

int x;

x= f(3, 5);

En un caso más general se pueden pasar expresiones como argumentos actuales:

x = f(a +b, a*b);

Donde a y b deben ser variables visibles, o dentro del alcance de la función, e inicializadas si
son variables locales. Antes de invocar a la función, se evalúan las expresiones y se copian los
valores resultantes en el espacio de memoria del frame de la función. Luego de la ejecución de
las acciones de la función, el único valor retornado por la función es copiado en el espacio
asignado a x.

El programador debe asegurar que los tipos declarados para los argumentos formales, en la
definición de la función, sean compatibles con los tipos resultantes de las expresiones en el
momento del llamado a la función. También es responsabilidad del programador que el tipo
declarado para el retorno de la función sea compatible con el tipo de la variable que recibe el
resultado de la función.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
En un caso más general, el resultado de la función puede formar parte de una expresión, cuyo
tipo debe ser compatible con el de la variable a la cual se asigna dicha expresión:

x = f(a, b) + c

Estas limitaciones pueden superarse si la función escribe o lee variables globales. En este caso
no hay restricciones para el número de variables que una función puede modificar. Lo anterior
también elimina el tener que copiar los valores en argumentos, pudiéndose diseñar funciones
con menor número de argumentos de entrada. Esta práctica genera efectos laterales de difícil
depuración, en un ambiente en el que varias personas diferentes diseñan las funciones de un
programa.

El concepto de puntero permite pasar la referencia a una variable que existe fuera de la función,
eliminando, de esta forma, la copia de variables de grandes dimensiones, ya que sólo es preciso
copiar el valor del puntero.

También puede modificarse la comunicación de más de un valor si la función retorna el valor


de un puntero a una colección (arreglo, string, estructura) agrupada de datos. De esta forma se
mantiene el concepto de que una función se comunica con el resto del ambiente, sólo a través de
sus argumentos y por único valor de retorno.

La evitación de la copia consiste en mantener en memoria sólo una copia de la variable de


interés. Se pasan como argumentos los valores de los punteros que referencian a las variables
que se desea ver o modificar, si se desea sólo referenciar variables aparecen éstas precedidas del
operador que obtiene la dirección, el &. Mediante la indirección de los punteros se pueden leer
o escribir variables externas, dentro de la función, lo cual implica la aparición de asteriscos en el
código.
La copia local contiene el valor del puntero, y a través de la indirección se puede modificar la
variable externa a la función, además del valor retornado; lo cual debe ser considerado un efecto
lateral, y siempre presenta riesgos de generar errores de difícil depuración.
Ejemplo 3.8. Se desea diseñar función que retorne dos resultados.
Sean estos resultados, la suma y la resta de dos variables o valores.
Paso por referencia.
Se desea obtener la suma y diferencia de dos valores cualesquiera, éstos pueden ser pasados por
valor.

Si se elige que el retorno de la función entregue la suma de los argumentos, es preciso agregar
un argumento que pase el valor de la dirección de variable externa a la función, en donde se
escribirá la diferencia:

int f(int x, int y, int *dif)


{
*dif = x – y ; //escribe la diferencia en variable fuera de la función. Aparece operador *.
return (x + y); //retorna la suma
}

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 13

Es conveniente que el código de la función sólo escriba una vez, en la variable pasada por
referencia. Lo cual delimita los efectos laterales.
Nótese que la declaración del tipo de la variable por referencia, recuerda cómo debe ser
empleada la variable dentro de la función. Desde este punto de vista conviene colocar el
asterisco precediendo a la variable.

Un ejemplo de invocación, escribiendo la suma en c y la diferencia en d, variables que se


asumen definidas:

c = f(3, 4, &d); //aparece el operador &

Donde los valores 3 y 4, pueden ser reemplazados por expresiones con valores enteros.

Si se tuviera definida una variable para almacenar el valor del puntero a la variable d, según:

int *vpd = &d; // vpd es de tipo puntero a entero

El llamado puede efectuarse sin el &, según:

c = f(3, 4, vpd);

Lo cual refleja que se está pasando el valor del puntero.

Si se tuviera definido el tipo: puntero a variable de tipo entero según:


typedef int* pi;

La codificación de los argumentos de la función podría escribirse sin asteriscos.

int f(int x, int y, pi pvar)


{
*pvar = x – y ; //De todas maneras aparece operador *.
return (x + y); //retorna la suma
}

Ejemplo de uso:
pi pd=&d;
c = f(3, 4, pd);

Retorno de estructura.
Esta forma posibilita devolver más de un resultado en el retorno de la función.

Se define la estructura dosretornos, para almacenar el resultado.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
struct dosretornos
{
int suma;
int diferencia;
}; //declaración de tipo

struct dosretornos g( int x, int y)


{
struct dosretornos temp;
temp.suma = x + y;
temp.diferencia = x-y;
return (temp);
}

Un ejemplo de uso, si se tiene definida una variable c de tipo dos retornos:


struct dosretornos c; //definición de variable

c = g(3, 4);

La suma queda en c.suma, la diferencia en c.diferencia.

Retorno en arreglo.
Un caso particular es cuando todos los valores que se desean retornar son de igual tipo. En esta
situación se puede efectuar los retornos en un arreglo.

Se tiene un arreglo de dos posiciones:


int a[2]; //variable global

La función puede escribir en la variable global

void g2( int x, int y)


{
*a = x + y; //por global
*(a+1) = x-y;
}

Los resultados quedan en las primeras posiciones del arreglo.

g2(5, 2);
printf(" %d %d\n", a[0], a[1]);

Se puede diseñar una función g3, que escriba en las dos primeras posiciones de un arreglo que
se pasa por referencia:

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 15
void g3( int x, int y, int *arr)
{
*arr = x + y;
*(arr+1) = x-y;
}

Ahora el llamado se efectúa pasando como tercer argumento el nombre del arreglo.

g3(3, 4, a);
printf(" %d %d\n", a[0], a[1]);

3.2.6. Evitación de la copia de argumentos que ocupan muchos bytes.

En general, en el lenguaje C, los arreglos, strings y estructuras se manipulan con funciones a las
cuales se les pasa una referencia. Las funciones pueden retornar una referencia a una
agrupación.
Ejemplo 3.9. Árbol binario.
Veamos un primer ejemplo de una estructura que puede ocupar grandes dimensiones.
Se tienen los tipos de datos: nodo y pnodo, definidos según:

typedef struct tnode


{
int valor;
struct tnode *left;
struct tnode *right;
} nodo, * pnodo;

Y se ha formado un árbol binario, con los siguientes nodos.


raiz

3
pn
1 6

4 8

Figura 3.10. Árbol Binario.

Nótese que para cada nodo: los valores de los nodos que están vinculados por la derecha son
mayores que los valores de los nodos que están conectados por la izquierda. En la Figura 3.10,
se tienen dos variables de tipo puntero a nodo, una apunta a un nodo denominado raíz, el otro
(pn) es una variable auxiliar.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Los nodos que no tienen descendientes deben tener valores nulos en los punteros left y right.

Se desea diseñar una función que encuentre el nodo con mayor valor.

No es razonable pasar como argumento el árbol completo a la función, por el espacio requerido
en el stack, y porque sería necesario además copiar la estructura completa. Entonces podemos
pasar un puntero a un nodo como argumento, esto sólo requiere crear el espacio y copiar el valor
de un puntero (normalmente el tamaño ocupado por un entero). El retorno de la función, será un
puntero al nodo que tenga el mayor valor.

pnodo busca_max(pnodo T)
{ if (T != NULL)
while (T->right != NULL) T = T->right; /* Iterativo. Siempre por la derecha */
return(T);
}

La función considera que se la podría invocar con un argumento apuntando a un árbol vacío, en
ese caso se decide retornar un puntero con valor nulo, que indica un árbol (o subárbol vacío).
Teniendo en cuenta la propiedad del árbol binario, basta descender por la vía derecha, hasta
encontrar un nodo sin descendiente u hoja.

Un ejemplo de uso:

pn = busca_max(raiz);

En el caso del ejemplo, retornaría un puntero al nodo con valor 8.

Otro ejemplo de uso:

pn = busca_max(raiz->left);

Retornaría un puntero al nodo con valor 1.

Ejemplo 3.10 Manipulación de strings.


Veremos la forma de tratar funciones que manipulen arreglos o strings.
Antes de desarrollar el ejemplo repasaremos la estructura de datos asociada a un string.
Definición de string.
a) Arreglo de caracteres.
La siguiente definición reserva espacio para un string como un arreglo de caracteres.

La definición de un string como arreglo de caracteres, debe incluir un espacio para el carácter
fin de string (el carácter NULL, con valor cero). Quizás es mejor definir el terminador de string
como null (o eos: end of string), para evitar confusiones con el valor de un puntero nulo.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 17
char str[6]; /*crea arreglo de chars con espacio para 6 caracteres. Índice varía entre 0 y 5 */
str[5] = NULL; //coloca el fin de string.

str

\0
Figura 3.11. Representación en memoria de un string.

Con la definición anterior el string almacenado en el arreglo puede tener un largo máximo de
cinco caracteres.

El nombre del arreglo str, es un puntero constante que apunta al primer carácter del string, por
ser constante no se le puede asignar nuevos valores o modificar. Las direcciones de memoria
deben considerarse consecutivas; en la dirección más alta se almacena el fin de string.
b) Puntero a carácter.
La definición de un string como un puntero a carácter, puede ser inicializada asignándole una
constante de tipo string. La que se define como una secuencia de cero o más caracteres entre
comillas dobles; el compilador agrega el carácter ‘\0’ automáticamente al final.
Si dentro del string se desea emplear la comilla doble debe precedérsela por un \.

En caso de escribir, en el texto de un programa, un string de varias líneas, la secuencia de un \ y


el retorno de carro (que es invisible en la pantalla) no se consideran parte del string.

char * str1 = "123456789"; /* tiene 10 caracteres, incluido el NULL que termina el string.*/

Un argumento de tipo puntero a carácter puede ser reemplazado en una lista de parámetros, en
la definición de una función por un arreglo de caracteres sin especificar el tamaño. En el caso
del ejemplo anterior, podría escribirse: char str1[ ]. La elección entre estas alternativas suele
realizarse según sea el tratamiento que se realice dentro de la función; es decir, si las
expresiones se elaboran en base a punteros o si se emplea manipulación de arreglos.

En la variable str1 se almacena la dirección de la memoria en donde se almacena el primer byte


del string, el cual en este caso es el número 1, con equivalente hexadecimal 0x31.

Nótese que str ocupa el espacio con que fue definido el arreglo, mientras que str1 es un puntero.

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos

str1
1
2
3
4
5
6
7
8
9
\0

Figura 3.12. Puntero a carácter y el string vinculado.

c) Strcpy. Copia el string fuente en el string destino.


Se detiene la copia después de haber copiado el carácter nulo del fin del string.
Retorna la dirección del string destino.

char *strcpy(char * destino, register const char * fuente)


{ register char * cp= destino;
while(*cp++ = *fuente++) continue;
return destino;
}

Los argumentos y valor de retorno son punteros a carácter. Lo cual evita la copia de los
argumentos. A continuación se dan explicaciones de diversos aspectos de la sintaxis del
lenguaje.

destino
cp fuente

Figura 3.13. Copia de string.

El diagrama ilustra los punteros fuente y cp, después de haberse realizado la copia del primer
carácter. Se muestra el movimiento de copia y el de los punteros.

Cuando el contenido de *fuente es el carácter NULL, primero lo copia y la expresión resultante


de la asignación toma valor cero, que tiene valor falso para la condición, terminando el lazo
while. Copiando correctamente un string nulo.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 19
La instrucción continue puede aparecer en el bloque de acciones de un while, do o for. Su
ejecución lleva a reevaluar la condición de continuación del bloque de repetición más interno
(en caso de bloques anidados). En el caso de la función anterior podría haberse omitido la
instrucción continue; ya que un punto y coma se considera una acción nula.

El operador de postincremento opera sobre un left value (que recuerda un valor que puede
colocarse a la izquierda de una asignación). Un lvalue es un identificador o expresión que está
relacionado con un objeto que puede ser accesado y cambiado en la memoria.

El uso de estos operadores en expresiones produce un efecto lateral, en el sentido que se


efectúan dos acciones. Primero se usa el valor del objeto en la expresión y luego éste es
incrementado en uno.

El operador de indirección (el *) y el operador ++ tienen la misma precedencia, entonces se


resuelve cuál operador recibe primero el operando mediante su asociatividad, que en el caso de
los operadores unarios es de derecha a izquierda. Es decir *fuente++ se interpreta según:

( * (fuente++) ) .

La expresión toma el valor del puntero fuente y lo indirecciona, posteriormente incrementa en


uno al puntero.

En la expresión (* fuente) ++, mediante el uso de paréntesis se cambia la asociatividad, la


expresión toma el valor del objeto apuntado por fuente, y luego incrementa en uno el valor del
objeto, no del puntero.

Puede evitarse la acción doble relacionada con los operadores de pre y postincremento, usando
éstos en expresiones que sólo contengan dichos operadores. En el caso de la acción de
repetición:
while(*cp++ = *fuente++) continue;

Puede codificarse:
while( *cp = *fuente) {cp++, fuente++};

Sin embargo los programadores no suelen emplear esta forma. Adicionalmente no producen
igual resultado, ya que en la primera forma los punteros quedan apuntando una posición más
allá de los caracteres de fin de string; la segunda forma deja los punteros apuntando a los
terminadores de los strings. Ambas formas satisfacen los requerimientos de srtcpy.

La primera forma sólo tendría ventajas si el procesador tiene mecanismos de direccionamientos


autoincrementados, y si el compilador emplea dichos mecanismos al compilar la primera forma.

Cuando en la lista de parámetros de una función aparece la palabra reservada const precediendo
a una variable de tipo puntero, el compilador advierte un error si la función modifica la variable
a la que el puntero apunta. Además cuando se dispone de diferentes tipos de memorias (RAM,
EEPROM o FLASH) localiza las constantes en ROM o FLASH. Si se desea que quede en un
segmento de RAM, se precede con volatile, en lugar de const.

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos

No se valida si el espacio a continuación de destino puede almacenar el string fuente sin


sobreescribir en el espacio asignado a otras variables. Este es un serio problema del lenguaje, y
se lo ha empleado para introducir código malicioso en aplicaciones que no validen el rebalse de
buffers.

Esta función tiene su prototipo definido en <string.h>

Un ejemplo de uso.

#include <string.h>
#include <stdio.h>
char string[10]; /*crea string con espacio para 10 caracteres */
char * str1 = "abcdefghi"; /* tiene 10 caracteres, incluido el NULL que termina el string.*/

int main(void)
{ strcpy(string, str1);
printf("%s\n", string);
printf(str1); //sin string de formato
return 0;
}

Note que en la invocación se pasan los nombres de los strings, que son considerados punteros
constantes. En lugar de string, se podría haber escrito: &string[0].

No se ocupa el retorno de la función, en este caso se usa como procedimiento no como función.

Arreglos de grandes dimensiones no conviene definirlos dentro de la función, ya que podrían


producir un rebalse del stack; es preferible definirlos en zona estática o en el heap.

Tradicionalmente se menciona que el diseño de strcpy puede ser difícil de entender. Se


muestran a continuación, dos diseños basados en arreglos; y su evolución a códigos basados en
punteros.

void strcpy1(char destino[], const char fuente[])


{ int i = 0;
while (1)
{
destino[i] = fuente[i];
if (destino[i] == '\0') break; // copió fin de string
i++;
}
}

Debido a que en el lenguaje C, la asignación es una expresión, y por lo tanto tiene un valor, se
puede escribir:

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 21

void strcpy2(char destino[], const char fuente[])


{ int i = 0;
while ((destino[i] = fuente[i]) != '\0') i++;
}

// Moviendo los punteros. Resolviendo precedencia por asociatividad.


void strcpy3(char *destino, const char *fuente)
{
while ((*destino++ = *fuente++) != '\0') ; //Trae el valor, luego incrementa puntero.
}

// Finalmente el fin de string '\0' equivale a valor lógico falso


void strcpy4(char *destino, const char *fuente)
{ while (*destino++ = *fuente++) ; }

Las versiones 3 y 4, reflejan en sus argumentos que el diseño está basado en punteros.

La última versión, strcpy4 puede ser difícil de entender. Empleando un compilador que
optimice el código assembler en velocidad, entrega costos similares para los cuatro diseños.

El assembler es para el microcontrolador MSP430, el que dispone de instrucciones con


autoincrementos.

strcpy1o2: mov.b @R14+, 0x0(R12) ;M[R12] = M[R14]; R14++;


mov.b @R12+, R15 ;R15 = M[R12]; R12++;
tst.b R15 ;test de R15;
jne strcpy1o2 ;Si R15 no es igual a cero repite bloque
ret ;Si R15 es cero retorna

strcpy3o4: mov.b @R14+, R15 ; R15 = M[R14]; R14++;


mov.b R15, 0x0(R12) ; M[R12] = R15
inc.w R12 ; R12++;
tst.b R15
jne strcpy3o4
ret

En este caso la versión mediante arreglos emplea una instrucción menos que las versiones con
punteros. La decisión de cual es el mejor código resultará de la comparación de los ciclos de
reloj que tarda la ejecución de las instrucciones del bloque repetitivo, ya que cada instrucción
puede durar diferentes ciclos de reloj.

La invocación a las funciones se logra pasando los argumentos vía registros R12 y R14.

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
mov.w #str1, R10 ; dirección de str1 (destino) en R10
mov.w #str2, R11 ; dirección de str2 (fuente) en R11
mov.w @R11,R14 ; R14 = M[R11] = contenido str2
mov.w @R10,R12 ; R12 = M[R10] = contenido str1
call strcpy

La moraleja de esto es escribir código del cual se tenga seguridad de lo que realiza.

Y dejar a los compiladores optimizantes el resto.

3.3. Manejo dinámico de la memoria en C.

3.3.1. Asignación, Referencias y tiempo de vida.

El compilador asigna un espacio determinado para las variables y genera las referencias para
accesar a las variables del stack y de la zona estática. El tamaño de las variables no puede
cambiarse durante la ejecución del programa, es asignado en forma estática.

El tiempo de vida de las variables de la zona estática es la duración del programa; las variables
denominadas automáticas, o en la zona del stack, existen durante la ejecución de la función que
las referencia. Los frames en el stack, son asignados y desasignados en forma dinámica durante
la ejecución de las funciones; pero en forma automática, el programador no tiene
responsabilidad en ese proceso.

En el heap el programador debe solicitar la asignación de espacio, establecer las referencias


entre el espacio asignado y las variables en las otras zonas, liberar el espacio, desasignar las
referencias. Cualquier equivocación lleva a errores, en tiempo de ejecución, difíciles de depurar.

Este mecanismo permite al programador tener un mayor control de la memoria, tanto en tamaño
como en tiempo de vida, pero al mismo tiempo le da la responsabilidad de administrarla
correctamente. Un arreglo en la zona estática debe ser definido con un tamaño determinado, el
cual no puede cambiar durante la ejecución del programa, sin embargo en el heap se puede
solicitar un arreglo del tamaño que se desee, siempre que no exceda el tamaño máximo asignado
al heap.

Escribir programas que manejen el heap es notablemente más difícil, por esta razón lenguajes
más modernos efectúan automáticamente la programación del heap, y no le permiten al
programador realizar esta tarea. Algunos errores de programas que manejan el heap, compilan
correctamente, sin embargo al ejecutarlos se producen errores difíciles de depurar.

3.3.2. Funciones para el manejo del heap.

En <stdlib.h> están los prototipos de las funciones de biblioteca que asignan y desasignan
bloques de memoria. Describiremos las dos fundamentales.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 23
3.3.2.1. void * malloc(size_t tamaño)
Solicita un bloque contiguo de memoria del segmento heap, del tamaño especificado en bytes, y
retorna un puntero de tipo genérico, el cual puede ser moldeado (cast) a cualquier tipo
determinado, al inicio del bloque; retorna NULL si no existen bloques del tamaño solicitado
dentro del heap.

El programador debe asignar el valor retornado a una variable de tipo puntero, estableciendo la
referencia; dicho puntero debe existir en alguna de las otras zonas de memoria. La única forma
de establecer la referencia es mediante un puntero, que inicialmente debe apuntar a NULL.

El contenido del bloque debe ser inicializado antes de ser usado, ya que inicialmente contiene
los valores que estaban previamente almacenados en el bloque del heap.

Suele emplearse el operador sizeof(tipo o nombre_de_variable) que retorna un valor entero sin
signo con el número de bytes con que se representa el tipo o la variable, para calcular el tamaño
del bloque que debe solicitarse.
3.3.2.2. void free(void * puntero)
Free libera el bloque apuntado por puntero y lo devuelve al heap. El valor del puntero debe
haber sido obtenido a través de malloc; produce errores difíciles de depurar invocar a free con
un puntero no devuelto por malloc.

Después de ejecutar free, el programador no puede volver a referenciar datos a través de ese
puntero; debería asignarle NULL, para prevenir errores. Tampoco puede liberarse un bloque
más de una vez.

Es importante liberar el bloque cuando el programa no siga utilizando los datos que almacenó
en el bloque, de esta forma puede volver a utilizarse dicho espacio. En caso de no hacerlo, van
quedando bloques inutilizables en el heap, lo cual origina en el largo plazo la fragmentación y
finalmente el rebalse de éste.

El administrador del heap, que es invocado a través de las funciones anteriores, mantiene sus
propias estructuras de datos en las cuales registra los bloques que están ocupados y los que están
disponibles, y también el tamaño de los bloques. También interactúa con el sistema operativo
para solicitar memoria adicional en caso de crecimiento del heap.
Ejemplo 3.11. Arreglo dinámico de enteros.
El siguiente ejemplo emplea una función para crear, usar y liberar un arreglo dinámico de
enteros de tamaño determinado por el argumento size de la función.

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
void UsaArregloDinámico(unsigned int size)
{
int * Arreglo;
int i;

/*Usar el arreglo antes de asignarlo, provoca errores */

if ( (Arreglo = (int *) malloc(size * sizeof(int)) ) == NULL) {


printf ("Memoria insuficiente para Arreglo\n");
exit(1);
}
/* Se puede usar el Arreglo. Pero sus valores no están iniciados. */
for(i=0; i<size; i++) Arreglo[i]=0; //

/* aquí puede usarse el arreglo…..*/

free(Arreglo); /*después de devolver el bloque no se puede referenciar el Arreglo */


}

Es práctica recomendable condicionar la asignación inicial del puntero Arreglo, a que exista
espacio disponible en el heap. Y salir del programa, a través de exit y comentado la causa, en
caso de no existir memoria disponible. Una alternativa es codificar, mediante assert (requiere
incluir assert.h), que aborta la ejecución si su argumento es falso:

Arreglo = malloc(size * sizeof(int)) ;


assert(Arreglo != NULL);
/*Si la ejecución continua, significa que había espacio, y que se referenció el puntero */

Una vez depurado el programa, conviene emplear el código condicional en lugar de assert, ya
que esta función almacena el sendero de la función que la invoca, además del texto de la
condición y de los números de líneas del archivo fuente.

Debido a que el puntero Arreglo es variable local, debe liberarse el bloque dentro de la función.
Después de salir de la función, queda indefinida la variable Arreglo.
El esquema empleado en este ejemplo, es que la función que solicita el espacio, sea la
responsable de liberarlo.

El administrador del heap, también dispone de la función realloc que permite cambiar
dinámicamente el tamaño de un bloque, y a la vez se encarga de copiar los valores previos de
manera eficiente. El nuevo tamaño puede ser mayor o menor que el original; si es menor sólo se
copian los datos existentes; si es mayor, los nuevos elementos se consideran no iniciados. El
nuevo espacio sigue siendo contiguo, el administrador se encarga de conseguir un nuevo bloque,
si es necesario, de copiar los valores, y de liberar el bloque original.

Arreglo = realloc(Arreglo, sizeof(int)*newsize);


assert(Arreglo != NULL);

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 25
Ejemplo 3.12. Strings dinámicos.
La función CreaString ilustra un uso común del heap, el cual es permitir almacenar strings de
largo variable con eficiente uso de la memoria. Si se emplean variable estáticas, el programador
debe asegurar que el tamaño de éstas sea suficiente para almacenar los strings, lo cual lleva a
reservar un espacio, que en la mayoría de las ocasiones no será empleado. A pesar de esto puede
que se presente un string aún mayor que el espacio fijo reservado, llevando a un rebalse del
buffer, con resultados impredecibles en la ejecución.

char* CreaString(const char* fuente){


char* nuevoString;
nuevoString = (char*) malloc(strlen(fuente) + 1);
/*agrega espacio para el fin de string '\0' */
assert(nuevoString != NULL);
strcpy(nuevoString, fuente);
return(nuevoString);}

La función CreaString retorna un puntero al string creado en el heap, e iniciado con el valor del
string fuente. Las funciones strlen y strcpy tienen sus prototipos en <string.h>
La función que invoca a CreaString es responsable de liberar el espacio, lo cual se ilustra en el
siguiente segmento:

char * texto;

texto = CreaString(“espacio “);


/*A partir de aquí se puede usar el string texto. */

free(texto);

La omisión de la liberación del espacio limita la reutilización de la memoria dedicada al heap,


pudiendo producir el rebalse de éste, ocasionando errores de difícil depuración. El programador
debe estar consciente de cuáles funciones invocan a malloc, para acompañarlas de la liberación
del espacio.
Ejemplo 3.13. Matriz de enteros, de r renglones y n columnas.
t
j
i

t[i][j] ó *(*(t+i)+j)

t[i] ó *(t+i)

Figura 3.14. Matriz. Arreglo de punteros a renglones.

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
La matriz se inicia con el valor val, y retorna un puntero a un puntero a entero.

int **CreaMatriz(int r, int c, int val)


{ int i, j;
int **t = malloc(r * sizeof(int *)); /*crea arreglo de punteros a enteros */
assert(t !=NULL);
for (i = 0; i < r; i++)
{ t[i] = malloc(c * sizeof(int)); /*crea arreglo de c enteros */
assert(t[i] !=NULL);
}
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
t[i][j] = val;
return t;
}

void BorreMatriz(int ** p, int r)


{ int i;
int **t = p;
for (i = 0; i < r; i++) free(t[i]); /*deben liberarse primero los renglones */
free(t);
}

El siguiente segmento ilustra el uso de las funciones:


int **m;
m=CreaMatriz(5, 6, -1);
/*se usa la matriz*/

BorreMatriz(m, 5); /*una vez empleada. Se libera el espacio*/

La creación de listas, árboles, colas, stacks, grafos, etc. puede realizarse eficientemente en el
heap.

Ejemplo 3.14. Crear nodo de un árbol binario.


Se declara la estructura de un nodo.
typedef struct tnode
{
int valor;
struct tnode *left;
struct tnode *right;
} nodo, * pnodo;

La siguiente función solicita y asigna el nodo obtenido en el heap, si es que había espacio
disponible, retornado un puntero al nodo, y dejando iniciadas las variables del nodo.

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 27
pnodo CreaNodo(int dato)
{
pnodo pn=NULL;
if ( (pn= (pnodo) malloc(sizeof(nodo))) ==NULL) exit(1);
else
{
pn->valor=dato; pn->left=NULL; pn->right=NULL;
}
return(pn);
}

void LiberaNodo( pnodo pn)


{
free( pn); //Libera el espacio
}

El siguiente segmento ilustra el uso de las funciones.

pnodo root=NULL; /* el espacio de la variable root existe desde su definición.


root=CreaNodo(5); //se pega el nodo a la raíz

LiberaNodo(root);

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos

Índice general.

CAPÍTULO 3 ..............................................................................................................................................1
ADMINISTRACIÓN DE LA MEMORIA EN C. ....................................................................................1
3.1. MANEJO ESTÁTICO DE LA MEMORIA. ..................................................................................................1
3.2. MANEJO AUTOMÁTICO DE LA MEMORIA EN C.....................................................................................2
3.2.1. Asignación, Referencias y tiempo de vida. .................................................................................2
3.2.2. Argumentos y variables locales. ................................................................................................2
Ejemplo 3.1. Función con dos argumentos de tipo valor, con dos locales, retorno de entero. ......................... 3
3.2.3. Visión lógica del stack. ..............................................................................................................4
Ejemplo 3.2. Riesgos de la desaparición del frame. ......................................................................................... 4
3.2.4. Copia de argumentos. ................................................................................................................5
Ejemplo 3.3. Una función f que invoca a otra función g. ................................................................................. 5
Ejemplo 3.4. La misma función anterior invocada dos veces........................................................................... 8
3.2.4. Recursión. ..................................................................................................................................9
Ejemplo 3.5. Diseño recursivo. ...................................................................................................................... 10
Ejemplo 3.6. Diseño iterativo. ........................................................................................................................ 11
3.2.6. Parámetros pasados por referencia y valor único de retorno. ................................................11
Ejemplo 3.7. Comunicación del valor retornado por una función. ................................................................. 11
Ejemplo 3.8. Se desea diseñar función que retorne dos resultados. ............................................................... 12
Paso por referencia. ................................................................................................................................... 12
Retorno de estructura. ............................................................................................................................... 13
Retorno en arreglo. .................................................................................................................................... 14
3.2.6. Evitación de la copia de argumentos que ocupan muchos bytes. ............................................15
Ejemplo 3.9. Árbol binario............................................................................................................................. 15
Ejemplo 3.10 Manipulación de strings. .......................................................................................................... 16
Definición de string. .................................................................................................................................. 16
a) Arreglo de caracteres. ............................................................................................................................ 16
b) Puntero a carácter. ................................................................................................................................. 17
c) Strcpy. Copia el string fuente en el string destino. .............................................................................. 18
3.3. MANEJO DINÁMICO DE LA MEMORIA EN C. .......................................................................................22
3.3.1. Asignación, Referencias y tiempo de vida. ...............................................................................22
3.3.2. Funciones para el manejo del heap. ........................................................................................22
3.3.2.1. void * malloc(size_t tamaño) ............................................................................................................ 23
3.3.2.2. void free(void * puntero) .................................................................................................................. 23
Ejemplo 3.11. Arreglo dinámico de enteros. ............................................................................................. 23
Ejemplo 3.12. Strings dinámicos. .............................................................................................................. 25
Ejemplo 3.13. Matriz de enteros, de r renglones y n columnas. ................................................................ 25
Ejemplo 3.14. Crear nodo de un árbol binario........................................................................................... 26
ÍNDICE GENERAL. ....................................................................................................................................28
ÍNDICE DE FIGURAS. ................................................................................................................................29

Profesor Leopoldo Silva Bijit 26-05-2008


Manejo de la memoria en C. 29

Índice de figuras.

FIGURA 3.1. SEGMENTOS DE MEMORIA. ........................................................................................................ 1


FIGURA 3.1A. STACK DESPUÉS DE INVOCAR A LA FUNCIÓN........................................................................... 3
FIGURA 3.2. STACK AL SALIR DE LA FUNCIÓN. .............................................................................................. 4
FIGURA 3.3. STACK ANTES DE INVOCAR A LA FUNCIÓN G. ............................................................................ 6
FIGURA 3.4. AL ENTRAR EN LA FUNCIÓN G. .................................................................................................. 6
FIGURA 3.5. STACK JUSTO ANTES DE SALIR DE LA FUNCIÓN G. ..................................................................... 7
FIGURA 3.6. STACK AL SALIR DE LA FUNCIÓN G. ........................................................................................... 7
FIGURA 3.7. STACK AL SALIR DE LA FUNCIÓN F. ........................................................................................... 8
FIGURA 3.8. STACK DESPUÉS DE LA SEGUNDA INVOCACIÓN A F. ................................................................... 8
FIGURA 3.9. AL SALIR DE LA SEGUNDA INVOCACIÓN. ................................................................................... 9
FIGURA 3.10. ÁRBOL BINARIO. ................................................................................................................... 15
FIGURA 3.11. REPRESENTACIÓN EN MEMORIA DE UN STRING. .................................................................... 17
FIGURA 3.12. PUNTERO A CARÁCTER Y EL STRING VINCULADO. ................................................................. 18
FIGURA 3.13. COPIA DE STRING. ................................................................................................................. 18
FIGURA 3.14. MATRIZ. ARREGLO DE PUNTEROS A RENGLONES. ................................................................. 25

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 4.

Complejidad temporal de algoritmos.

4.1. Tiempo de ejecución y tamaño de la entrada.

Se desea tener una medida de la duración del tiempo de ejecución de un algoritmo en función
del tamaño de la entrada.

A través de llamados al sistema operativo se puede conocer el valor del reloj de tiempo real.
Invocando al reloj, antes y después de realizar el algoritmo se tendrá una medida de la duración
del tiempo de ejecución. Sin embargo esta medida es muy dependiente del hardware (memoria,
reloj, procesador), del sistema operativo (multitarea, multiusuario) y puede variar
significativamente dependiendo del computador, del compilador, y de la carga del sistema. Al
disponer de sistemas con multiprocesadores o que la ejecución sea distribuida también afecta
medir el tiempo con cronómetro.

Por la razón anterior como una medida del tiempo de ejecución, se considera contar las
instrucciones del lenguaje de alto nivel que son necesarias realizar.

El tamaño de la entrada debe ser precisado con más detalle. Podría ser el número de bits que
miden la información que el algoritmo procesa, pero en forma tradicional se considera el
número de elementos o componentes básicas que son sometidos al proceso.

Por ejemplo si tenemos un arreglo de n componentes, y el algoritmo tiene por objetivo, sumar
los valores de las componentes, o bien ordenar las componentes, se suele decir que n es el
tamaño de la entrada. Independientemente si el arreglo es de enteros, o de estructuras.

4.2. Complejidad temporal. Definición.

Se denomina complejidad temporal a la función T(n) que mide el número de instrucciones


realizadas por el algoritmo para procesar los n elementos de entrada.
Cada instrucción tiene asociado un costo temporal.

Afecta al tiempo de ejecución el orden en que se procesen los elementos de entrada.


Podría considerarse que los valores de los n casos que se presentan como entrada son los
correspondientes: a un caso típico, o a un caso promedio, o de peor caso. El peor caso es el más
sencillo de definir (el que demore más para cualquier entrada), pero si se desea otros tipos de
entrada habría que definir qué se considera típico, o la distribución de los valores en el caso
promedio.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
4.3. Tipos de funciones.

Las funciones de n pueden ser de diferente tipo:


Funciones constantes: f(n) = 5, o bien g(n) =10.
Funciones logarítmicas: f(n) = log (n), o bien g(n) = nlog(n)
Funciones polinomiales: f(n) = 2 n2, o bien g(n) = 8 n2 + 5 n
Funciones exponenciales: f(n) = 2n, o bien g(n) = 25n.
O mezclas de las anteriores, o cualquier función de n en un caso general.

En general, a medida que aumenta n, las exponenciales son mayores que las polinomiales; a su
vez éstas son mayores que las logarítmicas, que son mayores que las constantes.

4.4. Acotamiento de funciones.

Veremos algunas definiciones que permiten clasificar las funciones por su orden de magnitud.
Interesa encontrar una cota superior de la complejidad temporal. Consideremos la siguiente
definición preliminar de la función O mayúscula (big oh), con la intención de clasificar
funciones polinomiales.

Se dice que T(n) es O(ni) , si existen c y k tales que: T(k) <= c ki

Sea T(n) = (n +1)2.


Entonces debemos encontrar c y k tales que: (k+1)2 <= cki.

Si suponemos que i toma valor 3, la desigualdad anterior se cumple para c= 4 y k>=1


Si suponemos que i toma valor 3, la desigualdad anterior se cumple para c= 2 y k>=1,4376

4n3

2n3

(n+1)2

Figura 4.1. T(n) es O(n3).

Se advierte que T(n) queda acotada por arriba por 2n3 para n>1.5. Entonces T(n) es O(n3).

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 3
Seguramente también es fácil encontrar soluciones si i es mayor que 3.

Interesa encontrar el i menor posible, que cumpla la definición.

Si suponemos que i toma valor 2, la desigualdad anterior se cumple para c= 4 y k>=1


Lo cual prueba que T(n) es O(n2).
Relaciones que podemos observar en el siguiente diagrama.

4n3

4n2

(n+1)2

Figura 4.2. T(n) también es O(n2).

Si para i=2, intentamos encontrar un c menor, por ejemplo 2, se tendrá que T(n) queda acotada
para n>2.41412:

4n2

2n2

(n+1)2

Figura 4.3. T(n) = (n+1)2 es O(n2).

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
Si seguimos disminuyendo c, por ejemplo 1.1, T(n) queda acotada para n>20,48.
Considerando los casos anteriores, una mejor definición de la función O es la siguiente:

Se dice que T(n) es O(ni) , si existen c y n0 tales que: T(n) <= c ni con n>=n0

Intentemos probar que: T(n) = 3n3 + 2 n2 es O(n3)

Debemos encontrar un c que cumpla: 3n3 + 2 n2 <= cn3


Reemplazando c=5, en la relación anterior, se encuentra n>=1. Por lo tanto c=5 y n0=1.
Debido a que existen c y n0, T(n) es O(n3)
La generalización para otro tipo de funciones se logra, con la siguiente definición.

4.5. Función O.

Se dice que T(n) es O( f(n) ) , si existen c y n0 tales que: T(n) <= c f(n) con n>=n0

Sin embargo se necesita un mejor concepto para acotar el orden o magnitud de una función.
Esto considerando el primer ejemplo, en el que se podía decir que T(n) era O(n3) y también que
era O(n2).

4.6. Función .

Una mejor definición para acotar funciones, es la función  que define simultáneamente cotas
superior e inferior para T(n).

Se dice que T(n) es ( f(n) ) , si existen c1, c2 y n0 tales que:

c1 f(n) <=T(n) <= c2 f(n) con n>=n0

Para el ejemplo anterior, c1 = 3, y c2 = 5, con n0 =1.


5n3

3n3 + 2 n2

3n3

Figura 4.4. T(n)= 3n3 + 2 n2 es (n3).

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 5
La definición de  encuentra una f(n) que acota tipo sándwich a la función T(n).

Cuando empleemos a lo largo del texto la notación O, realmente estamos refiriéndonos a la


definición de .

4.7. Costo unitario.

Aplicando la definición puede comprobarse que las funciones constantes tienen complejidad
O(1).

Ejemplo. Sea T(n) = 5.


Se puede escribir: c1*1 <= 5 <= c2*1 con n>=n0
Y es simple encontrar: c1 = 4, c2 = 6 y n0 cualquiera.
Es decir f(n) =1. Con lo cual, se puede afirmar que : T(n) =5 es O(1) para todo n.

El tiempo de ejecución de un algoritmo que no depende significativamente del tamaño de la


entrada es de complejidad O(1).

Una de las mayores simplificaciones para cálculos de complejidad es considerar que las
acciones primitivas del lenguaje son de costo unitario.

Por ejemplo es usual considerar que el cálculo de una expresión, la asignación, son de
complejidad O(1). Excepcionalmente, en comparaciones más detalladas de algoritmos se
cuentan aparte las comparaciones, y los movimientos o copias de datos.

4.8. Regla de concatenación de acciones. Regla de sumas.

Se realiza la acción A, como la secuencia dos acciones A1 y A2 de complejidades temporales


T1(n) y T2(n) respectivamente.

Teorema de sumas.

Si T1(n) es O(f(n)) y T2(n) es O(g(n)) entonces: A es de complejidad O( max( f(n), g(n) ).

Demostración:
Por definición:
T1(n) <= c1 f(n) para n >= n1
T2(n) <= c2 g(n) para n >= n2
Sea n0 = max(n1, n2)

Para n>=n0 se tiene: T(n) = T1(n) + T2(n) <= c1 f(n) + c2 g(n) (1)

Caso a) Sea f(n) > g(n)


Entonces el lado derecho de la relación (1):
c1 f(n) + c2 g(n) = (c1 +c2)f(n) - c2( f(n) - g(n) )
Y como c2( f(n) - g(n) ) es positivo, se puede escribir:
c1 f(n) + c2 g(n) < = (c1 +c2)f(n)

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

Caso b) Sea f(n) < g(n)


Entonces el lado derecho de la relación (1):
c1 f(n) + c2 g(n) = (c1 +c2)g(n) – c1( g(n) - f(n) )
Y como c1( g(n) - f(n) ) es positivo, se puede escribir:
c1 f(n) + c2 g(n) < = (c1 +c2)g(n)

De los dos resultados anteriores, se obtiene la desigualdad:


T(n) = T1(n) + T2(n) <= (c1 +c2) max(f(n), g(n)) para n0 = max(n1, n2).
Aplicando la definición, de la función O, se reconoce que:

T(n) = O( max(f(n), g(n) )

Corolario.

Si se conoce que f > g, entonces: O( f + g ) es O(f).

Ejemplo:
O( n2 + n ) = O (n2) para valores de n tales que n2 > n.

4.9. Regla de productos.

Si T1(n) es O(f(n)) y T2(n) es O(g(n)) entonces: T1(n)T2(n) es O( f(n) g(n) ).

Demostración.
Por definición:
T1(n) <= c1 f(n) para n >= n1
T2(n) <= c2 g(n) para n >= n2
Sea n0 = max(n1, n2)

Para n>=n0 se tiene: T(n) = T1(n) T2(n) <= c1c2 f(n)g(n)


Con c=c1c2 y aplicando la definición de la función O(n) se logra que T(n) es O( f(n) g(n) ) para
n>=n0.

Ejemplos:
O( 3 n2 ) = O (n2) ya que 3 es O(1), y n2 es O(n2).
La regla del producto también puede aplicarse en: n*O( n ) = O (n2)

Si c es una constante y n el tamaño de la entrada:


O(c) = c*O(1) = O(1)
O(cn) = c*O(n) = O(n)

4.10. Regla de alternativa.

if (c) a1; else a2;

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 7
En cálculos de peor caso se toma la complejidad de la acción de mayor orden. Luego se
considera la regla de sumas para el cálculo de la condición y la acción.

Considerando de costo unitario el cálculo de la condición, la Figura 4.5 muestra la complejidad


de la sentencia if.
Sentencia If

O(1)

O( f (n)) O( g (n))

O(max{ f (n), g (n)})

Figura 4.5. Costo de alternativa.

4.11. Regla de iteración.

for ( i=0; i< n; i++) a;


Por regla de sumas se tiene n veces la complejidad temporal de la acción a.

Si la acción del bloque a es O(1) entonces el for es de complejidad n*O(1) = O(n)


La Figura 4.6, considera costos unitarios para la inicialización, reinicio, y cálculo de la
condición; la complejidad del bloque es O(f(n)); el número de veces es de complejidad O(g(n)).
Sentencia For
O(1)
Sentencia While

O(1)
O(1)

O( f (n))
O( g (n)) O( g (n))
O( f (n))
O(1)

O( g (n) f (n)) O( g (n) f (n))

Figura 4.6. Costo de lazo for y while.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
Ejemplo 4.1.

Se tienen tres for anidados:


for (i=1; i<=n-1; i++)
for( j= i+1; j <=n; j++)
for (k=1; k <=j; k++){ O(1) ;}

Calcular la complejidad del segmento.

La sumatoria más interna se realiza j veces.


k j
 O(1)  jO(1)  O( j )
k 1
j n
El segundo for realiza la sumatoria:  j  (i  1)  (i  2)  ...  (n  1)  n
j i 1

j n j n j i
La cual puede componerse según:  j   j   j
j i 1 j 1 j 1

Pero son conocidas las sumas de los enteros y los cuadrados de los números:
j n n(n  1)
 j  O(n 2 )
j 1 2
j n n(n  1)(2n  1)
 j   O ( n3 )
2

j 1 6
Reemplazando la primera fórmula en las sumatorias del lado derecho, se logra:
j n j n j i n(n  1) i (i  1)
 j   j j  
j i 1 j 1 j 1 2 2

El tercer for realiza la sumatoria:


i  n 1 n(n  1) i(i  1) n(n  1) 1 i n1
 (  ) (n  1)   (i 2  i)
i 1 2 2 2 2 i 1

Lo cual se logra extrayendo la parte que no depende de i fuera de la sumatoria. Y reemplazando


las fórmulas de las sumatorias conocidas, con los índices correspondientes:
i  n 1 n(n  1) i(i  1) n(n  1)(n  1) n3  n
 (  )   O(n3 )
i 1 2 2 3 3

4.12. Algoritmos recursivos.

En varios casos de mucho interés se conoce la complejidad temporal de un algoritmo mediante


una relación de recurrencia.

Sea:
T(n) = T(n/2) + c con T(1) = c.

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 9

Es decir el algoritmo aplicado a una entrada de tamaño n, puede descomponerse en un problema


de la mitad del tamaño. El costo de la descomposición es una constante de valor c.
Para resolver una relación de recurrencia es preciso conocer el costo para una entrada dada; en
este caso para entrada unitaria el costo es la constante c.

Otra forma de visualizar el tipo de solución que da el algoritmo al problema anterior, es


considerar que en cada pasada se descarta la mitad de los datos de entrada.

Si a partir del caso conocido se van evaluando casos más complejos, pueden obtenerse:
T(1) = c = c = 0+c
T(2) = T(1) + c = 2c = c + c
T(4) = T(2) + c = 3c = 2c + c
T(8) = T(4) + c = 4c = 3c + c
T(16) = T(8) + c = 5c = 4c + c

T(2i) = T(2i-1) + c = (i+1)c= ic + c = c( i+1)

Nótese que se han evaluado en números que son potencias de dos, para obtener con facilidad
una expresión para el término general.
Con 2i = n, se tiene sacando logaritmo de dos en ambos lados:
log2 (2i) = log2(n), pero log2(2i) = i log2(2) = i. Resultando i = log2(n).

Reemplazando en el término general resulta: T(n) = c(log2(n) +1)

2log2(n)

c(log2(n) +1)

log2(n)

n0

Figura 4.7. T(n) = c(log2(n) +1) es O( log2(n) )

Las gráficas de la Figura 4.7, muestran que para T(n) existen c1 y c2 que la acotan por encima y
por debajo, para n> 2,2. Finalmente, se tiene que la solución de la ecuación de recurrencia es:

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
T(n) = O( log2(n) )

Las gráficas comparan el costo lineal versus el logarítmico.

O(n)

O(log2(n))

Figura 4.8 Costo lineal versus costo logarítmico.

Ejemplo 4.2. Evaluando la complejidad en función del tiempo.

Si T(n) refleja el número de instrucciones de costo unitario que deben realizarse para resolver
para n entradas, puede tenerse una medida en unidades de tiempo conociendo el valor
aproximado de la duración de una instrucción.

Si O(1) es equivalente a 1 [seg], se puede construir la siguiente tabla, en cada columna se tiene
una complejidad temporal diferente:

n 3n2 + 7n n2 n log2n
100 0,03 [seg] 0,01 [seg] 6,6 [mseg]
10.000 5 [minutos] 1,7 [minutos] 133 [mseg]
100.000 8 [horas] 3 [horas] 1,66[seg]
1.000.000 35 [días] 12 [días] 6 [seg]

Figura 4.9 Costo temporal.

Usando teoremas sobre comparación de funciones, se tiene que: O(3n2 + 7n) = O(n2 ).
La tabla muestra que las complejidades de los dos algoritmos cuadráticos son comparables en el
orden de magnitud.

Ejemplo 4.3. Aplicación a un algoritmo sencillo.

Calcular la complejidad del segmento que obtiene el mínimo elemento de un arreglo.

min = A[0];

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 11
for(i=1; i<n; i++)
if(A[i] < min) min = A[i];

Si la comparación y la asignación son de costo O(1), entonces el if, en peor caso, es de costo
O(1) + O(1) = O(1).
El for se realiza (n-1) veces, su costo es: (n-1) O(1) = O(n-1) = O(n).
La concatenación de la asignación con el for es de costo: O(1) + O(n) = O(n)
Finalmente el segmento es de orden de complejidad O(n).

Ejemplo 4.4. Comparación de complejidad entre dos algoritmos.

Comparar dos algoritmos para calcular la suma de los primeros n enteros.


Algoritmo 1.
suma= 0;
for(i=1; i<=n; i++) suma+=i;

Algoritmo 2.
Suma = n*(n+1)/2;

Algoritmo 1.
La primera asignación a la variable suma es O(1).
El for realiza una vez la asignación inicial y n veces: test de condición, suma, e incremento de
variable de control; más un test de condición con el que se termina el for.
Para el for, entonces, se tiene: O(1) + n*(O(1)+O(1) +O(1)) + O(1) = O(n)

El total es: O(1)+O(n) = O(n).

Algoritmo 2.
Costo de la suma, más costo de la multiplicación, más costo de la división. Es decir:
O(1)+O(1)+O(1), lo cual resulta O(1).

Por lo tanto conviene emplear el algoritmo 2.

Ejemplo 4.5. Búsqueda en arreglos.

Un problema básico es buscar si un valor está presente en una de las componentes de un arreglo.

Con las siguientes definiciones:


typedef int Tipo; /* tipo de item del arreglo */
typedef int Indice; /* tipo del índice */
#define noencontrado -1
#define verdadero 1
#define MaxEntradas 10

Tipo Arreglo[MaxEntradas]; //Define el arreglo en donde se busca

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
4.13. Búsqueda secuencial.

La búsqueda secuencial compara la clave con cada una de las componentes. La primera vez que
encuentre un elemento del arreglo igual al valor buscado se detiene el proceso. No encuentra
claves repetidas y no se requiere que el arreglo esté ordenado.
Si lo recorre en su totalidad, cuidando de no exceder los rangos del arreglo, y no lo encuentra
debe indicarlo con algún valor específico de retorno. En el diseño se considera retornar el índice
de la componente que cumple el criterio de búsqueda, se decide entonces que un retorno con
valor -1 (ya que no es un índice válido), indicará que el valor buscado no fue hallado.

Indice BusquedaSecuencial(Tipo A[], Indice Inf, Indice Sup, Tipo Clave)


{ Indice i;

for(i = Inf; i<=Sup; i++)


if (A[i] == Clave) return(i);
return (noencontrado) ;
}
La evaluación de la condición del if es O(1), también el retorno es O(1). El bloque que se repite
es entonces O(1).

La iniciación del for es O(1). El test de la condición del for es O(1), también el incremento de i
es O(1). El bloque se repite: (Sup-Inf +1) veces en peor caso.

La complejidad es:
O(1) + (Sup-Inf +1)*(O(1) +(O(1) + O(1)) +O(1) )

Simplificando:
(Sup-Inf+1) O(1) = O(Sup-Inf+1)

La entrada, en este caso, es el número de componentes en las que se busca.

Si n= Sup-Inf+1, se tiene finalmente:


T(n) = O(n).

4.14. Búsqueda binaria (Binary Search)

Se requiere tener un arreglo ordenado en forma ascendente. El algoritmo está basado en ubicar,
mediante la variable auxiliar M, la mitad del arreglo aproximadamente. Entonces o se lo
encuentra justo en el medio; o en la mitad con índices menores que M si el valor buscado es
menor que el de la componente ubicada en la mitad; o en la mitad con índices mayores que M si
el valor buscado es mayor. El costo de encontrar la mitad es de costo constante. El ajustar los
índices también es de costo constante, corresponde a los dos if then else anidados. Si se tienen n
componentes, la complejidad puede describir según:

T(n) = T(n/2) +c

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 13
El costo, en un arreglo con una componente, es constante; es decir T(1) = O(1). La solución de
esta ecuación de recurrencia es: T(n) = O(log(n)).

int BusquedaBinaria(Tipo A[], Indice Inf, Indice Sup, Tipo Clave)


{ Indice M;

while (verdadero)
{
M = (Inf + Sup)/2;
if (Clave < A[M])
Sup = M - 1;
else if (Clave > A[M])
Inf = M + 1;
else return M;
if (Inf > Sup) return (noencontrado) ;
}
}

0
1
2

….
M-1
M
M+1
….
….
n-3
n-2
n-1

Figura 4.10 Búsqueda binaria.

Un aspecto importante del diseño de un bloque repetitivo es asegurar que éste termina. Al inicio
se tiene que Sup> Inf. Cada vez que Sup disminuye, Inf no cambia; y también cuando Inf
aumenta Sup no cambia. En ambos casos la condición Inf >Sup cada vez está más cercana a
cumplirse.

Si el valor buscado es menor que el menor valor contenido en el arreglo, después de algunas
iteraciones, no importando si n inicialmente es par o impar, se llega a que Inf, Sup y M apuntan
a la primera componente del arreglo, etapa en la que se compara con la primera componente,
produciendo Inf>Sup. Similar situación se produce cuando el valor buscado es mayor que la
mayor componente del arreglo. Lo cual verifica que el algoritmo siempre termina en un número
finito de pasos, y que trata bien el caso de que la búsqueda falle.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
4.15. Sobre el costo O(1).

Las personas que conocen los detalles internos de un procesador saben que una suma demora
menos que una multiplicación o división; esto si los números son enteros. En el caso de
flotantes los costos son aún mayores que para enteros.

Veamos esto con más detalle. Si consideramos números de n bits, y un sumador organizado de
tal manera que sume primero los bits menos significativos, luego la reserva de salida de éstos
más los dos bits siguientes; y así sucesivamente. Si se considera O(1) el costo de la suma de un
bit, entonces la suma de dos enteros de n bits será O(n). Esta estructura de un sumador con
propagación ondulada de la reserva puede mejorarse con circuitos adicionales para generar
reservas adelantadas.

Un algoritmo primitivo para efectuar multiplicaciones es mediante la suma repetitiva de uno de


los operandos.
Por ejemplo el producto de dos números de 4 bits:
0101*1100
------
0000
0000
0101
0101
-------
0111100

Esto implica efectuar n sumas si los operandos son de n bits. Lo cual implica un costo O(n2)
para la multiplicación, considerando que cada suma es de costo O(n). Razón por la cual se
suelen diseñar unidades de multiplicación, en hardware, con mejores algoritmos.

En un ambiente de microcontroladores o microprocesadores que no tengan implementada la


operación multiplicación o división, pero que si tengan en su repertorio de acciones: sumas,
restas y desplazamientos a la izquierda y derecha en un bit, se consideran esas instrucciones de
costo O(1); nótese que en el ambiente anterior, la suma era O(n).

Consideremos, en este último contexto, dos algoritmos para multiplicar:

4.17.1. Algoritmos de multiplicación.

/* retorna m*n */
unsigned int multipliquelineal(unsigned int m, unsigned int n)
{ unsigned int r=0;
while ( n>0 )
{ r+=m; n--; }
return(r);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 15
El bloque se repite n veces, y está constituido por un test de condición, una suma y un
incremento. Todas operaciones que pueden traducirse a instrucciones de máquina, y se
consideran de costo O(1). Entonces esta multiplicación es de costo O(n) o lineal.

/* retorna m*n */
unsigned int multipliquelog(unsigned int m, unsigned int n)
{ unsigned int r=0;

while ( n>0 )
{ if(n&1) r+=m;
m*=2; n/=2;
}
return(r);
}

En cada ciclo del while se divide por dos el valor de n.


La multiplicación por dos, equivale a un corrimiento a la izquierda en una posición; la división
por dos equivale a un corrimiento a la derecha en una posición; el test del bit menos
significativo se realiza con una operación and; acumular el producto parcial en la local r, se
efectúa con una suma. Todas estas operaciones, en este ambiente se consideran de costo
constante: O(1).

El algoritmo puede describirse según:

T(n) = T(n/2) + O(1) con T(1) = O(1).

Resultado obtenido antes, como solución de una ecuación de recurrencia, con costo logarítmico:
O(log2n); concluyendo que la segunda rutina, es mucho mejor que la primera. Debe notarse que
en este ejemplo se emplea el valor de uno de los operandos como el tamaño de la entrada.

Dependiendo de lo que consideremos como costo unitario, un algoritmo puede tener costos muy
diferentes.

4.17.2. Algoritmos de división.

Consideremos un algoritmo primitivo de división, basado en restas sucesivas.

//Retorna cuociente q y resto r. n = q*d + r. El resto r se pasa por referencia.


unsigned int dividelineal(unsigned int n, unsigned int d, unsigned int *r)
{ unsigned int q=0;

while (n>=d) { n-=d; q++; }


*r= n; //escribe el resto
return(q);
}

En su peor caso, con denominador unitario, y n el máximo representable, se tiene costo: O(n).

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

El siguiente algoritmo ha sido tradicionalmente usado en procesadores que no tengan


implementada una unidad de multiplicación de enteros en hardware.

//Retorna cuociente q y resto r. Con n = q*d + r .


unsigned int dividelog(unsigned int n, unsigned int d, unsigned int *r)
{ unsigned int dd=d, q=0;
*r = n;
while (dd<=n) dd*=2;
while (dd > d)
{ dd/=2; q= q*2;
if (dd<=*r) {*r-=dd; q++;}
}
return(q);
}

El primer while duplica el denominador hasta que sea mayor que el numerador.
Por ejemplo para operandos de 8 bits, el mayor representable será: 11111111 en binario, lo cual
equivale a 255. Si d es 1, el while (en su peor caso) genera los siguientes valores para dd: 2, 4,
8, 16, 32, 64 128, 256. Es decir se realiza 8 veces. Lo cual también se puede calcular mediante
log2(256) = log2(28) = 8.

En su peor caso con denominador unitario y con el máximo valor para n, se tienen log2n
repeticiones.

El segundo while divide por 2 el tamaño de dd, cada vez que se realiza el lazo, su costo es:
O(log2dd).
Sumando estas complejidades, en un peor caso, se tendrá que O(log2n). Con n el mayor número
representable.

Conclusión: En los algoritmos que estudiaremos, en este curso, es importante tener claridad
acerca del significado del costo O(1).

4.18. Complejidad nlog(n).

Existen algoritmos que pueden describirse por la siguiente relación de recurrencia:

T(n) = 2T(n/2) + cn, con T(1) =c.

En cada iteración el problema puede descomponerse en dos subproblemas similares pero de la


mitad del tamaño. El costo de la descomposición es proporcional a n.

La fórmula anterior es un ejemplo de los algoritmos basados en el paradigma: Dividir para


vencer. Origina los métodos de solución basados en particiones.

La solución de la ecuación de recurrencia es : T(n) = cn(log2(n)+1)

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 17
La cual puede calcularse en forma similar al caso de costo logarítmico.
T(1) = c
T(2) = 2*T(1) + c*2 = 4c = 2c *2
T(4) = 2*T(2) + c*4 = 12c = 4c *3
T(8) = 2*T(4) + c*8 = 32c = 8c *4
T(16) = 2*T(8) + c*16 = 80c = 16c *5

T(2i) = 2*T(2i-1) + c*2i = = 2i c *(i+1)

Con 2i = n, se tiene i = log2(n), sacando logaritmo de dos en ambos lados.

Reemplazando en: T(2i) = 2i c *(i+1)

Se obtiene, finalmente:
T(n) = n c *( log2(n) + 1)

La complejidad temporal de T(n) es ( n log(n) ) para n>=2.


Por el teorema de los productos, no importa la base de los logaritmos.

2n(log2(n))

n(log2(n)+1)

n(log2(n))

Figura 4.11 Complejidad ( n log(n) ).

La gráfica a continuación compara la complejidad cuadrática con la n*log(n).

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos

n2

n(log2(n))

Figura 4.12 ( n log(n) ) versus cuadrática.

La cuadrática crece mucho más rápidamente que la n*log(n).

La n*log(n) crece mucho más rápidamente que la lineal, lo que se muestra a continuación:

n(log2(n))

Figura 4.13 ( n log(n) ) versus lineal.

Para mayores valores de n, se aprecian mejor las diferencias.

4.19. Comparación entre complejidades típicas.

La siguiente gráfica compara cuatro complejidades usuales.

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 19

n2
n(lo
g2(n n(log2(n))
))
n

log2(n)

Figura 4.14 Comparación entre cuatro tipos de complejidades.

Cuando un programador principiante encuentra que el primer algoritmo que se le ocurrió, para
resolver un problema, es O(n2), es posible que le sorprenda la existencia de un algoritmo (que
estudiaremos en este texto) de complejidad O(nlog(n)).

Lo mismo puede decirse de primeros intentos de diseño de algoritmos que conducen a uno de
costo O(n), que pueden ser planteados con complejidad O(log(n)).

4.20. Estudio adicional.

En los textos de referencia existen, normalmente al inicio, capítulos dedicados al cálculo de


complejidades temporales, y al acotamiento del orden de crecimiento. Donde se dan métodos
para acotar funciones o para resolver relaciones de recurrencia.

4.21. Solución de ecuaciones de recurrencia.

4.21.1. Recurrencias homogéneas.

Son del tipo:

a0T (n)  a1T (n  1)  a2T (n  2)  ...  akT (n  k )  0

Donde los ai son coeficientes reales y k un número natural entre 1 y n.


Si se reemplaza T ( n)  x n , resulta la ecuación:

a0 xn  a1 xn1  a2 xn2  ...  ak xnk  0


Factorizando:
(a0 xk  a1 x k 1  a2 x k 2  ...  ak ) x nk  0

Se tiene entonces la ecuación característica:

a0 xk  a1 x k 1  a2 x k 2  ...  ak  0

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
4.21.1.1. Raíces diferentes.
Si las k raíces resultan distintas: x1 , x2 ,...., xk , la solución de la ecuación de recurrencia es una
combinación lineal de las soluciones. Donde los ci se determinan a partir de las condiciones
iniciales.

i k
T (n)   ci xin
i 1

Para resolverla se requieren k condiciones iniciales: T (0), T (1), T (2),..., T (k 1)

Ejemplo 4.6.
La ecuación de recurrencia para cálculos de complejidad en árboles AVL, queda dada por la
ecuación de recurrencia de segundo orden, de Fibonacci, con n  2 :

T (n)  T (n 1)  T (n  2)

Para resolverla es necesario conocer los valores iniciales: T (0)  0, T (1)  1 .


Con el reemplazo: T ( n)  x n , resulta: x2  x  1 , ecuación de segundo grado con solución:
1 5
x1,2  . Entonces la solución de la ecuación de recurrencia es:
2
1 5 n 1 5 n
T (n)  c1 ( )  c2 ( )
2 2

La que evaluada en n=0 y n=1, permite calcular las constantes, resultando:

1 1 5 n 1 1 5 n
T (n)  ( )  ( )
5 2 5 2

Realizando los cálculos con el procesador Maple, se obtiene:

> S3:= rsolve( { T(n)=T(n-1)+T(n-2), T(0) = 0,T(1)=1}, T(n)):


> evalf(S3);
.4472135952 1.618033988n .4472135956 ( -.6180339886 )n
Puede comprobarse que el segundo término es una serie alternada que tiende rápidamente a
cero; y es menor que 0,2 para n>2. Graficando los valores absolutos del segundo término,
mediante:
> plot(abs(-.4472135956*(-.6180339886)^n),n=0..10,thickness=2);

Se obtiene el gráfico:

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 21

Figura 4.15 Acotamiento serie alternada

Para el primer término, se obtiene el crecimiento de T(n), mediante:


> plot(.4472135952*1.618033988^n,n=0..10,thickness=2);
1 5
Donde    1, 618033.. se denomina razón áurea.
2

Figura 4.16 Crecimiento exponencial

Un gráfico de la función y dos adicionales que la acotan, se logra con:

> plot([.4472135952*1.618033988^n,.1*1.618^n,1.618^n],n=20..30,
thickness=2,color=[black,red,blue]);

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos

Figura 4.17 Orden de complejidad de recurrencia Fibonacci

Lo que permite establecer que:


T ( n )  ( n )
4.21.1.2. Raíces múltiples.
En caso de tener una raíz múltiple de orden m, conservando el grado k de la ecuación de
recurrencia, se tiene la ecuación característica:

( x  x1 )m ( x  x2 )  ...  ( x  xk m1 )  0

Si tuviéramos que contar los elementos de la secuencia: s3 , s4 , s5 , podemos realizar el cálculo


según: (5-3 +1) = 3. Del mismo modo, podemos contar los elementos desde:
s2 , s3 , s4, ...., sk m1 , según: (k  m  1)  2  1  k  m , lo cual muestra que la ecuación
característica tiene k raíces en total.

La cual tiene como solución general a:

i m i k
T (n)   ci n x  i 1 n
1  cx n
i i  m 1
i 1 i  m 1

La primera sumatoria introduce m constantes en un polinomio de grado (m-1) en n.

i m

c n
i 1
i
i 1 n
x  (c1n0  c2 n1  ...  cm n m1 ) x1n
1

La forma polinomial se debe a que si x1n es solución de la ecuación de recurrencia, entonces


nx1n también será solución. Reemplazando x1n en la ecuación de recurrencia, debe cumplirse:

a0 x1n  a1 x1n1  a2 x1n2  ...  ak x1nk  0

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 23
La que derivada, respecto de x1 , resulta:

a0 nx1n  a1 (n  1) x1n 1  ...  ak (n  k ) x1n k


0
x1

Si x1 no es cero, debe cumplirse:

a0 nx1n  a1 (n  1) x1n1  a2 (n  2) x1n2  ...  ak (n  k ) x1nk  0


Lo que comprueba que nx1n también es solución de la ecuación de recurrencia.
Similar demostración puede realizarse para comprobar que ni x1n es solución, con i  m

La segunda sumatoria es una combinación lineal de las (m-k) raíces diferentes restantes.

Ejemplo 4.7.
Para la ecuación, con n  3 :

T (n)  5T (n 1)  8T (n  2)  4T (n  3)

con condiciones iniciales T (0)  0, T (1)  2, T (2)  8 para k = 0, 1, 2.

La ecuación característica resulta:


x3  5x2  8x  4  0

Una gráfica del polinomio, muestra que tiene una raíz en x=1.

Figura 4.17.a Raíces de polinomio cúbico.

> plot(x^3-5*x^2+8*x-4,x=0.5..3);

Dividiendo el polinomio cúbico por (x-1) se obtiene:

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos

x3  5 x 2  8 x  4
 x 2  4 x  4  ( x  2) 2
( x  1)

La ecuación característica tiene una raíz de multiplicidad dos:

x3  5 x 2  8 x  4  ( x  2) 2 ( x  1)  0

Entonces la solución general es:


T (n)  (c1n0  c2n1 )2n  c31n

Evaluando T(n) en las condiciones iniciales obtenemos, tres ecuaciones:

T (0)  (c1  c2 0)20  c3  0


T (1)  (c1  c2 )21  c3  2
T (2)  (c1  c2 2)22  c3  8

Las que permiten obtener: c1  0, c2  1, c3  0 .

> solve({c1+c3=0,2*c1+2*c2+c3=2,4*c1+8*c2+c3=8},{c1,c2,c3});

Reemplazando las constantes, la solución de la recurrencia, resulta:

T ( n)  n 2 n

Empleando Maple:
> S1:=rsolve({T(n)= 5*T(n-1)-8*T(n-2)+4*T(n-3),
T(0)=0,T(1)=2,T(2)=8},T(n)):
> simplify(factor(S1));
2n n
Puede graficarse la función y sus cotas, mediante:
> plot([S1,2*S1,0.5*S1],n=4..9,color=[black,blue,red]);

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 25

Figura 4.18 Cotas Ejemplo 4.7

Entonces la complejidad de T(n), resulta:


T ( n )  ( n 2 n )

Este crecimiento exponencial, no polinomial, es característico de determinado tipo de problemas


que son clasificados como NP. Algunos de estos problemas no pueden ser resueltos por las
computadoras actuales; a éstos se los denomina NPC por NP completo.

4.21.2. Recurrencias no homogéneas.

Veremos algunos tipos de ecuaciones de recurrencia no homogéneas que tienen solución


conocida.

4.21.2.1. Excitación potencia de n.


Cuando la excitación, el lado derecho de la ecuación de recurrencia, es una potencia de n; con b
un número real, se tiene:

a0T (n)  a1T (n 1)  a2T (n  2)  ...  akT (n  k )  bn

Puede intentarse, mediante una manipulación algebraica, la transformación a una ecuación


homogénea. Esto no siempre es sencillo.

Ejemplo 4.8.
Sea la relación de recurrencia no homogénea, con n  1:

T (n)  2T (n  1)  3n

Con condición inicial: T (0)  0


Si se plantea, la relación, en (n+1), se obtiene:

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos

T (n  1)  2T (n)  3n 1

Multiplicando por 3, la ecuación original, y restándolas, se logra la homogénea:

T (n 1)  5T (n)  6T (n 1)  0


Con ecuación característica:
x 2  5x  6  0

Con soluciones: x1  3, x2  2 . La solución general es:

T (n)  c1 3n  c2 2n

En la ecuación original, se puede calcular: T (1)  2T (0)  31  3

Evaluando las constantes, mediante las ecuaciones:

T (0)  c1 30  c2 20  0
T (1)  c1 31  c2 21  3
Se obtienen: c1  3, c2  3

Finalmente:
T (n)  3  3n  3  2n
En Maple:
> S4:= rsolve( { T(n)-2*T(n-1) =3^n, T(0) = 0, T(1)=3}, T(n));
S4 := 3 2 n 3 3n
Pueden encontrarse dos funciones que acoten, por encima y por debajo, a la función, mediante:
> plot([S4,1*3^n,2*3^n],n=1..5,thickness=2,color=[black,red,blue]);

Figura 4.19 Cotas Ejemplo 4.8

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 27

Entonces, se concluye que para n>1: T (n)  (3n )

4.21.2.2. Excitación polinómica.

a0T (n)  a1T (n  1)  a2T (n  2)  ...  akT (n  k )  p (n)

Donde p(n) es un polinomio de grado d.

Se intenta llegar a una ecuación característica del tipo:

(a0 xk  a1 xk 1  a2 x k 2  ...  ak )( x  1)d 1  0

La cual puede tratarse como una ecuación homogénea con raíces múltiples. La raíz múltiple
estará asociada a un polinomio en n, y ya conocemos un método general para resolverlas.

Ejemplo 4.9.
T (n)  2T (n 1)  n

El polinomio es de grado 1, por lo tanto d=1.


La ecuación característica resulta:
( x  2)( x  1) 2  0

Con solución general, para raíz simple en 2 y una múltiple doble en 1:

T (n)  c1 2n  (c2  c3n)1n

Con T(0) =0, resultan: T(1)=1, T(2)=4; y pueden calcularse las constantes:

c1  2, c2  2, c3  1
Finalmente, la solución resulta:

T (n)  22n  n  2  (2 n )

La ecuación característica, en forma de polinomio, resulta:

x3  4x2  5x  2  0

Lo cual equivale a la ecuación homogénea:

T (n)  4T (n 1)  5T (n  2)  2T (n  3)

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos
Comparando con la ecuación original, debería cumplirse:

2T (n 1)  5T (n  2)  2T (n  3)  n

Entonces para transformar en una ecuación homogénea se nos debería haber ocurrido derivar la
ecuación homogénea anterior a partir de la original no homogénea. Para esto es preciso
encontrar una expresión para n que dependa de T (n 1) , T (n  2) y T (n  3) .

Empleando la ecuación original se obtienen para: T (n 1) y T (n  2) :

T (n 1)  2T (n  2)  n 1
T (n  2)  2T (n  3)  n  2

Eliminando éstas en la ecuación anterior se comprueba la igualdad del lado izquierdo con n.

En Maple, basta escribir:

> S3:=rsolve( { T(n) = 2*T(n-1)+n , T(0) = 0}, T(n));


S3 := 2 2n 2 n
Para verificar el orden del crecimiento, pueden dibujarse:

> plot([S3,2^n,3*2^n],n=2..6,thickness=2,color=[black,red,blue]);

Figura 4.20 Cotas Ejemplo 4.9

Que muestra que T (n)  (2n ) para n  2 .

4.21.2.3. Método de los coeficientes indeterminados.


Permiten resolver ecuaciones de recurrencia con la forma:

a0T (n)  a1T (n  1)  a2T (n  2)  ...  akT (n  k )  b n p(n)

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 29
Donde p(n) es un polinomio de grado d.

Están basadas en descomponer la solución en sus partes homogénea y particular:

T (n)  Th (n)  Tp (n)


Donde Th ( n) es la solución homogénea, con excitación cero; y Tp (n) es la solución particular.

Tp (n)  pd (n)n mb n

Donde pd ( n ) es un polinomio, de orden d, con coeficientes que serán determinados; m es la


multiplicidad de la raíz b en la ecuación característica. Notar que con excitación solamente de
tipo polinomio, m es la multiplicidad de la raíz 1 en la ecuación característica (con b=1).

Se resolverán los ejemplos anteriores, usando este método.

Ejemplo 4.10.
Para:
T (n)  2T (n  1)  3n
Con condición inicial: T (0)  0

Se tiene la ecuación homogénea:


Th (n)  2Th (n  1)  0

Reemplazando Th (n)  x n se obtiene la ecuación: xn  2 xn1  0


Entonces, la ecuación característica es: x  2  0 , resultando:

Th (n)  c2n

Como 3 no es raíz de la ecuación homogénea, se tendrá que m es cero; además la excitación no


contiene un polinomio, entonces podemos escoger, la solución particular:

Tp (n)  a  3n
Con a el coeficiente de un polinomio de grado 0, que deberá determinarse:

Reemplazando en la ecuación de recurrencia:

a  3n  2(a  3n 1 )  3n
Arreglando, resulta:
2a n
(a  )  3  3n
3

Profesor Leopoldo Silva Bijit 26-05-2008


30 Estructuras de Datos y Algoritmos
De donde resulta que:
2a
a 1
3
Obteniéndose:
a3
La solución general es:
T (n)  Th (n)  Tp (n)  c2n  3  3n

Para evaluar la constante c, se tiene:


T (0)  c 20  3  30  0
La que permite calcular c  3
Obteniéndose igual solución que la anterior, determinada en el Ejemplo 4.8.

Ejemplo 4.11.
T (n)  2T (n 1)  n
Con condición inicial: T (0)  0

La solución homogénea, resulta: Th (n)  c2n


Como 1 no es solución de la ecuación homogénea, se tendrá que m es cero; además b es uno,
por lo tanto el polinomio p debe ser de grado uno. Tenemos entonces la siguiente solución
particular:
Tp (n)  p1 (n)  an  b

Que al ser reemplazada en la ecuación de recurrencia, permite obtener:

(a  n  b)  2  (a(n 1)  b)  n

Arreglando, para determinar coeficientes:

(a)  n  (2a  b)  n
De la cual se pueden plantear:
a  1
2a  b  0
Entonces: Tp (n)  an  b  n  2
La solución general: T (n)  c 2n  n  2

La constante se calcula de: T (0)  c 20  0  2  0 , obteniéndose igual solución que la anterior,


determinada en el Ejemplo 4.9.

Ejemplo 4.12.

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 31

T (n)  2T (n  1)  2 n
Con condición inicial: T (0)  0

Como b es igual a la raíz de la ecuación homogénea, se tendrá con d=0, m=1, b=2, que la
solución particular resulta:

Tp (n)  pd (n)n mb n  a  n  2n

Reemplazando en la relación de recurrencia, se obtiene:

a  n  2n  2(a  (n  1)2n 1 )  2n

El coeficiente debe cumplir: a  n  a  (n 1)  1, resultando a  1 .

La solución general: T (n)  c 2n  n 2n , evaluada en 0:

T (0)  c  20  0  20  0
Como c  0 , la solución es:
T ( n )  n 2 n  ( n 2 n )

En Maple, se obtiene igual solución:

> S4:= rsolve( { T(n)-2*T(n-1) =2^n, T(0) = 0}, T(n));


S4 := 2n ( n 1 ) 2n
> simplify(S4);
2n n

4.21.2.4. Método cuando n es potencia de dos.


Ejemplo 4.13.
T (n)  4T (n / 2)  n
Con condición inicial: T (1)  1

Nótese que n debe ser 2 o mayor. Para n=2, puede calcularse: T (2)  4T (1)  2  6 ; por esta
razón la condición inicial se da con n=1.

Si n es una potencia de dos, se tiene que: n  2k , entonces reemplazando en la ecuación de


recurrencia:

T (2k )  4T (2 k 1 )  2 k

Profesor Leopoldo Silva Bijit 26-05-2008


32 Estructuras de Datos y Algoritmos

Con el siguiente cambio de variable: U ( k )  T (2 k )

Se obtiene, la ecuación de recurrencia:


U (k )  4U (k  1)  2k

Ecuación que podemos resolver, obteniéndose:

U (k )  c  4k  2k

Arreglando, y cambiando la variable U, se obtiene:

T (2k )  c  (2k ) 2  2k
Expresando en términos de n:
T ( n)  c  n 2  n

La cual evaluada en n=1, permite calcular c.


T (1)  c 12  1  1
Finalmente:
T ( n )  2  n 2  n  ( n 2 )
En Maple:
> S6:= rsolve( { T(n)-4*T(n/2) =n, T(1) = 1}, T(n));
S6 := n ( 2 n 1 )
La determinación del orden de complejidad se logra con:

> plot([S6,1*n^2,2*n^2],n=2..8,thickness=2,color=[black,red,blue]);

Figura 4.21 Cotas Ejemplo 4.13

Ejemplo 4.14.
T (n)  2T (n / 2)  n

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 33
Con condición inicial: T (1)  1

Nótese que n debe ser 2 o mayor. Para n=2, se tiene: T (2)  2T (1)  2 . Por esta razón la
condición inicial se da con n=1.

Se tiene que: n  2k , entonces:


T (2k )  2T (2 k 1 )  2 k

Con el siguiente cambio de variable: U ( k )  T (2 k )

Se obtiene, la ecuación de recurrencia:


U (k )  2U (k  1)  2k

Ecuación que podemos resolver, obteniéndose:

U (k )  c  2k  k  2k  T (2k )

Expresando en términos de n, y empleando k  log 2 (n) :

T (n)  c  n  n  log 2 (n)

La cual evaluada en n=1, permite calcular que c es uno.

T (1)  c 1  1 0  1
Finalmente:
T (n)  n  n  log 2 (n)  (n  log 2 (n))

En Maple:
> S7:= rsolve( { T(n)-2*T(n/2) =n, T(1) = 1}, T(n));
n ln( n )
S7 := n
ln( 2 )
La determinación del orden de complejidad se logra con:

> plot([S7,n*ln(n)/ln(2),2*n*ln(n)/ln(2)],n=2..8,thickness=2,
color=[black,red,blue]);

Profesor Leopoldo Silva Bijit 26-05-2008


34 Estructuras de Datos y Algoritmos

Figura 4.22 Cotas Ejemplo 4.14

4.22. Cálculos de complejidad a partir del código.

Los cálculos de complejidad pueden efectuarse, independientemente de la función que el


algoritmo realiza. Se analiza un ejemplo, que muestra los diferentes costos basándose en
criterios de cuenta de instrucciones.

Si bien no es necesario conocer la función realizada por el algoritmo para efectuar el cálculo de
la complejidad temporal, la siguiente función implementa un algoritmo de ordenamiento
conocido como burbuja. Opera sobre un arreglo de enteros de n posiciones.

void Alg1(int a[], int n)


{
int i, j, temp;
for (i=0; i<n-1; i++) //1
for (j=n-1; j>=i+1; j--) //2
if(a[j-1] > a[j]) //3
{ temp=a[j-1]; //4
a[j-1]=a[j]; //5
a[j]=temp; //6
}
}

4.22.1. Cálculo de complejidad basada en operaciones elementales.

Se calculan las operaciones elementales asociadas a cada línea considerando como operación
elemental, a: comparaciones, asignaciones, sumas, restas, y acceso a componentes de un vector.
Línea 1: se ejecuta una asignación de inicio; una resta y una comparación de salida; una resta,
una comparación y una suma por cada una de las iteraciones del lazo.
Línea 2: se ejecutan una resta y asignación de inicio y una suma más una comparación de salida;
una suma y una comparación más una resta por cada una de las iteraciones.
Línea 3: se efectúa la condición, con 4 O(1): una diferencia, dos accesos a un vector, y una
comparación.

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 35
Las líneas 4 a 6, es una acción compuesta que sólo se ejecuta si se cumple la condición de la
línea 3, y se realiza un total de 9 O(1): 3, 4 y 2 O(1) respectivamente.

En el peor caso se efectúa siempre el bloque asociado al if. Tenemos entonces que el lazo
interno se realiza:

n 1
T1 (n)  2  O(1)   (2  4  9  1)  O(1)  2  O(1)
j  i 1

La sumatoria se realiza: el número final menos el inicial, más uno:

(n 1)  (i  1)  1  (n  i 1)

Entonces, en peor caso, el lazo interno tiene un costo:

T1 (n)  (4  16  (n  i  1))  O(1)

El lazo externo, tiene un costo de:

n2
T (n)  1 O(1)   (2  (4  16  (n  i  1))  1)  O(1)  2  O(1)
i 0

Arreglando, y considerando que los términos que no dependen de i, se suman (n-1) veces, se
obtiene:
 n2

T (n)   3  (7  16n  16)(n  1)  16 i   O(1)
 i 0 

La suma de los primeros (n-2) términos, puede plantearse:

n2 n2
(n  2)(n  1)
i  i 
i 0 i 1 2

Reemplazando en la ecuación anterior, se obtiene:

T (n)  (16n2  25n  12  8(n  2)(n  1))O(1)


T (n)  (8n2  n  4)O(1)
Finalmente:
T ( n)  O ( n 2 )

4.22.2. Cálculo de complejidad basada en instrucciones del lenguaje de alto nivel.

Cada instrucción del lenguaje se considera de costo unitario.

Profesor Leopoldo Silva Bijit 26-05-2008


36 Estructuras de Datos y Algoritmos

Se considera que las líneas 4, 5 y 6 son O(1), la acción compuesta tiene costo: O(1)+O(1)+O(1),
es decir 3O(1).
El peor caso es que se realice siempre el bloque del if, y si se considera que éste es de costo
O(1), entonces, las líneas 3, 4, 5 y 6 son 4O(1).

El for interno tiene un costo: (n  i 1)  4  O(1)

Para el for externo, se tiene:

n2
 (n  2)(n  1) 
T (n)   4(n  i  1)  O(1)   4(n  1)(n  2)  4  O(1)
i 0  2 
Resultando:
T (n)  (2n 2  6n  4)  O(1)  O(n 2 )

Debe notarse que el costo unitario, del cálculo basado en contar instrucciones de alto nivel, es
diferente del realizado contando operaciones elementales.

Puede comprobarse que los costos, de los dos procedimientos de cálculo, difieren en una
constante:
(8n 2  n  4)
R ( n) 
(2n 2  6n  4)

Aplicando la regla de L’Hôpital, para calcular el límite, se tiene:

lim R(n)  4
n 

La gráfica siguiente ilustra la relación anterior:

Figura 4.23. Razón entre complejidades

Se obtiene, mediante:

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 37
> e:=(8*n^2 -n-4)/(2*n^2-6*n+4);
8 n 2 n 4
e :=
2 n 2 6 n 4
> limit(e,n=infinity);
4
> plot(e,n=2.2..8,thickness=2);

4.22.3. Cálculo de complejidad basada en instrucciones del lenguaje de máquina.

La compilación de la función en C, genera el listado de las instrucciones en assembler, para el


microcontrolador MSP430; el que se muestra más adelante.

Una aproximación para efectuar la cuenta es considerar que todas las instrucciones tienen igual
costo.

Al listado se le han agregado comentarios para hacerlo más legible, y del listado pueden
obtenerse las siguientes cuentas:

Antes de realizar i=0, se han ejecutado 9 instrucciones assembler, cuestión que no se contempla
en los cálculos anteriores; tampoco se contemplan las 6 instrucciones que se realizan para salir
de la función; corresponden a armar y desarmar el frame de la función. También aparecen las
tres instrucciones necesarias para pasar los argumentos e invocar a la función.

El desarrollo del if resulta con 11 instrucciones. La acción compuesta dentro del if, se logra con
21 instrucciones. Reinicio de los for con 3 instrucciones; evaluación de las condiciones de los
for en 4 instrucciones. Inicio del primer for con una instrucción, del segundo for con 3.

Entonces el for interno, tiene un costo:


n 1
T1 (n)  3  O(1)   (4  11  21  3)  O(1)  4  O(1)
j  i 1

Arreglando, y realizando la sumatoria:

n 1
T1 (n)  7  O(1)  39  O(1)  (7  39( n  i  1))  O(1)  (39n  32  39i)  O(1)
j  i 1

El primer for, tiene costo:


n2
T2 (n)  9  O(1)  1 O(1)   (4  (39n  32  39i)  3)  O(1)  4  O(1)  6  O(1)
i 0
Simplificando:
n2
T2 (n)  20  O(1)   (39n  25  39i)  O(1)
i 0

Extrayendo el término que no depende de i de la sumatoria:

Profesor Leopoldo Silva Bijit 26-05-2008


38 Estructuras de Datos y Algoritmos
n2
T2 (n)  20  O(1)  (39n  25)(n  1)  O(1)  39 i  O(1)
i 0
Realizando la sumatoria:
(n  2)(n  1)
T2 (n)  (39n2  64n  45)  O(1)  39 O(1)
2
Simplificando, se obtiene:
39n2  11n  12
T2 (n)   O(1)
2

Existen tres adicionales, requeridas para pasarle valores a los argumentos e invocar a la función:

(39n2  11n  18)


T (n)   O(1)
2

Alg1(a, N);
0025EE 3E400500 mov.w #0x5, R14 ;copia valor constante N en R14
0025F2 3C400011 mov.w #0x1100,R12 ;copia valor de la dirección de a en R12
0025F6 B0126E25 call #Alg1 ;Invocación de la función

void Alg1(int *a, int n)


{
Alg1:
00256E 0A12 push.w R10 ;formación del frame
002570 0B12 push.w R11 ;salva registros
002572 0812 push.w R8
002574 0912 push.w R9
002576 0D4C mov.w R12,R13 ;argumento a en R12, copia en R13
002578 0C4E mov.w R14,R12 ;argumento n en R14, copia en R12
for (i=0; i<n-1; i++) //1
00257A 0A43 clr.w R10 ;inicio de i
00257C 293C jmp 0x25D0 ;salto a condición de primer for
if(a[j-1] > a[j]) //3
00257E 0F4B mov.w R11,R15 ;en R11 almacena j
002580 0F5F rla.w R15 ;cada entero ocupa 2 bytes. j*2
002582 0E4D mov.w R13,R14 ;a pasa a R14
002584 0E5F add.w R15,R14 ;en R14 apunta a a[j]
002586 084B mov.w R11,R8 ;mueve j a R8
002588 3853 add.w #0xFFFF,R8 ;R8=j-1
00258A 0858 rla.w R8 ;(j-1)*2 en bytes
00258C 0F4D mov.w R13,R15
00258E 0F58 add.w R8,R15 ;en R15 forma a[j-1]
002590 AE9F0000 cmp.w @R15,0x0(R14) ;compara
002594 1734 jge 0x25C4 ;salta a fin del if
{ temp=a[j-1]; //4
002596 0F4B mov.w R11,R15
002598 3F53 add.w #0xFFFF,R15

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 39
00259A 0F5F rla.w R15 ;R15=2(j-1)
00259C 0E4D mov.w R13,R14
00259E 0E5F add.w R15,R14 ;R14 apunta a a[j-1]
0025A0 284E mov.w @R14,R8 ;temp =R8=*(a+2(j-1))
a[j-1]=a[j]; //5
0025A2 0F4B mov.w R11,R15
0025A4 0F5F rla.w R15
0025A6 0E4D mov.w R13,R14
0025A8 0E5F add.w R15,R14 ;R14 apunta a a[j]
0025AA 094B mov.w R11,R9
0025AC 3953 add.w #0xFFFF,R9
0025AE 0959 rla.w R9
0025B0 0F4D mov.w R13,R15
0025B2 0F59 add.w R9,R15 ;R15 apunta a a[j-1]
0025B4 AF4E0000 mov.w @R14,0x0(R15) ;asignación
a[j]=temp; //6
0025B8 0E4B mov.w R11,R14
0025BA 0E5E rla.w R14
0025BC 0F4D mov.w R13,R15
0025BE 0F5E add.w R14,R15
0025C0 8F480000 mov.w R8,0x0(R15)
for (j=n-1; j>=i+1; j--) //2
0025C4 3B53 add.w #0xFFFF,R11 ;reinicio segundo for
for (j=n-1; j>=i+1; j--) //2
0025C6 0F4A mov.w R10,R15 ;mueve j al registro R15
;evalúa condición segundo for
0025C8 1F53 inc.w R15 ; incrementa i
0025CA 0B9F cmp.w R15,R11 ;compara j (en R10) con i+1 (en R15)
0025CC D837 jge 0x257E ;entra al bloque del segundo for
for (i=0;i<n-1; i++) //1 ;sale del segundo for
0025CE 1A53 inc.w R10 ;reinicio primer for
for (i=0; i<n-1; i++) //1
0025D0 0F4C mov.w R12,R15 ;trae operando n
0025D2 3F53 add.w #0xFFFF,R15 ;resta 1 a n
0025D4 0A9F cmp.w R15,R10 ;comparación
0025D6 0334 jge 0x25DE ;salida del primer for
for (j=n-1; j>=i+1; j--) //2
0025D8 0B4C mov.w R12,R11 ;inicia j
0025DA 3B53 add.w #0xFFFF,R11 ;resta 1 a j
0025DC F43F jmp 0x25C6 ;test de la condición del segundo for
}
0025DE 30407046 br #0x4670 ;termina Alg1

004670 3941 pop.w R9 ;desarma frame


004672 3841 pop.w R8 ;restaura registros
004674 3B41 pop.w R11
004676 3A41 pop.w R10
004678 3041 ret ;retorno de Alg1

Profesor Leopoldo Silva Bijit 26-05-2008


40 Estructuras de Datos y Algoritmos
Un cálculo más exacto consiste en contar los ciclos de reloj necesarios para ejecutar las
instrucciones. Esta información se obtiene en el manual del procesador en que se esté
compilando. Esto debido a que, dependiendo de la estructura del procesador, algunas
instrucciones demoran más que otras. Por ejemplo, la instrucción: mov.w @R14,0x0(R15)
demora más en su ejecución que las instrucciones ret o pop.

Otros aspectos que muestra el código assembler, es la aritmética de punteros y las instrucciones
relativas a registros para indireccionar; además del uso de registros y saltos.

El conteo de las instrucciones dependerá de la calidad del compilador que se esté empleando, y
si dispone o no de niveles de optimización. También dependerá del procesador y su repertorio
de instrucciones, y del sistema operativo que se esté empleando. Por estas razones, en cálculos
de complejidad, no suelen contarse las instrucciones de un procesador, sino las del lenguaje de
alto nivel o del pseudo código.

El orden de complejidad, que resulta de contar las instrucciones assembler, también es (n 2 ) .

Puede compararse el costo obtenido mediante contar instrucciones assembler, con la cuenta de
operaciones elementales, mediante:
(39n 2  11n  18)
R ( n)  2
(8n 2  n  4)

lim R(n)  2
n 
Se observa que R(n) tiende rápidamente a la constante dos.

> e2:=2*(39*n^2-11*n+18)/(8*n^2 -n-4);


2 ( 39 n 2 11 n 18 )
e2 :=
8 n 2 n 4
> limit(e2,n=infinity);
39
4
> plot(e2,n=1..6,thickness=2);

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 41

Figura 4.24 Razón constante entre complejidades.

Se concluye de este ejemplo, que una metodología razonablemente útil para calcular la
complejidad de un algoritmo es contar las instrucciones del lenguaje de alto nivel o del pseudo
código.

4.23. Resumen.

Se puede cuantificar el orden de crecimiento empleando la notación  .

El orden de crecimiento es el principal factor que determina el tiempo de ejecución de un


algoritmo.

Si se reduce el orden de crecimiento se logran enormes reducciones en el tiempo de ejecución.

Profesor Leopoldo Silva Bijit 26-05-2008


42 Estructuras de Datos y Algoritmos

Problemas resueltos.

P4.1

Determinar la solución de la siguiente relación de recurrencia:


T(n) = T(n/2) + n, con T(1)= 2.

Si es necesario puede emplear que la suma de la siguiente progresión geométrica es:


n
( n  1 )
 2 i 2  2
i 1
¿Cuál es el orden de complejidad?

Solución.
T(2) = T(1) + 2 = 4 = 22
T(4) = T(2) + 4 =8 = 23
T(8) = T(4) + 8 = 16 = 24
T(16) = T(8) + 16 = 32 = 25

T(2i) = T(2i-1) + 2i = 2i+1 =2*2i


Con 2i = n, se obtiene la solución:

T(n) = 2*n
Entonces: T(n) es ( n ), para todo n.

Ya que existen c1, c2 y n0 tales que:


c1 f(n) <=T(n) <= c2 f(n) con n>=n0
Con c1=1, c2=3 y n0 =0.
1n <=2n <= 3n con n>=0

3*n

2*n

1*n

Figura P4.1 Cotas de T(n).

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 43
Ejercicios propuestos.

E4.1.

Dado un número n, encontrar a y b tales que: a*a+b*b = n*n, con a, b y n enteros mayores que
cero.
Determinar la complejidad temporal y su orden de crecimiento.

E4.2.

Calcular la complejidad de: T(n) = T(n-1) + n con T(0)=0.

E4.3.

Calcular la complejidad:
T(n) = 3T(n–1) + 4T(n–2) si n>1; T(0) = 0; T(1) = 1.
Sol. T(n)=O(4n)

E4.4.

Calcular la complejidad:
T(n) = 2T(n–1) – (n+5)3n si n>0; T(0) = 0.

Sol. T(n) = 9·2n– 9·3n– 3n3n con orden de complejidad: Θ(n3n).

E4.5.

Calcular la complejidad:

T(n) = 4T(n/2) + n2 si n>4, n potencia de 2; T(1) = 1; T(2) = 8.

Sol. T(n) = n2+ n2logn Θ(n2logn).

Profesor Leopoldo Silva Bijit 26-05-2008


44 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 4. .............................................................................................................................................1
COMPLEJIDAD TEMPORAL DE ALGORITMOS. ............................................................................1
4.1. TIEMPO DE EJECUCIÓN Y TAMAÑO DE LA ENTRADA. ...........................................................................1
4.2. COMPLEJIDAD TEMPORAL. DEFINICIÓN. .............................................................................................1
4.3. TIPOS DE FUNCIONES. .........................................................................................................................2
4.4. ACOTAMIENTO DE FUNCIONES. ..........................................................................................................2
4.5. FUNCIÓN O. ........................................................................................................................................4
4.6. FUNCIÓN . ........................................................................................................................................4
4.7. COSTO UNITARIO. ...............................................................................................................................5
4.8. REGLA DE CONCATENACIÓN DE ACCIONES. REGLA DE SUMAS...........................................................5
Teorema de sumas................................................................................................................................5
Corolario. ............................................................................................................................................6
4.9. REGLA DE PRODUCTOS. ......................................................................................................................6
4.10. REGLA DE ALTERNATIVA..................................................................................................................6
4.11. REGLA DE ITERACIÓN. ......................................................................................................................7
Ejemplo 4.1. .........................................................................................................................................8
4.12. ALGORITMOS RECURSIVOS. ..............................................................................................................8
Ejemplo 4.2. Evaluando la complejidad en función del tiempo. ........................................................10
Ejemplo 4.3. Aplicación a un algoritmo sencillo. ..............................................................................10
Ejemplo 4.4. Comparación de complejidad entre dos algoritmos. ....................................................11
Ejemplo 4.5. Búsqueda en arreglos. ..................................................................................................11
4.13. BÚSQUEDA SECUENCIAL.................................................................................................................12
4.14. BÚSQUEDA BINARIA (BINARY SEARCH) .........................................................................................12
4.15. SOBRE EL COSTO O(1). ...................................................................................................................14
4.17.1. Algoritmos de multiplicación. ................................................................................................14
4.17.2. Algoritmos de división. ..........................................................................................................15
4.18. COMPLEJIDAD NLOG(N). ................................................................................................................16
4.19. COMPARACIÓN ENTRE COMPLEJIDADES TÍPICAS.............................................................................18
4.20. ESTUDIO ADICIONAL. .....................................................................................................................19
4.21. SOLUCIÓN DE ECUACIONES DE RECURRENCIA. ...............................................................................19
4.21.1. Recurrencias homogéneas. ....................................................................................................19
4.21.1.1. Raíces diferentes. ............................................................................................................................ 20
4.21.1.2. Raíces múltiples. ............................................................................................................................. 22
4.21.2. Recurrencias no homogéneas. ...............................................................................................25
4.21.2.1. Excitación potencia de n. ................................................................................................................ 25
4.21.2.2. Excitación polinómica. .................................................................................................................... 27
4.21.2.3. Método de los coeficientes indeterminados. .................................................................................... 28
4.21.2.4. Método cuando n es potencia de dos. .............................................................................................. 31
4.22. CÁLCULOS DE COMPLEJIDAD A PARTIR DEL CÓDIGO.......................................................................34
4.22.1. Cálculo de complejidad basada en operaciones elementales. ...............................................34
4.22.2. Cálculo de complejidad basada en instrucciones del lenguaje de alto nivel. ........................35
4.22.3. Cálculo de complejidad basada en instrucciones del lenguaje de máquina. .........................37
4.23. RESUMEN. ......................................................................................................................................41
PROBLEMAS RESUELTOS. ........................................................................................................................42
P4.1 ....................................................................................................................................................42
EJERCICIOS PROPUESTOS. ........................................................................................................................43

Profesor Leopoldo Silva Bijit 26-05-2008


Complejidad temporal de algoritmos 45
E4.1. .................................................................................................................................................. 43
E4.2. .................................................................................................................................................. 43
E4.3. .................................................................................................................................................. 43
E4.4. .................................................................................................................................................. 43
E4.5. .................................................................................................................................................. 43
ÍNDICE GENERAL. ................................................................................................................................... 44
ÍNDICE DE FIGURAS................................................................................................................................. 46

Profesor Leopoldo Silva Bijit 26-05-2008


46 Estructuras de Datos y Algoritmos

Índice de figuras.

FIGURA 4.1. T(N) ES O(N3). ............................................................................................................................2


FIGURA 4.2. T(N) TAMBIÉN ES O(N2). .............................................................................................................3
FIGURA 4.3. T(N) = (N+1)2 ES O(N2). ..............................................................................................................3
FIGURA 4.4. T(N)= 3N3 + 2 N2 ES (N3)...........................................................................................................4
FIGURA 4.5. COSTO DE ALTERNATIVA. ..........................................................................................................7
FIGURA 4.6. COSTO DE LAZO FOR Y WHILE. ...................................................................................................7
FIGURA 4.7. T(N) = C(LOG2(N) +1) ES O( LOG2(N) )........................................................................................9
FIGURA 4.8 COSTO LINEAL VERSUS COSTO LOGARÍTMICO. ..........................................................................10
FIGURA 4.9 COSTO TEMPORAL. ...................................................................................................................10
FIGURA 4.10 BÚSQUEDA BINARIA. ..............................................................................................................13
FIGURA 4.11 COMPLEJIDAD ( N LOG(N) ). .................................................................................................17
FIGURA 4.12 ( N LOG(N) ) VERSUS CUADRÁTICA. ......................................................................................18
FIGURA 4.13 ( N LOG(N) ) VERSUS LINEAL. ................................................................................................18
FIGURA 4.14 COMPARACIÓN ENTRE CUATRO TIPOS DE COMPLEJIDADES. ....................................................19
FIGURA 4.15 ACOTAMIENTO SERIE ALTERNADA..........................................................................................21
FIGURA 4.16 CRECIMIENTO EXPONENCIAL ..................................................................................................21
FIGURA 4.17 ORDEN DE COMPLEJIDAD DE RECURRENCIA FIBONACCI .........................................................22
FIGURA 4.17.A RAÍCES DE POLINOMIO CÚBICO. ...........................................................................................23
FIGURA 4.18 COTAS EJEMPLO 4.7 ...............................................................................................................25
FIGURA 4.19 COTAS EJEMPLO 4.8 ...............................................................................................................26
FIGURA 4.20 COTAS EJEMPLO 4.9 ...............................................................................................................28
FIGURA 4.21 COTAS EJEMPLO 4.13 .............................................................................................................32
FIGURA 4.22 COTAS EJEMPLO 4.14 .............................................................................................................34
FIGURA 4.23. RAZÓN ENTRE COMPLEJIDADES .............................................................................................36
FIGURA 4.24 RAZÓN CONSTANTE ENTRE COMPLEJIDADES...........................................................................41
FIGURA P4.1 COTAS DE T(N). ......................................................................................................................42

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 5.

Conjuntos dinámicos.

Listas, stacks, colas.

Se estudian estructuras abstractas de datos para representar el concepto matemático de


conjuntos, considerando que el número de los elementos del conjunto puede variar en el tiempo.

5.1. Nodos.

Cada elemento o nodo se representa por una estructura, cuyos campos pueden ser leídos y
escritos a través de un puntero a la estructura.

Suele existir un campo que se denomina clave, que identifica unívocamente al nodo; otros
campos suelen contener punteros a otros nodos de la estructura. La clave puede ser numérica o
alfanumérica.

5.2. Operaciones.

Las principales operaciones que suelen implementarse pueden clasificarse en consultas, y


modificaciones.

5.2.1. Consultas:

Buscar un nodo de la estructura que tenga igual valor de clave, que un valor que se pasa como
argumento; retornando un puntero al nodo encontrado o NULL si no está presente.
Seleccionar un nodo de la estructura que tenga el menor o mayor valor de la clave.

Hay otras consultas que pueden hacerse, como buscar el sucesor o antecesor de un nodo.

5.2.2. Modificaciones.

Insertar un nodo con determinados valores en la estructura.


Debe establecerse la forma en que será insertado, de tal modo de preservar la organización de la
estructura. Normalmente esto implica primero conseguir el espacio para el nuevo nodo, y la
inicialización de sus campos; también es usual retornar un puntero al nodo recién creado.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Descartar o remover un nodo de la estructura. Asumiendo que se pasa como argumento un
puntero al nodo que será descartado, o al nodo anterior. La operación debe mantener la
organización de la estructura.

Algunos algoritmos no requieren implementar todas las operaciones. Por ejemplo los que tienen
sólo las operaciones de buscar, insertar y descartar suelen denominarse diccionarios. Los
algoritmos en que sólo se busque e inserte se denominan arreglos asociativos, o tablas de
símbolos. En un diccionario puro sólo se implementa buscar.

La complejidad de estas operaciones suele cuantificarse de acuerdo al número de nodos de la


estructura.

Los principales conjuntos dinámicos que estudiaremos son: listas, stacks, colas, árboles binarios
de búsqueda, tablas de hash y colas de prioridad.

5.3. Listas.

Existe una gran variedad de estructuras denominas listas.

5.3.1. Lista simplemente enlazada.

La lista más básica es la simplemente enlazada, la que puede definirse como la secuencia de
cero (lista vacía) o más elementos de un determinado tipo. Los elementos quedan ordenados
linealmente por su posición en la secuencia. Se requiere sólo un enlace entre un elemento y su
sucesor.

Los elementos de un arreglo ocupan posiciones contiguas o adyacentes en la memoria.


En las listas debe asumirse que el espacio de un nodo no es contiguo con otro; por esta razón, no
basta incrementar en uno el puntero a un nodo, para obtener la dirección de inicio del nodo
siguiente.

Cada nodo está conectado con el siguiente mediante un puntero que es un campo del nodo.

Los elementos del arreglo se direccionan en tiempo constante, O(1). Los elementos de las listas
tienen un costo de acceso O(n), en peor caso.

Las operaciones sobre listas deben considerar que ésta puede estar vacía, lo cual requiere un
tratamiento especial; así también los elementos ubicados al inicio y al final de la lista deben
considerarse especialmente.

Los siguientes diagramas ilustran una lista vacía y una lista con tres elementos. Si los nodos se
crean en el heap, la variable lista, de la Figura 5.1, debe estar definida en el stack, o en la zona
estática, con el tipo puntero a nodo.

Note que el programador no dispone de nombres de variables para los nodos, éstos sólo pueden
ser accesados vía puntero (esto debido a que en el momento de la compilación no se conocen las
direcciones de los nodos; estas direcciones serán retornadas por malloc en tiempo de ejecución).

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 3

lista
lista

1 2 3

nodo1 nodo2 nodo3

Figura 5.1. Lista vacía y con tres nodos.

Se denominan listas con cabecera (header) o centinela aquellas que tienen un primer nodo al
inicio de la lista. Con esta definición algunas de las operaciones sobre listas resultan más
simples, que el caso anterior.
lista
lista
c 1 2 3
c
nodo1 nodo2 nodo3

Figura 5.2. Lista con encabezado vacía y con tres nodos.

El caso de lista vacía y las acciones con el primer o último elemento de la lista han intentado ser
resueltas agregando un nodo de encabezado o un centinela al fin de la lista. Estos elementos
facilitan que las funciones diseñadas traten en forma homogénea a todos los elementos de la
lista; por ejemplo, la inserción al inicio se trata de igual forma que la inserción en otra posición;
el costo del mayor tamaño es despreciable comparado con los beneficios.

Se definen los tipos:

typedef struct moldenodo


{ int clave;
struct moldenodo *proximo;
} nodo, *pnodo;
5.3.1.1. Crea Nodo
La siguiente función retorna un puntero al nodo inicializado:
pnodo CreaNodo(int dato)
{ pnodo pn=NULL;

if ( (pn= (pnodo) malloc(sizeof(nodo))) ==NULL) exit(1);


else
{
pn->clave=dato; pn->proximo=NULL;
}
return(pn);
}

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

pn
dato

Figura 5.3. Espacio antes de salir de CreaNodo.

El diagrama de la Figura 5.3, ilustra la situación justo antes de salir de la función. Después de
salir no existe la variable pn, ya que es automática.

Ejemplos de definición de listas:


pnodo lista=NULL; //Creación de lista vacía sin centinela

lista

Figura 5.4. Creación de lista vacía sin centinela.

//Creación de lista vacía con encabezado.


pnodo listaC = CreaNodo(0);

listaC
0

Figura 5.5. Creación de lista vacía con encabezado.

Se ha considerado valor de clave 0 en el encabezado, pero podría ser otro valor; por ejemplo,
uno que no sea usado por los valores que se almacenarán en la lista.

Debe liberarse, el espacio adquirido mediante malloc, cuando deje de usarse, y dentro del
alcance de lista, y siempre que la lista no esté vacía. Esto se logra con:

free(lista);

Si lista está definida dentro de una función, debe liberarse el espacio, antes de salir de ésta, ya
que luego será imposible liberar el espacio, debido a que las variables locales dejan de existir al
salir de la función. El ejemplo anterior libera el espacio del nodo que está al inicio de la lista; el
borrado de la lista completa requiere liberar el espacio de cada uno de los nodos.

5.3.1.2. Operaciones de consultas en listas.


a) Recorrer la lista.
Recorrer una lista es un tipo de operación frecuente. Veamos por ejemplo una función que
cuente los nodos de la lista.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 5
/*
Dada la dirección de un nodo de la lista
Retornar el número de nodos desde el apuntado hasta el final de la lista.
*/

int LargoLista(pnodo p)
{ int numeroelementos = 0;

while (p != NULL) {
numeroelementos ++;
p = p ->proximo; //recorre la lista
}
return (numeroelementos);
}

lista p->proximo

1 2 3

p numeroelementos

Figura 5.6. Variables en LargoLista.

Una alternativa de diseño es empleando un lazo for.

int LargoLista(pnodo p)
{ int numeroelementos = 0;
for( ; p != NULL; p=p->proximo) numeroelementos ++;
return (numeroelementos);
}

Otras operaciones que demandan recorrer la lista son el despliegue de los elementos de la lista o
buscar un nodo que tenga un determinado valor de clave.

b) Buscar elemento.
Se da una lista y un valor de la clave: se retorna un puntero al nodo de la lista que tiene igual
valor de clave, que el valor pasado como argumento; retorna NULL, si no encuentra dicho valor
en la lista.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
pnodo Buscar(pnodo p, int valor)
{
while (p != NULL) {
if (p->clave== valor) return (p); //lo encontró
else p = p ->proximo; //recorre la lista. O(n)
}
return (p); //retorna NULL si no lo encontró.
}

El costo de la operación es O(n).

Ejemplo de uso.
pnodo q;

if ( (q= Buscar(lista, 5)) == NULL) { /* no encontró nodo con clave igual a 5*/ }
else
{ /* lo encontró. …..*/ }

Si la lista es con centinela:


if ( (q= Buscar(listaC->proximo, 5)) == NULL)
{ /* no encontró nodo con clave igual a 5*/ }
else
{ /* lo encontró. …..*/ }
c) Seleccionar un valor extremo.
Se da una lista y se desea encontrar un puntero al nodo que cumple la propiedad de tener el
mínimo valor de clave. Si la lista es vacía retorna NULL. Nótese que en seleccionar sólo se dan
los datos de la lista; buscar requiere un argumento adicional.

Debido a la organización de la estructura las operaciones de consulta tienen costo O(n).


Veremos que existen estructuras y algoritmos más eficientes para buscar y seleccionar.

pnodo SeleccionarMinimo(pnodo p)
{ int min;
pnodo t;
if (p==NULL) return (NULL);
else
{min=p->clave; //Inicia min
t=p;
p=p->proximo;
}
while (p != NULL) {
if (p->clave <min ) {min=p->clave; t=p;}
p = p ->proximo; //recorre la lista. O(n)
}
return (t);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 7

Si se inicializa la variable min con el mayor valor de su tipo, se simplifica el tratamiento en el


borde.

pnodo SelMin(pnodo p)
{ int min= INT_MAX; //requiere incluir limits.h
pnodo t=NULL;
while (p != NULL) {
if (p->clave < min ) {min=p->clave; t=p;}
p = p ->proximo; //recorre la lista. O(n)
}
return (t);
}

d) Buscar el último nodo.


pnodo ApuntarAlFinal(pnodo p)
{ pnodo t;
if (p==NULL) return (NULL);
else
while (p != NULL) {
t=p;
p = p ->proximo; //recorre la lista. O(n)
}
return (t);
}
5.3.1.3. Operaciones de modificación de listas.
a) Análisis de inserción.
Si consideramos pasar como argumentos punteros a nodos, de tal forma de no efectuar copias de
los nodos en el stack, en la inserción, se requiere escribir direcciones en los campos próximos de
dos nodos, y en determinada secuencia. Esto se requiere para mantener la lista ligada.

Supongamos que tenemos dos variables de tipo puntero a nodo: p apunta a un nodo de una lista
y n apunta a un nodo correctamente inicializado (por ejemplo, el retorno de CreaNodo). La
situación se ilustra en la Figura 5.7 a la izquierda, donde las variables n y p, se han diagramado
por pequeños rectángulos. Los nodos se han representado por círculos, con una casilla para la
clave, y otra para el puntero al nodo siguiente.

El nodo n puede ser insertado después del nodo apuntado por p. La primera escritura en un
campo de la estructura puede describirse por:

n->proximo = p->proximo;

Después de esta acción, la situación puede verse en el diagrama a la derecha de la Figura 5.7.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos

p p->proximo p p->proximo

1 2 1 2

n n
3 3

n->proximo n->proximo

Figura 5.7. Inserción en listas. Primer enlace.

La segunda escritura, que termina de encadenar la lista, y que necesariamente debe realizarse
después de la primera, puede describirse por:

p->proximo = n;

La situación y el estado de las variables, después de la asignación, puede describirse según:

p p->proximo

1 2

n
3

n->proximo

Figura 5.8. Inserción en listas. Segundo enlace.

Los valores que toman las variables de tipo puntero son direcciones de memoria, y no son de
interés para el programador. Es de fundamental importancia apoyarse en un diagrama para
escribir correctamente expresiones en que estén involucrados punteros. Debe considerarse que si
en el diseño se elige que las variables n y p sean los argumentos de la función que inserta un
nodo, después de ejecutada la función, automáticamente ellas dejan de existir.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 9
Se puede emplear el siguiente código, si se desea insertar antes de la posición p; se requiere
una variable entera, de igual tipo que la clave del nodo, para efectuar el intercambio. Si el nodo
tiene más información periférica asociada, también debe ser intercambiada entre los nodos.

int temp;

n->proximo = p->proximo;
p->proximo = n;
temp=p->clave; p->clave=n->clave; n->clave=temp; //importa el orden de la secuencia.

Después de ejecutado el segmento anterior, se ilustra el estado final de las variables y un


esquema de la situación, en el diagrama siguiente.

p p->proximo

3 2

temp
1 n
1

n->proximo

Figura 5.9. Insertar antes.

Si la lista es sin cabecera, la inserción al inicio, debe codificarse en forma especial, ya que no
existe en este caso la variable p->proximo. El inicio de la lista sin cabecera es una variable de
tipo puntero a nodo, no es de tipo nodo, y por lo tanto no tiene el campo próximo.
b) Análisis de la operación descarte.
En el descarte de un nodo, si consideramos pasar como argumento un puntero a la posición del
nodo anterior al que se desea descartar, se requiere escribir una dirección y mantener una
referencia al nodo que se desea liberar a través de free.

Entonces la variable p apunta al nodo anterior al que se desea descartar, y t apunta al nodo que
se desea desligar de la lista. Se ilustra en la Figura 5.10, la situación de las variables, después de
ejecutada la acción:

t=p->proximo;

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

p p->proximo

1 2 3

t t->proximo

Figura 5.10. Fijación de t.

Fijar la posición de t es necesario, ya que el siguiente paso es escribir en p->proximo, lo cual


haría perder la referencia al nodo que se desea liberar.
La variable t es necesaria, ya que tampoco se puede efectuar la liberación del nodo mediante:
free(p->proximo) ya que esto haría perder la referencia al siguiente nodo de la lista (el nodo con
clave 3 en el diagrama).

La siguiente acción es la escritura en un campo, para mantener la lista ligada. Esto se logra con:

p->proximo = t->proximo;

p p->proximo

1 2 3

t t->proximo

Figura 5.11. Mantención de lista ligada.

Ahora puede liberarse el espacio, del nodo que será descartado, mediante:

free(t);

Lo cual se ilustra en la Figura 5.12.

También puede descartarse el nodo apuntado por el argumento, pero se requiere copiar los
valores del nodo siguiente, enlazar con el subsiguiente y liberar el espacio del nodo siguiente.
También debe notarse que descartar el primer nodo requiere un tratamiento especial, ya que se
requiere escribir en el puntero a un nodo, que define el inicio, y en éste no existe el campo
próximo.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 11

p p->proximo

1 3
?
t t->proximo

Figura 5.12. Espacio después de liberar el nodo.

Es un error serio, normalmente fatal, escribir expresiones formadas por:


*t, t->clave, o t->proximo, ya que éstas dejaron de existir, después de la ejecución de free(t).

Si no se libera el espacio, queda un fragmento de la memoria dinámica inutilizable.


No siempre es necesario liberar el espacio, por ejemplo se desea sacar un elemento de una lista e
insertarlo en otra, no debe invocarse a free.

Aparentemente las operaciones de modificación de listas son sencillas, pero como veremos a
continuación aún hay detalles que analizar.
c) Análisis adicionales en operación Insertar después.
Considerando lo analizado anteriormente un primer diseño de la función es el siguiente:

pnodo InsertarDespues( pnodo posición, pnodo nuevo)


{
nuevo->proximo=posicion->proximo;
posicion->proximo=nuevo;
return(nuevo);
}

Se decide retornar la dirección del nodo recién incorporado a la lista.


Pero el diseño puede originar problemas, si el nuevo nodo se obtiene invocando a la función
CreaNodo2 y éste no pudo ser creado por malloc, ya que en este caso tendrá valor NULL.

pnodo CreaNodo2(int dato)


{ pnodo pn=NULL;
if ( (pn= (pnodo) malloc(sizeof(nodo))) !=NULL) ;
{
pn->clave=dato; pn->proximo=NULL;
}
return(pn);
}

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
En este caso, en la función InsertarDespues, no existe nuevo->proximo, lo cual produciría un
error fatal en ejecución. Una forma de resolver lo anterior es agregando una línea para tratar la
excepción.

pnodo InsertarDespues( pnodo posición, pnodo nuevo)


{
if (nuevo == NULL) return (NULL);
nuevo->proximo=posicion->proximo;
posicion->proximo=nuevo;
return(nuevo);
}

El diseño considera que si la función retorna NULL, implica que la inserción falló.
La función funciona bien si la posición apunta al primer nodo, a uno intermedio o al último; ya
que todos éstos tienen el campo próximo. Pero si el argumento posición toma valor NULL, se
producirá un serio error, ya que posición->proximo apunta a cualquier parte, lo cual podría
suceder si se intenta insertar en una lista vacía sin header. Esto lleva a agregar otra alternativa en
el cuerpo de la función:

pnodo InsertarDespues( pnodo posición, pnodo nuevo)


{
if (nuevo == NULL) return (NULL);
if (posicion != NULL)
{ nuevo->proximo=posicion->proximo;
posicion->proximo=nuevo;
}
return(nuevo);
}

Se analiza a continuación la inserción en una lista vacía.


pnodo listaS=NULL; //lista sin header
pnodo listaC= CreaNodo(0); //lista con header

listaS = InsertarDespues(listaS, CreaNodo(1));


Es necesaria la asignación del retorno de la función a la variable listaS, para mantener vinculada
la lista.

En el caso de lista con header, el argumento listaC, no será NULL, en caso de lista vacía. El
llamado: InsertarDespues(listaC, CreaNodo(1)); inserta correctamente el nuevo nodo al inicio
de la lista. El valor de retorno apunta al recién agregado a la lista.

5.3.2. Listas doblemente enlazadas.

Una definición de tipos:

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 13
typedef struct moldecelda
{
int clave;
struct moldecelda *nx; //next
struct modecelda *pr; // previo
} nodo, *pnodo;

pr nx

clave

Figura 5.13. Lista doblemente enlazada.

Los diagramas describen el estado de las variables, antes y después de la operación de insertar el
nodo apuntado por q, después del nodo apuntado por p:
p
p

q q

Figura 5.14. Inserción de nodo en lista doblemente enlazada.

La secuencia de asignaciones describe la inserción.

q->nx = p->nx;
q->pr = p;
p->nx = q ;
q->nx->pr = q ;

Descartar el nodo apuntado por q:


q->pr->nx = q->nx;
q->nx->pr = q->pr ;
free(q) ;

Las operaciones de insertar, buscar y descartar deben considerar las condiciones en los bordes, y
que la lista pueda estar vacía.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
Una forma usual de tratar simplificadamente las condiciones de borde, es definir un nodo vacío,
denominado cabecera o centinela. La Figura 5.15 superior muestra una lista doblemente
enlazada vacía, la inferior una con dos elementos:

Las listas circulares doblemente enlazadas con cabecera son más sencillas de implementar y
manipular. Las listas circulares simplemente enlazadas ocupan menos espacio pero su
codificación debe incluir varios casos especiales, lo cual aumenta el código necesario para
implementarlas y el tiempo para ejecutar las acciones.
lista

lista
h

Figura 5.15. Lista doblemente enlazada circular con centinela.

Tarea: Desarrollar las operaciones: Insertar, descartar y buscar en una lista doblemente enlazada
circular.

5.3.3. Lista circular.

En listas simplemente enlazadas, sin o con cabecera, puede escogerse que el último nodo apunte
al primero, con esto se logra que el primer nodo pueda ser cualquier nodo de la lista.
lista
1 2 3 4

Figura 5.16. Lista simplemente enlazada circular.

La inserción al inicio, en el caso de la Figura 5.16, debe tratarse de manera especial, con costo
O(n), para que el último nodo apunte al nuevo primero. Si la lista es con cabecera, y si el último
apunta a la cabecera, no es necesario introducir código adicional.

5.3.4. Lista auto organizada.

La operación buscar mueve a la primera posición el elemento encontrado. De esta manera los
elementos más buscados van quedando más cerca del inicio de la lista.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 15
5.3.5. Lista ordenada.

Se mantiene, según el orden de la lista, los valores ordenados de las claves. La inserción
requiere primero buscar la posición para intercalar el nuevo nodo.

5.3.6. Listas en base a cursores.

En algunas aplicaciones se limita el número de nodos de la estructura por adelantado. En estos


casos tiene ventajas tratar listas en base a arreglos. Pudiendo ser éstos: arreglos de nodos, en los
cuales se emplean punteros; o bien arreglos que contienen la información de vínculos en base a
cursores que almacenan índices.

5.4. Ejemplos de operaciones en listas sin centinela.

Ejemplo 5.1 Inserción de un nodo.

a) Insertar antes.
Para el diseño de la función suponemos que disponemos del valor nuevo, un puntero que apunta
a un nodo inicializado.
nuevo
dato

Figura 5.17. Nuevo nodo que será insertado.

También disponemos del valor posición, un puntero que apunta al nodo sucesor del que será
insertado. Se ilustran dos posibles escenarios, cuando existe lista y el caso de lista vacía.
lista posición
posición
lista
1 2 3

Figura 5.18. Escenarios para inserción.

En el diseño de la función consideramos que se retorne un puntero al nodo recién insertado.


Para entender las operaciones sobre listas o estructuras que empleen punteros es recomendable
emplear diagramas.

Observamos que en caso de lista no vacía, debe escribirse en el campo nuevo->proximo el valor
del argumento posición, y retornar el valor de nuevo. Si la lista, estaba originalmente vacía no
es preciso escribir el puntero nulo en el campo nuevo->posición, si es que estaba correctamente
inicializado.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

lista
nuevo
dato
1 2 3
posición
nuevo
dato

Figura 5.19. Variables en InsertaNodo.

pnodo InsertaNodo(pnodo posicion, pnodo nuevo)


{
if (nuevo == NULL) return (NULL);
if (posicion!=NULL) nuevo->proximo=posicion; //O(1)
return nuevo;
}

Para una lista no vacía, un ejemplo de uso, se logra con:


lista->proximo=InsertaNodo(lista->proximo, CreaNodo(8));

lista

1 8 2 3

Figura 5.20. Inserta nodo con valor 8 en Figura 5.18.

Originalmente el primer argumento de InsertaNodo apuntaba al nodo dos. Dentro de la función


se escribe en el campo próximo del nodo recién creado, de este modo se apunta al sucesor.
Luego de la asignación, se escribe en el campo de enlace la dirección del nodo agregado.

Un ejemplo de inserción al inicio:


lista =InsertaNodo(lista, CreaNodo(7));
lista

7 1 2 3

Figura 5.21. Inserción al inicio de nodo con valor 7 en Figura 5.18.

La operación diseñada inserta antes de la posición indicada por el argumento.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 17
b) Insertar después.
Una variante es insertar después de la posición.

pnodo InsertaNodoDespues(pnodo posicion, pnodo nuevo)


{
if (nuevo == NULL) return (NULL);
if (posicion!=NULL)
{ nuevo->proximo=posicion->proximo; //enlaza con el resto de la lista
posicion->proximo=nuevo; //termina de enlazar el nuevo nodo
return (posicion);
}
return nuevo;
}
lista posición

1 2 3

nuevo 4

Figura 5.22. Inserción del nodo con valor 4, después del nodo 2 en Figura 5.18.

Es importante el orden de las asignaciones.


c) Insertar al final.
La siguiente función implementa la operación de insertar un nodo, con determinado valor, al
final de la lista.

pnodo InsertaNodoalFinal(pnodo posicion, int dato)


{ pnodo temp=posicion;
if (temp != NULL)
{
while (temp->proximo !=NULL) temp=temp->proximo; //O(n)
temp->proximo=CreaNodo(dato);
return (temp->proximo); //retorna NULL si no se pudo crear el nodo
}
else
return (CreaNodo(dato));
}

Si frecuentemente se realizarán las operaciones de insertar al inicio o insertar al final, es


preferible modificar la definición de la estructura de datos, agregando otra variable para apuntar
al último de la lista, que suele denominarse centinela.

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
d) Insertar al inicio y al final.
Asumiendo variables globales, se simplifica el paso de argumentos. Sin embargo las
operaciones sólo son válidas para la lista asociada a dichas variables globales:

static pnodo cabeza=NULL;


static pnodo cola=NULL;

cabeza
1 2 3 4
cola

Figura 5.23. Inserciones al inicio y al final.

pnodo insertainicio(int clave)


{ pnodo t=CreaNodo(clave);

if(cabeza==NULL) cola=t;
t->proximo=cabeza; cabeza=t; //O(1)
return(t);
}

pnodo insertafinal(int clave)


{ pnodo t =CreaNodo(clave);

if(cola==NULL) { cola=cabeza=t;}
else { cola->proximo=t; cola=t;} //O(1)
return(t);
}

Tarea: Diseñar descartar al inicio y descartar al final.

Cuando sólo se desea insertar y descartar en un extremo la estructura se denomina stack.


Cuando se inserta en un extremo y se descarta en el otro se denomina cola (en inglés queue).
Cuando la estructura posibilita insertar y descartar en ambos extremos se la denomina doble
cola (dequeue o buffer de anillo).
e) Procedimiento de inserción.
Es posible diseñar una función que no tenga retorno, en este caso uno de los argumentos debe
ser pasado por referencia, ya que para mantener la lista ligada debe escribirse en dos campos.
La operación puede aplicarse a varias listas, a diferencia del diseño con globales visto
anteriormente.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 19
void insertanodo_ref(pnodo *p, pnodo t)
{
if (*p==NULL) *p=t; //inserta en lista vacía.
else
{
t->proximo=*p; //lee variable externa.
*p=t; //escribe en variable externa.
}
}

Ejemplos de uso.
Insertanodo_ref(&lista1, CreaNodo(5)); //Paso por referencia. Aparece &.
Insertanodo_ref(&lista2, CreaNodo(3)); // Se inserta en lista2.

lista1
p 1 2 3 4

t
5

Figura 5.23a. Espacio luego de ingresar a la función Insertanodo_ref.

En el diseño anterior, se pasa como argumento un puntero a un puntero a nodo. Lo cual permite
pasar la dirección de la variable que define la lista.

En caso de no emplear definición de tipos, en la definición de la función aparece más de un


asterisco:

void insertanodo_ref(struct moldenodo ** p, pnodo t)

Complicando más aún la interpretación del código de la función.


f) Error común en pasos por referencia.
No es posible escribir fuera de la función sin emplear indirección.
void Push(pnodo p, int valor)
{
pnodo NuevoNodo = malloc(sizeof(struct node));
NuevoNodo->clave = valor;
NuevoNodo->proximo = p;
p = NuevoNodo; // No escribe en variable externa.
}

Push(lista, 1); //no se modifica la variable lista


p pertenece al frame. Desaparece después de ejecutada la función.

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
Ejemplo 5.2. Descartar o Borrar nodo.

Debido a que descartar un nodo implica mantener la estructura de la lista, resulta sencilla la
operación de borrar el siguiente a la posición pasada como argumento.

Se tienen tres escenarios posibles:


Que la lista esté vacía, que la posición dada apunte al último de la lista, y finalmente, que la
posición apunte a un nodo que tiene sucesor.

pnodo Descartar(pnodo p)
{ pnodo t = p;

if (p==NULL) return (p); // Lista vacía


if ( p->proximo==NULL)
{ free(p);
return(NULL); // Último de la lista
}
else
{ t=p->proximo;
free(p);
return (t); //Retorna enlace si borró el nodo.
}
}

Los diagramas ilustran las variables luego de ingresar a la función.


p p lista p->proximo
lista
lista
5 1 2 3
t
t p t

Figura 5.24. Tres escenarios en descarte de nodo.

Es responsabilidad de la función que llama a Descarte mantener ligada la lista, mediante el


retorno.

Tarea: Confeccionar ejemplos de invocación a Descartar, manteniendo ligada la lista.

Borrar el nodo apuntado por p, requiere recorrer la lista, para encontrar el nodo anterior al que
se desea borrar; contemplando el caso que el nodo ha ser borrado sea el primero de la lista. Esta
operación es O(n).

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 21
Para lograr un algoritmo de costo constante, debe modificarse la estructura de datos de la lista,
por ejemplo agregando un puntero al anterior.

Similar situación se tiene si se desea implementar la operación predecesor.

5.5. Stack. Pila. Estructura LIFO (last-in, first-out),

5.5.1. Definición.

La utilidad de esta estructura es muy amplia, y se la ha usado tradicionalmente incorporada al


hardware de los procesadores: para organizar el retorno desde las subrutinas, para implementar
el uso de variables automáticas, permitiendo el diseño de funciones recursivas, para salvar el
estado de registros, en el paso de parámetros y argumentos. Generalmente los traductores de
lenguajes, ensambladores y compiladores, emplean esta estructura para la evaluación y
conversión de expresiones y para la determinación del balance de paréntesis; también existen
arquitecturas virtuales denominadas máquinas de stack, para traducir a lenguajes de nivel
intermedio las sentencias de lenguajes de alto nivel.

Describiremos ahora lo que suele denominarse stack de usuario, como una estructura de datos
que permite implementar el proceso de componentes con la política de atención: la última que
entró, es la primera en ser atendida.

El stack es una lista restringida, en cuanto a operaciones, ya que sólo permite inserciones y
descartes en un extremo, el cual se denomina tope del stack.

Debido a esta restricción suelen darse nombres especializados a las operaciones. Se denomina
push (o empujar en la pila) a la inserción; y pop (o sacar de la pila) al descarte. No suele
implementarse la operación buscar, ya que en esta estructura la complejidad de esta operación
es O(n); en algunas aplicaciones se dispone de la operación leer el primer elemento del stack,
sin extraerlo.

En general la implementación de las operaciones generales de inserción y descarte usando


arreglos son costosas, en comparación con nodos enlazados vía punteros, debido a que es
necesario desplazar el resto de las componentes después de una inserción o descarte; además de
que el tamaño del arreglo debe ser declarado en el código, no pudiendo crecer dinámicamente
durante la ejecución. Sin embargo la primera dificultad no existe en un stack, la segunda se ve
atenuada ya que no se requiere almacenar punteros lo cual disminuye el tamaño del espacio de
almacenamiento; la única limitación es la declaración del tamaño del arreglo. Cuando es posible
predecir por adelantado la profundidad máxima del stack, se suele implementar mediante
arreglos.

5.5.2. Diagrama de un stack. Variables.

La representación gráfica siguiente, muestra el arreglo y dos variables para administrar el


espacio del stack. La variable stack es un puntero al inicio del arreglo.

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos

stack
Base del stack
0
1
Último ocupado
NumeroDeElementos 2
4 3
4
5
… Parte vacía del stack
MAXN-1

Figura 5.25. Variables en un stack

La variable NumeroDeElementos, contiene el número de elementos almacenados en el stack, el


cual en la gráfica crece hacia abajo. Usualmente suele representarse al revés, para mostrar que
es una estructura en que se van apilando las componentes; sólo se ve la primera componente, la
del tope. El uso de la variable NumeroDeElementos, facilita el diseño de las funciones que
prueban si el stack está lleno o vacío.

5.5.3. Archivo de encabezado ( *.h).

Si se desea utilizar en alguna implementación la estructura de datos stack, es una práctica usual
definir un archivo con extensión h (por header o encabezado), en el que se describen los
prototipos de las funciones asociadas al stack. Esto permite conocer las operaciones
implementadas y sus argumentos, acompañando a este archivo está el del mismo nombre, pero
con extensión .c, que contiene las definiciones de las operaciones; en éste, se suele incluir al
principio el archivo con extensión h, de tal modo que si existen funciones que invoquen a otras
del mismo paquete, no importe el orden en que son definidas, ya que se conocen los prototipos.

En el archivo siguiente, con extensión h, se ha empleado la compilación condicional, mediante


la detección de la definición de un identificador. En el caso que se analiza, si no está definido el
símbolo __STACK_H__ (note los underscores, para evitar alcances de nombres) se lo define y
se compila. En caso contrario, si ya está definido no se compila; esto permite compilar una sola
vez este archivo, a pesar de que se lo puede incluir en diferentes archivos que usen el stack.

En el texto se incluye un archivo datos.h que permite, usando la misma técnica, definir
focalizadamente los tipos de datos que emplee la aplicación que use la herramienta stack. En
este caso en particular debe definirse el tipo de datos ElementoStack, que describe la estructura
de una componente del arreglo.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 23
/*stack.h> */
#ifndef __STACK_H__
#define __STACK_H__

#include "datos.h"
#define push2(A, B) StackPush((B)); StackPush((A));
void StackInit(int);
int StackEmpty(void);
int StackFull(void);
void StackPush(ElementoStack);
ElementoStack StackPop(void);
void StackDestroy(void);

#endif /* __STACK_H__ */

El ejemplo también ilustra la definición de una macro: push2, que se implementa mediante el
reemplazo del macro por dos invocaciones a funciones del paquete. Note que los argumentos se
definen entre paréntesis.

5.5.4. Implementación de operaciones.

El diseño de las funciones contempla tres variables globales asociadas al stack. Tope y
NumeroDeElementos, que ya han sido definidas; además emplea la global MAXN, para
almacenar el máximo número de elementos, ya que el tamaño del stack, se solicita
dinámicamente, y no está restringido a ser una constante.

Las variables globales simplifican el paso de argumentos de las operaciones; sin embargo
restringen las operaciones a un solo stack. Si la aplicación empleara varios stacks diferentes, las
funciones tendrían que ser redefinidas.

/*stack.c Implementación basada en arreglos dinámicos. */


#include <stdlib.h>
#include <stdio.h>
#include "datos.h"
#include "stack.h"

static ElementoStack * stack; //puntero al inicio de la zona de la pila


static int NumeroDeElementos; //elementos almacenados en el stack
static int MAXN; //Máxima capacidad del stack

void StackInit(int max)


{stack = malloc(max*sizeof(ElementoStack) ); //se solicita el arreglo.
if (stack == NULL) exit(1);
NumeroDeElementos = 0; MAXN=max;
}

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
int StackEmpty(void)
{
return(NumeroDeElementos == 0) ; //Retorna verdadero si stack vacío
}

int StackFull(void)
{
return(NumeroDeElementos == MAXN) ; //Retorna verdadero si stack lleno
}

//se puede empujar algo al stack si no está lleno.


void StackPush(ElementoStack cursor)
{
if (!StackFull() ) stack[NumeroDeElementos ++]= cursor;
}

//se puede sacar algo del stack si no está vacío


ElementoStack StackPop(void)
{
if( StackEmpty() ) {printf("error. Extracción de stack vacio\n"); exit(1); return; }
else return ( stack[--NumeroDeElementos] ) ;
}

void StackDestroy(void)
{
free(stack);
}

Es buena práctica que las funciones StackInit y StackDestroy se invoquen en una misma
función, para asegurar la liberación del espacio.

Los programadores evitan la invocación de funciones innecesariamente, cuando las acciones de


éstas sean simples; esto debido al costo de la creación del frame, de la copia de valores de
argumentos y de la posterior destrucción del frame. En esta aplicación, podría haberse definido
como macros los test de stack vacío o lleno, según:

#define StackEmpty( ) (NumeroDeElementos == 0)


#define StackFull( ) (NumeroDeElementos == MAXN)

Ejemplo 5.3. Uso de stack. Balance de paréntesis.

a) Especificación del algoritmo:


Se dispone de un archivo de texto, que contiene expresiones que usan paréntesis. Se desea
verificar que los paréntesis están balanceados.
Es preciso identificar los pares que deben estar balanceados.
Ejemplo: “(“, “)”, “[“, “]”, “{“, “}”, etc.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 25
Se asume que se dispone de funciones para leer caracteres desde un archivo de texto, y para
discriminar si el carácter es uno de los símbolos que deben ser balanceados o no.
La secuencia siguiente no está balanceada: a+(b-c) * [(d+e])/f, al final están intercambiados dos
tipos de paréntesis.
b) Descripción inicial.
Crear el stack.
Mientras no se ha llegado al final del archivo de entrada:
Descartar símbolos que no necesiten ser balanceados.
Si es un paréntesis de apertura: empujar al stack.
Si es un paréntesis de cierre, efectuar un pop y comparar.
Si son de igual tipo continuar
Si son de diferente tipo: avisar el error.
Si se llega al fin de archivo, y el stack no esta vacío: avisar el error.
Destruir el stack.

El siguiente paso en el desarrollo es la descripción por seudo código, en la cual se establecen las
variables y el nombre de las funciones.

Ejemplo 5.4. Evaluación de expresiones en notación polaca inversa.

Las expresiones aritméticas que generalmente escribimos están en notación “in situ” o fija. En
esta notación los operadores se presentan entre dos operandos; por ejemplo: 2 + 3 * 4. Esta
notación no explica el orden de precedencia de los operadores; debido a esto los lenguajes de
programación tienen reglas de que establecen cuales operadores reciben primero sus operandos.
En el lenguaje C, la multiplicación tiene mayor precedencia que el operador suma; entonces, en
el caso del ejemplo, se realizará primero la multiplicación y luego la suma.
La relación entre operadores y operandos puede hacerse explícita mediante el uso de paréntesis.
La escritura de ( 2 + 3) *4 y 2 + (3 * 4) asocia operadores y operandos mediante paréntesis.

En C, además existen reglas de asociatividad para especificar los operandos de un operador, en


caso de que existan varios de igual precedencia, por ejemplo: 3*4*5.
Si la asociatividad es de izquierda a derecha: se interpreta: ((3 * 4) * 5); si es de derecha a
izquierda: (3* (4*5))

La notación inversa desarrollada por Jan Lukasiewicz (1878 - 1956) y empleada por los
ingenieros de Hewlett-Packard para simplificar el diseño electrónico de las primeras
calculadoras, permite escribir expresiones sin emplear paréntesis y definiendo prioridades para
los operadores. En esta notación el operador sigue a los operandos. La expresión infija 3 + 4
tiene su equivalente en notación inversa como: 3 4 +. Y el ejemplo inicial: 2 + 3 * 4, se
representa, en notación inversa, según: 2 3 4 * +.
Una generalización es agregar el nombre de funciones a los operadores. Normalmente las
funciones son operadores monádicos: sin[123 + 45 ln(27 - 6)]
a) Ejemplo de evaluación.
La expresión: (3 + 5) * (7 - 2) puede escribirse: 3 5 + 7 2 - *

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
Leyendo la expresión en notación inversa, de izquierda a derecha, se realizan las siguientes
operaciones:
Push 3 en el stack.
Push 5 en el stack. Éste contiene ahora (3, 5). El 5 está en el tope, el último en entrar.
Se aplica la operación + : la cual saca los dos números en el tope del stack, los suma y coloca el
resultado en el tope del stack. Ahora el stack contiene el número 8.
Push 7 en el stack.
Push 2 en el stack. Éste contiene ahora (8, 7, 2). El 2 está en el tope.
Se efectúa la operación – con los dos números ubicados en el tope.
Éste contiene ahora (8, 5)
Se efectúa la operación * con los dos números ubicados en el tope.
Éste contiene ahora (40)
La clave es entender que las operaciones se realizan sobre los dos primeros números
almacenados en el stack, y que se empujan los operandos.
b) Especificación.
Se dispone de un archivo de texto que contiene expresiones aritméticas en notación inversa.
Se dispone de funciones que permiten:
leer un número como una secuencia de dígitos;
reconocer los siguientes símbolos como operadores: +, -, * y /.
descartar separadores, que pueden ser los símbolos: espacio, tab, nueva línea.
reconocer el símbolo fin de archivo.
c) Seudo código.
While ( no se haya leído el símbolo fin de archivo EOF)
{ leer un símbolo;
Si es número: empujar el valor del símbolo en el stack
Si es un operador:
{ Efectuar dos pop en el stack;
Operar los números, de acuerdo al operador;
Empujar el resultado en el stack;
}
}

Retornar el contenido del tope del stack, mediante pop.

Ejemplo 5.5. Conversión de notación in situ a inversa.

Se emplea para convertir las expresiones infijas y evaluarlas en un stack. Para especificar el
algoritmo es preciso establecer las reglas de precedencia de operadores. La más alta prioridad
está asociada a los paréntesis, los cuales se tratan como símbolos; prioridad media tienen la
operaciones de multiplicación y división; la más baja la suma y resta.
Se asume solamente la presencia de paréntesis redondos en expresiones.
Como la notación polaca inversa no requiere de paréntesis, éstos no se sacarán hacia la salida.
Notar que el orden en que aparecen los números son iguales en ambas representaciones, sólo
difieren en el orden y el lugar en que aparecen los operadores.
Se empleará el stack para almacenar los operadores y el símbolo de apertura de paréntesis.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 27
Seudo código.
While ( no se haya leído el símbolo fin de archivo EOF)
{ leer un símbolo;
Si es número: enviar hacia la salida;
Si es el símbolo „)‟:
sacar del stack hacia la salida, hasta encontrar „(„, el cual no debe copiarse hacia la
salida.
Si es operador o el símbolo „(„:
Si la prioridad del recién leído es menor o igual que la prioridad del operado ubicado
en el tope del stack:
{ if( tope==‟(„ ) empujar el operador recién leído;
else
{ efectuar pop del operador y sacarlo hacia la salida hasta que la prioridad del
operador recién leído sea mayor que la prioridad del operador del tope.
Empujar el recién leído en el tope del stack.
}
}
}
Si se llega a fin de archivo: vaciar el stack, hacia la salida.

Se trata un stack con el símbolo „(„ en el tope como un stack vacío.

5.6. Cola. Buffer circular. Estructura FIFO (first-in, first-out).

5.6.1. Definición de estructura.

Una cola es una lista con restricciones. En ésta las inserciones ocurren en un extremo y los
descartes en el otro. La atención a los clientes en un banco, el pago de peaje en autopistas, son
ejemplos cotidianos de filas o colas de atención.
Si se conoce el máximo número de componentes que tendrán que esperar en la cola, se suele
implementar en base a arreglos.

Se requieren ahora dos variables para administrar los índices de la posición del elemento que
será insertado o encolado (cola, tail en inglés); y también el índice de la posición de la
componente que será descartada o desencolada en la parte frontal (cabeza. head).

- 1 2 3 4 - -

out in
cabeza cola
cabeza cola

Figura 5.26. Diagrama de una cola.

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos
El diagrama ilustra la situación luego: de la inserción de los elementos: 0, 1, 2, 3, y 4 y del
descarte del electo 0.
La cabeza (head) apunta al elemento a desencolar.
La cola (tail) apunta a la posición para encolar. Apunta a un elemento disponible.

Se observa que a medida que se consumen o desencolan componentes, van quedando espacios
disponibles en las primeras posiciones del arreglo. También a medida que se encolan elementos
va disminuyendo el espacio para agregar nuevos elementos, en la zona alta del arreglo. Una
mejor utilización del espacio se logra con un buffer circular, en el cual la posición siguiente a la
última del arreglo es la primera del arreglo.

5.6.2. Buffer circular.

Esto es sencillo de implementar aplicando aritmética modular, si el anillo tiene N posiciones, la


operación: cola = (cola+1) % N, mantiene el valor de la variable cola entre 0 y N-1. Operación
similar puede efectuarse para la variable cabeza cuando deba ser incrementada en uno.

La variable cola puede variar entre 0 y N-1. Si cola tiene valor N-1, al ser incrementada
en uno (módulo N), tomará valor cero.

cabeza
N-1 0
1
2

3
4
5

cola

Figura 5.27. Buffer circular.

Los números, del diagrama, muestran los valores del índice de cada casilla del arreglo circular.

La gráfica anterior ilustra la misma situación planteada con un arreglo lineal.

5.6.3. Cola vacía y llena.

El diagrama a la izquierda ilustra una cola vacía; la de la derecha una cola con un espacio
disponible. En esta última situación, el cursor cola (tail) dio la vuelta completa y está marcando
como posición disponible para encolar la posición anterior a la que tocaría consumir. Si se
encola un nuevo elemento, se producirá la condición de cola llena; pero esta situación es
indistinguible de la de cola vacía.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 29
cabeza

cola
N -1 N -1 0
0
1 1
2 2

3 3 cola

4 4
5 5 cabeza

Figura 5.28. Cola vacía y casi llena.

De esta forma no es posible distinguir entre las dos situaciones: cola llena o vacía.

Una de las múltiples soluciones a este problema, es registrar en una variable adicional la cuenta
de los elementos encolados; esto además facilita el diseño de las funciones que determinan cola
vacía o llena.
Si la variable la denominamos encolados. Entonces con cola vacía, encolados toma valor cero.
La cola llena se detecta cuando encolados toma valor N.

El algoritmo se basa en las funciones que operan sobre una cola circular basada en arreglos. Con
operaciones de colocar en la cola (put), sacar de la cola (get) y verificar si la cola está vacía o
llena.

5.6.4. Operaciones en colas.

/* QUEUE.c en base a arreglo circular dinámico */


#include <stdlib.h>
#include "QUEUE.h"

static Item *q; // Puntero al arreglo de Items


static int N, cabeza, cola, encolados; //Administran el anillo

Debe estar definido el tipo de datos Item.

void QUEUEinit(int maxN) //maxN es el valor N-1 de la Figura 5.27.


{ q = malloc((maxN+1)*sizeof(Item)); //Se pide espacio para N celdas.
N = maxN+1; cabeza = 0; cola = 0; encolados=0;
}

La detección de cola vacía se logra con:


int QUEUEempty()
{ return encolados == 0; }

Profesor Leopoldo Silva Bijit 26-05-2008


30 Estructuras de Datos y Algoritmos
Si la cola no está vacía se puede consumir un elemento:
Item QUEUEget()
{ Item consumido= q[cabeza];
cabeza = (cabeza + 1) % N ; encolados--;
return (consumido); }

Se emplea aritmética módulo N.

La detección de cola llena se logra con:


int QUEUEfull()
{return( encolados == N); }

Si la cola no está llena se puede encolar un elemento:


void QUEUEput(Item item)
{ q[cola] = item; cola = (cola +1) % N; encolados++;}

Para recuperar el espacio:

void QUEUEdestroy(void)
{ free ( q ); }

En un caso práctico las funciones cola llena y vacía se implementan con macros.
#define QUEUEempty() (encolados == 0)
#define QUEUEfull() (encolados == N)

Las dos aplicaciones, el stack de usuario y la cola, se emplearán en algoritmos para construir
árboles en grafos.
Ejemplo 5.6. Diseño de buffer circular estático de caracteres.
Para insensibilizarse de las diferentes velocidades que pueden tener un consumidor y un
productor de caracteres, se suele emplear un buffer.
En el caso de un computador alimentando a una impresora, la velocidad de producción de
caracteres del procesador es mucho mayor que la que tiene la impresora para liberar los
caracteres hacia el medio de impresión; el disponer de un buffer de impresora, permite al
procesador escribir en el buffer y no tener que esperar que la impresora escriba un carácter. Lo
mismo ocurre cuando un usuario escribe caracteres desde un teclado; su velocidad de digitación
es bastante menor que la velocidad con que el procesador utiliza los caracteres.

Se emplea la variable cnt para llevar la cuenta de los elementos almacenados en el buffer.
#define SIZE 16
#define LLENO (cnt==SIZE)
#define VACIO (cnt==0)

unsigned char Buffer[SIZE]; //buffer estático


int rd=0, wr=0, cnt=0; //administran el espacio

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 31
El cursor rd apunta al elemento a leer. El cursor wr al elemento que está disponible para ser
escrito.

La rutina put, coloca elementos en el buffer.

void put(unsigned char c)


{
Buffer[wr]=c;
wr=(wr+1)%SIZE; cnt++;
}

La rutina get consume elementos del buffer.


unsigned char get(void)
{ unsigned char ch;
ch=Buffer[rd];
rd=(rd+1)%SIZE; cnt--;
return(ch);
}
SIZE-1 0
cnt 1

2 2

rd
wr

Figura 5.29. Buffer de caracteres.

Las siguientes sentencias ilustran el uso de las funciones:

if ( !VACIO ) ch=get(); else printf("vacío\n");

while( !LLENO ) put('1'); //lo llena


if ( !LLENO ) put('2'); else printf("lleno\n");

while( !VACIO ) putchar(get()); //lo vacia


if ( !VACIO ) putchar(get()); else printf("\nvacio\n");

Usualmente una de las rutinas opera por interrupciones. La rutina que no es de interrupción debe
modificar la variable común cnt deshabilitando el tipo de interrupción.

Profesor Leopoldo Silva Bijit 26-05-2008


32 Estructuras de Datos y Algoritmos
Problemas resueltos.

P5.1
Se tienen los diagramas de una lista circular vacía, y luego de haber insertado uno, dos y tres
elementos. Notar que el puntero a la lista referencia el último nodo insertado en la estructura.

1 1 2 1 2 3

Figura P5.1. Buffer de caracteres.

Definir tipos de datos: nodo es el tipo de datos del nodo, y pnodo es el nombre del tipo puntero
a nodo. El valor almacenado en el nodo es de tipo entero. En cada caso ilustrar un ejemplo de
uso, mostrando las variables que sean necesarias, con diagramas que ilustren la relación entre
los datos.

a) Diseñar función insertar con prototipo: pnodo insertar(int);


El argumento es el valor que debe almacenarse en el nodo que se inserta.
Retorna puntero al recién insertado, nulo en caso que no se haya podido crear el nodo.
Asumir que se tiene variable global de nombre lista, de tipo pnodo.

b) Diseñar función sumar con prototipo: int sumar(pnodo);


El argumento es un puntero a un nodo cualquiera de la lista.
Retorna la suma de los valores almacenados en todos los nodos de la lista; 0 en caso de lista
vacía.

c) Asumir que se tienen varias listas circulares, cada una de ellas referenciadas por un puntero
almacenado en una variable de tipo pnodo. Se tiene la siguiente función, en la cual el argumento
sirve para referenciar a una de las listas.

pnodo funcion(pnodo *p)


{ pnodo t=*p;
if(*p==NULL) return (NULL);
*p = (*p)->proximo;
return (t);
}
Determinar que realiza la función.

Solución.

typedef struct moldenodo


{ int clave;
struct moldenodo *proximo;

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 33
} nodo, *pnodo;

a)
pnodo Insertar(int valor)
{ pnodo pn=NULL;
if ( (pn = (pnodo) malloc(sizeof(nodo))) == NULL) return NULL;
pn->clave = valor;
if (listac == NULL){pn->proximo = pn;}
else {pn->proximo = listac->proximo; listac->proximo = pn;}
listac = pn;
return (pn);
}

La siguiente definición, debe estar fuera de las funciones, y ubicada antes de la definición de la
función Insertar:
pnodo listac=NULL;

La sentencia siguiente forma la lista cuyo diagrama se muestra más a la izquierda, en la


definición del problema.

for(i=1; i<4; i++) if ( Insertar( i ) == NULL) break;

b)
int Sumar(pnodo p)
{ pnodo t = p;
int sum = 0;
if(p == NULL) {printf(" Lista vacía. "); return(0);}
sum += t->clave;
for(t = p->proximo; t != p; t = t->proximo)
sum += t->clave;
return (sum);
}

printf("La suma de los elementos de la lista circular es %d\n", Sumar(listac));


c)
La acción que realiza funcion(&Lista1), es apuntar al siguiente de la lista referenciada por la
variable Lista1.

Retorna puntero al que antes era el primero de la lista, nulo en caso de lista vacía.
En el caso de la lista con tres elementos, dada al inicio, después de invocar a la función, en esa
lista, debe retornar un puntero al nodo con valor 3, y la lista apunta al elemento con valor 1,
según se ilustra en el siguiente diagrama.

Profesor Leopoldo Silva Bijit 26-05-2008


34 Estructuras de Datos y Algoritmos

Lista1

1 2 3

Figura P5.2.

Si antes de invocar se tiene la situación dada al inicio, el siguiente segmento:

pnodo t=NULL;
if( (t=avanzar(&Lista1))!=NULL) printf("el anterior era %d\n", t->clave);

Imprime el valor 3.

Ejercicios propuestos.

E5.1. Verificar que para la siguiente entrada:

a+b*c+(d*e+f)*g
La salida, en notación polaca inversa, se genera en el siguiente orden:
abc
abc*+
abc*+
abc*+d
abc*+de
abc*+de*
abc*+de*f
a b c * + d e* f +
a b c * + d e* f +
a b c * + d e* f + g
a b c * + d e* f + g * +

Efectuar una traza del contenido del stack, a medida que se van procesando los símbolos de
entrada.

E5.2. Se tienen los siguientes tipos de datos:

typedef struct moldenodo


{ int clave;
struct moldenodo *proximo;
} nodo, *pnodo;

Para la estructura de la Figura E5.1:


a) Declarar las variables inicial y final.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 35
b) Diseñar función que inserte nodo, con un valor pasado como argumento, al inicio.
c) Diseñar función que inserte nodo, con valor pasado como argumento, al final.
d) Diseñar función que intercambie el nodo inicial con el nodo final.

Las funciones de inserción deben considerar la posibilidad de insertar en una cola vacía.

inicial

final

Figura E5.1. Cola.

E5.3. Búsqueda autoorganizada en listas.

El proceso de reorganizar una lista por transposición, tiene por objetivo mejorar el tiempo
promedio de acceso para futuras búsquedas, moviendo los nodos más accesados hacia el
comienzo de la lista.

Diseñar una rutina, en C, que busque un elemento en una lista en base a punteros. Y tal que
cuando encuentre un elemento lo trasponga con el anterior, excepto cuando lo encuentre en la
primera posición.

E5.4. Insertar en lista ordenada.

Comparar las dos funciones para insertar un nodo en una lista ordenada.

pnodo inserteenorden (pnodo p, int k )


{ pnodo p1, p2, p3;
for( p2 = NULL, p1 = p; p1 != NULL && p1->clave < k; p2 = p1, p1 = p1->proximo );

if (p1 != NULL && p1->clave == k) return p; //no acepta claves repetidas


p3= (pnodo) malloc (sizeof (nodo)) ;
if(p3!=NULL)
{
p3->clave = k;
if (p2 == NULL) { /* inserta al inicio */
p3->proximo = p1;
return p3 ;
}

Profesor Leopoldo Silva Bijit 26-05-2008


36 Estructuras de Datos y Algoritmos
p3->proximo = p2->proximo;
p2->proximo = p3;
}
return p ;
}

pnodo inserteenordenHeader( pnodo p, int k )


{ nodo header;
pnodo p1,p2;

header.proximo = p;

for(p2 = &header; p != NULL && p->clave< k; p2 = p, p = p->proximo);


if (p == NULL || p->clave !=k ){
p1 = (pnodo) malloc(sizeof(nodo));
if( p1!=NULL){
p1->clave = k;
p1->proximo = p;
p2->proximo = p1;
}
}
return header.proximo ;
}

Notar que se trata el encabezado como una variable local.

Referencias.

En el apéndice: Assemblers, Linkers, and the SPIM Simulator de James R. Larus, del libro de
Patterson A. David y Hennessy L. John, Computer Organization and Design: The
Hardware/software Interface, Morgan Kaufmann 2004, aparece una excelente descripción del
proceso de compilación, de la creación de archivos objetos, del proceso de ligado y carga de un
programa.

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 37
Índice general.

CAPÍTULO 5. ............................................................................................................................................ 1
CONJUNTOS DINÁMICOS. ................................................................................................................... 1
LISTAS, STACKS, COLAS. ..................................................................................................................... 1
5.1. NODOS. .............................................................................................................................................. 1
5.2. OPERACIONES. ................................................................................................................................... 1
5.2.1. Consultas:.................................................................................................................................. 1
5.2.2. Modificaciones. ......................................................................................................................... 1
5.3. LISTAS. .............................................................................................................................................. 2
5.3.1. Lista simplemente enlazada. ...................................................................................................... 2
5.3.1.1. Crea Nodo ........................................................................................................................................... 3
5.3.1.2. Operaciones de consultas en listas. ..................................................................................................... 4
a) Recorrer la lista. ...................................................................................................................................... 4
b) Buscar elemento. .................................................................................................................................... 5
c) Seleccionar un valor extremo. ................................................................................................................. 6
d) Buscar el último nodo. ............................................................................................................................ 7
5.3.1.3. Operaciones de modificación de listas. ............................................................................................... 7
a) Análisis de inserción. .............................................................................................................................. 7
b) Análisis de la operación descarte. ........................................................................................................... 9
c) Análisis adicionales en operación Insertar después............................................................................... 11
5.3.2. Listas doblemente enlazadas. .................................................................................................. 12
5.3.3. Lista circular. .......................................................................................................................... 14
5.3.4. Lista auto organizada. ............................................................................................................. 14
5.3.5. Lista ordenada. ........................................................................................................................ 15
5.3.6. Listas en base a cursores. ........................................................................................................ 15
5.4. EJEMPLOS DE OPERACIONES EN LISTAS SIN CENTINELA. .................................................................. 15
Ejemplo 5.1 Inserción de un nodo. .................................................................................................... 15
a) Insertar antes. ........................................................................................................................................ 15
b) Insertar después. ........................................................................................................................................ 17
c) Insertar al final. .......................................................................................................................................... 17
d) Insertar al inicio y al final. ......................................................................................................................... 18
e) Procedimiento de inserción. ....................................................................................................................... 18
f) Error común en pasos por referencia. ......................................................................................................... 19
Ejemplo 5.2. Descartar o Borrar nodo.............................................................................................. 20
5.5. STACK. PILA. ESTRUCTURA LIFO (LAST-IN, FIRST-OUT), ................................................................ 21
5.5.1. Definición. ............................................................................................................................... 21
5.5.2. Diagrama de un stack. Variables. ........................................................................................... 21
5.5.3. Archivo de encabezado ( *.h). ................................................................................................. 22
5.5.4. Implementación de operaciones. ............................................................................................. 23
Ejemplo 5.3. Uso de stack. Balance de paréntesis. ........................................................................... 24
a) Especificación del algoritmo: .................................................................................................................... 24
b) Descripción inicial. .................................................................................................................................... 25
Ejemplo 5.4. Evaluación de expresiones en notación polaca inversa. .............................................. 25
a) Ejemplo de evaluación. .............................................................................................................................. 25
b) Especificación. .......................................................................................................................................... 26
c) Seudo código. ............................................................................................................................................ 26

Profesor Leopoldo Silva Bijit 26-05-2008


38 Estructuras de Datos y Algoritmos
Ejemplo 5.5. Conversión de notación in situ a inversa. .....................................................................26
Seudo código.................................................................................................................................................. 27
5.6. COLA. BUFFER CIRCULAR. ESTRUCTURA FIFO (FIRST-IN, FIRST-OUT). ............................................27
5.6.1. Definición de estructura. ..........................................................................................................27
5.6.2. Buffer circular. .........................................................................................................................28
5.6.3. Cola vacía y llena. ...................................................................................................................28
5.6.4. Operaciones en colas. ..............................................................................................................29
Ejemplo 5.6. Diseño de buffer circular estático de caracteres. ....................................................................... 30
PROBLEMAS RESUELTOS. ........................................................................................................................32
EJERCICIOS PROPUESTOS. ........................................................................................................................34
E5.1. Verificar que para la siguiente entrada: ..................................................................................34
E5.2. Se tienen los siguientes tipos de datos: .....................................................................................34
E5.3. Búsqueda autoorganizada en listas. .........................................................................................35
E5.4. Insertar en lista ordenada. .......................................................................................................35
REFERENCIAS. .........................................................................................................................................36
ÍNDICE GENERAL. ....................................................................................................................................37
ÍNDICE DE FIGURAS. ................................................................................................................................39

Profesor Leopoldo Silva Bijit 26-05-2008


Conjuntos dinámicos. Listas, stacks, colas. 39

Índice de figuras.

FIGURA 5.1. LISTA VACÍA Y CON TRES NODOS. ............................................................................................. 3


FIGURA 5.2. LISTA CON ENCABEZADO VACÍA Y CON TRES NODOS................................................................. 3
FIGURA 5.3. ESPACIO ANTES DE SALIR DE CREANODO. ................................................................................ 4
FIGURA 5.4. CREACIÓN DE LISTA VACÍA SIN CENTINELA. .............................................................................. 4
FIGURA 5.5. CREACIÓN DE LISTA VACÍA CON ENCABEZADO. ........................................................................ 4
FIGURA 5.6. VARIABLES EN LARGOLISTA. ................................................................................................... 5
FIGURA 5.7. INSERCIÓN EN LISTAS. PRIMER ENLACE. ................................................................................... 8
FIGURA 5.8. INSERCIÓN EN LISTAS. SEGUNDO ENLACE. ................................................................................ 8
FIGURA 5.9. INSERTAR ANTES. ...................................................................................................................... 9
FIGURA 5.10. FIJACIÓN DE T. ...................................................................................................................... 10
FIGURA 5.11. MANTENCIÓN DE LISTA LIGADA. ........................................................................................... 10
FIGURA 5.12. ESPACIO DESPUÉS DE LIBERAR EL NODO. .............................................................................. 11
FIGURA 5.13. LISTA DOBLEMENTE ENLAZADA. ........................................................................................... 13
FIGURA 5.14. INSERCIÓN DE NODO EN LISTA DOBLEMENTE ENLAZADA. ..................................................... 13
FIGURA 5.15. LISTA DOBLEMENTE ENLAZADA CIRCULAR CON CENTINELA. ................................................ 14
FIGURA 5.16. LISTA SIMPLEMENTE ENLAZADA CIRCULAR. ......................................................................... 14
FIGURA 5.17. NUEVO NODO QUE SERÁ INSERTADO. .................................................................................... 15
FIGURA 5.18. ESCENARIOS PARA INSERCIÓN............................................................................................... 15
FIGURA 5.19. VARIABLES EN INSERTANODO. ............................................................................................. 16
FIGURA 5.20. INSERTA NODO CON VALOR 8 EN FIGURA 5.18. ..................................................................... 16
FIGURA 5.21. INSERCIÓN AL INICIO DE NODO CON VALOR 7 EN FIGURA 5.18. ............................................. 16
FIGURA 5.22. INSERCIÓN DEL NODO CON VALOR 4, DESPUÉS DEL NODO 2 EN FIGURA 5.18. ....................... 17
FIGURA 5.23. INSERCIONES AL INICIO Y AL FINAL. ...................................................................................... 18
FIGURA 5.23A. ESPACIO LUEGO DE INGRESAR A LA FUNCIÓN INSERTANODO_REF. ..................................... 19
FIGURA 5.24. TRES ESCENARIOS EN DESCARTE DE NODO. ........................................................................... 20
FIGURA 5.25. VARIABLES EN UN STACK...................................................................................................... 22
FIGURA 5.26. DIAGRAMA DE UNA COLA. .................................................................................................... 27
FIGURA 5.27. BUFFER CIRCULAR. ............................................................................................................... 28
FIGURA 5.28. COLA VACÍA Y CASI LLENA. .................................................................................................. 29
FIGURA 5.29. BUFFER DE CARACTERES....................................................................................................... 31
FIGURA P5.1. BUFFER DE CARACTERES. ..................................................................................................... 32
FIGURA P5.2. .............................................................................................................................................. 34
FIGURA E5.1. COLA. ................................................................................................................................... 35

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 6.

Árboles binarios de búsqueda.

En listas enlazadas de n componentes las operaciones generales de inserción, descarte y


búsqueda son O(n).
Como veremos, en árboles binarios de búsqueda (bst por binary search trees) con n nodos estas
operaciones serán O(log2 n) en promedio.

Un árbol presenta vínculos jerárquicos entre sus componentes y permite representar variadas
situaciones de interés. Por ejemplo: la estructura de un directorio, la conectividad entre vértices
de un grafo, representación de expresiones, el almacenamiento de las palabras reservadas,
diccionarios, etc.

6.1. Definiciones.

Un árbol es una colección de cero o más nodos vinculados con una relación de jerarquía.
Un árbol con cero nodos se denomina árbol vacío.

Un árbol tiene un nodo especial, o punto de entrada a la estructura, denominado raíz.


La raíz puede tener cero o más nodos accesibles desde ella. El conjunto de esos nodos forman
subárboles de la raíz, y son nodos descendientes de la raíz. La raíz es el ancestro de sus
descendientes.
El nodo raíz no tiene ancestros.

Un subárbol es un nodo con todos sus descendientes.


Un nodo sin descendientes es una hoja. Una hoja no tiene nodos hijos.

Un árbol es un: árbol vacío o un nodo simple o


un nodo que tiene árboles descendientes.
La definición es recursiva. Se define un árbol en términos de árboles.

Una trayectoria del nodo ni al nodo nk, es una secuencia de nodos desde ni hasta nk, tal que ni es
el padre de ni+1. Existe un solo enlace o vínculo entre un padre y sus hijos.
Largo de una trayectoria es el número de enlaces en la trayectoria. Una trayectoria de k nodos
tiene largo k-1.

Alto de un nodo: largo de la trayectoria más larga de ese nodo a una hoja.
Profundidad de un nodo: es el largo de la trayectoria de la raíz a ese nodo.

Profesor Leopoldo Silva Bijit 17-05-2009


2 Estructuras de Datos y Algoritmos
Ejemplos de definiciones.

árbol

raíz
1 6 0

4 8

5 9
hoja

Figura 6.1. Árboles.

El nodo con valor 3 es la raíz. Los nodos con valores: 1, 5 y 9 son hojas. Los nodos con valores:
4, 6 y 8 son nodos internos. El nodo con valor 6 es hijo de 3 y padre de 8. El nodo con valor 4
es ancestro del nodo con valor 5. {8, 9} y {4, 5} son subárboles. El nodo con valor 1 es subárbol
de la raíz. Los nodos con valores: 1, 6 y 0 son hermanos, por tener el mismo padre. El conjunto
de nodos con valores: {3, 6, 4, 5} es una trayectoria, de largo 3. Alto del nodo con valor 6 es 2.
La profundidad del nodo con valor 5 es 3.

Todos los nodos que están a igual profundidad están en el mismo nivel.
La profundidad del árbol es la profundidad de la hoja más profunda.

Se dice que un árbol es una estructura ordenada, ya que los hijos de un nodo se consideran
ordenados de izquierda a derecha. También es una estructura orientada, ya que hay un camino
único desde un nodo hacia sus descendientes.

6.2. Árbol binario.

Cada nodo puede tener: un hijo izquierdo, o un hijo derecho, o ambos o sin hijos. A lo más cada
nodo puede tener dos hijos.
Un árbol binario está formado por un nodo raíz y un subárbol izquierdo I y un subárbol derecho
D. Donde I y D son árboles binarios. Los subárboles se suelen representar gráficamente como
triángulos.
Árbol_ binario

I D

Figura 6.2. Árbol binario.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 3
Definición de tipos de datos.

Usualmente el árbol se trata como conjunto dinámico, mediante la creación de sus nodos bajo
demanda. Es decir, un nodo se crea con malloc, y contiene punteros a otros nodos de la
estructura.

En caso de un árbol binario, debe disponerse al menos de dos punteros. Se ilustra un ejemplo
con una clave entera, no se muestra espacio para la información periférica que puede estar
asociada al nodo. En la implementación de algunas operaciones conviene disponer de un
puntero al padre del nodo, que tampoco se declara en el molde del nodo.

typedef struct moldenode


{
int clave;
struct moldenode *left;
struct moldenode *right;
} nodo, * pnodo;

6.3. Árbol binario de búsqueda.

Para cada nodo de un árbol binario de búsqueda debe cumplirse la propiedad:

Las claves de los nodos del subárbol izquierdo deben ser menores que la clave de la raíz.
Las claves de los nodos del subárbol derecho deben ser mayores que la clave de la raíz
.
a a

I D
I D

Figura 6.3. Árbol binario de búsqueda.

Esta definición no acepta elementos con claves duplicadas.

Se indican en el diagrama de la Figura 6.3, el descendiente del subárbol izquierdo con mayor
clave y el descendiente del subárbol derecho con menor valor de clave; los cuales son el
antecesor y sucesor de la raíz.

El siguiente árbol no es binario de búsqueda, ya que el nodo con clave 2, ubicado en el subárbol
derecho de la raíz, tiene clave menor que ésta.

Profesor Leopoldo Silva Bijit 17-05-2009


4 Estructuras de Datos y Algoritmos

1 4

2 5

Figura 6.4. No es árbol binario de búsqueda.

Los siguientes son árboles de búsqueda ya que cumplen la propiedad anterior.


5 5
3 4
2
4
4
1 4 1 5
1 4 3
1
2 5 2
3 5 2

2
3 1

Figura 6.5. Varios árboles binarios de búsqueda con distinta forma.

La generación de estos árboles depende del orden en que se ingresen las claves en los nodos, a
partir de un árbol vacío. El de la izquierda se generó insertando las claves en orden de llegada:
2, 1, 4, 3, 5 (o bien: 2, 4, 1, 5, 3). El de más a la derecha, se generó con la llegada en el orden: 5,
4, 3, 2, 1.

Los dos árboles de más a la izquierda, en la Figura 6.5, se denominan balanceados, ya que las
diferencias en altura de los subárboles izquierdo y derecho, para todos los nodos, difieren a lo
más en uno. Los tres a la derecha están desbalanceados. El último tiene la estructura de una
lista, y es un árbol degenerado.

6.4. Cálculos de complejidad o altura en árboles.

6.4.1. Árbol completo.

Se denomina árbol completo, a aquél que tiene presentes todas las hojas en el menor nivel. La
raíz es de nivel cero, los hijos de la raíz están en nivel 1; y así sucesivamente.

Deduciremos, de manera inductiva la altura de las hojas en función del número de nodos.
El caso más simple de un árbol completo tiene tres nodos, un nivel y altura dos. Hemos
modificado levemente la definición de altura, como el número de nodos que deben ser revisados
desde la raíz a las hojas, ya que la complejidad de los algoritmos dependerá de esta variable.

Árbol de nivel 1.
Nodos = 3 = 22-1

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 5
Altura = 2

Figura 6.6. Árbol completo de nivel 1.

Árbol de nivel 2.
Nodos = 7 = 23-1
Altura = 3

Figura 6.7. Árbol completo de nivel 2.

Árbol de nivel 3.
Nodos = 15 = 24-1
Altura = 4

Figura 6.8. Árbol completo de nivel 3.

Se deduce para un árbol de m niveles:

Árbol de nivel m.
Nodos = n = 2A-1
Altura = A = m+1
Hojas = 2m
Nodos internos = n – Hojas

De la expresión para el número de nodos, puede despejarse A, se logra:

A = log2(n+1) = O(log n)

Resultado que es simple de interpretar, ya que cada vez que se sigue una trayectoria por un
determinado subárbol, se descarta la mitad de los nodos. Es decir, se rige por la relación de
recurrencia: T(n) = T(n/2) +c, con solución logarítmica. Esta propiedad le otorga, a la
estructura de árbol binario de búsqueda, grandes ventajas para implementar conjuntos
dinámicos, en relación a las listas, que permiten elaborar algoritmos de complejidad O(n).
La demostración por inducción matemática completa, del resultado anterior, es sencilla de
deducir a partir del caso m-avo.

Profesor Leopoldo Silva Bijit 17-05-2009


6 Estructuras de Datos y Algoritmos
Puede demostrarse por inducción completa, el siguiente teorema:
Teorema: Un árbol perfectamente balanceado que tiene n nodos internos tiene (n+1) hojas. El
que se demostrará en 6.4.4.

También se denominan árboles perfectamente balanceados, en éstos todas las hojas tienen igual
profundidad.

6.4.2 Árboles incompletos con un nivel de desbalance.

Se ilustran los tres casos de árboles, de nivel dos, con un nivel de desbalance, para n= 4, 5 y 6.
Un árbol con 3 nodos es completo en caso de aceptarse sólo un nivel de desbalance. Lo mismo
puede decirse de un árbol con 7 nodos y que tenga un nivel de desbalance.

Árboles de nivel 2.
Nodos de 4 a 6. De 23-1 hasta 23- 2.
Altura = 3 en peor caso.

Figura 6.9. Árboles incompletos de nivel 2.

Árboles de nivel 3.
Nodos de 8 a 14. De 24-1 hasta 24-2
Altura = 4 en peor caso.

Árboles de m niveles.
Nodos de 2A-1 hasta 2A-2.
Altura A.

La inecuación:
2 A−1 ≤ n ≤ 2 A − 2 tiene como solución: A <= (1 + log2(n) ) para la primera desigualdad y
A>= log2(n+2) para la segunda.

Se pueden encontrar constantes que acoten, por arriba y por abajo a ambas funciones:

1*log2n <=log2(n+2) <= A <= 1+ log2n <= 1.3* log2n para n>10,079.

> solve(1+(ln(n)/ln(2))<1.3*ln(n)/ln(2));
RealRange ( Open ( 10.07936840 ), ∞ )

Entonces, se tiene:
A = Θ(log n)

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 7
Veremos más adelante la estructura denominada heap, la cual puede interpretarse como un árbol
perfectamente balanceado excepto que en el mayor nivel no está completo. Si no está completo,
se van agregando nodos de izquierda a derecha.

El peor caso para la altura, se tiene con un árbol degenerado en una lista, en el cual la altura
resulta O(n).

6.4.3. Árboles construidos en forma aleatoria.

Para n nodos, con claves: 1, 2, 3, …, n, se pueden construir n! árboles. Ya que existen n!


permutaciones de n elementos. El orden de los elementos de la permutación, es el orden en que
se ingresan las claves a partir de un árbol vacío.

Lo que se desea conocer es la altura An, definida como la altura promedio de las búsquedas de
las n claves y promediadas sobre los n! árboles que se generan a partir de las n! permutaciones
que se pueden generar con la n claves diferentes.

Si el orden de llegada de las claves que se insertan al árbol se genera en forma aleatoria, la
probabilidad de que la primera clave, que es la raíz, tenga valor i es 1/n. Esto en caso de que
todas las claves sean igualmente probables.

Se ilustra esa situación en el siguiente diagrama.

i-1 n-i

Figura 6.10. Raíz con valor i.

Se considera que el subárbol izquierdo contiene (i-1) nodos; por lo tanto el subárbol derecho
contiene (n-i) nodos. Esto debido a la propiedad de árbol de búsqueda, en el subárbol izquierdo
debe tenerse los (i-1) valores menores que la clave i de la raíz.

Denominamos An −i al largo promedio de las trayectorias en el subárbol derecho, y Ai −1 al largo


promedio de las trayectorias del subárbol que contiene (i-1) nodos. Esto asume que el resto de
las permutaciones de las (n-1) claves restantes son igualmente probables.

Es decir:
Los i-1 nodos del subárbol izquierdo tienen largo de trayectorias promedio igual a Ai −1 +1.
Los n-i nodos del subárbol derecho tienen largo de trayectorias promedio igual a An −i +1.
La raíz tiene un largo de trayectoria igual a 1.

Profesor Leopoldo Silva Bijit 17-05-2009


8 Estructuras de Datos y Algoritmos
El promedio ponderado, de los largos de trayectoria para buscar los n nodos, para el árbol con
clave i en la raíz es:

(i − 1)( Ai −1 + 1) + 1 + (n − i )( An −i + 1)
An (i ) =
n

Y promediando considerando que el nodo i, en la raíz, puede ser: 1, 2, 3, …, n


1 i=n
An = ∑ An (i)
n i =1

Reemplazando la expresión para An (i ) se tiene:

1 i=n i − 1 1 n−i
An = ∑ (
n i =1 n
( Ai −1 + 1) + 1 +
n n
( An −i + 1)

efectuando factorizaciones:
i =n
1
An =
n2
∑ ((i − 1) A
i =1
i −1 + n + (n − i ) An −i )

Sumando el término que no depende de i, resulta:


i =n
1
An = 1 +
n2
∑ ((i − 1) A
i =1
i −1 + (n − i ) An −i )

El último paso considera que n es una constante, ya que el índice de la suma es i.

En la última sumatoria, los dos factores a sumar, dan origen a iguales sumandos. En ambas
sumatorias se obtiene: 0 ⋅ A0 + 1 ⋅ A1 + ...(n − 1) ⋅ Ai −1

Resulta entonces:
i=n
2
An = 1 +
n2
∑ (i − 1) A
i =1
i −1

Efectuando un cambio de variables, se obtiene:


2 i = n −1
An = 1 + ∑ iAi
n 2 i =1

Se tiene una relación de recurrencia de orden (n-1). Se requieren conocer (n-1) términos para
calcular el n-avo.

La cual puede ser transformada en una relación de recurrencia de primer orden, por el siguiente
procedimiento:

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 9
a) Se reemplaza n por (n-1), en la relación anterior, con lo que se obtiene:

2 i =n−2
An −1 = 1 + ∑ iAi
(n − 1) 2 i =1

b) Se extrae el término (n-1) de la sumatoria, de la relación de recurrencia de orden (n-1), con lo


que se obtiene:

i =n−2
2 2
An = 1 +
n 2
(n − 1) An −1 + 2
n
∑ iA
i =1
i

Si se despeja la sumatoria de la relación obtenida en a) y se reemplaza en la sumatoria de la


relación obtenida en b), quedará una relación de recurrencia de primer orden; es decir, An en
términos de An −1 .
2 2 ( An −1 − 1)(n − 1) 2
An = 1 + ( n − 1) An −1 + ( )
n2 n2 2
Arreglando:
1
An = ((n 2 − 1) An −1 + 2n − 1)
n2

Empleando maple, la solución de esta recurrencia, para n>1 es:

2 ( n + 1 ) Ψ( n + 1 ) + 2 γ − 3 n + 2 γ n
An =
n
Donde Ψ(n) es la función digama.

La relación también puede resolverse mediante la función armónica.


1 1 1
H n = 1 + + + .... +
2 3 n
Dando como resultado:
n +1
An = 2 Hn − 3
n
El procedimiento no es trivial. Se da el resultado sin desarrollo.

Para verificar que es solución puede reemplazarse ésta en la ecuación de recurrencia, con lo que
se obtiene:
n +1 1 n
2 H n − 3 = 2 ((n 2 − 1)(2 H n −1 − 3) + 2n − 1)
n n n −1

La cual es una identidad, ya que se obtiene, luego de un trabajo algebraico:

Profesor Leopoldo Silva Bijit 17-05-2009


10 Estructuras de Datos y Algoritmos
1
H n − H n −1 =
n

Retomando el cálculo, la función armónica puede ser aproximada por la función gama
(constante de Euler, γ=0,577….), mediante:

1
H n = γ + ln(n) + + ....
12n 2
Si n es grande:
H n ≈ ln(n)
An ≈ 2 H n − 3

Lo cual permite obtener, finalmente:


An ≈ 2 ln(n) = Θ(log n)

Resultado que garantiza que en promedio, el largo promedio de cualquier trayectoria en un


árbol generado aleatoriamente es de complejidad logarítmica.

1,4 log2(n)

Generado aleatoriamente

balanceado

Figura 6.11. Altura de árbol generado aleatoriamente.

La gráfica muestra que para árboles de tipo 1000 nodos, deben recorrerse cerca de nueve nodos
desde la raíz hasta las hojas (peor caso), si está balanceado. El largo promedio de los recorridos
es 12 en un árbol generado aleatoriamente, y 1000 en peor caso.

Otra forma de expresar el resultado, es considerando el alargue de la trayectoria en términos del


largo para un árbol perfectamente balanceado. Éste era A = log2(n+1), entonces para n grande:

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 11
An 2 ln(n)
≈ ≈ 2 ln(2) ≈ 1,386
A log 2 (n)

El resultado establece que el promedio de alargue es de un 39%. Pero esta cota es para el caso
promedio, además si n es relativamente grande el alargue es menor. Se muestra una gráfica del
alargue, en función de n. Para árboles con menos de 2000 nodos el alargue es menor que un
22%.

Figura 6.12. Alargue de altura de árbol generado aleatoriamente.

Si dada la naturaleza de la aplicación se desea tener un peor caso con complejidad logarítmica,
debemos garantizar que el árbol se mantendrá lo más balanceado posible. Existen una serie de
algoritmos que logran este objetivo: Árboles: coloreados, 2-3, AVL.

6.4.4. Número de comparaciones promedio en un árbol binario de búsqueda.

6.4.4.1. Árbol binario externo


Se consideran las hojas como nodos externos.

2 5

1 3 6

Figura 6.13. Nodos internos y externos.

Teorema
Para un árbol binario de búsqueda de n nodos internos el correspondiente árbol binario externo
tiene: (2n + 1) nodos. Hay (n+1) nodos externos u hojas.

Profesor Leopoldo Silva Bijit 17-05-2009


12 Estructuras de Datos y Algoritmos

Demostración.
Por inducción completa, cuyo método resumimos a continuación:
Se tiene un conjunto y una propiedad que puede expresarse mediante una fórmula P(n), se
verifica:
1° Que la propiedad se cumpla para el primer elemento o los primeros elementos del conjunto.
2° Que dado P(n) se cumpla que: P(n) ⇒ P(n+1).
La conclusión es: Todos los elementos del conjunto cumplen esa propiedad. Esta forma de
razonamiento también se denomina por recurrencia.

Sea ni el número de nodos internos, y ne el número de nodos externos.

Para ni = 1 se tiene: ne = 2 y se cumple que: ne = ni + 1

Figura 6.14. P(1): ne = ni + 1

Para ni = 2 se tiene: ne = 3 y se cumple que: ne = ni + 1

2 1

1 2

Figura 6.15. P(2): ne = ni + 1

En este caso se pueden formar dos árboles. Si a partir de un árbol vacío, llegan las claves en
orden 2,1 ó 1, 2, se tienen los diagramas anteriores.

Supongamos que para ni = n se tiene que: ne = n+1 entonces:

Para ni = n+1 podemos agregar el nodo interno de dos formas: Una reemplazando a un nodo
externo, o bien intercalándolo entre dos nodos internos.

En el primer caso, disminuye en uno el número de nodos externos, pero luego se agregan dos
nodos externos, se tendrá:
ne’ = (n+1) -1 + 2 = (n+1) + 1 = ni + 1 y se cumple la propiedad.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 13

a a

Figura 6.16. Primer caso de P(n+1): ne = ni + 1

En el segundo caso:
ne’’ = (n+1) + 1 = ni + 1 también se cumple la propiedad.
a
a
b
c
c

Figura 6.17. Segundo caso de P(n): ne = ni + 1

6.4.4.2. Largos de trayectorias interna y externa.


Teorema.
Sea el largo de la trayectoria interna I(n), la suma de las alturas de todos los nodos internos;
y sea el largo de la trayectoria externa E(n), la suma de las alturas de todos los nodos
externos.
Entonces: E(n) = I(n) + (2 n + 1)

Demostración por inducción.


La propiedad P(n), se cumple para el caso base, con n = 1.

a=1 1

a=2

Figura 6.18. P(1): E(n) = I(n) + (2 n + 1)

I(1) = 1, E(1) = 2 + 2 = 4
E(1) = I(1) + (2*1 + 1) = 4

Para n=2, en ambos diagramas:

Profesor Leopoldo Silva Bijit 17-05-2009


14 Estructuras de Datos y Algoritmos

2 1

1 2

Figura 6.19. P(2): E(n) = I(n) + (2 n + 1)

I(2) = 1 + 2 = 3
E(2) = 3 + 3 + 2 = 8
E(2) = I(2) + (2*2 +1) = 8 Entonces P(2) se cumple.

Nótese que I(n) es el número de comparaciones de claves que deben realizarse para encontrar
todos los nodos. También se tiene que E(n) –(n+1) es el número de comparaciones que deben
realizarse para encontrar las hojas o nodos externos; se cuentan sólo las comparaciones de
claves.

La hipótesis inductiva, está basada en asumir P(n) verdadero y demostrar que:


P(n) ⇒P(n+1)

Se cumple E(n) = I(n) + (2n+1)

Se reemplaza cualquiera de los nodos externos, digamos uno ubicado a altura d de la raíz, por
un nodo interno y sus dos hijos externos:

a a

d d b
n
d+1
n+1

Figura 6.20. Primer caso de P(n): E(n) = I(n) + (2 n + 1)

Los nuevos largos, para el árbol con (n+1) nodos internos quedan:
I(n+1) = I(n) + d ya que el nodo b tiene altura d respecto de la raíz.
E(n+1)’ = E(n) – d + 2(d+1) ya que pierde un nodo con altura d, pero adquiere dos a distancia
(d+1).

Entonces reemplazando P(n) en el largo externo queda:


E(n+1)’ = I(n) + (2n+1) – d + 2(d+1) = I(n) + d + 2n +1 + 2
Y empleando la relación para I(n+1), resulta:
E(n+1)’ = I(n+1) + 2(n+1) + 1 que demuestra que P(n+1) se cumple.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 15
Falta analizar la situación en la que se intercala un nodo a altura d de la raíz, entre dos nodos
internos.
a
a
b
c
c

Figura 6.21. Segundo caso de P(n): E(n) = I(n) + (2 n + 1)

Los nuevos largos en función de la situación para n nodos internos, se pueden plantear:
I(n+1) = I(n) + d + k. Con k igual al número de nodos internos bajo el insertado.
E(n+1)’’ = E(n) + (d+1) + j. Con j igual al número de nodos externos bajo el insertado.

Se tiene que j = k+1, ya que para un árbol de k nodos internos se tienen (k+1) nodos externos.
Reemplazando j y el valor de E(n) mediante la propiedad P(n) se tiene:
E(n+1)’’ = (I(n) + 2n+1) + (d+1) + k+1 = I(n) +d +k + 2n+3.

Reemplazando el valor de I(n+1) se obtiene:


E(n+1)’’ = I(n+1) + 2n+3 = I(n+1) + 2(n+1) +1 que muestra que P(n+1) se cumple.
6.4.4.3. Búsquedas exitosas y no exitosas.
Teorema
• S(n) ≤ 1.39 log2 (n) ⇒ S(n) = Θ(log2 n)
• U(n) ≤ 1.39 log2 (n + 1) ⇒ U(n) = Θ(log2 n)

Sea S(n) el número de comparaciones en una búsqueda exitosa en un árbol binario de búsqueda
de n nodos internos, construido aleatoriamente.
Para un árbol dado el número esperado de comparaciones para encontrar todas las claves es:

S(n) = I(n)/n
S(1) = 1
S(2) = 3/2
I(1) = 1, I(2) = 3

Sea U(n) el número de comparaciones en una búsqueda no exitosa en un árbol binario de


búsqueda de n nodos internos, construido aleatoriamente.
Para un árbol dado, U(n) es el número esperado de comparaciones para encontrar todas las hojas
es:
E (n) − (n + 1)
U ( n) =
n +1

Una búsqueda no exitosa termina en uno de los (n+1) nodos externos.


U(1) = 2

Profesor Leopoldo Silva Bijit 17-05-2009


16 Estructuras de Datos y Algoritmos
U(2) = 5/3
E(1) = 4, E(2) =8

U(0) = 0 ya que E(0) =1 y n=0

a=1

Figura 6.22. Evaluación de U(0).

Relación entre S(n) y U(n).


Puede expresarse el número promedio de comparaciones en una búsqueda exitosa S, en términos
del número de comparaciones no exitosas U, eliminando las variables E e I, empleando la
relación entre éstas, según:

I (n) E (n) − (2n + 1) (n + 1)U (n) + (n + 1) − (2n + 1)


S ( n) = = =
n n n

Resulta:
1
S (n) = (1 + )U (n) − 1
n

La inserción de un nuevo valor de clave en un árbol de búsqueda implica un recorrido


descendente, desde la raíz hasta una hoja, es decir una búsqueda no exitosa. Esto debido a que
no se aceptan claves repetidas.

Entonces: Se requiere una comparación adicional para encontrar una clave en una búsqueda
exitosa que lo que se requiere para insertar la clave en una búsqueda no exitosa que precede a su
inserción.

Si la búsqueda encuentra al nodo en la raíz se requieren U(0) +1 comparaciones, ya que fue el


primero que se insertó.
Si el buscado fue el k-ésimo insertado, se requieren U(k-1) +1 comparaciones para encontrarlo.

Como el nodo buscado puede haber sido insertado en un árbol vacío o en un árbol que ya
tuviera 1, 2, .. o (n-1) nodos se tiene, el siguiente promedio para las comparaciones:

(U (0) + 1) + (U (1) + 1) + ...(U (n − 1) + 1)


S ( n) =
n
1 k =n
S (n) = ∑ (U (k − 1) + 1)
n k =1
1 k =n 1 k = n −1
S (n) = 1 + ∑ U (k − 1) = 1 + ∑ U (k )
n k =1 n k =0

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 17
Relación de recurrencia para U(n).
Igualando las dos expresiones para S(n), se obtiene:

1 1 k = n −1
(1 + )U (n) − 1 = 1 + ∑ U (k )
n n k =0

Despejando:
k = n −1
(n + 1)U (n) = 2n + ∑ U (k )
k =0

Si en la anterior se reemplaza, n por n-1, se obtiene:

k =n−2
nU (n − 1) = 2(n − 1) + ∑ U (k )
k =0

Restando la segunda a la primera, se obtiene:

(n + 1)U (n) − nU (n − 1) = 2 + U (n − 1)

Despejando U(n), se logra, la relación de recurrencia de primer orden, con U(0) =0:

2
U (n) = U (n − 1) +
n +1

Que puede resolverse, mediante el siguiente método:


Remplazando n por n-1, n-2, …2, 1 se tienen:
2
U (n − 1) = U (n − 2) +
n
2
U (n − 2) = U (n − 3) +
n −1
…..
U (1) = U (0) + 1

Las que reemplazadas en la expresión para U(n) permiten expresar:

2 2 2 2 2 2
U (n) = U (0) + + + ... + + + +
2 3 n − 2 n −1 n n +1

Sumando y restando uno y factorizando por dos, se tiene:


1 1 1 1 1
U (n) = U (0) + 2(−1 + 1 + + + ... + + + )
2 3 n −1 n n +1

Reemplazando mediante H(n), la función armónica, cuya suma es conocida:

Profesor Leopoldo Silva Bijit 17-05-2009


18 Estructuras de Datos y Algoritmos
1 1 1 1
H (n) = 1 + + + ... + +
2 3 n −1 n

U (n) = U (0) + 2( H (n + 1) − 1)
Finalmente, con U(0)=0:
U (n) = 2( H (n + 1) − 1)

La que puede aproximarse por:


U (n) ≈ 2 ln(n + 1)
Relación de recurrencia para S(n).

Reemplazando U(n) en la expresión para S(n):


1
S (n) = (1 + )2( H (n + 1) − 1) − 1
n
1 1
S (n) = (1 + )2( H (n) + − 1) − 1
n n +1
Resultando:
1
S (n) = 2(1 + ) H (n) − 3
n

Aproximando:
S ( n) ≈ 2 H ( n)
S (n) ≈ 2 ln(n) = 2 ln(2) log 2 (n) = 1,39 log 2 (n) = Θ(log 2 (n))
Finalmente:
S (n) = Θ(log 2 (n))

1,3log2(n)

S(n)

log2(n+2)

Figura 6.23. S(n) es Θ(log2(n)).

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 19
Teorema
Después de insertar n claves en un árbol binario de búsqueda T, inicialmente vacío, la altura
promedio de T es Θ(log2 n).

6.5. Recorridos en árboles.

Se denominan recorridos a la forma en que son visitados todos los nodos de un árbol.
Existen tres modos de recorrido, con las siguientes definiciones recursivas.
6.5.1. En orden:
Se visita el subárbol izquierdo en orden;
Se visita la raíz;
Se visita el subárbol derecho en orden;
6.5.2. Pre orden:
Se visita la raíz;
Se visita el subárbol izquierdo en preorden;
Se visita el subárbol derecho en preorden;
6.5.3. Post orden:
Se visita el subárbol izquierdo en postorden;
Se visita el subárbol derecho en postorden;
Se visita la raíz;
6.5.4. Ejemplo de recorridos.
Recorrer el árbol formado con el siguiente conjunto de claves: { n0, n1, n2, n3, n4, n5}, y cuyo
diagrama se ilustra en la figura 6.24.

En orden:
{n1, n3, n4}, n0, {n2, n5}
n3, n1, n4, n0, {n2, n5}
n3, n1, n4, n0, n2, n5

Pre orden
n0, {n1, n3, n4}, {n2, n5}
n0, n1, n3, n4, {n2, n5}
n0, n1, n3, n4, n2, n5

Post orden
{n1, n3, n4}, {n2, n5}, n0
n3, n4, n1, {n2, n5},n0
n3, n4, n1, n5, n2, n0

Profesor Leopoldo Silva Bijit 17-05-2009


20 Estructuras de Datos y Algoritmos
n0

n1 n2

n3 n5
n4

Figura 6.24. Árbol con claves {n0, n1, n2, n3, n4, n5}.

6.5.5. Árboles de expresiones.


Un árbol puede emplearse para representar la vinculación entre operadores y operandos.

Notación in situ, corresponde a recorrido en orden: ( a * b) / (c + d)


/

* +

a b c d

Figura 6.25. Árbol que representa a: ( a * b) / (c + d)

Notación polaca inversa, corresponde a recorrido en post orden: a b * c d + /

Tarea: Encontrar expresión in situ para la polaca inversa: a b c / + d e f * - *


6.5.6. Árboles de derivación.
Los compiladores emplean árboles de derivación para verificar la construcción de sentencias
sintácticamente correctas.

sentencia

if (Condición) sentencia else sentencia

if (Condición) sentencia

Figura 6.26. Árbol de derivación.

6.6. Operaciones en árboles binarios.

Al estudiar las operaciones más frecuentes en árboles y al analizar sus complejidades se podrá
notar el poder de esta estructura de datos para implementar los conceptos de buscar y

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 21
seleccionar. Los árboles son necesarios para dar soporte a esas operaciones básicas de conjuntos
dinámicos.

6.6.1. Operaciones básicas

6.6.1.1. Crear árbol vacío.


La variable arbol debe ser un puntero en zona estática o en el stack. A través de este puntero se
tiene acceso a las componentes.

pnodo arbol=NULL;
6.6.1.2. Crea nodo inicializado con un valor de clave.
pnodo CreaNodo(int valor)
{ pnodo pi=NULL;

if ( (pi= (pnodo) malloc(sizeof(nodo))) ==NULL) exit(1);


else
{
pi->clave=valor; pi->left=NULL; pi->right=NULL;
}
return(pi);
}
6.6.1.3. Ejemplo de uso.
arbol = CreaNodo(5); //si el árbol estaba vacío, crea raíz del árbol con clave igual a 5.

6.6.2. Operaciones de recorrido

6.6.2.1. Mostrar en orden


Diferentes árboles de búsqueda que almacenen las mismas claves son mostrados en el mismo
orden al efectuar este recorrido.

Un árbol de búsqueda preserva el ordenamiento de sus componentes independiente de su


forma.

Una técnica de importancia para efectuar diseños recursivos consiste:


a) En conceptualizar lo que realiza la función y asumir que ésta realiza su objetivo.
b) En establecer las condiciones de término de las reinvocaciones.

En el diseño de mostrar en orden, es simple establecer que el término se logra cuando no se


encuentran los hijos de las hojas.

Profesor Leopoldo Silva Bijit 17-05-2009


22 Estructuras de Datos y Algoritmos
void RecorraEnOrden(pnodo p)
{
if (p!= NULL) //si llegó a las hojas o es un árbol vacío.
{
RecorraEnOrden(p->left); //primero recorre el subárbol izquierdo.
printf ("%d \n", p->clave); //terminado lo anterior, imprime el nodo apuntado por p
RecorraEnOrden(p->right);
}
}

La complejidad de un recorrido que debe visitar n nodos puede intuirse que será Θ(n).

Si se tiene un árbol de n nodos, y si se asume arbitrariamente que el subárbol izquierdo tiene k


nodos, se puede plantear que la complejidad temporal del recorrido es:
T(n) = T(k) + Θ(1) + T(n-k-1)
Considerando de costo constante la impresión, y la evaluación del condicional.

Para simplificar el cálculo podemos asumir un árbol balanceado.


T(n) = T(n/2)+ Θ(1)+ T(n/2 -1)

Y para grandes valores de n, podemos simplificar aún más:


T(n) = 2*T(n/2) que tiene por solución: T(n) = n = Θ(n)

Otro cálculo es considerar el peor caso para el subárbol derecho:


T(n) = T(1) + Θ(1) + T(n-2)
La que se puede estudiar como
T(n) = T(n-2) +2 con T(1) =1, T(2) =1 que tiene por solución
T(n) = n –(1/2)(1+(-1)n). El segundo término toma valor cero para n par, y menos uno para n
impar. Puede despreciarse para grandes valores de n, resultando: T(n) = Θ(n)

Si se desea mostrar el nivel, de cada nodo, basta una pequeña modificación, agregando un
argumento nivel; cada vez que se desciende un nivel se incrementa éste en uno. Lo cual es un
ejemplo de las posibilidades que tienen los algoritmos recursivos.

void inorder(pnodo t, int nivel)


{
if (t != NULL) {
inorder(t->left, nivel+1);
printf ("%d %d \n", t->clave, nivel);
inorder(t->right, nivel +1);
}
}

Ejemplo de uso:
inorder(arbol, 0); //Imprime considerando la raíz de nivel cero.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 23
6.6.2.2. Mostrar en post-orden
void prtpostorder(pnodo p)
{
if (p!= NULL)
{
prtpostorder(p->left);
prtpostorder(p->right);
printf ("%d \n", p->clave);
}
}
6.6.2.3. Mostrar en pre-orden
void prtpreorder(pnodo p)
{
if (p!= NULL)
{
printf ("%d \n", p->clave);
prtpreorder(p->left);
prtpreorder(p->right);
}
}

6.6.3. Operaciones de consulta.

Se suele pasar como argumento la raíz del árbol.


6.6.3.1. Seleccionar el nodo con valor mínimo de clave.
Considerando la propiedad de orden del árbol de búsqueda, debe descenderse a partir de la raíz
por el subárbol izquierdo hasta encontrar un nodo con hijo izquierdo nulo, el cual contiene el
valor mínimo de clave. Debe considerarse que el árbol puede estar vacío.

La implementación iterativa de esta operación es sencilla de implementar. Se retorna puntero al


nodo con valor mínimo de clave, y NULL si el árbol está vacío.

pnodo BuscarMinimoIterativo(pnodo t) {
while ( t != NULL){
if ( t->left == NULL ) return (t); //apunta al mínimo.
else t=t->left; //desciende
}
return (t); /* NULL si árbol vacío*/
}

Profesor Leopoldo Silva Bijit 17-05-2009


24 Estructuras de Datos y Algoritmos

t
t->left

Figura 6.27. Variables en BuscarMinimoIterativo.

/* Algoritmo recursivo. Descender siempre por la izquierda */


pnodo BuscaMinimo(pnodo t) {
if (t == NULL) return(NULL); //si árbol vacío retorna NULL
else
// Si no es vacío
if (t->left == NULL) return(t ); // Si no tiene hijo izquierdo: lo encontró.
else return( BuscaMinimo (t->left) ); //busca en subárbol izquierdo.
}

Otra forma de concebir la función, es plantear primero las condiciones de término, y luego
seguir tratando de hacer lo que la función realiza, pero acercándose a la solución.

pnodo BuscaMinimo(pnodo t)
{
if (t == NULL) return(NULL); //si árbol vacío retorna NULL
if (t->left == NULL) return(t ); // Si no tiene hijo izquierdo: lo encontró.
return( BuscaMinimo (t->left) ); //busca en subárbol izquierdo.
}
t
t
t t->left t->left

Figura 6.28. Condiciones en BuscaMinimo

La complejidad del algoritmo es O(a), donde a es la altura del árbol. Ésta en promedio es
O(log2(n)) , y en peor caso O(n).

6.6.3.2. Seleccionar el nodo con valor máximo de clave.

Algoritmo: Descender a partir de la raíz por el subárbol derecho hasta encontrar un nodo con
hijo derecho nulo, el cual contiene el valor máximo de clave. Debe considerarse que el árbol
puede estar vacío.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 25
pnodo BuscarMaximoIterativo(pnodo t) {
while ( t != NULL)
{
if ( t->right == NULL ) return (t); //apunta al máximo.
else t=t->right; //desciende
}
return (t); /* NULL Si árbol vacío*/
}

/* Recursivo */
pnodo BuscaMaximo(pnodo t)
{
if (t == NULL) return(NULL); //si árbol vacío retorna NULL
if (t->right == NULL) return(t ); // Si no tiene hijo derecho: lo encontró.
return( BuscaMaximo (t->right) ); //sigue buscando en subárbol derecho.
}

Si en las funciones obtenidas en 6.6.3.2. se cambian left por right y viceversa se obtienen las
funciones anteriores. Esta es una importante propiedad de los árboles binarios de búsqueda.
6.6.3.3. Nodo descendiente del subárbol derecho con menor valor de clave.
Se ilustra un ejemplo de árbol binario de búsqueda, en el cual si t apunta a la raíz, se tendrá que
el nodo con clave 6 es el menor descendiente del subárbol derecho.
t

3 8

2 4 6 9

7
1

Figura 6.29. Menor descendiente subárbol derecho.

Diseño recursivo.
Se emplea la función BuscaMínimo, que es recursiva.

pnodo MenorDescendienteSD(pnodo t)
{
if (t == NULL) return(NULL); //si árbol vacío retorna NULL
if (t->right == NULL) return(NULL ); // Si no tiene hijo derecho no hay sucesor.
return( BuscaMinimo (t->right) ); //sigue buscando en subárbol derecho.
}

Profesor Leopoldo Silva Bijit 17-05-2009


26 Estructuras de Datos y Algoritmos
Para el diseño iterativo, cuando existe subárbol derecho, deben estudiarse dos casos, los cuales
se ilustran en la Figura 6.30.

El caso D1, un nodo sin hijo izquierdo, indica que se encontró el mínimo.
El caso D2, debe descenderse por el subárbol derecho de t, por la izquierda, mientras se tengan
hijos por la izquierda.
t
t

D1
D2

Figura 6.30. Casos en búsqueda del menor descendiente

pnodo MenorDescendienteIterativoSD(pnodo t)
/*menor descendiente de subárbol derecho. */
{ pnodo p;
if (t == NULL) return(NULL); //si árbol vacío retorna NULL
if (t->right == NULL) return(NULL ); // Si no tiene hijo derecho no hay sucesor.
else p = t->right;
while ( p->left != NULL) { /* Mientras no tenga hijo izq descender por la izq. */
p = p->left;
}
/*Al terminar el while p apunta al menor descendiente */
return (p); /* Retorna el menor */
}
6.6.3.4. Sucesor.
Dado un nodo encontrar su sucesor no es el mismo problema anterior, ya que el nodo podría ser
una hoja o un nodo sin subárbol derecho. Por ejemplo en la Figura 6.29, el sucesor del nodo con
clave 4 es el nodo con clave 5. El sucesor del nodo 2 es el nodo con valor 3.

Se requiere disponer de un puntero al padre del nodo, para que la operación sea de costo
logarítmico, en promedio.

Si un nodo tiene subárbol derecho, el sucesor de ese nodo es el ubicado más a la izquierda en
ese subárbol (problema que se resolvió en 6.6.3.3); si no tiene subárbol derecho, es el menor
ancestro (que está sobre el nodo en la trayectoria hacia la raíz) que tiene a ese nodo en su
subárbol izquierdo.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 27
5

1 8

2 6 9

3 7

t 4

Figura 6.31. Sucesores de distintos nodos.

Algoritmo:
Si el árbol no es vacío.
Si no tiene subárbol derecho:
Mientras exista el padre y éste apunte al nodo dado por la derecha se asciende:
Hasta encontrar el primer padre por la izquierda.
Si no existe ese padre, se retorna NULL, t era el nodo con valor máximo
Si tiene subárbol derecho, el sucesor es el mínimo del subárbol derecho.

Revisar el algoritmo para los diferentes nodos del árbol de la Figura 6.31, especialmente para
los nodos con claves 4 y 9. En este último caso debe asumirse que se ha definido que el padre
de la raíz tiene valor NULL.

pnodo Sucesor(pnodo t)
{ pnodo p;
if (t == NULL) return(NULL); //si árbol vacío retorna NULL
if (t->right == NULL)
{ p = t->padre; //p apunta al padre de t
while( p!=NULL && t == p->right)
{t=p; p=t->padre;} //se asciende
return(p); //
}
else
return( BuscaMinimo (t->right) ); //busca mínimo en subárbol derecho.
}

Como en peor caso debe ascenderse un trayectoria del nodo hacia la raíz, el costo será O(a),
donde a es la altura del árbol.
6.6.3.5. Nodo descendiente del subárbol izquierdo con mayor valor de clave.
Basta intercambiar left por right y viceversa en el diseño desarrollado en 6.6.3.3.
6.6.3.6. Predecesor.
El código de la función predecesor es la imagen especular del código de sucesor.

Profesor Leopoldo Silva Bijit 17-05-2009


28 Estructuras de Datos y Algoritmos
6.6.3.7. Buscar
Es una de las operaciones más importantes de esta estructura. Debido a la propiedad de los
árboles binarios de búsqueda, si el valor buscado no es igual al de nodo actual, sólo existen dos
posibilidades: que sea mayor o que sea menor. Lo que implica que el nodo buscado puede
pertenecer a uno de los dos subárboles. Cada vez que se toma la decisión de buscar en uno de
los subárboles de un nodo, se están descartando los nodos del otro subárbol. En caso de árboles
balanceados, se descarta la mitad de los elementos de la estructura, esto cumple el modelo:
T(n) = T(n/2) +c, lo cual asegura costo logarítmico.

pnodo BuscarIterativo( pnodo t, int valor)


{
while ( t != NULL)
{
if ( t->clave == valor ) return (t);
else {
if (t->clave < valor ) t = t->right; //desciende por la derecha
else t = t->left; //desciende por la izquierda
}
}
return (t); /* NULL No lo encontró*/
}

Es preciso tener implementados los operadores de igualdad y menor que, en caso de que éstos
no existan en el lenguaje, para el tipo de datos de la clave. Por ejemplo si la clave es
alfanumérica (un string), una estructura, etc.

Complejidad de la búsqueda.
Si T(a) es la complejidad de la búsqueda en un árbol de altura a.

En cada iteración, el problema se reduce a uno similar, pero con la altura disminuida en uno, y
tiene costo constante el disminuir la altura.

Entonces:
T(a) = T(a-1)+Θ(1) con T(0) = 0

La solución de esta recurrencia, es:


T(a) = a*Θ(1) = Θ(a)

Pero en árboles de búsqueda se tiene que: log2 n ≤ a ≤ n

Entonces: Θ( log2 n) ≤ T(a) ≤ Θ(n)

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 29
pnodo BuscarRecursivo( pnodo t, int valor )
{
if ( t == NULL) return (NULL); /* árbol vacío o hijo de hoja */
else {
if ( t->clave == valor ) return(t); /* lo encontró */
else {
if ( t->clave > valor ) t = BuscarRecursivo ( t->left, valor);
else t = BuscarRecursivo ( t->right, valor);
}
}
return ( t ); /* ! Si se entiende esta línea, muestra que se entiende el diseño recursivo */
}

En caso de activarse una secuencia de llamados recursivos, los retornos de éstos, son pasados a
través de la asignación a la variable t.

Pueden eliminarse las asignaciones y el retorno final, del diseño anterior, de la siguiente forma:

pnodo BuscarRecursivo2( pnodo t, int valor )


{
if ( t == NULL) return (NULL); /* árbol vacío o hijo de hoja */
else {
if ( t->clave == valor )
return (t); /* lo encontró */
else
{
if ( t->clave > valor )
return ( BuscarRecursivo2 ( t->left, valor) );
else
return ( BuscarRecursivo2 ( t->right, valor)) ;
}
}
}

En caso de retorno nulo, no es posible determinar si no encontró el elemento buscado o si se


trataba de un árbol vacío.

6.6.4. Operaciones de modificación

6.6.4.1. Insertar nodo


Diseño iterativo.
Primero se busca el sitio para insertar. Si el valor que se desea insertar ya estaba en el árbol, no
se efectúa la operación; ya que no se aceptan claves duplicadas. Entonces: se busca el valor; y si
no está, se inserta el nuevo nodo.

Profesor Leopoldo Silva Bijit 17-05-2009


30 Estructuras de Datos y Algoritmos
Es preciso almacenar en la variable local q, la posición de la hoja en la que se insertará el nuevo
nodo. En la Figura 6.31.a, se desea insertar un nodo con valor 4; se muestra el descenso a partir
de la raíz, hasta encontrar el nodo con valor 3, que tiene subárbol derecho nulo.
5 porlado
right
1 q 8
valor
4
3 6 9
t
2 7

Figura 6.31.a. Variables al salir del while.

typedef enum {left, right, vacio} modo;


pnodo InsertarIterativo(pnodo t, int valor)
{ pnodo q= t;
modo porlado=vacio;
while ( t != NULL)
{
if ( t->clave == valor )
{/*lo encontró, no inserta. No se aceptan claves repetidas en conjuntos*/
return (t);
}
else
{ q=t ;
if (t->clave < valor) {t = t->right; porlado=right;}
else {t = t->left; porlado=left; }
}
}
/*Al salir del while q apunta al nodo donde se insertará el nuevo, y porlado la dirección */
/* El argumento t apunta a NULL */
t = CreaNodo(valor); //se pega el nuevo nodo en t.
if (porlado==left) q->left=t; else if(porlado==right) q->right=t;
return (t); /* Apunta al recién insertado. Null si no se pudo insertar*/
}

Si CreaNodo retorna un NULL, si no había espacio en el heap, sin invocar a exit, se tendrá un
retorno nulo, si no se pudo insertar.

Si p y raiz son de tipo pnodo, el siguiente segmento ilustra un ejemplo de uso de la función,
considerando la inserción en un árbol vacío:

if (raiz==NULL) raiz=InsertarIterativo(raiz, 4);


else if ( (p=InsertarIterativo(raiz, 4))==NULL ) printf(“error”);

Dejando en p el nodo recién ingresado, o el ya existente con ese valor de clave.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 31

Se recorre una trayectoria de la raíz hasta una hoja. Entonces, si a es la altura, la complejidad de
la inserción es: T(a).

Una alternativa al diseño iterativo, es mantener un puntero al puntero izquierdo o derecho, en la


posición para insertar. En la descripción del descarte iterativo se dan explicaciones más
completas sobre la variable local p, que es puntero a un puntero.

En la Figura 6.31.b, se desea insertar un nodo con valor 4; se muestra el descenso a partir de la
raíz, hasta encontrar el nodo con valor 3, que tiene subárbol derecho nulo. Nótese que p queda
apuntando a un puntero.

pnodo Insert2(pnodo t, int valor)


{ pnodo *p = &t;
while (*p != NULL) {
if ((*p)->valor < valor) p = &((*p)->right);
else if ((*p)->valor > valor) p = &((*p)->left);
else { /* Ya estaba. No hace nada */
return (*p);
}
}
return( *p = getnodo(valor) );
}
t 5

1 p 8

3 6 9

2 7
Figura 6.31.b. Variables al salir del while.

La inserción en un árbol vacío debe condicionarse, para poder escribir en la variable externa,
denominada raiz. Si p y raiz son de tipo pnodo, el siguiente segmento ilustra el uso de la
función, considerando la inserción en un árbol vacío:

if (raiz==NULL) raiz=Insert2(raiz, 4);


else if ( (p=Insert2(raiz, 4))==NULL ) printf(“error”);

Dejando en p el nodo recién ingresado, o el ya existente con ese valor de clave.

Profesor Leopoldo Silva Bijit 17-05-2009


32 Estructuras de Datos y Algoritmos

Diseño recursivo.
pnodo InsertarRecursivo( pnodo t, int valor)
{
if (t == NULL) t = CreaNodo(valor); //insertar en árbol vacío o en hoja.
else
if (valor < t->clave) //insertar en subárbol izquierdo.
t->left = InsertarRecursivo(t->left, valor);
else
if (valor > t->clave) //insertar el subárbol derecho
t->right = InsertarRecursivo (t->right, valor);
/* else: valor ya estaba en el árbol. No hace nada. */
return(t);
}

La inserción en un árbol vacío debe poder escribir en la variable externa, denominada raiz. El
siguiente segmento ilustra el uso de la función:

raiz=InsertRecursivo(raiz, 4);

Debe notarse que se reescriben los punteros de los nodos que forman la ruta de descenso, lo cual
es una sobrecarga de trabajo innecesario, salvo que el último retorno escribe en la raíz. Si desea
conocerse si CreaNodo falla, retornando NULL, habría que comentar la excepción o salir del
programa.
Trayectoria en el descenso.
Veremos que en el algoritmo recursivo, la trayectoria recorrida desde la raíz hasta la posición
para insertar queda registrada en el stack.

En el árbol de la Figura 6.32, se especifican los valores de los punteros, y se desea insertar un
nodo con clave igual a 7.
raiz

t0

5
t1

3 8
t3 t2

2 4 6 9

Figura 6.32. Trayectorias en llamados recursivos.

Se ilustra el stack, después del llamado: InsertarRecursivo(raiz, 7)

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 33

t valor Llam ado núm ero


t0 7 1

Figura 6.33. Stack después de InsertarRecursivo(raiz, 7).

Al ejecutarse el código de la función, se determina que 7 es mayor que 5, y se reinvoca


(segundo llamado) con los valores: InsertarRecursivo (t1, 7); después del llamado, el stack
puede visualizarse:
t valor Llam ado núm ero
t0 7 1
t1 7 2

Figura 6.34. Stack después de InsertarRecursivo(t1, 7).

Al ejecutarse el código de este segundo llamado se determina que 7 es menor que 8 y se genera
un tercer llamado a la función con: InsertarRecursivo (t3, 7); después de este tercer llamado, el
stack queda:

t valor Llam ado núm ero


t0 7 1
t1 7 2
t3 7 3

Figura 6.35. Stack después de InsertarRecursivo(t3, 7).

Al ejecutar el código del tercer llamado, se determina que 7 es mayor que 6, y se produce el
cuarto llamado: InsertarRecursivo (t3->right, 7); si denominamos por t4 al valor t3->right, el
esquema que muestra las variables en el stack, queda como sigue:
t valor Llam ado núm ero
t0 7 1
t1 7 2
t3 7 3
t4 7 4

Figura 6.36. Stack después de InsertarRecursivo(t3->right, 7).

Al iniciar la ejecución del código del cuarto llamado, se tiene en el stack los valores de los
punteros que recuerdan la trayectoria del descenso hasta la posición de inserción. Al ejecutar
el código del cuarto llamado se determina que t4 es un puntero nulo, con lo cual, se crea el nodo
y se retorna en t (que es t4, la cuarta encarnación de t) el valor de un puntero al nodo recién
creado. En este momento se sale del cuarto llamado y el stack queda:

Profesor Leopoldo Silva Bijit 17-05-2009


34 Estructuras de Datos y Algoritmos

t valor Llam ado núm ero


t0 7 1
t1 7 2
t3 7 3

Figura 6.37. Stack después del retorno del cuarto llamado.

Se regresa a la ejecución del código del tercer llamado, efectuando la asignación:

t->right= <valor del puntero retornado t4>

Pero como, dentro del tercer llamado, t tiene el valor de t3, esta instrucción pega efectivamente
el nuevo nodo. Esta instrucción es la última del tercer llamado, con lo cual termina retornando el
valor t3. El stack queda ahora:

t valor Llam ado núm ero


t0 7 1
t1 7 2

Figura 6.38. Stack después del tercer retorno.

Y reanudamos la ejecución del segundo llamado, que había quedado pendiente. Efectuando la
asignación:
t->left = <valor retornado de t por el tercer llamado t3>

Sobrescribiendo un valor igual al existente, copia en t1->left el valor de t3. Obviamente esta
escritura es un costo adicional de la recursividad. Terminando así el segundo llamado, el cual
retorna el valor de t1.

El stack queda:
t valor Llam ado núm ero
t0 7 1

Figura 6.39. Stack después del retorno del segundo llamado.

Y se reanuda la ejecución, efectuando:

t->right = <valor retornado de t por el segundo llamado. t1>

La cual sobrescribe, nuevamente en forma innecesaria en t0->left el valor de t1.

De esta forma finaliza, recién, la primera invocación, retornado el valor de t0.

La expresión: InsertarRecursivo(raiz, 7), después de ejecutada, tiene el valor de t0.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 35
Entonces la forma de invocar a esta función es:

raiz=InsertarRecursivo(raiz, 7);

De este modo la inserción en un árbol vacío, liga correctamente el nodo agregado.

Si el valor del nodo que se desea insertar es igual a uno ya perteneciente al árbol, el llamado
también retorna t0.

Si el llamado a CreaNodo falla por no disponer de memoria en el heap, y en su diseño se


hubiera retornado un NULL (sin invocar a exit) no habría forma de conocer que la inserción
falló.

El diseño recursivo recorre desde la raíz hasta el punto de inserción, quedando la ruta de
descenso en el stack; luego de la inserción, recorre la ruta en sentido inverso; lo cual permite
agregar alguna operación a los nodos involucrados. La acción debe realizarse antes del retorno.
Ver el punto 6.6.5.11 Inserción en la raíz.

La operación insertar en el lugar de la raíz, es más compleja, ya que requiere modificar el árbol.
Requiere primero insertar el nodo, y luego efectuar rotaciones para llevar ese nodo al lugar de la
raíz, preservando la propiedad de un árbol binario de búsqueda. También se puede implementar
si se dispone de una función que parta un árbol en dos (split).

6.6.4.2. Descartar nodo


Descarte recursivo.
Primero se busca el nodo cuyo valor de clave es igual al valor pasado como argumento. Si no lo
encuentra retorna NULL. Si lo encuentra, la operación requiere mayor análisis, ya que se
producen varios casos. Lo importante es mantener la vinculación entre el resto de los elementos
del árbol.

a) El nodo que se desea descartar es una hoja.

En este caso, la operación es trivial, basta escribir un puntero con valor nulo. La estructura se
conserva.

Figura 6.40. Descartar hoja

b) El nodo que se desea descartar es un nodo interno.

i) con un hijo

Profesor Leopoldo Silva Bijit 17-05-2009


36 Estructuras de Datos y Algoritmos
En este caso, el padre debe apuntar al nieto, para conservar la estructura de árbol. Ya sea que
sólo tenga hijo derecho o izquierdo. Esto implica mantener un puntero al padre, en el descenso.
t t

Figura 6.41. Descartar nodo con un subárbol

ii) con dos hijos.


t

I D

Figura 6.42. Descartar nodo con dos hijos.

Para conservar la estructura del árbol, se debe buscar I, el mayor descendiente del hijo
izquierdo; o bien D, el menor descendiente del hijo derecho. Luego reemplazar la hoja obtenida
por el nodo a descartar. Se implementa la operación buscando D.

pnodo Descartar(pnodo t, int valor)


{ pnodo temp;
if (t == NULL) printf("Elemento no encontrado\n");
else
if (valor < t->clave) /* por la izquierda */
t->left = Descartar(t->left, valor);
else
if (valor > t->clave) /* por la derecha */
t->right = Descartar(t->right, valor);
else /* se encontró el elemento a descartar */
if (t->left && t->right) /* dos hijos */
{
/* reemplázelo con el menor del subárbol derecho. D*/
temp = MenorDescendiente(t->right) ;
t->clave = temp->clave; //copia el nodo
t->right = Descartar(t->right, temp->clave); /*borrar la hoja */
}
else
{ /* un hijo o ninguno */
temp = t;
if (t->left == NULL) /* sólo hijo derecho o sin hijos */

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 37
t = t->right;
else
if (t->right == NULL) /* solamente un hijo izquierdo */
t = t->left;
free(temp); /*libera espacio */
}
return(t);
}

La complejidad del descarte de un nodo es mayor que la inserción o la búsqueda.

La operación puede implementarse en forma iterativa.


Descarte iterativo.
En algoritmos iterativos, es preciso mantener la información de la trayectoria del descenso en
una variable auxiliar; en los algoritmos recursivos, se mantiene esa información en el stack.
Bastaría tener dos punteros, uno al nodo actual, y otro al anterior; sin embargo existe la
dificultad, a diferencia de listas simplemente enlazadas, de que el anterior podría ser un
descenso por la izquierda o por la derecha. Para solucionar lo anterior, se mantiene un puntero
al puntero anterior.

Veamos una ilustración, para mantener un puntero a un puntero al anterior:

Si se tiene la definición e inicialización: pnodo *p = &t;


p t
*p

a b

Figura 6.43. Puntero a puntero a nodo.

Entonces (*p)->clave tiene el valor de la clave A. Note que *p equivale a la variable t.


Si se efectúa la asignación: p = &((*p)->right); se modifica el diagrama según:
t
p

A
q

a b

Figura 6.44. Memorización de trayectoria de descenso.

Ahora *p contiene la dirección del puntero ilustrado como q en el diagrama.

Profesor Leopoldo Silva Bijit 17-05-2009


38 Estructuras de Datos y Algoritmos
La asignación: p = &((*p)->left); deja a p apuntando al puntero izquierdo del nodo con valor A.

Con estos conceptos se elabora el siguiente algoritmo iterativo.

pnodo DescarteIterativo(pnodo t, int valor)


{
pnodo *p = &t;
pnodo temp;
while (*p != NULL) {
if ((*p)->clave < valor) p = &((*p)->right);
else if ((*p)->clave > valor) p = &((*p)->left);
else { /* La encontró */
if ((*p)->left == NULL) {temp = *p; *p = (*p)->right; free(temp); }
else if ((*p)->right == NULL) {temp = *p; *p = (*p)->left; free(temp);}
else /* Tiene ambos hijos */
*p = Descarte_Raiz(*p);
return t;
}
}
Error(); /*No encontró nodo con clave igual a valor */
return t;
}

El descarte de la raíz, o de un nodo con dos hijos, está basado en encontrar el menor
descendiente del hijo derecho, o el mayor descendiente del hijo izquierdo.

pnodo Descarte_Raiz(pnodo t)
{
pnodo *p = NULL, temp;
if (rand()%2) { /*Existen dos soluciones */
/* Busca mayor descendiente del subárbol izquierdo */
p = &(t->left);
while ((*p)->right != NULL)
p = &((*p)->right);
} else {
/* o Busca menor descendiente del subárbol derecho */
p = &(t->right);
while ((*p)->left != NULL)
p = &((*p)->left);
}
t->clave = (*p)->clave; /*copia los valores del encontrado en la raíz. */
if ((*p)->left == NULL) {
temp = *p;
*p = (*p)->right;
} else { /* ((*p)->right == NULL) */
temp = *p;
*p = (*p)->left;

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 39
}
free(temp);
return t;
}
6.6.4.3. Descartar árbol
Debe notarse que primero deben borrarse los subárboles y luego la raíz.

pnodo deltree(pnodo t)
{
if (t != NULL) {
t->left = deltree(t->left);
t->right = deltree(t->right);
free(t);
}
return NULL;
}

6.6.5. Otras operaciones

6.6.5.1. Profundidad del árbol.


int Profundidad(pnodo t)
{
int left=0, right = 0;
if(t==NULL) return 0; //Si árbol vacío, profundidad 0
if(t->left != NULL) left = Profundidad(t->left); //calcula profundidad subárbol izq.
if(t->right != NULL) right = Profundidad(t->right); //calcula profundidad subárbol der.
if( left > right) //si el izq tiene mayor profundidad
return left+1; //retorna profundidad del subárbol izq + 1
else
return right+1; //retorna profundidad del subárbol der + 1
}

El algoritmo se ha descrito mediante los comentarios.


6.6.5.2. Altura del árbol.
int Altura(pnodo T)
{ int h, max;
if (T == NULL) return -1;
else {
h = Altura (T->left);
max = Altura (T->right);
if (h > max) max = h;
return(max+1);
}
}

Profesor Leopoldo Silva Bijit 17-05-2009


40 Estructuras de Datos y Algoritmos

6.6.5.3. Contar hojas


int NumerodeHojas(pnodo t)
{ int total = 0;
//Si árbol vacío, no hay hojas
if(t==NULL) return 0;
// Si es hoja, la cuenta
if(t->left == NULL && t->right == NULL) return 1;
//cuenta las hojas del subárbol izquierdo
if(t->left!= NULL) total += NumerodeHojas(t->left);
//cuenta las hojas del subárbol derecho
if(t->right!=0) total += NumerodeHojas(t->right); //
return total; //total de hojas en subárbol
}
Nuevamente el algoritmo está descrito a través de los comentarios.
6.6.5.4. Contar nodos del árbol.
int ContarNodos(pnodo t)
{
if (t == NULL) return 0;
return (1 + ContarNodos(t->left) + ContarNodos(t->right) );
}
6.6.5.5. Contar nodos internos.
Tarea
6.6.5.6. Contar nodos con valores menores que un valor dado.
Tarea
6.6.5.7. Partir árbol.
Algoritmo iterativo, con pasos por referencia.
La descripción de las funciones split y join, son buenos ejemplos para mostrar si se domina el
concepto de punteros en el lenguaje C.

pnodo split(int key, pnodo t, pnodo *l, pnodo *r)


{
while (t != NULL && t->clave != key) {
if (t->clave < key) {
*l = t;
t = t->right;
l = &((*l)->right);
} else {
*r = t;
t = t->left;
r = &((*r)->left); //plop
}
}

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 41
if (t == NULL) {
*l = NULL;
*r = NULL;
} else { /* t->clave == key */
*l = t->left;
*r = t->right;
}
return t;
}
6.6.5.8. Insertar nueva raíz.
pnodo InsertarRaiz(int key, pnodo t)
{
pnodo l, r;
t = split(key, t, &l, &r);
if (t == NULL) {
t = CreaNodo(key);
t->left = l;
t->right = r;
} else {
t->left = l;
t->right = r;
Error();
}
return t;
}
6.6.5.9. Unir dos árboles.
Los árboles que deben ser unidos cumplen las siguientes relaciones de orden: a<A<b y d<D<e
y tal que b<d. Podrían ser dos árboles que se generan al eliminar la raíz de un árbol binario de
búsqueda.

Al inicio se tienen las siguientes variables


p t
l r

A D

a b d e

Figura 6.45. Variables en unir dos subárboles.

Si los dos subárboles no son vacíos, en la primera iteración se da valor inicial a t, la variable de
retorno que apuntará a la nueva raíz. Ésta puede ser l o r, dependiendo del azar.
Luego se mantiene en p: la dirección del puntero derecho de la raíz, si comienza por el subárbol
izquierdo; o la dirección del puntero izquierdo de la raíz, si comienza por el subárbol derecho.

Profesor Leopoldo Silva Bijit 17-05-2009


42 Estructuras de Datos y Algoritmos
Finalmente cada iteración finaliza haciendo descender l por la derecha o r por la izquierda.

En el supuesto que comienza por el subárbol izquierdo, la situación luego de la primera


iteración del while, efectuando la parte del if, es:
t
r

p A l D

a b d e

Figura 6.46. Parte del if dentro del while.

Si en la segunda iteración, realiza la parte del else, después de efectuado éste, queda:
t
A

a D
p

r
l

b d e

Figura 6.47. Parte del else dentro del while.

Lográndose que l y r apunten a dos subárboles. Se repite el while, hasta que uno de los dos
subárboles quede vacío. La alternativa final, pega el subárbol restante, a la estructura.

La solución trivial de agregar el subárbol derecho al nodo con valor mayor de clave del subárbol
cuya raíz es l, alarga la altura en la suma de las alturas individuales. Lo mismo ocurre si se une l
al menor elemento de r.
l

a b
r

d e

Figura 6.48. Unión de los árboles.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 43
La solución, propuesta a continuación, intenta disminuir la altura total del nuevo árbol.

pnodo join(pnodo l, pnodo r)


{
pnodo t = NULL;
pnodo *p = &t;
while (l != NULL && r != NULL) {
if (rand()%2) { //cara y sello.
*p = l;
p = &((*p)->right);
l = l->right;
} else {
*p = r;
p = &((*p)->left);
r = r->left;
}
}
if (l == NULL) *p = r;
else /* (r == NULL) */ *p = l;
return t;
}

La operación de descartar la raíz, DescartarRaiz, también puede implementarse en base a la


función join, que une dos árboles conservando la propiedad de árbol binario de búsqueda.

La operación de descartar la raíz y pegar los subárboles, se ilustra a continuación.

pnodo DescartarRaiz(pnodo t)
{ pnodo temp = t;
t = join(t->left, t->right);
free(temp);
return t;
}

Las rutinas split y join usan intensivamente punteros. Entenderlas es un indicador que esos
conceptos y sus principales usos han logrado ser dominados.

6.6.5.10. Rotaciones
Rotaciones simples a la izquierda y a la derecha
Un esquema de las variables, que permiten diseñar las funciones, se ilustra en las Figuras 6.49 y
6.50.

Profesor Leopoldo Silva Bijit 17-05-2009


44 Estructuras de Datos y Algoritmos
pnodo lrot(pnodo t)
{ pnodo temp=t;
t = t->right;
temp->right = t->left;
t->left = temp;
return t;
}

pnodo rrot(pnodo t)
{ pnodo temp = t;
t = t->left;
temp->left = t->right;
t->right = temp;
return t;
}

Las siguientes funciones son mejores que las anteriores, ya que tienen una asignación menos,
sólo escriben en tres punteros.

/* Rotación Izquierda*/
pnodo rotL(pnodo t)
{ pnodo temp = t->right;
t->right = temp->left;
temp->left = t;
return ( temp);
}

A B temp
temp
t

a B A
c

b c a b

antes después

Figura 6.49. Rotación izquierda.

/* Rotación derecha*/
pnodo rotR(pnodo t)
{ pnodo temp = t->left;
t->left = temp->right;
temp->right = t;
return (temp);
}

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 45
temp
B t A
temp t

A B
c a

a b c
b

antes después

Figura 6.50. Rotación derecha.

Las rotaciones pueden cambiar la forma del árbol, pero no la relación de orden; en el caso de las
figuras anteriores se preserva la relación: a<A<b<B<c.

Solamente afectan a los nodos rotados y sus hijos inmediatos, los ancestros y los nietos no son
modificados.

Pueden diseñarse funciones que pasen argumentos por referencia. Se ilustra el diseño de la
rotación simple a la derecha:

void rightRotRef( pnodo * t) //por referencia


{ pnodo temp = (*t)->left;
(*t)->left = temp->right;
temp->right = *t;
*t = temp; //modifica la variable pasada por referencia
}
En este caso se debe pasar la dirección de la variable donde debe escribir la función.

rightRotRef( &(root->right));
2 2
root
root

1 6 1 4

4 7 3 6

3 7

Figura 6.51. Ejemplo de Rotación derecha de nodos 4-6.

El diagrama a la derecha ilustra la rotación efectuada, la raíz no cambia.

En el diseño basado en retornos por punteros, es preciso escribir en una variable, mediante el
retorno de la función.

La siguiente asignación realiza la misma acción que la invocación al procedimiento anterior.

Profesor Leopoldo Silva Bijit 17-05-2009


46 Estructuras de Datos y Algoritmos
root->right=rotR(root->right);
6.6.5.11. Inserción en la raíz.
En la inserción común en un árbol binario de búsqueda, los elementos recién insertados quedan
alejados de la raíz, lo que implica que toma más tiempo encontrar los elementos insertados más
recientemente. Lo cual puede ser un inconveniente si en la aplicación los elementos recién
insertados tienden a ser buscados más a menudo que los insertados hace más tiempo; es decir si
existe localidad temporal en las referencias.

La inserción en la raíz coloca el nodo que será insertado en la posición de la raíz actual. De este
modo los nodos más recientemente insertados quedan más cercanos a la raíz que los más
antiguamente insertados.

Para mantener la propiedad del árbol binario de búsqueda, se inserta de manera convencional,
como una hoja, y luego mediante rotaciones se lo hace ascender a la posición de la raíz.

En el diagrama, se ilustra la inserción de un nodo con valor de clave 5, en la posición de una


hoja. Luego se lo hace ascender, rotando el par 4-5 a la izquierda; luego el par 5-6 a la derecha;
y finalmente el par 2-5 a la izquierda.
2
2 2
2 5
1 6

1 6 1 6 1 5 2 6
5 7

4 7 4 7 4 6 1 4 7
4

3 5 3 7 3
3 3

Figura 6.52. Inserción de nodo con clave 5 en la raíz.

El algoritmo está basado en observar que cuando se desciende por la izquierda desde un nodo n,
durante la búsqueda para insertar, se debe rotar hacia la derecha en n. Y si se desciende por la
rama derecha, ese nodo debe rotarse hacia la izquierda. En el nodo con clave 6, se descendió por
la izquierda; luego el nodo 6 se rota a la derecha.

A medida que se desciende, para buscar la posición para insertar, se registra el nodo; luego
después de la inserción, se retorna a cada uno de estos nodos, en orden inverso, y se realiza la
rotación.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 47
/*Inserta un nodo y lo convierte en la nueva raíz */
pnodo InserteRaiz(pnodo t, int valor)
{
if (t == NULL) return ( CreaNodo(valor);)
if (valor < t->clave)
{
t->left = InserteRaiz(t->left, valor);
t = rotR(t);
}
else {
t->right = InserteRaiz(t->right, valor);
t = rotL(t);
}
return t;
}
6.6.5.12. Imprimir la forma del árbol.
Las dos rutinas siguientes permiten desplegar un árbol, y pueden ser útiles para verificar las
funciones que los manipulan.

void printNodo(pnodo t, int h)


{ int i;
for(i=0; i<h; i++) putchar(‘\t’); //se emplean tabs para desplegar niveles.
if(t==NULL) {putchar(‘*’) ; putchar(‘\n’) ;}
else printf(“%d\n”, t->clave);
}

void Mostrar(pnodo t, int h)


{
if(t==NULL) printNodo(t, h);
else {Mostrar(t->right, h+1) ; printNodo(t, h); Mostrar(t->left, h+1);}
}
*
7
2 *
root
6
1 6
*
4
*
4 7 3
*
2
3 *
1
*

Figura 6.53. Impresión de la forma de un árbol.

Profesor Leopoldo Silva Bijit 17-05-2009


48 Estructuras de Datos y Algoritmos
La ejecución de Mostrar(root,0); muestra un ejemplo del despliegue de un árbol cuyo diagrama
se ilustra en la Figura 6.53 izquierda.

Problemas resueltos.

P6.1.

Para la siguiente estructura de un nodo de un árbol binario de búsqueda:


typedef struct tnode
{ int valor;
char *v; //se apunta a un string
struct tnode *left, *right;
} nodo, * pnodo;

a) Diseñar función que borre el subárbol apuntado por t, liberando todo el espacio que haya sido
solicitado en forma dinámica.

b) Diseñar función que cuente en el subárbol, apuntado por t, los nodos que tengan valores
menores o iguales que k.

Solución.
a) Puede diseñarse considerando:

Si el subárbol es vacío, retorna NULL


Si no es vacío: Borra subárbol izquierdo y luego el derecho; después de lo cual borra el nodo.
Previo a borrar el nodo; es decir, antes de borrar el puntero v, debe consultarse si existe un
string, en caso de haberlo, se borra éste primero, y luego el nodo.

Una posible implementación es la siguiente:

pnodo BorrarArbol(pnodo t)
{
if (t != NULL) {
t->left = BorrarArbol (t->left);
t->right = BorrarArbol (t->right);
if (t->v != NULL) free(t->v);
free(t);
}
return NULL;
}

Es preciso que la función retorne un puntero a nodo, para pasar los datos de los retornos de los
llamados recursivos. El esquema anterior, borra primero las hojas. El recorrido del árbol es
subárbol izquierdo, subárbol derecho y finalmente la raíz ( en orden).

Para agregar un string s a un nodo apuntado por t, puede emplearse:

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 49
#include <string.h>

char * ColocaString( pnodo t, char * s)


{
if ( (t->v = (char *) malloc( (strlen(s)+1)*sizeof(char) ) ) != NULL ) strcpy( t->v, s);
return ( t->v);
}

El espacio adicional se requiere debido a que strlen retorna el largo del string, sin incluir el
carácter de fin de string.
La function retorna NULL, si no pudo pegar el string al nodo, en caso contrario retorna un
puntero al inicio del string.

b) Contar en subárbol apuntado por t los nodos, que tengan valores menores o iguales que el
valor entero k.

int ContarMenor_o_Igual(pnodo t, int k)


{
if (t == NULL) return 0; //Si es subárbol nulo, no lo cuenta
else
if (t->valor < k)
{ // Si el valor es menor que k, contar el nodo y además los de ambos subárboles
return (1 + ContarMenor_o_Igual (t->left,k) + ContarMenor_o_Igual (t->right,k) );
}
else
if (t->valor == k)
{ // Si valor igual a k, contar el nodo y además sólo los del subárbol izquierdo

return (1 + ContarMenor_o_Igual (t->left, k) );


}
else
{ // Si valor mayor que k, contar sólo los nodos del subárbol izq. Sin incluirlo
return ( ContarMenor_o_Igual (t->left, k) );
}
}

El valor retornado por la función corresponde al número de nodos que cumplen la condición.

P6.2.

1. Se tiene el siguiente árbol de búsqueda.


a) ¿Cuáles son los órdenes posibles en los que llegaron las claves para formar el árbol?.
b) En un listado post-orden quienes figuran antes y después del valor 6.
c) En un listado post-orden quienes figuran antes y después del valor 2.
d) Dibujar el árbol, luego de: insertar el nodo con valor 5, y descartar los nodos con valores 4 y
luego el 7. Indicar alternativas de solución, si las hubiera.

Profesor Leopoldo Silva Bijit 17-05-2009


50 Estructuras de Datos y Algoritmos

3 8

1 4

2 6

Figura P6.1.

Solución.
a) Llega primero el 7. Luego pueden llegar el 3 o el 8. Luego del 3 pueden llegar el 1 o el 4. El
2 debe llegar después del 1; y el 6 después del 4.
b) 2 1 6 4 3 8 7. El 1 antes del 6, y el 4 luego de éste.
c) El 2 es el primero, luego viene el 1. No hay nada antes del 2, ya que es el primero.
d) Luego de insertar el 5 y descartar el 4, se tienen dos posibles soluciones para descartar el
nodo con valor 7:

7 6 8

3 8 3 8 3

1 6 1 5 1 6

2 5 2 2 5

Figura P6.2.

P6.3.

Para un árbol binario de búsqueda, diseñar una función, con el siguiente prototipo:
int trayectoria(pnodo t, int valor);

Retorna 1 si lo encontró, 0 si árbol vacío o no lo encontró.

a) Que imprima la trayectoria desde el nodo con clave igual al argumento valor hasta la raíz, si
éste se encuentra en el árbol, y “no encontrado” en caso contrario.
b) Discuta la forma de diseño, si se desea imprimir los valores de los nodos desde la raíz, hasta
el nodo que tiene igual valor de clave que el argumento dado.

Solución.

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 51
a) El diseño recursivo almacena en el stack los punteros a los nodos en la ruta de descenso desde
la raíz al nodo con el valor buscado, si éste se encuentra en el árbol. Si no se encuentra el valor
buscado, se desciende hasta encontrar un valor nulo. Entonces debe descenderse, pero no
imprimir si se llega a sobrepasar una hoja en el descenso.
Un diseño posible es mediante una variable flag, para indicar si debe o no imprimirse los
valores, luego de los retornos de la función recursiva.

static int flag=0;


int imprimetrayectoria(pnodo t, int valor)
{ if (t == NULL)
{ printf("\nArbol vacío o no lo encontró!\n");
flag=0; return(flag);
}
else
if (valor < t->valor)
{ imprimetrayectoria(t->left,valor);
if(flag==1) printf(" %d", t->valor);
}
else
if (valor > t->valor)
{ imprimetrayectoria(t->right,valor);
if(flag==1) printf(" %d", t->valor);
}
else /* lo encontró. */
{ printf("\n%d", t->valor);
flag=1;
}
return(flag);
}

No es necesaria la variable flag, ya que se puede emplear el retorno entero de la función, como
puede apreciarse en el siguiente diseño:

int trayectoria2(pnodo t, int valor)


{ if (t == NULL)
{ printf("\nArbol vacío o no lo encontró!\n"); return(0); }
else
if (valor < t->valor)
{ if( trayectoria2(t->left,valor)) printf(" %d", t->valor); }
else
if (valor > t->valor)
{if( trayectoria2(t->right,valor)) printf(" %d", t->valor);}
else /* lo encontró. */
{ printf("\n%d", t->valor); return(1); }
return(1);
}
El stack es lifo.

Profesor Leopoldo Silva Bijit 17-05-2009


52 Estructuras de Datos y Algoritmos

b) La impresión desde la raíz hasta el nodo con el valor buscado, puede resolverse empleando
una cola. Se encolan los valores desde la raíz, pero considerando que si se llega a un puntero
nulo, no deben imprimirse. Es más directo en este caso un diseño iterativo.

int ImpIterativo(pnodo t, int valor)


{
creacola();
while ( t != NULL)
{ if ( t->valor == valor )
{ encole(); putchar('\n'); /*lo encontró*/
imprimacola(); return (1);
}
else
{ encole(); //fifo
if (t->valor < valor) t = t->right; else t = t->left;
}
}
/*Al salir del while es árbol vacío o no lo encontró */
if(plista->next==NULL) printf("\nArbol vacío!");
else { printf("\nNo lo encontró!"); liberacola(); }
return (0);
}

Los tipos y macros para manejar la cola:

typedef struct lnode


{ int valor;
struct lnode *next;
} nodol, * pnodol; //tipos de la cola
#define creacola() pnodol pfondo= malloc(sizeof(nodol)); \
pnodol plista=pfondo; \
plista->next=NULL;

#define encole() pfondo->next=malloc(sizeof(nodol)); \


pfondo=pfondo->next; \
pfondo->next=NULL; \
pfondo->valor=t->valor;

#define imprimacola() while(plista->next!=NULL) \


{ pfondo=plista; plista=plista->next; \
printf(" %d ",plista->valor); free(pfondo); \
}

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 53
Cuando el valor no se encuentra, habría que liberar el espacio de la cola.
#define liberacola() while(plista->next!=NULL) \
{ pfondo=plista;plista=plista->next; free(pfondo); \
}

Otra solución es reemplazar en el código de la parte a), los printf por la acción de empujar los
valores a imprimir a un stack. Luego de terminada la función si el stack queda vacío, entonces la
clave no se encontró; si no está vacío, se imprimen desde el tope. De esta forma se imprimen los
valores desde la raíz hasta la hoja encontrada.

Ejercicio propuestos.

E6.1.

Desarrollar programa que efectúe listado postorden para multiárbol descrito por arreglos, con
hijo más izquierdista y hermano derecho.

E6.2.

Para un árbol binario de búsqueda, determinar procedimiento que escriba sólo las hojas.
a) Desde la hoja más izquierdista hasta la hoja más derechista.
b) Desde la hoja más derechista hasta la hoja más izquierdista.

E6.3.

Se tiene un árbol binario de búsqueda.


typedef struct tnode
{
int valor;
struct tnode *left;
struct tnode *right;
} nodo, * pnodo;

Diseñar función no recursiva que borre la raíz de un subárbol, que se pasa como argumento y
retorne un puntero a la nueva raíz.

Profesor Leopoldo Silva Bijit 17-05-2009


54 Estructuras de Datos y Algoritmos

Índice general.

CAPÍTULO 6. .............................................................................................................................................1
ÁRBOLES BINARIOS DE BÚSQUEDA.................................................................................................1
6.1. DEFINICIONES. ...................................................................................................................................1
Ejemplos de definiciones......................................................................................................................2
6.2. ÁRBOL BINARIO..................................................................................................................................2
Definición de tipos de datos.................................................................................................................3
6.3. ÁRBOL BINARIO DE BÚSQUEDA. .........................................................................................................3
6.4. CÁLCULOS DE COMPLEJIDAD O ALTURA EN ÁRBOLES.........................................................................4
6.4.1. Árbol completo...........................................................................................................................4
6.4.2 Árboles incompletos con un nivel de desbalance........................................................................6
6.4.3. Árboles construidos en forma aleatoria.....................................................................................7
6.4.4. Número de comparaciones promedio en un árbol binario de búsqueda..................................11
6.4.4.1. Árbol binario externo ........................................................................................................................11
6.4.4.2. Largos de trayectorias interna y externa............................................................................................13
6.4.4.3. Búsquedas exitosas y no exitosas. ..................................................................................................... 15
6.5. RECORRIDOS EN ÁRBOLES. ...............................................................................................................19
6.5.1. En orden:..............................................................................................................................................19
6.5.2. Pre orden: .............................................................................................................................................19
6.5.3. Post orden: ...........................................................................................................................................19
6.5.4. Ejemplo de recorridos. .........................................................................................................................19
6.5.5. Árboles de expresiones. .......................................................................................................................20
6.5.6. Árboles de derivación. .........................................................................................................................20
6.6. OPERACIONES EN ÁRBOLES BINARIOS. .............................................................................................20
6.6.1. Operaciones básicas ................................................................................................................21
6.6.1.1. Crear árbol vacío. ..............................................................................................................................21
6.6.1.2. Crea nodo inicializado con un valor de clave....................................................................................21
6.6.1.3. Ejemplo de uso..................................................................................................................................21
6.6.2. Operaciones de recorrido ........................................................................................................21
6.6.2.1. Mostrar en orden ...............................................................................................................................21
6.6.2.2. Mostrar en post-orden .......................................................................................................................23
6.6.2.3. Mostrar en pre-orden.........................................................................................................................23
6.6.3. Operaciones de consulta..........................................................................................................23
6.6.3.1. Seleccionar el nodo con valor mínimo de clave. ...............................................................................23
6.6.3.2. Seleccionar el nodo con valor máximo de clave................................................................................24
6.6.3.3. Nodo descendiente del subárbol derecho con menor valor de clave. ................................................25
6.6.3.4. Sucesor..............................................................................................................................................26
6.6.3.5. Nodo descendiente del subárbol izquierdo con mayor valor de clave. ..............................................27
6.6.3.6. Predecesor. ........................................................................................................................................27
6.6.3.7. Buscar ...............................................................................................................................................28
Complejidad de la búsqueda......................................................................................................................28
6.6.4. Operaciones de modificación...................................................................................................29
6.6.4.1. Insertar nodo .....................................................................................................................................29
Diseño iterativo. ........................................................................................................................................29
Diseño recursivo........................................................................................................................................32

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 55
Trayectoria en el descenso. ....................................................................................................................... 32
6.6.4.2. Descartar nodo .................................................................................................................................. 35
Descarte recursivo..................................................................................................................................... 35
Descarte iterativo. ..................................................................................................................................... 37
6.6.4.3. Descartar árbol.................................................................................................................................. 39
6.6.5. Otras operaciones ................................................................................................................... 39
6.6.5.1. Profundidad del árbol........................................................................................................................ 39
6.6.5.2. Altura del árbol. ................................................................................................................................ 39
6.6.5.3. Contar hojas ...................................................................................................................................... 40
6.6.5.4. Contar nodos del árbol. ..................................................................................................................... 40
6.6.5.5. Contar nodos internos. ...................................................................................................................... 40
6.6.5.6. Contar nodos con valores menores que un valor dado. ..................................................................... 40
6.6.5.7. Partir árbol. ....................................................................................................................................... 40
6.6.5.8. Insertar nueva raíz............................................................................................................................. 41
6.6.5.9. Unir dos árboles. ............................................................................................................................... 41
6.6.5.10. Rotaciones....................................................................................................................................... 43
6.6.5.11. Inserción en la raíz. ......................................................................................................................... 46
6.6.5.12. Imprimir la forma del árbol............................................................................................................. 47
PROBLEMAS RESUELTOS......................................................................................................................... 48
P6.1. .................................................................................................................................................. 48
P6.2. .................................................................................................................................................. 49
P6.3. .................................................................................................................................................. 50
EJERCICIO PROPUESTOS. ......................................................................................................................... 53
E6.1. .................................................................................................................................................. 53
E6.2. .................................................................................................................................................. 53
E6.3. .................................................................................................................................................. 53
ÍNDICE GENERAL. ................................................................................................................................... 54
ÍNDICE DE FIGURAS................................................................................................................................. 56

Profesor Leopoldo Silva Bijit 17-05-2009


56 Estructuras de Datos y Algoritmos

Índice de figuras.

FIGURA 6.1. ÁRBOLES. ..................................................................................................................................2


FIGURA 6.2. ÁRBOL BINARIO.........................................................................................................................2
FIGURA 6.3. ÁRBOL BINARIO DE BÚSQUEDA..................................................................................................3
FIGURA 6.4. NO ES ÁRBOL BINARIO DE BÚSQUEDA. .......................................................................................4
FIGURA 6.5. VARIOS ÁRBOLES BINARIOS DE BÚSQUEDA CON DISTINTA FORMA.............................................4
FIGURA 6.6. ÁRBOL COMPLETO DE NIVEL 1...................................................................................................5
FIGURA 6.7. ÁRBOL COMPLETO DE NIVEL 2...................................................................................................5
FIGURA 6.8. ÁRBOL COMPLETO DE NIVEL 3...................................................................................................5
FIGURA 6.9. ÁRBOLES INCOMPLETOS DE NIVEL 2. .........................................................................................6
FIGURA 6.10. RAÍZ CON VALOR I. ..................................................................................................................7
FIGURA 6.11. ALTURA DE ÁRBOL GENERADO ALEATORIAMENTE. ...............................................................10
FIGURA 6.12. ALARGUE DE ALTURA DE ÁRBOL GENERADO ALEATORIAMENTE. ..........................................11
FIGURA 6.13. NODOS INTERNOS Y EXTERNOS..............................................................................................11
FIGURA 6.14. P(1): NE = NI + 1 ......................................................................................................................12
FIGURA 6.15. P(2): NE = NI + 1 ......................................................................................................................12
FIGURA 6.16. PRIMER CASO DE P(N+1): NE = NI + 1 ......................................................................................13
FIGURA 6.17. SEGUNDO CASO DE P(N): NE = NI + 1 .......................................................................................13
FIGURA 6.18. P(1): E(N) = I(N) + (2 N + 1).................................................................................................13
FIGURA 6.19. P(2): E(N) = I(N) + (2 N + 1) ..................................................................................................14
FIGURA 6.20. PRIMER CASO DE P(N): E(N) = I(N) + (2 N + 1).......................................................................14
FIGURA 6.21. SEGUNDO CASO DE P(N): E(N) = I(N) + (2 N + 1) ...................................................................15
FIGURA 6.22. EVALUACIÓN DE U(0)...........................................................................................................16
FIGURA 6.23. S(N) ES Θ(LOG2(N)). ..............................................................................................................18
FIGURA 6.24. ÁRBOL CON CLAVES {N0, N1, N2, N3, N4, N5}. ......................................................................20
FIGURA 6.25. ÁRBOL QUE REPRESENTA A: ( A * B) / (C + D).........................................................................20
FIGURA 6.26. ÁRBOL DE DERIVACIÓN. ........................................................................................................20
FIGURA 6.27. VARIABLES EN BUSCARMINIMOITERATIVO...........................................................................24
FIGURA 6.28. CONDICIONES EN BUSCAMINIMO ..........................................................................................24
FIGURA 6.29. MENOR DESCENDIENTE SUBÁRBOL DERECHO. .......................................................................25
FIGURA 6.30. CASOS EN BÚSQUEDA DEL MENOR DESCENDIENTE.................................................................26
FIGURA 6.31. SUCESORES DE DISTINTOS NODOS..........................................................................................27
FIGURA 6.31.A. VARIABLES AL SALIR DEL WHILE........................................................................................30
FIGURA 6.31.B. VARIABLES AL SALIR DEL WHILE........................................................................................31
FIGURA 6.32. TRAYECTORIAS EN LLAMADOS RECURSIVOS..........................................................................32
FIGURA 6.33. STACK DESPUÉS DE INSERTARRECURSIVO(RAIZ, 7)...............................................................33
FIGURA 6.34. STACK DESPUÉS DE INSERTARRECURSIVO(T1, 7). .................................................................33
FIGURA 6.35. STACK DESPUÉS DE INSERTARRECURSIVO(T3, 7). .................................................................33
FIGURA 6.36. STACK DESPUÉS DE INSERTARRECURSIVO(T3->RIGHT, 7). ....................................................33
FIGURA 6.37. STACK DESPUÉS DEL RETORNO DEL CUARTO LLAMADO.........................................................34
FIGURA 6.38. STACK DESPUÉS DEL TERCER RETORNO. ................................................................................34
FIGURA 6.39. STACK DESPUÉS DEL RETORNO DEL SEGUNDO LLAMADO. .....................................................34
FIGURA 6.40. DESCARTAR HOJA ..................................................................................................................35
FIGURA 6.41. DESCARTAR NODO CON UN SUBÁRBOL ..................................................................................36
FIGURA 6.42. DESCARTAR NODO CON DOS HIJOS.........................................................................................36
FIGURA 6.43. PUNTERO A PUNTERO A NODO................................................................................................37

Profesor Leopoldo Silva Bijit 17-05-2009


Árboles binarios de búsqueda 57
FIGURA 6.44. MEMORIZACIÓN DE TRAYECTORIA DE DESCENSO. ................................................................ 37
FIGURA 6.45. VARIABLES EN UNIR DOS SUBÁRBOLES. ................................................................................ 41
FIGURA 6.46. PARTE DEL IF DENTRO DEL WHILE......................................................................................... 42
FIGURA 6.47. PARTE DEL ELSE DENTRO DEL WHILE. ................................................................................... 42
FIGURA 6.48. UNIÓN DE LOS ÁRBOLES........................................................................................................ 42
FIGURA 6.49. ROTACIÓN IZQUIERDA........................................................................................................... 44
FIGURA 6.50. ROTACIÓN DERECHA............................................................................................................. 45
FIGURA 6.51. EJEMPLO DE ROTACIÓN DERECHA DE NODOS 4-6.................................................................. 45
FIGURA 6.52. INSERCIÓN DE NODO CON CLAVE 5 EN LA RAÍZ...................................................................... 46
FIGURA 6.53. IMPRESIÓN DE LA FORMA DE UN ÁRBOL. ............................................................................... 47
FIGURA P6.1. .............................................................................................................................................. 50
FIGURA P6.2. .............................................................................................................................................. 50

Profesor Leopoldo Silva Bijit 17-05-2009


1

Capítulo 7

Tablas de hash.

7.1. Operaciones.

La tabla de hash pertenece a la categoría de diccionarios que son aquellas estructuras de datos y
algoritmos que permiten buscar, insertar y descartar elementos.
Si las operaciones se reducen solamente a buscar e insertar se llaman tablas de símbolos.
En diccionarios puros sólo se implementa buscar.

Los diccionarios y tablas pertenecen también a la categoría de conjuntos dinámicos.

7.2. Clave.

La información que se desea buscar suele ser una estructura que organiza la información. Uno
de los campos de esa estructura se denomina clave, y debe ser única. Sólo una de las estructuras
puede tener un determinado valor de la clave.
Si la clave es un string, deben definirse operadores de comparación, los cuales generalmente
comparan en forma alfabética.

7.3. Tabla de acceso directo.

Un arreglo es una estructura de datos que permite implementar tablas de acceso directo. Es este
caso la clave es el índice del arreglo, y existe una posición del arreglo para cada posible clave.
Si el contenido de una celda del arreglo es un puntero a la estructura con los datos asociados a la
clave, se puede implementar las operaciones en forma sencilla. Si el elemento asociado a una
clave no está presente se lo indica con un puntero de valor NULL.

Índice Tabla

0 Estructura 0
1
2 Estructura 2
3
4 Estructura 4

Figura 7.1 Tabla de acceso directo.

Profesor Leopoldo Silva Bijit 11-06-2008


2 Estructuras de Datos y Algoritmos
pnodo buscar(int clave)
{
return (Tabla[clave]);
}

int insertar(int clave, pnodo pestructura)


{
if (Tabla[clave] == NULL ) Tabla[clave]=pestructura; return 0;
else return (1); // error: ya estaba.
}
int descartar(int clave)
{
if (Tabla[clave]!= NULL ) {free(Tabla[clave]); Tabla[clave]= NULL ; return 0;}
else return (1); //error: no estaba.
}

Todas las operaciones son O(1).

Para emplear esta solución el tamaño del arreglo, debe ser igual al número de claves posibles y
éstas deben estar entre 0 y N-1.

7.4. Tablas de Hash.

Si el número de claves almacenadas en la tabla es pequeño en comparación con el número total


de claves posibles, la estructura tabla de hash resulta una forma eficiente de implementación.

Ejemplos:
Claves alfanuméricas: Con las letras del abecedario se pueden escribir un gran número de
palabras, pero el número de palabras empleadas como identificadores por un programador es
muchísimo menor; y se desea almacenar los identificadores del programador solamente.
Claves enteras: el número de RUTs posibles asciende a varios millones, pero el número de
RUTs de los alumnos de una carrera no sobrepasa los mil; y se desea almacenar solamente los
alumnos de una carrera.

Las tablas de hash se implementan basadas en un arreglo cuyo tamaño debe ser proporcional al
número de claves almacenadas en la tabla.

7.5. Función de hash.

Lo fundamental del método es encontrar una función que a partir de la clave (proveniente de un
universo de N claves posibles), sea ésta numérica o alfanumérica, encuentre un entero sin signo
entre 0 y B-1, si la tabla de hash está formada por B elementos. Con N>> B.

La función de hash muele o desmenuza o hace un picadillo con la clave, de allí el nombre; que
es el significado de la palabra inglesa hash (no es un apellido).

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 3
Para toda clave x, perteneciente al Universo, debe estar definida la función de hash, h(x), con
valores enteros entre 0 y (B-1).

La función h(x) debe distribuir las claves, lo más equitativamente posible, entre los B valores.

Si h( xi ) h( x j ) se dice que hay colisión, y debe disponerse de un método para resolverla.

Si la función de hash es “buena”, habrán pocas colisiones; si hay c colisiones en promedio, las
operaciones resultarán de complejidad O(c), constante; prácticamente independiente de B. Si
todas las claves que se buscan en la tabla, producen colisiones, que es el peor caso, las
operaciones resultarán O(B), esto en caso de tener B claves que colisionen en la misma entrada
de la tabla.

Las funciones de hash son generadores de números pseudo aleatorios.

7.5.1. Funciones de hash para enteros.

Una función bastante simple es dividir la clave por un número primo cercano al número de
baldes, y luego sacar módulo B.

int hash(int clave)


{ return clave%B; } //conviene escoger B como un número primo.

Si B es una potencia de dos, la división se efectuará mediante corrimientos a la derecha, con lo


cual el valor de hash sólo dependerá de los bits más significativos de la clave. Lo cual tenderá a
generar muchas colisiones. Mientras más bits de la clave participen en la formación del valor,
mejor será la función de hash.

También es deseable tener una función de hash que para claves muy parecidas genere valores de
hash con mucha separación, esta propiedad es muy útil en hash lineal. Existe una metodología
denominada clase universal de funciones de hash, en la cual p es un número primo mayor que B,
con a y b números enteros, y B no necesariamente primo.

int Uhash(int clave, int a, int b, int p)


{ return ((a*clave+b)%p)%B; }

Puede demostrarse que con estas funciones la probabilidad de tener colisión entre dos claves
diferentes es menor o igual a 1/B.

Para claves numéricas existen variados procedimientos experimentales. Todos ellos basados en
lograr una mezcla de los números que forman la clave, lo más aleatoria posible, mediante
corrimientos y operaciones lógicas, que son eficientemente traducidos a lenguaje de máquina.

Cambiando las siguientes definiciones de tipos las funciones para claves enteras se pueden
emplear en máquinas de diferente largo de palabra.

Profesor Leopoldo Silva Bijit 11-06-2008


4 Estructuras de Datos y Algoritmos

La siguiente definición de tipos es para máquinas de 16 bits.


typedef unsigned long int u32; //32 bits
typedef unsigned int u16; //16 bits

/* Mezcla los números de la clave, asumiendo enteros de 32 bits.


* Robert Jenkin */
u16 inthash(u32 key)
{
key += (key << 12); key ^= (key >> 22);
key += (key << 4); key ^= (key >> 9);
key += (key << 10); key ^= (key >> 2);
key += (key << 7); key ^= (key >> 12);
return (u16) key%B;
}

7.5.2. Funciones de hash para strings alfanuméricos.

Para claves alfanuméricas puede emplearse la suma de los valores enteros equivalentes de los
caracteres, y aplicando módulo B a la suma total, para entregar un valor de balde válido.

unsigned int h(char *s) /* función simple de hash */


{ int hval;
for (hval =0; *s!='\0';) hval+= *s++;
return (hval % B);
}
Sin embargo no tiene buen comportamiento evaluada experimentalmente.

Una mejor función, también propuesta por Brian Kernighan y Dennis Ritchie:
unsigned int stringhash(char *s)
{ int i;
unsigned int h=0;
for( i=0; *s; s++ ) h = 131*h + *s; /* 31 131 1313 13131 131313 .... */
return (h%B);
}

Robert Sedgwicks en su libro Algorithms in C, propone la siguiente función, que emplea dos
multiplicaciones para generar el valor de hash, y que evaluada experimentalmente, tiene muy
buen comportamiento.

unsigned int RSHash(char* str)


{ unsigned int b = 378551;
unsigned int a = 63689;
unsigned int hash = 0;

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 5
unsigned int i = 0;

for(i = 0; *str; str++, i++)


{ hash = hash * a + (*str);
a = a * b;
}
return (hash%B);
}

La siguiente función propuesta por Serge Vakulenko, genera mediante dos rotaciones y una
resta, por cada carácter del string, el valor aleatorio.

unsigned int ROT13Hash (char *s)


{ unsigned int hash = 0;
unsigned int i = 0;
for (i = 0; *s; s++, i++) {
hash += (unsigned char)(*s);
hash -= (hash << 13) | (hash >> 19);
}
return hash%B;
}

Con la introducción de algoritmos de encriptación, se han desarrollado nuevos métodos para


aleatorizar una clave alfanumérica.

MD5 es un algoritmo de reducción criptográfico diseñado en 1991 por Ronald Rivest del MIT
(Instituto Tecnológico de Masachusets).

Su uso principal es generar una firma digital de 128 bits (fingerprint) a partir de un documento
de largo arbitrario. La cual aplicada a strings podría emplearse como función de hash,
seleccionando algunos de los bits de los 128.

A continuación una breve descripción de este algoritmo.


MD5 es un acrónimo de Message Digest algorithm 5, derivado de versiones anteriores (MD4,
MD3, etc.). La digestión del mensaje debe entenderse como la generación de un resumen de 128
bits de éste. Está basado en el supuesto de que es poco probable que dos mensajes diferentes
tengan la misma firma digital, y menos probable aún producir el mensaje original a partir del
conocimiento de la firma.

Sigue empleándose para verificar si un determinado archivo ha sido modificado, pudiendo ser
éste un correo, imagen, o un programa ejecutable. El originador del documento puede establecer
cuál es su firma digital, y el usuario debería comprobar que su copia tiene la misma huella
digital, regenerándola localmente y comparándola con la de la distribución.

Una función de hash es irreversible, si no existe algoritmo que, ejecutado en un tiempo


razonable, permita recuperar la cadena original a partir de su valor de hash.

Profesor Leopoldo Silva Bijit 11-06-2008


6 Estructuras de Datos y Algoritmos

Si el número de valores a los cuales se les aplica la función de hash son mucho mayores que el
número de valores que produce la función, se producirán colisiones. En un buen algoritmo de
hash, se producirán menores colisiones.

La desincriptación de un valor de salida de la función de hash, consiste en determinar la entrada


que produjo ese valor. Para esto deben generarse valores de entrada y obtener el valor de salida
de la función de hash y compararlo con el conocido. El método, denominado de fuerza bruta
genera todas las combinaciones de las entradas para un largo dado, lo cual puede llevar mucho
tiempo. Otro ataque es conocido como el de diccionario, que consiste en probar las palabras
previamente almacenadas en un diccionario.

7.6. Tipos de tabla.

Se denomina hash abierto o externo o encadenado, a la estructura que resuelve las colisiones
mediante una lista. En éstas el número n, de elementos almacenados en la tabla, podría ser
superior al número B de elementos del arreglo ( n B ).

Se denomina hash cerrado, a las estructuras que almacenan los datos en las mismas casillas del
arreglo; debiendo disponerse de un método para determinar si el elemento del arreglo está vacío,
ocupado o descartado. En éstas el número n de elementos almacenados en la tabla no puede ser
superior al número B de elementos del arreglo ( n B ).

7.6.1. Hash abierto.

7.6.1.1. Diagrama de la estructura.

Se ilustra una tabla de B entradas, que puede considerarse como un arreglo de punteros.

En la figura 7.2: la entrada 1 está vacía; la entrada 0 tiene un elemento; la entrada dos, tiene dos
elementos, y muestra que las colisiones se resuelven mediante una lista. En la estructura de
datos asociada a cada nodo, sólo se ilustra el almacenamiento de la clave.

Se tienen: h(Clave0) = 0; h(ClaveB-1)=B-1; h(Clavei)=h(Clavej) = 2


Índice Tabla
0 Clave0
1
2 Clavei Clavej
….
B-1 ClaveB-1

Figura 7.2 Tabla de hash abierto o de encadenamiento directo.

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 7
7.6.1.2. Declaración de tipos, definición de variables.

typedef struct moldecelda


{
char *nombre;
struct moldecelda *next;
} tcelda, *pcelda;

#define B 10 /* 10 baldes */
static pcelda hashtabla[B]; /*tabla punteros */

La información almacenada en la celda es un string dinámico.

7.6.1.3. Operaciones en hash abierto.

7.6.1.3.1. Crear tabla vacía.


void makenull(void)
{ int i;
for ( i = 0; i < B; i++) hashtabla[i] = NULL;
}
7.6.1.3.2. Función de hash.
La función h(s) a partir de los caracteres que forman el string s genera un número entero sin
signo entre 0 y B-1. Puede usarse alguna de las descritas en 7.5.2.

7.6.1.3.3. Buscar si un string está en la tabla.


Calcula el índice;
Mientras la lista no haya llegado al final:
Si la clave del nodo es igual a la buscada: retorna puntero al nodo.
Si llegó al final: retorna nulo, que implica que no encontró la clave.

pcelda buscar(char *s)


{ pcelda cp; /* current pointer */

for (cp = hashtabla[h(s)]; cp!= NULL; cp = cp->next)


if (strcmp(s, cp->nombre ) == 0) return (cp); /* lo encontró */
return (NULL);
}

7.6.1.3.4. Insertar string en la tabla.


Buscar string en la tabla;
Si no la encuentra:
Crea nodo.
Si no puede crear el nodo, retorna NULL.

Profesor Leopoldo Silva Bijit 11-06-2008


8 Estructuras de Datos y Algoritmos
Si pudo crear nodo.
Crea string dinámico.
Si crea el string. Asocia el string con el nodo.
Si no puede crear el string, retorna NULL.
Ubica el balde para insertar.
Insertar al inicio de la lista.
Retorna puntero al insertado. Operación exitosa.
Si la encontró: Retorna NULL. Es error de inserción en conjuntos.

La siguiente función solicita espacio en el heap, y a través de strcpy copia el valor del
argumento en el espacio recién creado.

char *strsave(char *s) /* K.R. pág. 103 */


{ char *p;
if (( p = malloc(strlen(s) + 1)) != NULL)
strcpy(p, s);
return (p);
}

pcelda insertar(char *s)


{ pcelda cp;
int hval;

if (( cp = buscar(s)) == NULL)
{
cp = (pcelda ) malloc(sizeof (tcelda ));
if (cp == NULL) return (NULL);
if (( cp-> nombre = strsave(s)) == NULL ) return (NULL);
hval = h(cp -> nombre);
cp -> next = hashtabla[hval];
hashtabla[hval] = cp;
return (cp);
}
else return (NULL);
}

7.6.1.3.5. Descartar.
Cálcula índice.
Si no hay elementos ligados al balde, retorna 1.
Si hay elementos:
Busca en el primer elemento, si lo encuentra liga la lista.
Si no es el primer elemento recorre la lista, manteniendo un puntero q al anterior.
Si lo encuentra a partir del segundo: Pega la lista, mediante q;
Si estaba en la lista: Liberar espacio; retorna 0, indicando función realizada.
Si no la encontró: retorna 1, error de descarte.

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 9
int descartar(char *s)
{ pcelda q, cp;
cp = hashtabla[h(s)];
if (cp != NULL ) {
if (strcmp (s, cp-> nombre ) == 0) /* primero de la lista */
hashtabla[h(s)] = cp->next;
else
for (q=cp, cp = cp ->next; cp != NULL; q = cp, cp = cp ->next )
if (strcmp (s, cp->nombre ) == 0)
{ q ->next = cp ->next; break; }
if (cp != NULL )
{free((char *)cp -> nombre);
free ( (pcelda) cp );
return (0);
}
else return (1); //no lo encontró en lista
}
else return (1); //balde vacío
}

7.6.1.4. Análisis de complejidad en hash abierto.

7.6.1.4.1 Caso ideal.


En un caso ideal, la función h produce distribución uniforme.
Si se tienen n elementos en una tabla de B baldes, se define el factor de carga como:

FC = n/B

Las listas resultan de largo promedio n/B.


Las operaciones demandan en promedio: O( 1 + n/B)
Considerando de costo 1, la evaluación de la función de hash; y n/B el recorrer la lista.

Si B es proporcional a n, las operaciones resultan de costo constante.

Llenar una tabla con n items tiene complejidad: O( n(1+n/B) ). Ya que se debe buscar n veces.
7.6.1.4.2. Distribución binomial:
Con el siguiente modelo:

El experimento de Bernouilli es: introducir una clave en uno de B baldes.


Se desea introducir i claves, en el mismo balde, en un conjunto de n experimentos.
Es decir: Se tienen i éxitos en n experimentos Bernouilli con probabilidad 1/B.
i (n i)
1 1
Probabilidad de encontrar lista de largo i = Binomial ( n , i ) 1
B B

Profesor Leopoldo Silva Bijit 11-06-2008


10 Estructuras de Datos y Algoritmos

Para 100 baldes se ilustran los largos promedios de las listas con 90 y 50 elementos en la tabla.
Se aprecia que es baja la probabilidad de encontrar listas de largo mayor que 4.

n=90

n=50

Figura 7.3 Probabilidad de encontrar listas de largo i. Tabla de 100 baldes.

7.6.2. Hash cerrado.

7.6.2.1. Estructura de datos.

La tabla es un arreglo de estructuras. Con un campo para la clave y otro para el estado de la
celda. En este ejemplo se usan claves enteras.

Usamos el tipo enumerativo state, en lugar de códigos numéricos.

typedef enum {vacio, ocupado, descartado} state;

typedef struct moldecelda


{ int clave;
state estado;
} tcelda;

#define B 10 /* 10 celdas */
static tcelda hashtab[B]; /*tabla de estructura */
int n; //ocupados de la tabla

Se agrega la variable global n, para simplificar el diseño de las funciones.

7.6.2.2. Colisiones.

Las colisiones implican una estrategia de rehash.

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 11
Si la celda j asociada a h(x) está ocupada, debe buscarse o insertarse en las siguientes
posiciones:
hi(x) con i = 1, 2, 3,…

Si todas las localizaciones están ocupadas, la tabla está llena, y la operación falla.

7.6.2.3. Hash lineal.

La más simple forma de rehash es colocar los elementos que colisionan en las posiciones
siguientes al valor de hash j, en forma “ascendente”. Se denomina hash lineal (linear probing) al
siguiente conjunto de posiciones:

hi(x) = ( h(x) + i ) % B con i = 1, 2, 3,…

0 j
B-1

j+i

Figura 7.4 Posiciones siguientes para resolver colisiones en forma lineal.

Considerar las posiciones en aritmética módulo B, implica disponer el arreglo de manera


circular, como muestra la Figura 7.4; de esta forma al aumentar i, la nueva posición (j+i) % B
podrá llegar a j, dando la vuelta completa. Si se usa (j+i) solamente, para posiciones mayores
que B-1, se tendrán posiciones inexistentes.

De esta forma se genera una secuencia de elementos que tienen el mismo valor j de hash; estos
elementos no son necesariamente consecutivos, debido a que algunas celdas pueden estar
ocupadas por secuencias producidas por otros valores de hash, tanto anteriores como posteriores
al valor j; también pueden existir elementos que han sido descartados. En la Figura 7.5, se
muestra la secuencia de elementos asociados al valor j de hash, los elementos descartados se
ilustran rellenos de color gris, y los pertenecientes a otros valores de hash se muestran rellenos
de color negro.

Si al inicio se marcan las entradas como vacías, la secuencia de l componentes, iniciada en la


posición j, termina si se encuentra un elemento vacío o si se recorre todo el círculo, según reloj.

Profesor Leopoldo Silva Bijit 11-06-2008


12 Estructuras de Datos y Algoritmos
El número l debe ser igual al número de ocupados en la secuencia, que puede ser menor que el
número total de ocupados n, más el número de descartados.

La búsqueda entonces es de complejidad O(l). El largo de la secuencia l, varía dinámicamente


con las inserciones y descartes, pero siempre debe cumplirse que l es menor o igual que B. El
peor caso es O(B), que se produce si las B claves tienen colisiones para el mismo valor de hash,
sin embargo esto es difícil que se produzca en la práctica; como se verá más adelante el valor
esperado es de complejidad constante.

Si se parte de la posición j, el número k=(j – 1 + B) % B define el final de un recorrido circular,


según reloj, al que se llega después de B intentos.
k j

vacía

Figura 7.5. Secuencia asociada a colisiones con valor j de hash.

Si se desea eliminar un ítem de la tabla no se lo puede marcar como vacío, ya que se asumió que
esta es una condición para el término de la secuencia de colisiones; debido a esto se agrega el
estado descartado.

La búsqueda se detiene cuando se encuentra una posición vacía; pero debe seguir buscando si la
posición está descartada, ya que el descarte puede haber roto una cadena de colisiones, asociada
a la secuencia.

La inserción debe colocar el ítem en el primer elemento vacío o descartado de la secuencia que
comienza en j; sin embargo debe recorrer toda la secuencia asociada al valor de hash j, para
determinar si en ésta se encuentra la clave que se desea insertar, ya que no se aceptan claves
repetidas. Si se encuentra, en la secuencia la clave que se desea insertar, y la celda está ocupada
es un error de inserción. Además no debe insertarse en una tabla llena.

En descarte se recorre la secuencia que comienza en j buscando la clave, si la encuentra y está


marcada como ocupada se la descarta; si no la encuentra se tiene un error en la operación
descarte. También debe considerarse la excepción de no descartar en una tabla vacía.

Entonces en hash lineal, las colisiones se resuelven probando en las siguientes posiciones de la
tabla. Pero existen numerosas maneras de resolver colisiones en una tabla de hash cerrado.

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 13

Si luego de una colisión, el primer elemento que debe seleccionarse para colocar el nuevo ítem
puede escogerse de (B-1) formas, la siguiente posición para colocar la siguiente colisión podrá
escogerse de (B-2) formas; con lo cual pueden tenerse (B-1)! trayectorias posibles dentro de una
tabla. El hash lineal es una de esas formas.

No todas las formas son satisfactorias, ya que algunas pueden producir apilamiento. Esto puede
notarse observando que mientras más larga es una secuencia de colisiones, más probables son
las colisiones con ella cuando se desee agregar nuevos elementos a la tabla. Adicionalmente,
una secuencia larga de colisiones tiene gran probabilidad de colisionar con otras secuencias ya
existentes, lo cual tiende a dispersar aún más la secuencia.

Si sólo se implementa buscar e insertar, las funciones resultan más sencillas, y sólo son
necesarios los estados vacío y ocupado. Debido a las dificultades que genera la operación
descartar en hash cerrado, no suele estar implementada, y si se desea tenerla es preferible
emplear tablas de hash abiertas.

7.6.2.4. Otros métodos de resolver colisiones.

En el hash cuadrático se prueba en incrementos de dos.


j = ( j+ inc + 1) % B; inc+=2;

En hash doble se usa otra función de hash, para calcular el incremento (número entre 0 y B-1),
para obtener la posición del siguiente intento.

En hash aleatorio, se genera un número aleatorio entre 0 y B-1 para obtener el incremento.

En algunos casos, si las claves que pueden almacenarse en la tabla son fijas, puede encontrarse
una función de hash perfecta que no produzca colisiones.

7.6.2.5. Operaciones en tabla de hash cerrado lineal.

7.6.2.5.1. Crear tabla vacía.


void DejarTablaVacia(void)
{ int i;
for ( i = 0; i < B; i++) hashtab[i].estado = vacio;
n=0;
}
7.6.2.5.2. Imprimir item y la tabla. Listador.
void PrtItem(int i)
{
if( i>=0 && i<B )
{ printf( "Clave=%d Estado=", hashtab[i].clave);
if( hashtab[i].estado==vacio) printf("vacio\n");

Profesor Leopoldo Silva Bijit 11-06-2008


14 Estructuras de Datos y Algoritmos
else if (hashtab[i].estado==ocupado) printf("ocupado\n");
else if (hashtab[i].estado==descartado) printf("descartado\n");
else printf("error en estado\n");
}
else printf("Item inválido\n");
}

void PrtTabla(void)
{ int i;
if(n>0)
{ printf("Tabla\n");
for(i=0; i<B; i++) PrtItem(i);
putchar('\n');
}
else printf("Tabla vacía\n");
}

7.6.2.5.3. Buscar.
int buscar(int clave)
{ int i, last;
if (n>0)
{ for(i=hash(clave), last=(i-1+B)%B; i!=last && hashtab[i].estado != vacio; i=(i+1)%B)
{if (hashtab[i].estado == descartado) continue;
else if (hashtab[i].clave == clave) break; //sólo compara clave si está ocupado
}
if (hashtab[i].clave == clave && hashtab[i].estado == ocupado ) return (i);
else { printf("Error en búsqueda: No encontró %d\n", clave); return (-1);}
}
else { printf("Error en búsqueda de clave %d: Tabla vacía\n", clave); return (-2);}
}
7.6.2.5.4. Insertar.
int insertar(int clave)
{ int i, last, pd=-1; //en pd se almacena posición de primer descartado.
//Al inicio esa posición no existe.
if (n<B)
{
for(i=hash(clave),last=(i-1+B) % B; i!=last && hashtab[i].estado != vacio; i=(i+1)% B)
{if (hashtab[i].estado == descartado) {if(pd == -1) pd=i; continue;}
else if (hashtab[i].clave == clave) break; //sólo compara clave si está ocupado
}
if ( hashtab[i].clave == clave && hashtab[i].estado == ocupado )
{ printf("Error en inserción: Clave %d ya estaba en la tabla\n",clave);
return(1);
}
else

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 15
{ if (pd != -1) i=pd; //i apunta al primer descartado
hashtab[i].clave = clave; hashtab[i].estado = ocupado; n++;
return(0);
}
}
else {printf("Error en inserción de clave %d. Tabla llena\n", clave); return(2);}
}
7.6.2.5.5. Descartar.
int descartar(int clave)
{ int i, last;
if(n!=0)
{
for(i=hash(clave),last=(i-1+B) % B; i!=last && hashtab[i].estado != vacio; i=(i+1)% B)
{if (hashtab[i].estado == descartado) continue;
else if (hashtab[i].clave == clave) break; //sólo compara clave si está ocupado
}
if (hashtab[i].clave == clave && hashtab[i].estado == ocupado )
{ hashtab[i].estado=descartado; n--; return (0);}
else
{printf("Error en descarte: No se encuentra activa la clave=%d\n",clave); return (1);}
}
else { printf("Error en descarte de clave %d: Tabla vacía\n", clave); return (2);}
}

7.7. Análisis de complejidad en hash cerrado.

Se asume que todas las claves posibles son igualmente probables, y que la función de hash las
distribuye uniformemente en los baldes.

7.7.1. En inserción:

Se desea calcular Ek+1, el valor esperado promedio de los intentos necesarios para insertar una
clave en una tabla de n posiciones, cuando ya hay k posiciones ocupadas.

La probabilidad de encontrar un balde disponible en el primer intento de inserción es:

baldes disponibles primera vez n k


p1
total de baldes primera vez n
La probabilidad de que exactamente se requieran dos intentos, es el producto de tener colisión la
primera vez y la de encontrar una posición libre en el segundo intento.

disponibles segunda vez


p2 colisión primera vez *
total segunda vez

Profesor Leopoldo Silva Bijit 11-06-2008


16 Estructuras de Datos y Algoritmos
k (n 1) (k 1) k n k
p2 * *
n (n 1) n n 1

En la segunda vez hay (n-1) casillas disponibles, de las cuales pueden haber (k-1) ocupadas ya
que se está seguro de que la primera estaba ocupada.

Tener colisión en el segundo intento se produce con probabilidad:


k 1
n 1

La probabilidad de encontrar balde disponible en el tercer intento, puede anotarse:


n k
n 2
Entonces:
k k 1 n k
p3 * *
n n 1 n 2
Nótese que el denominador de la última fracción siempre será (n-k).

Finalmente la inserción, después de i intentos:

k 0 k 1 k 2 k i 2 n k
pi * * ..... * *
n 0 n 1 n 2 n i 2 n i 1

El numerador de la probabilidad de la tercera colisión es k-(3-1). Entonces el denominador de la


probabilidad de que la última colisión sea en el intento (i-1), es k-( (i-1)-1 ).
El denominador de la probabilidad de encontrar un balde disponible en el tercer intento es
n-(3-1). Entonces el denominador de la probabilidad de encontrar un balde disponible en el
intento i, es n-(i-1).

El número esperado de intentos requeridos para insertar una clave, cuando ya hay k en una tabla
de n posiciones es:
k 1
Ek 1 i pi
i 1
También es el número de intentos esperado para insertar la clave (k+1).

La sumatoria anterior es compleja de calcular, pero puede resolverse inductivamente:


Poner el primero cuando la tabla está vacía; es decir k=0, puede anotarse:
n 0
E0 1 1* p1 1* 1
n
Poner el segundo, cuando ya hay uno en la tabla; es decir con k=1:

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 17
n 1 1 n 1 n 1
E1 1 1* p1 2 * p2 1* 2* *
n n n 1 n 1 1
Poner el tercero, cuando ya hay dos, con k=2:

n 2 2 n 2 2 2 1 n 2
E2 1 1* p1 2 * p2 3 * p3 1* 2* * 3* * *
n n n 1 n n 1 n 2
n 1
E2 1
n 2 1
De lo anterior puede inducirse, que el número esperado de intentos requeridos para insertar una
clave, cuando ya hay k en una tabla de n posiciones es:
n 1
Ek 1
n k 1

7.7.2. En búsqueda:

Se desea calcular EB, el número de intentos necesarios para accesar una clave cualquiera en una
tabla en la que hay m elementos.

Podemos aprovechar el resultado anterior, observando que el número de intentos para buscar un
item es igual al número de intentos para insertarlo.

Entonces podemos plantear que:

EB = número promedio de intentos para insertar m elementos en la tabla.

De antes teníamos que para insertar el primer elemento se requerían E1 intentos. Para insertar el
segundo se requieren E2 intentos, y así sucesivamente hasta que para insertar el m avo elemento
se requieren Em intentos.

Entonces podemos calcular EB como el promedio:

E1 E2 ... Em 1 m
EB * Ek
m m k1
Reemplazando los valores de Ek, se obtiene:

n 1 1 1 1 1 1
EB ( ... )
m n 1 n n 1 n 2 n m 2

Para evaluar la sumatoria anterior, puede emplearse la siguiente sumatoria, conocida como la
función armónica, ya que para ella se conoce, en forma aproximada su suma:

Profesor Leopoldo Silva Bijit 11-06-2008


18 Estructuras de Datos y Algoritmos
1 1 1 1 1
Hn ... ln(n)
1 2 3 4 n
Tenemos que:
1 1 1 1 1
Hn m 1 ...
1 2 3 4 n m 1

Con = 0,577216.. denominada constante de Euler.


Formando Hn+1 y restando el término anterior, en la expresión para EB, obtenemos:

n 1 n 1
EB (H n 1 Hn m 1 ) (ln(n 1) ln(n m 1))
m m
Agrupando las funciones logaritmo y definiendo el factor de carga como:

m
n 1
Se obtiene:
n 1 n 1 ln(1 )
EB ln
m n 1 m

Tabla vacía implica =0; tabla llena implica =n/(n+1) 1

EB

Figura 7.6 Intentos versus Factor de carga. Hash cerrado.

Para tabla casi llena, con =99 % se tiene una búsqueda en menos de 5 intentos. Puede decirse
que en promedio se requieren 3 ó 4 intentos para buscar un elemento, con tabla casi llena.

7.8. Ventajas y desventajas.

La tabla de hash es mejor que los árboles binarios de búsqueda en inserción y búsqueda.

Sin embargo tiene las limitaciones de los métodos estáticos, ya que el tamaño de la tabla debe
ser estimada a priori; además la complejidad aumenta si se implementa la operación descarte.

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 19
Si el volumen de los datos cambia dinámicamente, creciendo o disminuyendo hasta límites no
previsibles, se suelen emplear árboles de búsqueda.

Problemas resueltos.

P7.1.

Para una tabla de hash cerrado de 10 posiciones, se tienen las siguientes funciones:
void imprime(void)
{ int i;
for(i=0; i<B; i++)
{
if (hashtab[i].estado==vacio) printf("%2d v ", hashtab[i].clave);
else if (hashtab[i].estado==descartado) printf("%2d d ", hashtab[i].clave);
else if (hashtab[i].estado==ocupado) printf("%2d o ", hashtab[i].clave);
else printf("%2d e ", hashtab[i].clave);
}
putchar('\n');
}

int hash(int key)


{ return key%B; }

Determinar que escribe el programa:


makenull();
insertar(5); insertar(6); insertar(7); imprime();
insertar(15); insertar(16); insertar(17); imprime();
descartar(6); insertar(25); descartar(7); imprime();
printf("%d e\n", buscar(15));
insertar(14); insertar(4); insertar(24); imprime();
Puede efectuar un diagrama con los datos.

Solución.
Un diagrama con los valores y estado del arreglo, a medida que se ejecutan las instrucciones:

Índice 1 2 3 4 5 6
0 vacío vacío 17 ocupado 17 ocupado 17 ocupado 17 ocupado
1 vacío vacío vacío vacío vacío 24 ocupado
2 vacío vacío vacío vacío vacío vacío
3 vacío vacío vacío vacío vacío vacío
4 vacío vacío vacío vacío vacío 14 ocupado
5 vacío 5 ocupado 5 ocupado 5 ocupado 5 ocupado 5 ocupado
6 vacío 6 ocupado 6 ocupado 6 descartado 25 ocupado 25 ocupado
7 vacío 7 ocupado 7 ocupado 7 ocupado 7 descartado 4 ocupado
8 vacío vacío 15 ocupado 15 ocupado 15 ocupado 15 ocupado
9 vacío vacío 16 ocupado 16 ocupado 16 ocupado 16 ocupado

Profesor Leopoldo Silva Bijit 11-06-2008


20 Estructuras de Datos y Algoritmos

Columna 1: Después de makenull


Columna 2: Después de insertar el 5, 6 y 7.
Columna 3: después de insertar el 15, 16 y 17.
Columna 4: después de descartar el 6.
Columna 5: después de insertar el 25 y descartar el 7.
Columna 6: después de insertar el 14, el 4 y el 24.

Para la parte de resultados impresos:

Algunas observaciones sobre la rutina imprime:


La línea: printf("%2d e ", hashtab[i].clave); no se ejecuta nunca.
Se ha comentado la impresión de la clave, en caso de estar la celda vacía, ya que makenull, no
inicializa el campo clave. En lugar de imprimir la clave, se anota el índice de la celda vacía.

El programa imprimiría:
0v 1v 2v 3v 4v 5o 6o 7o 8v 9v
17 o 1 v 2 v 3 v 4 v 5 o 6 o 7 o 15 o 16 o
17 o 1 v 2 v 3 v 4 v 5 o 25 o 7 d 15 o 16 o
8e
17 o 24 o 2 v 3 v 14 o 5 o 25 o 4 o 15 o 16 o

P7.2.

Para una tabla de hash cerrado de tamaño 9, se tiene la siguiente función de hash:
h(x) = (7x + 1) % 9;

Considere la siguiente secuencia de operaciones:


Insertar 13
Insertar 20
Insertar 4
Insertar 8
Descartar 4
Buscar 8
Insertar 16
Indice Valor Estado
0
1
2
3
4
5
6
7
8

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 21
a) Muestre el contenido de la tabla después de realizadas las operaciones anteriores, asumiendo
linear probing para resolver colisiones. Indicando cuándo se producen colisiones y la razón por
la que se almacena en una posición determinada.

b) Luego de lo anterior, indicar fundamentadamente qué casos de inserciones o búsquedas


tienen mayor costo, indicando el número de comparaciones que son necesarias.

Solución.

(13*7+1) % 9 = 2, (20*7+1) % 9 = 6, (4*7+1) % 9 = 2, (8*7+1) % 9 = 3,


(16*7+1) % 9 = 5,

Se inserta el 13 en posición 2 de la tabla, ya que estaba vacía. El 20 en la posición 6, que estaba


vacía.

Al insertar el 4 se produce colisión en la entrada 2,de la tabla, se almacena en la casilla


siguiente. Al insertar el 8 se produce colisión en la entrada 3, de la tabla, se almacena en la
casilla siguiente.
Indice Valor Estado
0 vacío
1 vacío
2 13 ocupado
3 4 ocupado
4 8 ocupado
5 vacío
6 20 ocupado
7 vacío
8 vacío
Busca primero el 4 en la posición 2, como está ocupada, prueba en la siguiente, lo encuentra y
lo descarta. La tabla queda:
Indice Valor Estado
0 vacío
1 vacío
2 13 ocupado
3 4 descartado
4 8 ocupado
5 vacío
6 20 ocupado
7 vacío
8 vacío
Al buscar el 8, como la entrada 3 de la tabla no está vacía, se busca en la siguiente y encuentra
el valor en la casilla siguiente.

Luego de esto se inserta el 16, en la posición 5, la tabla queda:

Profesor Leopoldo Silva Bijit 11-06-2008


22 Estructuras de Datos y Algoritmos

Indice Valor Estado


0 vacío
1 vacío
2 13 ocupado
3 4 descartado
4 8 ocupado
5 16 ocupado
6 20 ocupado
7 vacío
8 vacío
b) Buscar un valor tal que el retorno de la función de hash tenga valor 2, y que su valor sea
diferente de los valores no descartados u ocupados, implica 6 comparaciones, ya que la
búsqueda se detiene al encontrar una casilla vacía.
La inserción de un valor tal que el retorno de la función de hash tenga valor 2, implica 2
comparaciones, ya que la búsqueda para insertar se detiene al encontrar una casilla descartada o
vacía.

Buscar un valor tal que el retorno de la función de hash tenga valor 3, implica 5 comparaciones,
ya que la búsqueda se detiene al encontrar una casilla vacía.
La inserción de un valor tal que el retorno de la función de hash tenga valor 3, implica 1
comparación, ya que la búsqueda se detiene al encontrar una casilla descartada o vacía.

Buscar un valor tal que el retorno de la función de hash tenga valor 4, implica 4 comparaciones,
ya que la búsqueda se detiene al encontrar una casilla vacía.
La inserción de un valor tal que el retorno de la función de hash tenga valor 4, implica 4
comparaciones, ya que la búsqueda se detiene al encontrar una casilla descartada o vacía.

Entonces:
La búsqueda de un valor tal que el retorno de la función de hash tenga valor 2 es la que tiene
mayor costo en comparaciones, requiere seis comparaciones.
La inserción de un valor tal que el retorno de la función de hash tenga valor 4 es la que tiene
mayor costo en comparaciones, requiere cuatro comparaciones.

Ejercicios propuestos.

E7.1.

Se desea reemplazar una tabla de hash abierta de B1 baldes, que tiene bastante más que B1
elementos, por otra tabla de hash de B2 baldes. Describir la construcción de la nueva tabla a
partir de la vieja.

E7.2. Hash Cerrado.

a) El descarte de registros produce contaminación y disminuye la eficiencia de la búsqueda en


una tabla de hash cerrada. ¿Por qué?

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 23
b) Para mejorar la eficiencia se propone mantener un contador del largo de la secuencia de
prueba más larga (csml).
Modifique los algoritmos, vistos en clases para insertar y buscar, para mejorar la búsqueda con
csml. ¿Dónde se mantiene el contador?

E7.3. Para la funciones definidas en 7.6.2.5. Determinar las impresiones.

//Test de las funciones


void main(void)
{ DejarTablaVacia(); PrtTabla(); PrtItem(buscar(5)); descartar(5); insertar(5);
PrtItem(buscar(5)); PrtItem(buscar(6)); insertar(5); descartar(6); insertar(6);
PrtItem(buscar(6)); insertar(7); PrtTabla();

DejarTablaVacia();
insertar(6); insertar(7); insertar(16); descartar(7); insertar(5); insertar(26); PrtTabla();

DejarTablaVacia();
insertar(5); insertar(6); insertar(7); insertar(15); descartar(6); descartar(7); insertar(25);
insertar(26); PrtTabla();
}

P7.4. Hash abierto y cerrado.

Diseñar funciones para una tabla de hash abierto que resuelve las colisiones empleando una
tabla de hash cerrado. Ver 11.5 en texto de Cormen.

Referencias.

Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975.


Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. “Introduction
to Algorithms”, Second Edition. MIT Press and McGraw-Hill, 2001.

Profesor Leopoldo Silva Bijit 11-06-2008


24 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 7 ..............................................................................................................................................1
TABLAS DE HASH. ..................................................................................................................................1
7.1. OPERACIONES. ...................................................................................................................................1
7.2. CLAVE. ...............................................................................................................................................1
7.3. TABLA DE ACCESO DIRECTO. ..............................................................................................................1
7.4. TABLAS DE HASH. ..............................................................................................................................2
7.5. FUNCIÓN DE HASH. .............................................................................................................................2
7.5.1. Funciones de hash para enteros. ...............................................................................................3
7.5.2. Funciones de hash para strings alfanuméricos. .........................................................................4
7.6. TIPOS DE TABLA. ................................................................................................................................6
7.6.1. HASH ABIERTO. ...............................................................................................................................6
7.6.1.1. Diagrama de la estructura. .....................................................................................................6
7.6.1.2. Declaración de tipos, definición de variables. ........................................................................7
7.6.1.3. Operaciones en hash abierto. .................................................................................................7
7.6.1.3.1. Crear tabla vacía. .............................................................................................................................. 7
7.6.1.3.2. Función de hash................................................................................................................................ 7
7.6.1.3.3. Buscar si un string está en la tabla.................................................................................................... 7
7.6.1.3.4. Insertar string en la tabla. ................................................................................................................. 7
7.6.1.3.5. Descartar. ......................................................................................................................................... 8
7.6.1.4. Análisis de complejidad en hash abierto. ................................................................................9
7.6.1.4.1 Caso ideal. ......................................................................................................................................... 9
7.6.1.4.2. Distribución binomial: ...................................................................................................................... 9
7.6.2. HASH CERRADO. ............................................................................................................................10
7.6.2.1. Estructura de datos. ..............................................................................................................10
7.6.2.2. Colisiones. .............................................................................................................................10
7.6.2.3. Hash lineal. ...........................................................................................................................11
7.6.2.4. Otros métodos de resolver colisiones. ...................................................................................13
7.6.2.5. Operaciones en tabla de hash cerrado lineal. ......................................................................13
7.6.2.5.1. Crear tabla vacía. ............................................................................................................................ 13
7.6.2.5.2. Imprimir item y la tabla. Listador................................................................................................... 13
7.6.2.5.3. Buscar. ........................................................................................................................................... 14
7.6.2.5.4. Insertar. .......................................................................................................................................... 14
7.6.2.5.5. Descartar. ....................................................................................................................................... 15
7.7. ANÁLISIS DE COMPLEJIDAD EN HASH CERRADO................................................................................15
7.7.1. En inserción: ............................................................................................................................15
7.7.2. En búsqueda: ...........................................................................................................................17
7.8. VENTAJAS Y DESVENTAJAS. .............................................................................................................18
PROBLEMAS RESUELTOS. ........................................................................................................................19
P7.1. ...................................................................................................................................................19
P7.2. ...................................................................................................................................................20
EJERCICIOS PROPUESTOS. ........................................................................................................................22
E7.1. ...................................................................................................................................................22
E7.2. Hash Cerrado. ..........................................................................................................................22
E7.3. Para la funciones definidas en 7.6.2.5. Determinar las impresiones. ......................................23
P7.4. Hash abierto y cerrado. ............................................................................................................23
REFERENCIAS. .........................................................................................................................................23

Profesor Leopoldo Silva Bijit 11-06-2008


Tablas de hash 25
ÍNDICE GENERAL. ................................................................................................................................... 24
ÍNDICE DE FIGURAS................................................................................................................................. 25

Índice de figuras.

FIGURA 7.1 TABLA DE ACCESO DIRECTO....................................................................................................... 1


FIGURA 7.2 TABLA DE HASH ABIERTO O DE ENCADENAMIENTO DIRECTO. .................................................... 6
FIGURA 7.3 PROBABILIDAD DE ENCONTRAR LISTAS DE LARGO I. TABLA DE 100 BALDES. .......................... 10
FIGURA 7.4 POSICIONES SIGUIENTES PARA RESOLVER COLISIONES EN FORMA LINEAL. .............................. 11
FIGURA 7.5. SECUENCIA ASOCIADA A COLISIONES CON VALOR J DE HASH. ................................................. 12
FIGURA 7.6 INTENTOS VERSUS FACTOR DE CARGA. HASH CERRADO. ......................................................... 18

Profesor Leopoldo Silva Bijit 11-06-2008


1

Capítulo 8

Colas de prioridad.

Se desea disponer de una estructura de datos y encontrar los algoritmos asociados que sean
eficientes para seleccionar un elemento de un grupo.

Uno de los valores de la información agrupada en la estructura se denomina prioridad. Es un


valor entero, y por tradición, el menor valor entero está asociado a la estructura que tiene mayor
prioridad. Prioridad se entiende como sinónimo de lo más importante. Puede haber varias
estructuras con igual prioridad; en este sentido no son conjuntos.

8.1. Operaciones.

Se definen dos operaciones: Insertar un elemento en el conjunto, y otra que busca y descarta
(selecciona) el elemento con valor menor del campo prioridad.

Es necesario encontrar una nueva estructura, ya que las vistas anteriormente tienen limitaciones.
La tabla de hash permite insertar con costo constante, pero encontrar y descartar el mínimo tiene
alta complejidad.

Una lista ordenada en forma ascendente por la prioridad, permite seleccionar el mínimo con
costo O(1). Pero la inserción, manteniendo en orden tiene costo promedio O(n); si encuentra el
lugar para insertar a la primera es costo 1; pero si debe recorrer toda la lista, debe efectuar n
comparaciones; en promedio debe efectuar un recorrido de n/2 pasos.

Una lista no ordenada, tiene costo de inserción O(1), y búsqueda O(n).


En ambos casos la repetición de n operaciones sobre la estructura da origen a complejidad n2.

Estudiaremos usar la estructura de un árbol binario, ya que ésta garantiza que las operaciones de
inserción y descarte sean de complejidad O( log2 (n) ), pero deberemos efectuar modificaciones
ya que en una cola de prioridad se permiten claves duplicadas.

8.2. Relaciones entre el número de nodos y la altura en árboles binarios.

La altura es el número de nodos de una trayectoria desde la raíz hasta las hojas, incluyendo
ambos.

Los siguientes diagramas ilustran árboles binarios balaceados completos, con todas las hojas en
el nivel más bajo.

Profesor Leopoldo Silva Bijit 30-06-2008


2 Estructuras de Datos y Algoritmos
En un primer caso se tiene que el número de nodos n es tres; un nivel (m elementos entre los
nodos en una trayectoria desde la raíz a las hojas) y altura h igual dos.
Con un nivel:
n=3 m=1 h= 2

Con dos niveles:


n=7=23-1 m=2 h=3

Con tres niveles:


n=15=24-1 m=3 h=4

Figura 8.1. Árboles binarios completos.

En un caso general:
n = 2h -1, h = m +1 y h=log2 (n+1), despejando h de la primera relación.

La altura, es el concepto importante para la complejidad, ya que define el número de nodos a


revisar en una trayectoria desde la raíz hasta las hojas.

Se estudian a continuación las relaciones para árboles incompletos, pero tal que las hojas que
faltan se encuentran en el último nivel y siempre a la derecha de las presentes.

Se tienen para árboles de altura tres, los casos con n=4, 5, y 6.


Es decir para n >= 23-1 y n < 23-1

Figura 8.2. Árboles binarios incompletos.

Para árboles incompletos de altura cuatro se tienen árboles con nodos entre 8 y 9. Es decir para
n >= 24-1 y n < 24-1.

Para árboles incompletos de altura h se tienen árboles con nodos en el intervalo:

Profesor Leopoldo Silva Bijit 30-06-2008


Colas de prioridad. Heap. 3

2h 1
n 2h 1

Lo que implica: log2 (n+1) < h <= log2 (n) + 1

Se puede encontrar un n0 y un c tal que c*log2 (n+1) sea una cota superior para h.

Entonces:
h = Parte entera( log2 (n+1) ) = O( log2 (n) )

1,03log2 (n)

n0
h(n)

log2 (n)

Figura 8.3. Altura árbol binario incompleto.

8.3. Características de una posible estructura.

Si escogemos un árbol binario parcialmente completo, lo más balanceado posible; que podemos
definir como un árbol que en su nivel más bajo (cerca de las hojas) cumple con la condición de
que las hojas que le faltan están a la derecha de las presentes.

Además debemos definirlo como parcialmente ordenado, ya que es posible tener elementos
repetidos. Esta condición la logramos estableciendo, que los nodos queden ordenados según:

valor (hijo) >= valor (padre)

Estas definiciones, determinan que en la raíz se encuentra el nodo con valor mínimo; y que la
posición para insertar queda definida como la hoja de menor nivel que falta.

mínimo

Posición
para
insertar

Figura 8.4. Árbol binario parcialmente ordenado.

Profesor Leopoldo Silva Bijit 30-06-2008


4 Estructuras de Datos y Algoritmos
Si el elemento a insertar tiene un valor menor que su padre, debe ascender por intercambio.
Si se descarta la raíz, para mantener la estructura, se reemplaza la posición vacante en la raíz por
la hoja ubicada más a la derecha, y si ésta es mayor que los hijos se la hace descender por
intercambio. Ambas operaciones tienen complejidad O(a), donde a es la altura del árbol.

Esta estructura se denomina heap. Que equivale, en español, a grupo de cosas unas al lado de
otras (montón, colección).

8.4. Descripción de la estructura heap.

Se visualiza como un árbol binario, pero el valor de cualquier nodo es menor o igual a los
valores de los nodos hijos. La Figura 8.5, muestra 12 valores almacenados en un heap, todos los
nodos cumplen la propiedad.
Se agregan nodos primero en el sub-árbol izquierdo, y luego en el derecho, manteniendo un
árbol binario lo más balanceado posible.
12

20 16

29 20 18 21

35 38 24 25 20 last
Figura 8.5. Árbol binario con relación de orden tipo heap.

De esta forma el nodo de mayor prioridad (el de menor valor) se encuentra ubicado en la raíz.

La siguiente idea, además de ser una genialidad, es el concepto fundamental del heap.

La estructura anterior puede almacenarse en un arreglo.

La clave de la raíz se almacena en el primer elemento del arreglo, con índice 1 (no se usa el 0);
luego se almacenan los nodos de primer nivel (de izquierda a derecha); luego los de segundo
nivel (siempre de izquierda a derecha) y así sucesivamente; el último presente debe marcarse
con el cursor last:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
12 20 16 29 20 18 21 35 38 24 25 20

last max

Figura 8.6. Arreglo con relación de orden tipo heap.

Se denomina cursor a una variable que almacena un índice de un arreglo, para diferenciarla de
un puntero que almacena una dirección.

Profesor Leopoldo Silva Bijit 30-06-2008


Colas de prioridad. Heap. 5
Al almacenar en un arreglo se fija el número máximo de elementos que esperan en la cola de
prioridad, este valor es max en la figura 8.6. Esto es común a toda estructura estática.

De este modo:
el hijo izquierdo del nodo i tiene el índice 2*i.
el hijo derecho del nodo i, si existe, tiene el índice: 2*i + 1
el padre del nodo i, tiene el índice: i/2
el nodo no existe si: (i < 1) o (i > last)
La estructura anterior se denomina heap.

Si la raíz tiene índice 1, sus hijos tienen índices 2 y 3 respectivamente; si se hubiera escogido la
raíz con índice 0, tendríamos que tratar en forma excepcional a los hijos de la raíz; del mismo
modo esta elección permite determinar que el nodo raíz no tiene padre, ya que 1/2, en división
entera resulta con valor cero.

Existen algoritmos eficientes para agregar un nodo, manteniendo la propiedad del heap; y para
extraer la raíz, y reordenar los elementos de tal modo que se mantenga la propiedad de ser un
heap.

8.5. Complejidad.

En el caso del heap, como veremos a continuación, la inserción tiene complejidad log2(n); y la
extracción del mínimo, manteniendo el heap, también es log2(n).

Si se realizan n operaciones en un heap éstas tienen un costo: nlog2(n). Como veremos más
adelante se requieren realizar n operaciones para formar un heap a partir de un arreglo
desordenado, mostrando de este modo que el heap puede emplearse para desarrollar un eficiente
algoritmo de ordenamiento.

Cuando se busca: se tiene el valor a buscar; la operación de seleccionar es escoger el nodo que
tiene determinada propiedad, en este caso el de menor valor de prioridad. La selección retorna el
valor buscado.

8.5.1. La operación de inserción.

Se ilustra la inserción de un nodo con valor 13, respecto a la Figura 8.5.

Profesor Leopoldo Silva Bijit 30-06-2008


6 Estructuras de Datos y Algoritmos

12

20 16

29 20 18 21

35 38 24 25 20 13

Figura 8.7. Inserción de nodo con valor 13 en un heap que tenía 12 elementos.

Se agrega al final, y luego se lo hace ascender, manteniendo la propiedad del heap: El valor de
cualquier nodo es menor o igual a los valores de los nodos hijos.

El algoritmo se denomina sift-up (filtrar, separar, examinar).


El número de comparaciones es el del número de nodos desde una hoja hasta la raíz en un árbol
binario, lo más balanceado posible (este número es log(n) ).

Solo es necesario comparar el valor del nodo con el valor del padre del nodo, ya que el hermano
tiene un valor mayor o igual que el del padre. Se intercambia el nodo con valor 18 y el recién
insertado. Aún no se cumple la propiedad de ser un heap.

12

20 16

29 20 13 21

35 38 24 25 20 18

Figura 8.8. Ascenso del nodo con valor 13 por intercambio con el padre.

Debe continuarse la revisión, en forma ascendente hacia la raíz, para que se mantenga la
propiedad del heap. Y debe intercambiarse el nodo de valor 13, con su padre, el nodo 16.

Profesor Leopoldo Silva Bijit 30-06-2008


Colas de prioridad. Heap. 7

12

20 13

29 20 16 21

35 38 24 25 20 18

Figura 8.9. Sigue ascendiendo el nodo con valor 13. Queda un heap de 13 elementos.

Comparando con la raíz, se advierte que el recién insertado no debe intercambiarse. Luego de
estas operaciones, se mantiene la propiedad del heap.

8.5.2. Declaraciones de estructuras de datos y variables.

Para describir el algoritmo en lenguaje C, es preciso efectuar algunas definiciones. Definiremos


un tipo de datos denominado registro, que contenga el valor de prioridad y agregaremos otro
valor (ntarea) para ejemplificar que dicho registro contiene la información asociada a una tarea.
También se define el tipo puntero a registro con el nombre: preg.

typedef struct nn1{


int prioridad;
int ntarea;
} registro, *preg;

Luego definiremos un tipo de datos denominado heap, como un arreglo de registros de 10


elementos (el elemento r[0] puede emplearse como registro temporal, ya que en C, los arreglos
parten desde cero, y en este caso nos interesa usar desde el 1 en adelante), mediante:

#define Ntareas 10
typedef registro heap[Ntareas+1];

heap r; /* r es arreglo de registros */


int last=0; /* Apunta al último */

Definiremos un registro, al cual se apunta con nuevo, para mostrar la inserción de un nuevo
elemento al heap.

registro Tarea; /*se asume un registro iniciado con valores */


preg nuevo=&Tarea;

Profesor Leopoldo Silva Bijit 30-06-2008


8 Estructuras de Datos y Algoritmos
8.5.3. Inserción.

La función insertar se implementa en términos de la función siftup, descrita en 8.5.1.


//inserta en posición n-ésima y lo hace ascender
void siftup(int n)
{ int i, j;
registro temp;
for (j=n; j>1; j=i){ /*Al inicio j apunta al último ( n ) */
i=(j>>1) ; /* i= j/2 Con i el cursor del padre de j*/
if ( r[i].prioridad <= r[j].prioridad ) break; // Sale del for si es un heap.
/* invariante: heap(1, n) excepto entre j y su padre i */
temp=r[j], r[j]= r[i], r[i]=temp; /* intercambio sólo si el hijo es menor que el padre*/
/* La condición de reinicio hace que j apunte a su padre. j=i; */
}
}

Se lo inserta en la última posición y se lo hace ascender en log(last) comparaciones.

void insert(preg nuevo)


{ last++;
r[last]=*nuevo;
/* Antes se tenía un heap(1, last-1) */
siftup(last);
/* Luego de sift-up se tiene un heap(1, last) */
}

8.5.4. Extracción del mínimo.

Para obtener el elemento de mayor prioridad, basta tomar la raíz.


Una vez extraída la raíz la precondición es: heap(2, n) && n >= 0. Es decir los elementos desde
el 2 hasta el n, están en la relación de orden de un heap.

El algoritmo consiste en colocar el elemento apuntado por last en el lugar de la raíz, y hacer
descender este nodo, de tal modo de mantener la propiedad de un heap. Al final la postcondición
es: heap(1, n).

A partir de la Figura 8.5., se tiene, después de sacar el nodo con valor 12, y mover el último al
lugar de la raíz y decrementar last:

Profesor Leopoldo Silva Bijit 30-06-2008


Colas de prioridad. Heap. 9

12
20

20 16

29 20 18 21

35 38 24 25 last

Figura 8.10. Se extrajo la raíz, se movió el último a la raíz, se cambia last.

Se busca el hijo menor de la raíz y se lo intercambia con el padre, si el hijo tiene un valor menor
que el padre.
16

20 20

29 20 18 21

35 38 24 25 last

Figura 8.11. Descenso del más pesado. Sift-down.

Es necesario comparar la raíz con los dos hijos, esto en el caso de que ambos existan, y efectuar
el intercambio con el hijo con clave menor.
Aún es necesario seguir intercambiando, para mantener propiedad de heap, en la Figura 8.11, se
intercambia ahora el nodo con valor 20, con su hijo izquierdo, que tiene valor menor. El
recorrido se efectúa en log (n) pasos.

8.5.5. Descenso en el heap.

Basada en while.

void siftdown(int n)
{ int i=1, j;
registro temp; /* al inicio i apunta a la raíz; j al hijo izquierdo */
while (( j=(i<<1)) <= n ) /* j = 2*i */
{ if ( ( (j+1)<=n ) && ( r[j+1].prioridad < r[j].prioridad)) j++;
/*Si existe hijo derecho y es menor que el hijo izquierdo, j apunta al derecho */

Profesor Leopoldo Silva Bijit 30-06-2008


10 Estructuras de Datos y Algoritmos
if (r[i].prioridad <= r[j].prioridad) break;
temp=r[j], r[j]= r[i], r[i]=temp; /*Intercambia si el hijo es menor que el padre */
i=j; /* Nueva raíz en el descenso */
}
}

Basada en for:

void siftdown(int n)
{ int i, j;
registro temp; /* al inicio i apunta a la raíz; j al hijo izquierdo */
for(i=1, j=i<<1; j<=n; i=j, j=i<<1) /* j = 2*i */
{ if ( ( (j+1)<=n ) && ( r[j+1].prioridad < r[j].prioridad)) j++;
/*Si existe hijo derecho y es menor que el hijo izquierdo, j apunta al derecho */
if (r[i].prioridad <= r[j].prioridad) break;
temp=r[j], r[j]= r[i], r[i]=temp; /*Intercambia si el hijo es menor que el padre */
/* La condición de reinicio apunta a nueva raíz en el descenso: i=j; */
}
}

8.5.6. Extracción del mínimo.

Si asumimos que la raíz se copia en el registro Tarea, la extracción del mínimo se puede
escribir:

int extraemin(void)
{
if (last>=1)
{ Tarea = r[1] ; /*salvar raiz*/
printf(" %d ", Tarea.ntarea); //Hace algo con la tarea
r[1]=r[last--]; //Mueve el ultimo a la posición de la raíz. Decrementa last.
siftdown(last); //Lo hace descender, manteniendo propiedad de heap.
return(0);
}
else return(1); /*error intento extracción en heap vacio*/
}

8.6. Prueba de las funciones.

El printf, en extraemin, se ha colocado para revisar el algoritmo. Va mostrando el orden en que


se extraen las tareas del heap.
Las siguientes rutinas ilustran un test para las funciones. Llenecola le da valores al heap,
mediante insert.

void llenecola(void)
{ int i;
for(i=1; i< Ntareas+1; i++)

Profesor Leopoldo Silva Bijit 30-06-2008


Colas de prioridad. Heap. 11
{ /*se prueban con igual prioridad. Deben salir todas una vez. */
Tarea.prioridad=10;

/*Si se insertan con prioridad creciente. Las tareas deben salir en orden creciente*/
//Tarea.prioridad=i;

/*Si se prueban con prioridad decreciente. Deben salir tareas en orden decreciente*/
//Tarea.prioridad=Ntareas+1-i;

Tarea.ntarea=i; /*se escribe en registro Tarea, que es apuntado por nuevo*/


insert(nuevo);
}
}

La función Sch es un itinerador, que extrae uno a uno los elementos; cuando el heap queda
vacío se lo vuelve a llenar, y se avanza a la siguiente línea.

void Sch(void)
{
if ( extraemin()!= 0) { putchar('\n'); llenecola();}
}

El itinerador siempre está corriendo.

void main(void)
{
for(;;) { Sch(); }
}

Probar las funciones requiere un diseño cuidadoso, para asegurar que éstas cumplan las
especificaciones. El ejemplo anterior, sirve de inicio para diferentes pruebas a realizar. Por
ejemplo, qué pasa si se intenta agregar más tareas de las que puede soportar el tamaño definido
para el heap. Verificar que efectivamente siempre seleccionará al menor, requiere iniciar las
prioridades con valores diferentes.

8.7. Resumen:

La operación de descartar el mínimo consiste en:


Sacar la raíz, lo cual corta el árbol.
La hoja más derechista, de menor nivel, se coloca en el lugar de la raíz.
Se la hace descender, por intercambio con los hijos, desde la raíz, para mantener la propiedad
del heap.

La complejidad queda dada por altura del árbol, que es la trayectoria más larga desde la raíz
hasta las hojas, y es: O(log2(n))

En inserción:

Profesor Leopoldo Silva Bijit 30-06-2008


12 Estructuras de Datos y Algoritmos
Se coloca el nuevo elemento en el menor nivel, lo más a la izquierda posible.
Se la hace ascender, por intercambio con el padre, manteniendo la propiedad de heap.
La complejidad queda dada por altura del árbol, que es la trayectoria más larga desde la hoja,
recién insertada, hasta la raíz, y es: O(log2(n))

Se puede cambiar la relación de orden, de tal modo que el valor numérico del padre sea mayor
que el de los hijos. En este caso se hacen descender a los más livianos, y ascender a los más
pesados.

Referencias.

Robert W. Floyd. “Algorithm 113: Treesort,” Communications of the ACM , Volume 5 pág.
434. 1962.
J.W.J. Williams. Algorithm 232 (Heapsort). Communications of the ACM, Volume 7, pp.347-
348, 1964.

Profesor Leopoldo Silva Bijit 30-06-2008


Colas de prioridad. Heap. 13
Índice general.

CAPÍTULO 8 ............................................................................................................................................. 1
COLAS DE PRIORIDAD. ........................................................................................................................ 1
8.1. OPERACIONES. ................................................................................................................................... 1
8.2. RELACIONES ENTRE EL NÚMERO DE NODOS Y LA ALTURA EN ÁRBOLES BINARIOS. ............................ 1
8.3. CARACTERÍSTICAS DE UNA POSIBLE ESTRUCTURA. ............................................................................ 3
8.4. DESCRIPCIÓN DE LA ESTRUCTURA HEAP. ........................................................................................... 4
8.5. COMPLEJIDAD. ................................................................................................................................... 5
8.5.1. La operación de inserción. ........................................................................................................ 5
8.5.2. Declaraciones de estructuras de datos y variables. .................................................................. 7
8.5.3. Inserción. ................................................................................................................................... 8
8.5.4. Extracción del mínimo............................................................................................................... 8
8.5.5. Descenso en el heap. ................................................................................................................. 9
8.5.6. Extracción del mínimo. ............................................................................................................ 10
8.6. PRUEBA DE LAS FUNCIONES. ............................................................................................................ 10
8.7. RESUMEN: ....................................................................................................................................... 11
REFERENCIAS. ........................................................................................................................................ 12
ÍNDICE GENERAL. ................................................................................................................................... 13
ÍNDICE DE FIGURAS................................................................................................................................. 13

Índice de figuras.

FIGURA 8.1. ÁRBOLES BINARIOS COMPLETOS. .............................................................................................. 2


FIGURA 8.2. ÁRBOLES BINARIOS INCOMPLETOS. ........................................................................................... 2
FIGURA 8.3. ALTURA ÁRBOL BINARIO INCOMPLETO. .................................................................................... 3
FIGURA 8.4. ÁRBOL BINARIO PARCIALMENTE ORDENADO. ........................................................................... 3
FIGURA 8.5. ÁRBOL BINARIO CON RELACIÓN DE ORDEN TIPO HEAP. ............................................................. 4
FIGURA 8.6. ARREGLO CON RELACIÓN DE ORDEN TIPO HEAP. ....................................................................... 4
FIGURA 8.7. INSERCIÓN DE NODO CON VALOR 13 EN UN HEAP QUE TENÍA 12 ELEMENTOS. ........................... 6
FIGURA 8.8. ASCENSO DEL NODO CON VALOR 13 POR INTERCAMBIO CON EL PADRE. ................................... 6
FIGURA 8.9. SIGUE ASCENDIENDO EL NODO CON VALOR 13. QUEDA UN HEAP DE 13 ELEMENTOS. ............... 7
FIGURA 8.10. SE EXTRAJO LA RAÍZ, SE MOVIÓ EL ÚLTIMO A LA RAÍZ, SE CAMBIA LAST. ............................... 9
FIGURA 8.11. DESCENSO DEL MÁS PESADO. SIFT-DOWN. .............................................................................. 9

Profesor Leopoldo Silva Bijit 30-06-2008


1

Capítulo 9

Ordenar

Se desea ordenar una serie de estructuras, que contienen información, de acuerdo al valor de un
campo de la estructura denominado clave.

Se denomina ordenamiento interno si se ordena un arreglo de estructuras; y es externo si se


ordena un archivo de estructuras.

Se denomina ordenamiento interno in situ, a aquel que no requiere espacio adicional. Es decir se
ordena en el mismo arreglo.

Suelen tenerse dos medidas de eficiencia: C(n) el número de comparaciones de claves, y M(n) el
número de movimientos de datos, necesarios para lograr el ordenamiento.

Se denomina métodos directos a aquellos algoritmos primitivos de ordenamiento, que suelen ser
fáciles de entender, y que tienen buen comportamiento para n pequeños. Sin embargo suelen ser
de complejidad O(n2). Suelen ser el punto de partida de los métodos más elaborados que suelen
ser O(n*log2(n) ).

En los diversos algoritmos de ordenamiento es importante considerar su complejidad en el peor


caso y en promedio.

Si se requiere un ordenamiento ascendente, cada vez que en el arreglo se tenga:

i < j con a[i] > a[j]


se tiene una inversión.

Los algoritmos de ordenamiento eliminan todas las inversiones.

i j

Figura 9.1. Inversiones.

Profesor Leopoldo Silva Bijit 30-05-2009


2 Estructuras de Datos y Algoritmos
9.1. Métodos directos.

9.1.1. Selección.

9.1.1.1. Algoritmo:
Repetir hasta que quede un arreglo de un elemento:
Seleccionar el elemento de la última posición del arreglo. Sea j su cursor.
Buscar el elemento con la mayor clave en el resto del arreglo
Intercambiar el elemento de la última posición del arreglo con el de mayor clave.
Disminuir el rango del arreglo en uno.

Buscar el elemento con la mayor clave en el resto del arreglo puede traducirse por:
for (i=j-1; i>=0; i--)
{ Seleccione el mayor entre a[j-1] y a[0]; }

Se definen los siguientes tipos que serán empleados en las funciones:


typedef int Tipo; /* tipo de item del arreglo */
typedef int Indice; /* tipo del índice */

9.1.1.2. Operación.
/* Ordena ascendente. Desde Inf hasta Sup */
int selectsort(Tipo a[], Indice Inf, Indice Sup)
{ int op=0;
Indice i, j, max;
Tipo temp;

for (j=Sup; j>Inf; j--) {


max=j; temp=a[j]; //selecciona a[j]
for(i=j-1; i>=Inf; i--) {
if (a[i]>temp)
{ max=i; temp=a[i];} //busca el mayor
op++;
}
a[max]=a[j]; //intercambia el mayor con a[j]
a[j]=temp;
op++;
}
return op;
}
Se ha agregado una variable global, op, para contar las operaciones.
9.1.1.3. Ejemplo.
Se selecciona el mayor, se lo intercambia con el mayor del subarreglo.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 3
En el ejemplo a la izquierda de la figura 9.2 se muestra el peor caso para este algoritmo, el
arreglo de entrada está ordenado en forma descendente. La variable op, cuenta las operaciones
considerando: O(1) la complejidad realizada por el for interno, y también O(1) la realización del
intercambio, al final del for externo.

La figura a la derecha ilustra un caso con un arreglo de entrada ordenado en forma aleatoria.
Una variante es seleccionar el menor e intercambiarlo con el de la posición menor, del
subarreglo.

9 8 7 6 5 4 3 2 1 0 44 55 1 2 4 2 9 4 1 8 6 6 7 3 3 7 2

0 8 7 6 5 4 3 2 1 9 j= 9 4 4 5 5 1 2 4 2 7 2 1 8 6 6 7 3 3 9 4 j= 9

0 1 7 6 5 4 3 2 8 9 j= 8 4 4 5 5 1 2 4 2 3 3 1 8 6 6 7 7 2 9 4 j= 8

0 1 2 6 5 4 3 7 8 9 j= 7 4 4 5 5 1 2 4 2 3 3 1 8 6 6 7 7 2 9 4 j= 7

0 1 2 3 5 4 6 7 8 9 j= 6 4 4 6 1 2 4 2 3 3 1 8 5 5 6 7 7 2 9 4 j= 6

0 1 2 3 4 5 6 7 8 9 j= 5 1 8 6 1 2 4 2 3 3 4 4 5 5 6 7 7 2 9 4 j= 5

0 1 2 3 4 5 6 7 8 9 j= 4 1 8 6 1 2 3 3 4 2 4 4 5 5 6 7 7 2 9 4 j= 4

0 1 2 3 4 5 6 7 8 9 j= 3 1 8 6 1 2 3 3 4 2 4 4 5 5 6 7 7 2 9 4 j= 3

0 1 2 3 4 5 6 7 8 9 j= 2 1 2 6 1 8 3 3 4 2 4 4 5 5 6 7 7 2 9 4 j= 2

0 1 2 3 4 5 6 7 8 9 j= 1 6 1 2 1 8 3 3 4 2 4 4 5 5 6 7 7 2 9 4 j= 1
op= 54 o p= 54

Figura 9.2. Ejemplos de ordenamiento por selección.

El subarreglo ya ordenado, va creciendo desde la última posición, hasta llegar a la primera.


9.1.1.4. Análisis de complejidad.
El lazo for interno se realiza: (n-1)+(n-2) +…+ 1 veces. Ya que la primera vez hay que buscar el
mayor en un subarreglo de (n-1) componentes; la segunda vez se busca el mayor en un
subarreglo de (n-2) componentes; y así sucesivamente.

Suma que tiene por resultado: n*(n-1)/2. A esta suma debe agregarse la selección, que es O(1),
y el intercambio (que también es O(1)); acciones que se repiten (n-1) veces. La complejidad es
T(n) = n*(n-1)/2 +(n-1). Entonces T(n) es de complejidad O(n2).

Otra forma de calcular la complejidad es la observación de que el algoritmo se rige por la


relación de recurrencia: T(n) = T(n-1) + (n-1) con T(0) = 0, ya que cada vez se reduce el tamaño
del arreglo en uno, pero con un costo para reducirlo de (n-1) operaciones O(1). Se logra igual
resultado que el anterior.

Profesor Leopoldo Silva Bijit 30-05-2009


4 Estructuras de Datos y Algoritmos
9.1.2. Intercambio.

Se comparan e intercambian pares adyacentes de ítems, hasta que todos estén ordenados.
Los más conocidos son: bubblesort y shakesort.

9.1.2.1. Algoritmo bubblesort de una pasada.


Comenzar con i=0;
Repetir hasta que quede arreglo de tamaño uno:
Efectúa una pasada hacia arriba, hasta la última posición:
if (a[i] > a[i+1]) swap (a[i], a[i+1]); i++; (más pesado en última posición)
Disminuye en uno el tamaño del arreglo (por arriba)
9.1.2.2. Operación.

#define SWAP(a, b) { t=a; a=b; b=t; }


#define compGT(a, b) (a > b)

/* Ordena ascendente. Desde Inf hasta Sup */


void bubbleSort( Tipo a[], Indice Inf, Indice Sup )
{ Indice i, j;
Tipo t; //temporal
/* Recorre con i el arreglo desde Inf hasta Sup */
for(i=Inf ; i<= Sup; i++)
{
/* Recorre con j, desde el siguiente a Inf hasta el final de la zona no ordenada */
for(j=Inf+1; j<=(Sup-(i-Inf)); j++)
{ /* Ordena elementos adyacentes, intercambiándolos */
if( compGT( a[j-1], a[j] ) ) SWAP(a[j-1], a[j]);
}
}
}

9.1.2.3. Análisis de complejidad.


El for interno se realiza (n-1)+(n-2) +…1. Entonces es O(n2).
9.1.2.4. Algoritmo shakesort o bubblesort de dos pasadas:
Comenzar con i=0;
Repetir hasta que quede arreglo de tamaño uno:
Efectúa una pasada hacia arriba, hasta la última posición:
if (a[i] > a[i+1]) swap (a[i], a[i+1]); i++; (más pesado en última posición)
Disminuye en uno el tamaño del arreglo (por arriba)
Efectúa una pasada hacia abajo, hasta el inicio del arreglo:
if (a[i-1] > a[i]) swap (a[i], a[i-1]); i--; (más liviano en primera posición)
Acorta en uno el tamaño del arreglo (por abajo).
Este algoritmo no tiene mejor comportamiento que bubblesort.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 5
9.1.3. Inserción.

En la etapa i-ésima se inserta a[i] en su lugar correcto entre: a[0], a[1],…, a[i-1] que están
previamente ordenados.
Hay que desplazar los elementos previamente ordenados, uno a uno, hasta encontrar la posición
donde será insertado a[i].

En el peor caso: si a[0] es mayor que a[i] hay que efectuar i copias.
En el mejor caso si a[i-1] es menor que a[i], no hay desplazamientos.

Si existen claves repetidas, éstas conservan el orden original, se dice que el algoritmo de
ordenamiento es estable.

Puede entenderse como el procedimiento usado para ordenar una mano de naipes.
Dentro de éstos se encuentran la inserción directa y la inserción binaria.
9.1.3.1. Declaraciones de tipos y macros.
typedef int Tipo; /* tipo de item del arreglo */
typedef int Indice; /* tipo del índice */
#define compGT(a, b) (a > b)
9.1.3.2. Operación.
void InsertSort(Tipo *a, Indice inf, Indice sup)
{ Tipo t;
Indice i, j;
/*Ordena ascendentemente */
for (i = inf + 1; i <= sup; i++)
{ t = a[i]; //se marca el elemento que será insertado.
/* Desplaza elementos hasta encontrar punto de inserción */
for (j = i-1; j >= inf && compGT(a[j], t); j--) a[j+1] = a[j];
a[j+1] = t; /* lo inserta */
}
}

9.1.3.3. Ejemplo de inserción directa.


Se muestra a la izquierda de la Figura 9.3, un arreglo de 10 componentes, en orden descendente,
que es el peor caso. A la derecha se tiene un caso cualquiera.

Se muestra el elemento que es insertado en un subarreglo ya ordenado que lo precede.

Profesor Leopoldo Silva Bijit 30-05-2009


6 Estructuras de Datos y Algoritmos

9 8 7 6 5 4 3 2 1 0 44 55 12 42 94 18 6 67 33 72

8 9 7 6 5 4 3 2 1 0 i= 1 44 55 12 42 94 18 6 67 33 72 i= 1

7 8 9 6 5 4 3 2 1 0 i= 2 12 44 55 42 94 18 6 67 33 72 i= 2

6 7 8 9 5 4 3 2 1 0 i= 3 12 42 44 55 94 18 6 67 33 72 i= 3

5 6 7 8 9 4 3 2 1 0 i= 4 12 42 44 55 94 18 6 67 33 72 i= 4

4 5 6 7 8 9 3 2 1 0 i= 5 12 18 42 44 55 94 6 67 33 72 i= 5

3 4 5 6 7 8 9 2 1 0 i= 6 6 12 18 42 44 55 94 67 33 72 i= 6

2 3 4 5 6 7 8 9 1 0 i= 7 6 12 18 42 44 55 67 94 33 72 i= 7

1 2 3 4 5 6 7 8 9 0 i= 8 6 12 18 33 42 44 55 67 94 72 i= 8

0 1 2 3 4 5 6 7 8 9 i= 9 6 12 18 33 42 44 55 67 72 94 i= 9
op= 54 op= 30

Figura 9.3. Ejemplos ordenamiento por inserción.

9.1.3.4. Análisis de complejidad.


En el peor caso, el for interno se efectúa: 1 + 2 + 3 +…+(n-1). Esto en caso que el arreglo esté
previamente ordenado en forma descendente. En el primer recorrido del for interno hay que
mover un elemento; en la segunda pasada, hay que mover dos elementos; y así sucesivamente,
hasta mover (n-1) para dejar espacio en la primera posición, para insertar el último.
Esto tiene complejidad T(n) = n*(n-1)/2 + (n-1) = O(n2).

Se agregan (n-1) movimientos por el almacenamiento del elemento que será insertado, más la
escritura en la posición de inserción; ambas son O(1).

En el mejor de los casos con un arreglo previamente ordenado en forma ascendente, el for
interno no se realiza nunca; ya que no son necesarios los desplazamientos que éste efectúa; el
lazo externo debe efectuarse (n-1) vez. Este caso es de complejidad: T(n) = O(n).

Mientras más ordenado parcialmente esté el arreglo, mejor será el comportamiento del
algoritmo de inserción.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 7
9.1.3.5. Inserción binaria.
Una posible mejora del algoritmo es la consideración de que el subarreglo en el cual se insertará
el elemento está previamente ordenado. En este caso, en lugar de efectuar una búsqueda
secuencial de la posición de inserción, se puede realizar una búsqueda binaria.

Sin embargo esto disminuye el número de comparaciones y no el de movimiento de los datos


(que suele ser de mayor costo). Además si el arreglo original está ordenado la búsqueda binaria
toma mayor tiempo que la secuencial.

int BinaryInsertSort(Tipo *a, Indice inf, Indice sup)


{ int op=0;
Tipo t;
Indice i, j, right, left, m;
for (i = inf + 1; i <= sup; i++) {
t = a[i]; left=inf ; right=i-1;
while(left<=right)
{ m=(left+right)/2;
if( t <a[m]) right=m-1; else left=m+1;
}
/* Desplace elementos sobre punto de inserción */
for (j = i-1; j >= left ; j--) {a[j+1] = a[j]; op++;}
/* inserte */
a[left] = t; op++;
}
return(op);
}

Este ejemplo ilustra que no siempre se logra mayor eficiencia en un algoritmo efectuando
modificaciones que parecen convenientes. Éstas deben ser evaluadas, determinando su
complejidad.

9.1.4. Shellsort. (1959)

Es uno de los algoritmos más rápidos para ordenar arreglos de algunas decenas de componentes.
Es un método adaptivo que funciona mejor si los arreglos están casi ordenados. El análisis de su
complejidad es difícil.

Se ordenan elementos que están a una distancia h entre ellos en pasadas consecutivas.
Al inicio h es un valor elevado y disminuye con cada pasada.
Sea hk cuando la distancia de comparación es k. La pasada se denomina fase hk .
El algoritmo termina con h1 .
Se emplea el algoritmo de inserción directa para ordenar cada pasada. Este algoritmo funciona
bien ya que los subarreglos son pequeños o están casi ordenados.

Profesor Leopoldo Silva Bijit 30-05-2009


8 Estructuras de Datos y Algoritmos
Después de la fase hk los elementos que están a distancia hk entre ellos están ordenados, esto
quiere decir que: a[i] < a[i+ hk ] < a[i+ 2 hk ] < a[i+ 3 hk ] <…para todo i.

El ordenamiento burbuja ordena en fase h1 . Necesariamente shellsort tiene que llegar a ordenar
la fase h1 , pero su ventaja es que el número de intercambios en la fase h1 es menor que los
intercambios necesarios en inserción o por burbuja. La razón de esto es que si a una lista
ordenada de fase hk se la somete a un ordenamiento de fase hk −1 sigue hk ordenada.

9.1.4.1. Shell Sort original.


int shellsortShell(Tipo a[], Indice n)
{ int op=0;
Indice h, i, j;
Tipo temp;

for(h=n/2; h>0; h=h/2) {


for (i=h; i<n; i++) { //aplica inserción a partir de h
temp=a[i];
for (j=i-h; j>=0 && a[j]>temp; j-=h) { a[j+h]=a[j]; op++;}
a[j+h]=temp; //inserta
op++;
}
}
return op;
}

El algoritmo original emplea que el nuevo valor de hk sea el anterior dividido por dos.

9.1.4.2. Selección de la secuencia de valores para h.


La selección adecuada de la secuencia hk genera un menor costo. A través de métodos de ensayo
y error se han efectuado una serie de elecciones de la secuencia de pasadas, logrando una
estimación de la complejidad.

La secuencia original de Shell (1959): 1 2 4 8 16… tiene costo O(n2).

La secuencia hi = 4i+1 +3* 2i +1 con i>0 y h1 =1 y h2 = 8 genera:


1 8 23 77 281 1073 4193 16577…con complejidad O(n4/3)

La secuencia propuesta por Knuth (1998 ): hi +1 = 3hi + 1 , con h1=1


1 4 13 40… tiene complejidad O(n (log n)2).

Luego nuevas optimizaciones de Knuth permiten complejidades de O(n1.25) y


O(n 1+1/sqrt(log n)).

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 9
9.1.4.3. Shell sort. Knuth.
int shellsortKnuth(Tipo a[], Indice n)
{ int op=0;
Indice h,i,j;
Tipo temp;

for(h=1; h<=n/9; h=h*3+1); //inicia h


for( ; h>0; h=h/3) {
/* h = 1, 4, 13, 40, 121, 364... 3*h+1 */
for (i=h; i<n; i++) { //pasada por inserción
temp=a[i];
for (j=i-h; j>=0 && a[j]>temp; j-=h) { a[j+h]=a[j]; op++;}
a[j+h]=temp;
op++;
}
}
return op;
}

9.1.4.4. Shell sort Ciura.


Ciura (2001) conjetura que su secuencia es óptima para grandes valores de n. Se almacenan los
números de la secuencia en un arreglo local denominado incs. Se escoge el valor inicial de la
secuencia tal que sea menor que 0,44n. Por ejemplo si n=1000, el primer h será 301 generando
7 fases de ordenamiento.

int shellsortCiura(Tipo a[], Indice n)


{ int op=0;
Indice h,i,j,t;
Tipo temp;
int incs[18] = {
2331004, 1036002, 460445, 204643, 90952, 40423, 17965, 7985, 3549,
1577, 701, 301, 132, 57, 23, 10, 4, 1
};
for (t=0; t<18; t++) {
h=incs[t];
if (h > n*4/9) continue;
for (i=h; i<n; i++) { //Inserción.
temp=a[i];
for (j=i-h; j>=0 && p[j]>temp; j-=h) { a[j+h]=a[j]; op++; }
a[j+h]=temp; op++;
}
}
return op;
}

Profesor Leopoldo Silva Bijit 30-05-2009


10 Estructuras de Datos y Algoritmos
9.2. Métodos avanzados.

Debido al gran número de algoritmos para ordenar, se estudiarán en detalle dos de los más
conocidos y eficientes.

Heapsort que en peor caso es O(n*log2(n)), y que una refinación del método de selección.

Quicksort que es O(n*log2(n)) en promedio; es una elaboración sofisticada del método de


intercambio.

9.2.1. Heapsort (1964).

Los primeros en exponer este algoritmo fueron: J.W.J. Williams y Robert Floyd.

Se forma un heap, a partir del arreglo desordenado.


Luego, repetidamente se intercambia la raíz con la posición del último, y se hace descender la
nueva raíz en un heap de largo disminuido en uno. El arreglo queda ordenado en forma
descendente.

Se analiza primero la formación de un heap, la figura siguiente ilustra un arreglo no ordenado


visualizado como un heap.

44

55 12

42 94 18 06

67 last

Figura 9.4. Visualización de un arreglo desordenado como un árbol binario.

9.2.1.1. Formación del heap. Revisión de subárboles.


La formación del heap, a partir del arreglo desordenado, se logra con:

for(i=last/2; i>0; i--) siftdown(i, last);

Se modifica levemente la rutina vista en colas de prioridad, se pasa i de variable local, a


argumento de la función.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 11
void siftdown(int i, int n)
{ int j;
registro temp; /* al inicio i apunta a la subraíz; j al hijo izquierdo */
while ( (j=(i<<1)) <= n ) /* j = 2*i */
{ if ( ( (j+1)<=n ) && ( r[j+1].prioridad < r[j].prioridad)) j++;
/*Si existe hijo derecho y es menor que el hijo izquierdo, j apunta al derecho */
if (r[i].prioridad <= r[j].prioridad) break;
/*Intercambia si el hijo menor es menor que el padre */
temp=r[j], r[j]= r[i], r[i]=temp;
i=j; /* Nueva raíz en el descenso */
}
}

De esta forma la función forma un heap(i, n).

En la primera iteración con n=8 e i=4, no se producen cambios. Se revisa el subárbol del padre
del último.

44

55 06
i =4

42 94 18 12

67 last

Figura 9.5. Revisión del primer subárbol (i=4).

Luego cuando i=3, se apunta con j al elemento con valor 12.


Luego de la ejecución de siftdown(3,8), no hay cambios y el diagrama queda:

44

55 i =3 06

42 94 18 12

67 last

Figura 9.6. Revisión del segundo subárbol (i=3).

Profesor Leopoldo Silva Bijit 30-05-2009


12 Estructuras de Datos y Algoritmos
Se revisa el subárbol cuya raíz es apuntada por i=2, con j se apunta al hijo menor (el nodo con
valor 42, en la Figura 9.6). Se intercambian el 42 con el 52, y j queda apuntando al nodo con
valor 55, y sigue la revisión en descenso. En el caso del ejemplo, no hay nuevos cambios.

Luego de la iteración con i=2 se obtiene:


44

i =2 42 06

55 94 18 12

67 last

Figura 9.7. Revisión del tercer subárbol (i=2).

Luego de esto se tiene que el subárbol cuya raíz está apuntada por i=2, es un heap.

Finalmente la ejecución de siftdown(1,8) hace descender la raíz. Primero la intercambia con 06,
y luego con 12, resultando:

i =1 06

42 12

55 94 18 44

67 last

Figura 9.8. Revisión del cuarto subárbol (i=1).

Cada descenso tiene un costo menor a log2(n), ya que en los primeros casos los subárboles
tienen menos nodos, salvo el descenso desde la raíz que tiene un costo log2(n). Como existen
(n/2) subárboles, en peor caso el costo total es de (n/2)*log2(n); es decir O(n*log2(n))

Entonces: for(i=last/2; i>0; i--) siftdown(i, last); revisa todos los subárboles desde el padre
del último.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 13
Debe observarse que el número de subárboles de un heap es igual al número de hojas de un heap
de n elementos.

Se revisan las trayectorias en los subárboles desde su inicio hasta las hojas en siftdown.

9.2.1.2. Ordenamiento del heap.


El ordenamiento del heap se logra con:
Repetidamente se intercambia la raíz con la posición del último, y se hace descender la nueva
raíz en un heap de largo disminuido en uno. El arreglo queda ordenado en forma descendente.

for (i=last; i>1; i--)


{ temp=r[1], r[1]= r[i], r[i]=temp; /*swap(1, i) */
siftdown(1, i-1);
}

Como se realiza n veces el lazo for, si se asume costo constante para el intercambio, se tiene un
costo O(n*log2(n)).

Esto se debe a que la suma:


( log2(n-1) +1 ) + ( log2(n-2) +1 ) + ( log2(n-3) +1 ) + .. + ( log2(1) +1 ) es O(n*log2(n))

Mediante Maple pueden comprobarse que existen las constantes c1 y c2 para n0 >10.
Sum( 1+ln(k)/ln(2), k=2..n-1)=sum( 1+ln(k)/ln(2), k=1..n);
plot([0.8*n*ln(n)/ln(2),sum(1+ln(k)/ln(2), k=2..n-1),n*ln(n)/ln(2)],
n=10..2000, color=[red,black,blue]);

El algoritmo ordena en forma descendente.

Se agregan argumentos al segmento analizado antes: Se pasa un puntero a al arreglo que será
oredenado, y los índices inicial (ini debe ser siempre 1, salvo que se modifiquen las rutinas de
ascenso y descenso) y final (last) entre los cuales debe ordenar.

void heapsort(preg a, Indice ini, Indice last)


{int i;
registro temp;
for(i=last/2; i>ini-1; i--) siftdown(a, i, last); //ordena subárboles desde padre del último
for(i=last; i>ini; i--)
{ temp=a[ini], a[ini]= a[i], a[i]=temp; /*swap(ini, i) */
siftdown(a, ini, i-1);
}
}

Profesor Leopoldo Silva Bijit 30-05-2009


14 Estructuras de Datos y Algoritmos
9.2.2. Quicksort (1961).

Se parte el arreglo a ordenar en dos subarreglos, dejando los elementos mayores que uno
denominado pivote en una parte, y los menores en la otra. Si los subarreglos no están ordenados,
se los vuelve a partir, hasta lograr el ordenamiento.

Es del tipo:

T(n) = 2*T(n/2) +c*n

donde c es el costo de dividir en dos partes. La solución de esta relación, ver Capítulo 4,
Ejemplo 4.14, es:

c*n*( log2(2)+log2(n) ) la que es O(n*log2(n)).

Este costo es en el mejor caso, en el cual siempre se puede efectuar la división en mitades.

Es un tipo de ordenamiento por intercambio, en éstos se compara e intercambia los ítems hasta
ordenar.
El algoritmo burbuja es del tipo por intercambio, y requiere n2 comparaciones entre ítems
adyacentes.

En quicksort los intercambios se efectúan sobre distancias mayores y relativas a un pivote.


Fue publicado por primera vez por Hoare, en 1961, y aún se mantiene vigente.

9.2.2.1. Partición.
El núcleo del algoritmo es el ordenamiento de una partición respecto de un pivote.

i j
piv

Figura 9.13. Pivote en partición.

Se asume que las direcciones de las celdas contiguas del arreglo aumentan hacia la derecha.

Se desea tener que los elementos sobre el pivote sean mayores o iguales que el valor asociado
al pivote, y los elementos bajo el pivote sean menores o iguales que el valor de éste.

Se analiza el código que realiza la partición mediante el ordenamiento por intercambio respecto
al pivote. Es un código complejo que muestra genialidad.

do {
while ( a[i].clave < piv.clave) i++; //encuentra uno mayor o igual que el valor del pivote

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 15
while( piv.clave < a[j].clave) j--; //al salir apunta a uno menor o igual que el pivote
if( i<=j {swap(i, j) ; i++; j--;}
} while( i<=j );

Se ha usado el pivote como una estructura de igual tipo que las celdas del arreglo. Y se abrevia
el intercambio con el llamado a swap, que realiza:

temp=a[i]; a[i]=a[j]; a[j]=temp;

Donde temp es una estructura igual a la de las componentes del arreglo.


Traza de la ejecución del segmento partición.
Si al inicio tenemos la siguiente situación:
44 55 12 42 94 06 18 67

i j
piv 42

Figura 9.14. Elementos al inicio.

Después de los dos primeros while internos se tiene:


44 55 12 42 94 06 18 67

i j
piv 42

Figura 9.15. Elementos después de los primeros dos while.

En el lado izquierdo se busca un elemento mayor o igual que el pivote, y en el derecho uno
menor o igual que éste.

El if produce el intercambio y mueve los cursores, quedando:

18 55 12 42 94 06 44 67

i j
piv 42

Figura 9.16. Elementos después del if.

Como i es menor que j, se vuelve a iterar.


Ninguno de los while, cambia i ni j. Después de realizado el intercambio queda:

Profesor Leopoldo Silva Bijit 30-05-2009


16 Estructuras de Datos y Algoritmos

18 06 12 42 94 55 44 67

i j
42
piv

Figura 9.17. Elementos después de la segunda iteración.

Se vuelve a iterar, ya que i es menor que j.


El primer while interno cambia i, el segundo disminuye j (queda apuntando a 42).
18 06 12 42 94 55 44 67
i j
42
piv

Figura 9.18. Tercera iteración, después de los dos while.

El if efectúa el intercambio, e incrementa los punteros. Resultando con esto:


18 06 12 42 94 55 44 67
j i
42
piv

Figura 9.19. Tercera iteración, después del if.

Con lo cual termina el repeat.

El resultado es que los elementos sobre el pivote son mayores o iguales que el valor asociado al
pivote, y los elementos bajo el pivote son menores o iguales que éste.

En este caso se efectuó un intercambio demás (lo cual podría eliminarse si el intercambio se
protege con un if, que lo realice sólo si i es diferente de j).
Traza con valores repetidos en la partición.
Veamos un ejemplo con algunos valores iguales en la partición.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 17

1 1 1 2 1 1 1

i j
piv 2

Figura 9.20. Partición con valores repetidos.

Después de los dos primeros while, queda:

1 1 1 2 1 1 1

i j
piv 2

Figura 9.21. Primera iteración, después de los while.

Y después del if:


1 1 1 1 1 1 2

i j
piv 2

Figura 9.22. Al final de la primera iteración.

Debe notarse que la función partición cambia el orden original de las componentes con igual
valor de clave, por esto se dice que no es ordenamiento estable.

En la segunda pasada, i queda apuntando al último (recordar que se compara con el valor
almacenado en la estructura pivote); j no cambia.

Y finalmente termina el repeat.

1 1 1 1 1 1 2

j i
piv 2

Figura 9.23. Después de la segunda iteración.

Traza con valores repetidos del pivote en la partición.

Profesor Leopoldo Silva Bijit 30-05-2009


18 Estructuras de Datos y Algoritmos
Veamos un ejemplo con algunos valores repetidos del pivote en la partición.

1 2 1 2 6 2 5

i j
piv 2

Figura 9.23.a. Pivote con repeticiones.

Después de los dos primeros while, queda:

1 2 1 2 6 2 5

i j
piv 2

Figura 9.23.b. Luego de los dos while.

Y después del if, ha realizado un swap innecesario, quedando:

1 2 1 2 6 2 5

i j
piv 2

Figura 9.23.c. Después del if.

Vuelve a iterar, después de los dos while, resulta:

1 2 1 2 6 2 5

i j
piv 2

Figura 9.23.d. Segunda iteración.

Con lo cual vuelve a intercambiar consigo mismo, para finalmente dejar:

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 19

1 2 1 2 6 2 5

j i
piv 2

Figura 9.23.e. Al terminar la iteración.

De este modo se acortan los subarreglos.

Luego del análisis de estos casos, puede refinarse el segmento que realiza la partición. No
realiza el intercambio si: i es igual a j o si las claves son iguales.

do {
while ( a[i].clave < piv.clave) i++; //encuentra uno mayor o igual que el valor del pivote
while( piv.clave < a[j].clave) j--; //al salir apunta a uno menor o igual que el pivote
if (i<=j)
{ if ( (i!=j) && (a[i].clave!=a[j].clave) ) swap(i, j) ;
i++; j--;
}
} while( i<=j );

Sobre la elección del pivote.


Si se elige como pivote el primer elemento o el último, lo cual es fácil de realizar, se tendrá mal
funcionamiento si el arreglo está ordenado en forma inversa. Sin embargo el comportamiento es
bueno, si el orden original es aleatorio.

Si el cursor del pivote se elige en forma aleatoria, existirá buen comportamiento; sin embargo la
generación repetida de los números aleatorios puede requerir un tiempo no despreciable.
Encontrar la mediana de los números, que sería una mejor opción, también aumenta los costos,
debido a la complejidad de encontrar la mediana.

Una alternativa es escoger la mediana de tres números. Si l y r son los cursores del primer y
último elemento del arreglo, para formar el tercer número se toma el valor central. Es decir, se
toma la mediana de los tres números con cursores: l, r y (l+r)/2.

En el algoritmo a continuación se escoge como pivote, el valor central del arreglo.

9.2.2.2. Ordenamiento usando recursión.

El siguiente código muestra la forma en que se logra el ordenamiento.

Profesor Leopoldo Silva Bijit 30-05-2009


20 Estructuras de Datos y Algoritmos
void qsort(int l, int r)
{
int i=l, j=r;
registro piv=a[(l+r)/2)];
do {
while ( a[i].clave < piv.clave) i++;
while( piv.clave < a[j].clave) j--;
if( i<=j {swap(i, j) ; i++; j--;}
} while(i<=j);
if( l < j) qsort( l, j);
if( i< r ) qsort( i, r);
}

Se inicia una partición luego se aplica el mismo proceso a las dos particiones.

Las particiones menores, vuelven a generar dos particiones y así sucesivamente hasta que la
partición tiene un solo ítem, en este caso no se generan nuevas recursiones.

Para ordenar un arreglo se invoca: qsort(0, N-1) para un arreglo de N ítems.

Produce un ordenamiento ascendente.


9.2.2.3. Complejidad en Peor caso.
Si resulta que el pivote es el mayor elemento de la partición, ésta genera un nuevo llamado con
una partición de un elemento menos que la original; no la divide en dos. Ver el segundo
ejemplo, Figura 9.23; en este caso no se cumple que i<r. Se tendrán entonces: la primera
pasada con n, la segunda con (n-1), y así sucesivamente.

Sólo se llama recursivamente por un lado. Esto sucede siempre que la elección del pivote apunte
al elemento mayor de la subpartición. Este peor caso, de baja probabilidad daría origen a un
costo O( n2).

En este particular caso, se tiene: T(n) = T(n-1) +n con T(1)=1, cuya solución es:

n(n + 1)
T ( n) = = Θ( n 2 )
2

Se ilustra un ejemplo, de peor caso, en que sólo se invoca por la izquierda.

2 4 6 8 1 5 3 7

i j
piv 8

Figura 9.24. Peor caso, sólo se invoca por la izquierda.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 21
9.2.2.4. Complejidad en Caso promedio.
Complejidad de Quicksort en caso promedio.

Se asume que el tamaño de la primera partición puede ser cualquier valor entre 1 y (n-1), y que
cada tamaño es igualmente probable que ocurra.
1
Es decir, la probabilidad de que ocurra cualquiera de los tamaños es:
n

Si al formar dos particiones, la primera tiene i elementos, la segunda tendrá (n-i) elementos.
Entonces la complejidad temporal, puede expresarse por:

T (n) = T (i) + T (n − i ) + cn

Valor promedio para la primera partición:


1 n −1
T (i ) = ∑ T ( j)
n j =1
Debido a que una partición es la imagen de la otra, también el valor promedio, de la otra
1 n −1
partición será: T (n − i ) = ∑ T ( j)
n j =1
Entonces:
2 n −1
T ( n) = ∑ T ( j ) + cn
n j =1
Multiplicando por n, se obtiene:
n −1
nT (n) = 2 ∑ T ( j ) + cn 2
j =1

Para pasar a una relación de recurrencia de primer orden, podemos determinar la relación para
(n-1), en la fórmula anterior, resulta:
n−2
(n − 1)T (n − 1) = 2 ∑ T ( j ) + c(n − 1) 2
j =1

Restando las dos relaciones :


nT (n) − (n − 1)T (n − 1) = 2T (n − 1) + cn 2 − c(n − 1) 2

Despejando T(n):
nT (n) = (n + 1)T (n − 1) + 2cn − c

La cual puede aproximarse, para valores grandes de n por:


nT (n) = (n + 1)T (n − 1) + 2cn con T(0)=0 , es la relación de recurrencia de primer orden.

Para resolver esta relación, al dividir, en ambos lados, por n(n+1) se obtiene:
T (n) T (n − 1) 2c
= +
n +1 n n +1

Profesor Leopoldo Silva Bijit 30-05-2009


22 Estructuras de Datos y Algoritmos
Pueden generarse a partir de la anterior, por sucesivos reemplazos de n por n-1, n-2,…2
T (n − 1) T (n − 2) 2c
= +
n n −1 n

T (n − 2) T (n − 3) 2c
= +
n −1 n−2 n −1
…….
T (2) T (1) 2c
= +
3 2 3

Si se suman, se obtiene:
T (n) T (1) 1 1 1
= + 2c( + + ... + )
n +1 2 n +1 n 3

Empleando la serie armónica, y eliminando constantes:


T ( n)
≈ ln(n + 1)
n +1

Más exactamente:
T ( n ) := 2 γ − 4 n + 2 γ n + 2 ( n + 1 ) Ψ ( n + 1 )

Que puede obtenerse, mediante el comando Maple:


S:=rsolve( { T(n) = (n+1)*T(n-1)/n + 2*(n-1)/n , T(0) = 0}, T(n));

2n log2(n)

T(n)

0.5n log2(n)

Figura 9.25. Complejidad en caso promedio de Quicksort.

Entonces se demuestra que en el caso promedio, para n>2:

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 23
T (n) = Θ(n log(n))

Que es igual a la complejidad del mejor caso, es decir, que cada vez que se efectúa una partición
se divide ésta en dos mitades.
9.2.2.5. Variantes de quicksort.
Existen muchas variantes del algoritmo.

En el libro “Introduction to Algorithms” de Cormen y otros, figura la siguiente versión. Puede


notarse lo sencillo que resulta pasar a C el código que figura en pseudo-código.

Se plantea en rutina aparte la partición; sin embargo es preferible incluirla cómo código dentro
la función, para evitar la sobrecarga de un llamado a función.

#define SWAP(a,b) { temp=(a); (a)=(b); (b)=temp; }


Indice particionCormen(preg a, Indice left, Indice right)
{ Indice i= left -1, j;
registro temp;
int piv=a[r].clave;
for(j= left; j< right; j++)
{
if(a[j].clave <= piv)
{ i++; SWAP(a[i], a[j]); }
}
SWAP(a[i+1], a[right]);
return(i+1);
}

Nótese que en piv, se guarda ahora el valor de la clave. Y que el valor se escoge, no en el centro
sino en un extremo de la partición.
El macro SAWP requiere la variable local temp.
La función que ordena, de tipo recursiva, se muestra a continuación:

void qsortCormen(preg a, Indice left, Indice right)


{ int i;
if(left < right) //Detiene la recursión
{
i=particionCormen(a, left, right);
qsortCormen(a, left, i-1); //particion izquierda
qsortCormen(a, i+1, right); //particion derecha
}
}

El texto “Algorithms in C” de Robert Sedgewick plantea levemente diferente la función de


partición, la función tiene sólo una modificación menor, respecto de la anterior.

Profesor Leopoldo Silva Bijit 30-05-2009


24 Estructuras de Datos y Algoritmos
Indice particionSedgewick(preg a, Indice left, Indice right)
{ Indice i= left -1, j= right;
registro temp;
int piv=a[right].clave;
for(;;)
{while(a[++i].clave < piv);
while( piv < a[--j].clave) if (j==left) break;
if(i>=j) break;
SWAP(a[i], a[j]);
}
SWAP(a[i], a[right]);
return(i);
}

void qsortSedgewick(preg a, Indice left, Indice right)


{ int i;
if(right <=left) return;
i=particionSedgewick(a, left, right);
qsortSedgewick(a, left, i-1); //particion izquierda
qsortSedgewick(a, i+1, right); //particion derecha
}

9.2.3. Quicksort. No recursivo.

Una alternativa frecuente es desarrollar una función no recursiva. La cual en lugar de invocarse
a sí misma, almacena en un stack solamente los argumentos. Luego mientras el stack no esté
vacío los va sacando.

Se reemplaza el almacenamiento de los frames y la dirección de retorno y los valores de los


registros que deben ser salvados, sólo por lo estrictamente necesario. Para una mayor
efectividad, las funciones push y pop, deberían definirse como macros. Obviamente se ocupa
menos espacio del stack del programa; pero el programador debe estimar el tamaño del stack del
usuario.

La siguiente función ilustra la técnica.

#include "datos.h"
#include "stack.h"
#define Maxitems 10
//tamaño del stack = x. Con 2^x =N Sedgewick 7.3 pág. 314
#define SIZE 4

void qsortNoRec(preg a, Indice left, Indice right)


{ Indice i;

StackInit(SIZE);
push2(left, right); //inicia stack con los argumentos originales

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 25
while (!StackEmpty())
{ left=StackPop(); right =StackPop(); //los saca
if (right <=left) continue;
i = particionSedgewick(a, left, right);
if( i-left > right -i)
{ push2(left, i-1); push2(i+1, right);} //empuja en lugar de invocarse a sí misma.
else {push2(i+1, right); push2(left, i-1);} //empuja primero la partición con más elementos.
}
StackDestroy();
}
9.2.3.1. Stack de usuario.
Se ilustra una metodología de programación, para desarrollar herramientas, que podrán ser
usadas por otros programas.
Para poder emplear la función qsortNoRec deben tenerse las funciones que manejan el stack.
Esto se realiza en stack.c. Para hacer más flexible el diseño se efectúan algunas definiciones
adicionales.

Conviene definir un archivo con algunos tipos de datos que emplearán las aplicaciones.
/*datos.h> */
#ifndef __DATOS_H__
#define __DATOS_H__

typedef int Tipo; /* tipo de item del arreglo */


typedef int Indice; /* tipo del índice */
typedef Indice ElementoStack;

typedef struct nn1{


Tipo clave;
int ntarea;
} registro, *preg;

#endif /* __DATOS_H__ */
La compilación condicional asegura que este archivo sólo se incluirá una vez.

La funciones que emplean el stack, suelen describirse en un archivo, mostrando los prototipos.
/*stack.h> */
#ifndef __STACK_H__
#define __STACK_H__

#define push2(A,B) StackPush((B)); StackPush((A));


void StackInit(int);
int StackEmpty(void);
int StackFull(void);
void StackPush(ElementoStack);
ElementoStack StackPop(void);
void StackDestroy(void);

Profesor Leopoldo Silva Bijit 30-05-2009


26 Estructuras de Datos y Algoritmos

#endif /* __STACK_H__ */

Luego de los archivos anteriores se describe la implementación del stack.


/*stack.c Implementación basada en arreglos dinámicos. */
#include <stdlib.h>
#include <stdio.h>
#include "datos.h"
#include "stack.h"

static ElementoStack * s; //puntero al tope


static int N; //elementos en el stack
static int MAXN;

void StackInit(int max)


{ s = malloc(max*sizeof(ElementoStack) ); // Se define el arreglo dinámico
if(s== NULL) exit(1);
N=0; MAXN=max; //Se inician .
}

int StackEmpty(void)
{ return( N == 0) ; //Retorna verdadero si stack vacío }

int StackFull(void)
{
return( N == MAXN) ; //Retorna verdadero si stack lleno
}

//se puede empujar algo al stack si no está lleno.


void StackPush(ElementoStack cursor)
{ s[N++]= cursor; }

//se puede sacar algo del stack si no está vacío


ElementoStack StackPop(void)
{
if( StackEmpty() ) {printf("error. extracción stack vacio\n"); exit(1); return;}
else return ( s[--N] ) ;
}

void StackDestroy(void)
{
free(s);
}

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 27
9.2.3.2. Observaciones finales.
Una refinación muy empleada es detener la recursión cuando el tamaño de las particiones es
cercano a 10. Se llama a la función quicksort, y ésta entrega un arreglo parcialmente ordenado.
Luego se invoca a un método tipo n2, pero con n acotado.

Por ejemplo:

void ordene(preg a, Indice left, Indice right)


{
qsort(a, left, right);
insercionSort(a, left, right);
}

En este caso la primera acción de qsort es retornar si el tamaño de la partición es menor que un
valor dado.

Existen numerosas implementaciones de quicksort. Suele estar en las bibliotecas que


acompañan a los lenguajes. Ver qsort en stdlib.h.

9.2.4. Merging y Mergesort (1945)

La acción de unir (merging) es combinar dos estructuras ordenadas en una sola. Suele emplearse
para ordenamiento externo; es decir para ordenar archivos, pero también puede emplearse para
ordenamiento interno. Presenta ventajas en el ordenamiento de listas.
Fue presentado por John Von Neumann en 1945.

En quicksort se selecciona un pivote, y se efectúa una partición respecto del valor de éste, luego
en forma recursiva se ordena la partición izquierda y derecha.
En mergesort se parte la estructura en dos; luego en forma recursiva se ordena la parte izquierda
y derecha, y finalmente se mezclan (merge) las dos partes en una sola ordenada y mayor.

Sus ventajas son: es O(nlog(n)) en peor caso, y es estable; es decir mantiene el orden original
de los elementos repetidos. Los algoritmos heapsort y quicksort no son estables. Sus desventajas
son que requiere un espacio adicional proporcional a n y que su complejidad de mejor caso
también es O(nlog(n)).

Conviene emplearlo cuando se requiere velocidad y cuando no puede tolerarse el


comportamiento O(n2) en peor caso; también cuando se dispone de espacio o cuando se requiere
un ordenamiento estable. Es el método que suele emplearse si sólo se dispone de acceso
secuencial a las componentes, ya sea que se esté ordenando un archivo o una lista simplemente
enlazada.

Su principal tradicional aplicación es mezclar un archivo ordenado de grandes dimensiones (el


maestro) con uno pequeño (transacciones diarias), pero desordenado. Se ordena el pequeño por
alguno de los métodos avanzados, y luego se efectúa la mezcla.

Profesor Leopoldo Silva Bijit 30-05-2009


28 Estructuras de Datos y Algoritmos
9.2.4.1. Mezcla de dos arreglos. Ordenamiento estable.
Se unen dos arreglos a y b, ordenados en forma ascendente, en un arreglo c. Se va pasando al
arreglo c, el menor de los arreglos a y b, a medida que éstos se recorren ascendentemente.

a b

2 4 5 7 8 1 2 3 6

1 2 2 3 4 5 6 7 8
c

Figura 9.26. Mezcla de arreglos.

Si uno de los arreglos se agota, insertar los elementos restantes en c; en caso contrario: escoger
el menor valor entre a[i] y b[j] e insertarlo en c[k], ajustando los índices.

Se emplean tres índices: i para arreglo a, j para arreglo b, y k para el arreglo c.


Se usan como argumentos el número de elementos de los arreglos, n el de a; y m el de b. El
direccionamiento de los arreglos es relativo a la dirección inicial.
a
n
b
i m

c j
n+m

Figura 9.27. Argumentos y variables en merge.

void merge(Item c[], Item a[], int n, Item b[], int m )


{ int i, j, k;
for (i = 0, j = 0, k = 0; k < n+m; k++)
{
if (i == n) { c[k] = b[j++]; continue; }
if (j == m) { c[k] = a[i++]; continue; }
if (a[i] <= b[j]) c[k] = a[i++] ; else c[k] = b[j++];
}
}

Complejidad de la operación de mezclar: O(n+m).

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 29
9.2.4.2. Algoritmo básico estable.
Debido a la gran cantidad de invocaciones en los llamados recursivos, éstos suelen detenerse
cuando los arreglos sean de tamaños menores o iguales a 10. Se elimina el tiempo empleado en
copiar a los arreglos auxiliares conmutando entre poner la salida mezclada en el arreglo auxiliar
o en el arreglo de entrada.
El diseño permite ordenar una parte del arreglo a, desde left a right.

void MergeSort(Item a[], int left, int right)


{ int i;
Item * aux;
aux=(Item *) calloc(right-left+1, sizeof(Item)); if (aux==NULL) exit (1);
for (i = 0; i <= right-left; i++) aux[i] = a[i+left]; //se copia fuente al auxiliar.
mergesortR(a+left, aux, 0, right-left); //destino =a fuente= aux
free(aux);
}
El tamaño del auxiliar es igual al del subarreglo a.

void mergesortR(Item a[], Item b[], int left, int right)


{ int m;
if (right-left <= 10) { InsertSort(a, left, right); return; }
m = (left+right)/2;
mergesortR(b, a, left, m); //alterna arreglo con auxiliar
mergesortR(b, a, m+1, right);
merge(a+left, b+left, m-left+1, b+m+1, right-m); //operación de mezcla estable
}

Se ilustran los punteros y los tamaños al efectuar el llamado a la mezcla.


b+left
m-left+1
b+m+1
right-m

a+left
right-left+1

Figura 9.28. Argumentos en invocación a merge.

Se ilustra el proceso de subdivisión descendente de los arreglos mediante los llamados


recursivos. Como se desea ver cómo se forman los subarreglos, se cambia la línea que efectúa el
algoritmo cuadrático de ordenamiento para detener la recursión, mediante:

if (right-left <= 0) { InsertSort(a, left, right); return; }

Profesor Leopoldo Silva Bijit 30-05-2009


30 Estructuras de Datos y Algoritmos
Para el arreglo de la Figura 9.29, con left igual a 0 y right igual a 4, la división entera de (0+4)/2
resulta 2, entonces la primera división del arreglo es entre índices 0 y 2, y entre 3 y 4. Esta
división se muestra en el primer nivel bajo el arreglo original.
7 2 8 5 4

7 2 8 5 4

7 2 8 5 4

7 2

Figura 9.29. Formación subarreglos.

Luego se van mezclando los subarreglos, en forma ascendente.

2 4 5 7 8

2 7 8 4 5

2 7 8 5 4

7 2

Figura 9.29. Mezclas subarreglos.

Complejidad.
La complejidad de la mezcla o merge es O(n).

La complejidad de mergesort es la suma de los dos llamados recursivos más la operación de


mezclar, es decir:
T(n) = 2T(n/2) + n con T(1)=0.

Resolviendo esta ecuación de recurrencia, resulta complejidad temporal O(n log(n)).

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 31

Se emplea un espacio adicional proporcional a n. El algoritmo no depende del orden inicial de la


entrada y es estable si la función que efectúa la mezcla es estable.
9.2.4.3. Mezcla in situ. ( In-Place Merge. Ordenamiento no estable)
El algoritmo anterior asume tres arreglos disjuntos lo cual requiere el espacio adicional del
arreglo c. Es difícil realizar la mezcla empleando solamente el espacio de los arreglos a y b
(mezcla in situ), por esto suele emplearse un arreglo auxiliar.

Supongamos que deseamos efectuar la mezcla desde a[left] hasta a[mitad] con el subarreglo
a[mitad+1] hasta a[right].
a[left], a[left+1], …., a[mitad] y a[mitad+1], a[mitad+2], …., a[right]
Y luego dejar el resultado en el mismo arreglo a.

En estas condiciones es más costoso discernir cuando se han agotado los arreglos izquierdo y
derecho, suelen usarse centinelas con este propósito. Una alternativa es copiar los valores a un
arreglo auxiliar, pero el segundo arreglo en orden inverso.

Los valores en el arreglo auxiliar quedan en el siguiente orden:


a[left], a[left+1], …., a[mitad] , a[right], …a[mitad+2], a[mitad+1]

De esta forma, como se verá, no se requieren centinelas y se evitan los test para ver si se llegó o
no al final de los arreglos, mediante los cursores i (progresivo) y j (regresivo). La dificultad es
que este método deja no estable al algoritmo.

void mergeI(Item a[], int left, int mitad, int right)


{ int i, j, k;
static Item aux[maxN];
for (i = mitad+1; i > left; i--) aux[i-1] = a[i-1]; //copia parte izquierda desde m a left
for (j = mitad; j < right; j++) aux[right+mitad-j] = a[j+1]; // copia en orden inverso

//se tienen: i=left, j= right


for (k = left; k <= right; k++) //se mezcla en arreglo a
if (aux[j] < aux[i]) a[k] = aux[j--];
else a[k] = aux[i++];
}
9.2.4.4. Top-Down Mergesort
Se divide el arreglo en mitades, se ordenan recursivamente las mitades, y luego se las mezcla.

void mergesort(Item a[], int left, int right)


{ int m = (right+left)/2;
if (right <= left) return;
mergesort(a, left, m);
mergesort(a, m+1, right);
mergeI(a, left, m, right);
}

Profesor Leopoldo Silva Bijit 30-05-2009


32 Estructuras de Datos y Algoritmos
9.2.4.5. Bottom-Up Mergesort
Se pasa iterativamente a través del arreglo, mezclando subarreglos adyacentes; el tamaño de los
subarreglos se dobla en cada pasada.

void mergesortBU(Item a[], int left, int right)


{ int m, i;
for (m = 1; m <= right-left; m = m+m)
for (i = left; i <= right-m; i += m+m)
mergeI(a, i, i+m-1, min(i+m+m-1, right));
}
9.2.4.6. Comportamiento de Mergesort
A través de mediciones puede comprobarse que quicksort es casi el doble más rápido que
mergesort. El detener las iteraciones o recursiones para subarreglos menores que un valor,
reduce el tiempo en casi un 15 %. El algoritmo top-down es más eficiente que el bottom-up.

9.2.4.7. Implementación de Merging para listas enlazadas.


Se mezclan dos listas apuntadas por a y b. Retorna un puntero a la nueva lista ordenada.

pnodo merge(pnodo a, pnodo b)


{ nodo cabecera;
pnodo head = &cabecera, c = head;
while ((a != NULL) && (b != NULL))
if (a->item < b->item)
{ c->next = a; c = a; a = a->next; }
else { c->next = b; c = b; b = b->next; }
c->next = (a == NULL) ? b : a;
return head->next;
}

9.2.5. Ordenamientos por cuenta. CountSort.

Para ordenar arreglos formados por enteros positivos entre 0 y k, se han obtenido algoritmos
de complejidad O(n). Uno de ellos está basado en contar las ocurrencias de las claves, y
mediante éstas llevar los números a sus posiciones. Es un algoritmo estable.

Analizaremos la estrategia, mediante un ejemplo. Se tiene un arreglo a, de 5 elementos, el


mayor elemento tiene valor 8:
0 1 2 3 4

a 7 2 8 5 4

Figura 9.30. Arreglo a.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 33
Se cuentan las ocurrencias de las claves en un arreglo auxiliar c de (k+1) posiciones:
0 1 2 3 4 5 6 7 8

c 0 0 1 0 1 1 0 1 1

Figura 9.31. Ocurrencias de claves.

Se modifica el arreglo c, de tal modo que el contenido sea el número de claves menores que
el índice. De este modo el contenido determina la posición del valor del índice.
0 1 2 3 4 5 6 7 8

c 0 0 0 1 1 2 3 3 4

Figura 9.32. Posiciones de índices.

Se copian los datos del arreglo a, a su posición en otro arreglo auxiliar b, mediante:

b[ c[ a[i] ] ] = a[i]

0 1 2 3 4

a 7 2 8 5 4

Figura 9.33. Copia al arreglo b.

Como pueden existir claves repetidas, se va incrementando en uno la posición en el arreglo c;


esto se logra con: b[ c[ a[i] ] ++] = a[i];

Luego se copia el arreglo b, en el arreglo original. Las claves repetidas mantienen las posiciones
que tenían en el arreglo original, por lo cual es un ordenamiento estable.

Se requieren dos arreglos auxiliares con un total de (n+k+1) posiciones.

// Las n claves deben tener valores entre 0 y k.


// Requiere n+k+1 posiciones adicionales.
// Operaciones = 3n+k = O(n+k)
void countSort(int a[], int k, int n)
{ int i, tmp, cuenta = 0;
int *b;

Profesor Leopoldo Silva Bijit 30-05-2009


34 Estructuras de Datos y Algoritmos
int *c;
b = (int*)calloc(n, sizeof(int)); if (b==NULL) exit(1);
c = (int*)calloc(k+1, sizeof(int)); if (c==NULL) exit(1);

for (i = 0; i < n; i++) // cuenta número de claves


c[ a[i] ]++;
//mostrar(c, 0, k);
for (i = 0; i <= k; i++) {// calcula número de claves < valor del índice i
tmp = cuenta + c[i];
c[i] = cuenta;
cuenta = tmp;
}
//mostrar(c,0,k);
for (i = 0; i < n; i++)
b[ c[ a[i] ]++] = a[i]; // mueve claves a su localización.
//Postincrementa c[a[i]] para tratar caso de claves iguales.
//mostrar(b, 0, n-1);

for (i = 0; i < n ; i++) // copia arreglo ordenado hacia a


a[i] = b[i];

free(c);
free(b);
}

Problemas resueltos.

P9.1.

Se tiene un arreglo de registros con la siguiente estructura.


typedef struct nn1{
unsigned int clavep;
unsigned int claves;
} registro, *pregistro;

Las claves primaria y secundaria son enteros menores que 100.

Modificar la rutina quicksort, tanto en sus tipos como en el algoritmo, para que ordene un
arreglo de registros ordenando por la clave primaria en forma ascendente y por la clave
secundaria en forma descendente.

El siguiente ejemplo muestra a la izquierda antes de ordenar, y a la derecha después de ordenar.

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 35

Clavep Claves Clavep Claves


2 3 1 5
1 3 1 3
2 4 2 4
1 5 2 3
3 2 3 2

Solución.

Como los elementos son menores que 100, puede formarse una clave compuesta del siguiente
modo:

#define aclave(i) a[(i)].clavep*100 + (100-a[(i)].claves)


#define pivclave piv.clavep*100 + (100-piv.claves)

Agregando una columna con la clave compuesta:

Clavep Claves Clave compuesta


2 3 297
1 3 197
2 4 296
1 5 195
3 2 398

Al ordenar en forma ascendente por la clave compuesta resulta lo que se pide.


Clavep Claves Clave compuesta
1 5 195
1 3 197
2 4 296
2 3 297
3 2 398

Sólo se requiere modificar la sección que efectúa la comparación.


void qsort(int l, int r)
{
int i=l, j=r;
registro piv=a[(l+r)/2)];
do {
while ( aclave(i) < pi.clave ) i++;
while( pivclave < aclave(j) ) j--;
if( i<=j {swap(i, j) ; i++; j--;}
} while(i<=j);
if( l < j) qsort( l, j);
if( i< r ) qsort( i, r);
}

Profesor Leopoldo Silva Bijit 30-05-2009


36 Estructuras de Datos y Algoritmos
Otra solución.
Si se definen dos funciones, que ordenen en forma ascendente y descendente:
void qsorta(preg a, Indice l, Indice r)
{ Indice i=l, j=r;
registro temp;
registro piv=a[(l+r)/2];
do {
while ( a[i].clavep < piv.clavep) i++; ////ordena ascendente por clavep
while( piv.clavep < a[j].clavep) j--;
if( i<=j)
{ if (i!=j) {temp=a[i], a[i]= a[j], a[j]=temp;}
i++; j--;
}
} while(i<=j);
if( l < j) {qsorta( a, l, j);izq++;};
if( i < r ) {qsorta( a, i, r);der++;};
}
void qsortd(preg a, Indice l, Indice r)
{ Indice i=l, j=r;
registro temp;
registro piv=a[(l+r)/2];
do {
while ( a[i].claves > piv.claves) i++; //ordena descendente por claves
while( piv.claves > a[j].claves) j--;
if( i<=j)
{ if (i!=j) {temp=a[i], a[i]= a[j], a[j]=temp;}
i++; j--;
}
} while(i<=j);
if( l < j) {qsortd( a, l, j);izq++;};
if( i < r ) {qsortd( a, i, r);der++;};
}

Se puede implementar el subordenamiento de las claves secundarias, cuando previamente se ha


ordenado por clave primaria.

void qsort(preg a, Indice l, Indice r)


{ Indice i,j;
qsorta(a, l, r);
for(i=l;i<r;i++)
{
j=i;
while(a[i].clavep==a[i+1].clavep) i++;
if (i>j) qsortd(a,j,i);
}
}

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 37
Ejercicios propuestos.

E9.1.

Para el siguiente arreglo de enteros:

4 5 1 4 3 1 5 6 2 3

Efectuar una traza de qsort indicando los subarreglos que resultan antes de los llamados
recursivos. Hacia abajo debe reflejarse el tiempo.

E9.2.

Explicar cómo se forma un heap a partir de un arreglo desordenado.

E9.3

Diseñar, en forma recursiva, la rutina genheap( A, i, n) , que al ser invocada mantiene la


propiedad de heap, desde el elemento i hacia abajo, hasta llegar a una hoja. Esto en un arreglo A,
cuando el último elemento del heap es n.

La propiedad del heap debe ser:


Para cada nodo del heap, diferente a la raíz, el valor del padre es mayor o igual que el del
nodo.
Indicar cómo se sale de la rutina recursiva.

Referencias.

Shell, D. L. “A high-speed sorting procedure”. Communications of the ACM 2, 30–32. 1959.

Knuth, D. E. “The Art of Computer Programming. Vol. 3: Sorting and Searching.” Addison-
Wesley, Reading, MA, 1998.

Marcin Ciura, “Best Increments for the Average Case of Shellsort”, 13th International
Symposium on Fundamentals of Computation Theory, Riga, Latvia, Aug 22 2001.

J.W.J. Williams. “Algorithm 232”. Communications of the ACM, 7; 347-348; 1964.


Robert Floyd. “Algorithm 245”. Communications of the ACM, 7; 701; 1964.

Hoare, C. A. R. "Partition: Algorithm 63," "Quicksort: Algorithm 64," and "Find: Algorithm
65." Comm. ACM 4, 321-322, 1961.

H. H. Goldstine and J. von Neumann. “Planning and coding of problems for an electronic
computing instrument. Part II Volume 2”. Reprinted in “John von Neumann Collected Works
Volume V. Design of Computers. Theory of Automata and Numerical Analysis”. Pergamon
Press. Oxford. England. pp 152-214. 1963.

Profesor Leopoldo Silva Bijit 30-05-2009


38 Estructuras de Datos y Algoritmos
Animaciones a algoritmos de ordenamiento.
http://cg.scs.carleton.ca/~morin/misc/sortalg/

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 39

Índice general.

CAPÍTULO 9 ............................................................................................................................................. 1
ORDENAR ................................................................................................................................................. 1
9.1. MÉTODOS DIRECTOS. ......................................................................................................................... 2
9.1.1. Selección.................................................................................................................................... 2
9.1.1.1. Algoritmo:........................................................................................................................................... 2
9.1.1.2. Operación............................................................................................................................................ 2
9.1.1.3. Ejemplo............................................................................................................................................... 2
9.1.1.4. Análisis de complejidad. ..................................................................................................................... 3
9.1.2. Intercambio. .............................................................................................................................. 4
9.1.2.1. Algoritmo bubblesort de una pasada................................................................................................... 4
9.1.2.2. Operación............................................................................................................................................ 4
9.1.2.3. Análisis de complejidad. ..................................................................................................................... 4
9.1.2.4. Algoritmo shakesort o bubblesort de dos pasadas:.............................................................................. 4
9.1.3. Inserción.................................................................................................................................... 5
9.1.3.1. Declaraciones de tipos y macros. ........................................................................................................ 5
9.1.3.2. Operación............................................................................................................................................ 5
9.1.3.3. Ejemplo de inserción directa............................................................................................................... 5
9.1.3.4. Análisis de complejidad. ..................................................................................................................... 6
9.1.3.5. Inserción binaria. ................................................................................................................................ 7
9.1.4. Shellsort. (1959) ........................................................................................................................ 7
9.1.4.1. Shell Sort original. .............................................................................................................................. 8
9.1.4.2. Selección de la secuencia de valores para h. ....................................................................................... 8
9.1.4.3. Shell sort. Knuth. ................................................................................................................................ 9
9.1.4.4. Shell sort Ciura. .................................................................................................................................. 9
9.2. MÉTODOS AVANZADOS.................................................................................................................... 10
9.2.1. Heapsort (1964). ..................................................................................................................... 10
9.2.1.1. Formación del heap. Revisión de subárboles. ................................................................................... 10
9.2.1.2. Ordenamiento del heap. .................................................................................................................... 13
9.2.2. Quicksort (1961). .................................................................................................................... 14
9.2.2.1. Partición............................................................................................................................................ 14
Traza de la ejecución del segmento partición............................................................................................ 15
Traza con valores repetidos en la partición. .............................................................................................. 16
Traza con valores repetidos del pivote en la partición. ............................................................................. 17
Sobre la elección del pivote. ..................................................................................................................... 19
9.2.2.2. Ordenamiento usando recursión........................................................................................................ 19
9.2.2.3. Complejidad en Peor caso................................................................................................................. 20
9.2.2.4. Complejidad en Caso promedio. ....................................................................................................... 21
9.2.2.5. Variantes de quicksort....................................................................................................................... 23
9.2.3. Quicksort. No recursivo. ......................................................................................................... 24
9.2.3.1. Stack de usuario. ............................................................................................................................... 25
9.2.3.2. Observaciones finales. ...................................................................................................................... 27
9.2.4. Merging y Mergesort (1945) ................................................................................................... 27
9.2.4.1. Mezcla de dos arreglos. Ordenamiento estable. ................................................................................ 28
9.2.4.2. Algoritmo básico estable................................................................................................................... 29

Profesor Leopoldo Silva Bijit 30-05-2009


40 Estructuras de Datos y Algoritmos
Complejidad. .............................................................................................................................................30
9.2.4.3. Mezcla in situ. ( In-Place Merge. Ordenamiento no estable).............................................................31
9.2.4.4. Top-Down Mergesort........................................................................................................................31
9.2.4.5. Bottom-Up Mergesort .......................................................................................................................32
9.2.4.6. Comportamiento de Mergesort..........................................................................................................32
9.2.4.7. Implementación de Merging para listas enlazadas. ...........................................................................32
9.2.5. Ordenamientos por cuenta. CountSort.....................................................................................32
PROBLEMAS RESUELTOS. ........................................................................................................................34
P9.1....................................................................................................................................................34
EJERCICIOS PROPUESTOS.........................................................................................................................37
E9.1....................................................................................................................................................37
E9.2....................................................................................................................................................37
E9.3....................................................................................................................................................37
REFERENCIAS..........................................................................................................................................37
ÍNDICE GENERAL. ....................................................................................................................................39
ÍNDICE DE FIGURAS. ................................................................................................................................41

Profesor Leopoldo Silva Bijit 30-05-2009


Ordenar 41

Índice de figuras.

FIGURA 9.1. INVERSIONES............................................................................................................................. 1


FIGURA 9.2. EJEMPLOS DE ORDENAMIENTO POR SELECCIÓN......................................................................... 3
FIGURA 9.3. EJEMPLOS ORDENAMIENTO POR INSERCIÓN. ............................................................................. 6
FIGURA 9.4. VISUALIZACIÓN DE UN ARREGLO DESORDENADO COMO UN ÁRBOL BINARIO. ......................... 10
FIGURA 9.5. REVISIÓN DEL PRIMER SUBÁRBOL (I=4). ................................................................................. 11
FIGURA 9.6. REVISIÓN DEL SEGUNDO SUBÁRBOL (I=3)............................................................................... 11
FIGURA 9.7. REVISIÓN DEL TERCER SUBÁRBOL (I=2).................................................................................. 12
FIGURA 9.8. REVISIÓN DEL CUARTO SUBÁRBOL (I=1). ................................................................................ 12
FIGURA 9.13. PIVOTE EN PARTICIÓN. .......................................................................................................... 14
FIGURA 9.14. ELEMENTOS AL INICIO........................................................................................................... 15
FIGURA 9.15. ELEMENTOS DESPUÉS DE LOS PRIMEROS DOS WHILE. ............................................................ 15
FIGURA 9.16. ELEMENTOS DESPUÉS DEL IF. ................................................................................................ 15
FIGURA 9.17. ELEMENTOS DESPUÉS DE LA SEGUNDA ITERACIÓN................................................................ 16
FIGURA 9.18. TERCERA ITERACIÓN, DESPUÉS DE LOS DOS WHILE. .............................................................. 16
FIGURA 9.19. TERCERA ITERACIÓN, DESPUÉS DEL IF................................................................................... 16
FIGURA 9.20. PARTICIÓN CON VALORES REPETIDOS. .................................................................................. 17
FIGURA 9.21. PRIMERA ITERACIÓN, DESPUÉS DE LOS WHILE....................................................................... 17
FIGURA 9.22. AL FINAL DE LA PRIMERA ITERACIÓN.................................................................................... 17
FIGURA 9.23. DESPUÉS DE LA SEGUNDA ITERACIÓN. .................................................................................. 17
FIGURA 9.23.A. PIVOTE CON REPETICIONES. ............................................................................................... 18
FIGURA 9.23.B. LUEGO DE LOS DOS WHILE. ................................................................................................ 18
FIGURA 9.23.C. DESPUÉS DEL IF.................................................................................................................. 18
FIGURA 9.23.D. SEGUNDA ITERACIÓN......................................................................................................... 18
FIGURA 9.23.E. AL TERMINAR LA ITERACIÓN.............................................................................................. 19
FIGURA 9.24. PEOR CASO, SÓLO SE INVOCA POR LA IZQUIERDA. ................................................................. 20
FIGURA 9.25. COMPLEJIDAD EN CASO PROMEDIO DE QUICKSORT............................................................... 22
FIGURA 9.26. MEZCLA DE ARREGLOS. ........................................................................................................ 28
FIGURA 9.27. ARGUMENTOS Y VARIABLES EN MERGE. ............................................................................... 28
FIGURA 9.28. ARGUMENTOS EN INVOCACIÓN A MERGE. ............................................................................. 29
FIGURA 9.29. FORMACIÓN SUBARREGLOS. ................................................................................................. 30
FIGURA 9.29. MEZCLAS SUBARREGLOS. ..................................................................................................... 30
FIGURA 9.30. ARREGLO A. .......................................................................................................................... 32
FIGURA 9.31. OCURRENCIAS DE CLAVES. ................................................................................................... 33
FIGURA 9.32. POSICIONES DE ÍNDICES. ....................................................................................................... 33
FIGURA 9.33. COPIA AL ARREGLO B. ........................................................................................................... 33

Profesor Leopoldo Silva Bijit 30-05-2009


1

Capítulo 10

Grafos.

Los grafos enfatizan las relaciones complejas de conexión entre elementos de datos. Los árboles
son un caso simple de grafos.
Los grafos permiten estudiar problemas como: menor distancia o costo entre puntos; búsquedas
a través de enlaces en la web; redes eléctricas, cableado de impresos; itineración de tareas,
siguiendo trayectorias; sintonizar o relacionar postulantes con ofertas, partes con proveedores;
conectividad entre pares de sitios en la red; especificación de aplicaciones mostrando las
relaciones entre subsistemas; etc.

10.1. Definiciones.

Quedan definidos por un conjunto de vértices V, más un conjunto de elementos E que conectan
pares de vértices distintos. El grafo se describe por: G(V, E).

Vértices
elemento
adyacentes

vértice
Elemento
incidente
en vértice
Figura 10.1. Elemento, vértice, incidencia.

Grado de incidencia, del vértice, es el número de elementos que son incidentes en ese vértice.

En grafos simples se aceptan sólo un enlace entre un par de vértices diferentes. Es decir, no se
aceptan lazos (elementos adyacentes en el mismo vértice), ni elementos en paralelo (dos
elementos que son adyacentes con un mismo par de vértices.

Trayectoria: Secuencia de elementos entre vértices adyacentes.


Trayectoria simple: los vértices y los elementos están presentes sólo una vez.
Circuito o ciclo: Trayectoria simple, salvo que el vértice inicial y final son uno solo.
Largo de la trayectoria es el número de elementos que la componen.

Grafo conectado: Si existe trayectoria entre cualquier par de vértices.


Árbol de un grafo: Subgrafo conectado sin circuitos.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Densidad es el promedio de los grados de incidencia de los vértices. Si sumamos los grados de
incidencia se tendrá el valor 2E (se cuenta dos veces cada elemento). Entonces la densidad
resulta: 2E/V.

Se dice que un grafo es denso si su densidad en proporcional a V. Esto implica que el número
de elementos E será proporcional a V2. En caso contrario es un grafo liviano (sparse).

El concepto de densidad tiene relación con el tipo de algoritmo y la estructura de datos que se
empleará para representar el grafo. Complejidades típicas son V2 y E*logE, la primera es más
adecuada a grafos densos; y la segunda a grafos livianos.

Grafos dirigidos: Los elementos tienen asociada una dirección.


a b a b

Elemento= (a, b) Elemento= (b, a)


Figura 10.2. Elementos orientados.

Grafos ponderados: Los elementos ( w(E) ) o los vértices tienen asociado una distancia o
costo.

Árbol de cobertura (Spanning tree) de un grafo conectado, con pesos y no dirigido, es un árbol
cuyo peso se calcula según:
w(T ) w( E )
E T

De los innumerables problemas asociados a grafos se estudiarán dos.


El mínimo árbol de cobertura, en grafos con peso pero no dirigidos.
La ruta mínima: en grafos con peso y dirigidos.

10.2. Representaciones.

Para grafos densos se prefiere la matriz de adyacencias, para grafos livianos suele emplearse
una lista de adyacencias.

10.2.1. Matriz de adyacencia de los elementos en los vértices.

Se emplea una matriz cuadrada (V-1, V-1) donde V es el número de vértices.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 3
a 0 1 .. w .. V-1
0
1
..
v 1
..
V-1

Figura 10.3. Matriz de adyacencias.

Se coloca un 1 en (v, w) si existe elemento de v hacia w; 0 en caso contrario.


La matriz es simétrica si el grafo no es dirigido. Si no se aceptan lazos, la diagonal está formada
por ceros.

10.2.2. Matrices estáticas en C.

La notación: a[v][w] recuerda que la matriz a se visualiza como un arreglo de v renglones,


donde cada renglón está formado por w columnas.

Ejemplo:
La matriz m se define según:
int m[2][4];

2x4

Figura 10.4. Matriz de 2 renglones por 4 columnas.

Se almacenan por renglones. La siguiente figura ilustra el almacenamiento de los elementos del
arreglo, con direcciones crecientes hacia abajo:
m[0][0]

m[0][3]
m[1][0]

m[1][3]

Figura 10.5. Forma de almacenamiento de matrices.

Índice más derechista varía más rápido, si se accesa según el orden de almacenamiento.

Se inicializan mediante listas:


int m[2][4]={{0,1,2,3},{4,5,6,7}};
Matrices como argumentos.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
Si se pasa una matriz a una función, la declaración del argumento debe incluir la dimensión de
la columna. La dimensión del renglón es irrelevante, ya que se pasa un puntero.
f(int m[ ] [4]) ó f(int (*m)[4])

10.2.3. Matrices dinámicas en C.

Ver Sedgewick, cap. 17.

Las siguientes declaraciones describen un grafo a través de una matriz de adyacencias.


Declaración de estructuras de datos para un grafo.
struct graph {
int V; //Número de vértices
int E; //Número de elementos
int **adj; // Matriz de adyacencias
};

typedef struct graph *Graph;

Definición de variables.
Las siguientes definiciones crean el espacio. Se define un grafo G, cuya matriz de adyacencias
se define según un arreglo de r punteros para almacenar las direcciones de los arreglos de c
elementos:

Graph G = malloc(sizeof *G); //crea la cabecera del grafo.


int **t = malloc(r * sizeof(int *)); //crea arreglo de r renglones de punteros
G->adj = t; //Pega el arreglo de punteros

//crea y pega los renglones de c columnas:


for (i = 0; i < r; i++) t[i] = malloc(c * sizeof(int));

El siguiente diagrama ilustra las variables. Se ha omitido la casilla asociada a t.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 5
G

V
E t
adj j
i

t[i][j] ó **(t+i+j)

t[i] ó *(t+i)

Figura 10.6. Estructura para matriz de adyacencias.

También se puede definir una matriz estática (sin llamar a malloc()) : como un arreglo de r
punteros que apuntan a arreglos con la información de las columnas.

10.2.4. Funciones para grafos descritos por su matriz de adyacencias.

La función creación de un grafo vacío de V vértices:


10.2.4.1. Creación.

Graph GRAPHinit(int V)
{ Graph G = malloc(sizeof *G); //crea cabecera del grafo
G->V = V; G->E = 0; //Con V vértices y 0 elementos
G->adj = MATRIXint(V, V, 0); //Lo inicia con ceros.
return G;
}

Donde MATRIXint crea la matriz de incidencias de r renglones, c columnas y la inicializa con


val.
//Asignación dinámica de arreglo bidimensional
int **MATRIXint(int r, int c, int val)
{ int i, j;
int **t = malloc(r * sizeof(int *));
for (i = 0; i < r; i++)
t[i] = malloc(c * sizeof(int)); // t[i] equivale a *(t+i)
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
t[i][j] = val; //equivale a **(t+i+j) = val;
return t;
}

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

De complejidad: O(r + r*c)

La siguiente función, libera el espacio asociado al grafo, cuidando de borrar en orden inverso al
solicitado, de tal modo de no perder las referencias.
10.2.4.2. Liberación del espacio.
void BorreGrafo(Graph G)
{ int i;
int **t = G->adj;
for (i = 0; i < G->V; i++) free(t[i]); //primero borra los renglones
free(t); //luego el arreglo de punteros a los renglones
free(G); //finalmente la cabecera
}

Si se tiene la definición:
Graph Grafo;

Puede crearse la matriz de adyacencias del grafo mediante:


#define VERTICES 5
Grafo = GRAPHinit(VERTICES);
10.2.4.3. Definición de un elemento.
Un elemento puede describirse por:
typedef struct
{
int v; //vértice inicial. Desde.
int w; //vértice final. Hasta.
} Edge;

Con el siguiente constructor:

Edge EDGE(int v, int w)


{ Edge t;
t.v=v; t.w=w;
return (t);
}

La siguiente definición crea un elemento, y EDGE lo inicializa.


Edge elemento;
elemento = EDGE(1,2);

10.2.4.4. Inserción de elemento en un grafo.


La siguiente función inserta un elemento e en un grafo G.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 7
void GRAPHinsertE(Graph G, Edge e)
{
if (G->adj[e.v][e.w] == 0) G->E++; //aumenta el número de elementos del grafo
G->adj[e.v][e.w] = 1;
G->adj[e.w][e.v] = 1; //si el grafo no es dirigido.
}

10.2.4.5. Eliminación de elemento.


La función siguiente remueve el elemento e del grafo G:

void GRAPHremoveE(Graph G, Edge e)


{
if (G->adj[e.v][e.w] == 1) G->E--; //Disminuye el contador de elementos.
G->adj[e.v][e.w] = 0;
G->adj[e.w][e.v] = 0;
}

La acción siguiente, inserta el elemento en el Grafo:


GRAPHinsertE(Grafo, elemento);

10.2.4.6. Creación de los elementos.


La siguiente definición crea el conjunto de elementos de un grafo, como un arreglo denominado
Elementos. Esta es otra forma de definir un grafo. Requiere 2E datos para ser definida, en lugar
de V2 que necesita la matriz de incidencia.

#define ELEMENTOS 6
Edge Elementos[ELEMENTOS]={{1,2},{1,4},{2,3},{3,4},{4,0},{3,0} };

elemento v w
0 1 2
1 1 4
2 2 3
3 3 4
4 4 0
5 3 0

Figura 10.7. Grafo descrito por sus elementos.

Esta descripción es única, en el sentido que asocia nombres de elementos con nombres de
vértices.
Entonces la creación de la matriz de incidencia de un grafo de cinco vértices, a partir del arreglo
de elementos, definido por seis elementos, puede realizarse, según:

for(i=0; i<ELEMENTOS; i++) GRAPHinsertE(Grafo, Elementos[i] );

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
que tiene complejidad O(E).

Mostrando que la matriz de incidencia puede obtenerse del arreglo de los elementos, lo cual
indica que son representaciones equivalentes.

El grafo resultante, puede visualizarse, según:

1 0 2

1 4
3 2
4
5
0 3

Figura 10.8. Grafo para elementos de la figura 10.7.

10.2.4.7. Despliegue de un grafo. Lista de vértices.


Para visualizar el grafo, puede generarse una lista de los vértices: con los vértices que tienen
conectados elementos a ese vértice.

//muestra la matriz como listas de vértices que tienen conexión con cada vértice.
void GRAPHshowL(Graph G)
{ int i, j;
printf("%d vertices, %d edges\n", G->V, G->E); //número total de elementos y vértices.
for (i = 0; i < G->V; i++)
{ printf("%2d:", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] == 1) printf(" %2d", j);
putchar(‘\n’);
}
}

De complejidad O(V2).

El cual entregaría el siguiente listado.


5 vertices, 6 edges
0: 3 4
1: 2 4
2: 1 3
3: 0 2 4
4: 0 1 3

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 9

10.2.4.8. Despliegue de la matriz de incidencias.


//Muestra Matriz de incidencia.
void GRAPHshowM(Graph G)
{ int i, j;
printf("%d vertices, %d edges\n", G->V, G->E);
printf(" ");
for (j = 0; j < G->V; j++) printf(" %2d", j);
putchar(‘\n’);
for (i = 0; i < G->V; i++)
{
printf("%2d:", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] == 1) printf(" %2d", 1); else printf(" %2d", 0);
putchar(‘\n’);
}
}

Que genera, para el ejemplo:


5 vertices, 6 edges
0 1 2 3 4
0: 0 0 0 1 1
1: 0 0 1 0 1
2: 0 1 0 1 0
3: 1 0 1 0 1
4: 1 1 0 1 0
10.2.4.9. Generación de los elementos a partir de la matriz de incidencias.
Si el grafo ya está construido, la generación de los elementos, a partir del grafo, se logra con la
función:

int GRAPHedges(Edge a[], Graph G)


{ int v, w, E = 0; //numera los elementos desde el cero.
for (v = 0; v < G->V; v++) //Para todos los renglones
for (w = v+1; w < G->V; w++) //revisa por columnas
if (G->adj[v][w] == 1)
a[E++] = EDGE(v, w); //escribe por referencia
return E; //retorna el número de elementos.
}

Se advierte que debido a los dos for anidados es O( (V2-V)/2 ), ya que revisa sobre la diagonal.

Encontrar los elementos a partir del grafo se realiza con:


GRAPHedges(Elementos, Grafo); //Llena el arreglo a partir del Grafo.

Nótese que al recorrer la submatriz sobre la diagonal, por renglones, va reasignando, a partir de
cero, los nombres de los elementos, y sus correspondientes vértices.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

0 1 2 3 4 elemento v w 1 2
0 0 3
2
0 0 0 0 1 1
1 0 0 1 0 1 1 0 4
3 4
2 1 2 4
2 0 1 0 1 0 5
3 1 4 1
3 1 0 1 0 1
4 2 3
4 1 1 0 1 0 0
5 3 4 0 3
Figura 10.9. Generación de los elementos a partir de matriz de adyacencias.

Debido a que la matriz de adyacencias no almacena información sobre el nombre de los


elementos, el arreglo de elementos toma los nombres dados por el recorrido.

10.3. Trayectorias en grafos.

Un primer problema es determinar si existe una trayectoria entre dos vértices.


Si se define un arreglo en que se marque si los nodos han sido o no visitados, puede plantearse
el siguiente esquema recursivo:

Si el vértice inicial y el final son iguales, hay trayectoria (fin de recursión).


Marcar el vértice inicial como visitado.
Revisar todos los vértices, conectados al inicial:
Si uno no ha sido revisado: ver si hay trayectoria entre ese y el final.
Si revisados todos no hay trayectoria, entonces no existe la trayectoria buscada.

int pathR(Graph G, int v, int w)


{ int t;
if (v == w) return 1; //Existe trayecto. Nodo inicial y final son iguales.
visited[v] = 1;
for (t = 0; t < G->V; t++)
if (G->adj[v][t] == 1) //Si v está conectado con t
if (visited[t] == 0) //y t no ha sido visitado
{ printf("%d-%d ", v, t); //Debug: Muestra elemento de trayectoria en prueba.
if (pathR(G, t, w)) return 1;
}
return 0;
}

int GRAPHpath(Graph G, int v, int w)


{ int t;
for (t = 0; t < G->V; t++) visited[t] = 0;
return pathR(G, v, w); //Inicia búsqueda recursiva.
}

Un ejemplo de invocación:

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 11
if( GRAPHpath(Grafo, 1, 3))
printf("existe trayectoria, entre 1 y 3\n");
else printf("no existe trayectoria.\n");

Existen dos formas básicas de exploración de un grafo: La búsqueda primero en profundidad


(Depth-first search) y la búsqueda primero en extensión (Breadth –first search).

10.3.1. Búsqueda primero en profundidad.

10.3.1.1. Definición de búsqueda primero en profundidad.


Se recorre el grafo, siempre hacia adelante hasta que se llega al final o hasta una trayectoria que
ya se ha recorrido; luego se devuelve y busca trayectorias no exploradas. El objetivo es buscar
un árbol.

Se visita y marca un vértice como visitado, luego se visitan (recursivamente) todos los vértices
adyacentes al recién marcado que no estén visitados. Esto va formando un árbol, con el orden en
que se van visitando los vértices.

Para el grafo a la izquierda de la Figura 10.10.:


Si el vértice inicial es el 0 (se pasa el elemento (0,0), se lo visita y marca con la cuenta 0, y se
generan llamados con los elementos (0,3) y (0,4), en ese orden.

1 2 1 3 2

4 4
4
2

1
0 3 0 3
Figura 10.10. Orden de visitas, generación de llamados.

El llamado con el elemento (0, 3) marca el 3 con la cuenta 1, y se generan los llamados: con (3,
2), (3,4).
El llamado con el elemento (3, 2) marca el 2 con la cuenta 2, y se genera el llamado: (2,1).
El llamado con el elemento (2, 1) marca el 1 con la cuenta 3, y se genera el llamado: (1,4).
El llamado con el elemento (1, 4) marca el 4 con la cuenta 4 y no genera nuevos llamados.

Resultando el árbol T(1, 2, 3, 4) ilustrado en la figura a la derecha.

10.3.1.2. Diseño de la operación.


La función recursiva:

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
void dfsR(Graph G, Edge e)
{ int t, w = e.w;
pre[w] = cnt++; //Marca con contador creciente
for (t = 0; t < G->V; t++)
{
if (G->adj[w][t] != 0) //Si hay conexión entre w y t
if (pre[t] == -1) dfsR(G, EDGE(w, t)); //Y si t no está visitado sigue buscando.
}
}

La función que efectúa la búsqueda en profundidad:


void GRAPHsearchDFS(Graph G) //depth-first-search
{ int v;
cnt = 0; //es variable global
for (v = 0; v < G->V; v++) pre[v] = -1; //Marca todos como no visitados
for (v = 0; v < G->V; v++) //Revisa todos los vértices.
if (pre[v] == -1)
dfsR(G, EDGE(v, v)); //Se invoca varias veces si el grafo es no conectado.
}

Se visitan todos los elementos y todos los vértices conectados al vértice de partida, no
importando el orden en que revisa los elementos incidentes en ese vértice. La estrategia
recursiva implica un orden: el último que entró es el primero que salió (LIFO), de los elementos
posibles se elige el más recientemente encontrado.
10.3.1.3. Modificación para entender la operación.
Se agrega la variable estática indente, para ir mostrando los elementos que son sometidos a
revisión. Cada vez que se genera un llamado se produce un mayor nivel de indentación; el cual
es repuesto al salir del llamado recursivo.
Se agrega un asterisco para mostrar los elementos que generan llamados recursivos.

static int indente=0;


void dfsR(Graph G, Edge e)
{ int t,j, w = e.w;
pre[w] = cnt++;
for (t = 0; t < G->V; t++)
{
if (G->adj[w][t] != 0)
{ for (j = 0; j < indente; j++) printf(" ");
printf("%d-%d \n", w, t);
if (pre[t] == -1)
{ indente++;
putchar('*'); dfsR(G, EDGE(w, t));
indente--;
}
else putchar(' ');
}

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 13
}
}

Para el grafo del ejemplo de la figura 10.10., se produce el listado:

0-3
* 3-0
3-2
* 2-1
* 1-2
1-4
* 4-0
4-1
4-3
2-3
3-4
0-4

Este algoritmo puede emplearse: para detectar circuitos, para saber si existe una trayectoria que
conecte a dos vértices dados, para determinar si es un grafo conectado, para encontrar un árbol.
Ver propiedades de los árboles dfs en Sedgewick 18.4.
10.3.1.4. Arreglo de padres.
La siguiente modificación permite almacenar en el arreglo st el padre del vértice w. Esa
información es útil para aplicaciones de este algoritmo. Y es una forma de describir el árbol.

static int st[VERTICES];


void dfsR(Graph G, Edge e)
{ int t, j, w = e.w;
pre[w] = cnt++; //Marca con contador creciente
st[e.w] = e.v; //Se guarda el padre de w.
for (t = 0; t < G->V; t++)
{
if (G->adj[w][t] != 0) //Si hay conexión entre w y t
if (pre[t] == -1) dfsR(G, EDGE(w, t)); //Y t no está visitado busca t.
}
}

Para el ejemplo, quedarían almacenados en st: 0, 2, 3, 0, 1. Y en pre: 0, 3, 2, 1, 4.

El árbol que genera este algoritmo, se produce revisando los elementos en el siguiente orden:
0-3 , *3-0 , 3-2, *2-1, *1-2, 1-4, *4-0, 4-1, 4-3, 2-3, 3-4, 0-4. Los que generan llamados
recursivos, se preceden con un asterisco. La ilustración muestra que se avanza primero en
profundidad.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
0

3 4

0 2 4

1 3

2 4

0 1 3

Figura 10.11. Visualización de los llamados, en profundidad.

10.3.1.5. Uso de stack en búsqueda en profundidad.


Se emplea un stack para guardar los vértices.
//árbol en profundidad. No recursivo
void dfs(Graph G, Edge e)
{ int t, v, w, f;
StackInit(G->V*2);
StackPush(e.v); StackPush(e.w); //empuja los vértices del elemento
while(!StackEmpty())
{ w=StackPop();
v=StackPop();
StackPush(v); f=1; //guarda ruta en el árbol
pre[w] = cnt++;
st[w] = v; // se guarda el vértice padre de w.
while(f)
{
for (t = 0; t < G->V; t++)
{ if (G->adj[w][t] != 0)
if (pre[t] == -1)
{ StackPush(w);
StackPush(t);
printf("*%d-%d \n", w, t); f=0;
break;
}
}
if (f) //llegó al final de un recorrido en el árbol.
{if (!StackEmpty()) w=StackPop(); //extrae vértice anterior del árbol
else f=0;
}

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 15
}
}
Stackdestroy();
}

void GRAPHsearchDFSnr(Graph G)
{ int v;
cnt = 0;
for (v = 0; v < G->V; v++) {pre[v] = -1; st[v] = -1;}
for (v = 0; v < G->V; v++)
if (pre[v] == -1)
dfs(G, EDGE(v, v));
}

10.3.2. Búsqueda primero en extensión.

10.3.2.1. Definición de la búsqueda primero en extensión.


La búsqueda en extensión tiene por objetivo encontrar la ruta más corta entre dos vértices dados.
También se genera un árbol.

Se parte de un nodo, y se busca el siguiente nodo en todas las rutas de largo uno que existan;
luego se buscan todas las rutas de largo dos, y así sucesivamente.

Explorar los vértices, de acuerdo a su distancia al de partida, implica que de los elementos que
son posibles, se escoge uno y los otros se salvan para ser posteriormente explorados. Este orden
es: el primero que se encuentra, es el primero en ser procesado (FIFO).

Para visitar un vértice: se buscan los elementos que son incidentes con ese vértice, y los
elementos que tienen el otro vértice no visitado, se encolan.
Para el siguiente grafo:

1 2

0 3
Figura 10.12. Ejemplo para búsqueda primero en extensión.

A partir del nodo inicial 0, se marca el 0, y se exploran los elementos: 0-3 y 0-4. Se encolan el
0-3 y el 0-4. Ya que el 3 y 4 no han sido visitados aún.
Se desencola el 0-3, se marca el 3, y se revisan el 3-0, el 3-2 y el 3-4. Se encolan el 3-2 y el 3-4.
Ya que el 2 y el 4 no han sido visitados aún.
Se desencola el 0-4, se marca el 4, y se revisan el 4-0, 4-1 y 4-3. Se encola el 4-1.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Se desencola el 3-2, se marca el 2, y se revisan el 2-1, 2-3. Se encola el 2-1.
Se desencola el 3-4 sin procesar.
Se desencola el 4-1, se marca el 1, y se revisan el 1-2 y 1-4.
Se desencola el 2-1 sin procesar.
Del árbol formado, se visualiza, que el vértice 0 está a distancia uno de los vértices 3 y 4, y que
está a distancia 2, de los vértices 1 y 2.

Los vértices se marcan en el siguiente orden: 0, 3, 4, 2, 1.


0

3 4

0 2 4 0 1 3

1 3 2 4

Figura 10.13. Orden de visitas, generación de llamados. En extensión.

La figura10.13. ilustra que se busca por largos de trayectos.


10.3.2.2. Diseño de la operación.
El código de la función que encola los elementos. En la cola se almacenan elementos.
void bfs(Graph G, Edge e) //breadth-first-search
{ int v;
QUEUEput(e);
while (!QUEUEempty())
if (pre[(e = QUEUEget()).w] == -1)
{
pre[e.w] = cnt++; //en pre queda el orden en que se escogen los vértices
st[e.w] = e.v; //en st queda el padre.
for (v = 0; v < G->V; v++)
if (G->adj[e.w][v] == 1)
if (pre[v] == -1) QUEUEput(EDGE(e.w, v));
}
}

La función que genera el árbol BFS.


void GRAPHsearchBFS(Graph G)
{ int v;
cnt = 0;
QUEUEinit(ELEMENTOS);
for (v = 0; v < G->V; v++) {pre[v] = -1; st[v]=-1;} //Inicio de estructuras

for (v = 0; v < G->V; v++) // Para todos los vértices

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 17
if (pre[v] == -1)
bfs(G, EDGE(v, v)); //Se invoca una vez, si el grafo es conectado.
QUEUEdestroy();
}

10.4. Árboles con peso.

Si los elementos tienen asociado un peso se desea encontrar un árbol tal que la suma de los
pesos de sus ramas sea mínimo.

Previo a resolver este problema es necesario modificar las estructuras de datos para incorporar
el peso del elemento. Puede escogerse un número real entre 0. y menor que 1. Esto puede
lograrse dividiendo los pesos reales por el peso del mayor levemente incrementado.

Modificación de las funciones para tratar grafos con pesos.

Se elige:
typedef struct
{
int v; //vértice inicial
int w; //vértice final
float wt; //peso. Puede ser un double
} Edge;

El constructor queda ahora:


Edge EDGE(int v, int w, float wt)
{
Edge t;
t.v=v; t.w=w; t.wt=wt;
return (t);
}
Para un grafo se definen:
struct graph {
int V; //Número de vértices
int E; //Número de elementos
float **adj; // Matriz de adyacencias
};

typedef struct graph *Graph;

Para marcar la no existencia de adyacencias, se define:


#define maxWT 1. //Todo elemento tiene menor peso que maxWT.

La inicialización de un grafo vacío, se logra con:


Graph GRAPHinit(int V)
{ Graph G = malloc(sizeof *G); //crea cabecera del grafo
G->V = V; G->E = 0;

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
G->adj = MATRIXfloat(V, V, maxWT);
return G;
}

Donde la función que localiza espacio dinámico para la matriz de adyacencias es:
float **MATRIXfloat(int r, int c, float wt)
{ int i, j;
float **t = malloc(r * sizeof(float *));
for (i = 0; i < r; i++)
t[i] = malloc(c * sizeof(float));
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
t[i][j] = wt; //**(t+i+j) = wt;
return t;
}

El resto de las funciones se modifican para tratar grafos ponderados:

void BorreGrafo(Graph G) //Libera el espacio adquirido por malloc.


{ int i;
float **t = G->adj;
for (i = 0; i < G->V; i++) free(t[i]);
free(t);
free(G);
}

void GRAPHinsertE(Graph G, Edge e) //Inserta elemento


{
if (G->adj[e.v][e.w] == maxWT) G->E++;
G->adj[e.v][e.w] = e.wt;
G->adj[e.w][e.v] = e.wt; //suprimir para grafos dirigidos
}

void GRAPHremoveE(Graph G, Edge e) //Remueve elemento


{
if (G->adj[e.v][e.w] != maxWT) G->E--;
G->adj[e.v][e.w] = maxWT;
G->adj[e.w][e.v] = maxWT; //suprimir para grafos dirigidos
}

int GRAPHedges(Edge a[], Graph G) //Forma arreglo de elementos a partir del grafo
{ int v, w, E = 0;
for (v = 0; v < G->V; v++)
for (w = v+1; w < G->V; w++)
if (G->adj[v][w] != maxWT) a[E++] = EDGE(v, w, G->adj[v][w]);
return E;
}

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 19
//muestra la matriz mediante listas de vértices conectados a cada vértice
void GRAPHshowL(Graph G)
{ int i, j;
printf("%d vertices, %d edges\n", G->V, G->E);
for (i = 0; i < G->V; i++)
{
printf("%d: ", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] != maxWT) printf(" %0.2f", G->adj[i][j]);
putchar('\n');
}
}

//Muestra Matriz de adyacencias.


void GRAPHshowM(Graph G)
{ int i, j;
printf("%d vertices, %d edges\n", G->V, G->E);
printf(" ");
for (j = 0; j < G->V; j++) printf(" %4d ", j);
printf("\n");
for (i = 0; i < G->V; i++)
{
printf("%2d:", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] != maxWT) printf(" %0.2f", G->adj[i][j]); else printf(" * ");
putchar('\n');
}
}

Se limita a dos el número de cifras significativas.

Las siguientes líneas describen un grafo a partir de sus elementos.


#define VERTICES 8
#define ELEMENTOS 12
//Variables
Graph Grafo;
//Edge Elementos[ELEMENTOS]={{0,2,.29},{4,3,.34},{5,3,.18},{7,4,.46},{7,0,.31},\
{7,6,.25},{7,1,.21},{0,6,.51},{6,4,.52},{4,5,.40},\
{5,0,.59},{0,1,.32} };

La descripción del grafo, descrito por sus elementos, según la lista de vértices adyacentes
resulta:

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
8 vértices, 12 elementos
0: 0.32 0.29 0.59 0.51 0.31
1: 0.32 0.21
2: 0.29
3: 0.34 0.18
4: 0.34 0.40 0.52 0.46
5: 0.59 0.18 0.40
6: 0.51 0.52 0.25
7: 0.31 0.21 0.46 0.25

La matriz de adyacencias con los pesos, resulta:


0 1 2 3 4 5 6 7
0: * 0.32 0.29 * * 0.59 0.51 0.31
1: 0.32 * * * * * * 0.21
2: 0.29 * * * * * * *
3: * * * * 0.34 0.18 * *
4: * * * 0.34 * 0.40 0.52 0.46
5: 0.59 * * 0.18 0.40 * * *
6: 0.51 * * * 0.52 * * 0.25
7: 0.31 0.21 * * 0.46 * 0.25 *

El siguiente ejemplo muestra las modificaciones a la generación de un árbol, empleando


búsqueda primero en profundidad. Se agrega el vector wt con los pesos.

static int cnt;


static int pre[VERTICES];
static int st[VERTICES];
static float wt[VERTICES];

void dfsR(Graph G, Edge e)


{ int t;
pre[e.w] = cnt++;
st[e.w] = e.v; //se guarda el vértice v padre de w.
wt[e.w]=G->adj[e.w][e.v]; //se guarda el peso del elemento w-v
for (t = 0; t < G->V; t++)
{
if (G->adj[e.w][t] != maxWT)
if (pre[t] == -1)
{dfsR(G, EDGE(e.w, t, maxWT)); /*printf("%d-%d \n", e.w, t);*/}
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 21
void GRAPHsearchDFS(Graph G)
{ int v;
cnt = 0;
for (v = 0; v < G->V; v++) {pre[v] = -1; st[v] = -1;} //Inicialización
for (v = 0; v < G->V; v++)
if (pre[v] == -1)
dfsR(G, EDGE(v, v, maxWT));
}

La cual entrega en st la descripción del árbol a partir de un arreglo de padres de los vértices.

0 1 2 3 4 5 6 7 Vértices.
0 0 0 4 7 3 4 1 Padre del vértice.
1.0 0.32 0.29 0.34 0.46 0.18 0.52 0.21 Peso del elemento entre vértice y su Padre.

La raíz del árbol (vértice 0) tiene un lazo de peso infinito (valor 1.0) consigo misma.
Este árbol cubre todos los nodos, pero no es un árbol de cobertura mínima.

10.5. Mínimo árbol de cobertura.

Para un grafo dado existe un muy elevado número de árboles. No es fácil encontrar el árbol de
cobertura mínima.

Algunos conceptos para definir el árbol de cobertura mínima:

Un elemento del árbol se denomina rama.

Si se agrega un elemento a un árbol se crea un único circuito, el elemento agregado suele


denominarse cuerda. Un circuito fundamental está formado por una cuerda y el resto de los
elementos deben ser ramas.

Un conjunto de corte son los elementos que unen dos conjuntos disjuntos de vértices.
Uno de los elementos del conjunto de corte debe ser rama.

Si los elementos tienen pesos diferentes, la rama del conjunto de corte debe tener peso mínimo.
Si existen múltiples elementos mínimos en el corte, al menos uno de ellos debe estar presente.

Entonces: Cada rama de un mínimo árbol de cobertura es el elemento mínimo de un conjunto de


corte.

Así también las cuerdas deben tener los máximos pesos de los circuitos que formen con el
mínimo árbol de cobertura.

La formación de un MST (minimum spanning tree) consiste en aplicar las reglas anteriores, para
rechazar elementos que sean cuerdas, con peso máximo; y aceptar ramas, con pesos mínimos.

El algoritmo de Prim, elige un vértice cualquiera como inicial.

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
Luego repite para los (V-1) vértices restantes:
Agregar elemento de peso mínimo que conecte los vértices del MST, con los vértices
que aún no pertenecen al MST.

El algoritmo de Kruskal procesa los elementos ordenados por sus pesos. Se va agregando al
MST un elemento que no forme circuitos con los previamente agregados, y se detiene si se han
agregado (V-1) elementos. Esto porque en grafos conectados el árbol tiene (V-1) ramas.

10.5.1. Algoritmo de Prim.

Si se tienen vértices en el MST, se busca un vértice w que aún no está en el árbol, tal que esté a
menor distancia de los vértices del MST. Para esto es preciso registrar las menores distancias,
de cada nodo no perteneciente al MST, a los nodos pertenecientes al MST; y elegir la menor.

MST w

Figura 10.14. Conjuntos en el algoritmo de Prim.

Para diseñar el algoritmo se emplean tres arreglos:

static int padre[VERTICES]; // padre del vértice


static int DistaMenosDe[VERTICES]; //vértice que dista menos del vértice del árbol.
static float wt[VERTICES+1]; //distancia menor. Con espacio para centinela.

El árbol se describe por el arreglo padre[v], donde se almacena el padre del vértice v. Se inicia
con valores iguales a menos uno, para indicar que ningún vértice forma parte del árbol.

Se agrega un arreglo DistaMenosDe[w], en el cual, durante el procesamiento, se dejará el


vértice más cercano a w del árbol. Se inicia con DistaMenosDe[i] = i, indicando que esta
información no se conoce.

Antes se empleó un arreglo wt[w] para almacenar los pesos de los elementos, ahora se usa para
almacenar la mínima distancia al árbol, si el vértice w aún no pertenece al árbol; y la distancia al
padre, si el vértice w ya pertenece al MST. Al inicio se lo llena con maxWT, para indicar que
esta información sobre pesos mínimos aún no se conoce.

En la variable local min se almacena el vértice, que aún no pertenece al MST, y el cual debe
cumplir la propiedad de tener distancia mínima con los vértices que ya están en el MST. Para
asociarle un peso, se agrega una entrada al arreglo wt, con valor maxWT, y se lo inicia con valor
V (vértice que no existe en el grafo). De esta forma wt[min] tendrá un espacio y valor definido.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 23
#define NoVisitado -1
#define SinPadre -1
#define Peso G->adj[v][w]

void mstPrim(Graph G, int raiz)


{ //Inicio del espacio de variables
int v, w, min=raiz, cnt;
for (v = 0; v < G->V; v++)
{ padre[v] = SinPadre; DistaMenosDe[v] = v; wt[v] = maxWT; }
wt[G->V] = maxWT; //centinela. Arreglo requiere una posición adicional
wt[raiz]=0.; //raíz a distancia cero de sí misma.

for (cnt=0; cnt< G->V; cnt++) //agrega todos los vertices O(V2)
{
v = min; padre[v] = DistaMenosDe[v]; //agrega vértice v al MST
//printf(" %d \n", v); //orden en que son agregados los vértices.

//Selecciona vértice min con distancia mínima a los vértices del mst
for (w = 0, min = G->V; w < G->V; w++)
//Al inicio min es un vértice que no existe.
if (padre[w] == NoVisitado) //si w no está aún en el MST
{
if (Peso < wt[w])
{ wt[w] = Peso; DistaMenosDe[w] = v; } //salva distancia menor y el vértice.
if (wt[w] < wt[min]) min = w; //selecciona nuevo vértice a distancia mínima
}
//Al salir del for interno se tiene el nuevo vértice que se agrega al MST
}
}

Para los datos anteriores, luego de ejecutado el Algoritmo de Prim, con raíz en vértice 0, se
genera:

0 1 2 3 4 5 6 7 Vértices.
0 7 0 4 7 3 7 0 Padre del vértice.
0 0.21 0.29 0.34 0.46 0.18 0.25 0.31 Peso elemento entre vértice y su Padre.
0 7 0 4 7 3 7 0 Vértice que dista menos del vértice del árbol

La suma = 0.21+0.29+0.34+0.46+0.18+0.25+0.31 es la mínima.

10.5.2. Algoritmo de Kruskal.

Se inicia con un conjunto vacío los vértices del árbol de mínima cobertura. Luego de ordenados
los elementos por sus pesos, se agrega un elemento por vez, los de menor peso se intentan
agregar primero. Antes de agregar los vértices del elemento al árbol, se revisa que no se formen
circuitos. El procedimiento termina cuando se tengan todos los vértices en el conjunto o se
hayan agregado (Vértices-1) ramas al árbol.

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos

Verificar que los vértices agregados, asociados a un elemento, no formen circuitos no es una
tarea sencilla. Una primera aproximación es definir un arreglo id de vértices que mantenga la
información booleana de si un vértice ha sido agregado o no.

La Figura 10.15, muestra las ramas, definidas por sus vértices: (1, 4), (1, 5) y (0, 3), que ya han
sido agregadas al árbol. Al inicio se marcan todos los vértices con 0, indicando que aún no han
sido considerados. Entonces si el and de id[v] e id[w] es verdadero no se agrega el elemento
(u,w) y se considera el siguiente elemento de mayor peso; si es falso se marcan los vértices con
1, indicando su consideración.
Los vértices: 2 y 6 aún no han sido agregados, y los elementos considerados forman una foresta
o un conjunto de subárboles no conectados entre sí.

1 4 0
0 1 2 3 4 5 6
1 1 0 1 1 1 0
5 3

Figura 10.15. Generación MST. Algoritmo de Kruskal.

La información anterior no permitiría agregar una rama que conecte, por ejemplo los vértices 3
y 4; ya que estos vértices ya están marcados. Para superar esto pueden identificarse en el arreglo
a cual de los subárboles pertenecen los vértices. Se inicia el arreglo foresta con el índice
correspondiente al vértice; de este modo, al inicio cada vértice queda asociado a un subárbol
vacío. Al agregar un elemento cuyos dos vértices no pertenezcan a ninguno de los subárboles se
los marca pertenecientes a un árbol de la foresta; se emplea números para identificar los árboles
con enteros mayores que el número de vértices.

Después de agregar el elemento (1, 4) el arreglo foresta queda como se indica en la Figura
10.15a.
1 4 foresta
0 1 2 3 4 5 6
0 7 2 3 7 5 6

Figura 10.15a. Creación de primer árbol de la foresta.

La condición: (foresta[u] != foresta[w]) toma valor verdadero si el elemento (u, w) no pertenece


al árbol.
Si se agrega elemento (1,5), el vértice 5 no pertenece a un subárbol y el vértice 1 pertenece al
subárbol 7, se marca el vértice 5 como perteneciente al subárbol 7. Esto se ilustra en la Figura
10.15b.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 25

1 4 foresta
0 1 2 3 4 5 6
0 7 2 3 7 7 6

Figura 10.15b. Agrega elemento a subárbol de la foresta.

Si se agrega el elemento (0, 3), se crea un nuevo subárbol con identificador 8. Resulta la Figura
10.15c.
1 4 foresta
0 0 1 2 3 4 5 6
8 7 2 8 7 7 6

5 3

Figura 10.15c. Agrega elemento a subárbol de la foresta.

Si se desea agregar el elemento (3, 4), con la información de los subárboles de la foresta, ahora
es posible efectuarlo. Sin embargo deben unirse los subárboles, esto puede efectuarse cambiado
los identificadores de uno de los subárboles. Esto se muestra en la Figura 10.15d.
1 4 foresta
0 0 1 2 3 4 5 6
7 7 2 7 7 7 6

5 3

Figura 10.15d. Unión de subárboles de la foresta.

int foresta[VERTICES];
static int cntf;

void UneSubArboles(int p, int q)


{ int i, t;
if ( (foresta[p]==p) && (foresta[q]==q) )
{foresta[p]=cntf; foresta[q]=cntf; cntf++;} //crea subárbol
else
if (foresta[p]<VERTICES) foresta[p]=foresta[q]; //pega p a subárbol q
else
if(foresta[q]<VERTICES) foresta[q]=foresta[p]; //pega q a subárbol p
else
for(t=foresta[p], i=0; i<VERTICES; i++) // Complejidad O(VERTICES)
if(foresta[i] == t) foresta[i] = foresta[q]; //une subárbol p al subárbol q
}

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos

int NoPertenece(int p, int q)


{
return (foresta[p] != foresta[q]);
}

void mstKruskal(Graph G, Edge *e)


{
int i, k, E;
E=GRAPHedges(e, G);
qsortnw(e, 0, E-1); //ordena elementos por pesos.
for(i=0; i<G->V; i++) foresta[i]=i; //crea conjunto de vértices.
cntf=G->V;
for (i= 0, k = 0; i < E && k < G->V-1; i++)
if (NoPertenece( e[i].v, e[i].w )) //Elemento (u,w) no pertenece al mst
{
UneSubArboles(e[i].v, e[i].w); //La unión es O(n)
mst[k++] = e[i]; //agrega rama al árbol
}
}

En el arreglo mst de elementos, se deja el árbol, se emplea como variable global.


Edge mst[VERTICES-1];

La rutina de ordenamiento quicksort, se modifica, para adecuarla a los tipos de datos del grafo,
y para ordenar según el peso:

void qsortnw(Edge *a, int l, int r)


{
int i=l, j=r;
Edge temp;
float piv=a[(l+r)/2].wt;
do {
while ( a[i].wt < piv) i++;
while( piv < a[j].wt) j--;
if( i<=j)
{ if (i!=j) {temp=a[i], a[i]= a[j], a[j]=temp;}
i++; j--;
}
} while(i<=j);
if( l < j) qsortnw( a, l, j);
if( i < r ) qsortnw( a, i, r);
}

La siguiente secuencia prueba la función, e imprime el mínimo árbol de cobertura:

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 27
mstKruskal(Grafo, Elementos);

for (i = 0; i < Grafo->V-1; i++)


printf("%2d-%2d =%0.2f\n", mst[i].v, mst[i]. w, mst[i].wt);

Las funciones que manejan conjuntos, se encuentra en las primeras páginas del texto de R.
Sedgewick.

void GRAPHmstKruskal(Graph G, Edge *e)


{
int i, k, E;
E=GRAPHedges(e, G); //Se crea representación de elementos a partir del grafo.
qsortnw(e, 0, E-1); //se ordena a los elementos, según el peso.
UFinit(G->V); //crea conjunto de vértices.
for (i= 0, k = 0; i < E && k < G->V-1; i++) //se agregan máximo V-1 ramas
if (!UFfind(e[i].v, e[i].w)) //si v no está conectado a w
{
UFunion(e[i].v, e[i].w); //se agregan vértices al conjunto
mst[k++] = e[i]; //se agrega rama e al árbol mst
}
UFdestroy();
}
En el arreglo id, se identifican los vértices de un grafo
Se lleva la cuenta del número de nodos de cada subconjunto en sz.

static int *id, *sz;


void UFinit(int N) id sz
{ int i;
id = (int *) malloc(N*sizeof(int)); 0 1
sz = (int *) malloc(N*sizeof(int)); 1 1
for (i = 0; i < N; i++)
{ id[i] = i; sz[i] = 1; }
}
N-1 1

Figura 10.16. Descripción de conjuntos, mediante arreglos.

Cada vértice apunta a otro en el mismo subconjunto, sin ciclos.


Los vértices conectados, de un subconjunto apuntan a la raíz.

int find(int x)
{ int i = x;
while (i != id[i]) i = id[i]; return i; }

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos
//Retorna uno si están conectados
int UFfind(int p, int q)
{ return (find(p) == find(q)); }

void UFunion(int p, int q) // O(2*log(N)) R.S. pág 16


{ int i = find(p), j = find(q);
if (i == j) return;
if (sz[i] < sz[j]) //si nodos conectados a i es menor nodos conectados a j
{ id[i] = j; sz[j] += sz[i]; } //el i se pega al j y nodos de i se acumulan en j
else { id[j] = i; sz[i] += sz[j]; } //el j se pega al i y nodos de j se acumulan en i
//for(i=0;i<VERTICES;i++) printf("%d ",id[i]);
//for(i=0;i<VERTICES;i++) printf("%d ",sz[i]);
//putchar('\n');
}

void UFdestroy(void)
{
free(id); free(sz);
}

Un diseño menos eficiente de las rutinas:

//Retorna uno si p y q ya están conectados


int UFfind1(int p, int q)
{ return (id[p] == id[q]); } // O(1)

//Cada vértice apunta a otro en el mismo subconjunto, sin ciclos.


//Los vértices conectados, de un subconjunto apuntan a la raíz del subárbol
void UFunion1(int p, int q, int N)
{ int t,i;
for(t=id[p], i=0; i<N; i++) // O(N)
if(id[i] == t) id[i] = id[q]; //le coloca raíz q a todos los conectados al conjunto p
}

10.5. Trayectorias más cortas en grafos orientados.

La trayectoria más corta es la suma menor de las ponderaciones o pesos de los elementos, a
través de una trayectoria orientada.

Se denominan redes a los grafos orientados con pesos para sus elementos.

Es preciso modificar las rutinas anteriores para tratar grafos dirigidos con pesos. Se considera
ahora con peso cero a los elementos de la diagonal principal, reflejando que existe conexión del
vértice con sí mismo. No se aceptan elementos en paralelo, ni elementos con pesos negativos.

Existen tres tipos de problemas. Uno de ellos es: dados dos vértices encontrar la trayectoria
orientada más corta entre los vértices, se lo denomina fuente-sumidero. Otro caso: es encontrar

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 29
todos los trayectos más cortos entre un vértice y todos los demás; esto equivale a encontrar un
árbol (SPT shortest-path-tree) que conecte al vértice con todos los demás vértices que son
alcanzables, de tal modo que la trayectoria en el árbol sea la más corta dentro de la red. Un
tercer problema es encontrar todos los pares de rutas más cortas.

10.5.1. Modificación de las funciones para tratar grafos orientados con pesos.

La definición del elemento debe entenderse como dirigido de v hacia w.


Edge EDGE(int v, int w, float wt)
{ Edge t;
t.v=v; t.w=w; t.wt=wt;
return (t);
}
v w
wt

Figura 10.17. Elemento orientado, con peso.

Ahora se coloca en la matriz que cada vértice está a distancia cero consigo mismo.

//Asignación dinámica de arreglo bidimensional


float **MATRIXfloat(int r, int c, float wt)
{ int i, j;
float **t = malloc(r * sizeof(float *));
for (i = 0; i < r; i++)
t[i] = malloc(c * sizeof(float));
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
if(i==j) t[i][j] = 0.; else t[i][j] = wt; //se aceptan lazos en cada vértice
return t;
}

void BorreGrafo(Graph G)
{ int i;
float **t = G->adj;
for (i = 0; i < G->V; i++) free(t[i]);
free(t);
free(G);
}

Graph GRAPHinit(int V) //Crea grafo vacío, sin elementos. Sólo los vértices.
{ Graph G = malloc(sizeof *G); //crea cabecera del grafo
G->V = V; G->E = 0;
G->adj = MATRIXfloat(V, V, maxWT);
return G;
}

Profesor Leopoldo Silva Bijit 26-05-2008


30 Estructuras de Datos y Algoritmos
void GRAPHinsertE(Graph G, Edge e) //Inserta elemento
{
if (G->adj[e.v][e.w] == maxWT) G->E++;
G->adj[e.v][e.w] = e.wt; // Sólo el elemento dirigido.
}

void GRAPHremoveE(Graph G, Edge e) //Remueve elemento


{
if (G->adj[e.v][e.w] != maxWT) G->E--;
G->adj[e.v][e.w] = maxWT;
}

int GRAPHedges(Edge a[], Graph G) //Crea arreglo a de elementos a partir de G.


{ int v, w, E = 0;
for (v = 0; v < G->V; v++)
for (w = 0; w < G->V; w++)
if ((G->adj[v][w] != maxWT)&&(G->adj[v][w] != 0))
a[E++] = EDGE(v, w, G->adj[v][w]);
return E;
}

//muestra la matriz mediante listas de vértices conectados a cada vértice


void GRAPHshowL(Graph G)
{ int i, j;
printf("%d vértices, %d elementos.\n", G->V, G->E);
for (i = 0; i < G->V; i++)
{ printf("%d: ", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] != maxWT) printf(" %2d-%0.2f", j, G->adj[i][j]);//dos decimales
putchar('\n');
}
}

//Muestra Matriz de incidencia.


void GRAPHshowM(Graph G)
{ int i, j;
printf("%d vértices, %d elementos.\n", G->V, G->E); printf(" ");
for (j = 0; j < G->V; j++) printf(" %4d ", j); printf("\n");
for (i = 0; i < G->V; i++)
{
printf("%2d:", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] != maxWT) printf(" %0.2f", G->adj[i][j]); else printf(" * ");
putchar('\n');
}
}
Las siguientes definiciones, permiten crear un grafo de seis vértices y 11 elementos.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 31
#define VERTICES 6
#define ELEMENTOS 11

//Variables
Graph Grafo;
Edge Elementos[ELEMENTOS]={{0,1,.41},{1,2,.51},{2,3,.50},{4,3,.36},\
{3,5,.38},{3,0,.45},{0,5,.29},{5,4,.21},\
{1,4,.32},{4,2,.32},{5,1,.29} };

Se inicia el grafo con:


Grafo = GRAPHinit(VERTICES);

Y la inserción de los elementos con sus pesos se logra con:


for(i=0; i<ELEMENTOS; i++)
GRAPHinsertE(Grafo, Elementos[i] );

La lista de los vértices conectados a cada vértice se logra con:


GRAPHshowL(Grafo);

La cual muestra:
6 vértices, 11 elementos.
0: 0.00 0.41 0.29
1: 0.00 0.51 0.32
2: 0.00 0.50
3: 0.45 0.00 0.38
4: 0.32 0.36 0.00
5: 0.29 0.21 0.00

La matriz de la red, se logra con:

GRAPHshowM(Grafo);

Que muestra:

6 vértices, 11 elementos.
0 1 2 3 4 5
0: 0.00 0.41 * * * 0.29
1: * 0.00 0.51 * 0.32 *
2: * * 0.00 0.50 * *
3: 0.45 * * 0.00 * 0.38
4: * * 0.32 0.36 0.00 *
5: * 0.29 * * 0.21 0.00

La Figura 10.18 muestra el grafo.

Profesor Leopoldo Silva Bijit 26-05-2008


32 Estructuras de Datos y Algoritmos

0 1
0.41
0.29 0.29
5 0.32 0.51
0.45 0.21
0.32
0.38 0.36
4
2
0.50
3

Figura 10.18. Grafo orientado.

Se puede crear un árbol de trayectorias, entre un vértice y el resto, a partir del grafo G, con una
búsqueda primero en profundidad, invocando a: GRAPHsearchDFS(Grafo);

static int cnt;


static int pre[VERTICES];
static int padre[VERTICES];
static float wt[VERTICES]; //distancia menor de raíz a vértice
#define Pesoew G->adj[e.v][e.w]

void dfsR(Graph G, Edge e)


{ int t;
pre[e.w] = cnt++; //orden en que recorre el árbol
padre[e.w] = e.v; //se guarda el vértice v padre de w.
wt[e.w] = wt[e.v] + Pesoew; //se guarda el peso del elemento v-w más el acumulado
// desde la raíz a v.
for (t = 0; t < G->V; t++)
if (G->adj[e.w][t] != maxWT)
if (pre[t] == NoVisitado)
{dfsR(G, EDGE(e.w, t, maxWT));
/*printf("%d-%d \n", e.w, t);*/
}
}

La rutina recursiva anterior es llamada por:


void GRAPHsearchDFS(Graph G)
{ int v;
cnt = 0;
for (v = 0; v < G->V; v++) {pre[v] = NoVisitado; padre[v] = SinPadre; wt[v]=0.;}
for (v = 0; v < G->V; v++)
if (pre[v] == NoVisitado)
dfsR(G, EDGE(v, v, maxWT)); //Si el grafo no es conectado, obtiene foresta.
}

La cual genera el siguiente árbol de trayectorias, descrito por un arreglo de padres.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 33

0 1 2 3 4 5 Vértices.
0 0 1 2 5 3 Padre del vértice.
0.00 0.41 0.92 1.42 2.01 1.80 Peso trayecto entre vértice y la raíz.
0 1 2 3 5 4 Orden en que visita los vértices.

El árbol DFS generado no es un SPT mínimo.


0 1 0 1
0.41 0.41
0.29 0.29
5 0.51 5 0.32 0.51
0.21 0.45 0.21
0.32
0.38 0.38 0.36
4 4
2 2
0.50 0.50
3 3

Figura 10.19. Árbol de trayectorias DFS.

El árbol de búsqueda primero en extensión es una mejor aproximación, pero tampoco es un


SPT.
0 1 2 3 4 5 Vértices.
0 0 1 2 1 0 Padre del vértice.
0.00 0.41 0.92 1.42 0.73 0.29 Peso trayecto entre vértice y la raíz.
0 1 3 5 4 2 Orden en que visita los vértices.
0 1 0 1
0.41 0.41
0.29 0.29 0.29
5 0.32 0.51 5 0.32 0.51
0.45 0.21
0.32
0.38 0.36
4 4
2 2
0.50 0.50
3 3

Figura 10.19a. Árbol de trayectorias BFS.

10.5.2. Algoritmo de Dijkstra.

Determina un SPT, un árbol con las mínimas trayectorias, desde un vértice a los demás.
No se aceptan elementos en paralelo, ni elementos con pesos negativos.

El algoritmo consiste inicialmente en colocar el vértice fuente en el SPT.


Luego, se agrega un elemento por vez, agregando un vértice. Siempre tomando el elemento que
tenga el trayecto más corto entre la fuente y el vértice que no está en el SPT.

Profesor Leopoldo Silva Bijit 26-05-2008


34 Estructuras de Datos y Algoritmos
Se implementa una solución similar al algoritmo de Prim, pero se van agregando vértices que
estén a la menor distancia de la raíz.

Escoger raíz.
Repetir hasta agregar todos los vértices:
Encontrar un nodo (sea min) cuya distancia a la raíz sea la menor entre todos los nodos no
pertenecientes al SPT.
Marcar ese nodo (sea v) como perteneciente al SPT.
Repetir para cada w nodo no perteneciente al SPT:
Si hay conexión entre v y w, y si la distancia del nodo raíz a v más la distancia
de v a w es menor que la distancia actual de la raíz a w:
Actualizar la distancia de la raíz a w como la distancia de la raíz a v más
la distancia de v a w.
Actualizar el nuevo padre de w
Actualizar el vértice que está a distancia mínima.
raíz

wt[v]
wt[w]

Figura 10.20. Distancias en algoritmo de Dijkstra.

Se requieren tres arreglos para implementar el algortimo:

static int padre[VERTICES];


static int DistaMenosDe[VERTICES]; //vértice que dista menos del vértice del árbol.
static float wt[VERTICES+1]; //distancia menor de raíz a vértice. Requiere un espacio
// adicional que se emplea como centinela.

#define Peso G->adj[v][w]


#define PesoRuta wt[v] + Peso //distancia de raíz a v, más la del elemento v-w
#define SinPadre -1
#define NoVisitado -1

void sptDijkstra(Graph G, int raiz)


{
//Inicio del espacio de variables
int v, w, min=raiz, cnt;
for (v = 0; v < G->V; v++)

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 35
{ padre[v] = SinPadre; //se inician como no visitados todos los vértices
DistaMenosDe[v] = v; //Al inicio cada vértice dista menos de si mismo.
wt[v] = VERTICES ; //Peso máximo de los trayectos = número de ramas + 1
}

wt[raiz] = 0.; //raíz a distancia cero


wt[G->V] = VERTICES;//centinela. Requiere una posición adicional con peso máximo.

//El for externo agrega, uno a uno, los vértices al SPT.

for (cnt=0; cnt< G->V; cnt++) //agrega todos los vértices. O(V2)
{
v = min; padre[min] = DistaMenosDe[min]; //agrega vértice v al SPT
//printf(" %d \n", min);//orden en que agrega los vértices

for (w = 0, min = G->V; w < G->V; w++)


//Al inicio min es un vértice que no existe.
if (padre[w] == NoVisitado) //si w no está en el SPT
{
if ( (Peso!=maxWT) && (PesoRuta < wt[w]) )
//Si hay conexión desde v a w, y
//Si la distancia del nodo raíz a v más la distancia de v a w es menor que
// la distancia actual de la raíz a w
{ wt[w] = PesoRuta; //actualiza distancia de la raíz a w
DistaMenosDe[w] = v; //salva al padre de w.
}
if (wt[w] < wt[min]) min = w; //actualiza el vértice candidato al mínimo
}
//Al salir del for interno se tiene el nuevo vértice que se agrega al SPT
}
}

El siguiente llamado genera un shortest path tree, como un arreglo de padres, a partir del grafo
G, con fuente o raíz 2.

sptDijkstra(Grafo, 2);
0 1 2 3 4 5 Vértices.
3 5 2 2 5 3 Padre del vértice.
0.95 1.17 0.00 0.50 1.09 0.88 Peso trayecto entre vértice y raíz. wt[i]
3 5 2 2 5 3 Vértice que Dista Menos Del vértice del árbol
La raíz 2 a distancia cero de sí misma.

Profesor Leopoldo Silva Bijit 26-05-2008


36 Estructuras de Datos y Algoritmos

0 1

0.29
5
0.45 0.21
0.38
4
2
0.50
3

Figura 10.21. SPT, raíz 2.

El SPT con fuente en el vértice 0 se genera con: sptDijkstra(Grafo, 0).


0 1 2 3 4 5 Vértices.
0 0 4 4 5 0 Padre del vértice.
0.00 0.41 0.82 0.86 0.50 0.29 Peso trayecto entre vértice y raíz.
0 0 4 4 5 0 Vértice que dista menos del vértice del árbol
0 1
0.41
0.29 0.29
5 0.32 0.51
0.45 0.21
0.32
0.38 0.36
4
2
0.50
3

Figura 10.22. SPT, raíz 0.

El SPT con fuente en el vértice 1 se genera con: sptDijkstra(Grafo, 1);


0 1 2 3 4 5 Vértices.
3 1 1 4 1 3 Padre del vértice.
1.13 0.00 0.51 0.68 0.32 1.06 Peso trayecto entre vértice y raíz.

sptDijkstra(Grafo, 5), produce:


0 1 2 3 4 5 Vértices.
3 5 4 4 5 5 Padre del vértice.
1.02 0.29 0.53 0.57 0.21 0.00 Peso trayecto entre vértice y raíz.

sptDijkstra(Grafo, 4), produce:


0 1 2 3 4 5 Vértices.
3 5 4 4 4 3 Padre del vértice.
0.81 1.03 0.32 0.36 0.00 0.74 Peso trayecto entre vértice y raíz.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 37
Referencias.

Robert Sedgewick, "Algorithms in C", Third edition, Addison Wesley, 1998.

Profesor Leopoldo Silva Bijit 26-05-2008


38 Estructuras de Datos y Algoritmos

Problemas resueltos.

P10.1. Para el siguiente grafo orientado:


2
0,5
1
0,4 0,3 0,5
0,1
0
0,3 4 0,5 3

Determinar el arreglo de pesos del trayecto de peso mínimo entre cada vértice y la raíz, y el
arreglo padres del vértice.
a) Para raíz igual al vértice 4.
b) Para raíz igual al vértice 2.

Solución:
a)
0 1 2 3 4 Vértices.
4 0 3 4 4 Padre del vértice.
0.30 0.70 1.00 0.50 0.00 Peso trayecto entre vértice y raíz.

b)
0 1 2 3 4 Vértices.
4 2 2 1 1 Padre del vértice.
0.90 0.50 0.00 0.80 0.60 Peso trayecto entre vértice y raíz.

P10.2. Se tiene un grafo definido por un arreglo de elementos.

Edge Elementos[ELEMENTOS]={{0,1,.4},{1,2,.5},{2,3,.5},{3,4,.5},\
{1,4,.1},{0,4,.3},{1,3,.3}};

a) Dibujar el grafo.
b) Determinar el mínimo árbol de cobertura aplicando algoritmo de Kruskal. Indicando el orden
en que se eligen las ramas. Dibujar el árbol.
c) Determinar el mínimo árbol de cobertura aplicando algoritmo de Prim. Indicando el orden en
que se agregan los vértices, y los arreglos de padres y de pesos entre el vértice y su padre.
Dibujar el árbol.
d) Modificar la función de Prim para imprimir el orden en que se eligen los vértices.

Solución.

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 39
a)
2
0,5
1
0,4 0,3 0,5
0,1
0
4 0,5 3
0,3
b) Se eligen en el orden: 2
1- 4 =0.10 1
1- 3 =0.30 0,5
0- 4 =0.30 0,3
0,1
2- 3 =0.50 0
0,3 4 3

c)
Se agregan en orden: 0, 4, 1, 3, 2.
0 1 2 3 4 Vértices.
0 4 1 1 0 Padre del vértice.
1.00 0.10 0.50 0.30 0.30 Peso elemento entre vértice y su Padre.
2
1
0,3 0,5
0,1
0
0,3 4 3

d) Se intercala el printf, entre las líneas que se indican.


………..
v = min; st[min] = fr[min];//agrega vértice v al MST
printf(" %d \n", min);
for (w = 0, min = G->V; w < G->V; w++)
……….

Ejercicios propuestos.

E10.1.

Partiendo del nodo A, determinar arreglo de padres para:


Árbol de búsqueda primero en extensión (bfs)
Árbol de búsqueda primero en profundidad (dfs)
Árbol de cobertura mínima aplicando algoritmo de Prim.

Profesor Leopoldo Silva Bijit 26-05-2008


40 Estructuras de Datos y Algoritmos

4 C
A 10 18 I
H
3 17 22
4 J
B 14
7
E 6 5
8 G 8
22 9 13

F 13 K
D

Profesor Leopoldo Silva Bijit 26-05-2008


Grafos 41
Índice general.

CAPÍTULO 10 ........................................................................................................................................... 1
GRAFOS. .................................................................................................................................................... 1
10.1. DEFINICIONES. ................................................................................................................................. 1
10.2. REPRESENTACIONES. ....................................................................................................................... 2
10.2.1. Matriz de adyacencia de los elementos en los vértices. .......................................................... 2
10.2.2. Matrices estáticas en C. .......................................................................................................... 3
10.2.3. Matrices dinámicas en C. ........................................................................................................ 4
Declaración de estructuras de datos para un grafo. .......................................................................................... 4
Definición de variables. ................................................................................................................................... 4
10.2.4. Funciones para grafos descritos por su matriz de adyacencias. ............................................. 5
10.2.4.1. Creación. ........................................................................................................................................... 5
10.2.4.2. Liberación del espacio. ..................................................................................................................... 6
10.2.4.3. Definición de un elemento. ............................................................................................................... 6
10.2.4.4. Inserción de elemento en un grafo. ................................................................................................... 6
10.2.4.5. Eliminación de elemento. .................................................................................................................. 7
10.2.4.6. Creación de los elementos................................................................................................................. 7
10.2.4.7. Despliegue de un grafo. Lista de vértices. ......................................................................................... 8
10.2.4.8. Despliegue de la matriz de incidencias. ............................................................................................ 9
10.2.4.9. Generación de los elementos a partir de la matriz de incidencias. .................................................... 9
10.3. TRAYECTORIAS EN GRAFOS. .......................................................................................................... 10
10.3.1. Búsqueda primero en profundidad. ....................................................................................... 11
10.3.1.1. Definición de búsqueda primero en profundidad. ........................................................................... 11
10.3.1.2. Diseño de la operación. ................................................................................................................... 11
10.3.1.3. Modificación para entender la operación. ....................................................................................... 12
10.3.1.4. Arreglo de padres. ........................................................................................................................... 13
10.3.1.5. Uso de stack en búsqueda en profundidad. ..................................................................................... 14
10.3.2. Búsqueda primero en extensión. ............................................................................................ 15
10.3.2.1. Definición de la búsqueda primero en extensión............................................................................. 15
10.3.2.2. Diseño de la operación. ................................................................................................................... 16
10.4. ÁRBOLES CON PESO. ...................................................................................................................... 17
Modificación de las funciones para tratar grafos con pesos............................................................. 17
10.5. MÍNIMO ÁRBOL DE COBERTURA. .................................................................................................... 21
10.5.1. Algoritmo de Prim. ................................................................................................................ 22
10.5.2. Algoritmo de Kruskal. ........................................................................................................... 23
10.5. TRAYECTORIAS MÁS CORTAS EN GRAFOS ORIENTADOS. ................................................................ 28
10.5.1. Modificación de las funciones para tratar grafos orientados con pesos. .............................. 29
10.5.2. Algoritmo de Dijkstra. ........................................................................................................... 33
REFERENCIAS. ........................................................................................................................................ 37
PROBLEMAS RESUELTOS. ........................................................................................................................ 38
P10.1. Para el siguiente grafo orientado: ......................................................................................... 38
P10.2. Se tiene un grafo definido por un arreglo de elementos. ....................................................... 38
Ejercicios propuestos. ....................................................................................................................... 39
E10.1. ................................................................................................................................................ 39
ÍNDICE GENERAL. ................................................................................................................................... 41
ÍNDICE DE FIGURAS................................................................................................................................. 42

Profesor Leopoldo Silva Bijit 26-05-2008


42 Estructuras de Datos y Algoritmos
Índice de figuras.

FIGURA 10.1. ELEMENTO, VÉRTICE, INCIDENCIA. ..........................................................................................1


FIGURA 10.2. ELEMENTOS ORIENTADOS. .......................................................................................................2
FIGURA 10.3. MATRIZ DE ADYACENCIAS. ......................................................................................................3
FIGURA 10.4. MATRIZ DE 2 RENGLONES POR 4 COLUMNAS............................................................................3
FIGURA 10.5. FORMA DE ALMACENAMIENTO DE MATRICES...........................................................................3
FIGURA 10.6. ESTRUCTURA PARA MATRIZ DE ADYACENCIAS. .......................................................................5
FIGURA 10.7. GRAFO DESCRITO POR SUS ELEMENTOS. ..................................................................................7
FIGURA 10.8. GRAFO PARA ELEMENTOS DE LA FIGURA 10.7..........................................................................8
FIGURA 10.9. GENERACIÓN DE LOS ELEMENTOS A PARTIR DE MATRIZ DE ADYACENCIAS. ...........................10
FIGURA 10.10. ORDEN DE VISITAS, GENERACIÓN DE LLAMADOS. ................................................................11
FIGURA 10.11. VISUALIZACIÓN DE LOS LLAMADOS, EN PROFUNDIDAD. ......................................................14
FIGURA 10.12. EJEMPLO PARA BÚSQUEDA PRIMERO EN EXTENSIÓN. ...........................................................15
FIGURA 10.13. ORDEN DE VISITAS, GENERACIÓN DE LLAMADOS. EN EXTENSIÓN. .......................................16
FIGURA 10.14. CONJUNTOS EN EL ALGORITMO DE PRIM. .............................................................................22
FIGURA 10.15. GENERACIÓN MST. ALGORITMO DE KRUSKAL. ..................................................................24
FIGURA 10.15A. CREACIÓN DE PRIMER ÁRBOL DE LA FORESTA. ..................................................................24
FIGURA 10.15B. AGREGA ELEMENTO A SUBÁRBOL DE LA FORESTA. ............................................................25
FIGURA 10.15C. AGREGA ELEMENTO A SUBÁRBOL DE LA FORESTA. ............................................................25
FIGURA 10.15D. UNIÓN DE SUBÁRBOLES DE LA FORESTA. ...........................................................................25
FIGURA 10.16. DESCRIPCIÓN DE CONJUNTOS, MEDIANTE ARREGLOS. ..........................................................27
FIGURA 10.17. ELEMENTO ORIENTADO, CON PESO. .....................................................................................29
FIGURA 10.18. GRAFO ORIENTADO. .............................................................................................................32
FIGURA 10.19. ÁRBOL DE TRAYECTORIAS DFS. ..........................................................................................33
FIGURA 10.19A. ÁRBOL DE TRAYECTORIAS BFS. ........................................................................................33
FIGURA 10.20. DISTANCIAS EN ALGORITMO DE DIJKSTRA...........................................................................34
FIGURA 10.21. SPT, RAÍZ 2. ........................................................................................................................36
FIGURA 10.22. SPT, RAÍZ 0. ........................................................................................................................36

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 11

Árboles binarios balanceados de


búsqueda. AVL.

El alto de un árbol es el largo de la trayectoria más larga de una hoja hasta la raíz.
Adel'son-Vel'skii y Landis (1962) definieron árboles AVL en los cuales, para cada nodo, el alto
del subárbol derecho difiere del alto del subárbol izquierdo a lo más en uno.

El desarrollo del algoritmo muestra la necesidad de un análisis exhaustivo de los diferentes


casos que se presentan. Dicho análisis facilita la posterior codificación.

11.1 Análisis de complejidad.

Se define el factor de balance como el alto del subárbol derecho menos el alto del subárbol
izquierdo. Entonces en un árbol AVL, todos los nodos cumplen la propiedad de tener valores
del factor de balance iguales a: -1, 0, ó +1.

Sea nh el mínimo número de nodos en un árbol AVL de altura h dada, que se encuentra en su
peor caso de desbalance, si se agrega un nodo, tal que la nueva altura sea (h+1), dejan de ser
AVL.

Los siguientes diagramas ilustran dichos árboles, denominados de Fibonacci, y los factores de
balance de sus nodos, para alturas 0, 1 y 2. Se muestran todos los casos, separados por un eje de
simetría; a la derecha del eje se muestran los desbalanceados por la derecha; y a la izquierda los
desbalanceados por la izquierda. Las imágenes en ambos lados del eje se obtienen como
imágenes especulares de las del otro lado.

Lo que se desea encontrar es la altura máxima h de todos los árboles balanceados de n nodos.
Para resolver esto se da una altura h determinada y se intenta construir árboles balanceados
AVL con el mínimo número de nodos, éstos son los árboles de Fibonacci.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
n0 = 1
0

n1 = 2
-1 1
h=1
0 0

Figura 11.1 Árboles Fibonacci AVL, con alturas 0, 1.

n2 = 4

-1 -1 1 1

1 -1 0 0 1 0 -1
0

0 0 0 0

Figura 11.1.a. Árboles Fibonacci AVL, con altura 2.

Se cumple que: n2 = n1 + n0 + 1

Se pueden generar 4 árboles de Fibonacci con altura dos.


Existen adicionalmente varios árboles AVL de altura dos (los con 5, 6, y 7 nodos) pero se
consideran “más balanceados” que los de Fibonacci.

Para construir el árbol de Fibonacci de altura h, a la raíz se agrega un subárbol de altura (h-1) y
otro de altura (h-2). La Figura 11.2 ilustra un ejemplo, de los 16 posibles, de la generación de
un árbol de Fibonacci de altura 3, mediante dos subárboles de altura 1 y 2.

n3 = 7
Se tiene: n3 = n2 + n1 + 1
1

1 1
h=3

0 0 1

Figura 11.2 Ejemplo árbol AVL Fibonacci, con altura 3.

Se destaca el hecho de que estos árboles son el peor caso: logran máxima altura, con el mínimo
número de nodos.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 3
n4 = 12
Se tiene n4 = n3 + n2 + 1

Como ejemplo de árbol con altura 4, a la raíz se agrega por la derecha un árbol de Fibonacci de
altura 3, y por la izquierda uno de altura 2, resulta la Figura 11.3.

1
1

0 1 1
1

0_ 0 0 1

Figura 11.3 Árbol AVL Fibonacci, con altura 4.

Mediante inducción puede demostrarse que en general, se tiene la recurrencia:

nh nh 1 nh 2 1 con n0 1 y n1 2

Lo cual implica que un árbol AVL está formado por dos subárboles AVL.

La secuencia generada es: 1, 2, 4, 7, 12, 20, 33, 54… para h=0, 1, 2….

Empleando el siguiente comando Maple, se puede obtener la solución de la recurrencia:


> n[h]:= rsolve( { n(h) = n(h-1) + n(h-2) + 1, n(0)=1,n(1)=2}, n(h));

El término general de la serie n(h) es:


h h
1 1 1 1
1 5 2 5 1 2
5 1 5 5 1 5
nh := 1
1 5 1 5
h h
1 2 1
5 2 5 2
2 1 5 5 1 5
5 1 5 1 5

Evaluado numéricamente:
n(h) 1.894427191(1.618033988)h +.1055728091(-.6180339886)h 1

El segundo término tiende a cero, según muestra la secuencia:


> seq( evalf(subs( h =j, .1055728091*(-.6180339886)^h)), j = 0..6);

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
.1055728091 , -.06524758430 , .04032522477 , -.02492235950 , .01540286525 ,
-.009519494246 , .005883370999

Finalmente, el número de nodos en función de la altura en un árbol de Fibonacci, resulta:


n(h) 1.894427191(1.618033988) h

El comando solve permite despejar h, en términos de n.


> solve(n=1.894427191*1.618033988^h,h);

h (n)= 2.078086923 ln ( .5278640450 n )

Para acotar por arriba, se desea encontrar el valor de la constante c que satisface:

c*ln(n)/ln(2)=2.078086923*ln(.5278640450*n)
Resulta:
ln( .5278640450 n )
c = 1.440420092
ln( n )
El factor que depende de n, tiende a uno:

ln ( .5278640450 n)
c= lim 1.440420092 1.440420092
n
ln ( n )

Lo que también se aprecia en la gráfica:

Figura 11.4 Cota AVL en función de n.

Finalmente la altura en un árbol AVL queda acotada por:

1.440420092*log(n) > h(avl) > h(bst)

Donde h(bst) es la altura de un árbol de búsqueda binario completamente balanceado:

h(bst) = log(n+1)

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 5
Lo cual demuestra que: h(avl) = (log(n))

h=1,44..log(n)

altura AVL

altura BST

Figura 11.5 Complejidad de la altura de un árbol AVL.

La siguiente gráfica muestra la diferencia de altura del árbol AVL, respecto de uno
perfectamente balanceado, en función de n.

2, 07808ln(0,527864n)
Es decir la gráfica del cuociente:
log 2 (n)

Figura 11.6 Alargue de altura árbol AVL, respecto de perfectamente balanceado

Notar que para árboles con menos de 7000 nodos, la altura sólo se alarga en cuatro.

Otra forma de encontrar la solución de la recurrencia para n(h), es relacionarla con la secuencia
de Fibonacci, para la cual se conoce la solución.

La relación para la recurrencia de Fibonacci es:

F(i) = F(i – 1) + F(i – 2), F(0) = 0, F(1) = 1

La cual genera: 0, 1, 1, 2, 3, 5, 8, 13, … para i=0, 1, 2….

En el Ejemplo 4.6, se obtuvo, la solución:

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

1 1 5 n 1 1 5 n
F ( n) ( ) ( )
5 2 5 2
1 5 1 5
Empleando: 1, 61803.. se tiene que: 1 . Donde es la razón áurea.
2 2

C
AC
=
AB

A B
Figura 11.6.a. Razón áurea .

Reemplazando en F(n), n por h, y empleando , se obtiene:

1 1 1
F (h) ( )h (1 )h ( )h
5 5 5

Observando las dos secuencias de números que generan n(h) y F(h), se encuentra:

h 0 1 2 3 4 5 6 7
n(h) 1 2 4 7 12 20 33 54
F(h) 0 1 1 2 3 5 8 13
F(h+3) 2 3 5 8 13 21 34 55

Entonces, se tiene:
n(h) = F(h + 3) – 1.

En forma aproximada, se tiene la solución para n(h):


3
1 h 3 1 h 3 h h
n ( h) 1 (1,894413.)
5 5 5
Resultado igual, al obtenido antes.

11.2. Análisis de la inserción.

La función de inserción debe ser modificada para mantener la propiedad de árbol AVL.
Existen inserciones que sólo implican recalcular los factores de balance, ya que el árbol sigue
siendo AVL. Por ejemplo las dos inserciones siguientes, en la Figura 11.7. izquierda, sólo
modifican los factores de balance de algunos nodos ancestros del insertado, que están en la
trayectoria del recién insertado hacia la raíz.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 7

recalculado recalculados
1 1

1 1
1 1

1 0 1 0
0 1 1 0

1 0 0_ 0 1 1
0_ 0 0

0 0 0
insertados

Figura 11.7 Inserciones que sólo requieren recalcular factor de balance.

11.2.1. Detención de revisión en ascenso.

a) Al insertar por la izquierda, y en el proceso de ascenso, por la trayectoria desde el nodo recién
insertado hacia la raíz, revisando los factores de balance, si se llega a un nodo con factor uno,
basta corregir el factor de ese nodo (quedando éste en 0) y no es preciso seguir corrigiendo en el
ascenso. Esto debido a que ese nodo no cambiará su altura; estaba en h y queda en h.

La figura 11.8 izquierda, ilustra una situación general antes de la inserción por la izquierda; la
figura 11.8 derecha, muestra después de la inserción y de la corrección del factor de balance.

A A
0 detener
h 1

h -1 h h
h

Figura 11.8 Detención de revisión en ascenso. Caso a.

11.2.2. Continuar revisando factores de balance en ascenso.

b) Al insertar por la izquierda, y en el proceso de ascenso de revisión de los factores de balance,


si se llega a un nodo con factor cero, debe corregirse el factor de ese nodo (quedando éste en
menos uno) y es preciso seguir el ascenso. Esto debido a que ese nodo cambió su altura; estaba
en h y queda en h+1.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
A
A
-1 continuar
0

h+1 h
h h

Figura 11.9 Continuar revisión en ascenso. Caso b.

La Figura 11.9, a la izquierda, ilustra la situación antes de la inserción por la izquierda; la figura
a la derecha muestra después de la inserción y de la corrección del factor de balance. Pero la
inserción deja el subárbol cumpliendo la propiedad AVL.

Existen dos casos adicionales, que corresponden a inserciones por la derecha, y pueden
visualizarse con las imágenes especulares de las mostradas.

11.2.3. Casos que producen desbalances.

Otras inserciones producen desbalances que dejan al subárbol no AVL.

Para encontrar en qué situaciones se producen desbalances que rompan la propiedad AVL,
basaremos nuestro análisis en el siguiente subárbol AVL, al cual si se le inserta un nodo en el
subárbol derecho, quedará no AVL.

Dada la estructura de un árbol AVL, el cual está formado por subárboles AVL, se analiza un
árbol AVL de altura dos, pero el análisis es válido para cualquier subárbol AVL. Se escoge un
caso sencillo para extraer de él, el caso general:

0 0

0 0

Figura 11.10 Árbol AVL, para analizar pérdida de propiedad AVL.

Trataremos de insertar en posiciones que desbalanceen el árbol, notando que se deben recalcular
los factores de balance, a través de la trayectoria desde el nodo insertado hacia la raíz, y si
aparece uno con factor 2, se pierde la propiedad AVL.
11.2.3.1. Inserción externa por la derecha.
c) Si se inserta nodo F, en la rama externa más larga del subárbol derecho:
La relación de orden del árbol binario es: A<B<C<D<E<F

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 9
2 B

0 A 1 D

0 C 1 E

0 F

Figura 11.11 Árbol no AVL, después de inserción externa por la derecha.

Se trata igual el caso: F<E. Inserciones de nodos con valores menores que B, mejoran los
factores de balance, mantienen la propiedad AVL, y no existe necesidad de corregir. Han sido
tratados en los casos a y b.

Se detecta la pérdida de propiedad AVL, para este caso, cuando el factor de balance de un nodo
recalculado después de la inserción es +2, y el factor de balance del hijo derecho de éste es
positivo.
11.2.3.2. Inserción interna por la derecha.
d) Si se inserta nodo D, en la rama interna más larga del subárbol derecho.
Con orden: A<B<C<D<E<F
2 B

0 A -1 E

1 C 0 F

0 D

Figura 11.12 Árbol no AVL, después de inserción interna por la derecha.

Se trata igual el caso D<C. Inserciones para nodos con valores menores que B, mejoran los
factores de balance, y no existe necesidad de corregir.

Esta situación se detecta cuando el factor de balance de un nodo es +2, y el factor de balance del
hijo derecho de éste es negativo.

11.3.4. Rotaciones para mantener propiedad AVL.

Se analizan correcciones a los casos c y d, para mantener la propiedad AVL:

El caso c) requiere una reestructuración de los nodos para mantener la propiedad AVL.
Manteniendo la relación de orden: A<B<C<D<E<F, se denomina rotación simple a la izquierda,
la que deja al subárbol, según:

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

0 D

0 B 1 E

0 A 0 C 0 F

Figura 11.13 Árbol AVL de Figura 11.11, después de rotación simple a la izquierda.

El caso d) de la Figura 11.12, también requiere reestructurar para mantener el árbol con la
propiedad AVL.

Se corrige con una doble rotación. Primero una a la derecha, que hace ascender C (la situación
después de esta rotación, se muestra en la parte izquierda de la Figura 11.14) y luego otra a la
izquierda, que hace ascender C hasta la raíz, después de la cual se muestra en la Figura 11.14 a
la derecha.

2 B

0 C
0 A 2 C

-1 B 0 E
0 E

0 A 0 D 0 F
0 D 0 F

Figura 11.14 Árbol AVL de la figura 11.12, después de doble rotación.

En ambas se conserva la relación de orden: A<B<C<D<E<F

Existen dos casos adicionales, que corresponden inserciones por la izquierda, y pueden
visualizarse con las imágenes especulares de las mostradas.
11.3.4.1. Corrección con rotación simple en inserción.
Se puede generalizar, el caso c) con el diagrama de la Figura 11.15. La figura a la izquierda
ilustra la situación antes de agregar un nodo en el subárbol derecho de B. La figura al centro
muestra el árbol desbalanceado, no AVL. A la derecha se muestra después de una rotación
simple a la izquierda, la cual mejora el balance y genera un árbol AVL.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 11
A A B
1 2 0
positivo A
B B
0 1 0

h h h+1
h h h h+1 h h

Figura 11.15 Generalización de caso c). Rotación simple.

La rotación a la derecha es la imagen especular de las figuras.

Sólo se afectan los factores de balance del trayecto ascendente desde el nodo insertado hacia la
raíz.
11.3.4.2. Corrección con rotación doble en inserción.
El caso d) también se puede generalizar. Si se agrega nodo en el subárbol izquierdo o derecho
del nodo B , Figura 11.16 izquierda, se producirán cambios en los factores de balance, desde el
nodo insertado hacia la raíz. Ocasionando el desbalance que se muestra en la figura a la derecha,
el árbol no es AVL.
A A
negativo
1 2
C C
0 -1
B B
0 -1

h h h h
h-1 h-1 h h-1

Figura 11.16 Generalización de caso d).

Lo cual se corrige, con una rotación a la derecha, que hace ascender B , Figura 11.17 izquierda,
y luego otra a la izquierda para llevar B a la raíz del subárbol, produciendo un árbol AVL.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
A
B
2
0
B
A C
1
C 0 1
1

h h-1
h h
h h
h h-1

Figura 11.17 Generalización de caso d). Doble rotación.

Del análisis anterior, el algoritmo de inserción, debe implementar la rotación simple a la derecha
y la rotación simple a la izquierda.

Observando las correcciones después de las rotaciones, se concluye que no es necesario seguir
revisando los factores de balance de los nodos superiores a la raíz del subárbol que
originalmente produjo la necesidad de balancear (el que llegó a +2 ó -2), ya que éste queda con
factor de balance 0. El alto de ese nodo es (h+1), antes y después de la inserción y las
correcciones.

11.3. Análisis del descarte de un nodo.

Se descarta en forma similar a un árbol binario. Sin embargo, debido a la propiedad AVL, si el
nodo a descartar tiene un solo subárbol (derecho o izquierdo), ese subárbol debe ser una hoja.
Si tiene dos subárboles, de acuerdo al factor de balance se descarta el nodo que mejore el
balance.

Luego del descarte, debe ascenderse para mantener los factores de balance de la trayectoria
hacia la raíz, se pueden presentar varios casos. Debido a la simetría sólo se analizan casos de
descarte por la izquierda, la solución para descartes por la derecha, se obtiene mediante
imágenes especulares.

11.3.1. Detención de la revisión de los factores de balance en el descarte.

a) Al descartar por la izquierda, y en el proceso de ascenso de revisión de los factores de


balance, si se llega a un nodo con factor cero, basta corregir el factor de ese nodo (quedando
éste en +1) y no es preciso seguir el ascenso. Esto debido a que ese nodo no cambiará su altura;
estaba en h y queda en h.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 13
A A
0 1

h h h-1 h

Figura 11.18 Descarte por izquierda. Se mantiene AVL. Caso a.

11.3.2. Continuar revisando factores de balance en el descarte.

b) Al descartar por la izquierda, y en el proceso de revisión de los factores de balance, si se llega


a un nodo con factor menos uno, se debe corregir el factor de ese nodo (quedando éste en cero)
y es preciso seguir revisando en la vía de ascenso. Esto debido a que ese nodo cambió su altura
de (h+1) a h.
A A
-1 0

h+1 h h h

Figura 11.19 Descarte por izquierda. Se mantiene AVL. Caso b.

11.3.3. Rotación simple para corregir desbalance en descarte. Detener revisión.

c) En la situación de la figura a la izquierda, se descarta por la izquierda. La figura central


muestra la situación, y la necesidad de rebalancear por pérdida de propiedad AVL. Lo cual se
logra con una rotación a la izquierda, que se muestra en la Figura 11.20 de la derecha,
generando un árbol AVL. No es preciso seguir la revisión ascendente, ya que el subárbol, no
cambia su altura.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
A A B
cero
1 2 -1
B B A
0 0 1

h h-1 h
h h h h h-1 h

Figura 11.20 Descarte por rama izquierda. Deja de ser AVL. Caso c.

11.3.4. Rotación doble para corregir el desbalance en descarte.

d) En la situación de la figura 11.21 a la izquierda, se descarta por la izquierda. La figura central


muestra la situación, y la necesidad de rebalancear por pérdida de propiedad AVL. Lo cual se
logra con una doble rotación (primero a la derecha, luego a la izquierda). Es preciso seguir la
revisión ascendente, ya que el subárbol, cambia su altura de (h+1) a h.
A B
A
1 0
2 negativo
C A C
C
-1 0 0
-1
B
B
0
0
h-1
h-1 ó h-1
h h-1 h-1 h-1
h-1 h-1 h-2
h-1 ó h-1
h-1 ó
h-2
h-2
Figura 11.21 Descarte por rama izquierda. Deja de ser AVL. Caso d.

11.3.5. Rotación simple para corregir desbalance en descarte. Continuar revisión.

e) En la situación de la figura a la izquierda, se descarta por la izquierda. La figura central


muestra la situación, y la necesidad de rebalancear por pérdida de propiedad AVL. Lo cual se
logra con una rotación a la izquierda. Es preciso seguir la revisión ascendente, ya que el
subárbol cambia su altura, de (h+1) antes del descarte a h.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 15
A A B
positivo
1 2 0
B A
B
1 1 0

h h-1 h
h-1 h h-1 h h-1 h-1

Figura 11.22 Descarte por izquierda. Deja de ser AVL. Caso e.

Las figuras centrales de 11.21 y 11.22 muestran que se puede discernir entre los casos d y e,
observando el hijo derecho del nodo que pasó a tener factor de balance dos.

Esto implica que la función descartar debe analizar 10 casos. Cinco en descartes por la izquierda
y 5 por la derecha.

Se analizan en detalle rotaciones simples, que se deben emplear para mantener la propiedad de
árbol AVL, en aquellos casos que lo requieren.

11.4. Rotación simple a la izquierda.

Al inicio t apunta a la raíz del subárbol.


Luego de temp = t y t = t->right, queda la figura de la izquierda.
Luego de temp->right = t->left; y t->left = temp queda la figura de la derecha.

temp A B t
temp 0
2 t
B A
a 1
0 c
b c
a
h b
h h+1
h h h+1

Figura 11.23 Rotación simple a la izquierda.

En caso de un árbol AVL, la corrección de los factores de balance se puede efectuar según:

temp->bal =0 y t->bal =0.

Sin embargo para lograr una rutina general de rotación se analiza la siguiente situación:

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
temp A B t
temp nB
x t
B A
y
nA

a
b c
a b c

Figura 11.24 Análisis de los factores de balance.

Sean a, b y c los altos de los subárboles, que no cambian. Antes de la rotación, los factores de
balance de los nodos A y B son x e y, respectivamente; luego de la rotación éstos se denominan:
nA y nB, según se muestra en la Figura 11.24.

En la figura de la izquierda se cumplen:

y = c-b, x=b+1-a si b>c, o x=c+1-a si c>b

En la figura de la derecha se cumplen, por la definición del factor de balance:

nA= b-a, nB= c-1-a si a>b, o nB=c-1-b si b>a.

Se desea determinar nA y nB en términos de x e y.

Cálculo de nA:
Reemplazando (b-a) por (x-1) para b>c, y por (c-a)-(c-b) para c>b, se obtienen:

nA=x-1-0 con b>c o bien: nA=x-1-y para c>b

Las últimas dos ecuaciones pueden anotarse:

nA=x-1-max(y,0)

Lo cual puede comprobarse, ya que si y es negativo (para b>c), queda nA=x-1; y si y es positivo
(para c>b), queda nA=x-1-y.

Cálculo de nB:
Para a>b, se tiene reemplazando (c-a) por (b-a)+(c-b) si b>c se obtiene:
nB=(c-a)-1=(x-1)+y-1 es decir: nB=x-2+y
Para a>b, se tiene reemplazando (c-a) por (x-1) si c>b se obtiene
nB=(c-a)-1=x-2+0.
Las dos relaciones anteriores pueden anotarse, en forma compacta:

nB=x-2+min(y, 0)

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 17

Lo cual puede comprobarse, ya que si y es negativo (para b>c) se tiene que min(y,0) es y; si
y>0, se tiene que min(y,0) es cero.

Para b>a, se tiene nB= y-1

Observando la siguiente relación, que puede deducirse de la Figura 11.24 derecha:

nB = c-(max(a,b)+1)

Entonces nB debe ser la menor de las diferencias posibles:

nB = min(c-a-1, c-b-1)

Reemplazando (c-a-1) por nB y (c-b-1) por (y-1), se tiene:

nB = min(nB, y-1)

Finalmente, reemplazando nB, en la expresión en la derecha por x-2+min(y, 0), calculada antes,
se obtiene:
nB = min(x-2+min(y,0), y-1)

El siguiente segmento corrige factores de balance en una rotación simple a la izquierda:


x = temp->bal; // oldbal(A)
y = t->bal; // oldbal(B)
temp->bal = x-1-max(y, 0); // newbal(A)
t->bal = min(x-2+min(y, 0), y-1); // newbal(B)

Los siguientes macros implementan las funciones mínimo y máximo

# define max(A,B) ((A)>(B)?(A):(B)) /* Definición de macro */


# define min(A,B) ((A)>(B)?(B):(A))

Nótese los argumentos de los macros entre paréntesis.


La rotación simple a la derecha es la imagen especular del caso recién analizado.

11.5 Operaciones.

11.5.1. Definición de tipos.

typedef struct avlnode {


int clave;
int bal; /* Factor de balance -1,0,1 */
struct avlnode *left, *right;
} nodo, *pnodo;

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
11.5.2. Definición variables globales: Flag, key, alto_avl.

int flag; /* Marca para registrar cambios de altura. En rebalance ascendente */


//flag = 1 indica que debe seguir el ascenso rebalanceando.
int key; /* Variable global, para disminuir argumentos */
int alto_avl = 0; /* Altura árbol avl. Número de nodos desde la raíz a las hojas.*/

11.5.3. Inserta nodo en sub-árbol apuntado por t.

/* Mantiene variable global con el alto del árbol. */


pnodo InsertarAVL(int clave, pnodo t)
{
key = clave; //pasa argumento a global.
t = insertR(t);
if (flag == 1) alto_avl++;
//si la propagación llega hasta la raíz, aumenta la altura.
return t;
}

tree insertR(tree t)
{
if (t == NULL){ /* Llegó a un punto de inserción */
t = CreaNodo(key); /* Crea nuevo nodo */
t->bal = 0; /* Los dos hijos son nulos */
flag = 1; /* Marca necesidad de revisar balances */
return t; /* retorna puntero al insertado */
}
else if (t->clave < key){
//desciende por la derecha
t->right = insertR(t->right);
//se pasa por la siguiente línea en la revisión ascendente
t->bal += flag; /* Incrementa factor de balance */
}
else if (t->clave > key){
//desciende por la izquierda
t->left = insertR(t->left);
//se corrige en el ascenso
t->bal -= flag; /* Decrementa balance */
}
else { /* (t->k == key) Ya estaba en el árbol */
Error(1);
flag = 0;
}

if (flag == 0) /* No hay que rebalancear. Sigue el ascenso */


return t;

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 19
/*El código a continuación es el costo adicional para mantener propiedad AVL */
/* Mantiene árbol balanceado avl. Sólo una o dos rotaciones por inserción */
if(t->bal < -1) {
/* Quedó desbalanceado por la izquierda. Espejos casos c y d.*/
if(t->left->bal > 0)
/* Si hijo izquierdo está cargado a la derecha */
t->left = lrot(t->left);
t = rrot(t);
flag = 0; /* El subárbol no aumenta su altura */
}
else if(t->bal > 1) {
/* Si quedó desbalanceado por la derecha. Casos c y d.*/
if(t->right->bal < 0)
/* Si hijo derecho está cargado a la izquierda Caso d. Fig. 11.16*/
t->right = rrot(t->right);
t = lrot(t); /*caso c.*/
flag = 0; /* El subárbol no aumenta su altura */
}
else if(t->bal == 0)/* La inserción lo balanceo */
flag = 0; /* El subárbol no aumenta su altura. Caso a. Fig. 11.8*/
else /* Quedó desbalanceado con -1 ó +1 Caso b. Fig. 11.9 */
flag = 1; /* Propaga ascendentemente la necesidad de rebalancear */
return t;
}

11.5.4. Descarta nodo en sub-árbol apuntado por t.

pnodo DescartarAVL(int clave, pnodo t)


{
key = clave;
t = deleteR(t);
if (flag == 1) alto_avl--;
return t;
}

pnodo deleteR(pnodo t)
{ pnodo p;
if(t == NULL) { /* No encontró nodo a descartar */
Error(0);
flag = 0;
}
else if(t->clave < key) {
//Comienza el descenso por la derecha
t->right = deleteR(t->right);
//aquí se llega en el retorno ascendente.
t->bal -= flag; /* Se descartó por la derecha. Disminuye factor */

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
//Se retorna después de la revisión de los factores
}
else if (t->clave > key) {
//Desciende por la izquierda
t->left = deleteR(t->left);
//o se llega por esta vía si se descartó por la izquierda.
t->bal += flag; /* se descartó por la izq. Aumenta factor de balance */
}
else { /* (t->clave == key) */
/* Encontró el nodo a descartar */
if (t->left == NULL) { /*Si hay hijo derecho debe ser hoja, por ser AVL */
p = t;
t = t->right;
free(p);
flag = 1; /* Debe seguir revisando factores de balance */
return t; /* ascendentemente */
}
else if (t->right == NULL) { /*Si hay hijo izquierdo debe ser hoja */
p = t;
t = t->left;
free(p);
flag = 1; /* Asciende revisando factores de balance */
return t; /* Corrigiendo */
} else { /* Tiene dos hijos */
if(t->bal<0) {
/* Si cargado a la izquierda, elimina mayor descendiente hijo izq */
p = t->left;
while (p->right != NULL) p = p->right;
t->clave = p->clave;
key = p->clave; //busca hoja a eliminar
t->left = deleteR(t->left);
t->bal += flag; /* incrementa factor de balance */
} else {
/* Si cargado a la derecha, elimina menor descendiente hijo der */
p = t->right;
while (p->left != NULL) p = p->left;
t->clave = p->clave;
key = p->clave;
t->right = deleteR(t->right);
t->bal -= flag; /* decrementa balance */
}
}
}

/* Mantiene árbol balanceado avl. Sólo una o dos rotaciones por descarte */
if (flag == 0 ) /* No hay que rebalancear. Sigue el ascenso, sin rebalancear */
return t;

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 21
/* Hay que revisar factores de balance en el ascenso*/
if(t->bal < -1) {
/* Si quedó desbalanceado por la izquierda y dejó de ser AVL */
if(t->left->bal > 0) { /*espejos casos c, d y e */
/* Si el hijo izquierdo está cargado a la derecha */
t->left = lrot(t->left);
flag = 1; /*Continuar revisando factores */
}
else if (t->left->bal == 0)
flag = 0; /*No debe seguir el rebalance */
else
flag = 1;/* Debe seguir revisando factores de balance */
t = rrot(t);
}
else if(t->bal > 1) {
/* Si quedó desbalaceado por la derecha */
if(t->right->bal < 0) {
/* Si hijo derecho está cargado a la izquierda */
t->right = rrot(t->right);
flag = 1; //debe seguir revisando. Caso d.
}
else if (t->right->bal == 0)
flag = 0; /* No debe seguir el rebalance. Caso c. */
else //positivo
flag = 1;/* Debe seguir revisando factores de balance. Caso e. */
t = lrot(t);
}
else if (t->bal == 0) /* Si estaba en +1 ó -1 y queda en cero */
flag = 1; /* Debe seguir corrigiendo. Caso b.*/
else /* Si estaba en cero y queda en -1 ó +1 */
flag = 0; /* No debe seguir rebalanceando. Caso a.*/
return t;
}

11.5.5. Rotaciones.

/* Rotación Izquierda
*
* A B
* / \ / \
* a B ==> A c
* / \ / \
* b c a b
* Sólo cambian los factores de balance de los nodos A y B
* Los factores de balance de los sub-árboles no cambian.
*/

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
static pnodo lrot(pnodo t)
{ pnodo temp;
int x,y;
temp = t;
t = t->right;
temp->right = t->left;
t->left = temp;
//Recalcula factores de balance de los dos nodos
x = temp->bal; // oldbal(A)
y = t->bal; // oldbal(B)
temp->bal = x-1-max(y, 0);
t->bal = min(x-2+min(y, 0), y-1);
return t;
}

/* Rotación derecha
*
* A B
* / \ / \
* B c ==> a A
* / \ / \
* a b b c
*
*/
static pnodo rrot(pnodo t)
{ tree temp = t;
int x,y;

t = t->left;
temp->left = t->right;
t->right = temp;
x = temp->bal;
y = t->bal;
/* x=c-1-a ó x=c-1-b. y = b-a
* nA = c-b. nB =c+1-a ó nB=b+1-a
* nA= x+1-y o nA=x+1-0
* nA = x+1-min(y,0)
* nB = max(b,c)+1-a => max(b-a+1,c-a+1)
* => max(y+1,x+2+max(y,0))
*/
temp->bal = x+1-min(y, 0);
t->bal = max(x+2+max(y, 0), y+1);
return t;
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 23
11.5.6. Otras funciones.

static void Error(int tipo)


{
if (tipo) printf("\nError en inserción\n");
else printf("\nError en descarte\n");
}

int Altura(void)
{
return alto_avl;
}

pnodo deltree(pnodo t) //borra el árbol completo


{
alto_avl = 0;
return deltreeR(t);
}

pnodo deltreeR(pnodo t)
{
if (t != NULL) {
t->left = deltreeR(t->left);
t->right = deltreeR(t->right);
free(t); //borra la raíz subárbol
}
return NULL;
}

void inorder(pnodo t, int profundidad)


{
if (t != NULL) {
inorder(t->left, profundidad+1);
printf ("v= %d p=%d bal=%d \n", t->clave, profundidad, t->bal);
inorder(t->right, profundidad+1);
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos

Problemas resueltos.

P11.1. Para el siguiente árbol AVL


7

3 8

1 4

Figura P11.1.

a) Indique los factores de balance de cada nodo.


b) Dibujar el árbol AVL, después de la inserción de un nodo con valor 2. ¿Qué operaciones se
efectúan?.
c) Habiendo ya insertado el nodo con valor 2, dibujar el árbol AVL, después de la inserción de
un nodo con valor 6. ¿Qué operaciones se efectúan?.

Solución.
a) Los factores de balance:
-1 7

0 3 0 8

1 0 4 0

Figura P11.2.

b) Luego de insertar el 2, no queda AVL. Con una rotación simple a la derecha, se reestablece
la propiedad AVL.
+1
3
-2 7 0
3
+1 1 -1 7
3 -1 0 7
8 +1 1 0
0 2 +1 4 8 0
1 +1 4 0 0 8 0
0 2 4
6 0
2 0

Figura P11.3.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 25
c) La inserción del 6, no requiere rotaciones para mantener la propiedad AVL.

Ejercicios propuestos.

E11.1 Dado el siguiente árbol AVL:

4 12

2 7 9 14

1 3 5 11 13 15
1
Figura E11.1.

a) Determinar el árbol y el factor de balance de cada nodo después de Insertar nodos con
valores 6 y 10. Especificar las operaciones elementales para mantener el balance, indicando
cómo queda el árbol.

b) Determinar el árbol y el factor de balance de cada nodo después de Descartar nodos con
valores 13, 14, 15, 9 y 11. Especificar las operaciones elementales para mantener el balance,
indicando cómo queda el árbol.
Nota: en a) y b) se parte del árbol dado. Son situaciones independientes.

Referencias.

Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975.

G.M. Adel'son-Vel'skii and E.M. Landis. “An algorithm for the organization of information.”
Soviet Mathematics Monthly, Volume 3, pp.1259-1263, 1962.

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 11 ............................................................................................................................................1
ÁRBOLES BINARIOS BALANCEADOS DE BÚSQUEDA. AVL. ......................................................1
11.1 ANÁLISIS DE COMPLEJIDAD. ..............................................................................................................1
11.2. ANÁLISIS DE LA INSERCIÓN. .............................................................................................................6
11.2.1. Detención de revisión en ascenso. ...........................................................................................7
11.2.2. Continuar revisando factores de balance en ascenso. .............................................................7
11.2.3. Casos que producen desbalances. ............................................................................................8
11.2.3.1. Inserción externa por la derecha. ....................................................................................................... 8
11.2.3.2. Inserción interna por la derecha. ....................................................................................................... 9
11.3.4. Rotaciones para mantener propiedad AVL. .............................................................................9
11.3.4.1. Corrección con rotación simple en inserción................................................................................... 10
11.3.4.2. Corrección con rotación doble en inserción. ................................................................................... 11
11.3. ANÁLISIS DEL DESCARTE DE UN NODO. ..........................................................................................12
11.3.1. Detención de la revisión de los factores de balance en el descarte. ......................................12
11.3.2. Continuar revisando factores de balance en el descarte. ......................................................13
11.3.3. Rotación simple para corregir desbalance en descarte. Detener revisión.............................13
11.3.4. Rotación doble para corregir el desbalance en descarte. ......................................................14
11.3.5. Rotación simple para corregir desbalance en descarte. Continuar revisión. ........................14
11.4. ROTACIÓN SIMPLE A LA IZQUIERDA. ...............................................................................................15
11.5 OPERACIONES. ................................................................................................................................17
11.5.1. Definición de tipos. ................................................................................................................17
11.5.2. Definición variables globales: Flag, key, alto_avl. ..............................................................18
11.5.3. Inserta nodo en sub-árbol apuntado por t. ............................................................................18
11.5.4. Descarta nodo en sub-árbol apuntado por t. .........................................................................19
11.5.5. Rotaciones. .............................................................................................................................21
11.5.6. Otras funciones. .....................................................................................................................23
PROBLEMAS RESUELTOS. ........................................................................................................................24
P11.1. Para el siguiente árbol AVL ...................................................................................................24
EJERCICIOS PROPUESTOS. ........................................................................................................................25
E11.1 Dado el siguiente árbol AVL: ..................................................................................................25
REFERENCIAS. .........................................................................................................................................25
ÍNDICE GENERAL. ....................................................................................................................................26
ÍNDICE DE FIGURAS. ................................................................................................................................27

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles balanceados AVL 27

Índice de figuras.

Figura 11.1 Árboles Fibonacci AVL, con alturas 0, 1................................................................... 2


Figura 11.1.a. Árboles Fibonacci AVL, con altura 2. ................................................................... 2
Figura 11.2 Ejemplo árbol AVL Fibonacci, con altura 3. ............................................................. 2
Figura 11.3 Árbol AVL Fibonacci, con altura 4. .......................................................................... 3
Figura 11.4 Cota AVL en función de n. ........................................................................................ 4
Figura 11.5 Complejidad de la altura de un árbol AVL. ............................................................... 5
Figura 11.6 Alargue de altura árbol AVL, respecto de perfectamente balanceado ....................... 5
Figura 11.6.a. Razón áurea . ........................................................................................................ 6
Figura 11.7 Inserciones que sólo requieren recalcular factor de balance. ..................................... 7
Figura 11.8 Detención de revisión en ascenso. Caso a.................................................................. 7
Figura 11.9 Continuar revisión en ascenso. Caso b....................................................................... 8
Figura 11.10 Árbol AVL, para analizar pérdida de propiedad AVL. ............................................ 8
Figura 11.11 Árbol no AVL, después de inserción externa por la derecha. .................................. 9
Figura 11.12 Árbol no AVL, después de inserción interna por la derecha. .................................. 9
Figura 11.13 Árbol AVL de Figura 11.11, después de rotación simple a la izquierda. .............. 10
Figura 11.14 Árbol AVL de la figura 11.12, después de doble rotación. .................................... 10
Figura 11.15 Generalización de caso c). Rotación simple. ......................................................... 11
Figura 11.16 Generalización de caso d). ..................................................................................... 11
Figura 11.17 Generalización de caso d). Doble rotación. ........................................................... 12
Figura 11.18 Descarte por izquierda. Se mantiene AVL. Caso a. ............................................... 13
Figura 11.19 Descarte por izquierda. Se mantiene AVL. Caso b. ............................................... 13
Figura 11.20 Descarte por rama izquierda. Deja de ser AVL. Caso c......................................... 14
Figura 11.21 Descarte por rama izquierda. Deja de ser AVL. Caso d. ....................................... 14
Figura 11.22 Descarte por izquierda. Deja de ser AVL. Caso e.................................................. 15
Figura 11.23 Rotación simple a la izquierda. .............................................................................. 15
Figura 11.24 Análisis de los factores de balance. ....................................................................... 16
Figura P11.1. ............................................................................................................................... 24
Figura P11.2. ............................................................................................................................... 24
Figura P11.3. ............................................................................................................................... 24
Figura E11.1. ............................................................................................................................... 25

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 12.

Árboles coloreados. Red black.

Si las claves siempre ingresan ordenadas en forma aleatoria, puede emplearse un árbol binario
de búsqueda. Sin embargo si por momentos se producen ingresos de claves en orden, debe
escogerse una estructura que pueda rebalancear dinámicamente el árbol. El árbol coloreado,
como se verá, tiene un criterio de balance un tanto más relajado que un AVL.
Por esta razón, si los datos ingresan preponderantemente ordenados el árbol AVL tiene un mejor
comportamiento que el red-black, ya que tiene una altura menor.
Si los datos son accesados mayormente en forma secuencial, el árbol desplegado, splay, tiene un
mejor comportamiento que los anteriores.

12.1. Propiedades de los árboles coloreados.

Se agrega a cada nodo un dato que indica su color. Existen reglas que deben cumplirse para
asignar el color a cada nodo, de tal modo que una trayectoria cualquiera desde un nodo a una
hoja no sea mayor que el doble del largo de cualquier otra. Esto asegura que el árbol coloreado
se mantenga más o menos balanceado, asegurando un costo logarítmico para las operaciones en
peor caso.

Propiedades:
1. Cualquier nodo es rojo o negro.
2. Cualquier descendiente de hoja se considera negro. (Los nodos externos, son negros; éstos no
son nodos con información.) La raíz debe ser negra.
3. Si un nodo es rojo, sus hijos son negros. No hay dos rojos adyacentes.
4. Cualquier trayecto desde un nodo hacia una hoja contiene el mismo número de nodos negros.

La Figura 15.1 muestra los nodos con valores 4, 7, 12 y 20 de color rojo. La estructura cumple
las propiedades anteriores.

4 15

2 5 12 20

Figura 12.1 Árbol de búsqueda binaria coloreado.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Una buena interpretación de los nodos rojos en un árbol coloreado, es dibujarlos en un nivel
horizontal, de este modo se refleja que no alteran las alturas negras de los nodos. Esto se
muestra en la Figura 12.2, para el árbol coloreado de la Figura 12.1. En la representación con
rojos en nivel horizontal, debe cuidarse que cada nodo tenga dos hijos, para que el árbol sea
binario.

También puede verse, con esta representación, que en un árbol coloreado todas las hojas tienen
la misma altura. El árbol coloreado es un caso particular de un B-Tree, en los cuales los nodos
pueden almacenar varias claves.

4 9

2 5 7 12 15 20

Figura 12.1a Árbol coloreado con rojos horizontales.

Debido a la presencia de nodos rojos, al menos la mitad de los nodos de un trayecto de un nodo
hasta las hojas deben ser negros.
La trayectoria más larga, una que alterna entre nodos rojos y negros, es sólo el doble del largo
de la trayectoria más corta, la formada sólo por nodos negros.

Figura 12.2 Trayectos en peor caso.

Luego de insertar desde el 1 hasta el 14, que es un peor caso de árbol binario de búsqueda, en el
árbol coloreado que se muestra en la Figura 12.2, el trayecto más largo es de 6 nodos por la vía
más larga y de tres nodos por la más corta.
En la Figura 12.2, puede comprobarse que las alturas negras de todos los nodos son iguales, y
que no hay dos rojos adyacentes; además la raíz es negra, cumpliendo todas las propiedades.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 3
12.2. Complejidad en árboles coloreados.

En un árbol coloreado, con n nodos internos, debe cumplirse que la altura h es a lo más:

h 2log(n 1)

Se define la función altura negra de un nodo x, bh(x), como el número de nodos negros de
cualquier trayectoria desde x hasta una hoja, no contando el nodo x.

Se desea probar, mediante inducción, que un subárbol, que parte en el nodo x, contiene a lo
menos 2bh(x)-1 nodos internos.

Si x es un nodo externo, entonces bh(x) = 0, y el número de nodos internos es: 20-1=0.


Si x es una hoja, entonces bh(x) = 1, y el número de nodos internos es: 21-1=1.
Si x tiene alto h, los nodos hijos de x, tienen altura (h-1).
Si el hijo es rojo tiene altura negra: bh(x), igual a la del padre
Si el hijo es negro tiene altura negra: bh(x)-1, ya que no se cuenta el nodo negro.

Considerando verdadera la proposición para el número de nodos internos del subárbol que
comienza en x; los subárboles de los hijos de x, deben entonces tener a lo menos: 2bh(x)-1-1
nodos internos.

Para obtener el número de nodos internos del subárbol que comienza en x, sumamos los nodos
internos de los subárboles hijos, más el nodo interno x, se obtiene:

n >= (2bh(x)-1-1) + (2bh(x)-1-1) + 1 = 2(2bh(x)-1) - 1=2bh(x)-1

Lo cual demuestra la proposición inicial.

Pero en un árbol coloreado al menos la mitad de los nodos de un trayecto de un nodo hasta las
hojas deben ser negros, entonces si h es la altura de un árbol, que comienza en x, se tiene que:

h
bh( x)
2

Entonces, reemplazando en la expresión de n, en función de bh(x), la cota para bh(x), se obtiene:

n 2bh( x) 1 2h / 2 1
Despejando h, se logra:
h 2log(n 1) (log(n))

Debe notarse que ésta es la complejidad de peor caso.

Al insertar o descartar nodos en un árbol coloreado, pueden violarse las propiedades que los
definen; y para mantenerlo coloreado, como se verá más adelante, deben cambiarse los colores

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
de algunos nodos y también posiblemente efectuar rotaciones. Esto se refleja en un costo
adicional para las funciones de inserción y descarte en un árbol binario de búsqueda.

Para un árbol AVL, la cota para la altura resulta menor que en árbol coloreado:

hAVL 1, 4404 log(n) (log(n))

La Figura 12.2a, muestra la complejidad del árbol coloreado, respecto del AVL, y puede
observarse que son muy similares. Pero las operaciones se realizan en menor tiempo, en un
árbol coloreado.

En un árbol binario de búsqueda, en promedio, si las claves llegan aleatoriamente, se tiene la


altura: hBST 1,3863log( n) (log(n)) . Pero ésta se incrementa hasta n, en el peor caso.

coloreado

AVL

Bst promedio

balanceado

Figura 12.2a. Comparación de complejidades. Red-Black, AVL, Balanceado.

12.3. Análisis de inserción.

La inserción de un nuevo nodo siempre se realiza como descendiente de una hoja. Se inserta el
nuevo nodo con color rojo, ya que esto no altera las alturas negras de los trayectos. Cuando la
hoja donde se insertará el nuevo nodo es roja, se pierde la propiedad de ser un árbol coloreado,
ya que se producen dos nodos rojos adyacentes.

Si se inserta un nodo con color rojo, y si el árbol estaba vacío, debe cambiarse a negro para
mantener la propiedad de que la raíz sea negra.

Si se inserta un nodo en la raíz, como ésta es negra, se mantienen las propiedades de los árboles
coloreados.

En caso de inserción en árboles de mayores niveles, la inserción de un rojo en una hoja roja
requiere efectuar modificaciones para preservar las propiedades, en algunos casos bastará una

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 5
recoloración y en otros deben efectuarse rotaciones. A medida que se van efectuando estas
modificaciones puede que en el ascenso hacia la raíz se vuelva a producir la situación de dos
rojos adyacentes. Estudiaremos los diferentes casos que se producen y las soluciones propuestas
para mantener las propiedades.

Lo primero que observamos es que cuando se produce un doble rojo, el abuelo del nodo rojo
insertado o del rojo que asciende debe ser negro, a este último nodo lo denominaremos con x, en
los diagramas. Esto es así ya que antes de la inserción el árbol era coloreado, y por lo tanto no
pueden tenerse tres rojos adyacentes. La segunda consideración es que las alturas negras del
nodo denominado abuelo deben ser iguales antes y después de la inserción; esto se ilustrará en
los diagramas, colocando nodos negros de tal modo de reflejar las alturas negras iguales. Se
prefiere esta representación a la de mostrar subárboles descendientes con especificaciones de
alturas negras. Una tercera consideración es que el tío del nodo rojo que asciende puede ser
negro o rojo, y que deberán analizarse ambos casos.

Se ilustran en la Figura 12.3, alturas negras del abuelo iguales a dos (más la altura negra de los
descendientes, que no se muestran), se muestra el tío de color negro.
Dicho de otra forma, el diagrama insinúa que las alturas negras del tío y de los hijos negros de
los nodos rojos deben ser iguales, y menores en una unidad a la altura negra del abuelo.

abuelo abuelo

tío tío

x
x

Figura 12.3 Dobles rojos en inserción.

Existen dos casos adicionales, que son las imágenes especulares de las mostradas en la Figura
12.3, y que corresponden a los casos en que el nodo x es insertado como descendiente derecho
del nodo abuelo.

Existen dos situaciones que deben analizarse: una corresponde a tío rojo, la otra a tío negro.

12.3.1. Recoloración. Cuando el tío es rojo.

Si en la Figura 12.4 a la izquierda, se cambian los colores del abuelo, el padre y el tío, se
mantienen las alturas negras del nodo abuelo hacia abajo, y no se tienen dos rojos adyacentes.
Sin embargo es necesario seguir la revisión ascendente debido a que el cambio de color del
abuelo podría producir nuevamente un doble rojo en el trayecto hacia la raíz. Es decir debe
volver a repetirse el proceso ascendiendo el puntero x a la posición del abuelo.
Si los cambios se propagarán hasta el caso en que el abuelo es la raíz, debería cambiarse el color
de ésta, pero se mantendrían altura negras iguales; en este caso aumenta la altura negra del
árbol.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

abuelo abuelo
padre tío padre tío

x x

Figura 12.4 Recoloración en inserción. Tío rojo.

Cuando el nodo denominado x es descendiente derecho del padre, se soluciona de igual forma.

12.3.2. Rotaciones. Cuando el tío es negro.

Se ilustra el caso en que el padre y el nodo x son descendientes izquierdos del nodo abuelo. Si se
cambian colores al padre y al abuelo, se disuelve el doble rojo. Sin embargo se altera la altura
negra del subárbol derecho del abuelo, como se muestra en la Figura 12.5, disminuye su cuenta
en uno.
abuelo abuelo

padre tío padre tío

x x

Figura 12.5 Recoloración con tío negro, no quedan dos rojos adyacentes.

Si se efectúa una rotación a la derecha de la pareja padre-abuelo, en la Figura 12.5 a la derecha


se tiene la Figura 12.6 derecha.

A partir del nodo padre hacia abajo se mantienen los largos negros de los subárboles y además
no se puede volver a producir un doble rojo en el trayecto de ascenso hacia la raíz, ya que el
nodo, denominado padre, en la Figura 12.6 derecha, es negro. Nótese que en la Figura 12.6 a la
derecha, se han conservado los nombres originales para los nodos: padre, abuelo y tío; sin
embargo luego de la rotación éstos nombres pierden su significado original. También puede
observarse que luego de la rotación el sub-árbol, cuya raíz es el padre, queda más balanceado.
Esta situación da por terminada la revisión ascendente.
abuelo

padre tío padre


abuelo
x x
tío

Figura 12.6 Rotación derecha, preserva altura negras.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 7
Aún resta analizar el caso en que el doble rojo se produce como hijo derecho de un padre rojo
descendiente de un abuelo negro (y con tío negro). Lo cual se ilustra en la Figura 12.7.
abuelo abuelo

padre tío x tío


padre
x

Figura 12.7. Rotación izquierda, par padre-x.

Antes de producirse este doble rojo, ya sea por inserción o por revisión ascendente, las alturas
negras del padre (del abuelo y sus ancestros) eran iguales. La rotación izquierda del par x-
padre, respecto al padre, no altera esas cuentas. La Figura 12.7 a la derecha, es el caso analizado
antes, que se muestra en la Figura 12.5; para el cual ya se obtuvo una solución, pero cambiando
el puntero x a la posición ocupada finalmente por el padre.

Los casos en que el nodo x es agregado como descendiente del subárbol derecho del abuelo son
situaciones con simetría especular a los tratados.

Un caso particular es cuando el tío no existe, es decir cuando es un nodo externo; en este caso se
considera tío de color negro.

Esto completa el análisis de la inserción en un árbol coloreado.

12.4. Análisis de la operación descartar.

Deben analizarse tres casos, que el nodo que se desea descartar tenga dos descendientes, uno
solo o ninguno.

Cuando se borra un nodo rojo se mantienen las propiedades de los árboles coloreados. Ya que
no cambian las alturas negras de los nodos.

12.4.1. Dos descendientes.

Cuando se borra un nodo apuntado por t, que tiene los dos subárboles descendientes, se elige el
mayor descendiente del subárbol izquierdo (I) o el menor descendiente del subárbol derecho (D)
y se cambian los datos del nodo que será eliminado con el seleccionado. Si el nodo seleccionado
(I o D) es rojo, no puede tener descendientes y será una hoja; estos casos se muestran en la
Figura 12.8.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos

t
t

I D I D

Figura 12.8. Nodo seleccionado para ser descartado es hoja roja.

Si el nodo seleccionado es negro, sólo puede tener un hijo rojo ya que el árbol es coloreado,
antes del descarte. Situación que se muestra en la Figura 12.9.
t
t
I D
I D

Figura 12.9. Nodo seleccionado para ser descartado es nodo negro con hijo rojo.

Luego de copiar los datos del nodo seleccionado en el nodo apuntado por t, se procede a
descartar el seleccionado; el cual debe ser una hoja roja o un nodo negro con sólo un hijo que
debe ser rojo. Estos casos se analizan a continuación.

12.4.2. Un descendiente.

Cuando se descarta un nodo t con un solo hijo, se presentan tres casos, que se ilustran en la
Figura 12.10, cuando t tiene hijo izquierdo y es un descendiente izquierdo. Debe notarse que el
nodo t debe ser negro, y que su único hijo debe ser rojo.

t t t

Figura 12.10 Descartar nodo negro con un hijo rojo.

Se preservan las propiedades de los árboles coloreados si se liga el hijo de t, con el padre de
éste; y se le cambia el color a negro, para mantener iguales alturas negras, y no tener dos rojos
adyacentes. Igual solución se aplica si el hijo de t es descendiente derecho.
Las soluciones se muestran en la Figura 12.10a.

t t t

Figura 12.10a Descartar nodo negro con un hijo rojo.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 9
Si t es descendiente derecho, la solución es la especular de la analizada.

12.4.3. El nodo a descartar es una hoja. Sin descendientes.

En este caso se presentan cinco situaciones, que se ilustran en la Figura 12.11.

t t t t t

Figura 12.11 Descartar hoja.

Cuando el padre es rojo, éste debe tener dos hijos negros. Luego de borrar el nodo hay que
cambiar el color del padre y del tío; esto preserva iguales alturas negras.

Cuando el nodo para descartar es rojo, hay dos casos: con hermano nulo o con hermano rojo.
Ambos casos no requieren modificaciones, y sólo es preciso borrar el nodo.

Cuando el nodo y su padre son negros se tienen dos casos, con hermano negro o rojo. En este
último caso, luego de borrar el nodo el árbol deja de ser coloreado, ya que disminuye la altura
negra del subárbol izquierdo; se soluciona rotando a la izquierda, el par padre-hermano, y
cambiando de rojo a negro el color del nuevo padre, para igualar las alturas negras. Lo cual se
muestra en la Figura 12.12. En este caso, como el hermano es rojo, el padre necesariamente
debe ser negro.

Figura 12.12 Descartar hoja negra con hermano rojo.

El último caso que debe analizarse es el de hijo, padre y hermano de colores negros. Si se
elimina la hoja y se cambia a color rojo el huérfano, se produce una disminución de la altura
negra del nodo padre, denominado x, en la Figura 12.13, lo cual hará perder al árbol sus
propiedades. Nótese que de x hacia abajo el árbol es coloreado.

x
t

Figura 12.13 Descartar hoja negra con padre y hermano negro.

La altura negra de x es bh(x)=0, si no se cuenta el nodo externo; y uno si se cuenta el nodo


externo.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
Para corregir el balance de alturas negras debe efectuarse una revisión ascendente.

12.4.4. Balance de alturas negras en descarte. Caso doble negro.

Sea x un puntero al subárbol coloreado, que disminuyó su altura negra, y desde el cual debe
revisarse hacia arriba para mantener la propiedad de iguales alturas negras de los subárboles. La
raíz de este subárbol es negra.

Si x es la raíz debe detenerse la revisión.


a) Hermano rojo.
Si x tiene padre y el hermano h es rojo, el padre debe ser negro. La Figura 12.14, a la izquierda,
ilustra este caso, cuando x es descendiente izquierdo.

h
x x
h h
x h

Figura 12.14 Hermano rojo, padre negro.

Si la altura negra de x es bh(x), las alturas negras de los subárboles descendientes del hermano
rojo son iguales a bh(x)+1. Esto se ilustra con dos descendientes negros en los subárboles de h.
Si el árbol era coloreado, antes del descarte, necesariamente x tiene padre, hermano y
descendientes.
Si se cambia el color del padre y del hermano se tiene el resultado que se muestra en la Figura
12.14, al centro. Luego rotando a la izquierda el par padre-hermano, se obtiene la situación
mostrada a la derecha de la Figura 12.14, en la cual se ha reposicionado h, apuntado al hermano
actual de x.
Tanto el actual abuelo como el padre de x, siguen desbalanceados. El objetivo de esta
transformación es lograr que x tenga un hermano negro. Que es el siguiente caso que se estudia.
b) Hermano negro.
Si el hermano es negro, el padre puede ser rojo o negro. Lo cual se representará por un signo de
interrogación al lado del nodo.

En esta situación se tienen tres casos que analizar: Una es si ambos hijos del hermano son
negros; la segunda es si el hijo derecho del hermano es negro y el izquierdo es rojo; y la tercera
es que el hijo derecho del hermano sea rojo, pudiendo ser rojo o negro el hijo izquierdo del
hermano.
b1) Sobrinos negros.
Si x y su hermano son negros, los subárboles descendientes de h, tienen altura negra igual a
bh(x)+1, lo cual se insinúa con un descendiente, en la Figura 12.15. Esto es así, ya que
originalmente se borró un descendiente negro de x, cuando el árbol era coloreado.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 11

Si se cambia el color del hermano a rojo, y se mueve x, apuntando al padre, se tendrá que desde
el nuevo x hacia abajo el árbol es coloreado. Además se fuerza a negro el padre original de x,
para evitar un posible doble rojo.
? x
x
h h
x

Figura 12.15 Hermano negro y sobrinos negros.

Luego de esta transformación la altura negra del nuevo x, aumenta en uno; y se debe seguir
revisando en forma ascendente hacia la raíz. No es necesario continuar, si el nuevo x es la raíz.
b2) Sobrino derecho negro, sobrino izquierdo rojo.
Como x y su hermano son negros, el padre de x puede ser rojo o negro. Nuevamente los
descendientes de h, deben tener alturas negras: bh(x)+1.
? ? ?
x h x h x h
h h h

Figura 12.16 Hermano y sobrino derecho negros.

Primero se cambian los colores del hermano negro y del sobrino rojo, esto se muestra en el
medio de la Figura 12.16. Luego se rota a la derecha el par hermano-sobrino derecho, lo cual se
muestra a la derecha en la Figura 12.16, en la cual se mueve h, para apuntar al nuevo hermano
de x. Esta transformación conduce al caso siguiente, en la que el sobrino derecho es rojo.
b3) Sobrino derecho rojo, sobrino izquierdo negro.
El padre de x, puede ser rojo o negro. Se procede a copiar el color del padre en el hermano, y
pintar de negro al padre y al sobrino rojo, resultado que se muestra al centro de la Figura 12.17.
? ? h
x h x h
h ? h
h x

Figura 12.17 Hermano negro, sobrino derecho rojo.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
Luego se rota a la izquierda el par padre-hermano, resultando la ilustración a la derecha de la
Figura 12.17. En ésta se ha logrado reestablecer las alturas negras de todos los nodos desde h
hacia abajo. No es preciso continuar la revisión ascendente, y el árbol completo es coloreado.

Si h queda apuntando a la raíz, debe forzarse el color negro en ese nodo.

El caso en que ambos sobrinos son rojos, se trata de igual forma que el caso b3).

?
x h
h

Figura 12.18 Hermano negro, sobrinos rojos.

Esto completa el análisis del descarte de un nodo.

12.5. Estructura de datos y funciones básicas.

12.5.1. Estructura de datos.

Si las operaciones de inserción y descarte se implementan en forma recursiva, la ruta de


búsqueda queda almacenada en el stack, es decir se disponen de las referencias al padre y al
abuelo. Después de un retorno de una función que tiene como argumento un nodo apuntado por
t; se tiene ahora en t, el puntero al padre del anterior. De esta forma es posible definir un nodo
con sólo dos punteros, el izquierdo y el derecho, además del color.

Sin embargo si las exigencias de espacio no son fundamentales la elección de una estructura que
adicionalmente mantenga un puntero al padre del nodo, presenta ventajas en la velocidad de
ejecución de las operaciones. Además las funciones resultan iterativas, y no se produce un gran
espacio del stack, para almacenar los argumentos de las funciones recursivas.

typedef struct nn { /* RB-Tree */


int color; /* Solo puede ser Rojo o Negro */
int clave; /* clave entera */
struct nn *left, *right, *padre; /* tres punteros */
} nodo, *pnodo;

12.5.2. Crea un nodo para inserción.

Se inicia el nodo con color rojo.


#define RED 0
#define BLACK 1

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 13
pnodo CreaNodo(int valor)
{ pnodo t = (pnodo) malloc(sizeof(nodo));
if (t != NULL)
{ t->color=RED; t->clave=valor;
t->left=NULL; t->right=NULL; t->padre=NULL;
}
return t;
}

12.5.3. Sucesor.

El diseño del sucesor es simple si se dispone de un puntero al padre.

pnodo sucesor(pnodo x)
{ pnodo y;
if (x->right!=NULL)
{
/* Si hay subárbol derecho desciende por la izquierda en éste,
** hasta encontrar nodo sin descendiente izquierdo.
*/
for (y=x->right; y->left!=NULL; y=y->left);
}
else
{
/* Ascender hasta encontrar nodo que esté a la izquierda de su padre
** ( o la raíz) entonces retornar el padre.
*/
for(y=x->padre; y!=NULL && x==y->right; x=y, y=y->padre );
}
return(y);
}

12.5.4. Predecesor.

Basta cambiar left por right, y viceversa, en el código del sucesor.

pnodo predecesor(pnodo x)
{ pnodo y;
if (x->left!=NULL) for (y=x->left; y->right!=NULL; y=y->right);
else
{ y=x->padre;
while(y!=NULL && x==y->left){x=y; y=y->padre;}
}
return(y);
}

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
12.5.5. Rotación a la izquierda.

Se pasa la raíz del árbol por referencia. Esto en caso que el nodo Y, se convierta en la nueva
raíz.
/*
** Rotación izquierda con padre
**
** X lrot(X) ---> Y
** / \ / \
** A Y <--- rrot(Y) X C
** / \ / \
** B C A B
** Se asume que x o y no son nulos
*/
void lrot(pnodo * raiz, pnodo x)
{ pnodo y;
//assert(x!=NULL);
//assert(x->right!=NULL);

y=x->right; /* y es el hijo derecho de x */


/* Pega subárbol izquierdo de y, como subárbol derecho de x.*/
x->right = y->left;

/* Si B no es nulo, el padre de B ahora es x */


if (y->left != NULL) y->left->padre = x;

/* Padre de y es el padre de x */
y->padre = x->padre;

if (x->padre == NULL) *raiz=y; /* Si x es la raíz, la deja apuntando a y */


/* Si x era descendiente izquierdo, pega y por la izquierda */
else if (x == x->padre->left) {x->padre->left=y;}
else {x->padre->right=y;} //sino por la derecha

y->left=x; /* Al nodo y le pega x */


x->padre = y; /* Le pega el padre a x */
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 15

12.5.6. Rotación a la izquierda.

Es el código especular del anterior.


/*
** Rotación derecha con padre
**
** X rrot(X) ---> Y
** / \ / \
** Y C <--- lrot(Y) A X
** / \ / \
** A B B C
**
**
** Se asume que x o y no son nulos
*/

void rrot(pnodo * raiz, pnodo x)


{ pnodo y;
//assert(x!=NULL);
//assert(x->left!=NULL);

y=x->left; /* y es el hijo izquierdo de x */


/* Pega subárbol derecho de y, como subárbol izquierdo de x.*/
x->left = y->right;

/* Si B no es nulo, el padre de B ahora es x */


if (y->right != NULL) y->right->padre = x;

/* Padre de y es el padre de x */
y->padre = x->padre;

if (x->padre == NULL) *raiz=y;/* Si x es la raíz, la deja apuntando a y */


/* Si x era descendiente derecho, pega y por la derecha */
else if (x == x->padre->right) {x->padre->right=y;} else {x->padre->left=y;}

y->right=x; /* Al nodo y le pega x */


x->padre = y; /* Le pega el padre a x */
}

12.5.7. Comenta errores en inserción y descarte.

void Error(int tipo, int clave)


{
if (tipo==1)
printf("Error en inserción. No se pudo crear Nodo\n");
else if (tipo==2)

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
printf("Error en inserción. Clave duplicada=%d!\n", clave);
else if (tipo==3)
printf("Error en descarte. Clave no encontrada!\n");
else printf("Error en tipo=%d!\n", tipo);
}

12.5.8. Calcula altura negra.

//revisa que se cumpla largos de negros de subárboles iguales, de cada nodo.


//Retorna -1 si hay error; sino entrega la altura negra, la función black height, bh(x).
int bh(pnodo x)
{
int nleft, nright;

if (x==NULL) return(1); //cuenta el descendiente de hoja como negro.

nleft=bh(x->left);
nright=bh(x->right);
//si hay error al retornar de los llamados recursivos
if (nleft==-1 || nright==-1) return(-1); //propaga el error

if (nleft != nright)
{
printf("Negros del izquierdo=%d difieren de los del derecho=%d, clave=%d\n",
nleft, nright, x->clave);
return(-1);
}
if (x->color == BLACK)
{
nleft++; //acumula los negros en nleft
}
return(nleft);
}

12.5.9. Revisa propiedades de árbol coloreado.

int RevisaPropiedades(pnodo x)
{ int bhl=1,bhr=1;
if (x==NULL) {printf("Árbol vacío\n"); return (1);}
if (x->left==NULL && x->right==NULL)
{
if(x->color!=BLACK) printf("Raíz no es negra\n"); return(1);
}
if (x->left!=NULL && x->right==NULL)
if(x->color!=BLACK && x->left->color!=RED)
{
printf("Nodo con solo hijo izquierdo no es negro ni tiene hijo rojo\n"); return(1);

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 17
}
if (x->right!=NULL && x->left==NULL)
if(x->color!=BLACK && x->right->color!=RED)
{
printf("Nodo con solo hijo derecho no es negro ni tiene hijo rojo\n"); return(1);
}
if (x->color==RED)
{ //revisar
if (x->left->color!=BLACK && x->right->color!=BLACK)
{
printf("Rojo con dos hijos que no son negros, x=%d\n", x->clave); return(1);
}
}

if (x->left != NULL)
{
if (x->left->padre != x)
{
printf("Hijo izquierdo de x no apunta al padre, x=%d", x->clave);
return(1);
}
bhl=bh(x->left);
if (bhl==-1)
{ printf("Subárbol izquierdo no es coloreado\n");
return(1);
}
}

if (x->right != NULL)
{
if (x->right->padre != x)
{
printf("Hijo derecho de x no apunta al padre, x=%d", x->clave);
return(1);
}
bhr=bh(x->right);
if (bhr==-1)
{ printf("Subárbol derecho no es coloreado\n");
return(1);
}
}
if(bhl!=bhr)
{ printf("Arbol no es coloreado. l=%d r=%d\n",bhl,bhr);
return(1);
}
return(0); //si cumple propiedades.
}

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
12.6. Inserción.

La codificación resulta sencilla si se ha efectuado previamente el análisis detallado de los


diferentes casos, desarrollado en 12.3.

Se pasa por referencia el árbol.

pnodo insertar(pnodo *tree, pnodo nuevo)


{
pnodo y=NULL;
pnodo x=*tree;
if (nuevo==NULL) {Error(1,0); return (NULL);} //malloc no pudo crear nodo

//Busca posición para insertar.


while(x!=NULL)
{ y = x;
if (nuevo->clave < x->clave) x=x->left;
else if (nuevo->clave > x->clave) x=x->right;
else {Error(2, nuevo->clave); return(NULL);} //clave duplicada
}
//y apunta al padre del lugar de inserción.

nuevo->padre=y;
if (y == NULL) {*tree=nuevo; nuevo->color=BLACK; return(nuevo);}
else if (nuevo->clave < y->clave) y->left=nuevo;
else y->right=nuevo;

/*Código adicional para preservar las propiedades de árbol coloreado*/


x=nuevo;
while ( (x->padre!=NULL) && ( x->padre->color==RED) ) //doble rojo
{ if ( x->padre==x->padre->padre->left) //como la raíz es negra, existe el abuelo de x
//si x es descendiente izquierdo del abuelo.
{ y=x->padre->padre->right; // y apunta al tío

if ((y!=NULL)&&(y->color==RED)) // solo recoloración.


{ x->padre->color=BLACK;
y->color=BLACK; //pinta NEGRO al tío y al padre
x->padre->padre->color=RED; //el abuelo que era negro, cambia de color
x=x->padre->padre; //debe seguir revisando ascendentemente
}
else //Debe reestructurarse mediante rotaciones. Si y es nulo o y apunta a negro.
{
if (x==x->padre->right) { x= x->padre; lrot(tree,x);}
x->padre->color=BLACK;
x->padre->padre->color=RED;
rrot(tree, x->padre->padre);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 19
}

else //Código especular. Si x es descendiente derecho del abuelo.


{ y=x->padre->padre->left; // y apunta al tío
if ((y!=NULL)&& (y->color==RED)) // solo recoloración.
{ x->padre->color=BLACK;
y->color=BLACK; //pinta al tío
x->padre->padre->color=RED; //el abuelo que era negro, cambia de color
x=x->padre->padre; //debe seguir revisando
}
else //Debe reestructurarse mediante rotaciones
{
if (x==x->padre->left) { x= x->padre; rrot(tree,x);}
x->padre->color=BLACK;
x->padre->padre->color=RED;
lrot(tree, x->padre->padre);
}
}
}
(*tree)->color=BLACK; //pinta la raíz negra
return (nuevo);
}

12.7. Descarte.

Para marcar con x la raíz de un subárbol, que disminuyó su altura negra, se emplea un nodo
externo, de color negro, para almacenar un puntero al padre de éste, cuando el nodo que debe
ser descartado es negro y es una hoja. Se pasa como argumento, a la función que restaura las
propiedades de un árbol coloreado, en caso de descartar un nodo negro, si el nodo x es un
centinela (nodo externo); de esta forma antes de modificar el puntero x, se podrá escribir el
valor nulo en el padre de x.

/* Descarta nodo z, liberando el espacio*/


//z no debe ser NULL
void descarta(pnodo *rootp, pnodo z)
{ nodo externo; int centinela=0;
pnodo x, y;
if(z==NULL) Error(3,0);
if (z->left == NULL || z->right == NULL) y=z; //un hijo u hoja
else y=sucesor(z);

//Si hay hijo rojo, lo pega; si es hoja: y->right es nulo, y también x.


if (y->left != NULL) x=y->left; else x=y->right;

if(y->color==BLACK)
{ //y es negro.
if (x==NULL) {centinela=1; x=&externo; x->color=BLACK;}

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
//y es hoja; x es nodo externo.
x->padre = y->padre; //si y tiene hijo rojo, o si y es hoja
}

if (y->padre == NULL)
{ if(centinela) *rootp=NULL; else *rootp=x;}
else if (y==y->padre->left) y->padre->left = x;
else y->padre->right = x;

if (y!=z) { z->clave =y->clave; }

if (y->color == BLACK) delete_fix(rootp, x, centinela);

free(y);
}

/* Reestablece propiedades árbol coloreado luego de un descarte */


void delete_fix(pnodo *rootp, pnodo x, int esexterno)
{
pnodo w;

while (x!=*rootp && x->color==BLACK)


{
if (x==x->padre->left) //x es descendiente izquierdo
{
w=x->padre->right; //w es el hermano
if (w->color==RED)
{
w->color=BLACK;
x->padre->color=RED;
lrot(rootp, x->padre);
w=x->padre->right;
}
//ahora el hermano es negro.
if ( (w->left==NULL || w->left->color==BLACK)
//nodo externo o interno
&& (w->right==NULL || w->right->color==BLACK) )
{
w->color=RED;//ambos sobrinos negros
if(esexterno)
{x->padre->left=NULL; esexterno=0;}
x=x->padre; //cambia x. Asciende un nivel y sigue balanceando.
}
else
{ //uno o ambos sobrinos son rojos
if (w->right==NULL || w->right->color == BLACK)
//externo o interno

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 21
{ //sobrino derecho negro
w->left->color=BLACK;
w->color=RED;
rrot(rootp, w);
w=x->padre->right;
}
//ahora el sobrino derecho es rojo
w->color=x->padre->color;
x->padre->color = BLACK;
w->right->color = BLACK;
lrot(rootp, x->padre);
if(esexterno)
{x->padre->left=NULL; esexterno=0;}
x=*rootp; //lleva x a la raiz, se sale del lazo
}
}
else //código especular del if
{ //x es descendiente derecho.
w=x->padre->left;
if (w->color==RED)
{
w->color=BLACK;
x->padre->color=RED;
rrot(rootp, x->padre);
w=x->padre->left;
}

if ( (w->right==NULL || w->right->color==BLACK)
&& (w->left==NULL || w->left->color==BLACK))
{
w->color=RED;
if(esexterno)
{x->padre->right=NULL; esexterno=0;}
x=x->padre;
}
else
{
if (w->left==NULL || w->left->color == BLACK)
{
w->right->color=BLACK;
w->color=RED;
lrot(rootp, w);
w=x->padre->left;
}

w->color=x->padre->color;
x->padre->color = BLACK;

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
w->left->color = BLACK;
rrot(rootp, x->padre);
if(esexterno)
{x->padre->right=NULL; esexterno=0;}
x=*rootp;
}
}
}
//Al salir del while: x es la raíz o es rojo. Si es la raíz la pinta negra.
x->color=BLACK; //corrige caso con un solo hijo rojo
}

12.8. Test de las operaciones.

La inserción ascendente, genera el árbol coloreado, que se muestra en la Figura 12.2.

#define N 14
pnodo arbol=NULL;

int main(void)
{ int i;

printf("Insertando ascendente\n");
for(i=1;i<=N;i++)
{insertar(&arbol, CreaNodo(i));
printf("%d->", i);
//prtinorder(arbol,0); putchar('\n');
RevisaPropiedades(arbol);}

printf("\nDescartando\n");
for(i=1;i<=N;i++)
{descarta(&arbol, buscar(&arbol,i ));
printf("%d",i);
RevisaPropiedades(arbol);
}

printf("Insertando descendente\n");
for(i=N;i>0;i--)
{insertar(&arbol, CreaNodo(i));
printf("%d->",i);
//prtinorder(arbol,0); putchar('\n');
RevisaPropiedades(arbol);
}

printf("\nDescartando\n");
for(i=N;i>0;i--)
{descarta(&arbol, buscar(&arbol,i ));

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 23
printf("%d",i);
RevisaPropiedades(arbol);
}

return(0);
}

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
Referencias.

R. Bayer. “Symmetric binary B-trees: Data structure and maintenance algorithms.”


Acta Informatica 1:290-306, 1972.

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. “Introduction
to Algorithms”, Second Edition. MIT Press and McGraw-Hill, 2001.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles coloreados 25
Índice general.

CAPÍTULO 12. .......................................................................................................................................... 1


ÁRBOLES COLOREADOS. RED BLACK. ........................................................................................... 1
12.1. PROPIEDADES DE LOS ÁRBOLES COLOREADOS. ................................................................................ 1
12.2. COMPLEJIDAD EN ÁRBOLES COLOREADOS. ...................................................................................... 3
12.3. ANÁLISIS DE INSERCIÓN. ................................................................................................................. 4
12.3.1. Recoloración. Cuando el tío es rojo. ....................................................................................... 5
12.3.2. Rotaciones. Cuando el tío es negro. ........................................................................................ 6
12.4. ANÁLISIS DE LA OPERACIÓN DESCARTAR......................................................................................... 7
12.4.1. Dos descendientes. .................................................................................................................. 7
12.4.2. Un descendiente. ..................................................................................................................... 8
12.4.3. El nodo a descartar es una hoja. Sin descendientes. ............................................................... 9
12.4.4. Balance de alturas negras en descarte. Caso doble negro. ................................................... 10
a) Hermano rojo. ............................................................................................................................................ 10
b) Hermano negro. ......................................................................................................................................... 10
b1) Sobrinos negros. ................................................................................................................................. 10
b2) Sobrino derecho negro, sobrino izquierdo rojo. .................................................................................. 11
b3) Sobrino derecho rojo, sobrino izquierdo negro. .................................................................................. 11
12.5. ESTRUCTURA DE DATOS Y FUNCIONES BÁSICAS. ............................................................................ 12
12.5.1. Estructura de datos. ............................................................................................................. 12
12.5.2. Crea un nodo para inserción. ................................................................................................ 12
12.5.3. Sucesor. ................................................................................................................................. 13
12.5.4. Predecesor. ............................................................................................................................ 13
12.5.5. Rotación a la izquierda. ........................................................................................................ 14
12.5.6. Rotación a la izquierda. ........................................................................................................ 15
12.5.7. Comenta errores en inserción y descarte. ............................................................................. 15
12.5.8. Calcula altura negra. ............................................................................................................ 16
12.5.9. Revisa propiedades de árbol coloreado. ............................................................................... 16
12.6. INSERCIÓN. .................................................................................................................................... 18
12.7. DESCARTE. .................................................................................................................................... 19
12.8. TEST DE LAS OPERACIONES. ........................................................................................................... 22
REFERENCIAS. ........................................................................................................................................ 24
ÍNDICE GENERAL. ................................................................................................................................... 25
ÍNDICE DE FIGURAS................................................................................................................................. 26

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
Índice de figuras.

FIGURA 12.1 ÁRBOL DE BÚSQUEDA BINARIA COLOREADO. ...........................................................................1


FIGURA 12.1A ÁRBOL COLOREADO CON ROJOS HORIZONTALES. ...................................................................2
FIGURA 12.2 TRAYECTOS EN PEOR CASO. .....................................................................................................2
FIGURA 12.2A. COMPARACIÓN DE COMPLEJIDADES. RED-BLACK, AVL, BALANCEADO. .............................4
FIGURA 12.3 DOBLES ROJOS EN INSERCIÓN. .................................................................................................5
FIGURA 12.4 RECOLORACIÓN EN INSERCIÓN. TÍO ROJO. ...............................................................................6
FIGURA 12.5 RECOLORACIÓN CON TÍO NEGRO, NO QUEDAN DOS ROJOS ADYACENTES..................................6
FIGURA 12.6 ROTACIÓN DERECHA, PRESERVA ALTURA NEGRAS...................................................................6
FIGURA 12.7. ROTACIÓN IZQUIERDA, PAR PADRE-X. .....................................................................................7
FIGURA 12.8. NODO SELECCIONADO PARA SER DESCARTADO ES HOJA ROJA. ...............................................8
FIGURA 12.9. NODO SELECCIONADO PARA SER DESCARTADO ES NODO NEGRO CON HIJO ROJO. ....................8
FIGURA 12.10 DESCARTAR NODO NEGRO CON UN HIJO ROJO. .......................................................................8
FIGURA 12.10A DESCARTAR NODO NEGRO CON UN HIJO ROJO. .....................................................................8
FIGURA 12.11 DESCARTAR HOJA. .................................................................................................................9
FIGURA 12.12 DESCARTAR HOJA NEGRA CON HERMANO ROJO. ....................................................................9
FIGURA 12.13 DESCARTAR HOJA NEGRA CON PADRE Y HERMANO NEGRO. ...................................................9
FIGURA 12.14 HERMANO ROJO, PADRE NEGRO. ..........................................................................................10
FIGURA 12.15 HERMANO NEGRO Y SOBRINOS NEGROS. ..............................................................................11
FIGURA 12.16 HERMANO Y SOBRINO DERECHO NEGROS. ............................................................................11
FIGURA 12.17 HERMANO NEGRO, SOBRINO DERECHO ROJO. .......................................................................11
FIGURA 12.18 HERMANO NEGRO, SOBRINOS ROJOS. ...................................................................................12

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 13

Árboles Desplegados. Splay Trees.

13.1 Definición.

Es un árbol de búsqueda autoorganizado que emplea rotaciones para mover cualquier clave
accesada, ya sea en búsqueda, inserción o descarte, a la raíz.

Esto deja a los nodos más recientemente accesados cerca de la raíz, haciendo que la posterior
búsqueda de ellos sea eficiente.

No se requiere almacenar información adicional en el nodo, ya sea el factor de balance (AVL), o


el color en árboles coloreados, o un puntero adicional en árboles 2-3.

La forma del árbol va variando de acuerdo a los nodos que son más recientemente accesados.

Fueron desarrollados por Sleator y Tarjan in 1985, en la publicación del ACM Journal, “Self-
organizing Binary Search Trees” como una alternativa a los algoritmos que mantienen
balanceado un árbol binario de búsqueda.

La heurística es similar a la empleada en listas autoorganizadas, en las cuales los elementos


buscados se van colocando más cerca del inicio de la lista. Una opción conservadora, es
adelantar en una posición, el elemento buscado, cada vez que hay un acceso a esa clave; otra,
más enérgica, es llevar el elemento al inicio de la lista. Puede comprobarse que mover al frente
tiene un mejor comportamiento, en caso de distribuciones de búsqueda que cambian.

Si algo ha sido accesado, es muy probable que sea nuevamente accesado.

En el caso de los árboles splay se lleva el elemento buscado o insertado a la posición de la raíz.
En la búsqueda o la inserción bottom-up, se realiza un recorrido desde la raíz hasta encontrar el
elemento buscado; o bien hasta encontrar una hoja, en caso de inserción. Luego de lo anterior se
realiza una operación splay para mover el elemento a la posición de la raíz.

13.2 Operación splay.

La operación splay, consiste de una secuencia de dobles rotaciones, hasta que el nodo quede a
un nivel debajo de la raíz; en este caso basta una rotación simple para completar la operación.

En cada operación splay se hace ascender al nodo en uno o dos niveles, dependiendo de su
orientación relativa respecto de su nodo abuelo.

Profesor Leopoldo Silva Bijit 26-05-2008


232 Estructuras de Datos y Algoritmos
Hay tres casos:
Zig: el nodo es un hijo izquierdo o derecho (Zag) de la raíz. Sin abuelo.
Zig-Zag: El nodo es un hijo izquierdo de un hijo derecho; o un hijo derecho de un hijo izquierdo
(Zag-Zig).
Zig-zig: El nodo es un hijo izquierdo de un hijo izquierdo; o un hijo derecho de un hijo derecho
(Zag-Zag).

Gráficamente:
y x
Zig.
x C y
A
A B B C

Figura 13.1. Operación Zig.

Si t apunta al padre de x, la rotación simple, en este caso, se logra con: t=rrot(t);


Se rota el padre de x, a la derecha.
Pasar de la figura de la derecha hacia la de la izquierda se denomina Zag, y la operación que la
logra es: t=lrot(t).
Zig-Zig
z x

y A y
D
x z
C B
Figura 13.2. Operación Zig-Zig.
A B C D
Si inicialmente t apunta al abuelo de x. Se rota el abuelo de x, y luego el padre del nodo x.
Se logra con la secuencia :
t=rrot(t);
t=rrot(t);
Pasar de la figura derecha a la izquierda, el ascenso de z a la raíz se denomina Zag-zag.

Zig-Zag.

z x

y y z
D
x
A A B C D
B C

Figura 13.3. Operación Zig-Zag.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 3

Se rota el padre de x a la izquierda, y luego se rota el nuevo padre de x a la derecha. La imagen


especular se denomina Zag-Zig.

Mover a la raíz.

Debe notarse que mover un nodo hacia la raíz, siguiendo en forma inversa la trayectoria de
búsqueda desde a raíz hasta el nodo, no es enteramente equivalente a las dobles rotaciones
propuestas en árboles splay. Las operaciones Zig, Zag, Zig-Zag y Zag-Zig, son equivalentes a
las que produce el mover hacia la raíz; la diferencia está en las operaciones Zig-Zig y Zag-Zag.
En el caso de mover hacia la raíz, se rota el padre de x a la derecha y luego el nuevo padre de x,
a la derecha.
z x

y A z
D
x y D
C
B C
A B
Figura 13.4. Mover x hacia la raíz.

Medir el efecto de estos dos tipos de rotaciones requiere un análisis de costos denominado
“Análisis amortizado”. Se puede verificar que el costo amortizado de m operaciones splay sobre
un árbol con n nodos, es: O( (m + n) * log2(n + m) )
Es con este fundamento que se eligen las dobles rotaciones en este tipo de árboles, y como se
verá a través de ejemplos, tienden a acortar la altura del árbol.

13.3 Tipos de algoritmos.

Existen dos tipos de algoritmos, bottom-up (de abajo hacia arriba) o top-down (de arriba hacia
abajo).

13.3.1. Splay Bottom-up.

Las operaciones de búsqueda, inserción y descarte de un nodo se efectúan en forma similar a un


árbol binario de búsqueda. Luego se realiza una operación splay sobre un nodo.
En búsqueda el nodo es el que contiene el valor buscado, o el padre de la hoja si no lo
encuentra. En inserción, el nodo sobre el que se aplica la operación splay es el de igual valor al
buscado, si ya existía; o el nuevo nodo si éste no estaba en el árbol.

En bottom-up se requiere descender de la raíz hasta el nodo al que se le aplicará la operación


splay. Luego se van efectuado las rotaciones a medida que se asciende. Es decir se recorre el
árbol dos veces.
A partir del nodo, al que se le aplicará la operación, se asciende hasta encontrar el abuelo, y se
efectúa la rotación doble que corresponda; si no existe abuelo, pero sí padre, se efectúa
rotación simple.

Profesor Leopoldo Silva Bijit 26-05-2008


432 Estructuras de Datos y Algoritmos
13.3.2. Ejemplos de operaciones splay bottom-up.
Splay(3, root);

1 1 1 3

2 2 3 1 6

6 6 2 6 2 4

Zig-Zig
Zag-Zig
5 3 4 Zag 5

4 4 5

3 5

Figura 13.5. Operación Splay(3, root)

Splay(1, root);

7 7 7 1

6 6 6 6

Zig-Zig
5 5 1 4 7

4 4 Zag-Zag 4 2 5

3 1 2 5 3

2 Zig-Zig 2 3

1 3

Figura 13.6. Operación Splay(1, root)

Las operaciones tienden a disminuir la altura.


La figura siguiente, muestra la operación mover el nodo con valor 1, a la raíz. Lo que permite
comparar las formas de los árboles generados mediante las dos operaciones. Ver Figuras 13.6 y
13.7.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 5
MuevealaRaiz(1, root)

7 1

6 7

5 6
Mueve a la raíz
4 5

3 4

2 3

1 2

Figura 13.7. Operación Mover a la raíz.

Insertar nodo con valor 5.

7 7 7 Zig-Zag 5

3 8 3 8 3 8 3 7

1 4 1 5 1 4 6 8
1 4

2 6 2 4 6 2
2 6 Zag-Zig
Insert(5, root)
5
Splay(5, root)

Figura 13.8. Operación Insertar(5, root)

Para el diseño de descarte existen varias posibilidades.


a) Proceder como en árbol binario de búsqueda, y no emplear operaciones splay, considerando
que si algo se borra, no significa que se intentará buscar en la proximidad del elemento borrado.
b) Si lo busca y no lo encuentra efectúa una operación splay con el padre del buscado. Si lo
encuentra, efectúa operación splay sobre el nodo, dejándolo en la raíz. Luego efectúa una
operación splay con el nodo con mayor clave en el subárbol izquierdo; a continuación se
descarta la raíz; y finalmente se enlaza el subárbol derecho con el subárbol izquierdo.

Profesor Leopoldo Silva Bijit 26-05-2008


632 Estructuras de Datos y Algoritmos

La siguiente figura ilustra la alternativa b).

La operación descarte(4, root), ubica el nodo con valor 4, y lo lleva a la raíz. Luego se efectúa:
splay(3, TL), se descarta el nodo con valor 4, y se efectúa la unión de dos subárboles (join).
Descartar(4, root).
7 3
4 4

6 6
TL 2
2 6 TL 3 6
5 5 7
7 1
1 3 5
2 2 5 7

4 1
1

3
Figura 13.9. Operación Descartar(4, root)

Descartar(6, root)

3 6 6 5
TL

1 4 7 Zag 5 7 4 7
Zag-zag

2 6 3 5 8 4 8 3 8

5 7 1 3 1

8 2 1 2
LiberarRaíz
Splay(6, root) Splay(5, TL) join(L,R)
2

Figura 13.10. Operación Descartar(6, root)

Para tener un conjunto de operaciones que consideren las propiedades de esta estructura, se
pueden definir:

accesar(i, t): Si i está en el árbol t, retorna un puntero al nodo encontrado, en caso contrario
retorna NULL. Busca el nodo con valor i, y efectúa splay en ese nodo; si no lo encuentra,
efectúa la operación splay con el último nodo accesado buscando i.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 7

join (L, R): Retorna árbol formado por la combinación del árbol L y el árbol R, asumiendo que
todos los ítems de L tienen claves menores que cualquier item de R.
Para esto aplica splay en el nodo con mayor valor de L, luego agrega R como subárbol derecho
de la raíz.

split (i, t): Parte el árbol t en dos subárboles, L que contiene todos los items con claves menores
a i; y en R deja los nodos con claves mayores que i.
Realiza accesar(i, t) luego parte el árbol en la raíz.

inserte(i, t):
Realiza split(i, t), luego convierte i en la raíz de los dos árboles que retorna split.

descartar(i, t):
Realiza accesar(i, t) luego descarta la raíz y realiza la unión de los subárboles.

13.3.2. Splay top-down.

Se parte el árbol en dos subárboles, uno con claves menores al buscado y otro con claves
mayores al buscado, y a medida que se desciende se van efectuado las rotaciones. Cuando se
encuentra el nodo en la raíz del subárbol central, se unen los subárboles, dejando como raíz al
nodo.

Cada vez que se desciende desde un nodo x, por un enlace izquierdo, entonces x y su subárbol
derecho serán mayores que el nodo (que será insertado o que es buscado). De esta forma se
puede formar un subárbol, con x y su subárbol derecho, sea este subárbol R. El caso simétrico,
que se produce cuando se sigue un enlace derecho, permite identificar el subárbol izquierdo de
la nueva raíz, sea este subárbol denominado L.

Como se recorre sólo una vez, ocupa la mitad del tiempo que el bottom-up.

Se mantienen punteros a L y R, y punteros a los puntos de inserción de nuevos nodos en L y R;


éstos son el hijo derecho del máximo elemento de L; y el hijo izquierdo del mínimo elemento de
R. Estas variables evitan la necesidad de recorrer L y R; los nodos y subárboles que se
agreguen a L o R, no cambian sus posiciones en L o R.

A partir de la raíz se desciende hasta encontrar un posible nieto, se efectúa la operación


pasando el abuelo y el padre a los subárboles L y R; el nieto queda en la raíz del árbol central.
Si se encuentra el nodo se efectúa un join final.

Profesor Leopoldo Silva Bijit 26-05-2008


832 Estructuras de Datos y Algoritmos
Zig.

L X R L R L R
Y Y

Y X X
XR YL YL YR

YL YR YR XR XR

Figura 13.11. Top-down Zig

Se aplica operación splay al nodo con valor Y. Mediante rotación derecha Y llega a ser la raíz,
entonces el nodo X y su subárbol derecho (XR), se convierten en el hijo izquierdo del nodo con
menor valor en R. En este caso, Y pasa a ser la raíz del subárbol central.

Si t apunta a X, y si se tienen los punteros a punteros l y r, definidos según:


arbol *l=&L, *r=&R;
Se comienza a descender efectuando: p = t->left; entonces p apunta a Y. Si p no es nulo, y si el
valor sobre el que se realiza splay no es mayor ni es menor que la clave Y (ésta es la condición
para efectuar un Zig), entonces la siguiente secuencia, transforma el diagrama de la izquierda en
el de la derecha:

*r=t ; pega nodo X al subárbol R


r=&(t->left); mantiene puntero al menor descendiente de R.
t=t->left ; deja t apuntando a la nueva raíz (Y en el caso del ejemplo).

El siguiente macro realiza la operación Zig top-down:


#define rlink(t) (*r=(t), r=&((t)->left), (t)=(t)->left)
Zig-Zig

L R L R
X Z

Y Y
XR ZL ZR
Z X
YR

ZL ZR YR XR

Figura 13.12. Top-down Zig-Zig

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 9
Descendiendo buscando un nodo; cuando se llega a Z, se aplica Zig-Zig. Luego se extrae ZR,
que después de la operación Zig-Zig, es el hijo izquierdo del nodo Y, y se coloca como subárbol
derecho de Z; luego Y se liga como hijo izquierdo del menor valor en R.

Si t apunta a X, y p = t->lext, la condición p diferente de nulo y (p->valor) mayor que el valor


sobre el que se realiza la operación splay, se tiene la condición para la operación Zig-Zig. Se
logra la transformación, con la secuencia: t=trot(t); rlink(t);
Zig-Zag.

L R L
X R
Z

Y Y X
XR ZL ZR
Z
YL YL XR

ZL ZR

Figura 13.13. Top-down Zig-Zag

Descendiendo buscando un nodo; cuando se llega a Z, se aplica Zig-Zag. Luego se pega Y a L,


y X a R. Quedando Z en la raíz del árbol central.

Si t apunta a X, y p = t->lext, la condición p diferente de nulo y (p->valor) menor que el valor


sobre el que se realiza la operación splay, se tiene la condición para la operación Zig-Zag. Se
logra la transformación, con la secuencia: rlink(t); llink(t);
Con: #define llink(t) (*l=(t), l=&((t)->right), (t)=(t)->right)
Después de rlink(t), t apunta al nodo Y.
La descripción de llink es:
*l=t ; pega Y y su subárbol izquierdo a L
l=&(t->right) ; mantiene puntero al mayor descendiente de L.
t=t->right; deja t apuntando a la nueva raíz (Z en el caso del ejemplo).
Join.

L R X
X

L R
XL XR

XL XR
Figura 13.14. Top-down Join.

Profesor Leopoldo Silva Bijit 26-05-2008


1032 Estructuras de Datos y Algoritmos
Cuando el nodo X, sobre el que originalmente se deseaba efectuar la operación splay, llega a
estar en la raíz del subárbol central, se rearma el árbol, mediante la operación join. XL será el
hijo derecho del máximo elemento de L; y XR será el hijo izquierdo del mínimo valor de R.
Debe observarse que X es menor que los nodos en XR, y que éstos son menores que los que ya
pertenecen a R. También X es mayor que los nodos en XL, y éstos son mayores que los que
pertenecen a L.

Si L y R eran los punteros a las raíces de los subárboles izquierdo y derecho respectivamente, la
secuencia siguiente implementa la transformación de la Figura 13.14:
*l = t->left; *r = t->right;
t->left=L; t->right=R;
Ejemplo top-down.
Asumiendo que se busca E.
Se encuentra C, descendiendo dos nodos; se pasan A y B a R. (Zig-Zig).

L R L C R
A

B D B

E A
C

Figura 13.15. Top-down Zig-Zig en C.

Descendiendo dos niveles, se encuentra E, se deja en la raíz, con Zig-Zag. C se pega a L; D al


nuevo R.

L C L E R
R

D B C B

E A A

D
Figura 13.16. Top-down Zig-Zag en E.

Finalmente se efectúa el join.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 11

B
C
A
D

Figura 13.17. Top-down Join.

13.4. Animaciones.

http://www.ibr.cs.tu-bs.de/lehre/ss98/audii/applets/BST/SplayTree-Example.html
http://www.cs.technion.ac.il/~itai/ds2/framesplay/splay.html
http://webpages.ull.es/users/jriera/Docencia/AVL/AVL%20tree%20applet.htm

13.5. Códigos.

/* splay.h*/
/*
* 1985 D. D. Sleator R. E. Tarjan
*/
typedef int tipoclave;
typedef struct moldenode {
tipoclave clave; /* Clave */
struct moldenode *left, *right;
} nodo, *arbol;

/* Definiciones de macros */
#define max(A,B) ((A)>(B)?(A):(B))
#define search(valor,t) ((t)=splayBU((valor),(t),0), ((t)!=NULL&&(t)-
>clave==(valor))?(t):NULL)
//funciones definidas en splay.c Pueden invocarse si se incluye splay.h
extern arbol splayBU(tipoclave, arbol, int);
extern arbol splayTD(tipoclave, arbol);
extern arbol insertar(tipoclave, arbol);
extern arbol borrar(tipoclave, arbol);
extern int AlturaArbol(arbol);
extern int ContarNodos(arbol);
extern arbol BorraArbol(arbol);
/* end of splay.h */

/* splay.c */
/*
* Árbol binario autoorganizado.
*/
#include <stdlib.h>
#include <stdio.h>

Profesor Leopoldo Silva Bijit 26-05-2008


1232 Estructuras de Datos y Algoritmos
#include "splay.h"

//prototipos de funciones locales


arbol sucesor(arbol t) ;
static arbol join(arbol, arbol);
arbol descartar(tipoclave valor, arbol t);
static arbol lrot(arbol);
static arbol rrot(arbol);
static arbol CreaNodo(tipoclave);
static void LiberaNodo(arbol);
static void Error(int,tipoclave);
void ImprimeNodo(arbol t, int h); //test
void MuestraArbol(arbol t, int h);
arbol insertarrecursivo(tipoclave valor, arbol T);
arbol CreaArbol(arbol t, tipoclave a[]);

//Variables Globales y Definiciones.


static arbol NodoInsercion=NULL; /* Variable temporal, usada en insert, y por lo tanto en splay*/
static int flag; /* variable de estado */
/*
* Bottom up
*/
#define Root 0
#define Zag 1
#define Zig 2
#define NotFind 0
#define Find 1
#define Zig_Zig 2
#define Zig_Zag 3
#define Zag_Zag 4
#define Zag_Zig 5
arbol splay(tipoclave valor, arbol t, int fw)
{
if (t == NULL && NodoInsercion == NULL) {
flag=NotFind; /* árbol vacio o no lo encontró en búsqueda*/
return NULL;
}
else if (t == NULL && NodoInsercion != NULL) { /* encuentra posición para insertar */
t=NodoInsercion; /* Lo inserta */
NodoInsercion=NULL; /* Limpia variable global */
flag=Find; //comienza el ascenso y la operación splay.
return t;
}
else if (t->clave == valor) { /* Lo encuentra antes de llegar a una hoja */
flag=Find; //comienza operación splay. No marca global NodoInsercion (3).
return t; //retorna puntero al encontrado
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 13
else if (t->clave < valor) {
t->right=splay(valor,t->right, Zag); //desciende por la derecha
if (flag) { /* rotaciones sólo si estaba en el árbol */
if (flag==Zag_Zag) {
t=lrot(t);
t=lrot(t); //efectúa doble rotación LL
flag=Find; //resetea al ascender dos niveles.
}
else if (flag==Zag_Zig) {
t=lrot(t); //rota el abuelo a la izquierda. (2)
flag=Find; //resetea después de la doble rotación.
}
else if (fw==Zag)
flag=Zag_Zag; //se juntan dos seguidas ascendiendo por la derecha
else if (fw==Zig) { //está procesando Zig, y la anterior era Zag.
t=lrot(t); //rota el padre a la izquierda (1)
flag=Zig_Zag;
}
else /* (fw==Root) */
t=lrot(t); //efectúa Zag, un nivel bajo la raíz
}
}
else { /* (t->clave < valor) */
t->left=splay(valor,t->left,Zig); //desciende por la izquierda
if (flag) { /* rotaciones sólo si estaba en el árbol */
if (flag==Zig_Zig){
t=rrot(t);
t=rrot(t); //efectúa doble rotación RR
flag=Find; //resetea al ascender dos niveles.
}
else if (flag==Zig_Zag){
t=rrot(t); //rota el abuelo a la derecha (1)
flag=Find; //resetea al ascender dos niveles.
}
else if (fw==Zig)
flag=Zig_Zig; //se juntan dos seguidas ascendiendo por la izquierda
else if (fw==Zag) { //está procesando Zag, y la anterior era Zig.
t=rrot(t); //rota el padre a la derecha (2)
flag=Zag_Zig;
}
else /* (fw==Root) */
t=rrot(t); //efectúa Zig, un nivel bajo la raíz
}
}
return t;
}

Profesor Leopoldo Silva Bijit 26-05-2008


1432 Estructuras de Datos y Algoritmos
/*
* Top Down
*/
#define rlink(t) (*r=(t), r=&((t)->left), (t)=(t)->left)
#define llink(t) (*l=(t), l=&((t)->right), (t)=(t)->right)

arbol splayTD(tipoclave valor, arbol t)


{
arbol L=NULL, R=NULL; /* Subárboles */
arbol *l=&L, *r=&R; /* punteros para insertar en L y R*/
arbol p;
while (t != NULL && t->clave != valor) {
if( valor < t->clave) {
p = t->left; /*Desciende por la izquierda*/
if (p != NULL && valor < p->clave) { /* Zig_Zig */
printf("Zig-Zig en %d\n",t->clave);
t=rrot(t);
rlink(t);
}
else if (p != NULL && valor > p->clave) { /* Zig_Zag */
printf("Zig-Zag en %d\n",t->clave);
rlink(t);
llink(t);
}
else if(p != NULL && valor == p->clave) /* Zig */
{
printf("Lo encontró. Zig en %d\n",t->clave);
rlink(t);
}
else if(p==NULL && NodoInsercion !=NULL)
{
printf("Zig para insertar en %d\n",t->clave);
rlink(t); //no está y debe insertarlo.
}
else if ((p==NULL) && NodoInsercion==NULL)
{
printf("Sube %d. Splay con el padre del no encontrado\n",t-
>clave);
break; //no está el buscado. sube el padre del no encontrado a la raíz
} }
else { /* (valor > t->clave) */
p = t->right; /*Desciende por la derecha*/
if (p != NULL && valor > p->clave) { /* Zag_Zag */
printf("Zag-Zag en %d\n",t->clave);
t=lrot(t);
llink(t);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 15
else if (p != NULL && valor < p->clave) { /* Zag_Zig */
printf("Zag-Zig en %d\n",t->clave);
llink(t);
rlink(t);
}
else if(p!=NULL && valor == p->clave)/* Zag */
{
printf("Lo encontró. Zag en %d\n",t->clave);
llink(t);
}
else if(p==NULL && NodoInsercion !=NULL)
{
printf("Zag para insertar en %d\n",t->clave);
llink(t); //no está y debe insertarlo.
}
else if ((p==NULL) && NodoInsercion==NULL)
{
printf("Sube %d .Splay con el padre del no encontrado\n",t->clave);
break; //no está el buscado. sube el padre del no encontrado a la raíz
}
}
}
if (t==NULL && NodoInsercion == NULL) { /* si busca y árbol vacío */
return t;

}
if (t == NULL && NodoInsercion != NULL) { /* */
t=NodoInsercion; /* inserta y lo deja en la raíz */
NodoInsercion=NULL; /* reinicia global */
}

if(L!=NULL) {*l = t->left; t->left =L; } /*join final*/


if(R!=NULL) {*r = t->right; t->right=R;}
return t;
}

/*
* insertar(valor, t): inserta nodo con clave igual a valor en arbol t
*/
arbol insertar(tipoclave valor, arbol t)
{
arbol p;
NodoInsercion = CreaNodo(valor); /* Crea el nodo y pega en la global */

// p=splayBU(valor, t, Root); /* Si no lo encuentra, lo inserta y lo coloca en la raíz */


p=splayTD(valor, t); /* Si no lo encuentra, lo inserta y lo coloca en la raíz */
if (NodoInsercion != NULL) { /* Si ya estaba, libera el nodo */

Profesor Leopoldo Silva Bijit 26-05-2008


1632 Estructuras de Datos y Algoritmos
free(NodoInsercion);
NodoInsercion=NULL;
Error(1,valor); // Avisa error de inserción.
}
return p;
}
arbol buscar(tipoclave valor, arbol t)
{
arbol p;
NodoInsercion = NULL; /* */
//p=splayBU(valor, t, Root); /* si lo encuentra, lo coloca en la raíz. */
p=splayTD(valor, t); /* si lo encuentra, lo coloca en la raíz. */
if(p==NULL) Error(2,valor); // Busca en árbol vacío.
return p;
}

arbol sucesor(arbol t) /* Algoritmo iterativo */


/*menor descendiente de subárbol derecho */
{ arbol p;
if(t!=NULL) p = t->right; else return(NULL);
if(p==NULL) return(NULL);
if (p->left == NULL) /* No hay hijo izq. */
return (p); /* Retorna el menor */
while ( p->left != NULL) { /* Mientras no tenga hijo izq descender por la izq */
t = p;
p = p->left;
}
/*Al terminar el while p apunta al menor descendiente */
return (p); /* Retorna el menor */
}
arbol borrar(tipoclave valor, arbol t)
{
arbol p,q,r;
NodoInsercion = NULL; /* */
p=buscar(valor, t); /* si lo encuentra, lo coloca en la raíz. */
//MuestraArbol(p, 1);
if (p==NULL) return(NULL);
r=sucesor(p);
if(r!=NULL)
{q=splayTD(r->clave, p->right);
t=join(p->left,q);
}
else t=p->left;
LiberaNodo(p);
return(t);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 17
13.6. Operaciones utilitarias.

Se agrega descartar para completar las operaciones básicas.

/*
* descartar(valor, t): borra nodo con clave == valor en árbol t
* No se implementa mediante splay. Lo que se borra no se volverá a emplear.
*/
arbol descartar(tipoclave valor, arbol t)
{
arbol *p = &t;
arbol temp;
while (*p != NULL && (*p)->clave != valor) {//descenso iterativo
if((*p)->clave < valor)
p = &((*p)->right);
else
p = &((*p)->left);
}
if (*p != NULL) { /* (*p)->clave == valor. Encontró el nodo para descartar */
temp = *p;
/* Uno o dos hijos? */
if ((*p)->left == NULL) *p = (*p)->right;
else if ((*p)->right == NULL)*p = (*p)->left;
else /* si tiene dos hijos */
*p =join((*p)->left,(*p)->right);
LiberaNodo(temp);
}
else /* No lo encontró */
Error(0,valor);
return(t);
}

//join (l, r): Retorna el árbol formado por la combinación del árbol "l", y del árbol "r".
//Se asume que cualquier item en "l" tiene valores menores que cualquier item en "r".
static arbol join(arbol l, arbol r)
{
arbol t;
arbol *p = &t;
while (l != NULL && r != NULL) {
*p = l;
l = l->right;
(*p)->right = r;
p = &(r->left);
r = r->left;
}
if (l == NULL) *p = r;

Profesor Leopoldo Silva Bijit 26-05-2008


1832 Estructuras de Datos y Algoritmos
else /* (r == NULL) */
*p = l;
return t;
}

static arbol lrot(arbol t)


{ arbol temp = t->right;
t->right = temp->left;
temp->left = t;
return ( temp);
}
static arbol rrot(arbol t)
{ arbol temp = t->left;
t->left = temp->right;
temp->right = t;
return (temp);
}
static void Error(int error, tipoclave valor)
{
if(error==1) printf("Error: Intenta insertar clave=%d existente!\n",valor);
else if(error==0) printf("Error: Intenta descartar clave=%d inexistente!\n", valor);
else if(error==2) printf("Error: Busca clave=%d en árbol vacío!\n", valor);
}
static nodo* CreaNodo(tipoclave valor)
{
arbol p;
p=(arbol)calloc(1, sizeof(nodo));
//p->nombre=(char*) NULL;
p->clave = valor;
p->left = NULL;
p->right = NULL;
return p;
}

static void LiberaNodo(arbol p)


{
//if (p->nombre != (char *)NULL) free(p->nombre);//libera string
free(p);
}

int AlturaArbol(arbol t)
{
if (t == NULL) return 0;
else return 1+max(AlturaArbol(t->left),AlturaArbol(t->right));
}
int ContarNodos(arbol t)
{

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 19
if (t == NULL) return 0;
else return 1+ContarNodos(t->left)+ContarNodos(t->right);
}
arbol BorraArbol(arbol t)
{
if (t != NULL) {
t->left=BorraArbol(t->left);
t->right=BorraArbol(t->right);
LiberaNodo(t);
}
return NULL;
}
/* end of splay.c */

13.7. Funciones para efectuar test de splay.

void ImprimeNodo(arbol t, int h)


{ int i;
for(i=0; i<h; i++) putchar('\t');
if(t==NULL) { putchar('*') ; putchar('\n') ;}
else printf("%d\n",t->clave);
}

void MuestraArbol(arbol t, int h)


{
if(t==NULL) ImprimeNodo(t, h);
else {MuestraArbol(t->right, h+1); ImprimeNodo(t, h); MuestraArbol(t->left, h+1);}
}

arbol insertarrecursivo(tipoclave valor, arbol T) /* recursivo */


{
if (T == NULL)
{ T = (arbol) malloc(sizeof(nodo));
if (T == NULL) printf("Rebalse del heap!\n");
else {T->clave = valor; T->left = T->right = NULL;}
}
else
if (valor < T->clave) T->left = insertarrecursivo(valor,T->left);
else if (valor > T->clave) T->right = insertarrecursivo(valor,T->right);
else Error(1,valor);
return(T);
}

#define maxnodos 2

arbol CreaArbol(arbol t, tipoclave a[])


{ int i;

Profesor Leopoldo Silva Bijit 26-05-2008


2032 Estructuras de Datos y Algoritmos
for(i=0;i<maxnodos;i++)
if(insertarrecursivo(a[i],t)==NULL) printf("error en inserción\n");
return t;
}

/*Probar con:
*
* 4 4 4 4 4 4
* / \ / \ / \ / \ / \
* 2 6 2 6 2 6 2 6 2 6
* / \ / \
* 1 3 5 7
*/

//Variables para mantener el árbol.


arbol root=NULL;
tipoclave arr[maxnodos]={2,6}; //orden de ingreso de claves

void main(void)
{
root=insertarrecursivo(4, root); //crea raíz
CreaArbol(root,arr); //agrega nodos
MuestraArbol(root, 1);
//comenzar test

//…………..
root=delarbol(root); //despedida limpia
}

13.8. Análisis de complejidad.

13.8.1 Objetivo del análisis amortizado.

La complejidad temporal de mantener un splay tree se suele analizar empleando Análisis de


amortizaciones.

La metodología está basada en la siguiente situación análoga: Una deuda bancaria genera
intereses todos los meses; la deuda puede pagarse mediante amortizaciones mensuales. Si la
amortización es mayor que los intereses la deuda disminuye; en caso contrario aumenta. Si se
decide aumentar la amortización en algunos períodos, en otros se la podrá disminuir.

En el caso de algoritmos, se puede “pagar más” (pre pagar) por una operación, los créditos
generados compensarán, más adelante operaciones que sean más costosas; lo que no se puede
hacer es ir aumentando la deuda.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 21
El análisis amortizado se emplea para demostrar que el costo promedio de una operación es
pequeño, si se promedia sobre una serie de operaciones. A pesar de que una de las operaciones
de la serie pueda ser muy costosa.

Se busca encontrar el costo promedio de una operación en el peor caso.

El análisis de amortizaciones significa analizar costos promedios para una secuencia de


operaciones. Este acercamiento es razonable, ya que los análisis de complejidad temporal
basados en la operación de peor caso conducen a cotas pesimistas si la única forma de aceptar
una operación costosa es tener previamente un gran número de operaciones de bajo costo.
Tampoco es un análisis de un caso promedio, ya que en éstos se asumen que las entradas vienen
en determinados órdenes; lo que se efectuará es un promedio, en el tiempo, para una secuencia
de operaciones, sin asumir órdenes para los datos de entrada.

El método consiste en asignar un costo artificial para cada operación de la secuencia, este costo
artificial es denominado costo amortizado de una operación. La propiedad clave para asignar un
costo amortizado es que los costos reales de la secuencia de operaciones queden acotados por el
costo total amortizado de todas las operaciones. Entonces para analizar un algoritmo se pueden
emplear los costos amortizados, en lugar de los reales. La ventaja es que existe cierta
flexibilidad en la asignación de los costos amortizados.

13.8.2. Tipos de análisis.

Se tienen tres estilos de análisis.

Agregación:
Se demuestra que todas las secuencias de m operaciones tienen complejidad temporal O(g(n)),
entonces cada operación tendrá costo amortizado: O( g(n)/m) . Su dificultad para aplicarlo es
que normalmente el costo real de las operaciones depende fuertemente de los datos.

Balance: Se asocian créditos con los items en la estructura de datos.


A veces es difícil entender que son los créditos. Por ejemplo si las rotaciones generan créditos:
algunos nodos rotan más que otros, además los nodos pueden moverse en forma no predecible.
Se denomina también método del banquero.

Potencial: Se asocian créditos con la estructura completa de los datos.


Su principal dificultad es escoger una función potencial. Su elección no siempre es obvia, y
podría ignorar detalles de la estructura. A menudo puede intentarse elegirla, por ensayo y error;
a veces por intuición.

13.8.3. Analogía para la función potencial.

Primero consideremos una deuda bancaria, de valor D0, que se va pagando mediante
amortizaciones periódicas. Sea j el período; i la tasa de interés en el período j; a la
amortización; Dj, el valor de la deuda al finalizar el período j.

Profesor Leopoldo Silva Bijit 26-05-2008


2232 Estructuras de Datos y Algoritmos
Entonces, al finalizar el período j, la deuda incurre en intereses, pero la deuda total se ve
disminuida en la amortización, se tiene entonces la relación:
D j 1 (1 i) a Dj
La deuda debe ir disminuyendo, hasta extinguirse.

D
Dj-1
D0 Dj

j
t
j

Figura 13.18. Amortizaciones.

Si denominamos c j D j 1i al costo real de los intereses, y cˆ j a la amortización realizada en el


período j, tendremos:
cˆi ci Dj 1 Dj
Si definimos: D j j D0
Se tendrá, haciendo j=0, que: 0 = 0, y la relación puede escribirse, en términos de , que suele
denominarse función potencial, del siguiente modo:
cˆi ci j j 1
Que es la relación que suele emplearse en el análisis de amortizaciones.

13.8.4. Ejemplo de análisis amortizado.

Incremento de un contador binario.

Se desea almacenar un gran contador binario en un arreglo A.


Todas las celdas comienzan en 0. La operación que se desea analizar es la de contar.

El algoritmo empleado es conmutar el bit A[0], si éste cambia de 1 a 0, se conmuta A[1]; y se


continua conmutando hasta que un bit cambia de 0 a 1. La tabla siguiente ilustra los cambios de
las componentes del arreglo, a medida que se cuenta en binario.

El costo de un incremento en la cuenta es el número de bits que cambian, esto se anota en la


última columna, y corresponde al costo real de pasar de una cuenta a la siguiente.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 23
A[m] A[m-1] …. A[3] A[2] A[1] A[0] Costo
0 0 0 0 0 0
0 0 0 0 0 1 1
0 0 0 0 1 0 2
0 0 0 0 1 1 1
0 0 0 1 0 0 3
0 0 0 1 0 1 1
0 0 0 1 1 0 2
0 0 0 1 1 1 1
0 0 1 0 0 0 4

Nótese que en ocasiones cuesta poco el incremento y en otras cuesta más.

El número de bits que cambian cuando el incremento produce un número n es a lo más


1+floor(log n). Este es justamente el número de bits que se requieren para representar n en
binario. Por ejemplo, si n es 8, puede expresarse con 1+ParteEntera(log2(8)) = 4 bits.

Entonces, en una secuencia de n incrementos, el costo de peor caso por incremento está acotado
por n(1+floor(log n)) = O(n log n).

Calculemos costos amortizados, por los diferentes métodos.


Método de agregación.
Se observa que A[0] cambia en cada paso. Que A[1] cambia cada dos incrementos, que A[2]
cambia cada 4 incrementos, y así sucesivamente. Entonces si el costo total para conmutar A[0]
es n, el de conmutar A[1] será floor(n/2), y el costo total para conmutar A[2] será floor(n/4), etc.

El costo total será:


costo total = n + floor(n/2) + floor(n/4) + ...
costo total <= n + n/2 + n/4 + n/8 + ... <= 2n
Ya que la serie: 1+ ½ +1/4 + 1/8 +… tiende a 2.

Si el costo total de la secuencia de n operaciones de incremento es 2n, entonces esto implica que
el costo amortizado de un incremento es 2.

Método del banquero.


Por cada bit que es 1, en el contador, mantendremos un peso ($) en ese bit. Si la cuenta decimal
equivalente es 9 (1001 en binario), tendremos que tener un peso en el bit 0 y un peso en el bit 3.
Por otro lado, cada vez que se conmuta un bit, debemos pagar un peso para efectuarlo. Este es el
costo real.

Si pagamos dos pesos por la operación de incremento (este es el costo amortizado) veamos
cuánto cuesta realmente efectuar el incremento: En general se tiene una serie de bits menos
significativos que cambian de 1 a 0, y finalmente un bit cambia de 0 a 1, terminando el proceso.
Para los bits que cambian de 1 a 0, teníamos un peso asociado a ese bit, justamente para pagar

Profesor Leopoldo Silva Bijit 26-05-2008


2432 Estructuras de Datos y Algoritmos
su bajada; y para el bit que cambia de 0 a 1, con los dos pesos de la amortización, pagamos su
cambio a 1, y a la vez dejamos el otro peso, asociado a ese bit, para uso futuro (pagamos por
adelantado, es decir estamos amortizando).

Si se asignan dos pesos por la operación de incremento (costo amortizado) esto garantiza que
siempre tendremos suficiente dinero para pagar por la operación, no importando cuánto cueste,
en forma real efectuar la operación de incremento.
Esto prueba que el costo amortizado de la operación incremento es dos.
Método del potencial.
En lugar de almacenar el dinero asociado a los elementos de la estructura, se puede
conceptualizar que el dinero total está asociado a la estructura completa de datos. El dinero total
almacenado, en el banco, se denomina función potencial.

Si escogemos:
(contador) = Número de unos en el contador.

Usando este formalismo, se define el costo amortizado de una operación. Si el sistema cambia
de estado D a D’ como resultado de una operación, se define:
Costo amortizado = costo real + = costo real + (D’)- (D)

Es decir, el costo de la amortización debe ser el dinero que mantendremos en el banco y el


necesario para pagar por el trabajo.

Cálculo de costos amortizados:


Cuando los últimos m bits son unos, se tendrá: (D) = m.
El costo real para obtener la nueva cuenta será: m+1, ya que m+1 bits deben ser conmutados.
La nueva cuenta estará formada por un solo 1, en la posición m+1, entonces: (D’) = 1.
Se obtiene entonces:
Costo amortizado = (m + 1) +1 –m = 2

Si el potencial es siempre positivo, y comienza desde cero, como en el ejemplo, se tendrá, que
la suma de costos amortizados es una cota para la suma de los costos reales:
m m
cˆi (ci )
i 1 i 1

Puede comprobarse, que para cualquier operación incremento, el costo amortizado será dos.

Estudiaremos ahora cotas para la función potencial:


Para el caso inicial, de cuenta cero, tendremos: (inicial) = 0.
Si el contador es de 2 bits, después de 3 (22-1) operaciones, tendremos: (final) = 2.
Si el contador es de 3 bits, después de 7 (23-1) operaciones, tendremos: (final) = 3.
Si el contador es de n bits, después de (2n-1) operaciones, tendremos: (final) = n.

Si m es el número de operaciones: m= (2n-1), entonces: n = log2(m+1)

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 25

En la fórmula general, reemplazamos la suma de los costos amortizados por 2*m (ya que la
amortización es 2 por cada operación, y tenemos m operaciones); y la diferencia de potencial
por (0 –n) ; y a su vez expresamos n en función de m, se obtiene:
m m
ci (cˆi ) ( Dinicial ) ( D final ) = 2*m + (0 – n) = 2*m- log2(m+1)
i 1 i 1

m
Costo real de m operaciones = ci = 2*m- log2(m+1) = O(m)
i 1
Es O(m) ya que m es mucho mayor que log(m) para valores grandes de m.
Esto comprueba que el costo amortizado de una operación es O(m/m) = O(1), es decir,
constante.

13.8.5. Definiciones.

Para aplicar el método del potencial, al análisis de algoritmos, efectuemos algunas definiciones.

Consideremos una secuencia de m operaciones op1 , op2 ,… , opm.


Asumiremos que la estructura de datos tiene una función potencial. La función potencial puede
pensarse como el pago de una cuenta bancaria, como se mencionó antes.

Di = Estado de la estructura de datos después de la operación i-ésima


(Di) =Potencial (i.e., créditos) de la estructura completa Di.
ci = Costo actual de operación i-ésima, lo que corresponde a los intereses. Cada operación opi ,
tiene un costo proporcional a su tiempo de ejecución.
Se pagan los costos ci , de las operaciones opi mediante amortizaciones cˆi , entonces:

cˆi ci ( Di ) ( Di 1 ) = Costo amortizado de operación i-ésima

La diferencia entre el costo real y las amortizaciones se carga al potencial de la estructura. Se


aumenta (invierte o prepaga) el potencial si los costos amortizados son mayores que los costos
actuales, en caso contrario el potencial disminuye.
Si la diferencia de potencial ( Di ) ( Di 1 ) es positiva el costo amortizado representa un
sobrepago de la operación i-ésima, ya que el potencial aumenta.

Como se estableció antes, lo que interesa es el costo promedio en una secuencia de operaciones,
entonces para m operaciones:
m m
cˆi (ci ( Di ) ( Di 1 ))
i 1 i 1

Pero la diferencia de potencial es serie telescópica, y se puede escribir:


m m
cˆi (ci ) ( Dm ) ( D0 )
i 1 i 1

Profesor Leopoldo Silva Bijit 26-05-2008


2632 Estructuras de Datos y Algoritmos

La cual permite establecer que:


m m
cˆi (ci ) si ( Dm ) ( D0 ) entonces el costo amortizado total será una cota superior del
i 1 i 1
costo real.

Si se logra tener una función potencial que cumpla con ( D0 ) 0 (potencial de referencia) y
como el número de operaciones que se realizarán no suele conocerse por adelantado, entonces
lo que basta demostrar es que ( Di ) 0 para todo i; de esta forma se asegura que el costo
m m
amortizado será una cota para el costo real. Es decir: cˆi (ci ) con ( Dm ) 0 para todos
i 1 i 1
los valores de m.

Entonces: Se paga por la secuencia de operaciones op1 , op2 ,… , opm no más que la disminución
n
de potencial más la suma de los costos de las amortizaciones cˆi .
i 1
El costo real se calcula sumando al costo amortizado la diferencia de potencial:
m m
ci (cˆi ) ( D0 ) ( Dm )
i 1 i 1

13.8.6. Pasos en la aplicación del método del potencial.

En un caso general, el potencial puede ser negativo y no comenzar de cero. Lo que debe
observarse son los potenciales inicial y final.

En el método del potencial, se suelen realizar tres pasos:

Primero escoger una función potencial adecuada (esto es casi arte). Segundo, empleando la
función potencial acotar el costo amortizado de la secuencia de operaciones en que se está
interesado. Y tercero, es preciso acotar ( Dinicial ) ( D final ) , para obtener una cota para los
costos reales.

Puede notarse que si el costo actual de una operación es elevado, y si se desea pagar una
amortización baja, entonces el potencial debe disminuir bastante para efectuar la cancelación.

Para el caso de los árboles splay, el análisis está basado en definir una función potencial, y
demostrar que cada operación splay tiene costo amortizado O( log(n) ), si esto es así, se puede
concluir que la secuencia completa tiene costo O (m log (n) ) .

En árboles balanceados, se tiene O(log n) por operación, y O(m log n) para m operaciones.
En árboles splay, como se verá, se podría tener O(n) para alguna operación en peor caso, y O(m
log n) como costo promedio para la secuencia de m operaciones.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 27
Puede definirse, en general, que si todas las secuencias de m operaciones tienen costo O(m f(n)
), entonces cada operación tiene costo amortizado O( f(n)) .

13.8.7. Función potencial en árboles splay.

Algunas consideraciones para encontrar una función potencial.


En árboles splay se rotan todos los nodos visitados en un acceso, esto se logra con un costo
constante adicional asociado a la rotación. Pero las rotaciones tienden a balancear el árbol, y
además los nodos accesados son movidos a la raíz.

Sin embargo las reglas heurísticas asociadas a las rotaciones no tienen beneficios obvios. Las
reglas para las rotaciones en árboles splay se obtuvieron ensayando diversas heurísticas, hasta
encontrar un conjunto que redujera los costos amortizados.

Para un árbol T, se puede definir la función tamaño s(i) como la suma de pesos positivos
arbitrarios w(i), asignados a cada nodo, de todos los nodos del árbol Ti, que tienen al i como
raíz:
s(i ) w(i )
i Ti

Por ejemplo puede definirse w(i) como q(i), el número de accesos al nodo i, partido por el
número de operaciones splay, realizadas sobre la estructura.
n
Sea w(i) = q(i)/m. Nótese que en este caso: q(i) m
i 1
Esta elección de los pesos permite obtener cotas más adecuadas en la ejecución de casos reales.

Si cada nodo es accesado sólo una vez: w(i) sería el número de descendientes del nodo i, con
éste incluido, partido por el número de nodos en el árbol.

La elección de los pesos para cada nodo nos permitirá acotar los costos reales de una secuencia
de operaciones.
Sea además la función rango r(i) = floor(log2S(i)). Lo cual permite rangos enteros.

Se define el potencial según:

(T ) floor (log 2 s(i)) r (i)


i T i T
La función floor(x) da el mayor entero menor o igual a su argumento. Es decir, la parte entera.
Se obtiene truncando el número real.

Profesor Leopoldo Silva Bijit 26-05-2008


2832 Estructuras de Datos y Algoritmos

Ejemplo función potencial.


Efectuaremos una asignación simple de pesos, todos con valor 1. En este caso s(i) es la suma de
nodos pertenecientes al árbol cuya raíz es i. Calcularemos el potencial de la estructura en dos
situaciones. La primera un árbol bastante desbalanceado, al cual mediante una operación splay,
se lleva el nodo con valor 4 a la raíz.

7-2 7 7-2
4

6-2 6

3-1 2 6 3-1
5-2 5

1 3 5 7
4-2 2
1-0 1-0 1-0 1-0
4 2-1
1-0 1

s(i)-r(i)
3
Figura 13.19. 1-0 Tamaños y rangos de los nodos.

En las figuras se han anotado los valores de s(i) y r(i) para cada nodo.
Notar que s(4)=2 y r(4)=1 en la figura izquierda; y que s(4)=7 y r(4) =2 en la de la derecha. Los
nodos más alejados de la raíz tienen rangos menores; a medida que ascienden, aumentan su
rango; y el rango de las hojas es 0.

Para la figura a la izquierda, la estructura inicial, el potencial es:


(T D0 ) log(1) log(1) log(2) log(4) log(5) log(6) log(7) 9
Para la figura a la derecha, después de las dos rotaciones, el potencial es:
(T D2 ) log(1) log(1) log(1) log(1) log(3) log(3) log(7) 4
Para llegar desde la figura a la izquierda a la de la derecha, se requiere efectuar una operación
Zig-Zag, y una operación Zig-Zig. Ambas operaciones tienen costo real de 2 rotaciones, cada
una.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 29
Puede verse que la elección de esta función potencial (con todos los pesos iguales a 1) y
definiendo el rango como logaritmo produce un potencial cercano a n para árboles balanceados,
y nlog(n) para árboles muy desbalanceados.

Si consideramos los r(i) como pesos asociados al nodo, en el caso del método del banquero,
puede apreciarse que se requiere invertir poco en nodos muy alejados de la raíz. Ya que las
operaciones los llevaran a rangos mayores. Otra forma de verlo, es que debe tenerse mucha
plata en el banco si el árbol, está muy desbalanceado.

Se dispone de (9-4)=5 pesos en la estructura para un costo real de 4 rotaciones. Si se efectúa una
amortización, ésta aumentará el potencial de la estructura.

La ventaja de esta definición, es que en general, a lo más tres nodos cambian su función s (y por
lo tanto r): El abuelo, el padre y el nieto. Estos cambios pueden calcularse, como se verá a
continuación.

13.8.8 Cálculo de costos amortizados por operación.

Antes de la operación, se tienen, para el nodo x: r(x), s(x)


Después de la operación, para el nodo x, se tienen: r’(x), s’(x)

Se desea demostrar que los costos amortizados para cada operación individual quedan acotados
según:
Si x es la raíz: ĉi = 1
Zig: ĉi 3(r’(x) - r(x)) + 1
Zig-zig: ĉi 3(r’(x) - r(x))
Zig-zag: ĉi 3(r’(x) - r(x))

Se calculan las cotas para los costos amortizados por operación, contando cada rotación como
un costo real O(1) y calculado la diferencia de potencial, antes y después de la operación.
Caso: Nodo x es la raíz
No cambia la función potencial, los nodos permanecen con sus mismos rangos individuales. El
costo de encontrar al nodo en la raíz es constante, y se le da valor 1.
Entonces:
cˆi ci ( Di ) ( Di 1 ) = 1 + 0 = 1
Caso: Zig
y x
x C y
A
A B B C
Figura 13.20. Cambios de tamaños y rangos en Zig.

Profesor Leopoldo Silva Bijit 26-05-2008


3032 Estructuras de Datos y Algoritmos

El costo real es una rotación.


cˆi ci ( Di ) ( Di 1 ) = 1 +( r'(x) + r'(y) ) - (r(y) + r(x)) sólo x e y cambian su rango.
Se tiene: s(y) > s’(y) y s’(x)> s(x) y mediante logaritmos se obtienen: r(y)> r’(y), y
r’(x)>r(x).
Entonces se puede acotar la amortización según:
cˆi <= 1 + r'(x) - r(x) ya que r’(y)-r(y) < 0
Entonces con mayor razón, ya que r’(x)-r(x)>0:
cˆi <= 1+ 3( r'(x) - r(x) )
Caso: Zig-Zag

z x

y y z
D
x
A A B C D
B C
Figura 13.21. Cambios de tamaños y rangos en Zig-Zag.

Se tienen: s’(x) = s(z), s(y)>=s(x) , s’(x)>=s’(y)+s’(z)


La primera implica: r’(x)-r(z)=0
La segunda: r(y)>=r(x)
Para la tercera, se emplea la relación:
c a b, a 0, b 0 log(a) log(b) 2log(c) 2 Los logaritmos son en base dos.
La que se demuestra, según:
0 ( a b) 2 a 2 2ab b 2 Un cuadrado siempre es positivo
4ab a2 2ab b 2 (a b) 2 c 2 Sumando 4ab en ambos lados, se obtiene
c2
ab
4
log(a) log(b) 2log(c) 2

Entonces a partir de s’(x)>=s’(y)+s’(z), se obtiene:


r’(y)+r’(z) <=r’(x)-2 la que implica: 2 <= 2r'(x) - r'(y) - r'(z)

Calculando ahora el costo amortizado de Zig-Zag:


cˆi ci ( Di ) ( Di 1 ) . Se requieren dos rotaciones, entonces el costo real es 2.
cˆi = 2 + ( r’(x)+r’(y)+r’(z) ) - ( r(x)+r(y)+r(z) ) ordenando, se obtiene:
cˆi = 2 + r’(x)-r(z) –(r(x)+r(y)) + r’(y) + r’(z)
Acotando la amortización, reemplazando r'(x) = r(z) y r(y) = r(x)+a, con a>0, se obtiene:
cˆi <= 2 - 2r(x) + r'(y) + r'(z) , reemplazando ahora el primer 2, por algo mayor, se tiene:

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 31
cˆi <= (2r'(x) - r'(y) - r'(z)) + r'(y) + r'(z) - 2r(x), simplificando, se obtiene:
cˆi <=2r’(x)-2r(x) y con mayor razón, ya que 3 es mayor que 2, y r’(x)>=r(x):
cˆi <=3 ( r'(x) - r(x) )
Caso: Zig-Zig.

z x

y A y
D
x z
C B

A B C D

Figura 13.22. Cambios de tamaños y rangos en Zig-Zig.

Se tienen;
s'(x) = s(z) , s'(x) >= s'(y), s(y) >= s(x), s(x) + s'(z) <= s'(x)
La última refleja que todos los nodos bajo x, luego de la rotación, deben ser al menos la suma de
todos los nodos del árbol.

Calculando ahora el costo amortizado de Zig-Zig, que también tiene costo real de dos
rotaciones:
cˆi ci ( Di ) ( Di 1 )
cˆi = 2 + ( r’(x)+r’(y)+r’(z) ) - ( r(x)+r(y)+r(z) )
cˆi = 2 + ( r’(y)+r’(z) ) - ( r(x)+r(y) ) usando en la anterior que r'(x) = r(z)
Buscando acotar la amortización, se tiene:
cˆi <=2 + r'(x) + r'(z) - 2r(x) usando r'(x) >= r'(y) y r(y) >= r(x)
Usando log(a) log(b) 2log(c) 2 , con s(x) + s'(z) <= s'(x) se tiene:
r(x)+r’(z)<=2r’(x) -2 la que puede escribirse:
2 <= 2r'(x) - r(x) - r'(z) , lo cual puede reemplazarse en el costo amortizado por el primer 2.
Se obtiene:
cˆi <= (2r'(x) - r(x) - r'(z) ) + r'(x) + r'(z) - 2r(x) y arreglando, finalmente.
cˆi <= ( 3r'(x) - 3r(x) )
Ejemplo
Veamos un cálculo amortizado para splay(3, root)

Profesor Leopoldo Silva Bijit 26-05-2008


3232 Estructuras de Datos y Algoritmos

1 1 1 3

2 2 3 1 6

6 6 2 6 2 4

Zig-Zig
Zag-Zig
5 3 4 Zag 5

4 4 5

3 5

Figura 13.23. Cálculos amortizados.

Sea x el nodo con valor 3, entonces:


La operación Zig-Zig tiene costo amortizado menor que: 3(r’(x) - r(x) )
La operación Zag-Zig tiene costo amortizado menor que: 3(r’’(x) - r’(x) )
La operación Zag tiene costo amortizado menor que: 3(r’’’(x) - r’’(x)) + 1

Costo total:
3
cˆi <= 3(r’(x) - r(x) ) + 3(r’’(x) - r’(x) ) + 3(r’’’(x) - r’’(x)) + 1
i 1
Debido a la suma telescópica se cancelan r’(x) y r’’(x). Además la última rotación deja al nodo
en la raíz, por lo tanto:
3
cˆi <= 3(r’’’(x) –r(x)) +1 = 3( r(raíz) – r(x) ) +1
i 1

13.8.9. Costo total amortizado de una operación splay.

El costo total amortizado de una operación splay, de un nodo x en un árbol cuya raíz es t, será
debido a las sumas telescópicas: 3(r(t)-r(x)) +1
Lo cual puede escribirse:
s(t ) s(t ) n
3(r (t ) r ( x)) 1 3log( ) 1 O(log( )) O(log( ))
s ( x) s ( x) p

Ya que s(t) es n, el número de nodos en el árbol; y s(x) = p, donde p<n es el número de nodos
en subárbol con raíz x, incluida la raíz x. Si el nodo x, es hoja, entonces p=1, y el costo total
amortizado será: O(log(n)).

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 33
Las operaciones insertar, buscar y descartar difieren sólo en un factor constante de la operación
splay, ya que se implementan en función de ésta.

13.8.10. Cambios de potencial.

El mayor cambio de potencial se produce cuando un nodo se mueve desde la raíz a una hoja.
En el peor caso el potencial inicial está asociado a una lista.
(T D0 ) log(1) log(2) log(3) log(4)... log(n) log( (n 1)) O(n log(n))
En el peor caso el potencial final tiene valor cero. En el cual el árbol está formado por una sola
hoja.
Entonces la mayor diferencia de potencial se tiene para:
( D0 ) ( Dm ) O(n log(n)) 0

13.8.11. Teorema de Balance.

Una secuencia de m operaciones splay en un árbol de n nodos tiene una complejidad temporal:
O(m log(n) + n log(n)).

Demostración:
Con r(t) <= log(n) y r(x) >=0 se obtiene el costo amortizado de una operación: 3log(n) 1
Entonces, las m operaciones, más el cambio de potencial, se expresa según:
m(3log(n) 1) O(n log(n)) la cual es O(m log(n) + n log(n)).
Completando la demostración.

Referencias.

Daniel Sleator, Robert Tarjan, “Self-Adjusting Binary Search Trees”, Journal of the Association
for Computing Machinery. Vol. 32, No. 3, July 1985, pp. 652-686.

Profesor Leopoldo Silva Bijit 26-05-2008


3432 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 13 ............................................................................................................................................1
ÁRBOLES DESPLEGADOS. SPLAY TREES.......................................................................................1
13.1 DEFINICIÓN. ......................................................................................................................................1
13.2 OPERACIÓN SPLAY. ...........................................................................................................................1
Zig. .......................................................................................................................................................2
Zig-Zag.................................................................................................................................................2
Mover a la raíz. ....................................................................................................................................3
13.3 TIPOS DE ALGORITMOS. .....................................................................................................................3
13.3.1. Splay Bottom-up. ......................................................................................................................3
13.3.2. Ejemplos de operaciones splay bottom-up. .......................................................................................... 4
Splay(3, root); ............................................................................................................................................. 4
Splay(1, root); ............................................................................................................................................. 4
MuevealaRaiz(1, root)................................................................................................................................. 5
Insertar nodo con valor 5. ............................................................................................................................ 5
Descartar(4, root). ....................................................................................................................................... 6
Descartar(6, root) ........................................................................................................................................ 6
13.3.2. Splay top-down.........................................................................................................................7
Zig-Zig ............................................................................................................................................................. 8
Zig-Zag. ........................................................................................................................................................... 9
Join................................................................................................................................................................... 9
Ejemplo top-down. ......................................................................................................................................... 10
13.4. ANIMACIONES. ...............................................................................................................................11
13.5. CÓDIGOS. .......................................................................................................................................11
13.6. OPERACIONES UTILITARIAS. ...........................................................................................................17
SE AGREGA DESCARTAR PARA COMPLETAR LAS OPERACIONES BÁSICAS ........................................................17
13.7. FUNCIONES PARA EFECTUAR TEST DE SPLAY. .................................................................................19
13.8. ANÁLISIS DE COMPLEJIDAD. ...........................................................................................................20
13.8.1 Objetivo del análisis amortizado.............................................................................................20
13.8.2. Tipos de análisis.....................................................................................................................21
13.8.3. Analogía para la función potencial. .......................................................................................21
13.8.4. Ejemplo de análisis amortizado. ............................................................................................22
Método de agregación. ................................................................................................................................... 23
Método del banquero. .................................................................................................................................... 23
Método del potencial. ..................................................................................................................................... 24
13.8.5. Definiciones. ..........................................................................................................................25
13.8.6. Pasos en la aplicación del método del potencial. ..................................................................26
13.8.7. Función potencial en árboles splay. ......................................................................................27
Ejemplo función potencial. ............................................................................................................................ 28
13.8.8 Cálculo de costos amortizados por operación. .......................................................................29
Caso: Nodo x es la raíz .................................................................................................................................. 29
Caso: Zig ........................................................................................................................................................ 29
Caso: Zig-Zag ................................................................................................................................................ 30
Caso: Zig-Zig. ................................................................................................................................................ 31
Ejemplo .......................................................................................................................................................... 31
13.8.9. Costo total amortizado de una operación splay. ....................................................................32
13.8.10. Cambios de potencial. ..........................................................................................................33

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles desplegados. Splay trees. 35
13.8.11. Teorema de Balance. ........................................................................................................... 33
REFERENCIAS. ........................................................................................................................................ 33
ÍNDICE GENERAL. ................................................................................................................................... 34
ÍNDICE DE FIGURAS................................................................................................................................. 35

Índice de figuras.

FIGURA 13.1. OPERACIÓN ZIG. ..................................................................................................................... 2


FIGURA 13.2. OPERACIÓN ZIG-ZIG. .............................................................................................................. 2
FIGURA 13.3. OPERACIÓN ZIG-ZAG. ............................................................................................................. 2
FIGURA 13.4. MOVER X HACIA LA RAÍZ. ....................................................................................................... 3
FIGURA 13.5. OPERACIÓN SPLAY(3, ROOT)................................................................................................... 4
FIGURA 13.6. OPERACIÓN SPLAY(1, ROOT)................................................................................................... 4
FIGURA 13.7. OPERACIÓN MOVER A LA RAÍZ................................................................................................ 5
FIGURA 13.8. OPERACIÓN INSERTAR(5, ROOT) ............................................................................................. 5
FIGURA 13.9. OPERACIÓN DESCARTAR(4, ROOT).......................................................................................... 6
FIGURA 13.10. OPERACIÓN DESCARTAR(6, ROOT)........................................................................................ 6
FIGURA 13.11. TOP-DOWN ZIG ..................................................................................................................... 8
FIGURA 13.12. TOP-DOWN ZIG-ZIG .............................................................................................................. 8
FIGURA 13.13. TOP-DOWN ZIG-ZAG ............................................................................................................. 9
FIGURA 13.14. TOP-DOWN JOIN. ................................................................................................................... 9
FIGURA 13.15. TOP-DOWN ZIG-ZIG EN C. ................................................................................................... 10
FIGURA 13.16. TOP-DOWN ZIG-ZAG EN E. .................................................................................................. 10
FIGURA 13.17. TOP-DOWN JOIN. ................................................................................................................. 11
FIGURA 13.18. AMORTIZACIONES. .............................................................................................................. 22
FIGURA 13.19. TAMAÑOS Y RANGOS DE LOS NODOS. .................................................................................. 28
FIGURA 13.20. CAMBIOS DE TAMAÑOS Y RANGOS EN ZIG........................................................................... 29
FIGURA 13.21. CAMBIOS DE TAMAÑOS Y RANGOS EN ZIG-ZAG. ................................................................. 30
FIGURA 13.22. CAMBIOS DE TAMAÑOS Y RANGOS EN ZIG-ZIG. .................................................................. 31
FIGURA 13.23. CÁLCULOS AMORTIZADOS. ................................................................................................. 32

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 14.

Árboles enhebrados. Threaded tree.

Las operaciones de recorrido en un árbol binario de búsqueda, implementadas mediante


funciones recursivas o con un stack de los nodos a revisar, son generalmente costosas en tiempo
de ejecución.

Para lograr recorridos eficientes en un árbol puede modificarse la estructura del nodo agregando
un puntero al padre, o bien agregando un par de punteros al sucesor y predecesor formando de
este modo listas doblemente enlazadas.

Una alternativa, que demanda menos bits en cada nodo, es emplear un puntero derecho con
valor nulo, para apuntar al nodo sucesor de éste, y similarmente emplear un puntero izquierdo
nulo para apuntar a su predecesor; obviamente esto requiere dos bits adicionales por nodo para
indicar si el puntero referencia a un nodo hijo o al nodo sucesor o antecesor. Estos árboles se
denominan enhebrados (threaded). La idea es utilizar de mejor forma los (n+1) punteros que
tienen almacenados valores nulos en un árbol con n elementos.

Si sólo interesa acelerar la operación de encontrar al sucesor se emplean hebras solamente en los
punteros derechos de las hojas, para hilvanar los nodos con sus sucesores, dando origen a
árboles enhebrados por la derecha (right-threaded tree). Situación que será analizada en este
capítulo.

Los recorridos en árboles enhebrados siguen siendo de costo O(n) pero existirá un considerable
ahorro en tiempo al poder diseñar rutinas iterativas.

2 6

1 4 8

5 7 9

Figura 14.1 Árbol con hebras por la derecha.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
La Figura 14.1 muestra los punteros que son hebras a los sucesores. La operación sucesor de un
nodo tiene en muchos casos ahora costo O(1); salvo los nodos con hijo derecho que son de costo
O(log(n)). La función predecesor es de costo O(log(n)), ya que debe efectuarse un recorrido
desde la raíz, si el nodo no tiene descendiente izquierdo; es el caso de los nodos con valores 1,
4, 5, 7, y 9 en la Figura 14.1. El nodo con mayor valor en el árbol tiene hebra con valor nulo, lo
que indica que no tiene sucesor.

Debido a que ahora cada nodo puede tener dos punteros que lo referencian, el del padre y el de
su antecesor, las operaciones de inserción y descarte resultan levemente más complejas, para
mantener el árbol correctamente hilvanado.

Si un nodo tiene hijo derecho, su sucesor pertenecerá al subárbol derecho y será un nodo sin hijo
izquierdo; es decir el sucesor será el menor descendiente del subárbol derecho. Un nodo tiene
una hebra derecha apuntándolo si y sólo si tiene hijo izquierdo, es el caso de los nodos con
valores 2, 3, 6, y 8 en la Figura 14.1.

14.1. Análisis de las operaciones.

14.1.1. Buscar.

La búsqueda resulta una operación asimétrica, ya que debemos distinguir si el puntero derecho
referencia a un hijo o es una hebra.

La búsqueda falla si al descender por la izquierda se llega a un puntero nulo, o si se llega a una
hebra descendiendo por la derecha. En forma excepcional la búsqueda falla si el árbol está
vacío.

14.1.2. Inserción.

Como es usual se inserta en una hoja. Se denomina ndes al campo que describe si el puntero
derecho apunta a un HIJO o a una HEBRA. El nombre de la variable abrevia: el nodo derecho
es.

Si p apunta a la hoja donde debe insertarse el nuevo nodo, y t es el puntero que indica la
posición para insertar, pueden ocurrir dos situaciones.
a) Inserción en hoja, descendiendo por la izquierda.
Se inserta el nuevo nodo y su puntero derecho se marca como hebra, apuntando a p.

p->left=t; t->ndes=HEBRA; t->right=p;


p
p

t t

Figura 14.2 Inserción en hoja, descendiendo por la izquierda.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles enhebrados por la derecha 3
El nuevo nodo tendrá como sucesor al apuntado por p.
b) Inserción en hoja, con hebra derecha.
Es preciso marcar como hebra el nuevo nodo, y copiar el valor del puntero derecho de p, en el
nuevo nodo, hilvanándole el sucesor. Además debe marcarse que p ahora apunta a un hijo
derecho, y enlazar p con el nuevo nodo.

t->right=p->right; t->ndes=HEBRA; p->right=t; p->ndes=HIJO;


p
p
t
t

Figura 14.3 Inserción en hoja, descendiendo por la derecha.

El nuevo nodo tendrá clave mayor que la del apuntado por p, y menor que la apuntada por la
hebra de p; por esta razón el nuevo nodo tiene como sucesor al sucesor de p, antes de la
inserción.

14.1.3. Descarte.

En el descarte de un nodo en un árbol binario de búsqueda suelen analizarse los tres siguientes
casos: nodo que será descartado es hoja, el nodo tiene un hijo o tiene dos hijos. Sin embargo en
estos árboles enhebrados la clasificación de los casos conviene efectuarla considerando si el
nodo que será descartado tiene o no hijo izquierdo.
Esto es así, ya que si no hay hijo izquierdo no puede existir hebra que lo apunte, y no será
necesario efectuar escrituras en punteros para mantener el árbol correctamente hilvanado.
Si existe hijo izquierdo, existirá un puntero, el del antecesor, que referencia al que será
descartado y que debe ser actualizado.

Sea t el nodo que será descartado y p su padre.


Primero analizamos los casos a) y b) en los cuales t no tiene hijo izquierdo. Los casos c) y d)
serán para un nodo t que tiene hijo izquierdo. Los comentarios de fin de línea asocian los casos
con los de descarte en un árbol binario de búsqueda.

if(t->left==NULL)
if(t->ndes==HIJO) caso a) //un hijo
else caso b) //hoja
else
if(t->left->ndes==HEBRA) caso c) //dos hijos o hijo izquierdo y hebra derecha
else caso d) //dos hijos o hijo izquierdo y hebra derecha

Si el nodo que debe descartarse es la raíz, será preciso actualizar el árbol, para que apunte a la
nueva raíz. En este caso no existe p, el padre de t; sin embargo para evitar escribir códigos para
tratar el caso particular, se inicia p, apuntando a un nodo, denominado centinela, cuyo hijo
derecho apunta a t. Esto se desarrolla en 14.4.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
a) Nodo t con hijo derecho que tiene hijo izquierdo nulo.
Basta escribir en p el descendiente derecho de t. En la Figura 14.4 se ilustra el caso en que t es
descendiente derecho de p.

if ( t==p->right) p->right = t->right; else p->left=t->right; free(t);

p p

t t

x x

Figura 14.4 Descarte de nodo t con un hijo derecho x.

Ejemplo de este caso es el descarte del nodo con valor 4 en la Figura 14.1.
b) Nodo t con hebra derecha e hijo izquierdo nulo.
En este caso t es hoja con hebra derecha.

b1) Si t es descendiente derecho de su padre p, se debe marcar que p tiene hebra derecha, y
copiar en el puntero derecho de p, la dirección del sucesor de t.

p p
s
s

t t

Figura 14.5 Descarte de hoja t con un hebra derecha. Descendiente derecho de p.

b2) Si t es descendiente izquierdo de su padre p, se debe apuntar a nulo en el puntero izquierdo


de p.

p p

t s t s

Figura 14.6 Descarte de hoja t con hebra derecha. Descendiente izquierdo de p.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles enhebrados por la derecha 5
Las dos situaciones se resumen en las acciones:

if (t==p->right) {p->right=t->right; p->ndes=HEBRA} else p->left=t->left; free(t);


c) Nodo t tiene hijo izquierdo l, el que a su vez tiene hebra derecha.
Se copia en el hijo derecho de l, el hijo derecho de t. Se copia la marca del tipo de puntero
derecho del nodo t en el nodo l. Finalmente se pega l como descendiente de p.

p p

t t

b
l s l
b
a a

Figura 14.7 Descarte de nodo t con hijo izquierdo. Descendiente izquierdo con hebra.

l->right = t->right; l->ndes = t->ndes;


if (t==p->right) p->right = l; else p->left=l; free(t);

Cualquier hebra del subárbol izquierdo de l apunta dentro del subárbol o a l.


La Figura 14.7 muestra a t como descendiente izquierdo de p. Si t es descendiente derecho de p,
se trata igual.
d) Nodo t tiene hijo izquierdo l, el que a su vez tiene hijo derecho.
Este caso se trata en forma similar al del descarte de un nodo con dos hijos, eligiendo la
solución de encontrar el nodo a, el mayor descendiente del subárbol izquierdo de t, que en este
caso es su antecesor. Como puede comprobarse mediante un análisis similar, la solución
alternativa de encontrar el menor descendiente del subárbol derecho de t, tiene un costo mayor.

p
p

t c

c
l b
b l

a a

Figura 14.8 Descarte de nodo t con hijo izquierdo. Descendiente izquierdo con hijo.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
El primer paso es encontrar el nodo a, el antecesor de t. Para lo cual se desciende por el hijo
derecho de l, siempre por la derecha hasta encontrar un nodo que tenga hebra derecha. Se
desplaza el puntero l, apuntando siempre al padre de a.

for (;;) { a = l->right; if (a->ndes == HEBRA) break; l = a; }

Se actualiza el nodo l, dependiendo de si a tiene o no hijo izquierdo.

d1) Nodo antecesor a, tiene hijo izquierdo.


p
p
t
c t
c

b a
l b
l
a

Figura 14.8 Nodo antecesor a con hijo izquierdo.

Se pega subárbol izquierdo de a, como hijo derecho de l.

d2) Nodo antecesor a, no tiene hijo izquierdo.


p

p
t
c
t
c
b a
l
b
a l

Figura 14.9 Nodo antecesor a sin hijo izquierdo.

Se escribe en el hijo derecho de l la dirección de a, y se marca como hebra.

if (a->left != NULL) l->right = a->left;


else { l->right = a; l->ndes = HEBRA; }

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles enhebrados por la derecha 7
Luego se copian los punteros de t en los punteros de a, además se copia la marca del tipo de
puntero derecho del nodo t en el nodo a.

p p

t a t a
c c

b b
l l

Figura 14.10 Nodo antecesor a reemplaza al nodo t.

a->left = t->left; a->right = t->right; a->ndes = t->ndes;

Finalmente se pega, según si t es descendiente izquierdo o derecho de su padre p, la dirección


del antecesor a.
if ( t==p->right) p->right = a ; else p->left=a; free(t);

Esta solución no modifica las hebras de los descendientes del subárbol c, ni del subárbol b.

Si el nodo t es la raíz, debe modificarse ésta para apuntar ahora al descendiente derecho del
centinela, que es apuntado por p.

El descarte en un árbol enhebrado por la derecha es muy similar al descarte en árboles binarios
de búsqueda, difieren en las instrucciones que escriben el tipo de enlace derecho y en la
determinación de uno de los casos, por esta razón suele implementarse árboles enhebrados por
la derecha y no los árboles hilvanados con sucesores y antecesores.

14.1.4. Recorrido en orden.

a) Se inicia en el nodo ubicado más a la izquierda del árbol, se muestra el valor de la clave y se
sigue su hebra derecha.
b) Si se sigue una hebra a la derecha se muestra el valor de la clave del sucesor y se continúa
por el enlace derecho de éste.
c) Si se sigue un enlace a un hijo a la derecha, primero se desciende hasta el nodo ubicado más a
la izquierda, se imprime su clave y se continúa en b).

La impresión en orden muestra las claves en forma ascendente.

Si se tiene un arreglo y se insertan las claves en un árbol enhebrado por la derecha, luego con
un recorrido en orden puede generarse el arreglo ordenado ascendentemente.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
14.2. Estructura de datos y funciones básicas.

14.2.1. Estructura de datos.

#define HEBRA 1
#define HIJO 0
typedef struct tnode
{ int clave;
int ndes; //nodo derecho es: Hebra o Hijo
struct tnode *left; struct tnode *right;
} nodo, * pnodo;

14.2.2. Creación de un nodo.

pnodo getnodo(int dato)


{
pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) == NULL) return(NULL);
else
{ p->clave=dato; p->left=NULL; p->right=NULL; p->ndes=HIJO; }
return(p);
}

14.2.3. Mostrar nodo y el árbol enhebrado en niveles.

Se muestra la letra H, luego de la clave del nodo, si éste tiene hebra.

void printNodo(pnodo t, int h)


{ int i;
for(i=0; i<h; i++) putchar('\t'); //se emplean tabs para desplegar niveles.
if (t==NULL) {putchar('*') ; putchar('\n') ;}
else printf("%d", t->clave);
if (t->ndes==HEBRA) printf("H\n"); else putchar('\n') ;
}

void Mostrar(pnodo t, int h)


{
if (t==NULL) printNodo(t, h);
else
{if (t->ndes==HIJO) Mostrar(t->right, h+1) ;
printNodo(t, h);
Mostrar(t->left, h+1);}
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles enhebrados por la derecha 9
14.2.4. Recorrido en orden.

pnodo MasIzquierdista(pnodo t)
{
if (t == NULL) { return NULL;}
while (t->left != NULL) t = t->left;
return t;
}

void InOrden( pnodo t)


{ pnodo p = MasIzquierdista(t);
while (p != NULL)
{ printf(" %d ", p->clave);
if (p->ndes==HEBRA) p = p->right;
else p = MasIzquierdista(p->right);
}
putchar('\n');
}

14.2.5. Sucesor.

pnodo sucesor(pnodo t)
{
If (t==NULL) return NULL;
if (t->ndes==HIJO) return( MasIzquierdista(t) );
else if (t->right ==NULL) return NULL; //no hay sucesor
else return(t->right);
}

14.2.6. Buscar.

pnodo buscar(pnodo t, int valor) /* algoritmo iterativo */


{
while ( t != NULL)
{
if ( t->clave == valor ) return (t);
else if (t->clave > valor) t = t->left;
else if (t->ndes==HIJO) t = t->right; //asimétrico por la derecha
else return (NULL);
}
return (t); /* NULL No lo encontró*/
}

14.3. Insertar.

typedef enum {left, right, vacio} modo; //modos de descenso

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
pnodo Insertar(pnodo *praiz, int valor)
{ pnodo t= *praiz; //se pasa la raíz por referencia, para modificarla si el árbol está vacío.
pnodo p; //mantiene un puntero al padre del que será insertado
modo porlado=vacio; //dirección en el descenso.

if (t==NULL) //inserta en la raíz


{ p=getnodo(valor);
if (p!=NULL) {*praiz=p; p->ndes=HEBRA;}
return(p);
}

while ( t != NULL)
{
if ( t->clave == valor )
{/*lo encontró, no inserta. No se aceptan claves repetidas en conjuntos*/
return (t); //devuelve el encontrado.
}
else
{ p=t ;
if (t->clave > valor) {t = t->left; porlado=left;}
else
{ porlado=right;
if(t->ndes==HIJO) t = t->right;
else break;
}
}
}

/*Al salir del while p apunta al nodo donde se insertará el nuevo, y porlado la dirección */
/* El argumento t apunta a NULL o al sucesor de p */
t = getnodo(valor); //se pega el nuevo nodo en t.
if(t==NULL) return(NULL);

if (porlado==left)
{p->left=t; t->ndes=HEBRA; t->right=p;}
else if(porlado==right)
{t->right=p->right; t->ndes=HEBRA; p->right=t;p->ndes=HIJO;}
return (t); /* Apunta al recién insertado. Null si no se pudo insertar*/
}

14.4. Descartar.

En la Figura 14.11, se muestran los argumentos y variables locales de la función Descartar,


luego de iniciadas. Raíz es una variable estática que referencia al árbol. Se ha dibujado sólo el
nodo raíz del árbol.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles enhebrados por la derecha 11

praiz p

raiz
centinela
t

Figura 14.11 Argumentos y variables locales luego de ser iniciadas.

int Descartar(pnodo *praiz, int valor)


{ pnodo t= *praiz; //se pasa la raíz por referencia, para modificarla si el árbol queda vacío.
nodo centinela;
pnodo p=&centinela; //mantiene un puntero al padre del que será descartado
p->ndes=HIJO; p->left=NULL; p->right=t; //inicia centinela

if (t==NULL) return(0); //no puede descartar en árbol vacío

for ( ;; )
{
if (t->clave > valor)
{ if(t->left==NULL) return(0); else {p=t;t = t->left;}
//porlado=left;
}
else if (t->clave < valor)
{ if(t->ndes==HIJO) {p=t; t = t->right;} else return(0);
//porlado=right;
}
else break; //lo encontró
}
/*Al salir del for p apunta al padre de t */
/* t apunta al nodo que será descartado */

if(t->left==NULL)
{
if(t->ndes==HIJO) //caso a) un hijo
{ if ( t==p->right) p->right = t->right; else p->left=t->right; }
else //caso b) hoja
{ if (t==p->right) {p->right=t->right; p->ndes=HEBRA;} else p->left=t->left; }
}
else
{ pnodo l= t->left; //variable local al bloque para hijo izquierdo de t.
if(t->left->ndes==HEBRA) //caso c) dos hijos o hijo izquierdo y hebra derecha
{ l->right = t->right; l->ndes = t->ndes;

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
if (t==p->right) p->right = l; else p->left=l;
}
else //caso d) dos hijos o hijo izquierdo y hebra derecha
{ pnodo a; //variable local al bloque para el antecesor de t
for (;;) { a = l->right; if (a->ndes == HEBRA) break; l = a; }
if (a->left != NULL) l->right = a->left;
else { l->right = a; l->ndes = HEBRA; }
a->left = t->left; a->right = t->right; a->ndes = t->ndes;
if ( t==p->right) p->right = a ; else p->left=a;
}
}
if (t==*praiz) *praiz= p->right; //cambio de raíz
free(t);
return(1); //descarte exitoso
}

14.5 Test de estructura de árbol enhebrado por la derecha.

La siguiente función recursiva aplica las propiedades que deben cumplir los nodos de un árbol
enhebrado por la derecha y es útil para verificar el diseño de las funciones.

//retorna 1 si es rtbst
int testrtbst(pnodo t)
{ int l=1, r=1;
//Si arbol está vacío, es rtbst
if (t==NULL) return 1;
//test subarbol izq
if (t->left != NULL)
{ if (t->clave > t->left->clave) l=testrtbst(t->left);
else l=0;
}

//test subarbol der


if (t->ndes==HIJO)
{ if (t->right != NULL)
{if (t->clave < t->right->clave) r=testrtbst(t->right);
else r=0;
}
else r=0; //no puede haber hijo con puntero derecho nulo
}
else
//es hebra
{
if (t->right!=NULL)
{ if (sucesor(t) == t->right) ; //debe ser el sucesor de t
else r=0;
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles enhebrados por la derecha 13
else
if (sucesor(t)==NULL); //debe ser el máximo
else return(0);
}
if ( (l+r)==2) return(1) ; else return(0);
}

Referencias.

Knuth, D. E., “The Art of Computer Programming, Volume 1: Fundamental Algorithms”,


section 2.3.1. 3rd ed. Addison-Wesley, 1997. ISBN 0-201-89683-4.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 14. ...........................................................................................................................................1


ÁRBOLES ENHEBRADOS. THREADED TREE. .................................................................................1
14.1. ANÁLISIS DE LAS OPERACIONES. ......................................................................................................2
14.1.1. Buscar. .....................................................................................................................................2
14.1.2. Inserción. .................................................................................................................................2
a) Inserción en hoja, descendiendo por la izquierda. ........................................................................................ 2
b) Inserción en hoja, con hebra derecha. .......................................................................................................... 3
14.1.3. Descarte. ..................................................................................................................................3
a) Nodo t con hijo derecho que tiene hijo izquierdo nulo. ................................................................................ 4
b) Nodo t con hebra derecha e hijo izquierdo nulo. .......................................................................................... 4
c) Nodo t tiene hijo izquierdo l, el que a su vez tiene hebra derecha. ............................................................... 5
d) Nodo t tiene hijo izquierdo l, el que a su vez tiene hijo derecho. ................................................................. 5
14.1.4. Recorrido en orden. .................................................................................................................7
14.2. ESTRUCTURA DE DATOS Y FUNCIONES BÁSICAS. ..............................................................................8
14.2.1. Estructura de datos. .................................................................................................................8
14.2.2. Creación de un nodo. ...............................................................................................................8
14.2.3. Mostrar nodo y el árbol enhebrado en niveles.........................................................................8
14.2.4. Recorrido en orden. .................................................................................................................9
14.2.5. Sucesor. ....................................................................................................................................9
14.2.6. Buscar. .....................................................................................................................................9
14.3. INSERTAR. ........................................................................................................................................9
14.4. DESCARTAR. ..................................................................................................................................10
14.5 TEST DE ESTRUCTURA DE ÁRBOL ENHEBRADO POR LA DERECHA.....................................................12
REFERENCIAS. .........................................................................................................................................13
ÍNDICE GENERAL. ....................................................................................................................................14
ÍNDICE DE FIGURAS. ................................................................................................................................14

Índice de figuras.

FIGURA 14.1 ÁRBOL CON HEBRAS POR LA DERECHA. ....................................................................................1


FIGURA 14.2 INSERCIÓN EN HOJA, DESCENDIENDO POR LA IZQUIERDA. .........................................................2
FIGURA 14.3 INSERCIÓN EN HOJA, DESCENDIENDO POR LA DERECHA. ...........................................................3
FIGURA 14.4 DESCARTE DE NODO T CON UN HIJO DERECHO X........................................................................4
FIGURA 14.5 DESCARTE DE HOJA T CON UN HEBRA DERECHA. DESCENDIENTE DERECHO DE P. .....................4
FIGURA 14.6 DESCARTE DE HOJA T CON HEBRA DERECHA. DESCENDIENTE IZQUIERDO DE P. ........................4
FIGURA 14.7 DESCARTE DE NODO T CON HIJO IZQUIERDO. DESCENDIENTE IZQUIERDO CON HEBRA. .............5
FIGURA 14.8 DESCARTE DE NODO T CON HIJO IZQUIERDO. DESCENDIENTE IZQUIERDO CON HIJO. .................5
FIGURA 14.8 NODO ANTECESOR A CON HIJO IZQUIERDO. ...............................................................................6
FIGURA 14.9 NODO ANTECESOR A SIN HIJO IZQUIERDO. .................................................................................6
FIGURA 14.10 NODO ANTECESOR A REEMPLAZA AL NODO T. .........................................................................7
FIGURA 14.11 ARGUMENTOS Y VARIABLES LOCALES LUEGO DE SER INICIADAS. .........................................11

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 15

Árboles AA.

Mediante árboles binarios de búsqueda balanceados, se han desarrollado algoritmos eficientes


para mantener diccionarios, que garantizan un costo logarítmico de las operaciones en peor
caso.
Para desarrollar algoritmos para árboles AVL y árboles coloreados se requiere un análisis
cuidadoso de los diferentes y numerosos casos que se presentan, para implementar las
estrategias de rebalance luego de una inserción o un descarte.

Los árboles AA, así denominados por las iniciales de su creador, son una simplificación de las
reglas de los árboles coloreados y originan un número bastante menor de situaciones que deben
ser analizadas. Además de las reglas de los árboles coloreados, sólo se permiten nodos rojos
como descendientes derechos.

En estos árboles, en lugar de la información sobre el color, se almacena el nivel del nodo; las
hojas son de nivel 1. En lugar de color rojo se habla de enlace derecho horizontal, o de un nodo
descendiente derecho con igual nivel que su padre. La Figura 15.1, muestra las estructuras de
árboles AA, con número de nodos n, de uno a tres. A la derecha se muestra el nivel de los
nodos.
n=1 1
0

n=2 1
0
2
n=3
1
0

Figura 15.1. Árboles AA, de hasta 3 nodos.

15.1. Propiedades de los árboles AA.

El nivel de un hijo izquierdo debe ser menor que el nivel de su padre.


El nivel de un hijo derecho debe ser menor o igual al nivel de su padre.
El nivel de un nieto derecho debe ser menor que el nivel de su abuelo.
Las hojas son de nivel 1.
Los nodos que no son hojas deben tener dos hijos.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
El número de nodos visitados, desde la raíz hasta las hojas, por la vía más larga debe ser a lo
más el doble del número de nodos recorridos por la vía más corta. Esto asegura un costo
logarítmico para las operaciones.

15.2. Análisis de la operación inserción.

Un nuevo nodo que será de nivel 1, se inserta en las hojas, que también son de nivel 1.
Se presentan dos casos.

a) Si se inserta por la izquierda se viola la regla de sólo horizontales derechos, y debe efectuarse
una rotación derecha.
Descendiendo desde la raíz, se determina la posición para insertar, ésta se muestra con una
flecha, en la Figura 15.2, superior. Al centro, se muestra una representación convencional, con
un nodo descendiente horizontal izquierdo. En la parte inferior se muestra luego de una rotación
derecha, respecto del padre del nodo insertado; ahora se cumplen las propiedades de un árbol
AA. Se definió esta operación como torcer (skew).

Figura 15.2. Inserción por la izquierda. Skew.

La inserción por la izquierda seguida de una torsión, puede originar una nueva violación a las
propiedades de los árboles AA. Esto se ilustra en la Figura 15.3 en la parte superior; luego se
dibuja en forma convencional, de acuerdo a los niveles de los nodos. La tercera ilustración
muestra la situación luego del skew; lo cual produce el doble rojo, o dos hijos horizontales, debe
notarse que el recién insertado es la nueva raíz. La situación se corrige con una rotación a la
izquierda, respecto del nodo recién insertado, y luego incrementando el nivel del nodo central;
lo que se muestra en el diagrama inferior de la Figura 15.3. Esta última operación se denomina
partir (split), y se deriva de los B-trees antecesores de los árboles AA, en los cuales se divide un
multinodo que excede el máximo número de claves en dos multinodos.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 3

1
2
1

Figura 15.3. Inserción por la izquierda. Skew y split en el mismo nivel.

Puede observarse que cuando se ejecuta un skew seguido de un split, en el mismo nivel de
recursión, puede simplificarse la secuencia evitando las rotaciones, ya que basta incrementar el
nivel del nodo raíz en el segundo diagrama; pasando de este modo al diagrama inferior de la
Figura 15.3.

Otra situación de inserción por la izquierda se muestra en la Figura 15.4. Luego de un skew en
el padre del recién insertado resulta el diagrama al centro. Y al retornar por la vía por la cual se
descendió para insertar, debe efectuarse un split en el nuevo padre del recién insertado,
resultando el diagrama inferior de la Figura 15.4. Nótese que el skew y el split se aplican en
nodos diferentes, en dos niveles de recursión.
1

Figura 15.4. Inserción por la izquierda. Skew y split en niveles de recursión diferentes.

b) Si se inserta por la derecha, si el padre no tiene descendiente derecho no hay modificaciones


que efectuar, lo cual se muestra a la izquierda de la Figura 15.5; pero si el padre es un
descendiente derecho se producen dos descendientes horizontales, la situación equivale a dos
rojos adyacentes, y se corrige efectuando una rotación a la izquierda, e incrementando en uno el
nivel del nodo central; es decir con un split. No se requiere en este caso de la operación skew.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

1 1
2
1 1

Figura 15.5. Inserción por la derecha.

La operación split, al subir el nivel de un nodo, puede ocasionar un nuevo descendiente


izquierdo de igual nivel que el padre o tres nodos adyacentes de igual nivel. Por esta razón debe
ascenderse hasta la raíz, siguiendo la ruta por la que se descendió, para ubicar la posición de
inserción, efectuando las operaciones skew y split, en ese orden, en cada uno de los nodos de la
ruta.
Sin embargo, como se verá más adelante, en cada nivel de recursión, cuando debe efectuarse
una modificación para mantener las propiedades sólo se efectúa un skew o un split o un
incremento de nivel.

Se han analizado las diferentes situaciones de inserción que se producen en el primer nivel de un
árbol AA. Esto se logró efectuando inserciones en un árbol con un nodo, e inserciones en un
árbol con dos nodos.

Aplicando las operaciones anteriores, puede comprobarse la estructura de los árboles AA, para
4, 5, 6 y 7 nodos que se muestran en la Figura 15.6. Debe notarse que en cada diagrama hay
que estudiar (n+1) casos de inserción, ya que éste es el número de nodos externos, o los posibles
lugares para insertar el nuevo nodo.

Sin embargo si se observan las formas que se producen en los diferentes niveles de la Figura
15.6, puede deducirse que los casos de inserción analizados antes son los únicos que se
presentan.
2
n=4 1
2
n=5 1
2
n=6 1
3
2
n=7
1

Figura 15.6. Formas de árboles AA para 4, 5, 6 y 7 nodos.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 5
15.2.1. Ejemplos de inserción.

Si se realiza una inserción de claves en orden ascendente, desde el 1 hasta el 15, se obtiene
luego de las operaciones de rebalance, después de cada inserción, la Figura 15.7,
inmediatamente luego de insertado el nodo con valor 15.

Se devuelve siguiendo la ruta desde la raíz al nodo insertado, pero en forma ascendente. Como
el nodo 15 cumple las propiedades, es un árbol AA; se asciende al 14, que también es AA. Pero
al ascender al 13, éste tiene dos hijos horizontales; entonces debe realizarse un split en el nodo
13, con lo cual resulta la Figura 15.8. El árbol cuya raíz es el nodo con valor 14 es AA, notar
que ésta es la raíz luego del split.

Figura 15.7. Inserción de nodo con valor 15 en árbol AA.

Figura 15.8. Luego de split en nodo con clave 13.

Al ascender al nodo con valor 12, también se cumplen las propiedades de los árboles AA. Pero
al ascender al nodo con valor 10, debe volver a efectuarse un split, resultando la Figura 15.9.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

Figura 15.9. Luego de split en nodo con clave 10.

Continuando el ascenso, no es necesario efectuar correcciones en el nodo con valor 8, pero al


ascender a la raíz, debe volver a efectuarse un split, resultando la Figura 15.10; que muestra un
árbol perfectamente balanceado, con raíz 8. En secuencias de inserciones ascendentes sólo es
necesario realizar operaciones splits.

Figura 15.10. Luego de split en nodo con clave 4.

Estudiemos ahora una inserción de secuencias descendentes de claves. Se han insertado, en


forma descendente las claves desde 15 a 9, y luego se han efectuado las operaciones de
mantención. La Figura 15.11, a la izquierda, muestra la inserción de un nodo con clave 8; lo
cual produce un descendiente izquierdo de igual nivel que su padre, por lo cual debe efectuarse
un skew, al ascender al nodo 9. A la derecha de la Figura 15.11, se muestra el árbol AA
resultante, ya que al recorrer la ruta ascendente, pasando por el 10 y llegando a la raíz 12, no es
necesario efectuar mantenciones.

Figura 15.11. Luego de insertar el 8, y luego de skew en 9 y ascender hasta la raíz.

Si en el árbol AA de la Figura 15.11, a la derecha se inserta un nodo con valor 7, se tiene el


diagrama a la izquierda de la Figura 15.12.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 7

Figura 15.12. Luego de insertar el nodo con valor 7 y luego del skew en 8.

Se efectúa un skew en 8, luego de la operación se tiene el diagrama a la derecha de la Figura


15.12; y es preciso efectuar un split en 7. Luego de esta operación resulta el diagrama a la
izquierda de la Figura 15.13. El cual podría haberse obtenido sin rotaciones, incrementando el
nivel del nodo con clave 8, en la Figura 15.12 a la izquierda.
Al ascender al nodo 10, la operación anterior produce un hijo izquierdo de 10 de igual nivel, lo
cual implica un skew en 10. Luego de esta operación y al ascender a la raíz, no son necesarias
más operaciones para mantener las propiedades de los árboles AA. Lo cual se muestra en la
Figura 15.13, a la derecha.

Figura 15.13. Luego de split en 7, y luego del skew en 10.

Puede comprobarse que luego de las inserciones de los nodos 6 a 1, y de las posteriores
verificaciones del cumplimiento de las propiedades, resulta la Figura 15.10.

15.2.2. Diseño de las operaciones skew y split.

Las operaciones pueden efectuarse de manera recursiva o de arriba hacia abajo.


La operación torcer (skew) elimina los hermanos horizontales izquierdos bajo el nodo. Se
desciende por la derecha efectuado rotaciones derechas cuando se encuentra un enlace
izquierdo.

Si un nodo tiene más de un hermano derecho de igual nivel debe partirse (split), aumentando el
nivel de cada segundo nodo en el descenso. Se desciende por la derecha, efectuando rotaciones
izquierdas.

Debido a que tanto la inserción como el descarte requieren recorrer una ruta desde la raíz hasta
las hojas, es preferible realizar la mantención de las propiedades de los árboles AA, desde abajo
hacia arriba, efectuando operaciones a medida que se asciende por la ruta de descenso. Para

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
lograr esto las operaciones skew y split pueden implementarse como procedimientos actuando
sobre un nodo solamente. Como se verá más adelante esto implica invocar tres veces a torcer y
dos veces a partir en cada nodo en la operación descartar; y una vez a torcer y partir en cada
nodo en la operación insertar.

Podemos analizar en general la inserción, asumiendo que el subárbol apuntado por t, es AA en


el lado en que no se realizó la inserción y que su raíz tiene nivel i. Por otro lado, el subárbol
cuya raíz es n, donde se realizó la inserción es AA, ya que se ha logrado reestablecer las
propiedades en los niveles inferiores. Mediante el puntero t, se recorre la ruta ascendente hacia
la raíz.

Si el nivel de n es menor que i, no es necesario seguir revisando la ruta ascendente.

15.2.3. Inserción por la izquierda.

a) B de nivel i.
Si el subárbol derecho era AA, los nodos C y D deben tener nivel (i-1). En la Figura 15.14, se
muestra el nodo n de nivel i, lo que viola las propiedades de un árbol AA, y la estructura debe
ser corregida. Según se analizó antes, la única forma en que un nodo aumenta su nivel es
teniendo ambos hijos de igual nivel. Entonces si antes de insertar el nodo n el árbol era AA, y si
el nodo n alcanza grado i quiere decir que lo ha logrado teniendo hijos de igual nivel. Esto
implica que a y b deben ser de nivel (i-1). Si A tiene padre debe ser de nivel (i+1).
t A i

n i B i

a i-1 b i-1 C i-1 D i-1

Figura 15.14. Inserción por la izquierda. B de nivel i.

En este caso luego de un skew en t, resulta el diagrama a la izquierda de la Figura 15.15; y luego
del split en el nuevo t, resulta el subárbol AA a la derecha de la Figura 15.15. En este caso,
debido a que el nodo A aumenta su nivel inicial, es preciso continuar la revisión ascendente. La
situación puede ser corregida, sin realizar rotaciones, cambiando solamente el nivel del nodo A,
en la Figura 15.14. Nótese que se preserva la propiedad de que el nodo que aumenta su nivel
tiene ambos hijos iguales y de un nivel menor.

t n i
t A i+1
a i-1 A i
b i-1
B i n i B i

C i-1 D i-1 a i-1 b i-1 C i-1 D i-1

Figura 15.15. Inserción por la izquierda. Cambio de nivel.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 9
b) B de nivel (i-1).
El nodo C debe tener nivel (i-2). D puede ser de nivel (i-1) o (i-2). Si A es hijo izquierdo, su
padre tiene nivel (i+1); y si es hijo derecho podría ser de nivel i.
t A i

n i B i-1

a i-1 b i-1 C i-2 D i-1

Figura 15.16. Inserción por la izquierda. B de nivel (i-1).

Luego del skew en t, el árbol es AA, y el diagrama se muestra en la Figura 15.17. Es preciso
continuar la revisión ascendente si A era un hijo derecho, ya que en este caso podrían producirse
tres nodos consecutivos de igual nivel.

t n i

a i-1 A i
b i-1
B i-1

C i-2 D i-1

Figura 15.17. Inserción por la izquierda. Luego de skew en t.

15.2.4. Inserción por la derecha.

En este caso B debe ser de nivel (i-1). Si en el ascenso por el lado derecho, n incrementó su
nivel, debe cumplirse que sus hijos son de nivel (i-1). El árbol es AA, y no requiere
modificaciones. Debe continuarse la revisión ascendente si A es hijo derecho, ya que podrían
producirse tres nodos adyacentes de igual nivel.

t A i

B i-1 n i

C i-2 D i-1 a i-1 b i-1

Figura 15.18. Inserción por la derecha. B de nivel i-1.

La operación torcer (skew) remueve los enlaces horizontales izquierdos (en el mismo nivel),
rotando en el padre a la derecha. No se propagan cambios hacia abajo, ya que se cambia un
horizontal izquierdo por un horizontal derecho.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
Sin embargo la operación torcer (skew) puede crear dos enlaces horizontales derechos
consecutivos. La operación partir (split) remueve los dos rojos adyacentes mediante una
rotación izquierda y aumentando un nivel al padre.

Para mantener las propiedades de un árbol AA, es necesario balancear los nodos, que se
recorren al descender para insertar, en forma ascendente hasta la raíz.

Basta efectuar, de acuerdo al trabajo original, en cada nodo de la ruta ascendente:


t=skew(t);
t=split(t);

Sin embargo de acuerdo al análisis realizado, en cada nivel de recursión se efectúa un cambio de
nivel, o un skew o un split. Lo cual puede plantearse:

Si niveles de los hijos de t son iguales, aumentar el nivel de t.


Si son diferentes:
Si hijo izquierdo de t es de igual nivel que t: torcer
Si son diferentes:
Si nieto derecho de t es de igual nivel que t: partir.

El siguiente segmento explica las operaciones, que deben efectuarse en cada nodo de la ruta
ascendente, en términos de rotaciones. La rotación izquierda debe incorporar el cambio de nivel
de la nueva raíz.

if ((t->left->nivel== t->nivel ) && (t->right->nivel== t->nivel)) t->nivel++;


else if (t->left->nivel== t->nivel ) t=rrot(t);
else if (t->right->right->nivel== t->nivel ) t=lrot(t);

15.3. Análisis del descarte.

En el proceso de ajuste de niveles en el ascenso, se ha borrado una hoja en uno de los subárboles
de t, de nivel (i+1). El árbol cuya raíz es n, ya ha sido corregido y cumple las propiedades; así
también el árbol cuya raíz es B, ya que no ha sido modificado. Corresponde recuperar las
propiedades del árbol con raíz A, apuntado por t.

La Figura 15.19, a la izquierda, muestra los nodos involucrados en el análisis.

Si n es de nivel i, no es necesario seguir revisando en el ascenso. Solo es preciso corregir si


nivel de n es (i-1).

En el trabajo original se plantea, como ejercicio, que el peor caso de desbalance en el descarte
puede ser corregido por a lo más 3 skews y 2 splits. Es decir con:

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 11
if (ok && ((t->left->nivel < t->nivel-1) || (t->right->nivel < t->nivel-1)))
{ tnivel=t->nivel;
t->nivel = t->nivel -1;
if (t->right->nivel > t->nivel) t->right->nivel = t->nivel;
t=skew(t);
t->right=skew(t->right);
t->right->right=skew(t->right->right);
t=split(t);
t->right=split(t->right);
if (tnivel==t->nivel) ok=0;
}

Sin embargo el análisis de los diferentes casos resulta complejo como se verá a continuación.

15.3.1. Ascenso por la izquierda.

a1) B de nivel (i+1).


Esto implica que C y D deben ser de nivel i.
Se tienen 4 subcasos. E de nivel i o (i-1) y F de nivel i o (i-1).
a11) Con E y F de nivel i.
A la derecha de la Figura 19, se muestra luego del cambio de niveles de t y t->right. No se
requiere en este caso efectuar un skew en t.

t A i+1 t A i

n i-1 B i+1 n i-1 B i

C i D i C i D i

a i-1 E i F i a i-1 E i F i

e i-1 f i-1 e i-1 f i-1

Figura 15.19. Ascenso por la izquierda. E y F son de nivel i.

La Figura 15.20 a la izquierda muestra el diagrama luego del skew en t->right. A la derecha de
la Figura 15.20, se muestra luego del skew en t->right->right (el nieto de t).

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
t
A i
a i-1 C i t
A i

a i-1 B i n i-1 C i
E i
E i D i a i-1 B i
e i-1 D i
e i-1 f i-1 F i f i-1 F i

Figura 15.20. Ascenso por la izquierda. Luego de tres operaciones torcer.

La Figura 15.21 a la izquierda muestra el diagrama luego del split en t. A la derecha de la Figura
15.21, se muestra luego del split en t->right.

t C i+1
t C i+1
A i B i+1
A i E i
n i-1 a i-1 B i n i-1 a i-1 E i D i
e i-1 D i
f i-1 F i e i-1 f i-1 F i

Figura 15.21. Ascenso por la izquierda. Luego de las dos operaciones partir.

Como el nivel del nodo raíz no cambia, no sigue la revisión ascendente.


a12) Con E de nivel (i-1) y F de nivel i.
B de nivel (i+1) implica que C y D son de nivel i.
A la derecha de la Figura 15.22, se muestra luego del cambio de niveles de t y t->right. No se
requiere en este caso efectuar un skew en t.

t A i+1 t A i

n i-1 B i+1 n i-1 B i

C i D i C i D i

F i F i
a i-1 E i-1 a i-1 E i-1

Figura 15.22. Ascenso por la izquierda. Con E de nivel (i-1), y F de nivel i.

La Figura 15.23 muestra luego del skew en t->right.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 13
t A i
n i-1 C i

a i-1 B i

E i-1 D i

F i

Figura 15.23. Ascenso por la izquierda. Con E de nivel (i-1), y F de nivel i.

No hay skew en t->right->right. La Figura 15.24 muestra luego del Split en t.


t C i+1
A i B i

n i-1 a i-1 E i-1 D i

F i

Figura 15.24. Ascenso por la izquierda. Con E de nivel (i-1), y F de nivel i.

La Figura 15.24 muestra luego del split en t->right .


t C i+1

A i D i+1

n i-1 a i-1 B i F i

E i-1

Figura 15.24a. Ascenso por la izquierda. Con E de nivel (i-1), y F de nivel i.

No sigue revisión ascendente.


a13) Con E de nivel (i-1) y F de nivel ( i-1).
Similar al caso anterior, excepto que no se realiza el split en t->right, resulta la Figura 15.24b.

t C i+1
A i B i

n i-1 a i-1 E i-1 D i

F i-1

Figura 15.24b. Ascenso por la izquierda. Con E de nivel (i-1), y F de nivel (i-1).

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
a14) Con E de nivel i y F de nivel ( i-1).
Se trata igual al caso a11).
a2) B de nivel i.
C debe ser (i-1). No importa nivel de E.
a21) Con D de nivel i.
A la derecha de la Figura 15.25, se muestra luego de cambio nivel sólo en A. No se efectúa el
skew en t ni en t->right, ni en t->right->right.

t A i+1 t A i

n i-1 B i n i-1 B i

C i-1 D i C i-1 D i

a i-2 E i-1 a i-2 E i-1

Figura 15.25. Ascenso por la izquierda. Con B de nivel i.

La Figura 15.26, muestra luego del split en A. No hay split en D.


t B i+1

A i D i

n i-1 C i-1

a i-2 E i-1

Figura 15.26. Ascenso por la izquierda. Con B de nivel i.

No sigue revisión ascendente.


a22) Con D de nivel (i-1).
Si D es (i-1) no hay split en t, sólo cambia el nivel de A, y la revisión ascendente debe
continuar.

Pueden resumirse las acciones necesaria para balancear el árbol cuando se asciende por la
izquierda, con el siguiente segmento, que muestra las acciones en términos de rotaciones. Se
emplea la variable ok, para continuar o no la revisión de las propiedades en el ascenso.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 15
//ascenso por la izquierda.
if(t->left->nivel< t->nivel-1)
{
if (t->right->nivel== t->nivel ) // Caso a1). B de nivel (i+1)
{ t->nivel--; t->right->nivel--; ok=0; //ok=0 implica no continuar revisión ascendente
t->right=rrot(t->right); t=lrot(t);
if (t->right->nivel==t->right->right->right->nivel) // F de nivel i.
{ if (t->right->left->nivel== t->right->nivel ) {t->right=rrot(t->right);} //a11
t->right=lrot(t->right); //a13
}
else if (t->right->left->nivel==t->right->nivel) t->right->nivel++; //a12
}
else //if (t->right->nivel==t->nivel-1) //Caso a2). B de nivel i.
{ t->nivel--;
if(t->right->right->nivel==t->nivel) {t=lrot(t); ok=0;} //a21
}
}

Lo cual muestra que en el peor caso se requieren 4 rotaciones para mantener las propiedades de
un árbol AA, en los nodos de la ruta ascendente. Pero el código es similar en complejidad a los
árboles coloreados.

15.3.2. Ascenso por la derecha.

B debe ser de nivel i y C de nivel (i-1).


b1) Con D de nivel i.
F debe ser de nivel (i-1). Luego de corregir nivel sólo en A se tiene el diagrama a la derecha de
la Figura 15.27.
t A i+1 t A i

B i n i-1 B i n i-1

C i-1 D i C i-1 D i

b i-1 F i-1 b i-1 F i-1

f i-2 a i-1 f i-2 a i-1

Figura 15.27. Ascenso por la derecha. Con D de nivel i.

Luego del primer Skew en t, resulta la Figura 15.28.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

t B i

C i-1 A i

D i n i-1

b i-1 F i-1

f i-2 a i-1

Figura 15.28. Ascenso por la derecha. Con D de nivel i.

Luego del segundo skew en t->right, se tiene la Figura 15.29.


t B i

C i-1 D i

b i-1 A i

F i-1 n i-1

f i-2 a i-1

Figura 15.29. Ascenso por la derecha. Con B de nivel i.

El tercer skew en t->right->right no se realiza.


Luego del primer split en t, resulta la Figura 15.30.
t D i+1

B i A i

C i-1 b i-1 F i-1 n i-1

f i-2 a i-1

Figura 15.30. Ascenso por la derecha. Con B de nivel i.

El split en t->right no se realiza. No sigue revisión en ascenso


b2) Con D de nivel i-1.
B debe ser de nivel i y C debe ser de nivel (i-1). El nivel de F no importa.
La Figura 15.31 a la derecha muestra luego de la corrección del nivel de A.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 17
t A i+1 t A i

B i n i-1 B i n i-1

C i-1 D i-1 C i-1 D i-1

b i-2 F i-1 b i-2 F i-1

Figura 15.31. Ascenso por la derecha. Con D de nivel i-1.

Luego del primer Skew en t, resulta La Figura 15.32.


t B i

C i-1 A i

D i-1 n i-1

b i-2 F i-1

Figura 15.32. Ascenso por la derecha. Con D de nivel i-1.

No se realizan los skew en el hijo y en el nieto derecho de t. Tampoco se realizan los split.
El siguiente segmento ilustra las acciones para mantener las propiedades en caso de ascenso por
la derecha.

if (t->right->nivel < t->nivel-1) //ascenso por la derecha


{ t->nivel--; //se desciende el nivel
if (t->left->right->nivel == t->nivel) {t->left = lrot(t->left); ok=0;} //Caso b1).
t=rrot(t);
}

15.4. Centinelas y encabezados.

En aplicaciones con estructuras que tienen un inicio y un final, el diseño de las funciones
requiere un tratamiento especial en los bordes. Tradicionalmente puede uniformarse el
tratamiento al inicio introduciendo como primer elemento un nodo denominado encabezado o
header. Así también para finalizar la estructura puede agregarse un nodo denominado centinela
o fondo que facilite las operaciones en la base de la estructura.
Además la introducción de este nodo permite uniformar el tratamiento de funciones que operan
con los descendientes de un nodo, como es el caso de las operaciones skew y split en los árboles
AA.

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
Si se agrega un nodo al cual apuntan los descendientes de todas las hojas, se tiene la Figura
15.33. Nótese que los enlaces, izquierdo y derecho, del nodo centinela apuntan a sí mismo. De
este modo todos los nodos de los primeros niveles tienen descendientes bien definidos y no es
preciso establecer condicionales para construcciones como: p->right y p->right->right.
Si las hojas tuvieran enlaces izquierdos y derechos apuntando a NULL, antes de emplear en una
expresión p->right habría que asegurarse que p no apunte a NULL.

2
1

Figura 15.33. Nodo centinela. De nivel 0.

En otras aplicaciones los enlaces del centinela pueden apuntar al nodo raíz, logrando de este
modo listas circulares. Si los nodos tienen padre, el del centinela puede apuntar a la raíz o a
NULL dependiendo de la condición que se desee simplificar.

Veremos a continuación el uso clásico de un nodo centinela en una operación de búsqueda de un


valor en la estructura, cuyo resultado puede ser o no exitoso. Se carga en el nodo centinela el
valor que desea buscarse, de este modo se asegura una búsqueda exitosa. Discernir si el valor
estaba o no, consiste simplemente en determinar si se lo encontró o no en el nodo centinela.

El siguiente segmento ilustra el diseño, se ha supuesto un nodo centinela apuntado por una
variable externa a la función denominada nil.

//buscar con centinela


pnodo Buscar(data x, pnodo p)
{ if (p==nil) return NULL;
nil->clave=x; //carga valor en nodo centinela.
while ( p->clave != x) if (p->clave> x) p=p->left; else p=p->right;
if(p==nil) return (NULL); else return p;
}

15.5. Tipos de datos.

typedef int data;

typedef struct node {


struct node *left, *right;
int nivel;
data clave;
} nodo, *pnodo;

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 19
typedef pnodo arbol;

//Varibles globales
pnodo nil;
nodo centinela;

void initglobalvariables()
{ nil=&centinela;
nil->nivel = 0; //nivel del centinela, está bajo las hojas.
nil->left = nil;
nil->right = nil;
}

15.6. Creación de nodo.

La inserción se produce en las hojas, y siempre en nivel 1. Además el nuevo nodo tiene sus
descendientes apuntando al centinela.

pnodo getnodo(data valor)


{ pnodo p = (pnodo) malloc(sizeof(nodo));
if (p==NULL) {printf("Error memoria\n"); exit(1);}
else
{ p->clave = valor;
p->left = nil; //apuntan al centinela
p->right = nil;
p->nivel = 1; //hojas en nivel 1.
}
return(p);
}

15.7. Listador de la estructura.

Con fines de depurar las funciones, conviene disponer de una función que muestre en forma de
texto, los valores almacenados en el árbol.

Se efectúa un recorrido en orden, mostrando la clave y el nivel de cada nodo.

void prtnivel(pnodo p)
{ if (p!= nil)
{
prtnivel(p->left);
printf ("%d,%d ", p->clave, p->nivel);
prtnivel(p->right);
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
15.8. Operaciones básicas.

/* rotación derecha */
pnodo rrot(pnodo t)
{ register pnodo temp=t;
t = t->left;
temp->left = t->right;
t->right = temp;
return (t);
}

/* rotación izquierda */
pnodo lrot (pnodo t)
{ register pnodo temp=t;
t = t->right;
temp->right = t->left;
t->left = temp;
t->nivel++;
return(t);
}

pnodo skew (pnodo t)


{ pnodo temp;
if (t->left->nivel== t->nivel ) //no falla, en los niveles inferiores, debido al centinela
{ /* rotación derecha */
temp = t;
t = t->left;
temp->left = t->right;
t->right = temp;
}
return (t);
}

pnodo split (pnodo t)


{ pnodo temp;

if (t->right->right->nivel== t->nivel )
{ /* rotación izquierda */
temp = t;
t = t->right;
temp->right = t->left;
t->left = temp;
t->nivel = t->nivel +1;
}
return(t);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 21
Para evitar la sobrecarga del llamado a funciones, las operaciones se pueden codificar
empleando macros. Debido a que son de varias líneas, se los suele definir empleando un lazo
while y el carácter de continuación de línea. En lugar de usar t=lrot(t); se escribe: lrotm(t);.

#define lrotm(t) do { \
{ temp=t; \
t = t->right; \
temp->right = t->left;\
t->left= temp; \
t->nivel++; \
} \
} while(0)

15.9. Buscar.

El mismo creador de los árboles AA, plantea una forma original de efectuar una búsqueda en
árboles, empleando solamente una comparación. Para visualizar la diferencia se tiene la
búsqueda clásica con dos comparaciones y una asignación en el lazo.

pnodo Buscar2(data x, pnodo p)


{ if (p==nil) return NULL;
while ( p != nil)
{ if (p->clave>x) p=p->left; //primera comparación
else if (p->clave< x) p=p->right; //segunda comparación
else return p; //lo encontró
}
return (NULL);
}

La siguiente tiene una comparación y dos asignaciones en el peor caso, en cada pasada por el
lazo de repetición. Se emplea esta forma en la operación descarte.

pnodo Buscar1(data x, pnodo p)


{ pnodo t=p;
if (p==nil) return NULL;
while ( p != nil)
if (p->clave>x) p=p->left; else {t=p; p=p->right;}
if(t->clave==x) return t;
else return (NULL);
}

15.10. Insertar.

Diseño recursivo. Empleando rotaciones para mantener las propiedades de los árboles AA.

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
pnodo insertar(data x, pnodo t)
{
if (t == nil) {t=getnodo(x); ok=1;}
else {
if (x < t->clave) t->left=insertar(x, t->left);
else if (x> t->clave) t->right=insertar(x, t->right);
else ok=0; //si la clave ya estaba, no se inserta ni se revisan propiedades.

if (ok) //rebalancea si logró insertar


if ((t->left->nivel== t->nivel ) && (t->right->nivel== t->nivel)) {t->nivel++;}
else if (t->left->nivel== t->nivel ) t=rrot(t);
else if (t->right->right->nivel== t->nivel ) t=lrot(t);
}
return (t);
}

15.11. Descartar.

El diseño es más complejo y las operaciones para preservar las propiedades ocupan parte
importante del código; sin embargo son más sencillas que en el caso de árboles coloreados. Las
operaciones se han planteado en términos de rotaciones, en lugar de las de torcer y partir (skew
y split).

pnodo descartar(data x, pnodo t)


{ register pnodo p;
int tnivel;
//register pnodo temp;
if (t != nil)
{ /* 1: Desciende hasta el centinela, manteniendo punteros last y deleted. */
ok=0;
last = t; //last y deleted no son automáticas. Son globales.
if (x < t->clave) t->left=descartar(x, t->left);
else { deleted =t; t->right=descartar(x, t->right);
}
/* 2: Al llegar al centinela, last apunta a la hoja sucesora */
/* deleted apunta al que será descartado, si lo encontró. */
if (t ==last && (deleted != nil) && (x ==deleted->clave))
{ deleted->clave = last->clave; //reemplaza clave del sucesor
deleted = nil;
t=nil; //t = t->right;
free (last); //libera hoja
ok=1;
}
/* 3: Luego del descarte, las invocaciones recursivas continúan aquí.*/
/* Se revisan los nodos por los cuales se descendió */
/* Inicialmente, t apunta al padre del que se descartó */
else

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 23
if (ok && ((t->left->nivel < t->nivel-1) || (t->right->nivel < t->nivel-1)))
{ //printf ("%d, %d \n", t->clave,t->nivel);//muestra ruta en ascenso
tnivel=t->nivel;
t->nivel = t->nivel -1; //se desciende el nivel
//también si tenía horizontal derecho
if (t->right->nivel > t->nivel) t->right->nivel = t->nivel;

if (t->left->nivel== t->nivel ) t=rrot(t); //rrotm(t);


// si queda un horizontal izquierdo
p=t->right;
if (p->left->nivel== p->nivel ) p=rrot(p); //rrotm(p);
// si queda un horizontal izquierdo del hermano derecho
p=p->right;
if (p->left->nivel== p->nivel ) p=rrot(p); //rrotm(p);
// si queda un horizontal izquierdo del nieto

//si se tiene el peor caso se requieren dos split


if (t->right->right->nivel== t->nivel ) t=lrot(t); //lrotm(t);
p=t->right;
if (p->right->right->nivel== p->nivel ) p=lrot(p); //lrotm(p);
if (tnivel==t->nivel) ok=0;
}
}
return (t);
}

15.12. Verificación de las propiedades.

La siguiente función recursiva, efectúa un recorrido en todos los nodos del árbol, revisando si se
cumplen las propiedades. Es útil en la verificación de las funciones.

void check(pnodo p)
{ int k, tipo;
if (p!= nil)
{
check(p->left);
k=0;
if (p->left->nivel== p->nivel ) {tipo=1; k++;} //no se permiten hijos izquierdos horizontales
if (p->right->right->nivel== p->nivel ){tipo=2;k++;}//no pueden existir dos rojos adyacentes

//Se pueden agregar algunos test para verificar la estructura


//las hojas tienen como descendiente al centinela
if ((p->nivel==1) && (p->left!=nil) ) {tipo=3;k++;}
if ((p->nivel==1) && (p->right->nivel==1)&& (p->right->right!=nil)) {tipo=4;k++;}

//hijo izquierdo debe tener nivel menor que su padre


if (p->left->nivel >= p->nivel ) {tipo=5;k++;}

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos

//hijo derecho debe tener nivel menor o igual que su padre


if (p->right->nivel > p->nivel ) {tipo=6;k++;}

//nivel del nieto menor que el del abuelo


if (p->right->right->nivel >= p->nivel ){tipo=7;k++;}
//nodo de nivel mayor que uno debe tener dos hijos
if ( (p->nivel>1) && (p->left==nil) ) {tipo=8;k++;}
if ( (p->nivel>1) && (p->right==nil) ) {tipo=9;k++;}
//Condiciones en rebalance de la operacion descarte
if (p->left->nivel < p->nivel-1) {tipo=10;k++;}
if (p->right->nivel < p->nivel-1) {tipo=11;k++;}

if(k)
{printf("Error=%d %d \n",tipo, p->clave);
prtinorder(p); putchar('\n');
prtinorder(tree); putchar('\n');
prtnivel(tree); putchar('\n');
}
check(p->right);
}
}

15.13. Test de las funciones.

Pueden agregarse inserciones y descartes en órdenes crecientes y decrecientes, lo cual genera


cuatro segmentos de inserciones seguidas de descartes.
Se pueden contar mediante una variable global op, las operaciones realizadas. Agregando op++,
dentro de las funciones que se desee evaluar. La medición del tiempo permite comparar el uso
de macros o la declaración de variables de tipo registro.

#define N 79
arbol tree;
int main(void)
{ int i;
clock_t start, stop; //tipo definido en time.h
int totaltime = 0;
initglobalvariables();
tree=nil;
start = clock();

srand(1);
for(i=1;i<=N;i++)
{ tree=insertar(rand()%1023, tree);
check(tree);
//prtinorder(tree);putchar('\n');
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 25
for(i=1;i<=N;i++)
{ tree=descartar(tree->clave, tree);
check(tree);
//prtinorder(tree); putchar('\n');
}
stop = clock();
totaltime += (stop-start); // Mantiene el tiempo usado por la acción. Aproximadamente.
printf("Tiempo acumulado = %d [ticks]\n", totaltime);
printf("\nOp= %d \n", op);
printf("\nFin test \n");
return (0);
}

Referencias.

Arne Andersson. “Balanced Search Trees Made Simple”. Workshop on Algorithms and Data
Structures, pages 60-71. Springer Verlag, 1993.

A. Andersson. “A note on searching in a binary search tree”. Software-Practice and Experience,


21(10):1125 1128, 1991.

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 15 ............................................................................................................................................1
ÁRBOLES AA. ...........................................................................................................................................1
15.1. PROPIEDADES DE LOS ÁRBOLES AA. ................................................................................................1
15.2. ANÁLISIS DE LA OPERACIÓN INSERCIÓN. ..........................................................................................2
15.2.1. Ejemplos de inserción. .............................................................................................................5
15.2.2. Diseño de las operaciones skew y split. ...................................................................................7
15.2.3. Inserción por la izquierda. .......................................................................................................8
a) B de nivel i. .................................................................................................................................................. 8
b) B de nivel (i-1). ............................................................................................................................................ 9
15.2.4. Inserción por la derecha. .........................................................................................................9
15.3. ANÁLISIS DEL DESCARTE. ...............................................................................................................10
15.3.1. Ascenso por la izquierda. .......................................................................................................11
a1) B de nivel (i+1). ....................................................................................................................................... 11
a11) Con E y F de nivel i.......................................................................................................................... 11
a12) Con E de nivel (i-1) y F de nivel i. .................................................................................................... 12
a13) Con E de nivel (i-1) y F de nivel ( i-1). ............................................................................................. 13
a14) Con E de nivel i y F de nivel ( i-1). .................................................................................................. 14
a2) B de nivel i. .............................................................................................................................................. 14
a21) Con D de nivel i. ............................................................................................................................... 14
a22) Con D de nivel (i-1). ......................................................................................................................... 14
15.3.2. Ascenso por la derecha. .........................................................................................................15
b1) Con D de nivel i. ...................................................................................................................................... 15
b2) Con D de nivel i-1.................................................................................................................................... 16
15.4. CENTINELAS Y ENCABEZADOS. .......................................................................................................17
15.5. TIPOS DE DATOS. ............................................................................................................................18
15.6. CREACIÓN DE NODO. ......................................................................................................................19
15.7. LISTADOR DE LA ESTRUCTURA. ......................................................................................................19
15.8. OPERACIONES BÁSICAS. .................................................................................................................20
15.9. BUSCAR. .........................................................................................................................................21
15.10. INSERTAR. ....................................................................................................................................21
15.11. DESCARTAR..................................................................................................................................22
15.12. VERIFICACIÓN DE LAS PROPIEDADES. ...........................................................................................23
15.13. TEST DE LAS FUNCIONES. .............................................................................................................24
REFERENCIAS. .........................................................................................................................................25
ÍNDICE GENERAL. ....................................................................................................................................26
ÍNDICE DE FIGURAS. ................................................................................................................................27

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles AA. 27
Índice de figuras.

FIGURA 15.1. ÁRBOLES AA, DE HASTA 3 NODOS. ......................................................................................... 1


FIGURA 15.2. INSERCIÓN POR LA IZQUIERDA. SKEW. .................................................................................... 2
FIGURA 15.3. INSERCIÓN POR LA IZQUIERDA. SKEW Y SPLIT EN EL MISMO NIVEL. ........................................ 3
FIGURA 15.4. INSERCIÓN POR LA IZQUIERDA. SKEW Y SPLIT EN NIVELES DE RECURSIÓN DIFERENTES. ......... 3
FIGURA 15.5. INSERCIÓN POR LA DERECHA. .................................................................................................. 4
FIGURA 15.6. FORMAS DE ÁRBOLES AA PARA 4, 5, 6 Y 7 NODOS. ................................................................. 4
FIGURA 15.7. INSERCIÓN DE NODO CON VALOR 15 EN ÁRBOL AA................................................................ 5
FIGURA 15.8. LUEGO DE SPLIT EN NODO CON CLAVE 13. .............................................................................. 5
FIGURA 15.9. LUEGO DE SPLIT EN NODO CON CLAVE 10. .............................................................................. 6
FIGURA 15.10. LUEGO DE SPLIT EN NODO CON CLAVE 4. .............................................................................. 6
FIGURA 15.11. LUEGO DE INSERTAR EL 8, Y LUEGO DE SKEW EN 9 Y ASCENDER HASTA LA RAÍZ. ................. 6
FIGURA 15.12. LUEGO DE INSERTAR EL NODO CON VALOR 7 Y LUEGO DEL SKEW EN 8. ................................ 7
FIGURA 15.13. LUEGO DE SPLIT EN 7, Y LUEGO DEL SKEW EN 10. ................................................................. 7
FIGURA 15.14. INSERCIÓN POR LA IZQUIERDA. B DE NIVEL I. ........................................................................ 8
FIGURA 15.15. INSERCIÓN POR LA IZQUIERDA. CAMBIO DE NIVEL. ............................................................... 8
FIGURA 15.16. INSERCIÓN POR LA IZQUIERDA. B DE NIVEL (I-1). .................................................................. 9
FIGURA 15.17. INSERCIÓN POR LA IZQUIERDA. LUEGO DE SKEW EN T. .......................................................... 9
FIGURA 15.18. INSERCIÓN POR LA DERECHA. B DE NIVEL I-1. ....................................................................... 9
FIGURA 15.19. ASCENSO POR LA IZQUIERDA. E Y F SON DE NIVEL I. ........................................................... 11
FIGURA 15.20. ASCENSO POR LA IZQUIERDA. LUEGO DE TRES OPERACIONES TORCER. ............................... 12
FIGURA 15.21. ASCENSO POR LA IZQUIERDA. LUEGO DE LAS DOS OPERACIONES PARTIR. ........................... 12
FIGURA 15.22. ASCENSO POR LA IZQUIERDA. CON E DE NIVEL (I-1), Y F DE NIVEL I. .................................. 12
FIGURA 15.23. ASCENSO POR LA IZQUIERDA. CON E DE NIVEL (I-1), Y F DE NIVEL I. .................................. 13
FIGURA 15.24. ASCENSO POR LA IZQUIERDA. CON E DE NIVEL (I-1), Y F DE NIVEL I. .................................. 13
FIGURA 15.24A. ASCENSO POR LA IZQUIERDA. CON E DE NIVEL (I-1), Y F DE NIVEL I................................. 13
FIGURA 15.24B. ASCENSO POR LA IZQUIERDA. CON E DE NIVEL (I-1), Y F DE NIVEL (I-1). .......................... 13
FIGURA 15.25. ASCENSO POR LA IZQUIERDA. CON B DE NIVEL I. ................................................................ 14
FIGURA 15.26. ASCENSO POR LA IZQUIERDA. CON B DE NIVEL I. ................................................................ 14
FIGURA 15.27. ASCENSO POR LA DERECHA. CON D DE NIVEL I. .................................................................. 15
FIGURA 15.28. ASCENSO POR LA DERECHA. CON D DE NIVEL I. .................................................................. 16
FIGURA 15.29. ASCENSO POR LA DERECHA. CON B DE NIVEL I. .................................................................. 16
FIGURA 15.30. ASCENSO POR LA DERECHA. CON B DE NIVEL I. .................................................................. 16
FIGURA 15.31. ASCENSO POR LA DERECHA. CON D DE NIVEL I-1................................................................ 17
FIGURA 15.32. ASCENSO POR LA DERECHA. CON D DE NIVEL I-1................................................................ 17
FIGURA 15.33. NODO CENTINELA. DE NIVEL 0............................................................................................ 18

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 16.

Árboles de búsqueda aleatorizados.


Treaps.

Cuando los ítems almacenados en un árbol de búsqueda binario cambian en el tiempo, debido a
que éstos pueden ser insertados o descartados de manera no predecible, no puede asegurarse que
todas las hojas mantengan profundidades similares. Debido a esto se han desarrollado diversos
algoritmos determinísticos para lograr mantener una estructura balanceada: Árboles AVL,
árboles 2-3, árboles coloreados, árboles AA y muchos otros; todos ellos modifican la estructura
basados en pequeñas operaciones locales denominadas rotaciones y en un análisis cuidadoso de
las diferentes y determinadas situaciones que se producen.

Antes se demostró que un árbol generado aleatoriamente tiene una altura esperada que varía
logarítmicamente con respecto al número de nodos almacenados en la estructura, y que tiende a
ser balanceado.

Se han desarrollado algoritmos que introducen aleatoriedad en las operaciones de actualización


y búsqueda en un árbol binario, con el objetivo de mantener el árbol balanceado. La distribución
de probabilidades no proviene de las claves sino de las elecciones aleatorias que realiza el
algoritmo; por esto se denominan algoritmos aleatorizados. Para analizar el costo de un
algoritmo usando probabilidades, es preciso conocer la distribución de las entradas, pero en
muchas situaciones se conoce muy poco o no es posible modelar la distribución de éstas; sin
embargo puede lograrse una conducta aleatoria en el algoritmo.

Más específicamente se denominan aleatorizados a aquellos algoritmos cuyo comportamiento


queda determinado no sólo por sus entradas sino por valores producidos por un generador de
números aleatorios.

Se estudiará uno de estos métodos que consiste en introducir un número aleatorio en cada nodo.
El número aleatorio será la prioridad del nodo. Las claves y las prioridades deben ser diferentes
y pertenecer a un universo totalmente ordenado. En el árbol, las claves están almacenadas según
un árbol binario de búsqueda, y las prioridades según una cola de prioridad. Por esta razón a la
estructura se la denomina treaps que es un acrónimo de trees y heaps.

Si las claves y las prioridades son diferentes para un conjunto determinado de valores de claves
y prioridades el treap es único; y su forma correspondería al árbol binario de búsqueda que se
obtendría al insertar, a partir de un árbol vacío, los ítems en orden de prioridad creciente.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Un treap aleatorio también es el resultado de insertar las claves en orden aleatorio, en un árbol
binario de búsqueda, lo cual puede lograrse si antes de insertar una clave se le asigna una
prioridad generada aleatoriamente.

Lo notable de esta estructura es que está basada en dos de las fundamentales que se estudian en
un curso básico de estructuras de datos: el árbol binario de búsqueda y una cola de prioridad o
heap. Si se tuviera una función de hash que genere un valor aleatorio (la prioridad), a partir de
la clave, no sería necesario almacenar el valor de prioridad en cada nodo, reduciendo el tamaño
del almacenamiento, ya que mediante la función pueden calcularse las prioridades de los nodos
cuando sea necesario disponer de ellas. En esta forma de diseño, a partir de tres estructuras
fundamentales de datos se construye una nueva.

16.1. Análisis de las operaciones.

Si se inserta un nodo con clave determinada, de manera usual, en las hojas de un árbol binario,
se tiene que todas las claves de los nodos cumplen la propiedad de un árbol binario de
búsqueda; sin embargo puede ser que la propiedad del heap, que es tener prioridades de los hijos
mayores que el padre, no se cumpla. Para reestablecer esa propiedad, al nodo recién insertado se
lo hace ascender, mediante rotaciones, mientras el padre tenga prioridad mayor. El proceso se
detiene si se encuentra un padre con prioridad menor o si el nuevo nodo se convierte en la raíz.

El descarte de un nodo se logra invirtiendo las operaciones que se realizan para insertarlo; es
decir, una vez ubicada la clave, se hace descender al nodo, mediante rotaciones con el nodo con
menor prioridad de sus hijos, hasta que el nodo sea una hoja; instancia en que se lo puede
descartar, preservándose las propiedades de los árboles binarios de búsqueda y de los heaps.

La Figura 16.1, ilustra un treap, donde en la parte superior se muestra la clave y en la inferior de
cada nodo la prioridad. El árbol se formó ingresando las claves de acuerdo a un orden creciente
de las prioridades; es decir en orden: 10, 12, 13, 18, 22, 24, 33 y 40. El nodo con menor valor
de prioridad se ubica en la raíz.
9
10

4 15
12 13

2 5 12 20
22 24 18 40

7
33

Figura 16.1. Treap.

Si se inserta un nodo con clave 8 y prioridad 11, de acuerdo al algoritmo de inserción debe
agregárselo a la derecha del nodo con clave 7. Esto se muestra a la izquierda de la Figura 16.2.
Como no se cumple la propiedad que la prioridad del nodo 8 sea mayor que la de su padre,

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 3
puede reestablecerse esta condición mediante la rotación a la izquierda en el nodo padre, el 7 en
este caso. Lo cual se muestra a la derecha de la Figura 16.2.
9 9
10 10

4 15 4 15
12 13 12 13

2 5 12 20 2 5 12 20
22 24 18 40 22 24 18 40

7 8
33 11

8 7
11 33

Figura 16.2. Inserción de clave 8 en Treap.

Del nodo con clave 8 hacia abajo se tiene un heap, pero no hacia arriba. Por esto es preciso
efectuar otra rotación a la izquierda en el nodo padre del 8, el nodo con clave 5 en la Figura 16.2
a la derecha. Luego de realizada, se obtiene el diagrama a la izquierda de la Figura 16.3.
Finalmente volviendo a rotar a la izquierda en el nodo con clave 4, padre del nodo con clave 8,
se logra el treap a la derecha de la Figura 16.3.
9 9
10 10

4 15 8 15
12 13 11 13

2 8 12 20 4 12 20
22 11 18 40 12 18 40

5 2 5
24 22 24

7 7
33 33

Figura 16.3. Inserción de clave 8 en Treap.

El descarte del nodo con clave 8, sigue el proceso inverso a su inserción, se lo hace descender,
mediante rotaciones con el nodo hijo que tenga menor prioridad, hasta llegar a ser una hoja.

La operación descarte se ve simplificada si los enlaces derecho e izquierdo de las hojas apuntan
a un nodo de fondo o centinela que tenga un valor mayor de prioridad que los valores de
prioridad que puedan tener los nodos.
Entonces los condicionales, que deberían efectuarse cerca de las hojas, para asegurar la
existencia de los valores p->left->prioridad o p->right->prioridad no son necesarios.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

La operación de inserción debe descender hasta las hojas y luego ascender preservando el heap.
Esto puede lograrse de manera simple mediante un diseño recursivo, o empleando un stack, para
almacenar la ruta de descenso; una vez ubicada la posición para insertar, al retornar de las
invocaciones recursivas, se dispone del nodo padre, en el cual deben realizarse las rotaciones
que corresponda.

La operación de descarte, debe descender hasta encontrar el nodo que se busca para descartar, y
luego seguir descendiendo hasta las hojas, manteniendo el heap. Para lograrlo, de manera
iterativa, es necesario mantener en el descenso un puntero al padre del nodo que está
descendiendo para ser descartado, además de la dirección de descenso; ya que es preciso
mantener los enlaces. Lo anterior también puede simplificarse si los nodos contienen un puntero
al padre, pero esto aumenta el tamaño del almacenamiento del nodo.

Si la función que genera aleatoriamente las prioridades, produjera algunos números con iguales
valores, los algoritmos de inserción y descarte se realizan normalmente. En la inserción no se
realizan rotaciones adicionales, y quedarían elementos con prioridades iguales adyacentes,
alargando la altura del último nodo ingresado; pero si esto ocurre con baja probabilidad puede
tolerarse. En el descarte, si la prioridad del nodo es igual a la de uno de sus hijos, disminuye el
largo de secuencias de igual prioridad y mejora el balance; por otro lado si los hijos tienen
iguales prioridades, tiende a aumentar la altura del último nodo. Por lo tanto es perfectamente
tolerable aceptar un generador aleatorio simple que demande bajo número de operaciones y que
produzca un limitado número de colisiones en las prioridades.

En un árbol binario de búsqueda, si la raíz es la clave menor, entonces no hay subárbol


izquierdo; así también si la raíz es el nodo con la mayor clave del conjunto no habrá subárbol
derecho. Obviamente en estos casos se tiene un árbol desbalanceado. Pero si los valores de
prioridad son estadísticamente independientes de los valores de las claves es poco probable que
la raíz, que tiene prioridad mínima, sea coincidente con las claves de valores máximo o mínimo.
De este modo el número de nodos ubicados a la derecha pueden ser similares al número de
nodos ubicados a la izquierda; lo cual genera una estructura con cierto grado de balance.

El algoritmo aleatorizado fuerza una distribución de probabilidades en las entradas asegurando


que ninguna combinación particular de éstas producirá un peor caso de comportamiento. En
árboles de búsqueda, las elecciones aleatorias de las prioridades impiden que largas secuencias
de claves en orden ascendente o descendente provoquen alargues de algunas ramas del árbol.

Los autores del algoritmo diseñan recursivamente ambas operaciones de actualización. También
demuestran que el valor esperado de la altura de un treap, con n nodos, es O(log(n)) , y que el
valor esperado de las rotaciones necesarias para mantener el treap es menor que 2; es decir un
número constante.

También muestran que una función de hash adecuada, para generar las prioridades, a partir de
una clave i, es un polinomio de grado 4.

q(i) = ((((c4*i + c3)*i+ c2)*i +c1)*i+c0) mod U

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 5

Donde las constantes deben ser números aleatorios entre 0 y (U-1). Con U un número primo
mayor que n3 , con n elementos en el árbol; lo cual logra asegurar que dos elementos pueden
tener prioridades iguales con una probabilidad menor que 1/n.

16.2. Complejidad.

16.2.1. Conceptos básicos de probabilidad.

Una variable aleatoria indicadora I (e) asociada con el evento e de un espacio muestreal S, está
definida según:
I (e) =1 si ocurre el evento e, y 0 si el evento e no ocurre.

Se define el valor esperado como el promedio de los valores que la variable puede tomar. En un
espacio discreto, es la suma ponderada, de acuerdo a su probabilidad, de todos los valores que
puede tomar la variable X:
E[ X ] x Pr( X x)
x

El valor esperado de la variable aleatoria indicadora Ve I (e) se calcula de acuerdo a su


definición, y considerando que sólo puede tomar dos valores, según:

E[Ve ] E[ I (e)] 1 Pr(e) 0 Pr(e ) Pr(e) Pr( I 1)

Donde e simboliza el complemento de e en el espacio S; es decir: S-e.

Las variables aleatorias indicadoras son útiles para analizar situaciones en las que se realizan
repetidos ensayos aleatorios.
Si se emplea la variable indicadora Vi para asociarla con la producción del evento e en el
ensayo i-ésimo. Entonces la variable aleatoria V que indica el número total de eventos e en los n
ensayos, queda descrita por:
i n
V Vi
i 1
Y el valor esperado de V, se logra tomando la expectación en ambos lados de la expresión
anterior:
i n
E[V ] E[ Vi ]
i 1
Pero el operador expectación es lineal, y se tendrá que el valor esperado de la suma de dos
variables aleatorias es la suma de sus expectaciones, esto aún si las variables no son
independientes. Para el caso de la sumatoria anterior, se tiene:
i n
E[V ] E[Vi ]
i 1

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
Pero el valor esperado de una variable aleatoria indicadora es simplemente su probabilidad,
entonces se tiene:
i n
E[V ] Pr(Vi 1)
i 1
Este último resultado es útil en el cálculo de complejidades en algoritmos aleatorizados, como
se verá a continuación.

16.2.2. Altura.

Sea xk el nodo que tiene la k-ésima clave menor en el treap; y ai ,k una función que vale 1 si xi es
ancestro propio de xk , y 0 en caso contrario.
Entonces el número de nodos del trayecto de xk a la raíz, está dada por:
i n
h( xk ) ai ,k
i 1

Que es la suma de los ancestros propios de xk .


El valor esperado para la altura del nodo xk , equivale a calcular la probabilidad de que un nodo i
sea un ancestro propio del nodo k.
i n
h E (h( xk )) Pr(ai ,k 1)
i 1
Para efectuar el cálculo definiremos un subconjunto ordenado de nodos del treap:

X (i, k ) {xi , xi 1 ,.., xk } si i < k;


X (i, k ) {xk , xk 1 ,.., xi } si i > k;

El subconjunto anterior contendrá exactamente: | k i | 1 nodos.

La Figura 16.4, ilustra la situación de los nodos del conjunto. Sin perder generalidad se ha
supuesto que las claves están formadas por números entre 1 y n. El diagrama a la derecha
muestra un caso en que i < k , y el izquierdo un caso en que i > k.
i i
i+1 i+1

k k

Figura 16.4. Subconjunto de nodos en un treap.

Debido a la construcción del treap, puede asegurarse que xi es un ancestro propio de xk si y sólo
si xi tiene la menor prioridad entre todos los nodos en el subconjunto X (i, k ) .

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 7
Demostración:
a) Si xi es la raíz, entonces es un ancestro de xk , y por ser la raíz tiene la menor prioridad que
cualquier nodo del treap, y por lo tanto cumple con tener la menor prioridad del conjunto
X (i, k ) .
b) Si xk es la raíz, entonces xi no es un ancestro de xk ; y xi tampoco tiene la menor prioridad de
los elementos de X (i, k ) , ya que en este caso la menor prioridad la tiene xk .
c) Si otro nodo, x j es la raíz; entonces xi y xk están en diferentes subárboles, y por lo tanto uno
no puede ser ancestro del otro. Se cumple además que i < j < k o bien que i > j > k; es decir x j
pertenece al subconjunto X (i, k ) y tiene la prioridad menor del conjunto; por lo que xi
obviamente no tiene la prioridad menor del conjunto.
j j
i
k
i
k

Figura 16.5. Caso c). Nodo j es la raíz.

d) Si xi y xk no son la raíz y están en el mismo subárbol, se desciende hasta que uno de ellos
sea la raíz o hasta encontrar una raíz x j que los deje en subárboles diferentes y se vuelven
(inductivamente) a aplicar las consideraciones realizadas en a), b) y c).

Como cada nodo del subconjunto X (i, k ) tiene una prioridad elegida en forma independiente,
como una variable aleatoria con una distribución continua, que i tenga la prioridad menor del
conjunto ocurre con probabilidad:

1
Pr(ai ,k 1)
|k i| 1
Es decir, es uno entre todos los nodos del conjunto.

El resultado anterior permite calcular el valor esperado para la altura, definida como el número
de nodos del trayecto de xk hasta la raíz, según:

i n i k 1 i n
1 1
h Pr(ai ,k 1)
i 1 i 1 k i 1 i k 1i k 1

Donde la primera sumatoria es para i < k y la segunda para i > k.

Efectuando (k-i+1) = j en la primera sumatoria y (i-k+1) = j en la segunda, se obtiene:

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
j k j n k 1
1 1
h
j 2 j j 2 j

Iniciándolas desde 1, y reconociendo la serie armónica se logra:

j k j n k 1
1 1
h 2 H (k ) H (n k 1) 2
j 1 j j 1 j
Con H (n) la serie armónica:
i n
1
H ( n) ln(n)
i 1 i
Con =0,577. Para n >>1, se tiene:

h ln(n) O(log(n))

Debemos suponer que k >1 y n > k, es decir un treap que contenga un número mayor de nodos
que la k-ésima clave menor del treap. Con estas condiciones puede realizarse, mediante Maple,
las siguientes gráficas, para ilustrar la complejidad de la altura de un treap.

> h:=sum(1/(k-i+1),i=1..k-1)+sum(1/(i-k+1),i=k+1..n);
> k:=30;
> plot([1.1*ln(n)/ln(2),h,0.8*ln(n)/ln(2)],
n=k..100*k,color=[red,black,blue],thickness=2);

Se han elegido constantes de 1,1 y 0,8 para acotar por arriba y por abajo a la función h.
Los diagramas, para la 30ava clave menor del treap, se muestran en la Figura 16.6.

Figura 16.6. Valor esperado para la altura de un nodo con valor pequeño de clave.

Las gráficas muestran que la altura tiene complejidad: h (log( n))

Entonces la complejidad temporal de las operaciones de búsqueda, inserción y descarte en un


treap, tienen una cota esperada que varía logarítmicamente con el número de nodos.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 9
16.2.3. Rotaciones.

Puede determinarse el largo del trayecto formado por los descendientes izquierdos de la raíz
(que se define como espina o columna vertebral izquierda). Si suponemos un treap formado por
n claves y consideramos, sin perder generalidad, que las claves almacenadas en el árbol son los
números de 1 a n, podemos definir el indicador X i ,k con valor uno, si el nodo con clave i está en
la espina izquierda del subárbol con raíz k; y cero en caso contrario.

En un treap debe cumplirse que k > i y la prioridad del nodo con clave k debe ser menor que la
prioridad del nodo con clave i. Además para cada nodo con clave z, con i < z < k, debe
cumplirse que la prioridad de z debe ser menor que la prioridad del nodo con clave i.
k

Figura 16.7. Espina izquierda con raíz k.

La probabilidad de que el indicador X i ,k sea uno, puede calcularse según:

(k i 1)! 1
Pr[ X i ,k 1]
(k i 1)! (k i)(k i 1)

El numerador contabiliza el número de permutaciones que pueden escribirse con las claves
desde i hasta k, es decir con (k-i+1) claves. De todas las anteriores, aquellas que comienzan en k
y terminan en i, son las que pueden generarse con las permutaciones de las cifras contenidas
entre ambas; es decir con (k-i-1) claves.

Si para cada permutación se ingresan las claves en el orden de la permutación se producirán


diferentes árboles binarios; el numerador describe las diferentes espinas izquierdas que pueden
formarse; todas aquellas que tengan por raíz k, y cuyo último nodo de la espina sea i.

El valor esperado para el largo de la espina izquierda, es la suma de los nodos que están
presentes en la espina:
i k 1 i k 1
1
E(I ) Pr[ X i ,k 1]
i 1 i 1 (k i)(k i 1)

Efectuando el cambio de variable (k-i) por j en la sumatoria anterior, se obtiene:

j k 1
1 1
E(I ) 1
j 1 j ( j 1) k

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
El resultado de la sumatoria puede demostrarse por inducción.
Se tiene la propiedad:
j k 1
1 1
P(k ) 1
j 1 j ( j 1) k
Se cumple para k=2
1 1
P(2) 1
12 2
Entonces se tiene:
j ( k 1) 1 j k 1
1 1 1 1
P(k 1) P(k )
j 1 j ( j 1) j 1 j ( j 1) k (k 1) k ( k 1)

1 1 1
P(k 1) 1 1
k k (k 1) k 1

Demostrando que la propiedad se cumple para todo k.

El valor esperado del largo de la espina derecha, se realiza de igual forma, pero sumando desde
1 hasta (n-k).
j n k
1 1
E ( D) 1
j 1 j ( j 1) n k 1

Para E(D) puede considerarse E(I) y realizar el cálculo por simetría.

Si xk es la raíz, la espina izquierda se calcula sumando Pr[ X i ,k 1] desde i igual a 1 a (k-1); para
la espina derecha pueden cambiarse los índices para sumar desde i igual 1 hasta (n-k), según se
muestra en la Figura 16.8. A la derecha se ilustra con i=5, k=8 y n=11.
k 8

k-1 k+1 n-k 7 9 3


6 10
i n 1 5 11 1

Figura 16.8. Simetría en el cálculo de espina derecha, a partir de la espina izquierda.

El número de rotaciones necesarias para insertar una determinada clave es el mismo que el
número de rotaciones para descartarla. Para descartar un clave se realizan rotaciones que la
hacen descender; en cada rotación a la izquierda la espina derecha disminuye en uno, y en cada
rotación a la derecha la espina izquierda disminuye en uno. Al llegar la clave a la posición de
una hoja, ambas espinas son cero. Entonces el número de rotaciones para descartar una clave es
la suma de las espinas derecha e izquierda de esa clave, antes de descartarla.

Para la clave k, se tiene:

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 11
1 1
rotaciones E ( D) E ( I ) 2 2
k n k 1

Expresión que es menor que 2 para: n > k > 0.

Lo que implica que el número de rotaciones en las operaciones de inserción y descarte en


promedio son menores que 2. Es decir, para mantener el balance por medios aleatorios, se
agrega una complejidad constante a las funciones de inserción y descarte.

Los siguientes comandos Maple, muestran las gráficas del número de rotaciones para tres
valores de k.
> rot:=2-1/k-1/(n-k+1);
> subs(k=1,rot),subs(k=n/2,rot),subs(k=n,rot);
1 2 1 1
1 ,2 ,1
n n 1 n
n 1
2
> plot([subs(k=1,rot),subs(k=n/2,rot),subs(k=n,rot)],n=1..20,
color=[red,black,blue],thickness=2);

Para k=n/2, el número de rotaciones es menor que 2, lo que se muestra en la Figura 16.9.

Figura 16.9. Número de rotaciones.

16.3. Estructura de datos.

Se emplea un tipo especial para la clave, para facilitar los cambios en caso que la clave no sea
entera.
typedef int data;

typedef struct node {


struct node *left, *right;
unsigned int prioridad;
data clave;
} nodo, *pnodo;

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos

typedef pnodo arbol;

Si se emplea un centinela en la base del árbol.


pnodo nil;
nodo centinela;

void initglobalvariables()
{ nil=&centinela;
nil->prioridad = UINT_MAX; //
nil->left = nil; nil->right = nil;
}

16.4. Creación de nodo.

pnodo getnodo(data valor)


{ pnodo p = (pnodo) malloc(sizeof(nodo));
if (p==NULL) {printf("Error memoria\n"); exit(1);}
else
{ p->clave = valor;
p->left = nil; p->right = nil; //apuntan al centinela
p->prioridad =q(valor); //se crea con determinada prioridad. //rand();
}
return(p);
}

Puede no almacenarse la prioridad en el nodo. Cuando las operaciones de inserción y descarte la


requieran se la calcula evaluando con la función de hash: q(p->clave).

Si la clave no es numérica puede tomarse q(&p), para generar la prioridad del nodo.

16.5. Función de hash. Generador aleatorio de prioridades.

Según los autores del algoritmo basta un polinomio de grado 4 en actualizaciones.


Los coeficientes se pueden calcular con el generador aleatorio rand().
#define U 20117
#define c4 397
#define c3 965
#define c2 140
#define c1 730
#define c0 498
unsigned int q(int i)
{ return ( (((c4*i + c3)*i+ c2)*i +c1)*i+c0)%U; }

Si se disminuye U, se tendrán algunas colisiones, pero que no degradan el funcionamiento de las


operaciones. Aumenta levemente el número de rotaciones necesarias para reestablecer el
balance.

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 13
16.6. Rotaciones

Las rotaciones se implementan como macros. Se agregan globales para contar las rotaciones, en
el momento de la depuración, luego pueden eliminarse los contadores.
int opl=0;
int opr=0;

#define rrotm(t) do { \
{ temp=t; \
t = t->left; \
temp->left = t->right; \
t->right = temp; \
opr++; \
} \
} while(0)

#define lrotm(t) do { \
{ temp=t; \
t = t->right; \
temp->right = t->left;\
t->left= temp; \
opl++; \
} \
} while(0)

16.7. Insertar.

pnodo insertar(data x, pnodo t) //Diseño recursivo.


{ pnodo temp;
if (t == nil) {t=getnodo(x); } //inserta en hojas
else
{
if (x < t->clave)
{t->left=insertar(x, t->left);
//en el ascenso mantiene propiedades de heap
//si son prioridades iguales no rota
if ( t->left->prioridad < t->prioridad) rrotm(t); //t=rrot(t);
}
else if (x> t->clave)
{t->right=insertar(x, t->right);
if (t->right->prioridad < t->prioridad) lrotm(t); //t=lrot(t);
}
//si la clave ya estaba, no se inserta ni revisa propiedades.
}
return (t);
}

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
16.8. Descartar.

Diseño iterativo. Se pasa la raíz del árbol por referencia.


La variable dir indica la vía por la que se desciende, y pp apunta al padre del nodo actual.
Se ha definido como macro las acciones necesarias para ajustar el padre, luego de las rotaciones,
de acuerdo a la dirección de descenso. Estas operaciones podrían agregarse a los macros de las
rotaciones, sin embargo se las mantiene aparte para mantener el concepto puro de rotación.

#define raiz 2
#define izq 1
#define der 0
#define AjustaPadre(p) if (dir==der) pp->right=(p); else if(dir==izq) pp->left=(p);else if(dir==raiz)*t=(p);

void descartar(data x, arbol *t)


{ pnodo p=*t, pp, temp;
int dir=raiz;
if (p!=nil)
{ while(p!=nil && p->clave!=x) //buscar con padre y dirección de descenso.
{ pp=p;
if(p->clave > x) {dir=izq; p=p->left;}
else {dir=der; p=p->right;}
}
if(p!=nil && p->clave==x) //lo encontró
{ //printf(" %d-> %d", pp->clave, p->clave); putchar('\n');
while(p->left!=nil || p->right!=nil) //no haya llegado a una hoja
{ if (p->left->prioridad < p->right->prioridad)
{ rrotm(p); //p=rrot(p);
AjustaPadre(p);
pp=p; //nuevo padre
dir=der; p=p->right; //desciende por la derecha
}
else //especular
{ lrotm(p); //p=lrot(p);
AjustaPadre(p);
pp=p; //nuevo padre
dir=izq; p=p->left; //desciende por la izquierda
}
}
//p es hoja
AjustaPadre(nil);
free(p); //se puede retornar p, y liberar fuera de la operación descarte.
}
//si no lo encontró no hace nada
}
//si árbol vacío no hace nada
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 15
16.9. Operaciones útiles en depuración.

La función espinas cuenta los nodos de las espinas derecha e izquierda.

int espina=0;
int espinas(pnodo t)
{ int n=0;
pnodo p;
if (t!=NULL)
if (t!=nil )
{ p=t->left; //cuenta descendientes de t en espina izquierda
while (p!=nil) { n++; p=p->left;}
p=t->right; //cuenta descendientes de t en espina derecha
while (p!=nil) { n++; p=p->right;}
}
return n;
}

La función check efectúa un recorrido en el árbol, verificando las propiedades del treap en cada
nodo.

void check(pnodo p)
{ int k, tipo;
if (p!= nil)
{ check(p->left);
k=0;
//Hay error si prioridad del padre mayor o igual que la del hijo izq
if (p->left->prioridad < p->prioridad ) {tipo=1; k++;}
if (p->left->prioridad == p->prioridad ) {tipo=2; k++;} //warning
//Hay error si prioridad del padre mayor que la del hijo der
if (p->right->prioridad < p->prioridad ){tipo=3;k++;}
if (p->right->prioridad == p->prioridad ){tipo=4;k++;}//warning

//Se pueden agregar algunos test para verificar la estructura de árbol de búsqueda
//hijo izquierdo debe tener clave menor que su padre
if(p->left !=nil) if (p->left->clave >= p->clave ) {tipo=5; k++;}
//hijo derecho debe tener clave mayor que su padre
if(p->right !=nil) if(p->right->clave <= p->clave ) {tipo=6; k++;}

if(k)
{ if(tipo==2 || tipo==4) printf("Warning=%d %d \n", tipo, p->clave);
else printf("Error=%d %d \n", tipo, p->clave);
// prtprioridades(tree); putchar('\n');
}
check(p->right);
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

void prtprioridades(pnodo p) //Imprime en orden las claves y sus prioridades.


{ if (p!= nil)
{ prtnivel(p->left);
printf ("%d,%u ", p->clave, p->prioridad);
prtnivel(p->right);
}
}

16.10. Test de las funciones.

#define N …
int instrucciones=0;
int main(void)
{ int i;
clock_t start, stop; //tipo definido en time.h
int totaltime = 0;
initglobalvariables();
tree=nil;
start = clock();

srand(1);
for(i=1; i<=N; i++)
{ int aleatorio=rand()%1023;
tree=insertar(aleatorio, tree);
check(tree);
espina+=espinas(Buscar(aleatorio, tree));
//prtinorder(tree); putchar('\n');
}
instrucciones+=N;

for(i=1; i<=N; i++)


{ espina+=espinas(Buscar(tree->clave, tree));
descartar(tree->clave, &tree);
check(tree);
//prtinorder(tree);putchar('\n');
}
instrucciones+=N;
stop = clock();
totaltime += (stop-start); // Mantiene el tiempo usado por la acción. Aproximadamente.
printf("Tiempo acumulado = %d [ticks]\n", totaltime);
printf("\nOpl=%d Opr=%d Op=%d\n",opl, opr, opl+opr);
printf("\nEspinas= %d \n", espina);
printf("Rotaciones promedio= %f\n ", (float)(opl+opr) /(float)instrucciones );
printf("\nFin test \n");
return (0);
}

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 17
Ejercicios propuestos.

E16.1.

A veces es deseable partir un conjunto de items X en dos conjuntos. Uno X1 con las claves de X
que son menores que a; y otro X2 con las claves mayores que a. O bien es necesario unir dos
conjuntos X1 y X2, cuando se asume que las claves del conjunto X1 son menores que las claves
del conjunto X2.

Estas operaciones son fáciles de implementar con las operaciones de inserción y descarte en un
treap. Para partir (split) un treap que almacena X, basta insertar la clave a, con prioridad
mínima, lo cual lleva dicho ítem a la raíz; dejando X1 como el subárbol izquierdo y X2 como el
subárbol derecho. Para unir o mezclar (join) se forma un treap con una raíz con prioridad
máxima, y con subárbol izquierdo el treap formado por X1, y con subárbol derecho el treap
formado por X2; luego se descarta la raíz.

Diseñar las funciones:


void split(arbol x, data a, pnodo * x1, pnodo *x2); Que escribe por referencia los árboles X1 y
X2.

pnodo join(pnodo x1, pnodo x2); que retorna x.

E16.2.

Modificar la función de inserción cuando se pasa la dirección del nodo antecesor o sucesor de la
clave que será insertada.
Modificar la función de descarte cuando se pasa la dirección del nodo será descartado.

E16.3.

Describir un método de ordenamiento de la claves de un conjunto X, empleando un treap y sus


funciones. Evaluar el costo.

E16.4.

Modificar la estructura de datos, suprimiendo el campo prioridad. Rediseñar las operaciones de


inserción y descarte en estas condiciones. Evaluar la necesidad de un nodo centinela y si éste
simplifica o no la codificación. Para los descendientes de las hojas puede asumir que su
prioridad es UINT_MAX.

Referencias.

Raimund Seidel, Cecilia Aragon. “Randomized search trees”. Algorithmica 16:464-497, 1996.

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 16. ...........................................................................................................................................1


ÁRBOLES DE BÚSQUEDA ALEATORIZADOS. TREAPS. ...............................................................1
16.1. ANÁLISIS DE LAS OPERACIONES. ......................................................................................................2
16.2. COMPLEJIDAD. .................................................................................................................................5
16.2.1. Conceptos básicos de probabilidad. ........................................................................................5
16.2.2. Altura. ......................................................................................................................................6
16.2.3. Rotaciones. ...............................................................................................................................9
16.3. ESTRUCTURA DE DATOS. ................................................................................................................11
16.4. CREACIÓN DE NODO. ......................................................................................................................12
16.5. FUNCIÓN DE HASH. GENERADOR ALEATORIO DE PRIORIDADES. .....................................................12
16.6. ROTACIONES ..................................................................................................................................13
16.7. INSERTAR. ......................................................................................................................................13
16.8. DESCARTAR. ..................................................................................................................................14
16.9. OPERACIONES ÚTILES EN DEPURACIÓN. .........................................................................................15
16.10. TEST DE LAS FUNCIONES. .............................................................................................................16
EJERCICIOS PROPUESTOS. ........................................................................................................................17
E16.1. .................................................................................................................................................17
E16.2. .................................................................................................................................................17
E16.3. .................................................................................................................................................17
E16.4. .................................................................................................................................................17
REFERENCIAS. .........................................................................................................................................17
ÍNDICE GENERAL. ....................................................................................................................................18
ÍNDICE DE FIGURAS. ................................................................................................................................19

Profesor Leopoldo Silva Bijit 26-05-2008


Árboles de búsqueda aleatorizados. Treaps. 19
Índice de figuras.

FIGURA 16.1. TREAP. .................................................................................................................................... 2


FIGURA 16.2. INSERCIÓN DE CLAVE 8 EN TREAP. .......................................................................................... 3
FIGURA 16.3. INSERCIÓN DE CLAVE 8 EN TREAP. .......................................................................................... 3
FIGURA 16.4. SUBCONJUNTO DE NODOS EN UN TREAP. ................................................................................. 6
FIGURA 16.5. CASO C). NODO J ES LA RAÍZ. .................................................................................................. 7
FIGURA 16.6. VALOR ESPERADO PARA LA ALTURA DE UN NODO CON VALOR PEQUEÑO DE CLAVE. .............. 8
FIGURA 16.7. ESPINA IZQUIERDA CON RAÍZ K. .............................................................................................. 9
FIGURA 16.8. SIMETRÍA EN EL CÁLCULO DE ESPINA DERECHA, A PARTIR DE LA ESPINA IZQUIERDA. ........... 10
FIGURA 16.9. NÚMERO DE ROTACIONES. .................................................................................................... 11

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 17.

B-Trees.

17.1 Definición de B-Trees de grado t.

Un árbol T es un B-Tree de grado t si:


a) Todas las hojas de T tienen igual profundidad.
b) Todos los nodos tienen a lo menos (t-1) claves almacenadas, excepto la raíz.
c) Todos los nodos tienen a lo más (2t-1) claves almacenadas.
d) La raíz tiene a lo menos una clave.
e) Un nodo con (2t-1) claves tiene (2t) hijos.

17.2. Cálculo de la altura de un B-Tree.

Si n 1 y t 2 , un B-Tree con n claves tiene altura h acotada según:

n 1
log 2t (n 1) 1 h logt ( )
2
Demostración.
En el caso de que todos los nodos, incluida la raíz tengan el menor número de nodos, se tiene
que la raíz tiene dos hijos; un nodo con (t-1) claves tiene t hijos y t 2 nietos. La Figura 17.1,
ilustra el crecimiento del B-Tree con t=4. El menor número de claves que puede tener la raíz es
una.

1
2
3

Figura 17.1 B-tree con (t-1) claves en un nodo. Cada nodo tiene t hijos.

La siguiente serie geométrica, genera el número de nodos en uno de los subárboles de la raíz. En
el primer nivel hay un nodo, en el segundo se tienen t nodos, y en el tercero t 2 ; por inducción se
tendrán t h 1 en el nivel h.
k h
th 1
tk 1
1 t t 2 ... t h 1

k 1 t 1

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
Como la raíz tiene una clave y dos descendientes; y cada nodo tiene a lo menos (t-1) claves, el
número total n de claves almacenadas en los nodos, incluida la raíz, en un B-Tree de altura h
será:

k h
k 1 th 1
n 1 2(t 1) t 1 2(t 1)( ) 2t h 1
k 1 t 1

Sacando logaritmo en base t, se obtiene:


n 1
h logt ( )
2
Para acotar h por abajo:
Si los nodos tienen a lo más (2t-1) claves, tendrán 2t hijos.
En la raíz se tendrán (2t-1) claves, y ésta tendrá 2t hijos, cada uno con (2t-1) claves. Se tienen
(2t ) 2 nodos nietos, en el nivel 2 del B-Tree. Finalmente en el nivel h, se tendrán: (2t ) h nodos.

Figura 17.2 B-tree con máximo número de claves en cada nodo, con t=4.

La suma total de nodos del B-Tree, en este caso, puede expresarse según:

k h 1
(2t ) h 1 1
(2t ) k 1
1 2t (2t ) 2 ... (2t ) h
k 1 2t 1

Entonces el número total de claves almacenadas en un B-Tree de altura h, debe ser menor que:

k h 1
(2t ) h 1 1
n (2t 1) (2t ) k 1
(2t 1)( ) (2t ) h 1
1
k 1 2t 1

Sacando logaritmo en base 2t, se obtiene la cota inferior de h.

log 2t (n 1) 1 h

La Figura 17.3 muestra el acotamiento de la altura de un B-Tree de grado t=4.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 3

Binario balanceado

B-Tree

Figura 17.3 Altura de B-tree con t=4.

El número de total de claves en un B-Tree de grado t, con altura h está en el siguiente intervalo.

(2t ) h 1
1 n 2t h 1

Figura 17.4 Claves máximas y mínimas almacenadas en B-tree con t=4.

17.3. Requerimientos de espacio y relaciones de ordenamiento de las claves.

Los requerimientos de espacio para un nodo dependen de t. Para t=2, se requiere un


nodo que pueda almacenar 3 claves y que tenga espacio para 4 punteros. Si t=4, se
requiere nodo con espacio para 7 claves y espacio para 8 punteros.

El esquema de la Figura 17.5, muestra que si se tienen 3 claves, el nodo debe contener el
número de claves más uno, de tal forma que puedan definirse donde se almacenaran las claves
menores y mayores que cada una de las claves del nodo.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

a0 a1 a2
p0 p3
p1 p2

b0 b1 b2 c0 c1 c2 d0 d1 d2 e0 e1 e2

Figura 17.5 Esquema de B-tree con t=2.

El ordenamiento de las claves, en los nodos, en forma similar a un árbol binario de búsqueda,
cumple las relaciones, para el caso de la Figura 17.5:

e2 e1 e0 a2 d2 d1 d0 a1 c2 c1 c0 a0 b2 b1 b0

Se tiene que p0 apunta a descendientes con valores de claves menores que a0 ; y p1 apunta a
descendientes con valores de claves menores que a1 , y mayores que a0 .

El requerimiento del mínimo número de claves almacenadas en un nodo, se aclarará cuando se


analicen las operaciones de inserción y descarte.

17.4. Complejidad en la búsqueda.

Para buscar una clave en un B-Tree de grado t, el peor caso es cuando se tienen (t-1) claves en
cada nodo, ya que en este caso se tiene la mayor altura. Si buscamos la clave en un nodo,
mediante una búsqueda binaria, aprovechando que el arreglo de claves está ordenado, y luego
repetimos esta búsqueda en cada uno de los nodos de la ruta de descenso, que es el peor caso en
búsqueda, tendremos:

T ( n) h TBB (t 1)

Una búsqueda binaria entre n elementos tiene una complejidad:

TBB (n) (log 2 (n) 1)

n 1
La altura en peor caso es: h logt ( )
2

Reemplazando, se obtiene:
n 1
T (n) logt ( ) (log 2 (t 1) 1)
2

Puede efectuarse la siguiente aproximación, para t mayor que 100:

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 5
log 2 (t 1) 1 log 2 (t )

Y cambiando el logaritmo de base t a base 2, se tiene:


n 1
log 2 ( )
n 1 2
log t ( )
2 log 2 (t )

Lo que finalmente permite obtener la complejidad de buscar una clave en un B-Tree de grado t:

n 1
T (n) log 2 ( ) (log(n))
2
Que muestra que la búsqueda de una clave, en peor caso, en un B-Tree, tiene complejidad
logarítmica.

17.5 Análisis de la operación inserción.

17.5.1 Ejemplo de inserción en un 2-4-Tree.

Asumiremos un nodo con 4 claves máximo y 2 claves mínimo. Insertaremos las claves en forma
ascendente a partir del número 1, lo cual es el peor caso para un árbol binario de búsqueda. Esta
estructura es una variación del B-Tree de grado t; en el caso del ejemplo se denomina 2-4-Tree.

Luego de crear el nodo raíz, se pueden agregar 4 claves. Cuando se agrega el número 5, se
produce un rebalse del nodo raíz; y es preciso dividir las claves del nodo raíz en dos nodos
descendientes, de esta manera el B-Tree crece un nivel. La altura crece por rebalse de las hojas.
Se activan dos punteros en el nodo raíz, como se muestra al centro de la Figura 17.6.

Como tenemos 5 claves, podemos dejar los nodos descendientes con dos claves cada una, y de
este modo quedan con el mínimo posible; y además mover la clave 3, del nodo que rebalsó, a la
raíz. Quedando ésta con una sola clave, cumpliendo las propiedades de un B-tree. Luego se
procede al ingreso de las claves 6 y 7, completando las claves de ese nodo, lo cual se muestra en
la Figura 17.6 a la derecha.

Figura 17.6. B-Tree después de ingresar la clave 4, 5 y 7.

Las claves siempre se insertan en las hojas, para cumplir la propiedad de que un B-Tree tiene
hojas de igual profundidad.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
Luego, al introducir la clave 8, se rebalsa el nodo, y se lo divide en dos, pasando una clave a la
raíz, ahora ésta tiene tres punteros activos. Luego de ingresadas las claves 9 y 10, se completan
las claves de ese nodo, lo cual se ilustra en la Figura 17.7, a la derecha.

Figura 17.7. B-Tree después de ingresar la clave 8, y 10.

Luego de ingresar la clave 11, se activa un nuevo puntero en la raíz, debido a la división de un
nodo en dos, el ascenso del 9 y la redistribución de claves. La Figura 17.8, a la derecha muestra
el nodo con todas sus claves, luego de ingresadas las claves 12 y 13.

Figura 17.8. B-Tree después de ingresar la clave 11, y 13.

Al insertar la clave 14, se activa el último puntero de la raíz, y además ésta queda con todas sus
claves ocupadas.

Figura 17.9. B-Tree después de ingresar la clave 14.

Se pueden ingresar las claves 15 y 16, completando las claves del nodo ubicado más a la
derecha, lo que se muestra en la Figura 17.10.

Figura 17.10 B-Tree después de ingresar la clave 16.

Al introducir la clave 17, se divide el nodo ascendiendo la clave 15, pero ésta no puede ser
almacenada en la raíz, y se procede a la división del nodo raíz, ascendiendo la clave 9, que se
deposita en una nueva raíz; de este modo se vuelve a incrementar la altura del B-Tree.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 7

Figura 17.11 B-Tree después de ingresar la clave 17.

La Figura 17.12, muestra que después de ingresada la clave 25, se han agregado dos nodos al
subárbol izquierdo, quedando completos los nodos de más a la derecha del segundo y tercer
nivel.

Figura 17.12 B-Tree después de ingresar la clave 25.

Después de ingresar la clave 26, asciende la clave 24, y posteriormente la clave 18.

Figura 17.13 B-Tree después de ingresar la clave 26.

Cada vez que asciende una clave, se activan nuevos puntero, y se dividen los nodos.

17.5.2 Análisis de inserción en un B-Tree de grado t.

Otra forma de diseñar la operación es insertar siempre la nueva clave en una hoja que no esté
completa; para lograr esto a medida que se desciende desde la raíz, buscando la posición para
insertar, si se encuentra un nodo completo, se procede a la división de éste, repartiendo
equitativamente las claves; y repitiendo este procedimiento hasta insertar finalmente la clave en
una hoja que no está completa.

Es importante destacar que antes de invocar a insertar, debe asegurarse que la clave no esté
presente, ya que en el descenso se modifica la estructura del B-Tree, y es posible que la hoja
donde debería insertarse la clave quede con menos de (t-1) claves, violando las propiedades de
un B-Tree.
a) Árbol vacío.
Se crea un nodo vacío y se le agrega el valor en la primera posición, el nodo se marca como
hoja.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
b) Inserción en hoja con menos de (2t-1) claves.
p
i n-1
a0 … ai … an-1

Figura 17.14 Inserción en hoja que no está llena.

Se ubica el índice i, cuya clave ai es menor que el valor v que debe insertarse. Se desplazan las
claves, a partir de la clave ai 1 , en una posición hacia la derecha. Se inserta la clave v, a la
derecha de la clave ai . Se aumenta el número de claves activas en p.

p
i n
a0 … ai v ai+1 … an-1

Figura 17.15 Después de insertar v en hoja que no estaba llena.

Buscar el índice i, tal que ai v , mediante búsqueda binaria tiene un costo de O(log(2t 1)) .
En el peor caso, cuando i 1 , es decir cuando a0 v , hay que desplazar (2t 2) claves y
además copiar la clave v, entonces el costo de esta operación es:

O(log(2t 1)) O(2t 2) O(1) O(t )


c) Inserción en nodo interno.
Se ubica la clave ai que es menor que el valor v que debe insertarse. Sea pi 1 la dirección del
nodo donde debe insertarse la clave v, que puede denominarse ancestro de v.
c1) Si ancestro no está completo
Si el nodo apuntado por pi 1 no está completo, se procede a insertar recursivamente v en el
nodo apuntado por pi 1 , descendiendo un nivel en el B-Tree. Esta operación tiene el costo de la
búsqueda de la clave menor que v, que es O(log(2t 2)) .
c2) Si el ancestro de v, está completo.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 9
Para mantener la condición de descender a un nodo que no está completo, debe partirse el nodo
apuntado por pi 1 en dos.

p
i
a0 … ai ai+1 … ana-1

pi+1

c0 … ct-2 ct-1 ct … c2t-2

Figura 17.16 Descender a un nodo que está completo.

La clave central ct 1 se inserta a la derecha de ai , previo a esto deben desplazarse una posición
a la derecha, las claves y los punteros desde ai 1 hasta el final, incrementando en uno el número
de claves activas en nodo p. Esto es posible siempre que p apunte a un nodo que no está
completo. Se crea un nuevo nodo, que se muestra como pi 2 en la Figura 17.17, y se copian las
(t 1) claves mayores del nodo pi 1 en las primeras posiciones del nuevo nodo. Además se
disminuye a (t 1) el número de claves activas de pi 1 . Si el ancestro de v es una hoja, también
será hoja el nuevo nodo; y si el ancestro de v es un nodo interno, el nuevo nodo también debe
ser nodo interno. En este último caso es preciso copiar también los punteros a descendientes en
las primeras posiciones del nuevo nodo.

p i
a0 … ai ct-1 ai+1 … ana-1

pi+1 pi+2

c0 … ct-2 ct … c2t-2

Figura 17.17 Después de partir ancestro de v.

Se continua invocando recursivamente a insertar valor v en el nodo pi 1 que no está lleno si se


tiene que: ai v ct 1 ; y si se tiene que: ai 1 v ct 1 , el descenso debe ser insertar clave v en
nodo apuntado por pi 2 . Pero ahora ambos hijos de p, son nodos que no están completos.
El costo de la operación debe considerar la búsqueda del puntero pi 1 , de costo O(log(2t 2))
; debe considerar el desplazamiento de (2t-2) claves en nodo p, además debe copiar (t-1) claves
en pi 2 , y si este nodo no es hoja debe copiar adicionalmente t punteros; finalmente tiene que
copiar una clave y un puntero en p. Entonces esta operación tiene un costo:

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

O(log(2t 2)) O(2t 2) O(t 1) O(t ) O(2) O(t )


c3) Si p es la raíz y está completa.
Un caso particular es si el nodo p es la raíz y si está completa. Demás esta decir que v no puede
estar en el nodo raíz, ya que no se aceptan claves duplicadas en un B-Tree.

raíz

c0 … ct-2 ct-1 ct … c2t-2

Figura 17.18 Inserción en raíz completa.

En este caso se crea un nuevo nodo raíz, y en la primera clave se coloca la clave central de p. Se
crea un nuevo nodo en el cual se colocan las (t-1) claves mayores de p. Se actualiza la raíz, y se
procede a insertar v, en la nueva raíz, pero ahora ésta no está completa. Esto da inicio al proceso
de insertar la clave en un nodo ( p0 o p1 ) que no esté completo.

raíz 0
ct-1 …
p
p0 p1

c0 … ct-2 ct … c2t-2

Figura 17.19 Después de intentar insertar en raíz completa.

17.5.3 Costo de la inserción.

Si se asume que el costo de insertar una clave en un nodo que no esté lleno, en el peor caso es
de costo O(t ) ; y como esto se repetirá h veces, uno por cada descenso desde la raíz hasta la
hoja en que se insertará efectivamente la clave. Siendo h la altura del B-Tree, la complejidad de
la operación insertar, será O(th) .
n 1
Antes se demostró que: O(h) O(logt ( )) O (logt (n ))
2
Entonces finalmente, la operación insertar tiene complejidad: O (t log t ( n)) , siendo n el número
de claves almacenadas en el B-Tree.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 11
17.6 Análisis de la operación descartar.

En un multiárbol, se puede borrar la clave en cualquier nodo; sin embargo, si el nodo en el cual
se encuentra la clave que será descartada tenía el número mínimo de claves, dejará de cumplirse
una de las propiedades del B-Tree, y será preciso corregir la estructura. La estrategia es
descartar la clave en una hoja que tenga más del número mínimo de claves.
La operación como se verá a continuación es bastante más compleja que la inserción, sin
embargo como en un B-Tree, el mayor número de claves se encuentran en las hojas, es más alta
la probabilidad de descartar una clave que se encuentre en una hoja.

17.6.1. Descarte en hoja con más claves que el mínimo número.

Veremos primero el caso de descartar la clave en una hoja, y siempre que ésta tenga más del
número mínimo de claves almacenadas.

a0 a1 a2 … an-1

Figura 17.20. Hoja con más de (t-1) claves.

En este caso sólo es preciso desplazar las claves en una posición hacia la izquierda, a partir de la
siguiente a la descartada, y disminuir en uno el número de claves activas en el nodo. No es
preciso copiar los punteros, pues éstos almacenan valores iguales a nulos.

Un caso particular es si el nodo es la raíz y tiene un solo elemento; en este caso el B-Tree queda
vacío.

Buscar la clave dentro del nodo, en el peor caso, es de complejidad O(2t-1) si se emplea
búsqueda secuencial y O(log(2t-1)) si se emplea búsqueda binaria. Se requiera además mover,
en el peor caso, (2t-2) claves lo cual implica complejidad O(2t-2).
Si se emplea búsqueda binaria, esta operación tiene un costo:

O(log(2t 1)) O(2t 2) O(t )

Para lograr que el nodo que contenga la clave que será descartada, tenga más del número
mínimo de elementos, puede procederse al descenso desde la raíz hasta la hoja que contenga el
nodo con la clave buscada, realizando modificaciones para asegurar que siempre se producirá un
descenso a un nodo que tenga más de (t-1) claves, cuya solución se acaba de describir.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
17.6.2. Descarte en nodo p que no es hoja y que contiene la clave v.

Se asume que nodo p, tiene más de (t 1) claves, y que i es el índice de la clave v.


p
i
a0 … v ai+1 … ana-1

pi pi+1

b0 … bi … bnb-1 c0 … ci … cnc-1

Figura 17.21. Nodo interno con más de (t-1) claves, y clave con hijo izquierdo y derecho.

Hijo izquierdo con más claves que la mínima.


Si nb (t 1) se copia la clave bnb 1 , que es el mayor descendiente del subárbol izquierdo, en
lugar de v; y se continua recursivamente descartando la clave bnb 1 en el nodo apuntado por pi .
p
i

a0 … bnb-1 ai+1 … ana-1

pi

b0 … bi … bnb-1

Figura 17.21a. Se recurre a borrar el predecesor de v.

Debe notarse que en este caso, se desciende un nivel, y se está borrando una clave en un nodo
que tiene más de (t-1) claves. Esta operación incurre en el costo de encontrar la clave, y luego
efectuar una copia; entonces se tiene, para búsqueda binaria:

O(log(2t 1)) O(1) O(log(t )) .


Hijo derecho con más claves que la mínima.
Si lo anterior no se cumple, pero nc (t 1) , se copia la clave c0 , que es el menor descendiente
del subárbol derecho, en el lugar de v; y se continua descartado la clave c0 en el nodo apuntado
por pi 1 . Debe notarse que en este caso se está borrando una clave en un nodo que tiene más de
(t-1) claves. Con igual costo que la anterior.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 13

p
i

a0 … c0 ai+1 … ana-1

pi+1

c0 … ci … cnc-1

Figura 17.21b. Se recurre a borrar el sucesor de v.

Ambos hijos con número de claves mínimas.


Si no se cumplen las dos condiciones anteriores, y el nodo p no es la raíz, se asume que debe
tener más de (t-1) claves, en este caso se agrega al nodo apuntado por pi , la clave v, y las claves
del hermano derecho, el nodo apuntado por pi 1 , quedando éste completo con (2t-1) claves. Se
libera el espacio del nodo apuntado por pi 1 , y se procede a descartar recursivamente v, en el
nodo apuntado por pi .
p
i na-2
a0 … ai+1 … ana-1

pi pi+1
nb 2t-2
b0 … v … cnc-1 c0 … ci … cnc-1

Figura 17.22. Mezcla del hijo izquierdo y derecho.

Esto implica desplazar las claves y los punteros en el nodo apuntado por p. Además deben
copiarse las nc claves en pi , a continuación de v; y si pi no es hoja, los punteros del nodo pi 1
deben también copiarse al nodo pi .
Si consideramos que el costo de encontrar la clave es O(log(2t 1)) , en el peor caso; y que
además deben moverse (2t 2) claves y (2t 1) punteros en p; y que también deben moverse t
claves, (t 1) desde pi 1 y la clave v desde p; y finalmente t punteros desde pi 1 , se tiene un
costo de: O(log(2t 1)) O(2t 2) O(2t 1) O(t ) O(t ) .
Nodo raíz con claves mínimas.
Si el nodo p es la raíz, puede quedar luego de la operación con menos de (t-1) claves. Esto se
ilustra en la Figura 17.23, con t=3, cuando se desea descartar la clave 15. En este caso la clave
está en el nodo apuntado por p, y luego de la operación, propuesta antes, se obtiene la Figura

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
17.24. En la cual se cumple que luego de la operación descrita antes, el nodo apuntado por pi
tiene (2t-1) claves.
p
1
10 15

pi pi+1

4 6 18 20

Figura 17.23. Hijos con claves mínimas, raíz con dos elementos.

p
0
10

t-1 pi pi+1

4 6 15 18 20 18 20

Figura 17.24. Fusión de hijos.

Nodo raíz con una sola clave.


Si el nodo p es la raíz, y tiene una sola clave, esto se muestra en la Figura 17.25, y se desea
borrar la clave 10, se procede de igual forma, sin embargo es preciso cambiar la raíz, y liberar
adicionalmente el nodo p. Esta es la única forma en que el B-Tree disminuye su altura.
raíz
p 0
10

pi pi+1

4 8 14 18

Figura 17.25. Hijos con claves mínimas, raíz con un elemento.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 15

raíz p
0
10
pi
t-1 pi+1

4 8 10 14 18 14 18

Figura 17.26. Nueva raíz, y fusión de hijos.

17.6.3. Descarte en nodo p que no es hoja y que no contiene la clave v.

Si se ubica, con la posición i, la clave ai que es menor que el valor v buscado para descartar, se
tendrá que pi 1 apunta al nodo que contiene el valor, o a alguno de sus descendientes, que
contiene efectivamente el valor v.

p
i

a0 … ai ai+1 … ana-1

pi pi+1 pi+2

b0 … bi … bnb-1 c0 … ci … cnc-1 d0 … di … dnd-1

Figura 17.27. Descarte en nodo interno que no contiene la clave buscada.

Se asume que p tiene más de (t 1) claves, excepto que sea la raíz. Y lo que desea asegurar es
que se descenderá a un nodo con más de (t 1) claves.
Ancestro de v, tiene más de (t-1) claves.
Si pi 1 tiene más de (t 1) claves, se procede a descartar recursivamente v, en el nodo apuntado
por pi 1 , descendiendo un nivel.
Esta operación tiene el costo de encontrar la clave: O(log(2t 1)) .

Si pi 1 tiene (t-1) claves, pueden suceder tres casos:

Hermano izquierdo de ancestro de v, tiene más de (t-1) claves.


Es decir: nb t 1 . Se desplazan las claves y punteros de pi 1 en una posición hacia la derecha;
los punteros sólo si pi 1 no es hoja,. Luego se mueve ai al inicio de pi 1 , el lugar que antes
ocupaba c0 , a continuación se toma bnb 1 y se lo coloca en el lugar que ocupaba ai .

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Finalmente, para mantener la estructura del B-Tree, se copia el puntero que apunta a claves
mayores que bnb 1 en la posición inicial de pi 1 ; es decir apuntando al nodo con claves menores
que ai . Se disminuye en uno el número de claves activas en pi . Luego se procede a descartar v
en el nodo apuntado por pi 1 , que ahora es seguro que tiene más de (t 1) claves.
Debe notarse que no hay hermano izquierdo de pi 1 , si v a0 .
p
i

a0 … bnb-1 ai+1 … ana-1

pi pi+1 pi+2

b0 … bi … bnb-2 ai c0 … … cnc-1 d0 … di … dnd-1

Figura 17.28. Préstamo del hermano izquierdo.

Si consideramos que el costo de encontrar la clave es O(log(2t 1)) , en el peor caso; y que
además deben moverse (t 1) claves y t punteros en pi 1 ; y que también deben efectuarse dos
copias de claves y una de puntero, se tiene un costo de:

O(log(2t 1)) O(t 1) O(t ) O(3) O(t )


Esta operación puede aplicarse si el nodo p es la raíz.

Hermano derecho de ancestro de v, tiene más de (t-1) claves.


Es decir: nd t 1 . Se agrega al final de pi 1 la clave ai 1 y el puntero que apunta al nodo con
claves menores que d 0 , se coloca apuntado a nodos con claves mayores que ai 1 ; y se
incrementa en uno las claves activas de pi 1 , quedando de este modo el ancestro de v, con más
de (t 1) claves. Luego d 0 se coloca en el lugar que antes ocupaba ai 1 . Se desplazan en una
posición a la izquierda las claves del nodo pi 2 ; y si ese nodo no es hoja también se desplazan
los punteros; finalmente se disminuye en uno las claves activas de pi 2 . Luego se procede a
descartar v en el nodo apuntado por pi 1 , que ahora tiene más de (t 1) claves, descendiendo
un nivel.
Debe notarse que no hay hermano derecho de pi 1 , si v ana 1 .
El costo es similar al caso anterior.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 17

p
i

a0 … ai d0 … ana-1
1
pi pi+1 pi+2

b0 … bi … bnb-1 c0 … … cnc-1 ai+1 d1 … di … dnd-2

Figura 17.29. Préstamo del hermano derecho.

Esta operación puede aplicarse si el nodo p es la raíz, sin que ésta cambie.

Hermanos de ancestro de v, tienen (t-1) claves.


Al menos uno de los hermanos del ancestro de v debe existir, ya que p no contiene la clave con
valor v.

Si existe el hermano izquierdo


Se baja la clave ai , al final del nodo pi , y se colocan las claves del ancestro de v a
continuación; y si éste no es hoja se copian también los punteros. Quedando de este modo el
nodo pi con (2t 1) claves activas. A partir de ai 1 se desplazan hacia la izquierda las claves y
punteros en p, disminuyendo en uno el número de claves activas. Se libera el espacio apuntado
por pi 1 . Se continua descartando recursivamente v en el nodo apuntado por pi .
p
i

a0 … ai+1 ai+2 … ana-1

pi pi+1 pi+2

b0 … bnb-1 ai c0 .. cnc-1 c0 … … cnc-1 d0 … di … dnd-1

Figura 17.30. Fusión del ancestro con el hermano izquierdo.

Si consideramos que el costo de encontrar la clave es O(log(2t 1)) , en el peor caso; y que
además deben copiarse t claves y (t 1) punteros en pi ; y que también deben efectuarse
(t 2) movimientos de clave y (t 1) de punteros en nodo p, se tiene un costo de:

O(log(2t 1)) O(t ) O(t 1) O(t 2) O(t 1) O(t )

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
Si no existe el hermano izquierdo.
Se baja clave ai 1 , al final del nodo pi 1 , a continuación se copian las claves de pi 2 , y los
punteros si éste no es hoja. El nodo ancestro de v, queda con (2t 1) claves activas. A partir de
ai 2 se desplazan las claves y punteros en p, disminuyendo en uno el número de claves activas.
Se libera el espacio apuntado por pi 2 . Se continua descartando v en el nodo apuntado por pi 1 .
De similar costo a la anterior.
p
i

a0 … ai ai+2 … ana-1

pi+1
pi+2
c0 … cnc-1 ai+1 d0 .. dnd-1 d0 … di … dnd-1

Figura 17.31. Fusión del ancestro con el hermano derecho.

Si el nodo p es la raíz, y tiene una sola clave, debe cambiarse la raíz al nodo que queda con
(2t 1) claves, y liberar además el antiguo nodo raíz.

17.6.4. Complejidad de la operación descarte.

Si se asume que el costo de descartar una clave en un nodo con al menos t elementos, en el peor
caso es de costo O(t ) ; y como esto se repetirá h veces, uno por cada descenso desde la raíz
hasta la hoja en que se descarta efectivamente una clave. Siendo h la altura del B-Tree, la
complejidad de la operación descartar, será O(th) .
n 1
Antes se demostró que: O(h) O(logt ( )) O(logt (n))
2
Entonces finalmente, la operación descartar tiene complejidad: O (t log t (n )) , siendo n el
número de claves almacenadas en el B-Tree.

17.7. Árboles coloreados.

Corresponden a un B-Tree de grado 2. Con un máximo de tres claves en un nodo, y un mínimo


de una clave en cada nodo.

Para convertir un árbol coloreado en un B-Tree se emplean las siguientes reglas:


a) Si el nodo contiene 3 claves, la central es negra, la primera y última son rojas.
b) Si el nodo contiene una clave, ésta es negra.
c) Si el nodo contiene dos claves, puede interpretarse de dos formas: Una es que la primera
clave es negra con hijo derecho rojo con la segunda clave; la otra es que la segunda clave es
negra con hijo rojo izquierdo con la primera clave.

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 19

Una buena interpretación de los nodos rojos en un árbol coloreado, es dibujarlos en un nivel
horizontal, de este modo se refleja que no alteran las alturas negras de los nodos. Esto se
muestra en la Figura 17.33, para el árbol coloreado de la Figura 17.32.
9

4 15

2 5 12 20

Figura 17.32. Árbol binario coloreado.

En la representación con rojos en nivel horizontal, debe cuidarse que cada nodo tenga dos hijos,
para que el árbol sea binario. En el par 4-9, el primero se considera rojo; y en el par 5-7 el
segundo se considera rojo. La representación muestra que todas las hojas están en el mismo
nivel.

4 9

2 5 7 12 15 20

Figura 17.33. Coloreado con rojos horizontales.

Luego los nodos que están unidos por horizontales se consideran pertenecientes a un nodo de un
B-tree de grado dos, lo cual se muestra en la Figura 17.34.

4 9
p0
p1 p2

2 5 7 12 15 20

Figura 17.34. B-tree de grado dos, equivalente del coloreado de la Figura 17.32.

Con esta interpretación pueden diseñarse las operaciones de inserción y descarte en árboles
coloreados, agrupando las claves que pertenezcan a un nodo de un B-Tree y realizando las
operaciones de mezcla y separación que se realizan en esta estructura, pero es preferible
especializar las operaciones, debido a que la estructura interna de un nodo de un árbol coloreado
es bastante más simple que la de un B-Tree.

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
17.8. Estructura de datos y funciones básicas.

17.8.1. Estructura de datos.

#define N 5 //2t-1 . t es el grado del B-tree


#define eshoja 1
#define esnodointerno 0

typedef struct nn
{
int clave[N]; //con N claves en el Nodo
struct nn *pn[N+1];
int act; //número de claves activas en el nodo
int hoja; // 1 es hoja; 0 en nodo interno
} nodo, *pnodo;

17.8.2. Creación de un nodo.

pnodo CreaNodo()
{ int i;
pnodo p=malloc(sizeof(nodo));
if (p!=NULL)
{ p->act=0;p->hoja=esnodointerno;
for(i=0;i<N+1;i++) p->pn[i]=NULL;
for(i=0;i<N;i++) p->clave[i]=0;
}
return(p);
}

17.8.4. Mostrar nodo y el B-Tree en niveles.

void PrtNodo(pnodo p)
{ int i;
if(p->act==0) printf("Nodo vacío\n");
else for(i=0; i<p->act;i++) printf("%d ", p->clave[i]);
//putchar('\n');
}

void PrtBtree(pnodo p, int level)


{ int i;
if (p!=NULL)
{ for(i=1;i<level;i++) printf(" ");
PrtNodo(p); putchar('\n');
PrtBtree(p->pn[0],level+1);
for(i=1;i<p->act+1;i++) PrtBtree(p->pn[i],level+1);
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 21
17.8.5. Búsqueda de un valor.

En casos prácticos el número de claves en un nodo es elevado, por lo que suele emplearse
búsqueda binaria.

int search(pnodo p, int valor)


{ int i,l,r;
while (p!=NULL)
{
l=0; r=p->act-1;
while (r>=l) //búsqueda binaria
{ i=(l+r)/2;
if (valor<=p->clave[i]) r=i-1;
if (valor>= p->clave[i]) l=i+1;
}
/* Si (valor < p->clave[0]) r=-1, l=0, (l-r)=1
Si (valor > p->clave[p->act-1]) r=p->act-1, l=p->act, (l-r)=1
Si Si (valor == p->clave[i]) r=i-1, l=i+1, (l-r)=2
*/
r++;
/* Lo encontró en posición r. ( l-r=1 => l+1=r => l>r )
Si no lo encontró: r=0 si (valor < p->clave[0])
r=p->act si (valor > p->clave[p->act-1])
*/
if(l>r) return(1); //lo encontró en posición r del arreglo.
else p=p->pn[r]; //busca en descendiente
}
return(0);//no lo encontró
}

17.8.6. Partir un nodo.

//en la posición i-esima de x ingresa la clave central de y.


//Pega el nuevo nodo z, en la posición (i+1) de x.
void split(pnodo x, int i, pnodo y)
{ int j;
pnodo z = CreaNodo();
if (z==NULL){printf("Error creación de nodo. Split"); exit(1);}
z->hoja=y->hoja; z->act=N/2; //z es hermana de y. Y se le copian los (t-1) superiores de y.
for(j=0;j<N/2;j++) z->clave[j]=y->clave[j+N/2+1];//copia claves en z
if(y->hoja ==esnodointerno) //si no es hoja
for(j=0;j<(N/2)+1;j++) z->pn[j]=y->pn[j+(N/2)+1];//copia punteros en z
y->act=N/2; //se desactivan las claves de la mitad superior de y
for(j=x->act+1;j>i ;j--) x->pn[j]=x->pn[j-1];//desplaza punteros en x

x->pn[i+1]=z;

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
for(j=x->act;j>i ;j--) x->clave[j]=x->clave[j-1];//desplaza claves en x
x->clave[i]=y->clave[N/2]; //asciende clave central del lleno
x->act++;
}

17.8.7. Inserción en nodo que no está lleno.

void insertenolleno(pnodo x,int valor)


{ int i;
if (x->hoja==eshoja)
{ for(i=x->act-1;i>=0 && valor < x->clave[i];i--)
x->clave[i+1]=x->clave[i];//crea un espacio
//i apunta a clave menor que el valor

x->clave[i+1]=valor;x->act++; //solo se inserta en hojas


}
else
{ for(i=x->act-1;i>=0 && valor<x->clave[i];i--); //búsqueda secuencial
i++; //pn[i] apunta a la hoja donde debe insertarse. Ahora i es mayor o igual que cero
if(x->pn[i]->act==N)
{
split(x,i,x->pn[i]);
if(valor> x->clave[i])i++; //fija puntero en el descenso.
}
insertenolleno(x->pn[i],valor); //recursiva por el fondo
}
}

17.9. Inserción.

//Debe buscarse antes de insertar. Si la clave ya estaba, cambia B-Tree.


void inserte(pnodo *pbt, int valor)
{ pnodo p=*pbt,s;
if (p ==NULL)
{ p=CreaNodo();//si árbol vacío, crea la raíz
if (p==NULL) {printf("Error creación de nodo raíz"); exit(1);}
else {p->act=1;p->hoja=eshoja;p->clave[0]=valor;*pbt=p;}
}
else if(p->act==N)//nodo raíz lleno
{ s=CreaNodo();if (s==NULL) {printf("Error creación de nueva raíz"); exit(1);}
*pbt=s; s->hoja=esnodointerno; s->pn[0]=p;
split(s,0,p);
insertenolleno(s,valor);
}
else insertenolleno(p,valor);
}

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 23
17.10. Descartar.

La función descartar puede descomponerse en funciones primitivas como desplazar a la derecha


o izquierda y mezclar dos nodos. Se muestran expandidas, para evitar el llamado a funciones.

int descartar(pnodo *pbt, int valor)


{ int i,j,kp;
pnodo y,z,c,x=*pbt;
if(x==NULL) {printf("Error en descartar: Arbol vacío\n"); return(1);}
if(x->hoja==eshoja)
{//borrar valor;
for(i=x->act-1;i>=0 && valor< x->clave[i];i--);
//i es la clave mayor o igual al valor
if( valor==x->clave[i] )
{
for(j=i;j<x->act-1;j++) x->clave[j]=x->clave[j+1]; //mover claves en x;
x->act--;
if (debug) printf("Borrando %d en hoja\n", valor);
if(x->act==0) //B-Tree queda vacío
{ if (debug) printf("B-Tree queda vacío\n");
*pbt=NULL;
}
return(0);
}
else {printf("Error en descartar: clave %d no existe\n",valor); return(1);}
}
else //no es hoja
{
for(i=x->act-1;i>=0 && valor<x->clave[i];i--);
//v está en x ssi:
if( i>=0 && valor==x->clave[i] ) //valor está en x->clave[i]. i está entre 0 y x->cnt-1
{ y=x->pn[i]; // y apunta al hermano izquierdo
z=x->pn[i+1]; // z apunta al hermano derecho.
if (y->act>N/2)
{ kp=y->clave[(y->act)-1]; //mayor descendiente hijo izquierdo
x->clave[i] =kp;
if (debug) printf("Borrando con préstamo izquierdo en nodo %d\n",kp);
descartar(&y,kp);
}
else if (z->act>N/2)
{kp=z->clave[0]; //menor descendiente hijo derecho
x->clave[i]=kp;
if (debug) printf("Borrando con préstamo derecho en nodo %d\n",kp);
descartar(&z,kp);
}
else //merge y con z
{y->clave[y->act]=x->clave[i]; y->act++;

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
//copiar los de z a continuación en y;
for(j=0;j<z->act;j++) {y->clave[y->act+j]=z->clave[j];}
if (y->hoja==esnodointerno)
for(j=0;j<z->act+1;j++) {y->pn[y->act+j]=z->pn[j];}//copia punteros
y->act+=z->act;

for(j=i;j<x->act-1;j++) x->clave[j]=x->clave[j+1]; //mover claves en x;


for(j=i;j<x->act;j++) x->pn[j]=x->pn[j+1];//desplaza punteros en x
x->act--;free(z);
if(x->act==0) //B-Tree disminuye altura
{free(x);
if (debug) printf("Disminuye altura B-Tree\n");
*pbt=y;
}
if (debug) printf("Borrando con fusión de derecho en nodo izquierdo %d\n",valor);
descartar(&y,valor);
}
}
else //valor está en un descendiente de x
//pn[i] apunta a la página en la que está el valor, o cuyos descendientes contienen el valor.
{i++; // ahora i es mayor o igual que cero
c=x->pn[i];
if(c->act==N/2) //nodo descendiente con claves mínimas
{
if(i==0) y=NULL; else y=x->pn[i-1]; //si c no tiene hermano izquierdo: y=NULL
if(i==x->act) z=NULL; else z=x->pn[i+1]; //si c no tiene hermano derecho: z=NULL

if(y!=NULL && y->act>N/2)


{ for(j=c->act; j>0;j--) c->clave[j]=c->clave[j-1]; //mueve c a la derecha
if(c->hoja==esnodointerno)
for(j=c->act+1;j>0;j--) c->pn[j]=c->pn[j-1]; //mueve punteros de c a la derecha
c->clave[0]=x->clave[i-1];//baja uno de x a c
c->act++; //c queda con mas de N/2.
x->clave[i-1]=y->clave[y->act-1]; //mayor de y lo sube a x
c->pn[0]=y->pn[y->act]; //hijo de y se cuelga como hijo de c
y->act--;
if (debug)
{ printf("Deja descendiente con más del mínimo\n");
PrtBtree(btree,1);
printf("Borrando %d en nodo descendiente con prestamo de izquierdo \n",valor);
}
descartar(&c,valor);
}//y es nulo o y no tiene (t-1)
else if (z!=NULL && z->act>N/2)
{ c->clave[c->act]=x->clave[i];//baja central de x a c.
c->act++; //c queda con mas de N/2.
c->pn[c->act]=z->pn[0]; //pega menores de z a mayores del mayor de c

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 25
x->clave[i]=z->clave[0]; //el menor de z lo sube a x.
for(j=0;j<z->act-1;j++) z->clave[j]=z->clave[j+1]; //mueve claves de z a la izquierda
if (z->hoja==esnodointerno)
for(j=0;j<z->act;j++) z->pn[j]=z->pn[j+1];//mueve punteros de z a la izquierda
z->act--;
if (debug)
{ printf("Deja descendiente con más del mínimo\n");
PrtBtree(btree,1);
printf("Borrando %d en nodo descendiente con préstamo de derecho \n",valor);
}
descartar(&c,valor);
}
else //merge
{ if(y==NULL) //merge c con z;
{ c->clave[c->act]=x->clave[i]; c->act++;
for(j=0;j<z->act;j++) {c->clave[c->act+j]=z->clave[j]; }
if(c->hoja==esnodointerno)
for(j=0;j<z->act+1;j++) {c->pn[c->act+j]=z->pn[j]; }
c->act+=z->act;
//correr x (que no es hoja, tiene descendientes)

for(j=i;j<x->act-1;j++) x->clave[j]=x->clave[j+1]; //mover claves a la izquierda en x;


for(j=i+1;j<x->act;j++) x->pn[j]=x->pn[j+1];//desplaza punteros a la izquierda en x
x->pn[i]=c;
x->act--;
if(x->act==0) //B-Tree disminuye altura
{ free(x);
*pbt=c;
if (debug) printf("Disminuye altura\n");
}
free(z);
if (debug)
{ printf("mezcla c con z. Deja descendiente con más del mínimo por fusión\n");
PrtBtree(btree,1);
printf("Borrando %d en nodo descendiente con fusión de derecho \n",valor);
}
descartar(&c,valor);
}
else // merge c con y; Código es similar al del if.
{
y->clave[y->act]=x->clave[i-1]; y->act++;
for(j=0;j<c->act;j++) {y->clave[y->act+j]=c->clave[j]; }
if (y->hoja==esnodointerno)
for(j=0;j<c->act+1;j++) {y->pn[y->act+j]=c->pn[j]; }
y->act+=c->act;
//correr x
for(j=i-1;j<x->act-1;j++) x->clave[j]=x->clave[j+1]; //mover claves en x;

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
for(j=i;j<x->act;j++) x->pn[j]=x->pn[j+1];//desplaza punteros en x
x->pn[i-1]=y;
x->act--;
if(x->act==0) //B-Tree disminuye altura
{ free(x);
*pbt=y;
if (debug) printf("Disminuye altura\n");
}
free(c);
if (debug)
{ printf("mezcla c con y\n");
PrtBtree(btree,1);
printf("Borrando %d en nodo descendiente con fusión en izquierdo \n",valor);
}
descartar(&y,valor);
}
}
}
else //nodo descendiente con claves mayores que el número mínimo. ( >= t)
{
if (debug) printf("Borrando %d en nodo descendiente con más claves que el
mínimo\n",valor);
descartar(&c,valor);
}
}
return(0); //finalizan recursiones por el fondo.
}
}

17.11. Test de las funciones.

pnodo btree=NULL;

int main(void)
{ int i;
/* Test básico
PrtNodo(btree);
descartar(&btree,1);
inserte(&btree,1);
PrtNodo(btree);
descartar(&btree,1);
PrtNodo(btree);
*/

for(i=1;i<=Nclaves;i++) inserte(&btree,i);
if (debug) PrtBtree(btree,1);
for(i=Nclaves;i>0;i--) descartar(&btree,i);

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 27

for(i=1;i<Nclaves;i++) inserte(&btree,i);
for(i=1;i<Nclaves;i++) descartar(&btree,i);

for(i=Nclaves;i>0;i--) inserte(&btree,i);
if (debug) PrtBtree(btree,1);
for(i=1;i<=Nclaves;i++) descartar(&btree,i);

for(i=Nclaves;i>0;i--) inserte(&btree,i);
for(i=Nclaves;i>0;i--) descartar(&btree,i);

//test de búsquedas
for(i=1;i<=Nclaves;i++) inserte(&btree,i);
for(i=1;i<=Nclaves;i++)
if (search(btree,i)==0) printf("Error no encuentra clave %d que está en B-tree\n",i);
for(i=Nclaves+1;i<=Nclaves+10;i++)
if (search(btree,i)) printf("Encuentra clave %d que no está en B-tree\n",i);
for(i=1;i<=Nclaves;i++) descartar(&btree,i);
printf("Ok\n");

return (0);

Referencias.

Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975.

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. “Introduction
to Algorithms”, Second Edition. MIT Press and McGraw-Hill, 2001.

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 17. ...........................................................................................................................................1


B-TREES. ....................................................................................................................................................1
17.1 DEFINICIÓN DE B-TREES DE GRADO T. ..............................................................................................1
17.2. CÁLCULO DE LA ALTURA DE UN B-TREE. .........................................................................................1
17.3. REQUERIMIENTOS DE ESPACIO Y RELACIONES DE ORDENAMIENTO DE LAS CLAVES..........................3
17.4. COMPLEJIDAD EN LA BÚSQUEDA. .....................................................................................................4
17.5 ANÁLISIS DE LA OPERACIÓN INSERCIÓN. ...........................................................................................5
17.5.1 Ejemplo de inserción en un 2-4-Tree. .......................................................................................5
17.5.2 Análisis de inserción en un B-Tree de grado t. .........................................................................7
a) Árbol vacío. ................................................................................................................................................. 7
b) Inserción en hoja con menos de (2t-1) claves. ............................................................................................. 8
c) Inserción en nodo interno. ............................................................................................................................ 8
c1) Si ancestro no está completo ................................................................................................................. 8
c2) Si el ancestro de v, está completo. ......................................................................................................... 8
c3) Si p es la raíz y está completa.............................................................................................................. 10
17.5.3 Costo de la inserción...............................................................................................................10
17.6 ANÁLISIS DE LA OPERACIÓN DESCARTAR. .......................................................................................11
17.6.1. Descarte en hoja con más claves que el mínimo número. ......................................................11
17.6.2. Descarte en nodo p que no es hoja y que contiene la clave v. ...............................................12
Hijo izquierdo con más claves que la mínima. ............................................................................................... 12
Hijo derecho con más claves que la mínima. ................................................................................................. 12
Ambos hijos con número de claves mínimas. ................................................................................................ 13
Nodo raíz con claves mínimas. ...................................................................................................................... 13
Nodo raíz con una sola clave.......................................................................................................................... 14
17.6.3. Descarte en nodo p que no es hoja y que no contiene la clave v. ..........................................15
Ancestro de v, tiene más de (t-1) claves. ........................................................................................................ 15
Hermano izquierdo de ancestro de v, tiene más de (t-1) claves. ..................................................................... 15
Hermano derecho de ancestro de v, tiene más de (t-1) claves. ....................................................................... 16
Hermanos de ancestro de v, tienen (t-1) claves. ............................................................................................. 17
Si existe el hermano izquierdo .................................................................................................................. 17
Si no existe el hermano izquierdo. ............................................................................................................ 18
17.6.4. Complejidad de la operación descarte. ..................................................................................18
17.7. ÁRBOLES COLOREADOS. .................................................................................................................18
17.8. ESTRUCTURA DE DATOS Y FUNCIONES BÁSICAS. ............................................................................20
17.8.1. Estructura de datos. ..............................................................................................................20
17.8.2. Creación de un nodo. ............................................................................................................20
17.8.4. Mostrar nodo y el B-Tree en niveles. .....................................................................................20
17.8.5. Búsqueda de un valor. ............................................................................................................21
17.8.6. Partir un nodo. .......................................................................................................................21
17.8.7. Inserción en nodo que no está lleno. ......................................................................................22
17.9. INSERCIÓN. .....................................................................................................................................22
17.10. DESCARTAR..................................................................................................................................23
17.11. TEST DE LAS FUNCIONES. .............................................................................................................26
REFERENCIAS. .........................................................................................................................................27
ÍNDICE GENERAL. ....................................................................................................................................28
ÍNDICE DE FIGURAS. ................................................................................................................................29

Profesor Leopoldo Silva Bijit 26-05-2008


B-Trees 29

Índice de figuras.

FIGURA 17.1 B-TREE CON (T-1) CLAVES EN UN NODO. CADA NODO TIENE T HIJOS. ....................................... 1
FIGURA 17.2 B-TREE CON MÁXIMO NÚMERO DE CLAVES EN CADA NODO, CON T=4. ..................................... 2
FIGURA 17.3 ALTURA DE B-TREE CON T=4. .................................................................................................. 3
FIGURA 17.4 CLAVES MÁXIMAS Y MÍNIMAS ALMACENADAS EN B-TREE CON T=4. ....................................... 3
FIGURA 17.5 ESQUEMA DE B-TREE CON T=2................................................................................................. 4
FIGURA 17.6. B-TREE DESPUÉS DE INGRESAR LA CLAVE 4, 5 Y 7. ................................................................. 5
FIGURA 17.7. B-TREE DESPUÉS DE INGRESAR LA CLAVE 8, Y 10. .................................................................. 6
FIGURA 17.8. B-TREE DESPUÉS DE INGRESAR LA CLAVE 11, Y 13. ................................................................ 6
FIGURA 17.9. B-TREE DESPUÉS DE INGRESAR LA CLAVE 14. ......................................................................... 6
FIGURA 17.10 B-TREE DESPUÉS DE INGRESAR LA CLAVE 16. ........................................................................ 6
FIGURA 17.11 B-TREE DESPUÉS DE INGRESAR LA CLAVE 17. ........................................................................ 7
FIGURA 17.12 B-TREE DESPUÉS DE INGRESAR LA CLAVE 25. ........................................................................ 7
FIGURA 17.13 B-TREE DESPUÉS DE INGRESAR LA CLAVE 26. ........................................................................ 7
FIGURA 17.14 INSERCIÓN EN HOJA QUE NO ESTÁ LLENA. .............................................................................. 8
FIGURA 17.15 DESPUÉS DE INSERTAR V EN HOJA QUE NO ESTABA LLENA. .................................................... 8
FIGURA 17.16 DESCENDER A UN NODO QUE ESTÁ COMPLETO. ...................................................................... 9
FIGURA 17.17 DESPUÉS DE PARTIR ANCESTRO DE V. ..................................................................................... 9
FIGURA 17.18 INSERCIÓN EN RAÍZ COMPLETA. ........................................................................................... 10
FIGURA 17.19 DESPUÉS DE INTENTAR INSERTAR EN RAÍZ COMPLETA. ........................................................ 10
FIGURA 17.20. HOJA CON MÁS DE (T-1) CLAVES. ........................................................................................ 11
FIGURA 17.21. NODO INTERNO CON MÁS DE (T-1) CLAVES, Y CLAVE CON HIJO IZQUIERDO Y DERECHO. ..... 12
FIGURA 17.21A. SE RECURRE A BORRAR EL PREDECESOR DE V. .................................................................. 12
FIGURA 17.21B. SE RECURRE A BORRAR EL SUCESOR DE V. ........................................................................ 13
FIGURA 17.22. MEZCLA DEL HIJO IZQUIERDO Y DERECHO. ......................................................................... 13
FIGURA 17.23. HIJOS CON CLAVES MÍNIMAS, RAÍZ CON DOS ELEMENTOS. .................................................. 14
FIGURA 17.24. FUSIÓN DE HIJOS. ................................................................................................................ 14
FIGURA 17.25. HIJOS CON CLAVES MÍNIMAS, RAÍZ CON UN ELEMENTO. ...................................................... 14
FIGURA 17.26. NUEVA RAÍZ, Y FUSIÓN DE HIJOS. ........................................................................................ 15
FIGURA 17.27. DESCARTE EN NODO INTERNO QUE NO CONTIENE LA CLAVE BUSCADA. .............................. 15
FIGURA 17.28. PRÉSTAMO DEL HERMANO IZQUIERDO. ............................................................................... 16
FIGURA 17.29. PRÉSTAMO DEL HERMANO DERECHO................................................................................... 17
FIGURA 17.30. FUSIÓN DEL ANCESTRO CON EL HERMANO IZQUIERDO. ....................................................... 17
FIGURA 17.31. FUSIÓN DEL ANCESTRO CON EL HERMANO DERECHO. ......................................................... 18
FIGURA 17.32. ÁRBOL BINARIO COLOREADO. ............................................................................................. 19
FIGURA 17.33. COLOREADO CON ROJOS HORIZONTALES............................................................................. 19
FIGURA 17.34. B-TREE DE GRADO DOS, EQUIVALENTE DEL COLOREADO DE LA FIGURA 17.32. .................. 19

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 18

Pagodas. Seleccionar.

Una pagoda es una representación de un árbol binario tal que los hijos tienen claves mayores o
iguales a las del padre. Es un algoritmo de selección que puede agregar ítems en forma
dinámica, a diferencia del heap implícito, árbol binario embebido en un arreglo, que utiliza
memoria estática.

18.1. Propiedades.

Cada nodo s de una pagoda contiene dos punteros i(s) y d(s) definidos como sigue:
(1) si s es la raíz o es un hijo derecho: i(s) apunta al nodo más izquierdista del subárbol de raíz s.
(2) si s es un hijo izquierdo, i(s) apunta al padre de s.
El puntero d(s) está definido de una manera simétrica:
(1) si s es la raíz o es un hijo izquierdo d(s) apunta al nodo más derechista del subárbol de raíz s.
(2) si s es un hijo derecho, d(s) apunta al padre de s.

Si se aplican las propiedades anteriores al árbol binario de prioridad, a la izquierda de la Figura


18.1, se obtiene la pagoda que se muestra a la derecha, de la misma Figura.

1
1

2 3
2 3

4 6 5
4 6 5

9 8 7
9 8 7

Figura 18.1. Árbol de prioridad y Pagoda equivalente.

Una pagoda es un edificio de varios niveles, común en países asiáticos. La Figura 18.1, a la
derecha adopta una forma similar a una pagoda, que se muestra en la Figura 18.2, por esto el
nombre.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos

Figura 18.2. Pagodas.

18.2. Listas circulares de la raíz de una pagoda.

Sin embargo si se redibuja la Figura 18.1, a la derecha, según un árbol binario convencional, se
advierten dos listas circulares asociadas a la raíz; una formada por los punteros derechos del
subárbol derecho; y otra formada por los punteros izquierdos del subárbol izquierdo. Esto se
ilustra en la Figura 18.3. A partir de la raíz, las listas están en orden descendente, y los
elementos menores de estas listas apuntan a la raíz. Si se desea eliminar la raíz, deben
modificarse esas dos direcciones.
1

4 7

2 5

8 3
6

Figura 18.3. Pagoda y sus listas circulares principales.

Cada nodo puede considerarse la raíz de una pagoda, que debe cumplir las propiedades que la
definen, para los descendientes de ese nodo. Por ejemplo, en el nodo con valor 2, se tiene una
pagoda que tiene lista circular izquierda nula; en forma similar las pagodas asociadas a los
nodos 8 y 3 pueden considerarse formadas por un solo elemento.

18.3. Mezcla de pagodas.

La operación básica es la de mezclar o fundir (meld or merg), que puede definirse como unir
dos estructuras de datos, con determinadas propiedades, en una estructura mayor, que cumple la
misma propiedad.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 3
La inserción se logra mezclando una pagoda formada por un elemento con aquella en la que se
inserta el nuevo elemento. Esta forma de realizar la operación cubre el caso en el cual el nuevo
elemento es el menor de todos.

Figura 18.3a. Pagoda con un elemento.

En el ejercicio E1, se propone otro algoritmo de inserción, aplicando la definición de una


pagoda.

El descarte, remoción o selección de la raíz, que tiene el valor de prioridad mínimo, va seguida
de la mezcla de las subpagodas izquierda y derecha de la raíz.

Éstas se obtienen desplazando los elementos menores a la posición de la nueva raíz de las
subpagodas. Esto se muestra en la Figura 18.4, considerando que se descartó la raíz de valor 1,
en la Figura 18.3.
left
2 3

7
4 6

9 5

Figura 18.4. Subpagodas de la raíz, de la Figura 18.3.

Si p apunta a la raíz, y la pagoda izquierda es apuntada por left, el siguiente segmento determina
el puntero left, que define la subpagoda izquierda. Se tiene código especular para obtener la
pagoda derecha.

if ( p->left == p ) left = NULL;


else
{ for ( left = p->left; left->left != p; left = left->left) ;
left->left = p->left;
};

18.4. Análisis de la mezcla.

La mezcla de dos pagodas a y b, en rn, se realiza uniendo la lista circular izquierda de una
pagoda con la lista circular derecha de la otra.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
En la Figura 18.5, se eligió la lista circular izquierda de la pagoda a la izquierda de la Figura
18.4. Se han abstraído los elementos que no forman parte de las listas circulares, empleando
pequeños triángulos para los descendientes.
Los códigos de las funciones figuran en la segunda referencia, al final del capítulo.
b
rn
2 4

3 7 5

Figura 18.5. Listas circulares de las pagodas que se desea mezclar.

Se recorren ambas listas en forma descendente, colocando en una nueva pagoda los elementos
de las listas circulares; es decir agregando el 7, luego el 5, después el 4, a continuación el 3, y
finalmente el 2. Se emplea rn para apuntar a la nueva pagoda en formación.

Para recorrer las listas se emplean los punteros la y lb. El siguiente segmento inicia los punteros
a las listas circulares:

la = a->right; //apunta al mayor de lista descendente derecha


a->right = NULL; // marca fin de lista derecha
lb = b->left; //apunta al mayor de lista descendente izquierda
b->left = NULL; // marca fin de lista izquierda

rn = NULL; //forma nueva pagoda con raíz rn


b
rn
2 lb 4

3 la 7 5

Figura 18.6. Inicio de listas circulares descendentes.

Si como en el caso del ejemplo, el elemento mayor se encuentra en la lista derecha, el siguiente
código va formando la nueva pagoda:

t = la->right;
if ( rn==NULL ) la->right = la; // pagoda con un elemento. Padre de sí mismo
else
{ la->right = rn->right; //apunta al mayor de lista derecha

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 5
rn->right = la; //cierra lista circular
};
rn = la; //la raíz siempre apunta al menor
la = t; //avanza por la derecha

Después de ejecutado el segmento anterior, del cual se ejecuta el if, la Figura 18.7, muestra la
situación:
rn

3 la 5

Figura 18.7. Después de insertado el 7.

Si se vuelve a repetir el código anterior, se ejecutará el else, y luego de éste se tiene la Figura
18.8.

t
a rn
3 la 5 7

Figura 18.8. Después de insertado el 5, luego del else.

El segmento finaliza actualizando la nueva pagoda, que incluye al 7 y al 5. Se muestra ahora el


triángulo rojo descendiente del 5, expandido, de acuerdo a la Figura 18.4. Nótese que no se
alteran los punteros de los elementos del triángulo rojo.

la rn
a

3 5

8 7

Figura 18.9. Después de insertado el 5.

De acuerdo a la Figura 18.6, ahora debe agregarse el nodo 4, que pertenece a la lista circular
izquierda, ya que es mayor que el nodo que aún resta considerar en la lista circular derecha. Para
esto debe ejecutarse el código especular del anterior:

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
t = lb->left;
if ( rn==NULL ) lb->left = lb;
else { lb->left = rn->left;
rn->left = lb;
};
rn = lb;
lb = t;

Luego de la ejecución, la nueva pagoda, se muestra a la izquierda en la Figura 18.10. Luego se


repite el proceso en la lista derecha para agregar el nodo 3.
rn
3
rn

4 4

8 8

5 5

7 7

Figura 18.10. Formación de pagodas, después de agregar los nodos: 4, 3.

Habiéndose agotado la lista circular derecha, es preciso finalizar la pagoda agregando el resto de
la lista circular izquierda, esto puede lograrse, con el siguiente segmento:

b->left = rn->left;
rn->left = lb;
b lb
2

Figura 18.11. Resto de lista circular izquierda.

Finalmente, después de agregar el nodo con valor 2 y su descendencia derecha, donde el


triángulo se ha reemplazado por su equivalente, de acuerdo a la Figura 18.4, se tiene la Figura
18.12, la pagoda queda apuntada por b.

Si se termina primero la lista circular izquierda, debe ejecutarse el código especular:

a->right = rn->right;
rn->right = la;

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 7
Quedando, en este caso, la pagoda apuntada por a.

3 6

4 9

Figura 18.12. Mezcla de las pagodas de la Figura 18.4.

18.5. Definición de tipos.

Se tiene la siguiente definición para la estructura del nodo:


typedef struct bintree
{ int prioridad;
struct bintree * left;
struct bintree * right;
} nodo, *pnodo ;

18.6. Creación de nodo.

pnodo getnodo(int dato)


{ pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) ==NULL)
{ printf("Error: Memoria. \n"); exit(1);}
else
{ //forma pagoda de un elemento
p->prioridad=dato; p->left=p; p->right=p;
}
return(p);
}

18.7. Función mezclar.

La función completa para la mezcla de dos pagodas a y b, contempla los casos particulares en
los cuales una de las pagodas es nula.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
pnodo merge( pnodo a, pnodo b )
{pnodo la, lb, rn, temp;
if ( a==NULL ) return( b );
else if ( b==NULL ) return( a );
else
{la = a->right; //En pagoda a, la apunta al mayor de lista descendente derecha
a->right = NULL; // marca fin de lista derecha
lb = b->left; //En pagoda b, lb apunta al mayor de lista descendente izquierda
b->left = NULL; // marca fin de lista izquierda
rn = NULL; //forma nueva pagoda con raíz rn
/*** Merging loop ***/
while ( la!=NULL && lb!=NULL )
if ( la->prioridad > lb->prioridad ) //mantiene el de menor prioridad en la raíz
{ temp = la->right; //colocando en la nueva pagoda primero el mayor de ambas listas.
if ( rn==NULL ) la->right = la; // pagoda con un elemento. Padre de sí mismo
else
{ la->right = rn->right; //apunta al mayor de lista derecha
rn->right = la; //cierra lista circular
};
rn = la; //apunta al menor
la = temp; //avanza por la derecha
}
else //especular
{ temp = lb->left;
if ( rn==NULL ) lb->left = lb;
else
{ lb->left = rn->left;
rn->left = lb;
};
rn = lb;
lb = temp;
};
/*** Si se termina una de las listas circulares, se agrega el resto de la otra ***/
if ( lb==NULL )
{ a->right = rn->right;
rn->right = la;
return( a );
}
else //la==NULL especular
{ b->left = rn->left;
rn->left = lb;
return( b );
}
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 9
18.8. Descartar.

pnodo descartar( pnodo pq )


{
pnodo le, ri;
if ( pq==NULL ) {printf("Error: Descarte en pagoda vacía.\n"); return(NULL);}
else
{ /*** Encuentra le descendiente izquierdo de pq ***/
if ( pq->left == pq ) le = NULL;
else
{ le = pq->left;
while ( le->left != pq ) le = le->left;
le->left = pq->left;
};
/*** Encuentra ri descendiente derecho de pq ***/
if ( pq->right == pq ) ri = NULL;
else
{ ri = pq->right;
while ( ri->right != pq ) ri = ri->right;
ri->right = pq->right;
};
/*** mezcla pagodas ***/
//Debería usarse el elemento obtenido.
printf("%d ", pq->prioridad); //Modo debug: el que se saca se imprime
free(pq);
return( merge( ri, le ) );
}
}

18.9. Insertar.

pnodo insertar( pnodo pq, int valor )


{ //forma pagoda con un elemento
pnodo nuevo=getnodo(valor);
return( merge( pq, nuevo ) );
}

Si se introducen los valores de los nodos en forma ascendente, se produce una lista circular
izquierda con todos los nodos menos la raíz, y una derecha vacía. La pagoda degenera en una
lista circular. En este caso el costo de insertar n ítems es O(n), ya que en cada inserción se
realiza una vez el ciclo while de la función mezclar; y el descarte es O(1), ya que una de las
listas circulares es vacía y no se ejecuta el ciclo while de la función mezclar.
En el ejercicio E1, se muestra un diseño alternativo de la inserción en una pagoda, la cual se
construye aplicando las propiedades de una pagoda, sin implementar en base a la operación
mezclar.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
18.10. Mostrar listas circulares de la raíz de una pagoda.

int prtpagoda(pnodo p)
{ pnodo t;
if (p!=NULL) printf("rn=%d ri->", p->prioridad);
else {printf("Pagoda nula\n"); return (0);}
for(t=p->right; t!=NULL && t!=p; t=t->right) printf("%d ", t->prioridad);
printf(" le->");
for(t=p->left; t!=NULL && t!=p; t=t->left) printf("%d ", t->prioridad); putchar('\n');
return(1);
}

18.11. Test de las funciones.

pnodo pagoda=NULL;

srand(1); //se generan 20 claves aleatorias.


for(i=0; i<20; i++) pagoda=insertar(pagoda, rand()%100);
prtpagoda(pagoda);
for(i=0; i<20; i++) pagoda=descartar(pagoda);
putchar('\n');
prtpagoda(pagoda);

Lo cual genera:

rn=12 ri->22 17 le->51 32


12 17 22 25 32 43 48 50 51 54 58 61 69 80 81 82 97 97 98 98
Pagoda nula

Nótese que la función descartar, va imprimiendo los valores de los ítems seleccionados, de
menor a mayor. El ciclo while de mezclar se repite 34 veces para la inserción de los 20 items, y
37 veces para descartarlos; esta cuenta es fácil de obtener agregando un contador de las veces
que se ejecuta el lazo de mezcla. Nótese que para 20 ítems, resultan en este caso aleatorio listas
circulares de largo dos.

Ejercicios.

E18.1.

Verificar que la siguiente función inserta en una pagoda, cuya dirección es pasada por
referencia.

void enqueue(pnodo *r, int valor)


{ pnodo p, q=*r, n=getnodo(valor);
if (q==NULL) *r=n; //pagoda nula, inserción trivial
else
{ p=q->right;

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 11
if(valor >= p->prioridad) // nuevo al inicio lista derecha
{ n->right=q->right;
q->right=n;
}
else if (valor<=q->prioridad) //reemplaza raíz en fondo lista izquierda
{ n->left=q->left;
q->left=n;
*r=n;
}
else //hay que recorrer la lista
{ while(valor < p->right->prioridad) p=p->right;
n->right=p->right; //pega al nuevo el descendiente derecho
p->right=q->right; //pega inicio de lista de q en p
q->right=n; //acorta lista derecha
n->left=p->left; //resto de lista derecha la pega a la izquierda del nuevo
p->left=n;
}
}
} /* enqueue */

E18.2.

Verificar que la siguiente función retorna un puntero al nodo con valor mínimo, en una pagoda,
cuya dirección es pasada por referencia.

pnodo dequeue( pnodo * q )


{
pnodo lb, rb, f, t, dequeue_result;
dequeue_result = *q;
lb = (*q)->right;
rb = (*q)->left;
if (lb == rb) { /* Hay un solo nodo en la pagoda */
*q = NULL;
} else if (lb == *q) { /* no hay descendiente derecho */
do { rb = rb->left; } while (!(rb->left == *q));
rb->left = (*q)->left;
*q = rb;
} else if (rb == *q) { /* no hay descendiente izquierdo. especular */
do { lb = lb->right;} while (!(lb->right == *q));
lb->right = (*q)->right;
*q = lb;
} else { /* descarte no trivial */
/* compara hijos izquierdo y derecho, prepara mezcla de sub-pagodas */
if (lb->prioridad >= rb->prioridad) {
f = lb;
t = lb->right;
lb->right = lb;

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
lb = t;
} else { //especular
f = rb;
t = rb->left;
rb->left = rb;
rb = t;
}

/* mezcla ramas izquierda y derecha, desde el fondo hasta el tope */


while ((lb != *q) && (rb != *q)) {
if (lb->prioridad >= rb->prioridad) {
t = lb->right;
lb->right = f->right;
f->right = lb;
f = lb;
lb = t;
} else { //especular
t = rb->left;
rb->left = f->left;
f->left = rb;
f = rb;
rb = t;
}
}
/* Coloca lo que quedo de la mezcla en el tope de la cola */
if (lb == *q) {
t = f->left;
f->left = rb;
while (rb->left != *q) { rb = rb->left; }
rb->left = t;
*q = rb;
} else { //especular
t = f->right;
f->right = lb;
while (lb->right != *q) {lb = lb->right;}
lb->right = t;
*q = lb;
}
}
return dequeue_result;
} /* dequeue */

Luego de usar el nodo, debería liberarse el espacio asociado al nodo.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 13
E18.3.

Verificar que las siguientes funciones implementan una cola de prioridad empleando una lista
simplemente enlazada, ordenada por el valor de prioridad.

typedef struct lista


{ int prioridad;
struct lista * proximo;
} nlista, *plista ;

plista getlista(int dato)


{ plista p=NULL;
if ( (p= (plista) malloc(sizeof(nlista))) ==NULL)
{ printf("Error: Memoria lista. \n"); exit(1);}
else { p->prioridad=dato; p->proximo=NULL; }
return(p);
}

//encola ordenado por prioridad.


//prioridades iguales en orden de llegada. Estable
plista encola (plista *p, int k )
{ plista p1, p2, p3;
if (*p==NULL)
{p1=getlista(k); *p=p1; return(p1);}
else
for( p2 = NULL, p1 = *p; p1 != NULL && p1->prioridad <=k; p2 = p1, p1 = p1->proximo );

p3= (plista) malloc (sizeof (nlista)) ;


if(p3!=NULL)
{ p3->prioridad = k;
if (p2 == NULL) { /* inserta al inicio */
p3->proximo = p1;
*p=p3;
}
else
{ p3->proximo = p2->proximo;
p2->proximo = p3;
}
}
return p3 ;
}

//desencola en lista ordenada.


plista desencola(plista *q)
{ plista t=*q;
if (t!=NULL)
{*q=t->proximo; return (t);}

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
else return(NULL);
}

void prtcola(plista p)
{
for( ; p!=NULL; p=p->proximo)
printf( "%d ", p->prioridad);
putchar('\n');
}

E18.4.

Modificar las funciones para tener selección de máximo, en lugar de mínimo.

Referencias.

J. Francon, G. Viennot, and J. Vuillemin, “Description and analysis of an efficient priority


queue representation”, Proc. 19th Annual Symp. on Foundations of Computer Science. IEEE,
1978, pages 1-7.

Gaston H. Gonnet, R. Baeza-Yates, “Handbook of Algorithms and Data Structures in Pascal


and C”, Addison-Wesley Pub., May 1991.

Douglas W. Jones, “An empirical comparison of priority queue and even-set implementations”,
Communications of the ACM, April 1986 Volume 29 Number 4.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Pagodas. 15
Índice general.

CAPÍTULO 18 ........................................................................................................................................... 1
PAGODAS. SELECCIONAR. .................................................................................................................. 1
18.1. PROPIEDADES. ................................................................................................................................. 1
18.2. LISTAS CIRCULARES DE LA RAÍZ DE UNA PAGODA............................................................................ 2
18.3. MEZCLA DE PAGODAS. ..................................................................................................................... 2
18.4. ANÁLISIS DE LA MEZCLA. ................................................................................................................ 3
18.5. DEFINICIÓN DE TIPOS. ...................................................................................................................... 7
18.6. CREACIÓN DE NODO......................................................................................................................... 7
18.7. FUNCIÓN MEZCLAR. ......................................................................................................................... 7
18.8. DESCARTAR. .................................................................................................................................... 9
18.9. INSERTAR. ....................................................................................................................................... 9
18.10. MOSTRAR LISTAS CIRCULARES DE LA RAÍZ DE UNA PAGODA. ...................................................... 10
18.11. TEST DE LAS FUNCIONES. ............................................................................................................. 10
EJERCICIOS. ............................................................................................................................................ 10
E18.1. ................................................................................................................................................ 10
E18.2. ................................................................................................................................................ 11
E18.3. ................................................................................................................................................ 13
E18.4. ................................................................................................................................................ 14
REFERENCIAS. ........................................................................................................................................ 14
ÍNDICE GENERAL. ................................................................................................................................... 15
ÍNDICE DE FIGURAS................................................................................................................................. 15

Índice de figuras.

FIGURA 18.1. ÁRBOL DE PRIORIDAD Y PAGODA EQUIVALENTE. ................................................................... 1


FIGURA 18.2. PAGODAS. .............................................................................................................................. 2
FIGURA 18.3. PAGODA Y SUS LISTAS CIRCULARES PRINCIPALES. ................................................................. 2
FIGURA 18.3A. PAGODA CON UN ELEMENTO. ............................................................................................... 3
FIGURA 18.4. SUBPAGODAS DE LA RAÍZ, DE LA FIGURA 18.3. ...................................................................... 3
FIGURA 18.5. LISTAS CIRCULARES DE LAS PAGODAS QUE SE DESEA MEZCLAR............................................. 4
FIGURA 18.6. INICIO DE LISTAS CIRCULARES DESCENDENTES. ..................................................................... 4
FIGURA 18.7. DESPUÉS DE INSERTADO EL 7. ................................................................................................ 5
FIGURA 18.8. DESPUÉS DE INSERTADO EL 5, LUEGO DEL ELSE. .................................................................... 5
FIGURA 18.9. DESPUÉS DE INSERTADO EL 5. ................................................................................................ 5
FIGURA 18.10. FORMACIÓN DE PAGODAS, DESPUÉS DE AGREGAR LOS NODOS: 4, 3...................................... 6
FIGURA 18.11. RESTO DE LISTA CIRCULAR IZQUIERDA................................................................................. 6
FIGURA 18.12. MEZCLA DE LAS PAGODAS DE LA FIGURA 18.4. .................................................................... 7

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 19

Árboles izquierdistas. Seleccionar.

Un leftist es una representación de un árbol binario tal que los hijos tienen claves mayores o
iguales a las del padre y se mantiene balanceado.

Es un algoritmo de selección que puede agregar ítems en forma dinámica, a diferencia del heap
implícito, árbol binario embebido en un arreglo, que utiliza memoria estática.

19.1. Propiedades.

Usa un árbol binario con estructura de heap, el cual se representa con punteros de padres a hijos.
Los dos hijos, de cada nodo, están ordenados tal que la trayectoria derecha es la más corta desde
la raíz hasta un nodo externo, el largo de la trayectoria derecha se almacena en cada nodo.

La distancia equivale a contar los nodos recorridos desde el nodo hasta la hoja ubicada más a la
derecha de ese nodo. Las hojas tienen distancia 1.

Si se aplican las propiedades anteriores y se introducen nodos con prioridades desde el 1 hasta el
10, en orden ascendente, se forma al árbol binario de prioridad leftist, que se muestra en la
Figura 19.1. Las distancias se han colocado a la derecha del nodo. Se aprecia que el árbol tiene
más nodos ubicados en ramas izquierdas, de allí el nombre. Debe notarse que el árbol está
balanceado en forma similar a un heap, pero la ubicación de las hojas en los últimos niveles es
diferente.

1 3

3 2 2 2

4 1 5 1 7 2 6 1

8 1 9 1 10 1

Figura 19.1. Árbol de prioridad leftist.

19.2. Mezcla de leftist.

La operación fundamental es la mezcla de dos leftist.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos

Para insertar se colocan los nuevos datos en un nodo, en un árbol con un elemento, y se mezcla
con el árbol existente.

La distancia se calcula desde la raíz hasta el nodo externo derecho.

n 1

Figura 19.2. Leftist con un elemento.

Para descartar el mínimo, se remueve la raíz, y se mezclan los subárboles izquierdo y derecho.

Dos leftist se mezclan, en un nuevo árbol, fundiendo sus ramas de más a la derecha, en orden
descendente; es decir el nodo con menor valor de prioridad queda más abajo. Debido a la forma
en que se define la operación mezcla, y para lograr una operación eficiente, la estructura intenta
mantener las trayectorias derechas lo más cortas que sea posible.

Luego se mantiene, descendiendo por la derecha, el balance del árbol registrando, para cada
item en el descenso, la distancia a la hoja derecha más cercana, y siempre manteniendo
ordenados los dos hijos de un ítem de tal modo que la trayectoria hasta la hoja más cercana esté
a través del hijo derecho.

Las operaciones son O(log(n)).

19.3. Análisis de la mezcla.

Si se introducen, nodos con prioridades: 3, 6, 5 se obtiene el leftist a que se muestra a la


izquierda en la Figura 19.3. A la derecha se muestra el leftist b, formada por la inserción de
nodos con prioridades: 4, 8, 7, en ese orden. La mezcla se formará en la nueva raíz r.

a r b
3 2 4 2

6 1 5 1 8 1 7 1

Figura 19.3. Mezcla de dos leftist.

Se mezclan las ramas derechas de los árboles. Manteniendo el nodo con mayor prioridad en la
raíz de la mezcla.

El siguiente segmento describe el bloque repetitivo, que realiza la mezcla de las ramas derechas:

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Leftist. 3
if (a->prioridad < b->prioridad) //el de menor prioridad queda abajo
{ t = a->right; a->right = r;
r = a; a = t;
}
else
{ t = b->right; b->right = r;
r = b; b = t;
}

En el caso del ejemplo de la Figura 19.3, en primer lugar se ejecuta el código del if, resultando
la Figura 19.4:
r a b
3 5 4 2

6 8 1 7 1

Figura 19.4. Luego de ejecutado el if.

Si se repite el lazo de mezcla, se ejecutará el else, resultando la Figura 19.5:

a r b
5 4 7

8 3

Figura 19.5. Luego de ejecutado el else.

Se vuelve a repetir el lazo de mezcla, ejecutándose nuevamente el if, se agota la lista derecha
del leftist a, y resulta la Figura 19.6:
a r b
5 1 7 1

4 2

8 1 3 2

6 1

Figura 19.6. Luego de agotada la lista derecha a.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
El ciclo de mezcla termina cuando se agota una de las ramas derechas, el siguiente código
ilustra la terminación, registrando la distancia d del resto de la lista que no se mezcló:

if (b == NULL) //leftist derecho nulo o llegó al final


{ d = a->distancia; break;}
else if (a== NULL) //leftist izquierdo nulo
{ a = b; d = a->distancia; break; } //deja en a el resto de la lista derecha

Una vez ejecutado el código de terminación, resulta la Figura 19.7, a la derecha. Notar que se
alteran las distancias de los nodos del trayecto formado en la mezcla.

A continuación se mantiene el árbol balanceado, y al mismo tiempo se recupera el ordenamiento


según un heap. Esto lo realiza descendiendo por la derecha del árbol r, recién formado, pasando
en cada nodo, al lado derecho las ramas izquierdas más livianas; es decir, si tienen menor
distancia que las del lado derecho. Al mismo tiempo se van recalculando las distancias de los
nodos involucrados en el trayecto.

//Al inicio d es la distancia del resto de la rama derecha.


while (r != NULL)
{ if (r->left == NULL) dizq = 0;
else dizq = r->left->dist;
//dizq es largo de trayecto izquierdo.
q = r->right; //desciende por la derecha
if (dizq < d)
{ r->right = r->left; //rama izquierda más liviana pasa a la derecha
d = dizq + 1; // el nodo r tiene la distancia izquierda más la del nodo r.
r->left = a; //los de mayor prioridad los pega por la izquierda
}
else
{ d = d + 1; //se incrementa distancia de la nueva raíz
r->right = a;
}
r->distancia = d; //actualiza distancia del nodo r
a = r;
r = q; //desciende un nivel por la derecha
}
Se inicia el balance del árbol, a partir de la Figura 19.7 a la izquierda. En la primera iteración
dizq es 0, y d es 1. Entonces pasa al lado derecho la rama más liviana, resultando la Figura 19.7,
a la derecha.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Leftist. 5
r a
5 1 7 1
a
r
4 2 5 1 4 2

7 1
8 1 3 2 8 1 3 2

d=1
6 1 6 1

Figura 19.7. Al inicio, y luego de pasar rama liviana a la derecha.

En la siguiente iteración, dizq es 1, y d es 1, por lo tanto no pasa el lado izquierdo hacia el


derecho, incrementa d, resultando:
a r
4 2 d=2
3 2

8 1 5 1
6 1

7 1

Figura 19.8. Luego de dejar el nodo 8 a la izquierda.

En la última iteración, dizq es 1, y d es 2, por lo tanto se pasa el nodo 6 a la derecha, resultando:


a r
3 2

4 2 6 1

8 1 5 1
d=2

7 1

Figura 19.9. Balance. Mezcla de los leftist de la Figura 19.3.

Nótese que el árbol tiene un leve desbalance por la izquierda.

19.4. Definición de tipos.

Se tiene la siguiente definición para la estructura del nodo:

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
typedef struct bintree
{ struct bintree * left;
struct bintree * right;
int prioridad;
int distancia;
} nodo, *pnodo;

typedef pnodo queue;

19.5. Creación de nodo y de cola.

pnodo getnodo(int prioridad)


{ pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) ==NULL)
{ printf("Error: Memoria. \n"); exit(1);}
else
{ //forma leftist de un elemento
p->prioridad=prioridad; p->left=NULL; p->right=NULL;
p->distancia=1; //distancia desde la raíz al nodo externo
}
return(p);
}

void initqueue( queue* q )


/* inicia una variable de tipo queue */
{ *q = NULL;
} /* initqueue */

19.6. Test de cola vacía.

int emptyqueue( queue q )


/* test para determinar si la cola está vacía */
{ return (q == NULL);
} /* emptyqueue */

19.7. Insertar. Encolar.

Se pasa la cola por referencia.


void enqueue( queue* qraiz , int prioridad)
{ pnodo nuevo=getnodo(prioridad);
merge( qraiz, nuevo );
} /* enqueue */

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Leftist. 7

qraiz leftist

raíz

Figura 19.10. Paso por referencia a la cola de prioridad leftist.

19.8. Seleccionar el mínimo. Desencolar.

Se pasa la cola por referencia. Retorna el nodo con valor de prioridad mínimo.

pnodo dequeue( queue* qraiz )


{ pnodo p= *qraiz; //extrae el mínimo
if (p!=NULL)
{ if ((p->left != NULL) || (p->right != NULL))
merge( &(p->left), p->right );
*qraiz = p->left;
}
return p;
} /* dequeue */

19.9. Función mezclar.

void merge( queue *p, pnodo q )


/* Mezcla dos árboles izquierdistas.
Cualquiera de ellos puede estar formado por un nodo simple */
{ int d, dizq;
pnodo t, r;
r = NULL; // raíz para la mezcla
/* Recolecta nodos de ramas derechas en forma descendente */
while (1)
{
if (q == NULL) //leftist derecho nulo o llego al final
{ d = (*p)->dist;
break;
}
else if (*p == NULL) //leftist izquierdo nulo
{ *p = q; //cambia la raíz
d = (*p)->dist;
break;
}
else if ((*p)->prioridad < q->prioridad) //el de menor prioridad queda abajo
{ t = (*p)->right;
(*p)->right = r;
r = *p;

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
*p = t;
}
else
{ t = q->right;
q->right = r;
r = q;
q = t;
}
}
/* mantiene el balance. Recuperando la estructura de heap */
//Al inicio d es la distancia del resto de la rama derecha.
while (r != NULL)
{ if (r->left == NULL) dizq = 0;
else dizq = r->left->distancia;
//dizq es la distancia del nodo izquierdo de r.
q = r->right; //desciende por la derecha
if (dizq < d)
{ r->right = r->left; //rama izquierda más liviana pasa a la derecha
d = dizq + 1; // el nodo r tiene la distancia izquierda más la del nodo r.

r->left = *p; //los de mayor prioridad los pega por la izquierda


}
else
{ d = d + 1; //asciende un nivel en el trayecto
r->right = *p;
}
r->distancia = d; //actualiza distancia del nodo r
*p = r;
r = q; //desciende un nivel por la derecha
}
} /* merge */

19.10. Test de las funciones.

queue leftist=NULL;
srand(1);
for(i=0; i<20; i++) enqueue(&leftist, rand()%100);
for(i=0; i<20; i++)
{ t=dequeue(&leftist);
printf("%d ", t->prioridad); free(t);
}; putchar('\n');

19.11. Ejemplo.

Para el leftist de la Figura 19.1, si se elimina el mínimo, luego de formar la mezcla con
prioridades descendentes se obtiene la Figura 19.11 a la izquierda. Debe notarse que las
distancias de los nodos involucrados en el trayecto, 5, 3 y 2, tienen distancias no válidas.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Leftist. 9
Luego el segmento que mantiene las propiedades de un leftist, produce el heap, que se muestra
a la derecha de la Figura 19.11. Se han recalculado las distancias de los nodos 5, 3 y 2 de
acuerdo a la definición.
r a 2 3

5 1 6 1

3 2 10 1 7 2 3 2

4 1 2 1 9 1 4 1 5 1
8 1

7 2 6 1

8 1 9 1 10 1

Figura 19.11. Extracción del mínimo en el leftist de la Figura 19.1.

Si se inserta un nodo con valor 1, en el leftist a la derecha de la Figura 19.11, la mezcla se


ejecuta más rápidamente, y deja un nuevo leftist, con rama derecha vacía, lo que se muestra en
la Figura 19.12.
1 1

2 3

7 2 3 2

8 1 9 1 4 1 5 1

6 1

10 1

Figura 19.12. Inserción de nodo con menor prioridad que el de la raíz de la Figura 19.11.

Si se inserta un nodo con valor 11, en el leftist a la derecha de la Figura 19.11, la mezcla se
ejecuta más lentamente, ya que debe formarse el trayecto descendente y luego rebalancear los
nodos del trayecto. Resulta el leftist que se muestra en la Figura 19.13.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

2 3

7 2 3 2

8 1 9 1 5 2 4 1

6 1 11 1

10 1

Figura 19.13. Inserción de nodo con mayor prioridad que el de la raíz de la Figura 19.11.

Referencias.

Knuth. D. E. “The Art of Computer Programming”. Vol. 3, Sorting and Searching. Addison-
Wesley, Reading, Mass., 1973. Knuth acredita el trabajo a Clark A. Crane.

C. A. Crane, “Linear lists and priority queues as balanced binary trees”, Technical Report
STAN-Cs-72-259, Computer Science Dept., Stanford Univ., Stanford, CA, 1972.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Leftist. 11
Índice general.

CAPÍTULO 19 ........................................................................................................................................... 1
ÁRBOLES IZQUIERDISTAS. SELECCIONAR. .................................................................................. 1
19.1. PROPIEDADES. ................................................................................................................................. 1
19.2. MEZCLA DE LEFTIST. ....................................................................................................................... 1
19.3. ANÁLISIS DE LA MEZCLA. ................................................................................................................ 2
19.4. DEFINICIÓN DE TIPOS. ...................................................................................................................... 5
19.5. CREACIÓN DE NODO Y DE COLA. ...................................................................................................... 6
19.6. TEST DE COLA VACÍA. ...................................................................................................................... 6
19.7. INSERTAR. ENCOLAR. ...................................................................................................................... 6
19.8. SELECCIONAR EL MÍNIMO. DESENCOLAR. ........................................................................................ 7
19.9. FUNCIÓN MEZCLAR. ......................................................................................................................... 7
19.10. TEST DE LAS FUNCIONES. ............................................................................................................... 8
19.11. EJEMPLO. ....................................................................................................................................... 8
REFERENCIAS. ........................................................................................................................................ 10
ÍNDICE GENERAL. ................................................................................................................................... 11
ÍNDICE DE FIGURAS................................................................................................................................. 11

Índice de figuras.

FIGURA 19.1. ÁRBOL DE PRIORIDAD LEFTIST. .............................................................................................. 1


FIGURA 19.2. LEFTIST CON UN ELEMENTO. .................................................................................................. 2
FIGURA 19.3. MEZCLA DE DOS LEFTIST. ....................................................................................................... 2
FIGURA 19.4. LUEGO DE EJECUTADO EL IF. .................................................................................................. 3
FIGURA 19.5. LUEGO DE EJECUTADO EL ELSE. ............................................................................................. 3
FIGURA 19.6. LUEGO DE AGOTADA LA LISTA DERECHA A. ........................................................................... 3
FIGURA 19.7. AL INICIO, Y LUEGO DE PASAR RAMA LIVIANA A LA DERECHA. .............................................. 5
FIGURA 19.8. LUEGO DE DEJAR EL NODO 8 A LA IZQUIERDA. ....................................................................... 5
FIGURA 19.9. BALANCE. MEZCLA DE LOS LEFTIST DE LA FIGURA 19.3. ....................................................... 5
FIGURA 19.10. PASO POR REFERENCIA A LA COLA DE PRIORIDAD LEFTIST. .................................................. 7
FIGURA 19.11. EXTRACCIÓN DEL MÍNIMO EN EL LEFTIST DE LA FIGURA 19.1. ............................................. 9
FIGURA 19.12. INSERCIÓN DE NODO CON MENOR PRIORIDAD QUE EL DE LA RAÍZ DE LA FIGURA 19.11. ....... 9
FIGURA 19.13. INSERCIÓN DE NODO CON MAYOR PRIORIDAD QUE EL DE LA RAÍZ DE LA FIGURA 19.11. .... 10

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 20

Skew heaps. Seleccionar.

Un skew heap es una representación de un árbol binario tal que los hijos tienen claves mayores
o iguales a las del padre y que mantiene las trayectorias derechas lo más cortas que sea posible.

Es un algoritmo de selección que puede agregar ítems en forma dinámica, a diferencia del heap
implícito, árbol binario embebido en un arreglo, que utiliza memoria estática.

Es un algoritmo derivado de los árboles izquierdistas, leftist, pero que no almacena información
de los trayectos derechos en cada nodo. Su característica es que es auto ajustable o
autoorganizado, en el sentido de que en cada operación de actualización se efectúa una
modificación sencilla de la estructura, tal que esto reduzca el costo de las operaciones futuras. Si
bien esto incurre en un costo mayor de las operaciones, en promedio se logra un
comportamiento eficiente. Esto lo logra no intentando optimizar el peor caso de una operación
individual, sino que el peor caso de una secuencia de operaciones.

La codificación resulta más sencilla que los algoritmos que garantizan el balance de la
estructura, y demandan menos espacio para almacenar cada nodo.

20.1. Mezcla de dos skew heaps.

En forma similar a un leftist, las operaciones están basadas en la mezcla de los trayectos
derechos de los dos heaps, pero con la modificación de que en lugar de mantener distancias
derechas más cortas, se efectúa directamente un intercambio de los hijos derecho e izquierdo de
cada nodo en el trayecto de la mezcla; salvo en el último nodo.

1
2
3 4
3 5

7 6
8 7 8
8 9 10

Figura 20.1. Mezcla de trayectos derechos de dos heaps.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
La Figura 20.1 muestra dos heaps. La trayectoria derecha del ubicado a la izquierda está
formada por los nodos: 1, 4 y 6. La trayectoria derecha del heap ubicado a la derecha está
formada por los nodos: 2, 5, y 8. Las trayectorias derechas se recorren de arriba hacia abajo.

En la Figura 20.2, a la izquierda se muestra la mezcla de los dos trayectos derechos, se forma
una ruta derecha más larga, formada por la secuencia: 1, 2, 4, 5, 6 y 8. Luego a la derecha se
muestra el intercambio de los hijos de cada nodo del trayecto derecho; lo cual deja un skew
heap con trayecto derecho de un nodo.
1
1

3 2 2 3

3 4
4 3

8 7 5 5 7 8

8 9 7 6 6 7 8 9

10 8 8 10

Figura 20.2. Mezcla e intercambio de hijos en el trayecto.

El algoritmo puede realizar simultáneamente la mezcla y el intercambio de los hijos, para esto
se recorren los dos trayectos derechos de arriba hacia abajo (top-down) y cuando se llega al final
de uno de los trayectos, el resto del otro se enlaza al final y el proceso termina.

Sólo los nodos visitados en el trayecto tienen sus hijos intercambiados, el último nodo que tiene
intercambio de hijos es el último nodo del trayecto que se recorre totalmente; el nodo con valor
6 en la Figura 20.2.

20.2. Operación Mezcla con intercambio. Top-down.

Se tienen que h1 apunta al heap cuya raíz tiene prioridad menor, y h2 al heap con raíz con
prioridad mayor, y h al heap con la mezcla, que inicialmente tiene valor nulo.

h1 h2
a d

b c e f

Figura 20.3. Mezcla e intercambio de hijos en el trayecto.

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 3
El siguiente segmento inicia el heap h con la mezcla, e intercambia los hijos; al mismo tiempo
inicia la variable y, que recuerda al último nodo agregado al heap con la mezcla:

h = h1;
y = h1; // apunta al último agregado
h1 = y->right; //desciende en trayecto derecho
y->right = y->left; //intercambia hijos
h
y
a
h1
c b

Figura 20.4. Inicio de la mezcla con intercambio.

Luego debe repetirse un procesamiento similar hasta agotar uno de los trayectos:

while (h1 != NULL)


{ if (h1->prioridad > h2->prioridad) /* selecciona menor */
{ temp = h1;
h1 = h2;
h2 = temp;
} //ahora h1 apunta al con prioridad menor
y->left= h1; //lo pega por la izquierda
y = h1; // apunta al último agregado
h1 = y->right; //desciende
y->right = y->left; //intercambia hijos del último agregado
}
y->left= h2; //pega el resto del trayecto

Para insertar se colocan los nuevos datos en un nodo, en un heap con un elemento, y se mezcla
con el árbol existente.

n 1

Figura 20.5. Heap con un elemento.

Para descartar el mínimo, se remueve la raíz, y se mezclan los subárboles izquierdo y derecho.

Las operaciones son de costo amortizado O(log(n)).

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

20.3. Definición de tipos.

Se tiene la siguiente definición para la estructura del nodo:

typedef struct bintree


{ struct bintree * left;
struct bintree * right;
int prioridad;
} nodo, *pnodo;

typedef pnodo queue;

20.4. Creación de nodo y de cola.

pnodo getnodo(int prioridad)


{ pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) ==NULL)
{ printf("Error: Memoria. \n"); exit(1);}
else
{ //forma heap de un elemento
p->prioridad=prioridad; p->left=NULL; p->right=NULL;
}
return(p);
}

void initqueue( queue* q )


/* inicia una variable de tipo queue */
{ *q = NULL;
} /* initqueue */

20.5. Test de cola vacía.

int emptyqueue( queue q )


/* test para determinar si la cola está vacía */
{ return (q == NULL);
} /* emptyqueue */

20.6. Insertar. Encolar. Top-down.

Se pasa la cola por referencia.


void enqueue( pnodo n, queue* q )
{ *q = meld( n, *q );
} /* enqueue */

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 5

q skew
*q
raíz

Figura 20.6. Paso por referencia a la cola de prioridad skew.

20.7. Seleccionar el mínimo. Desencolar. Top-down.

Se pasa la cola por referencia. Retorna el nodo con valor de prioridad mínimo.

pnodo dequeue( queue* q )


{
pnodo result= *q;
*q = meld( (*q)->left, (*q)->right );
return result;
} /* dequeue */

20.8. Función mezclar. Top-down.

pnodo meld( queue h1, queue h2 )


/* mezcla dos skew heaps de arriba hacia abajo. top down */
{ pnodo temp, y;
pnodo h;
if (h1 == NULL) { h = h2;}
else if (h2 == NULL) {h = h1;}
else {
if (h1->prioridad > h2->prioridad) { /* selecciona menor */
temp = h1;
h1 = h2;
h2 = temp;
} //ahora h1 apunta al con prioridad menor
h = h1; //inicia h
y = h1; // apunta al último agregado
h1 = y->right; //desciende por la derecha
y->right = y->left; //intercambia
while (h1 != NULL) {
if (h1->prioridad > h2->prioridad) { /* selecciona menor */
temp = h1;
h1 = h2;
h2 = temp;
}
y->left= h1; //lo pega por la izquierda
y = h1; // apunta al último agregado
h1 = y->right; //desciende

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
y->right = y->left; //intercambia hijos del último agregado
}
y->left= h2; //pega el resto del trayecto
}
return h;
} /* meld */

20.9. Test de las funciones.

//listas izquierda y derecha a partir de la raíz


int prtskew(pnodo p)
{ pnodo t;
if (p!=NULL) printf("rn=%d ri->", p->prioridad);
else {printf("Skewheap nulo\n"); return (0);}
for(t=p->right; t!=NULL ; t=t->right) printf("%d ", t->prioridad);
printf(" le->");
for(t=p->left; t!=NULL ; t=t->left) printf("%d ", t->prioridad);
putchar('\n');
return(1);
}

queue skewheap=NULL;

int main (void)


{ pnodo t;
int i;

srand(1);
for(i=0; i<20; i++)
{ enqueue(getnodo(rand()%100), &skewheap );
prtskew(skewheap); //muestra los largos derechos
}

for(i=0; i<20; i++)


{ prtskew(skewheap);
t=dequeue(&skewheap);
//printf("%d ", t->prioridad);
free(t);
};
putchar('\n');
}

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 7
20.10. Mezcla ascendente (bottom-up).

Pueden lograrse mejores resultados para las operaciones de mezcla e inserción, si las mezclas e
intercambios de hijos de los trayectos derechos se efectúa de abajo hacia arriba.

Se inicia la mezcla e intercambio de hijos en los nodos más derechistas de los heaps, cuando se
llega al tope de uno de los trayectos, la raíz de uno de los heaps que se están mezclando, se pega
como hijo derecho del último nodo del otro trayecto, el que aún no ha participado de la mezcla.

El intercambio de hijos se realiza incluyendo el nodo raíz del heap que se agotó primero, luego
no se sigue con el intercambio.

3 4
5

7 6 7 8

8 9 10 8 9

Figura 20.7. Mezcla de trayectos derechos de dos heaps.

En el caso del ejemplo que se muestra en la Figura 20.7, se deben mezclar los nodos 8, 5 con los
nodos 6, 4, 1, en ese orden. El último nodo que intercambia sus hijos es el con valor 5, luego
éste se pega como hijo derecho del nodo con valor 4, que aún no se ha considerado en la
mezcla. El resultado de la mezcla de abajo hacia arriba, se muestra en la Figura 20.8.

3 4

7 5

8 9 6 7

8 10 8
0

Figura 20.8. Mezcla bottom-up de los heaps de la Figura 20.7.

La realización eficiente de esta operación requiere tener punteros al padre y referencias a los
nodos más derechistas de ambos heaps. Sin embargo, en forma similar a las pagodas, pueden
emplearse punteros al padre y al nodo ubicado más a la derecha del hijo izquierdo. La raíz

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
apunta a los dos descendientes de más a la derecha de ambos hijos, lo que facilita la mezcla
hacia arriba. Un nodo sin subárbol izquierdo apunta a sí mismo.
La estructura es cíclica y permite el descarte de cualquier elemento con complejidad O(log n).

La Figura 20.9, a la izquierda, muestra un heap mediante un árbol binario con punteros a los
hijos. A la derecha se muestra con los punteros derechos la referencia al padre, el puntero
derecho de la raíz referencia al hijo más derechista. Con los punteros izquierdos se apunta al
hijo más derechista del subárbol izquierdo, o a sí mismo si no tiene descendientes izquierdos.

1
1

2 3
2 3

4 6 5
4 6 5

9 8 7
9 8 7

Figura 20.9. Árbol de prioridad y skew bottom-up equivalente.

Puede redibujarse como un árbol binario, lo cual se muestra en la Figura 20.10, a la izquierda.
Pero una mejor representación es mediante listas circulares o anillos, en la cual se tienen listas
ordenadas descendentes con los punteros derechos y listas ordenadas ascendentes hacia abajo, lo
cual se muestra a la derecha de la Figura 20.10.

De acuerdo a la Figura 20.10, a la derecha, es preferible denominar down al puntero usualmente


denominado left en un nodo binario.

1 h

1 7 5 3

6 7

8
9 5 6 2
2

8 3
4 9 4

Figura 20.10. Enderezando punteros y representación por listas.

Puede resumirse las propiedades notando que en cada nodo el puntero derecho apunta a un nodo
con menor prioridad, y que hacia abajo se apunta a uno con mayor prioridad. La raíz es la
excepción ya que hacia la derecha apunta a una lista circular ordenada descendentemente y
hacia abajo a una lista ascendente. Desde cualquier nodo pueden seguirse rutas descendentes

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 9
hasta la raíz, mediante los punteros derechos; y rutas ascendentes hasta las hojas, mediante los
punteros hacia abajo.

20.11. Análisis de inserción bottom up.

Si el elemento que se inserta es menor o igual que la raíz, se coloca el nuevo elemento como la
raíz; y se coloca el primer elemento de la lista, el mayor de ésta, como descendiente ascendente
de la nueva raíz. Notar que la raíz antigua ya estaba ligada al elemento mayor. Efectuando tres
asignaciones se logra la Figura 20.11, a partir de la inserción de un nodo con valor 0, en el heap
que se muestra a la derecha de la Figura 20.10. Si se insertan secuencias de nodos con
prioridades descendentes, crecerá la lista ascendente hacia abajo.
h

7 5 3 1

8
6 2

9 4

Figura 20.11. Inserción de elemento con menor prioridad que la raíz.

Si el elemento que se inserta es mayor o igual que el primero de la lista descendente derecha se
lo coloca como el primero de la lista. La Figura 20.12, muestra la inserción del elemento con
prioridad 9, en el heap de la Figura 20.10 a la derecha. Su realización se logra con la escritura de
dos punteros. Si se insertan secuencias de nodos con prioridades ascendentes, crecerá la lista
derecha.

1 9 7 5 3

8
6 2

9 4

Figura 20.12. Inserción de elemento mayor o igual que el primero de la lista derecha.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
Si el elemento que se inserta es mayor que la raíz y menor que el primero de la lista descendente
derecha se recorre la lista descendentemente ordenada hasta encontrar el lugar de inserción, al
mismo tiempo que se van colocando los elementos mayores en las listas ascendentes hacia
abajo.

Esto se logra con dos punteros auxiliares, y que busca la posición para insertar; y el puntero z
que apunta al descendiente hacia abajo.

Al inicio ambos punteros auxiliares apuntan al primero de la lista derecha. Donde n es el nodo
que se insertará, y h apunta a la raíz del heap.

y = z = h->right;
while (n->prioridad < y->right->prioridad)
{ y=y->right; //avanza en la lista descendente.
t=z; z=y->down; y->down=t; // swap(z, y->down)
}
n->right=y->right; n->down=z; //enlaza lista ascendente y descendente.
y->right=h->right=n; // cambia inicio lista y encadena el nuevo nodo

La Figura 20.13 ilustra la inserción del nodo con prioridad 6, en la Figura 20.10, a la derecha.
En este caso no se efectúa el código del while.
h

1 6 5 3

6 2 7 8

9 4

Figura 20.13. Inserción de elemento 6, menor que el primero de la lista derecha.

La Figura 20.14, muestra la inserción de un nodo con valor 4, en el heap de la Figura 20.10, a la
derecha. En este caso se efectúa el código del while, al salir de éste z apunta al nodo con valor 8,
y el puntero y al elemento inmediatamente mayor que el que se insertará, al 5 en este caso.

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 11
h

1 4 3

8 5
6 2

7
9 4

Figura 20.14. Inserción de elemento 4, menor que el primero de la lista derecha.

Debe notarse que en el caso de inserción en medio de la lista, el largo de ésta tiende a acortarse.
Si bien el costo de esta operación es más compleja, las siguientes inserciones resultarán más
simples. Equivale a un pago por adelantado, con el fin de disminuir el costo total de la deuda; es
realmente una amortización.

20.12. Análisis de descartar el mínimo: bottom up.

Si se desea borrar la raíz del heap que se muestra a la derecha de la Figura 20.10, deben
reescribirse los punteros derechos de los nodos 2 y 3, que son los finales de las listas principales
que derivan de la raíz. Se mezclan los trayectos derechos de las listas, intercambiado los hijos a
través del trayecto de mezcla, y se detiene la mezcla cuando se llega a la raíz en ambos
trayectos.

Se tratan como casos especiales que el heap esté vacío, y que una de las listas esté vacía. En el
primer caso se retorna un puntero nulo; en el segundo, no es necesario realizar la mezcla, sino
que encontrar el nodo con prioridad mínima, recorriendo la lista descendente hasta el final.

En un caso general, se requieren tres variables auxiliares: mayor y menor que apuntan a los
elementos de las listas que deben mezclarse, next se emplea para escoger el mayor de los
elementos, y si es preciso se cambian los punteros mayor y menor:

h = mayor; //inicia raíz para la mezcla con el mayor (bottom)


h->right = h; //sin lista derecha. Se apunta a sí mismo
mayor = mayor->right; //avanza y selecciona el siguiente: next
if (mayor->prioridad < menor->prioridad)
{ next = menor; menor = mayor;}
else { next = mayor; }

Luego de esto se procede a un lazo de mezcla, el cual agrega el nodo next al nuevo heap, el que
debe repetirse hasta encontrar la raíz. Sea r la raíz que apunta al elemento mínimo que se desea
descartar.

La Figura 20.15, muestra un ejemplo de las variables y sus relaciones:


En el nuevo heap h, que está en formación, los elementos cumplen: b > … > c > a.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
Los elementos a la derecha del nodo next cumplen: d > e, por pertenecer a uno de los trayectos
que forman una lista descendente; y d < f por formar parte de una lista ascendente.
La lista derecha de f, cumple … > g > d > f.

El proceso de abajo hacia arriba garantiza que d es menor que a.


h
a b … c

next
d e

f … g

Figura 20.15. Lazo de mezcla e intercambio.

El siguiente lazo de mezcla, describe la actualización mediante la escritura de cinco punteros.


Luego de esto se obtiene la Figura 20.16, y el proceso continúa.

while (next != r)
{ mayor = next->right; //avanza en lista mayor
next->right = next->down; //intercambia hijos de next
next->down = h->right; //agrega nodos con prioridades mayores
h->right = next; //enlaza a con el padre d
h = next; //la nueva raíz absorbe nodo next

//selecciona siguiente
if (mayor->prioridad < menor->prioridad)
{ next = menor; menor = mayor;}
else {next = mayor;}
}
h

d f … g
mayor
e
b … c a

Figura 20.16. Después de la actualización de cinco punteros.

20.13. Tipos y obtención de nuevo nodo.

Se emplea ahora down, en lugar de left.

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 13
typedef struct bintree
{ struct bintree * down;
struct bintree * right;
int prioridad;
} nodo, *pnodo;

pnodo getnodo(int prioridad)


{ pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) ==NULL)
{ printf("Error: Memoria. \n"); exit(1);}
else
{ p->prioridad=prioridad; p->down=p; p->right=p;} //inicia listas
return(p);
}

La creación de una nueva cola de prioridad, y el test de cola vacía, resultan idénticas a las
funciones para el caso top-down.

20.13. Insertar. Bottom-up.

void insert (pnodo n, queue * q)


{ pnodo t, y, z;
if (*q == NULL) {*q =n; }
else if (n->prioridad <= (*q)->prioridad) //inserción en lista descendente. Figura 20.12
{n->down=(*q)->right ; *q=(*q)->right=n; } //n es el menor nodo de trayecto mayor
else if (n->prioridad >= (*q)->right->prioridad) //inserción en el tope. Figura 20.11.
{n->right=(*q)->right; (*q)->right=n;} //n es la nueva raíz
else
{ y =z=(*q)->right;
//Al inicio ambos punteros auxiliares apuntan al primero de la lista derecha
while (n->prioridad < y->right->prioridad)
{ y=y->right; //avanza en la lista descendente.
t=z; z=y->down; y->down=t; // swap(z, y->down)
}
n->right=y->right; n->down=z; //enlaza lista ascendente y descendente.
y->right=(*q)->right=n; //cambia inicio lista y encadena el nuevo nodo
}
}

20.14. Descartar. Bottom-up.

pnodo dequeue( queue* q )


{ pnodo mayor, menor, next, newq;
pnodo minimo= *q; /* selecciona el mínimo */
if (*q != NULL)
{ /* descarta sólo si el heap no está vacío */
mayor = (*q)->right;

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
menor = (*q)->down;
if (mayor->prioridad < menor->prioridad)
{ mayor = menor; /* intercambia mayor y menor */
menor = (*q)->right;
}
if (mayor == *q)
{ /* la lista apuntada por mayor está vacía. */
if (menor == *q)
{ *q = NULL; } /* la cola tenía un solo elemento */
else
{ /* se busca nueva raíz en lista apuntada por menor. */
next = menor;
while (menor->right != *q) menor = menor->right;
//menor apunta a la nueva raíz. next pasa a ser el primero de la lista derecha
menor->right = next;
*q = menor;
}
}
else
{ /* realiza la mezcla de listas derechas apuntadas por mayor y menor */
newq = mayor; //inicia raíz para la mezcla con el mayor (bottom)
newq->right = newq; //sin lista derecha. Se apunta a sí mismo
mayor = mayor->right; //avanza y selecciona el siguiente: next
if (mayor->prioridad < menor->prioridad)
{ next = menor; menor = mayor;}
else { next = mayor; }

//lazo de mezcla: agrega next al nuevo heap.


while (next != *q)
{ mayor = next->right; //avanza
next->right = next->down; //intercambia hijos de next
next->down = newq->right; //agrega nodos con prioridades mayores
newq->right = next; //enlaza con el padre
newq = next; //la nueva raíz absorbe nodo next

//selecciona siguiente
if (mayor->prioridad < menor->prioridad)
{ next = menor; menor = mayor;}
else {next = mayor;}
}

/* se agotó lista apuntada por mayor */


if (menor == *q) {*q = newq;} /* si se agotan ambas, la mezcla termina */
else
{ /* busca la nueva raíz en la lista menor */
next = menor;
while (menor->right != *q) menor = menor->right;

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 15
//menor apunta a la nueva raíz
menor->right = newq->right; //le pega lista descendente
newq->right = next; //pega el resto de los nodos que no participaron en la mezcla
*q = menor;
}
}
}
return minimo;
} /* dequeue */

20.15. Test de las funciones.

//Muestra listas a partir de la raíz. Bottom.up.


int prtskew(pnodo p)
{ pnodo t;
if (p!=NULL) printf("r=%d ri->", p->prioridad);
else {printf("Skewup nulo\n"); return (0);}
for(t=p->right; t!=p ; t=t->right) printf("%d ", t->prioridad);
printf(" dw->"); //lista ascendente hacia abajo
for(t=p->down; t!=p ; p=t, t=t->down)
{printf("%d ", t->prioridad);}
putchar('\n');
return(1);
}

queue skewup=NULL;
#define N 10

int main (void)


{ pnodo t;
int i;
srand(1);
for(i=1; i<=N; i++)
{ enqueue(getnodo(rand()%100), &skewup);
//prtskew(skewup);
}
putchar('\n');
for(i=1; i<=N; i++)
{ t= dequeue(&skewup);
//prtskew(skewup);
printf(" %d ", t->prioridad);
free(t);
}
putchar('\n');

return(0);
}

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Ejercicios.

E20.1.

Para el procesamiento top-down:


Comprobar que la inserción de la secuencia: 5, 6, 4, 7, 3, 8, 2, 9, 1, 10 deja un trayecto derecho
de 5 elementos de un total de 10. Lo cual es un ejemplo de secuencia de inserción de peor caso.
Comprobar que al descartar el mínimo, la estructura recobra un largo derecho razonable, de
largo uno en este caso.

Referencias.

D. D. Sleator, R.E. Tarjan., “Self-adjusting Heaps”, SIAM J. Comput. Vol 15, N° 1, Feb 1986.

Profesor Leopoldo Silva Bijit 26-05-2008


Seleccionar. Skewheap. 17
Índice general.

CAPÍTULO 20 ........................................................................................................................................... 1
SKEW HEAPS. SELECCIONAR. ........................................................................................................... 1
20.1. MEZCLA DE DOS SKEW HEAPS. ......................................................................................................... 1
20.2. OPERACIÓN MEZCLA CON INTERCAMBIO. TOP-DOWN. .................................................................... 2
20.3. DEFINICIÓN DE TIPOS. ...................................................................................................................... 4
20.4. CREACIÓN DE NODO Y DE COLA. ...................................................................................................... 4
20.5. TEST DE COLA VACÍA. ...................................................................................................................... 4
20.6. INSERTAR. ENCOLAR. TOP-DOWN. ................................................................................................... 4
20.7. SELECCIONAR EL MÍNIMO. DESENCOLAR. TOP-DOWN. .................................................................... 5
20.8. FUNCIÓN MEZCLAR. TOP-DOWN...................................................................................................... 5
20.9. TEST DE LAS FUNCIONES. ................................................................................................................. 6
20.10. MEZCLA ASCENDENTE (BOTTOM-UP). ............................................................................................ 7
20.11. ANÁLISIS DE INSERCIÓN BOTTOM UP. ............................................................................................ 9
20.12. ANÁLISIS DE DESCARTAR EL MÍNIMO: BOTTOM UP....................................................................... 11
20.13. TIPOS Y OBTENCIÓN DE NUEVO NODO. ......................................................................................... 12
20.13. INSERTAR. BOTTOM-UP. .............................................................................................................. 13
20.14. DESCARTAR. BOTTOM-UP. ........................................................................................................... 13
20.15. TEST DE LAS FUNCIONES. ............................................................................................................. 15
EJERCICIOS. ............................................................................................................................................ 16
E20.1. ................................................................................................................................................ 16
REFERENCIAS. ........................................................................................................................................ 16
ÍNDICE GENERAL. ................................................................................................................................... 17
ÍNDICE DE FIGURAS................................................................................................................................. 17

Índice de figuras.

FIGURA 20.1. MEZCLA DE TRAYECTOS DERECHOS DE DOS HEAPS. ............................................................... 1


FIGURA 20.2. MEZCLA E INTERCAMBIO DE HIJOS EN EL TRAYECTO.............................................................. 2
FIGURA 20.3. MEZCLA E INTERCAMBIO DE HIJOS EN EL TRAYECTO.............................................................. 2
FIGURA 20.4. INICIO DE LA MEZCLA CON INTERCAMBIO. ............................................................................. 3
FIGURA 20.5. HEAP CON UN ELEMENTO. ...................................................................................................... 3
FIGURA 20.6. PASO POR REFERENCIA A LA COLA DE PRIORIDAD SKEW. ....................................................... 5
FIGURA 20.7. MEZCLA DE TRAYECTOS DERECHOS DE DOS HEAPS. ............................................................... 7
FIGURA 20.8. MEZCLA BOTTOM-UP DE LOS HEAPS DE LA FIGURA 20.7. ....................................................... 7
FIGURA 20.9. ÁRBOL DE PRIORIDAD Y SKEW BOTTOM-UP EQUIVALENTE. .................................................... 8
FIGURA 20.10. ENDEREZANDO PUNTEROS Y REPRESENTACIÓN POR LISTAS. ................................................ 8
FIGURA 20.11. INSERCIÓN DE ELEMENTO CON MENOR PRIORIDAD QUE LA RAÍZ. ......................................... 9
FIGURA 20.12. INSERCIÓN DE ELEMENTO MAYOR O IGUAL QUE EL PRIMERO DE LA LISTA DERECHA. ........... 9
FIGURA 20.13. INSERCIÓN DE ELEMENTO 6, MENOR QUE EL PRIMERO DE LA LISTA DERECHA. ................... 10
FIGURA 20.14. INSERCIÓN DE ELEMENTO 4, MENOR QUE EL PRIMERO DE LA LISTA DERECHA. ................... 11
FIGURA 20.15. LAZO DE MEZCLA E INTERCAMBIO. .................................................................................... 12
FIGURA 20.16. DESPUÉS DE LA ACTUALIZACIÓN DE CINCO PUNTEROS. ...................................................... 12

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 21

Heaps Binomiales. Seleccionar.

Una cola de prioridad binomial considera los datos almacenados en una foresta; ésta puede ser
definida como una colección de subárboles, en la que cada subárbol es un árbol binomial; y tal
que cada uno de ellos cumple la propiedad de que los hijos tienen claves o prioridades mayores
o iguales a las del padre. Esta estructura favorece la operación de mezclar dos colas de
prioridad, para formar una nueva.

21.1. Árbol binomial.

Un árbol binomial Bp 1 , de orden p+1, es un multiárbol formado por la unión de dos árboles
binomiales de orden p, para p 0 . B0 está formado por un elemento. Entendemos por unión el
que la raíz de uno de los árboles binomiales B p es el hijo más izquierdista de la raíz del otro
árbol binomial B p , según se muestra en la Figura 21.1.
B0 Bp+1

Bp
Bp

Figura 21.1. Definición inductiva de árbol binomial.

La Figura 21.2, muestra algunos ejemplos de árboles binomiales.

B0 B1 B2 B3

Figura 21.2. Ejemplos de árboles binomiales.

Puede demostrarse por inducción que un árbol binomial B p tiene 2 p nodos; que la raíz tiene p
hijos; y que la máxima profundidad del árbol es p.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos

Es importante destacar que si n 2 p , entonces: p log 2 (n) . Las principales operaciones sobre
esta estructura dependen del número de hijos de la raíz.

Si se enumeran los nodos, empleando el sistema binario, en preorden (izquierdo-derecho-raíz),


partiendo del nodo con mayor profundidad, se obtiene la Figura 21.3, para el caso de B3 .
111
011 101 110
001 010 100
000

Figura 21.3. Numeración binaria en preorden de B3.

p
Se tienen nodos con profundidad k en B p . Debido al coeficiente binomial, que figura en la
k
relación anterior, se da el nombre de binomiales a estos árboles.

p p
Para la raíz y el nodo más profundo de B p , se tienen: 1.
p 0
p p!
Los p hijos de la raíz de B p , se obtienen de: p.
1 1!( p 1)!

Los valores de los coeficientes binomiales corresponden al número de secuencias binarias de p


bits, con exactamente k ceros. También puede comprobarse que las hojas tienen números pares;
y que el número de hijos de un nodo es el número de unos que siguen al último cero de su
numeración binaria.

Como Bp está formado por la unión de dos árboles Bp-1, un nodo a profundidad k en Bp-1 aparece
en Bp una vez a profundidad k y una vez a profundidad k + 1. Entonces para obtener los nodos a
profundidad k en Bp debemos sumar los nodos a profundidad k en Bp-1, más los nodos a
profundidad k-1 en Bp-1.

k-1 k

Figura 21.4. Nodo con profundidad k en Bp.

Considerando verdadera la proposición inductiva, debería cumplirse:

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 3

p p 1 p 1
k k k 1

Desarrollando el lado derecho, y aplicando la definición de coeficiente binomial, se obtiene:

( p 1)! ( p 1)!
k !( p 1 k )! (k 1)!( p 1 (k 1))!

Sacando factores comunes, y multiplicando y dividiendo el segundo término por k, se tiene:

( p 1)! 1 k
( )
k ! ( p 1 k )! ( p k )!
Multiplicando y dividiendo el primer término por p-k, factorizando y aplicando la definición de
coeficiente binomial, se tiene:

( p 1)! p! p
(( p k ) k )
k !( p k )! k !( p k )! k

Lo que demuestra la propiedad.

Una forma alternativa de definir Bp 1 , es agregar como hijos de la raíz a los (p+1) árboles
binomiales: Bp , Bp 1 , Bp 2 ,..., B2 , B1 , B0 . La Figura 21.5, muestra la generación de B3 .
B3

B2 B1 B0

Figura 21.5. Subárboles de B3.

21.2. Cola de prioridad binomial.

Si se desea representar conjuntos de n elementos, tal que n no sea una potencia de dos, puede
considerarse la representación de n en el sistema binario, mediante la secuencia de (p+1) cifras
binarias, con el equivalente decimal:
i p
n bi 2i
i 0

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
Donde bi es uno de los dígitos binarios {0, 1}. Se define la foresta binomial Fn de orden n,
como una colección finita de árboles binomiales Bi , uno por cada cifra bi diferente de cero en la
representación binaria de n.

Por ejemplo para n=13, se tiene: F13 B3 , B2 , B0 ya que 1310 11012

Entonces una cola de prioridad de n elementos puede ser representada por una foresta Fn , donde
cada uno de los árboles binomiales que la forman, cumplen la propiedad de que los hijos tienen
prioridades mayores que la de su padre.

Observando la Figura 21.5, una cola de prioridad con n elementos también puede representarse
con la raíz como el nodo con prioridad mínima unido a una foresta Fn-1.

21.3. Análisis de la operación unión.

Dadas dos forestas: Fn y Fm la unión estará formada por (n+m) elementos en una foresta Fn m .
Es notable que la operación mezcla pueda realizarse en forma similar a una suma binaria. El
siguiente ejemplo describe el algoritmo:

Se desea obtener la mezcla de F13 B3 , B2 , B0 con F5 B2 , B0 . La suma binaria de 13 y


5, se realiza según:
1 1 0 1
+ 0 1 0 1
1 0 0 1 0

Al sumar (unir, mezclar o fundir) dos árboles binomiales Bi se produce reserva, ésta es un árbol
binomial Bi 1 .

B3 B2 B0
+ B2 B0
B4 B1

Entonces, para el ejemplo, resulta: F18 B4 , B1 .

Observando la Figura 21.2, puede notarse que la unión de dos árboles binomiales puede
realizarse en tiempo constante, basta escribir una referencia para el árbol descendiente. En el
peor caso un heap con n elementos está formado por log(n) árboles binomiales, entonces
obtener uno, mediante mezclas, demanda un costo O(log(n)).

La inserción puede realizarse como la mezcla de un árbol binomial con un elemento con la
estructura ya existente. También su costo es O(log(n)).

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 5
La extracción del mínimo requiere buscar la raíz menor de los árboles binomiales que forman la
foresta, esto tiene un costo O(log(n)), ya que ese es el número de heaps que forman la cola de
prioridad. Observando la Figura 21.5, la remoción de la raíz de uno de los árboles binomiales
genera una nueva foresta, la cual debe unirse al resto de los árboles binomiales, lo cual se
realiza también en O(log(n-1)), dando para la operación completa un costo O(log(n)).

21.4. Estructura de datos.

La Figura 21.6, muestra las prioridades almacenadas en los multiárboles binomiales: B3 , B2, B1 ,
B0.
1

1
2 3 5

1 2 3
4 6 7

1 2 4
8

Figura 21.6. Multiárboles binomiales: B3 , B2, B1 , B0.

21.4.1. Nodo binario.

Inicialmente se considera un nodo binario, que minimiza el tamaño de memoria ocupada por el
nodo.
La Figura 21.7, muestra una posible estructura que emplea los punteros derechos para
referenciar el hijo más derechista del nodo, y los punteros izquierdos para enlazar los hermanos
hacia la izquierda. Las listas de hermanos son circulares: Si no hay hermanos de un nodo, el
puntero izquierdo apunta al mismo nodo; el último nodo de la lista de hermanos apunta al
hermano más derechista. Las hojas tienen punteros derechos, o hacia abajo, nulos.

Se han ilustrado árboles binomiales, donde la raíz tiene lista izquierda vacía, sin embargo puede
acordarse unir los árboles binomiales de la foresta en la lista circular de la raíz.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

1 3 2 5

1 2 3 4 6 7

1 2 4 8

Figura 21.7. Representación mediante listas.

Por ejemplo si a B1 en la Figura 21.7, se agrega un nodo con prioridad 3, el árbol binomial B0,
que lo representa, se coloca en la lista izquierda de la raíz, esto se muestra en la Figura 21.8.
Debe notarse que la operación es sencilla si el número de elementos en la foresta es par; ya que
esto implica agregar el árbol binomial B0, formado por un elemento.

3 1

Figura 21.8. Foresta con 3 elementos.

Si en B2 en la Figura 21.7, se agregan nodos con prioridades 5 y 6, debe formarse el árbol


binomial B1 , el cual se coloca en la lista izquierda de la raíz, formando una foresta de 6
elementos, esto se muestra en la Figura 21.9, a la izquierda. Nótese que después de agregado el
5, debe procederse a formar, mediante la unión o suma un árbol binomial B1, formado por los
elementos 5 y 6.

A la derecha de la Figura 21.8, se muestra un foresta con 7 elementos, donde se ha enlazado B0,
formado por el nodo con valor 7, con B1 y B2. Esta inserción también es sencilla.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 7

5 7 1
5 1

6 2 3 6 2 3

4 4

Figura 21.9. Foresta con 6 y 7 elementos.

La inserción del elemento con valor 8, en el heap a la derecha de la Figura 21.9, debería formar
el árbol binomial B3, que se muestra a la derecha de la Figura 21.7. Esto se logra con la unión de
dos árboles B1, para formar un árbol B2; y luego la suma de dos árboles B2, para formar un árbol
B3.

B2 B1 B0
+ B0
B3

Figura 21.10. Generación de B3 mediante mezclas.

21.4.2. Nodo binario aumentado con información del grado.

Si se almacena el número de elementos en la foresta, su representación binaria indica los árboles


binomiales que la forman. Si al inicio, antes de insertar, el número de elementos es impar, esto
indica que está presente B0; y si es par, indica que la foresta no contiene a B0. La división
sucesiva de este número por dos va indicando, en la cifra binaria menos significativa, si están
presentes en la cola de prioridad los árboles binomiales B1, B2, y así sucesivamente. Sin
embargo, con esta estructura, la operación para extraer el mínimo es compleja de codificar.

Si se almacena el grado del árbol binomial en el nodo, puede representarse la foresta como una
lista enlazada de las raíces de los árboles binomiales ordenadas por grado, ya que esto facilita la
operación unión de dos árboles binomiales de igual grado. Sin embargo el descarte del mínimo
requiere recorrer la lista completa, pero el largo de ésta está acotada; el largo, en el peor caso, es
el logaritmo del número de nodos almacenados en la cola de prioridad. Para representar los
multiárboles se emplea la organización hijo más izquierdista-hermano derecho.

La Figura 21.11, muestra los multiárboles binomiales B2 y B3 de la Figura 21.6, empleando el


enlace izquierdo del nodo binario para apuntar al hijo más izquierdista, y el enlace derecho para
apuntar al hermano derecho; en cada nodo se almacena la prioridad y el grado del árbol
binomial asociado al nodo. Nótese que dentro del árbol binomial, las listas de hermanos están
ordenadas en forma descendente según el grado, esto facilita la operación unión de dos árboles
binomiales de igual grado, basta escribir dos punteros e incrementar el grado de la nueva raíz.
Esto se ilustra a la derecha de la Figura 21.11, destacando los dos árboles que se unen.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos

1
3

1 5 3 2
2 2 1 0

3 2 7 6 4
1 0 1 0 0

4 8
0 0

Figura 21.11. Representación hijo izquierdo-hermano derecho de B2 y B3.

La Figura 21.12, muestra una cola de prioridad con 13 nodos, mediante el enlace de tres árboles
binomiales: B0, B2 y B3. El enlace derecho de las raíces de los árboles binomiales se emplea
para formar una lista ordenada en forma descendente por grado; debe notarse que no quedan
ordenados por prioridad. El ejemplo muestra que el nodo con prioridad mínima puede estar en
cualquier posición de la lista; en la Figura 21.12, se ilustra al final de ésta.

9 10 1
0 2 3

11 12 5 3 2
1 0 2 1 0

14 7 6 4
0 1 0 0

8
0

Figura 21.12. Foresta con 13 nodos. Lista de raíces.

Ordenando de este modo la lista, se facilita la operación de unir (o sumar) dos forestas. Equivale
a realizar las sumas binarias desde los dígitos menos significativos.

21.4.3. Nodo binario con punteros al padre.

Si se desea disponer de operaciones de búsqueda o de decrementar la prioridad de un nodo, para


que estas operaciones resulten eficientes, debe agregarse un puntero al padre.

21.5. Análisis de la inserción.

Si la foresta no contiene un árbol binomial B0, la inserción coloca el nuevo nodo al comienzo de
la lista de raíces. Si está presente B0, se une a éste el nodo que se desea insertar, formando un
árbol binomial B1; y debe seguirse efectuado uniones si pueden formarse árboles binomiales de
mayores grados, esto se realiza recorriendo la lista de raíces.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 9

Si antes de la inserción, la foresta no puede tener dos árboles binomiales de igual grado,
entonces la lista debe recorrerse mientras el grado de la última unión sea igual al grado del
siguiente árbol binomial de la lista.

21.6. Análisis de la extracción del mínimo.

Se recorre la lista hasta encontrar el mínimo, y se liga la lista con el siguiente. Los hijos del
nodo mínimo forman otra lista de árboles binomiales; sin embargo, debido a la construcción,
está ordenada por grados en forma descendente. Entonces se debe reversar la lista de hijos del
nodo mínimo, dejándola ordenada en forma ascendente por grado.
Luego se procede a mezclar las dos listas ordenadas por grados, dejando adyacentes a los nodos
que tengan grados iguales. Luego debe procederse a unir los árboles de igual grado, y si es
posible, formar árboles de mayores grados; esta operación debe recorrer la lista completa.

21.7. Tipos de datos.

Se considera un nodo binario más la información del grado. De ser necesario puede emplearse
un puntero al padre.

typedef struct bintree


{ //struct bintree * padre;
struct bintree * hijo; //puntero al hijo más izquierdista.
struct bintree * hermano; //apunta al hermano derecho
int prioridad;
int grado;
} nodo, *pnodo;

typedef pnodo queue;

21.8. Creación de nodo.

pnodo getnodo(int prioridad)


{ pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) ==NULL)
{ printf("Error: Memoria. \n"); exit(1);}
else
{ p->prioridad=prioridad; p->grado=0;
//p->padre=NULL;
p->hijo=NULL; p->hermano=NULL;
}
return(p);
}

21.9. Inicio de cola.

void initqueue( queue *q )

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
/* inicia una variable de tipo cola de prioridad */
{
*q = NULL;
} /* initqueue */

21.10. Test de cola vacía.

int emptyqueue( queue q )


/* Retorna 1 si la cola está vacía */
{
return (q == NULL);
} /* emptyqueue */

21.11. Unión de dos árboles binomiales de igual grado.

Enlaza árbol de grado (k-1) con raíz mayorp, con árbol de grado (k-1) con raíz menorp, por
nodo con menor prioridad. Ver Figura 1.
Donde menorp es el padre de mayorp, y además es la raíz de un nuevo árbol de grado k. Se
coloca como comentario, la mantención de punteros al padre.

void BinomialLink(pnodo mayorp, pnodo menorp)


{ //mayorp->padre=menorp;
mayorp->hermano=menorp->hijo;
menorp->hijo= mayorp;
menorp->grado++;
}
menorp
1
mayorp 3

5 3 2
2 1 0

7 6 4
1 0 0

8
0

Figura 21.13. Enlace de dos árboles de grado 2.

21.12. Mezcla dos listas ordenadas por campo grado.

pnodo BinomialHeapMerge(pnodo H1, pnodo H2)


{ pnodo H=NULL, t;
if (H1==NULL ) return H2; else if (H2==NULL) return H1;

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 11
else
{ if (H1->grado < H2->grado) {H=H1; H1=H1->hermano;} //Inicio lista
else {H=H2; H2=H2->hermano;}
t=H;
while(H1!=NULL && H2!=NULL) //se pasan en orden ascendente
{ if (H1->grado < H2->grado){t->hermano=H1; H1=H1->hermano;}
else {t->hermano=H2; H2=H2->hermano;}
t=t->hermano;
}
if (H1!=NULL ) t->hermano=H1; //copia el resto de la lista que no se agotó
else t->hermano=H2;
return(H);
}
}
Puede compactarse la codificación de la función si se emplea un nodo de encabezado, de este
modo la inicialización de la mezcla se realiza dentro del lazo while.

pnodo BinomialHeapMerge(pnodo H1, pnodo H2)


{ nodo h;
pnodo t=&h;
while (H1!=NULL && H2!=NULL)
{ if (H1->grado < H2->grado){t->hermano=H1; H1=H1->hermano;}
else {t->hermano=H2; H2=H2->hermano;}
t=t->hermano;
}
if (H1==NULL ) t->hermano=H2;
else if (H2==NULL ) t->hermano=H1; else t->hermano=NULL;
return ((&h)->hermano); //retorna puntero a la mezcla.
}

21.13. Mezcla dos colas binomiales.

Es la operación fundamental de la estructura. Su costo depende del largo de la lista de árboles


binomiales.
Si dos árboles adyacentes son de grados diferentes, debe seguir revisando la lista. También debe
seguir revisando sin efectuar enlaces, si se encuentran tres árboles binomiales de igual grado; si
esto ocurre, avanza una posición, dejando uno de los árboles y mezclando los dos siguientes.
Este caso equivale a la suma de dos dígitos binarios con reserva.

La función emplea tres punteros para el análisis de los casos: un puntero al nodo previo, otro al
actual, y otro al siguiente. Al inicio el puntero previo apunta a nulo.

Debido a que los operadores lógicos operan con cortocircuitos, la realización de la segunda
parte de un or, sólo se efectúa si el primer operando es falso; ya que si el primero es verdadero
la condición será verdadera no importando el valor lógico de la segunda. Entonces, la detección
de tres adyacentes iguales puede codificarse, como el segundo término del or, según:

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
(x->grado != nextx->grado ||
( (nextx->hermano!= NULL) && (nextx->hermano->grado == x->grado)) )

La formulación de la segunda parte del or, se plantea mediante un and lógico para asegurar la
existencia del tercer nodo; ya que si no existe el tercer nodo, preguntar por su grado produce un
error en ejecución.

El caso 1, se produce cuando los nodos raíces tienen grados diferentes, y se activa con la
primera parte del or. El caso 2, se presenta cuando existe suma de árboles binomiales de igual
grado con reserva.

Cuando debe efectuarse el enlace binomial de dos árboles de igual grado, se tienen dos casos
que se disciernen según la prioridad de los nodos raíces. Son los casos 3 y 4. Se emplea el
puntero previo para enlazar las listas.

pnodo BinomialHeapUnion(pnodo H1, pnodo H2)


{ pnodo H=NULL, prevx, nextx, x;
H = BinomialHeapMerge(H1, H2);
if (H ==NULL) return H;
prevx = NULL;
x = H;
nextx = x->hermano;
while (nextx != NULL)
{ //Si grado de x y siguiente son diferentes o tres de grados iguales
if (x->grado != nextx->grado ||
((nextx->hermano!= NULL) && (nextx->hermano->grado == x->grado)) )
{ prevx = x; // Casos 1 y 2. Avanza sin enlazar árboles.
x = nextx;
}
//Entra al else: Si grados de x y siguiente son iguales
//y no hay tres nodos de iguales grados adyacentes.
else if (x->prioridad <= nextx->prioridad ) // Caso 3
{ x->hermano = nextx->hermano; //Liga lista y deja
BinomialLink(nextx, x); //x como raíz
}
else
{if (prevx == NULL) H = nextx; // Caso 4
else prevx->hermano= nextx; //Liga lista y deja a
BinomialLink(x, nextx); //nextx en la raíz
x = nextx; //actualiza el nodo denominado actual
}
nextx = x->hermano; //avanza al siguiente
}
return H;
}

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 13
21.14. Insertar.

Se pasa la cola de prioridad con una referencia a ésta.


Encola nodo n en cola referenciada por q, empleando la operación de unión.

void enqueue( pnodo n, queue *q )


{ pnodo p=*q;
*q= BinomialHeapUnion(p, n);
} /* enqueue */

Una pequeña refinación de la operación, se logra aislando dos situaciones triviales. La primera
es que la cola esté vacía; la otra es que el número de nodos en la foresta sea par, en este caso no
hay al principio de la lista un árbol binomial de un nodo, o de grado cero, y basta insertar al
inicio. Puede especializarse aún más la operación, ya que en este caso no pueden presentarse
tres árboles de igual grado, y tampoco es preciso recorrer toda la lista.
void enqueue( pnodo n, queue *q )
{ pnodo p=*q;
if (p==NULL) *q=n;
else if (p->grado!=0) {n->hermano=*q; *q=n;}
else
*q= BinomialHeapUnion(p, n);
} /* enqueue */

21.15. Descartar.

La operación desencolar retorna el nodo con valor mínimo de prioridad, y deja una foresta
disminuida en un elemento.
Para esto, luego de extraer de la lista el árbol binomial cuya raíz es el nodo con prioridad
mínima, deja ligada la lista con los árboles binomiales siguientes al que contiene el mínimo.
Posteriormente reversa el orden de la lista de hijos del mínimo, dejándola ordenada ascendente.
Finalmente realiza la suma de las dos forestas.

pnodo dequeue( queue *q )


{ pnodo p=*q, min, ListaHijosMinimo;
if (p==NULL) return NULL;
min=ExtraeMinimo(q); //Se enlaza lista sin el árbol cuya raíz es el mínimo

//reversar lista descendente (ordenada por grado) de hijos del mínimo, apuntada por x.
ListaHijosMinimo=ReversarLista(min->hijo);
//Queda lista ordenada ascendente por grado, apuntada por x.

//Se mezcla lista de árboles sin el mínimo, con la lista de hijos del mínimo
*q=BinomialHeapUnion(*q, ListaHijosMinimo);
return (min);
} /* dequeue */

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
21.16. Extracción del mínimo.

Se emplean variables locales que apuntan al nodo actual de la lista p, y al nodo mínimo min.
Además para facilitar el ligado de la lista se emplean punteros al padre de p (pp) y al padre de
min (pm). Se enlaza lista sin el árbol cuya raíz es el mínimo

pnodo ExtraeMinimo(queue *q)


{ pnodo p=*q, min, pm, pp;
//pp padre de p (hermano izquierdo de p). pm padre del minimo.
pp=NULL; //al inicio p no tiene padre
//min apunta al mínimo. Al inicio apunta al primer nodo de la lista.
min=p; pm=pp;
pp=p; p=p->hermano; //Avanza al siguiente de la lista

while (p!=NULL)
{ if (min->prioridad > p->prioridad) { min=p; pm=pp;}
pp=p; p=p->hermano;
}
if (pm==NULL) *q=min->hermano; //cambia la raíz. Si el mínimo era el primero.
else pm->hermano=min->hermano; //liga resto de la lista
return (min);
}

El código puede simplificarse usando un nodo de encabezado, lo que permite tratar al primer
nodo dentro del lazo while. Se coloca el máximo valor de prioridad en el nodo centinela, de
acuerdo al tipo del campo prioridad; estos valores se encuentran en <limits.h>.

pnodo ExtraeMinimo(queue *q)


{ nodo n ={NULL, NULL, INT_MAX, 0}; //centinela con la prioridad mayor posible
pnodo p=*q, min=&n, pm, pp=NULL;
while (p!=NULL)
{ if (min->prioridad > p->prioridad) { min=p; pm=pp;}
pp=p; p=p->hermano;
}
if (pm==NULL) *q=min->hermano; //cambia la raíz
else pm->hermano=min->hermano; //liga resto de la lista
return(min);
}

21.17. Reversar Lista.

Reversar lista descendente, ordenada por grado, de hijos del mínimo, apuntada por x, dejándola
ordenada en forma ascendente. La operación contempla el caso en que la estructura emplee
punteros al padre, dejando como comentarios las mantenciones de esos punteros.
Se emplean los punteros y y t, para recorrer la lista. Donde y es el siguiente de x, y t el siguiente
de y.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 15
pnodo ReversarLista(pnodo x)
{ pnodo y, t;
if (x!=NULL)
{ //x->padre=NULL;
y=x->hermano; //Inicia siguiente del primero de la lista.
x->hermano=NULL; //Inicia el primer nodo como el último o fin de lista
while(y!=NULL)
{ //y->padre=NULL;
t=y->hermano; //salva dirección del siguiente
y->hermano=x; //apunta hacia atrás
x=y; y=t; //recorre la lista
}
}
return(x);
}

21.18. Busca mínimo.

La siguiente función retorna un puntero al nodo con valor de prioridad mínima, pero no lo
extrae de la foresta.

/*retorna puntero al nodo con prioridad mínima en un heap binomial con n elementos*/
pnodo BinomialHeapMinumun(queue *q)
{
pnodo pmin =NULL; //puntero al mínimo, aún no se conoce
pnodo p =*q;
int min =INT_MAX;
while (p!=NULL)
{ if (p->prioridad < min) { min = p->prioridad; pmin = p;}
p =p->hermano;
}
return pmin;
}

21.19. Lista de raíces de árboles binomiales.

Para ayudar a la depuración de la funciones es conveniente disponer de un listador simple de la


estructura.

//lista raíz-grado
int prtbinomial(queue *q)
{
pnodo p=*q, t;
if (p!=NULL) printf("%d-%d ", p->prioridad, p->grado);
else {printf("Binomial nulo\n"); return(0);}

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
for(t=p->hermano; t!=NULL ; t=t->hermano)
printf("%d-%d ", t->prioridad, t->grado);

putchar('\n');
return(1);
}

21.20. Test de las funciones.

queue binomial=NULL;

#define N 30
int main (void)
{ pnodo t;
int i;
initqueue( &binomial);

srand(1);
for(i=1;i<=N;i++)
{ enqueue2(getnodo(rand()%100), &binomial);
//prtbinomial(&binomial);
}
putchar('\n');
for(i=1;i<=N;i++)
{ t= dequeue(&binomial);
//prtbinomial(&binomial);
printf(" %d ", t->prioridad);
free(t);
}
putchar('\n');

return(0);
}

Referencias.

J. Vuillemin, “A Data Structure for Manipulating Priority Queues”, CACM Vol. 21, No. 4,
April 1978, 309-315.

M. R. Brown. PhD thesis, “The Analysis of a Practical and Nearly Optimal Priority Queue”,
Stanford University, March 1977.

Douglas W. Jones, “An Empirical Comparison of Priority-Queue and Event-Set


Implementations”, Communications of the ACM, April 1986, Volume 29, Number 4.

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. “Introduction
to Algorithms”, Second Edition. MIT Press and McGraw-Hill, 2001.

Profesor Leopoldo Silva Bijit 26-05-2008


Colas de prioridad. Heaps Binomiales. 17

Índice general.

CAPÍTULO 21 ........................................................................................................................................... 1
HEAPS BINOMIALES. SELECCIONAR. ............................................................................................. 1
21.1. ÁRBOL BINOMIAL. ........................................................................................................................... 1
21.2. COLA DE PRIORIDAD BINOMIAL. ...................................................................................................... 3
21.3. ANÁLISIS DE LA OPERACIÓN UNIÓN. ................................................................................................ 4
21.4. ESTRUCTURA DE DATOS................................................................................................................... 5
21.4.1. Nodo binario. .......................................................................................................................... 5
21.4.2. Nodo binario aumentado con información del grado. ............................................................ 7
21.4.3. Nodo binario con punteros al padre. ....................................................................................... 8
21.5. ANÁLISIS DE LA INSERCIÓN. ............................................................................................................ 8
21.6. ANÁLISIS DE LA EXTRACCIÓN DEL MÍNIMO. ..................................................................................... 9
21.7. TIPOS DE DATOS............................................................................................................................... 9
21.8. CREACIÓN DE NODO......................................................................................................................... 9
21.9. INICIO DE COLA. ............................................................................................................................... 9
21.10. TEST DE COLA VACÍA. .................................................................................................................. 10
21.11. UNIÓN DE DOS ÁRBOLES BINOMIALES DE IGUAL GRADO. ............................................................. 10
21.12. MEZCLA DOS LISTAS ORDENADAS POR CAMPO GRADO. ............................................................... 10
21.13. MEZCLA DOS COLAS BINOMIALES. ............................................................................................... 11
21.14. INSERTAR..................................................................................................................................... 13
21.15. DESCARTAR. ................................................................................................................................ 13
21.16. EXTRACCIÓN DEL MÍNIMO. .......................................................................................................... 14
21.17. REVERSAR LISTA. ........................................................................................................................ 14
21.18. BUSCA MÍNIMO. ........................................................................................................................... 15
21.19. LISTA DE RAÍCES DE ÁRBOLES BINOMIALES. ................................................................................ 15
21.20. TEST DE LAS FUNCIONES. ............................................................................................................. 16
REFERENCIAS. ........................................................................................................................................ 16
ÍNDICE GENERAL. ................................................................................................................................... 17
ÍNDICE DE FIGURAS................................................................................................................................. 17

Índice de figuras.

FIGURA 21.1. DEFINICIÓN INDUCTIVA DE ÁRBOL BINOMIAL. ........................................................................ 1


FIGURA 21.2. EJEMPLOS DE ÁRBOLES BINOMIALES. ...................................................................................... 1
FIGURA 21.3. NUMERACIÓN BINARIA EN PREORDEN DE B3. .......................................................................... 2
FIGURA 21.4. NODO CON PROFUNDIDAD K EN BP. ......................................................................................... 2
FIGURA 21.5. SUBÁRBOLES DE B3. ................................................................................................................ 3
FIGURA 21.6. MULTIÁRBOLES BINOMIALES: B3 , B2, B1 , B0. ......................................................................... 5
FIGURA 21.7. REPRESENTACIÓN MEDIANTE LISTAS. ..................................................................................... 6
FIGURA 21.8. FORESTA CON 3 ELEMENTOS. .................................................................................................. 6
FIGURA 21.9. FORESTA CON 6 Y 7 ELEMENTOS. ............................................................................................ 7
FIGURA 21.10. GENERACIÓN DE B3 MEDIANTE MEZCLAS. ............................................................................. 7
FIGURA 21.11. REPRESENTACIÓN HIJO IZQUIERDO-HERMANO DERECHO DE B2 Y B3. .................................... 8
FIGURA 21.12. FORESTA CON 13 NODOS. LISTA DE RAÍCES. .......................................................................... 8

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
FIGURA 21.13. ENLACE DE DOS ÁRBOLES DE GRADO 2. ...............................................................................10

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 22

Pairing heaps. Seleccionar.

El algoritmo de aparear heaps, considera los datos en un multiárbol en el que cada subárbol
cumple la propiedad de que los hijos tienen claves o prioridades mayores o iguales a las del
padre.

6 4 7 5

Figura 22.1. Heap multiárbol.

Es un algoritmo de selección que puede agregar ítems en forma dinámica, a diferencia del heap
implícito, árbol binario embebido en un arreglo, que utiliza memoria estática.

22.1. Propiedades.

La operación de insertar se realiza en tiempo constante agregando el nuevo ítem como la raíz
del árbol, o agregándolo como un nuevo hijo de la raíz.

A partir del heap de la Figura 22.1, se muestra en la Figura 22.2, la inserción de un elemento
con prioridad 1, menor que la de la raíz; y a la derecha, la inserción de ítem con prioridad 3,
mayor que la de la raíz.

Si los hijos de la raíz se consideran enlazados en una lista, puede escogerse insertar el nuevo
elemento al inicio de la lista, lo que da características de auto organizado o autoajustable, de tal
modo que los elementos más recientemente insertados están primero.

2 2

6 4 7 5 6 4 7 5 3

Figura 22.2. Inserción en heap multiárbol.

Profesor Leopoldo Silva Bijit 09-11-2009


2 Estructuras de Datos y Algoritmos
Extraer el mínimo o descartar la raíz, que es una operación O(1), requiere además reconstruir el
heap, para esto se requiere seleccionar el menor de los hijos de la raíz y promocionarlo al lugar
de la nueva raíz, y esta operación será costosa.

Para seleccionar la nueva raíz el algoritmo consiste en formar una lista de pares de heaps, donde
cada par se construye enlazando dos subheaps, cada uno de estos enlaces tiene costo O(1).

En la Figura 22.3 se muestra el enlazamiento de los pares, 6 con 4; y 7 con 5, quedando el 3


aislado, luego que se descarta la raíz con valor 2, en la Figura 22.2 a la derecha.

Se puede recorrer la lista de hijos de la raíz de derecha a izquierda o de izquierda a derecha, se


ilustró la formación de izquierda a derecha o de atrás para adelante (back to front), considerando
el orden en que han sido insertados como hijos de la raíz. Luego debe efectuarse una segunda
pasada, enlazando los heaps resultantes del paso anterior, con uno de ellos que se seleccione, lo
cual también puede efectuarse de diferentes modos.

4 5 3

6 7

Figura 22.3. Formación de pares de heaps.

Si se enlaza nuevamente de izquierda a derecha, primero se unen los subárboles con raíces 4 y
5, quedando como raíz el nodo con prioridad 4; luego se enlaza este subheap, con el cuya raíz es
3, resultando la Figura 22.4.

5 6

Figura 22.4. Enlazamiento de pares.

La operación descarte al mismo tiempo que encuentra la nueva raíz, reorganiza el heap. Puede
notarse que se acortan las listas de los hijos de los nodos. Los autores del algoritmo demuestran
empleando técnicas de análisis amortizado de la complejidad, que la operación descartar tiene
costo O(log(n)) amortizado.

En un heap de n nodos, en peor caso la raíz tiene (n-1) hijos, entonces formar los pares de heaps
y luego enlazarlos será en peor caso de complejidad O(n). Sin embargo si lo que interesa es el
tiempo total de ejecución, y no la realización de una sola operación, una buena medida del costo

Profesor Leopoldo Silva Bijit 09-11-2009


Colas de prioridad. Pairing heaps. 3
es el tiempo amortizado. Lo que se efectúa es el cálculo del tiempo promedio de ejecución por
operación, respecto de una secuencia de peor caso de operaciones.

Una manera muy simplificada de verlo es considerar que la operación descarte es más compleja,
pero reorganiza el árbol, implicando que otras operaciones sucesivas de descarte o inserción
serán de menor costo.

22.1. Estructura de datos.

La elección de la estructura de datos debe permitir realizar las operaciones descritas


anteriormente. Un multiárbol puede ser representado mediante nodos binarios, empleando un
enlace al hijo más izquierdista, y el otro puntero al hermano derecho. Una variante de lo anterior
se logra apuntando hacia abajo, al inicio de la lista de hijos del nodo, y confeccionando una lista
simplemente enlazada de hijos con los punteros izquierdos.

La inserción de un nodo, con menor prioridad que la raíz, coloca a éste como la nueva raíz; y la
raíz antigua como descendiente del nuevo nodo. Si la prioridad del nuevo nodo es mayor que la
de raíz, se inserta al inicio de la lista de hijos de la raíz.

La raíz tiene puntero izquierdo nulo, ya que no tiene hermanos; y un nodo que se inserta como
hijo, tiene puntero hacia abajo nulo.

La Figura 22.5, muestra un multiárbol de prioridad, y la Figura 22.6, su representación por


nodos binarios, de acuerdo a la estructura escogida. Un buen esquema de la estructura de los
datos es indispensable en la codificación de las funciones.

6 4 7 5 3

8 10 12 9 6

Figura 22.5. Multiárbol de prioridad.

6 4 7 5 3

8 10 12 9 6

Figura 22.6. Listas de hermanos y descendientes.

Profesor Leopoldo Silva Bijit 09-11-2009


4 Estructuras de Datos y Algoritmos
Si se insertan nodos con una secuencia ascendente de prioridades, la lista de hijos de la raíz
crece. Si la secuencia de nodos que se insertan tienen claves en orden descendente crece la lista
de descendientes hacia abajo. Sin embargo si ocurren descartes entre las operaciones de
inserción, la lista de hijos de la raíz se acorta. Si bien la extracción del mínimo es una operación
individualmente costosa, su ejecución reorganiza la estructura reduciendo el costo de
operaciones futuras.

La implementación de otras operaciones, como cambiar la prioridad de un nodo, requieren


agregar punteros al padre, ya que se requiere implementar la operación buscar. Debe notarse que
la lista de hijos no está ordenada.

Es notable destacar que en la concepción de este algoritmo se emplean listas para representar
multiárboles que representan colas de prioridad. Mostrando las posibilidades de las estructuras
básicas, que se exponen en este curso, para construir nuevas estructuras más complejas.

22.3. Operación para formar pares adyacentes de heaps.

Se recorren las listas de hijos, tomando dos adyacentes; se mezclan y se va formando una lista
con los menores elementos de cada par. Suponemos una lista ya formada apuntada por lp, y dos
heaps adyacentes: ha y hb, como se muestra en la Figura .7.
hb ha
lp
b a

db da dl

Figura 22.7. Mezcla de dos heaps adyacentes.

El siguiente segmento describe la operación. Si a<b, los descendientes de a, representados por


el triángulo da, se colocan a continuación de la lista encabezada por b. Al inicio la lista lp está
vacía, de este modo los elementos menores de los pares quedan en orden inverso al que tenían al
comenzar.
Si a >= b, se deja b arriba. La operación se ilustra en la Figura 22.8, a la izquierda después de
ejecutado el if; y a la derecha luego de realizado el else.
if (a < b)
{ hb->left = ha->down; ha->down = hb;
ha->left = lp; lp = ha;
}
else { ha->left = hb->down; hb->down = ha;
hb->left = lp; lp = b;
}

Si ha y hb son heaps, debe cumplirse que da, los descendientes de a, son mayores que a; y
db>b. La Figura 22.8, muestra que lp es un heap, ya que cumple la propiedad de que los hijos
tienen claves mayores o iguales a las del padre.

Profesor Leopoldo Silva Bijit 09-11-2009


Colas de prioridad. Pairing heaps. 5
lp lp

a b
dl dl

b a
da db

db da

Figura 22.8. Lista de pares.

Si el número de hijos de la raíz es impar, el último heap se coloca al inicio de la lista de pares.

Para el caso de la Figura 22.6., se procesa primero el par 3-5, quedando al fondo de la lista, en
este caso se ejecuta el if. Luego se agrega el par 7-4, ejecutándose el else; finalmente se agrega
al inicio de la lista el heap con raíz 6.
lp
3 4 6

5 12 7 8 10

9 6

Figura 22.9. Lista de pares para lista de hijos de la raíz de la Figura .6.

Debe notarse que en la lista izquierda de lp, los elementos no quedan ordenados. Sin embargo
de una lista hijos de cinco elementos, ahora se tiene una lista de los menores de los pares
formada por tres elementos. También debe observarse que la nueva raíz puede ser cualquier
nodo de la lista lp, y por lo tanto deberá recorrerse ésta hasta el final.

22.4. Selección del mínimo y formación del nuevo heap.

Si ha es el inicio de la lista y hb, el siguiente, la selección del mínimo de la lista, puede


obtenerse con el siguiente segmento, que se realiza para todos los elementos de la lista:

hb ha
b a

db da

Figura 22.10. Selección del menor, y formación del heap.

Profesor Leopoldo Silva Bijit 09-11-2009


6 Estructuras de Datos y Algoritmos
hb = ha->left;
if (a < b) { hb->left = ha->down; ha->down = hb; }
else { ha->left = hb->down; hb->down = ha; ha = hb; }

Luego de realizados los enlaces, se muestra la situación a la izquierda de la Figura 22.11,


cuando se efectúa el if; y a la derecha, luego de realizado el else. El objetivo es dejar al inicio de
la lista al menor.
ha ha
a b

b a
da db

db da

Figura 22.11. Recorrido de la lista y selección del menor.

Nótese que nuevamente se acorta el largo de las listas de hijos.

Para la Figura 22.9, se ejecuta primero el if, y resulta la Figura 22.12, a la izquierda. Luego se
ejecuta el else y se obtiene el heap, que se muestra a la derecha, en la Figura 22.12; el inicio de
la lista apunta a la nueva raíz. Si bien las listas de hijos no quedan ordenadas, disminuye el largo
de éstas.
ha

3
ha
3 4 5 4

5 12 7 6 9 6 12 7 6

9 6 8 10 8 10

Figura 22.12. Formación del nuevo heap.

22.5. Tipos de datos.

typedef struct bintree


{ struct bintree * left;
struct bintree * down;
int prioridad;
} nodo, *pnodo;

Profesor Leopoldo Silva Bijit 09-11-2009


Colas de prioridad. Pairing heaps. 7
typedef pnodo queue;

22.6. Creación de nodo.

pnodo getnodo(int prioridad)


{ pnodo p=NULL;
if ( (p= (pnodo) malloc(sizeof(nodo))) ==NULL)
{ printf("Error: Memoria. \n"); exit(1);}
else
{ p->prioridad=prioridad; p->left=NULL; p->down=NULL; }
//heap sin hijos ni descendientes.
return(p);
}

22.7. Inicio de cola.

void initqueue( queue *q )


/* inicia variable de tipo queue */
{ *q = NULL;
} /* initqueue */

22.8. Test de cola vacía.

int emptyqueue( queue q )


/* test para determinar si la cola de prioridad está vacía */
{ return (q == NULL);
} /* emptyqueue */

22.9. Insertar.

Se pasa la cola de prioridad con una referencia a ésta.

void enqueue( pnodo n, queue *q )


{
if (*q == NULL) { /* encola en heap vacío */
*q = n;
} else { /* considera n como un heap de un elemento, y lo enlaza con q */
if ((*q)->prioridad < n->prioridad) { /* q permanece como raíz */
n->left = (*q)->down;
(*q)->down = n;
} else { /* n se promueve como nueva raíz */
(*q)->left = NULL; //asegura que la raíz vieja no tiene hermanos.
n->down = *q;
*q = n; //cambia la raíz
}
}
} /* enqueue */

Profesor Leopoldo Silva Bijit 09-11-2009


8 Estructuras de Datos y Algoritmos
La Figura 22.13, a la izquierda, muestra los argumentos de la función, se ilustra la variable q,
que apunta a la variable que apunta a la raíz de la cola. El nuevo nodo n, puede obtenerse
invocando a getnodo(b).

Al centro se muestra la situación de las variables luego de ejecutadas las acciones realizadas en
el if, cuando a<b. Donde a toma el valor de (*q)->prioridad; y b es la prioridad del nuevo nodo
que será insertado: n->prioridad. A la derecha luego de las acciones del else.
q q
q
*q *q
cola *q a a n
a n
a b
a b n
b a

da da da

Figura 22.13. Casos en Inserción.

22.10. Desencolar.

pnodo dequeue( queue* q )


{ pnodo lp /* puntero a lista de pares */;
pnodo next /* puntero al siguiente heap de la lista */;
pnodo ha,hb /* punteros a heaps adyacentes de la lista */;

pnodo minimo = *q; //selecciona el mínimo


if (*q != NULL) { /* sólo si la cola no está vacía */
next = (*q)->down;
lp = NULL;
while (next != NULL) { /* forma lista con los elementos menores de los pares */
ha = next; next = ha->left;
if (next != NULL)
{ hb = next; next = hb->left;
/* enlaza heap ha con heap hb */
if (ha->prioridad < hb->prioridad)
{ hb->left = ha->down; ha->down = hb;
ha->left = lp; lp = ha;
}
else
{ ha->left = hb->down; hb->down = ha;
hb->left = lp; lp = hb;
}
}
else { /* coloca el último hijo de la raíz al inicio de la lista */
ha->left = lp; lp = ha;

Profesor Leopoldo Silva Bijit 09-11-2009


Colas de prioridad. Pairing heaps. 9
}
}
/* selecciona el mínimo, para fijar la nueva raíz */
ha = lp;
if (ha != NULL)
{ next = ha->left;
while (next != NULL) {/* enlaza nodos de la lista de hijos formando un heap */
hb = next; next = hb->left;
if (ha->prioridad < hb->prioridad) {hb->left = ha->down; ha->down = hb;}
else {ha->left = hb->down; hb->down = ha; ha = hb;}
}
}
*q = ha; //nueva raíz
}
return (minimo);
} /* dequeue */

La función pasa por referencia la cola de prioridad.

22.11. Test de las funciones.

//Imprime raíz y lista de hijos


void prtpair(pnodo p)
{ pnodo t;
if (p!=NULL)
{ putchar('\n');
printf("r=%d l=", p->prioridad);
t=p->down;
while(t!=NULL)
{ printf("%d ", t->prioridad);
t=t->left;
}
}
}

queue pairingheap=NULL;
#define N 30
int main (void)
{ pnodo t;
int i;

//ascendente
for(i=1; i<=N; i++)
{enqueue(getnodo(i), &pairingheap);
//prtpair(pairingheap);
}
putchar('\n');

Profesor Leopoldo Silva Bijit 09-11-2009


10 Estructuras de Datos y Algoritmos
for(i=1; i<=N; i++)
{ //prtpair(pairingheap);
t= dequeue(&pairingheap);
printf(" %d ", t->prioridad);
free(t);
}
putchar('\n');
//descendente
for(i=N; i>0; i--)
{enqueue(getnodo(i), &pairingheap); putchar('\n');
//prtpair(pairingheap);
}
for(i=1; i<=N; i++)
{ //prtpair(pairingheap);
t= dequeue(&pairingheap); //extrae el mínimo. Aquí debería usarse este dato.
printf(" %d ", t->prioridad); //realiza la acción de imprimir.
free(t);
}
putchar('\n');

srand(1);
for(i=1; i<=N; i++)
{enqueue(getnodo(rand()%100), &pairingheap); putchar('\n');
//prtpair(pairingheap);
}
for(i=1; i<=N; i++)
{ //prtpair(pairingheap);
t= dequeue(&pairingheap);
printf(" %d ", t->prioridad);
free(t);
}
putchar('\n');

return(0);
}

Referencias.

Fredman, Sedgewick, Sleator and Tarjan, "Pairing Heaps: A New Form of Self-Adjusting
Heaps", Algorithmica, (1986), vol. 1, 111-129).

Profesor Leopoldo Silva Bijit 09-11-2009


Colas de prioridad. Pairing heaps. 11

Índice general.

CAPÍTULO 22 ........................................................................................................................................... 1
PAIRING HEAPS. SELECCIONAR. ...................................................................................................... 1
22.1. PROPIEDADES. ................................................................................................................................. 1
22.1. ESTRUCTURA DE DATOS................................................................................................................... 3
22.3. OPERACIÓN PARA FORMAR PARES ADYACENTES DE HEAPS.............................................................. 4
22.4. SELECCIÓN DEL MÍNIMO Y FORMACIÓN DEL NUEVO HEAP. .............................................................. 5
22.5. TIPOS DE DATOS............................................................................................................................... 6
22.6. CREACIÓN DE NODO......................................................................................................................... 7
22.7. INICIO DE COLA................................................................................................................................ 7
22.8. TEST DE COLA VACÍA. ...................................................................................................................... 7
22.9. INSERTAR. ....................................................................................................................................... 7
22.10. DESENCOLAR. ................................................................................................................................ 8
22.11. TEST DE LAS FUNCIONES. ............................................................................................................... 9
REFERENCIAS. ........................................................................................................................................ 10
ÍNDICE GENERAL. ................................................................................................................................... 11
ÍNDICE DE FIGURAS................................................................................................................................. 11

Índice de figuras.

FIGURA 22.1. HEAP MULTIÁRBOL. ................................................................................................................ 1


FIGURA 22.2. INSERCIÓN EN HEAP MULTIÁRBOL........................................................................................... 1
FIGURA 22.3. FORMACIÓN DE PARES DE HEAPS............................................................................................. 2
FIGURA 22.4. ENLAZAMIENTO DE PARES. ..................................................................................................... 2
FIGURA 22.5. MULTIÁRBOL DE PRIORIDAD. .................................................................................................. 3
FIGURA 22.6. LISTAS DE HERMANOS Y DESCENDIENTES. .............................................................................. 3
FIGURA 22.7. MEZCLA DE DOS HEAPS ADYACENTES. .................................................................................... 4
FIGURA 22.8. LISTA DE PARES. ..................................................................................................................... 5
FIGURA 22.9. LISTA DE PARES PARA LISTA DE HIJOS DE LA RAÍZ DE LA FIGURA .6. ....................................... 5
FIGURA 22.10. SELECCIÓN DEL MENOR, Y FORMACIÓN DEL HEAP................................................................. 5
FIGURA 22.11. RECORRIDO DE LA LISTA Y SELECCIÓN DEL MENOR. ............................................................. 6
FIGURA 22.12. FORMACIÓN DEL NUEVO HEAP. ............................................................................................. 6
FIGURA 22.13. CASOS EN INSERCIÓN. ........................................................................................................... 8

Profesor Leopoldo Silva Bijit 09-11-2009


1

Capítulo 23

Algoritmos numéricos.

23.1. Solución de sistema simultáneo de ecuaciones lineales.

Si se aplica método nodal con modificaciones, para tratar fuentes de voltajes controladas e
independientes, se obtiene un sistema de ecuaciones, del tipo:

A x b

Donde A es la matriz nodal aumentada, x es el vector de incógnitas y b el vector de excitaciones.

Existen dos esquemas generales para resolver sistemas lineales de ecuaciones: Métodos de
eliminación directa y Métodos Iterativos. Los métodos directos, están basados en la técnica de
eliminación de Gauss, que mediante la aplicación sistemática de operaciones sobre los
renglones transforma el problema original de ecuaciones en uno más simple de resolver.

De entre los variados esquemas, basados en la eliminación de Gauss, el método de


descomposición en submatrices triangulares (LU, de Lower y Upper) es preferentemente
empleado en implementaciones computacionales, para sistemas de menos de 300 ecuaciones.

Para sistemas de un mayor número de ecuaciones se emplean métodos iterativos.


La mayoría de estos procedimientos están basados en el método de Gauss Seidel, con
aceleraciones para la convergencia.

23.1.1. Descomposición LU.

Está basado en descomponer la matriz de coeficientes en dos matrices triangulares L y U, según:

A L U

Donde L es una matriz triangular inferior (lower), y U es una matriz triangular superior (upper).

El sistema original de ecuaciones, queda:

L U x b

Que puede ser interpretado como dos sistemas de ecuaciones:

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos

L d b
U x d

Los dos sistemas anteriores son sencillos de resolver, como se verá más adelante. El sistema con
matriz L, puede ser resuelto por substituciones hacia adelante; el sistema con matriz U se
resuelve por substituciones hacia atrás.

El procedimiento está basado en obtener las matrices L y U, a partir de A; luego en obtener el


vector d; y finalmente en calcular la solución en el vector x.

Existen varias formas de efectuar la descomposición, el método de Doolittle asigna unos a los
elementos de la diagonal principal de L.

Veremos a través de un ejemplo, las principales ideas, intentando obtener un algoritmo para el
cálculo.

Se tiene la matriz A de 4x4 y se desea obtener L y U.

a11 a12 a13 a14 1 0 0 0 u11 u12 u13 u14


a21 a22 a23 a24 l21 1 0 0 0 u22 u23 u24
A
a31 a32 a33 a34 l31 l32 1 0 0 0 u33 u34
a41 a42 a43 a44 l41 l42 l43 1 0 0 0 u44

Efectuando la multiplicación de las matrices L y U, se obtiene:

u11 u12 u13 u14


l21u11 l21u12 u22 l21u13 u23 l21u14 u24
A
l31u11 l31u12 l32u22 l31u13 l32u23 u33 l31u14 l32u24 u34
l41u11 l41u12 l42u22 l41u13 l42u23 l43u33 l41u14 l42u24 l43u34 u44

El primer renglón de A permite, por comparación, determinar el primer renglón de U.

u11 a11 ; u12 a12 ; u13 a13 ; u14 a14

Una vez conocido u11, la primera columna de A permite determinar el primer renglón de L, se
obtienen:

l21 a21 / u11 ; l31 a31 / u11 ; l41 a41 / u11

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 3
El segundo renglón de A, permite calcular el segundo renglón de U, una vez conocidos los
elementos del primer renglón de U, se tienen:

l21u12 u22 a22 ; l21u13 u23 a23 ; l21u14 u24 a24

Despejando los elementos del segundo renglón de U, se obtienen:

u22 a22 l21u12


u23 a23 l21u13
u24 a24 l21u14

La segunda columna de A, permite calcular la segunda columna de L.

l31u12 l32u22 a32 ; l41u12 l42u22 a42

Despejando los elementos de la segunda columna de L. se obtienen:

l32 (a32 l31u12 ) / u22


l42 (a42 l41u12 ) / u22

Del tercer renglón de A, resultan:

l31u13 l32u23 u33 a33 ; l31u14 l32u24 u34 a34

Las que permiten despejar los elementos del tercer renglón de U:

u33 a33 l31u13 l32u23


u34 a34 l31u14 l32u24

De la tercera columna de A, se puede calcular la tercera columna de L:

l43 (a43 l41u13 l42u23 ) / u33

Finalmente, el cuarto renglón de A, permite calcular el cuarto renglón de U.

u44 a44 l41u14 l42u24 l43u34

Si bien se ha desarrollado para una matriz de 4x4, de las expresiones obtenidas puede inducirse
relaciones generales, como veremos a continuación:

Con N es el número de renglones y columnas de A.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
Para: n 1,..., N ;
ln, n 1
La diagonal de L, se obtiene por definición de la descomposición de Doolittle.

El n-avo renglón de U se obtiene según:


n 1
un, i an,i ln , k u k , i
k 1
Para: i n,..., N ;

Y la n-ava columna de L con:


n 1
l j,n a j,n l j , k uk , n / un , n
k 1
Para: j n 1,..., N

A continuación se obtiene el algoritmo para la substitución hacia delante:

De la relación:
L d b
Se obtiene:

l11 0 0 0 d1 b1
l21 l22 0 0 d2 b2
l31 l32 l33 0 d3 b3
l41 l42 l43 l44 d4 b4

Efectuando las multiplicaciones, en el lado derecho, se tienen:

l11d1 b1
l21d1 l22 d 2 b2
l31d1 l32 d 2 l33 d 3 b3
l41d1 l42 d 2 l43 d 3 l44 d 4 b4

Las componentes del vector d, se obtienen según:

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 5
d1 b1 / l11
d2 (b2 l21d1 ) / l22
d3 (b3 l31d1 l32 d 2 ) / l33
d4 (b4 l41d1 l42 d 2 l43d3 ) / l44

Una vez obtenido d1, se substituye en la expresión siguiente para calcular d2; con d1 y d2, se
puede calcular d3; y así sucesivamente. Por esta razón, al procedimiento se lo denomina
substitución hacia adelante (forward).

El vector d, puede recalcularse para diferentes valores del vector b, que es la situación que se
produce en un barrido DC. Debido a que en el método de Gauss se ocupa, desde el inicio de las
operaciones, los valores de b; el efectuar cálculos con b variable lo realiza con ventajas el
método de descomposición triangular.

La relación anterior, permite deducir una expresión para calcular los d i, en una matriz de orden
N.
i l
di (bi lij d j ) / lii
j 1

Para: i 1, 2, ,N
En la descomposición de Doolittle, los lii son unos.

El algoritmo para la substitución hacia atrás se obtiene de manera similar a las anteriores.
Para la triangular superior:

U x d
Se tiene:
u11 u12 u13 u14 x1 d1
0 u22 u23 u24 x2 d2
0 0 u33 u34 x3 d3
0 0 0 u44 x4 d4

Efectuando las multiplicaciones, se obtiene:

u11 x1 u12 x2 u13 x3 u14 x4 d1


u22 x2 u23 x3 u24 x4 d2
u33 x3 u34 x4 d3
u44 x4 d4
Despejando los xi, se obtienen:

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
x4 d 4 / u44
x3 (d3 u34 x4 ) / u33
x2 (d 2 u23 x3 u24 x4 ) / u22
x1 (d1 u12 x2 u13 x3 u14 x4 ) / u11

Que entrega la solución del sistema de ecuaciones. Nótese que primero se obtiene x4; y luego x3,
que se calcula en términos de x4; y así sucesivamente. Por esta razón a este algoritmo se lo
denomina substitución hacia atrás (back).

En general:
xN d N / u NN
N
di uij x j
j i 1
xi
uii

Para: i ( N 1),( N 2), ,3, 2,1

La observación cuidadosa de la generación de las matrices L y U, muestra que no es necesario


emplear espacio adicional para éstas. Los valores que se van calculando pueden almacenarse en
la matriz A. Tampoco es necesario almacenar los elementos de la diagonal principal de L, ya que
son unos por definición. Entonces luego de la descomposición, la matriz original queda, en el
caso del ejemplo de 4x4:

u11 u12 u13 u14


l21 u22 u23 u24
A
l31 l32 u33 u34
l41 l42 l43 u44

void ludcmp(float **a, int N)


/*Dada a[1..N][1..N], la reemplaza por la descomposición LU Doolittle.*/
{ int n,i,j,k;
float sum;

for (n=1; n<=N; n++)


{
for (i=n; i<=N; i++)
{ sum=0; n 1
for (k=1; k<=(n-1); k++) sum += a[n][k]*a[k][i]; un, i an,i ln , k u k , i
a[n][i]-=sum; k 1
}

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 7
for (j=n+1; j<=N; j++)
{ sum=0; n 1
for (k=1; k<=(n-1); k++) sum += a[j][k]*a[k][n]; l j,n a j,n l j , k uk , n / un , n
a[j][n]=(a[j][n]-sum)/a[n][n]; k 1
}
}
}

/*Realiza substituciones hacia adelante y hacia atrás.


a[1..N][1..N] es de entrada y debe contener la descomposición L y U.
b[1..N] el vector de excitación.
Retorna en b la solución.
a no es modificada y pueden realizarse sucesivos llamados
con diferentes valores de b.
*/
void lufwbksb(float **a, int N, float b[])
{ int i,j;
float sum;
for (i=1; i<=N; i++)
{ sum=0; i l
for (j=1; j<=(i-1); j++) sum += a[i][j]*b[j]; d i (bi lij d j ) / lii
b[i]-=sum; //l[i][i]=1. j 1
} //se genera vector d en el espacio ocupado por b.
b[N]/=a[N][N]; xN d N / u NN
for (i=(N-1); i>=1; i--)
N
{ sum=0; di uij x j
for (j=(i+1); j<=N; j++) sum += a[i][j]*b[j]; j i 1
b[i]=(b[i]-sum)/a[i][i]; xi
} //se almacena solución x en el espacio ocupado por b.
uii
}

Ejemplo de uso.
//Resuelve el sistema de ecuaciones lineales a·X = b.
ludcmp(a, n);
lufwbksb(a, n, b);

23.1.2. Métodos iterativos.

Para deducir expresiones generales que permitan escribir algoritmos iterativos, consideremos el
sistema lineal de tres ecuaciones:
a11 a12 a13 x1 b1
a21 a22 a23 x2 b2
a31 a32 a33 x3 b3

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
Despejando de la primera ecuación, la variable x1 ; de la segunda x2 ; y de la tercera x3 ,
obtenemos:
x1 (b1 a12 x2 a13 x3 ) / a11
x2 (b2 a21 x1 a23 x3 ) / a22
x3 (b3 a31 x1 a32 x2 ) / a33

Si consideramos conocidos los valores de las variables del lado derecho, podremos estimar un
nuevo valor para las variables del lado izquierdo de las ecuaciones. Podemos anotar lo anterior,
mediante:

x1[n 1] (b1 a12 x2 [n] a13 x3[n]) / a11


x2 [n 1] (b2 a21 x1[n] a23 x3[n]) / a22
x3 [n 1] (b3 a31 x1[n] a32 x2 [n]) / a33

Donde xi [ n 1] es el valor de xi en la iteración (i+1); y xi [ n ] es el valor obtenido en la


iteración anterior.

Durante el proceso iterativo se verifica la convergencia calculando el mayor cambio relativo


entre una iteración y la siguiente, y comparando el valor absoluto de esta diferencia con la
tolerancia deseada.

| xi [n 1] xi [n] | tolerancia

Si el error es menor que la exactitud requerida el proceso termina; en caso contrario se realiza
una nueva iteración.

Si se tienen N variables, pueden generalizarse las iteraciones según:

j i 1 j N
xi [n 1] (bi aij x j [n] aij x j [n]) / aii
j 1 j i 1

El esquema anterior se reconoce como método de Jacobi.

Si el cálculo de las variables se realiza en orden, desde x1 hasta xN , puede observarse que una
vez obtenido x1 puede usarse este valor para calcular x2 ; y así sucesivamente. Entonces en el
cálculo xi se pueden emplear los nuevos valores de las variables desde x1 hasta xi 1 .

Entonces el esquema iterativo puede plantearse:

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 9
j i 1 j N
xi [n 1] (bi aij x j [n 1] aij x j [n]) / aii
j 1 j i 1

El que se denomina método de Gauss Seidel.

Mejores resultados se logran calculando las variables en orden decreciente de los valores de la
diagonal principal.

Una mejora notable de la convergencia se logra empleando un promedio ponderado de los


resultados de las dos últimas iteraciones para obtener el nuevo valor. Esto se denomina método
de sucesivas sobre relajaciones (SOR Successive Over-Relaxation).

xi [n 1] axi [n 1] (1 a ) xi [n]
Con: 0 a 2

Si a es 1, se tiene la fórmula de Gauss Seidel. Con a>1, el nuevo valor, en la iteración (n+1),
tiene mayor importancia. Con a<1, se tiene subrelajación. La elección de este valor, y su
influencia en la convergencia debería aclararse en un curso de análisis numérico.

La recurrencia para encontrar el nuevo valor por el método SOR:

j i 1 j N
xi [n 1] (1 a) xi [n] a (bi aij x j [n 1] aij x j [n]) / aii
j 1 j i 1

El algoritmo descrito en pseudo código:

SOR. Dados: N, a, b, x[0], tol, nmax.


for n = 1, … nmax do Comienza iteraciones
for i = 1,.... N do
j i 1 j N
yi [n] (1 a) xi [n] a(bi aij y j [n] aij x j [n]) / aii
j 1 j i 1
end for i
yi xi
for j = 1,….. N do
xj yj
end for i
if ( tol ) stop
end for n

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

/*Dados a[1..N][1..N], b[1..N] calcula x[1..N]


Con nmax iteraciones y factor de relajación alfa. Retorna solución en x[1..N]*/
int sor(float **a, int N, float *x, float *b, int nmax, float alfa)
{ int n, i, j, and;
float sumi, sums;
float *y;
y=vector(1, N); //vector de flotantes
for (n=1; n<=nmax; n++)
{
for(i=1; <=N; i++)
{
sumi=0; for (j=1; j<=(i-1); j++) sumi += a[i][j]*y[j];
sums=0; for (j=i+1; j<=N; j++) sums += a[i][j]*x[j];
y[i]=(1.-alfa)*x[i] + alfa*(b[i]-sumi-sums)/a[i][i];
}
and=1;
for(j=1; j<=N; j++)
{ and = and && (fabs(y[j]-x[j]) < tol*(fabs(x[j])); //error relativo
if( and==0) break;
}
for(j=1; j<=N; j++) x[j]=y[j];
if (and) break;
//putchar('.');
}
free_vector(y, 1, N);
return (n);
}

En lugar de emplear errores absolutos: (fabs(y[j]-x[j]) < tol), es preferible utilizar errores
relativos: (fabs(y[j]-x[j]) < tol*(fabs(x[j]) );

23.2. Solución numérica de sistemas de ecuaciones diferenciales.

Una ecuación diferencial de primer orden puede resolverse numéricamente mediante


integración.
Si se tiene:
dr (t )
F (t )
dt
Entonces:
t
r (t ) r (0) F ( )d
0

F (t ) considera la variación de r(t) y de las excitaciones que producen la respuesta r(t).

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 11
Una manera simple y aproximada de realizar la integración es calcular el área mediante la suma
de rectángulos, que estudiaremos como el método de Euler.

Una mejor aproximación se logra sumando trapecios, si se desea mayor precisión se emplea
aproximación por segmentos parabólicos, con la regla de Simpson. Para disminuir la
acumulación de errores se emplea el método de Runge-Kutta.

23.2.1. Formulación de ecuaciones de estado.

La formulación de las ecuaciones de una red eléctrica en términos de las variables de estado
permite encontrar la solución de un sistema de ecuaciones diferenciales de primer orden en el
dominio del tiempo. La solución numérica, que veremos a continuación, puede extenderse a
sistemas no lineales.

La representación se logra con un sistema de ecuaciones diferenciales de primer orden:

dx
Ax Bu
dt

Donde x es el vector de estado, u es el vector de entrada o de excitaciones.

El resto de las variables del sistema puede expresarse en términos del estado, según:

y Cx Du

Donde y es el vector de salida.


A se denomina matriz de estado del sistema, B es la matriz de entrada, C es la matriz de salida, y
D se denomina matriz de alimentaciones directas (feedforward).

23.2.2. Método de Euler.

A partir de la expansión en serie de Taylor, para una variable escalar x, se tiene:

dx(t ) 1 dx 2 (t ) 2
x(t t) x(t ) t t ....
dt 2 dt 2

La relación anterior, puede generalizarse considerando a x como el vector de estado. Pueden


calcularse, aproximadamente, los valores de las variables de estado en el instante siguiente
(k+1), a partir de los valores en el instante k-ésimo, mediante:

dxi (tk )
xi [k 1] xi [k ] t
dt
Este procedimiento iterativo se denomina esquema simple de Euler.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
Los valores de las derivadas, en un instante determinado, se obtienen mediante la matriz de
estado.
j n j n
xi [k 1] xi [k ] ( aij x j [k ] bij u j [k ]) t
j 1 j 1

Para: i 1, 2, ,n

A partir de la ecuación de estado se determina el valor de las derivadas en un punto.

La siguiente función calcula en la matriz x, los valores de las variables de estado en npuntos
separados en intervalos de tiempo Delta.
No se considera la matriz b, ni el vector u de excitaciones. Esto equivale a resolver un sistema
de ecuaciones diferenciales lineales homogéneas y de primer orden.

void euler(float **a, int N, float **x, float *ic, int npuntos, float Delta)
/*Dados a[1..N][1..N], ic[1..N] calcula x[1..N][1..npuntos]*/
{ int i, j, k;
float sum, t=0.;
for(i=1; i<=N; i++) x[i][1]=ic[i]; //condiciones iniciales.
for (k=1; k<npuntos; k++)
{ t= t+Delta;
for(i=1; i<=N; i++)
{
sum=0; for (j=1; j<=N; j++) sum += a[i][j]*x[j][k];
x[i][k+1]= x[i][k]+sum*Delta;
}
}
}

Una alternativa de diseño es generar un archivo de datos en lugar de almacenar los puntos en
una matriz. Con el archivo de datos se pueden generar formas de ondas.

La siguiente función ilustra la generación de un archivo de datos compatible con el comando


pointplot de Maple. Generando los puntos (t[k], x[i][k]) para la variable xi .

void genseq(float **a, int N, float **x, int npuntos, float Delta, int i)
/*Dados a[1..N][1..N], ic[1..N] y x[1..N][1..npuntos]
genera seq compatible para gráficos de tipo pointplot en Maple.*/
{ int k;
float t=0.;
printf("Seq:=[");
for (k=1; k<=npuntos; k++, t=t+Delta)
{ printf("[%g,%g]\n", t, x[i][k]);
if (k<npuntos) putchar(',');
}
putchar(']'); putchar('\n');

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 13
}

Ejemplo de uso:
n=2;npuntos=20;
a=matrix(1, n, 1, n); //pide espacio
a[1][1]=0.; a[1][2]=1.;
a[2][1]=-3.; a[2][2]=-2.;
ic=vector(1, n);
ic[1]=1.;ic[2]=0.;
x=matrix(1, n, 1, npuntos); //pide espacio

//Resuelve sistema ecuaciones diferenciales.


euler(a,n,x,ic,npuntos,0.1);

genseq(a,n,x,npuntos,0.1,1);

La última invocación genera para la variable x1 :


Seq:=[[0,1],[0.1,1],[0.2,0.97],[0.3,0.916],[0.4,0.8437],[0.5,0.75838],[0.6,0.664813]
,[0.7,0.567208],[0.8,0.46918],[0.9,0.373741],[1,0.283314],[1.1,0.199761]
,[1.2,0.124418],[1.3,0.0581519],[1.4,0.00140604],[1.5,-0.0457352]
,[1.6,-0.0834903],[1.7,-0.112322],[1.8,-0.132883],[1.9,-0.145962]
]

Debido a la acumulación de errores no suele emplearse el algoritmo de Euler.

23.2.3. Algoritmo de Euler.

Para el caso de una variable, tenemos:


dy(t )
f (t , y (t ))
dt

Se pueden obtener los valores sucesivos de y, mediante:

yn 1 yn hf (tn , y (tn ))
Si definimos:
k1 hf (tn , yn )
Se realiza la integración mediante:
yn 1 yn k1

Se tiene que f (tn , yn ) es la derivada de la función y(t ) , y k1 es el área del rectángulo bajo la
curva de f (t , y) , y también el incremento de la ordenada. Las relaciones se muestran en la
Figura 1, para la variable y(t ) .

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
yn 1 yn k1
tg ( n ) f (tn , yn )
tn 1 tn h
yn+1 f(tn+1,yn+1)

n
k1
yn f(tn,yn)

tn tn+1 tn tn+1
h h

Figura 23.1. Método de Euler.

23.2.4. Algoritmo trapezoidal.

Una mejor aproximación para el área bajo la curva de f (t , y) es mediante el trapecio entre las
paralelas t n y tn 1 .

yn+1 f(tn+1,yn+1)
(k1 + k2)/2
n
yn f(tn,yn)

tn tn+1 tn tn+1
h h

Figura 23.2. Método trapezoidal.

Entonces:
h
yn 1 yn ( f (tn , yn ) f (tn 1 , yn 1 ))
2
Con:
k1 hf (tn , yn )
k2 hf (tn 1 , yn 1 )

Se realiza la integración trapezoidal mediante:

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 15
1
yn 1 yn (k1 k2 )
2

En cada paso de integración es preciso evaluar dos veces la función: f (t , y) . La determinación


de y n 1 que se emplea en el cálculo de k2 , puede determinarse, aplicando Euler, según:
yn 1 yn k1

23.2.5. Algoritmo de Simpson.

Para refinar el cálculo del área, se define un punto dentro del intervalo, de este modo puede
calcularse el área bajo una parábola que pasa por los tres puntos:

f (t0 , y0 ) , f (t1 , y1 ) , f (t2 , y2 ) , con t1 t0 h / 2 y t2 t0 h.

yn+1 f(tn+h/2, y(tn+h/2))


f(tn+1,yn+1)
(k0 +4 k1+ k2)/6
n
yn f(tn,yn) fa(t)

tn tn+1 tn tn+1
h h

Figura 23.3. Método de Simpson.

Si la ecuación de la parábola que aproxima el área bajo la curva de f (t , y) es:

f a (t ) at 2 bt c
Se tienen:
f0 at0 2 bt0 c
f1 at12 bt1 c
f2 at2 2 bt2 c

Que permiten calcular a, b, c , conociendo: f 0 , f1 , f 2 , t0 , t1 , t2 . Luego se realiza la integral:

t t2

A f a (t )dt
t t0

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Aplicando que t1 t0 h / 2 y t2 t0 h , se obtiene:
h
A ( f 0 4 f1 f 2 )
6
Con:
k0 hf (tn , yn )
k1 hf (tn h / 2, y (tn h / 2))
k2 hf (tn h, y (tn h))

El paso de integración puede realizarse según:

1
yn 1 yn (k0 4k1 k2 )
6

En cada paso de integración es preciso evaluar tres veces la función: f (t , y) .


Los valores de las ordenadas intermedias, pueden calcularse, según:

y(tn h / 2) yn k0 h / 2
y(tn h) yn k0 h

La fórmula anterior permite deducir la conocida fórmula para la aproximación de Simpson.


Si se tienen cinco valores de tiempos para los cuales se calcula la integral, se tiene el área
acumulada, según:

1 1
Area (k0 4k1 k2 ) (k2 4k3 k4 )
6 6
La cual puede simplificarse a:
1
Area (k0 4k1 2k2 4k3 k4 )
6
Con 2n+1 puntos en total, se tiene en general:

h
Area ( f 0 4 f1 2 f 2 4 f3 ...... 2 f 2 n 2 4 f2n 1 f2n )
6

double SimpsonIntegral(double a, double b, int n)


{ double h, suma, sumap, sumai;
int i;
if (n%2==1) n++; //deja a n como número par.
h=(b-a)/n;
suma=f(a)+f(b); //suma los extremos
for(i=1, sumap=0; i<n; i+=2){
sumap+=f(a+i*h); //acumula impares

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 17
}
for(int i=2, sumai=0; i<n; i+=2){
sumai+=f(a+i*h); //acumula pares.
}
return ((suma+4*sumap+2*sumai)*h/3);
}

23.4.6. Métodos multietapas.

Existen métodos más elaborados para efectuar un paso de integración, En éstos la función se
evalúa en varias etapas entre dos puntos. Los puntos de las etapas sólo se emplean para el
cálculo del nuevo valor.

Consisten en definir una función en la cual se escogen los parámetros de tal modo de minimizar
los errores.

Sea la función yn , tn ; h , entonces, la integración se realiza mediante:


yn 1 yn h yn , t n ; h

El método de Euler es: yn , t n ; h f ( yn , t n )

El siguiente esquema es de Runge-Kutta, de segundo orden, con cuatro parámetros.

k1 f yn , t n
k2 f yn hk1 , tn h
yn 1 yn h ak1 bk2
Con:
y tn , tn af y (tn ), tn bf y (tn ) hf y (tn ), tn , tn h

Donde deben determinarse: a, b, , por consideraciones de exactitud.

Si se define el error de la aproximación por:

Tn y tn h y tn h y tn , tn

Para el primer término de Tn, por expansión de Taylor de y(t), resulta:

dy(tn ) h2 d 2 y(tn ) h3 d 3 y(tn )


y tn h y tn h O (h 4 )
dt 2! dt 2 3! dt 3

Como se tiene:

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
dy
f y t ,t
dt

La segunda derivada de y, se obtiene según:

d2y d f f dy f f
f y t ,t f ft fy f
dt 2 dt t y dt t y

Las derivadas parciales de f se han representado, en forma abreviada, mediante subíndices.

Para la tercera derivada, se tiene:

d3y d f f d f d f f df
f ( ) ( )f
dt 3 dt t y dt t dt y y dt

La cual permite obtener:

d3y 2
f f 2 2
f 2
f f f f
f f f f
dt 3 t 2
t y y t y 2
y t y

Finalmente para la tercera derivada:

d3y
ftt f yt f ( f yt f yy f ) f f y ( ft fy f )
dt 3

Reemplazando las derivadas obtenidas, en la expansión de Taylor, se logra:

h2 h3
y tn h y tn hf ( ft fy f ) ( ftt 2 fty f f yy f 2 f y ft f y 2 f ) O (h 4 )
2! 3!

Donde se ha empleado: f f y tn , tn

Para el segundo término de Tn, se requiere expandir el término que está multiplicado por b. Para
esto se emplea la expansión de Taylor de dos variables de f yn y, tn t , con:
y hf , t h . Como está multiplicado por h, sólo es necesario considerar hasta los
términos de segundo orden.

Entonces;

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 19

f y y, t t f y, t
f ( y, t ) f ( y, t ) 1 f 2 ( y, t ) 2 f 2 ( y, t ) 1 f 2 ( y, t ) 2
y t y y t t
y t 2 y2 y t 2 t2

Reemplazando en la anterior, los valores de los incrementos y empleando notación abreviada


para las derivadas parciales de f, se logra:
f yn y, tn t f yn , t n
1 1
f y ( hf ) f t ( h) f yy ( hf ) 2 f yt ( hf )( h) f tt ( h) 2 O(h3 )
2 2

Reemplazando la expresión anterior en:

y tn , tn af y (tn ), tn bf y (tn ) hf y (tn ), tn , tn h


Se obtiene:
1 1
y tn , tn af b( f f y hf ft h f yy ( hf )2 f yt ( hf ) h f tt 2 2
h ) O h3
2 2

Donde las funciones y las derivadas parciales están evaluadas en tn.

Se tiene finalmente para el error:

h h2
Tn h f ft ff y f tt 2 f ty f f yy f 2 f y ft fy f
2 3!
2
f yy f ftt 2 2
h af b f ( fy f f t )h h2 f yt fh 2 h O (h 4 )
2 2

Los parámetros: a, b, , , se escogen del tal modo de minimizar el error. En este caso pueden
hacerse cero los coeficientes de h y h 2 , pero no es posible eliminar la parte que depende de h3
.

Para eliminar la parte proporcional a h , se debe cumplir: f af bf 0


2
Para eliminar la parte proporcional a h , se debe cumplir: ( ft ff y ) / 2 bf y f bft 0

De la primera se obtiene: a b 1
1 1
De la segunda, debe cumplirse: ft ( b ) ff y (
b ) 0 , de la que se desprenden:
2 2
1
b b
2

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos

Con a 1, b 0 se tiene el método de Euler. No pueden ajustarse , . Por esta razón puede
decirse que el algoritmo de Euler pertenece a la familia Runge-Kutta de segundo orden.

23.4.6.1. Método de Heun.


1
Considera: 1, 0, a b que satisfacen las dos relaciones anteriores.
2
Se realiza la integración mediante:
h
yn 1 yn k1 k2
2
Con:
k1 f yn , t n
k2 f yn k1h, tn h
23.4.6.2. Método del punto medio.
1
Considera: , a 0, b 1 que satisfacen las dos relaciones anteriores.
2

yn 1 yn hk2
Con:
k1 f yn , t n
k1h h
k2 f yn , tn
2 2

La pendiente de y está dada ahora por el valor de f en el punto medio del intervalo.
yn+1 f(tn+h/2,yn+k1h/2)
f(tn+1,yn+1)
n
hk2
yn f(tn,yn)

tn tn+1 tn tn+1
h h

Figura 23.4. Método del punto medio.

23.4.6.3. Método de Ralston.


3 1 2
Considera: a , b que satisfacen las dos relaciones anteriores.
4 3 3

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 21
Se integra mediante:
h
yn 1 yn (k1 2k2 )
3
Con:
k1 f yn , t n
3k1h 3h
k2 f yn , tn
4 4

En los algoritmos del método de Runge-Kutta de segundo orden, debe evaluarse dos veces la
función: f y (t ), t , para obtener el siguiente punto de la función.

23.4.7. Métodos de Runge-Kutta de cuarto orden.

Para derivar el algoritmo de cuarto orden de Runge Kutta, se definen los 10 parámetros:
a1 , b1 , a2 , b2 , a3 , b3 , w1 , w2 , w3 , w4 . Los cuales deben elegirse de tal modo de eliminar los errores
proporcionales hasta la cuarta potencia del intervalo temporal h.

k1 hf yn , tn
k2 hf yn b1k1 , tn a1h
k3 hf yn b2 k2 , tn a2 h
k4 hf yn b3k3 , tn a3h
yn 1 yn w1k1 w2 k2 w3k3 w4 k4
Con:
y tn , tn w1 f yn , tn w2 f yn b1k1 , tn a1h
w3 f yn b2 k2 , tn a2 h w4 f yn b3k3 , tn a3h

La expresión para el error es:


Tn yn 1 yn h y tn , tn

Antes se obtuvieron las tres primeras derivadas de y. Se requiere calcular la cuarta derivada de
y, se tiene:
d4y d
( ftt 2 fty f f yy f 2 f y ft f y2 f )
dt 4 dt

Derivando, se obtiene:

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos

d4y dy dy d2y dy dy
(( f yyy f yyt ) f yy 2 f yyt f ytt )
dt 4 dt dt dt dt dt
2 3
dy d y d y dy dy d2y dy
2( f yy f yt ) 2 f y 3 ( f yyt f ytt ) f yt 2 f ytt f ttt
dt dt dt dt dt dt dt

Reemplazando la primera derivada de y:

d4y d2y
(( f yyy f f yyt ) f f yy f yyt f f ytt ) f
dt 4 dt 2
d2y d3y d2y
2( f yy f f yt ) fy ( f yyt f f ytt ) f f yt f ytt f fttt
dt 2 dt 3 dt 2

Reemplazando la segunda derivada de y, se tiene:

d4y
(( f yyy f f yyt ) f f yy ( ft fy f ) f yyt f f ytt ) f
dt 4
d3y
2( f yy f f yt )( ft fy f ) fy ( f yyt f f ytt ) f f yt ( f t fy f ) f ytt f f ttt
dt 3

Reemplazando la tercera derivada de y, se obtiene:

d4y
(( f yyy f f yyt ) f f yy ( f t fy f ) f yyt f f ytt ) f
dt 4
2( f yy f f yt )( f t f y f ) f y ( f tt f ty f ( f yt f yy f ) f
f y ( ft f y f )) ( f yyt f f ytt ) f f yt ( f t fy f ) f ytt f f ttt

Reemplazando las derivadas obtenidas, en la expansión de Taylor, se logra la serie:


h2 h3
y tn h y tn hf ( ft fy f ) ( f tt 2 f ty f f yy f 2 f y ft f y2 f )
2! 3!
h4
( f yyy f 3 3 f yyt f 2 4 f yy f y f 2 3 f yy ft f 3 f yyt f 5 f yt f y f
4!
3 f yt ft f y 3 f f y 2 ft f y f tt f ttt ) O(h5 )

Donde se ha empleado: f f y tn , tn

La segunda componente del error, requiere calcular:

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 23

h y tn , tn w1k1 w2 k2 w3k3 w4 k4

Se emplea la expansión de Taylor de dos variables de f yn y, tn t , con los diferentes


y, t . Como está multiplicado por h, sólo es necesario considerar hasta los términos de
tercer orden.

Entonces;
f yn y, tn t f yn , t n
1 1
fy y ft t f yy y 2 f yt y t f tt t 2
2 2
1 1 1 1
f yyy y 3 f yyt y 2 t f ytt y t2 f ttt t 3
6 2 2 6

Reemplazando en la anterior, los valores de los incrementos y empleando notación abreviada


para las derivadas parciales de f, se logra:
k1 hf

1 3 3 1 2 2 1 1 3
b1 f fyyy b1 f fyyt a1 b1 f a1 2 fytt a1 fttt h 4
6 2 2 6
k2 = 1 1
fyy b1 2 f 2 b1 f fyt a1 ftt a1 2 h 3 ( fy b1 f ft a1 ) h 2 hf
2 2

k3 =
fyy b1 2 f 2 ftt a1 2
fy b2 b1 f fyt a1 fyy b2 2 f ( fy b1 f ft a1 )
2 2
b2 2 f 2 fyyt a2 b2 f a2 2 fytt b2 3 f 3 fyyy a2 3 fttt
b2 ( fy b1 f ft a1 ) fyt a2
2 2 6 6
2 2 2
ftt a2 fyy b2 f
h4 fy b2 ( fy b1 f ft a1 ) b2 f fyt a2 h 3
2 2
( fy b2 f ft a2 ) h 2 hf

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
k4 =
a3 3 fttt b3 3 f 3 fyyy b3 f a3 2 fytt
hf fyy b3 2 f ( fy b2 f ft a2 )
6 6 2
ftt a2 2 fyy b2 2 f 2
fy b3 fy b2 ( fy b1 f ft a1 ) b2 f fyt a2
2 2
2 2
b3 f fyyt a3
b3 ( fy b2 f ft a2 ) fyt a3 h4
2
fyy b3 2 f 2 ftt a3 2
fy b3 ( fy b2 f ft a2 ) b3 f fyt a3 h 3
2 2
( fy b3 f ft a3 ) h2

Reemplazando las expresiones anteriores en: y tn , tn


Se obtiene:

h y tn , tn
b1 3 f 3 fyyy b1 2 f 2 fyyt a1 b1 f a1 2 fytt a1 3 fttt a3 3 fttt
w2 w4
6 2 2 6 6
b3 3 f 3 fyyy b3 f a3 2 fytt
fyy b3 2 f ( fy b2 f ft a2 )
6 2
ftt a2 2 fyy b2 2 f 2
fy b3 fy b2 ( fy b1 f ft a1 ) b2 f fyt a2
2 2
b3 2 f 2 fyyt a3
b3 ( fy b2 f ft a2 ) fyt a3 w3
2
fyy b1 2 f 2 ftt a1 2
fy b2 b1 f fyt a1 fyy b2 2 f ( fy b1 f ft a1 )
2 2
b2 2 f 2 fyyt a2 b2 f a2 2 fytt b2 3 f 3 fyyy a2 3 fttt
b2 ( fy b1 f ft a1 ) fyt a2
2 2 6 6

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 25
fyy b3 2 f 2 ftt a3 2
h4 w4 fy b3 ( fy b2 f ft a2 ) b3 f fyt a3
2 2
fyy b1 2 f 2 ftt a1 2
w2 b1 f fyt a1
2 2
ftt a2 2 fyy b2 2 f 2
w3 fy b2 ( fy b1 f ft a1 ) b2 f fyt a2 h3
2 2
( w4 ( fy b3 f ft a3 ) w2 ( fy b1 f ft a1 ) w3 ( fy b2 f ft a2 ) ) h 2
( w1 f w2 f w4 f w3 f ) h

Donde las funciones y las derivadas parciales están evaluadas en tn.

Deducir los valores de los parámetros para eliminar términos en la expresión para el error de la
aproximación resulta complejo.

La generación automática de la función , se logra con el segmento:


> restart;
> tmv:=mtaylor(f(y,t),[y=yn,t=tn],4):
> sus1:={D[1,1](f)(yn,tn)=fyy,D[1,2](f)(yn,tn)=fyt,D[2](f)(yn,tn)=ft,
D[1](f)(yn,tn)=fy,D[2,2](f)(yn,tn)=ftt,f(yn,tn)=f,
D[1,1,2](f)(yn,tn)=fyyt,D[1,2,2](f)(yn,tn)=fytt,
D[2,2,2](f)(yn,tn)=fttt,D[1,1,1](f)(yn,tn)=fyyy}:
> tmv:=subs(sus1,tmv):
> k1:=h*f:
> k2:=h*eval(tmv,{y-yn=b1*k1,t-tn=a1*h}):collect(k2,h):
> k3:=h*eval(tmv,{y-yn=b2*k2,t-tn=a2*h}):k3:=collect(k3,h):
> temp3:=k3:
for i from 5 to 13 do
temp3:=eval(temp3,h^i=0)
od:
collect(temp3,h):
> k4:=h*eval(tmv,{y-yn=b3*k3,t-tn=a3*h}):
k4:=collect(k4,h):
temp4:=k4:
for i from 5 to 40 do
temp4:=eval(temp4,h^i=0)
od:
> temp:=collect(w1*k1+w2*k2+w3*k3+w4*k4,h):
> for i from 5 to 40 do
temp:=eval(temp,h^i=0)
od:
Phi:=collect(temp,h):

La generación del primer término del error, puede generarse automáticamente con:
> restart;
> y1:=taylor(y(t),t=tn,5):y1:=subs({t-tn=h},y1):

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
>
sus0:={y(tn)=0,D(y)(tn)=f,`@@`(D,2)(y)(tn)=d2,`@@`(D,3)(y)(tn)=d3,`@@`
(D,4)(y)(tn)=d4}:
F:=subs(sus0,y1):
> d2:=diff(f(y(t),t),t):
> d3:=diff(d2,t):
> d4:=diff(d3,t):
>
sus1:={D[1,1](f)(y(t),t)=fyy,D[1,2](f)(y(t),t)=fyt,D[2](f)(y(t),t)=ft,
D[1](f)(y(t),t)=fy,
D[2,2](f)(y(t),t)=ftt,f(y(t),t)=f,D[1,1,1](f)(y(t),t)=fyyy,D[1,1,2](f)
(y(t),t)=fyyt,
D[1,2,2](f)(y(t),t)=fytt,D[2,2,2](f)(y(t),t)=fttt}:
sus2:={diff(y(t),t)=f}:
sus3:={diff(y(t),`$`(t,2))=fy*f+ft}:
sus4:={diff(y(t),`$`(t,3))=(fyy*f+fyt)*f+fy*(fy*f+ft)+fyt*f+ftt}:
> d2:=subs(sus1,d2):d2:=subs(sus2,d2):
d3:=subs(sus1,d3):d3:=subs(sus3,d3):d3:=subs(sus2,d3):d3:=expand(d3):
d4:=subs(sus1,d4):d4:=subs(sus4,d4):d4:=subs(sus3,d4):d4:=subs(sus2,d4
):d4:=expand(d4):
> F;

Efectuando la comparación de los coeficientes del error, de tal forma de anular hasta la potencia
cuarta de h, se obtienen 19 ecuaciones:

Para eliminar el término del error proporcional a h:

e1 := w1 w2 w3 w4 1

Para eliminar el término del error proporcional a h2:


1
e2 := w4 b3 w2 b1 w3 b2
2
1
e3 := w4 a3 w2 a1 w3 a2
2
Para eliminar el término del error proporcional a h3:

w2 b1 2 w4 b3 2 w3 b2 2 1
e4 :=
2 2 2 6
1
e5 := w4 b3 a3 w2 b1 a1 w3 b2 a2
3
1
e6 := w4 b3 b2 w3 b2 b1
6
1
e7 := w4 b3 a2 w3 b2 a1
6

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 27
w4 a3 2 w2 a1 2 w3 a2 2 1
e8 :=
2 2 2 6

Para eliminar el término del error proporcional a h4:

w2 b1 3 w4 b3 3 w3 b2 3 1
ec9 :=
6 6 6 24
2 2 2
w2 b1 a1 w4 b3 a3 w3 b2 a2 1
ec10 :=
2 2 2 8
1 1 1
ec11 := w4 b3 2 b2 b3 b2 2 b2 b1 2 b2 2 b1
w3
2 2 6
1
ec12 := w4 b3 2 a2 w3 b2 2 a1
8
2 2
w2 b1 a1 w4 b3 a3 w3 b2 a2 2 1
ec13 :=
2 2 2 8
5
ec14 := w4 ( b3 b2 a2 b3 b2 a3 ) w3 ( b2 b1 a1 b2 b1 a2 )
24
1
ec15 := w4 b3 a2 a3 w3 b2 a1 a2
8
1
ec16 := w4 b3 b2 b1
24
1
ec17 := w4 b3 b2 a1
24
w4 b3 a2 2 w3 b2 a1 2 1
ec18 :=
2 2 24
3 3 3
w2 a1 w4 a3 w3 a2 1
ec19 :=
6 6 6 24
Se tienen 19 ecuaciones para 10 incógnitas, sin embargo considerando que:

Las ecuaciones 16 y 17, implican que a1 = b1.


Las ecuaciones 6 y 7, con a1 = b1, implican: a2 = b2
Las ecuaciones 2 y 3, con a1 = b1, y a2 = b2 implican: a3 = b3

Las ecuaciones 12 y 15 implican, con a1 = b1, a2 = b2 que a3 = b3. Por lo tanto no se requieren
las ecuaciones 12 y 15.

Las ecuaciones 9, 10, 13 y 19, con a1 = b1, a2 = b2 y a3 = b3 son idénticas. Sólo se requiere
considerar una de ellas.

Las ecuaciones 4, 5 y 8, con a1 = b1, a2 = b2 y a3 = b3 son idénticas. Sólo se requiere considerar


una de ellas.

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos

Las ecuaciones 12 y 11 implican la ecuación 18. Por lo tanto no se requiere emplear la ecuación
11.
Las ecuaciones 12 y 14 implican la ecuación 18. Por lo tanto no se requiere emplear la ecuación
14.

Lo anterior reduce el sistema a 10 ecuaciones en 10 incógnitas (a1, a2, a3, b1, b2, b3, w1, w2,
w3, w4).

Las 10 ecuaciones son las numeradas: 1, 2, 3, 6, 7, 16, 17, 4, 9, 18.

> ecc:={e1,e2,e3,e6,e7,ec16,ec17,e4,ec9,ec18}:
> sol:=[solve(ecc)]:sol[1];

1 1 1 1 1 1 1 1
{ b1 , a2 , w1 , a3 1, b3 1, a1 , w3 , w2 , b2 , w4 }
2 2 6 2 3 3 2 6

Puede comprobarse que la solución satisface las 19 ecuaciones, y que el error queda:

Tn O(h5 )

Entonces el algoritmo de Runge Kutta de cuarto orden resulta:

k1 hf yn , tn
k1 h
k2 hf yn , tn
2 2
k2 h
k3 hf yn , tn
2 2
k4 hf yn k3 , t n h
1
yn 1 yn k1 2k2 2k3 k4
6

Se calculan cuatro valores de la función, para obtener el siguiente punto.

Existen numerosos algoritmos en los que se varía el intervalo de tiempo entre puntos.

23.3. Solución de ecuación no lineal.

El problema consiste en encontrar la raíz de la ecuación no lineal: f ( x) 0

Normalmente la solución de f ( x) 0 , puede ser difícil de encontrar analíticamente, pero como


veremos es sencilla de resolver iterativamente.

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 29
23.3.1. Método de Newton-Raphson.

Para resolver f ( x) 0 , se parte de un valor x0 y se genera una serie de iteraciones xi que se


acerquen a la solución xs , donde f ( xs ) 0 .

En cursos de análisis numérico se responden las preguntas: ¿Cuándo la secuencia xi converge a


la solución correcta? ¿Cuán rápido se converge? ¿La convergencia depende del intento inicial
x0 ? ¿Cuándo detener las iteraciones?.

El método de Newton-Raphson consiste en reemplazar, mediante la expansión de Taylor, la


función por su versión lineal, en torno a la solución:
df
f ( x) f ( xs ) ( xs )( x xs )
dx

Para un punto cualquiera se obtiene:

df
f ( xk 1 ) f ( xk ) ( xk )( xk 1 xk )
dx

Efectuando: f ( xk 1 ) 0 , se obtiene la fórmula de la iteración de Newton-Raphson, despejando


xk 1 :

f ( xk )
xk 1 xk
df
( xk )
dx

Podemos interpretar la fórmula de la iteración, planteando la relación anterior en x0 , y


calculando x1 . Situación que se ilustra en la Figura 23.5.
f(x)
f(x0)
f(x1)

xs 0

x2 x1 x0

Figura 23.5. Iteración Newton-Raphson.

Resulta, de la interpretación gráfica de la derivada en x0 :

Profesor Leopoldo Silva Bijit 26-05-2008


30 Estructuras de Datos y Algoritmos

df f ( x0 )
tg ( 0 ) ( x0 )
dx x0 x1

Despejando x1 , se obtiene el primer valor de aproximación del método de Newton-Raphson:


1
df
x1 x0 ( x0 ) f ( x0 )
dx

Nótese que f ( x1 ) no es cero, lo cual implica que x1 es una aproximación de xs . También debe
notarse que para calcular la siguiente aproximación deben calcularse la función y la derivada en
el punto anterior.

El proceso debe repetirse hasta que: xk 1 xk tolerancia

Donde el valor de tolerancia debe ser un valor lo suficientemente pequeño, para que la solución
se considere aceptable. Con números reales de precisión simple (float en C), un valor razonable
de tolerancia es 10-6, que es el valor del número real más pequeño representable, en el formato
interno normalizado IEEE754.

Si el valor inicial es adecuado conviene limitar el número máximo de iteraciones, de este modo
si no existe convergencia se asegura que el algoritmo termine.

También puede verificarse que la ordenada en los puntos sucesivos esté dentro de cierto rango:

f ( xk 1 ) tolerancia

Para evitar oscilaciones o ciclos no convergentes se limita el número de iteraciones. Pueden


producirse en un caso como el que se ilustra en la Figura 23.6.

Figura 23.6. Oscilación.

Se emplea un intervalo [x1..x2] en el cual se busca la raíz, para evitar la no convergencia debido
a máximos o mínimos dentro del intervalo. Si en una iteración se encuentra un punto con
derivada casi horizontal en el intervalo, el valor para el nuevo x se aleja de la raíz, como se
ilustra en la Figura 23.7. Para prevenir esta situación se verifica que la nueva aproximación de la
raíz permanezca dentro del intervalo.

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 31

Figura 23.7. Divergencia.

Se detienen las iteraciones si los dos últimos valores obtenidos difieren en determinada
tolerancia.

La siguiente función intenta encontrar una raíz en el intervalo [x1, x2].

El primer argumento es un puntero a función con un argumento flotante y que retorna un


flotante. Cuando se invoca a NewtonRaphson el argumento actual es el nombre de la función
que calcula el cuociente de la función con su derivada evaluada en el x actual.

#define nmax 20 //Máximo número de Iteraciones.

float NewtonRaphson(float (*pfuncion)(float), float x1, float x2, float tolerancia)


{
int k;
float dx, x;
x=0.5*(x1+x2); //Intento inicial.
for (k=1; k<=nmax; k++)
{
dx=(*pfuncion)(x); //se invoca a la función cuyo nombre se pasa como primer argumento.
x -= dx; //nuevo valor

if ((x<x1) || (x>x2))
{printf("Salta fuera del intervalo");
exit(1);
}
if (fabs(dx) < tolerancia) return (x); //Converge.
}
printf("No converge en %d iteraciones.\n", nmax);
exit(1);
return (0.0); //Para evitar warning.
}

La siguiente función, para un polinomio, define la derivada y retorna el cuociente entre la


función y su derivada. Nótese que la función recibe un argumento flotante y retorna un flotante.
De este modo, su nombre fin2 es un puntero constante compatible con el primer argumento de
NewtonRaphson.

Profesor Leopoldo Silva Bijit 26-05-2008


32 Estructuras de Datos y Algoritmos
float fun2(float x)
{ float funcion, derivada;
funcion=.2*pow(x,4)-2*pow(x,3)+x+7; //función de x, evaluada en x;
derivada=.8*pow(x,3)-6*pow(x,2)+1; //derivada de f evaluada en x;
return (funcion/derivada);
}

La función: f ( x) 0.2 x 4 2 x 3 x 7 , tiene dos raíces reales, como se ilustra en la Figura


23.8.
La solución con fsolve de Maple, entrega: 1.742869875, 9.913192964

Figura 23.8.

Empleando la función NewtonRaphson, pueden obtenerse las raíces mediante:

printf("Valor de la raíz = %f\n", NewtonRaphson(fun2,1,2,1e-6 ));


printf("Valor de la raíz = %f\n", NewtonRaphson(fun2,9,11,1e-6 ));

23.3.2. Generalización para sistemas de ecuaciones no lineales.

Para un sistema de ecuaciones no lineales, se emplea la expansión de Taylor para varias


variables.

La expansión es una linealización en torno a la solución:

F ( x) F ( xs ) J ( xs )( x xs )

Las cantidades F ( x) y ( x xs ) se expresan como vectores, y J ( xs ) como una matriz,


denominada Jacobiano.

Para un punto cualquiera, con aproximación de primer orden, se tiene:

F ( xk 1 ) F ( xk ) J ( xk )( xk 1 xk )

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 33
Para entender la relación anterior se ilustra la forma que ella toma para dos funciones de dos
variables x1 y x2, se tiene:
F1 ( x1k , x 2 k ) F1 ( x1k , x 2 k )
F1 ( x1k 1 , x 2 k 1 ) F1 ( x1k , x 2 k ) x1 x2 x1k 1
x1k
F2 ( x1k 1 , x 2 k 1 ) F2 ( x1k , x 2 k ) F2 ( x1k , x 2 k ) F2 ( x1k , x 2 k ) x2k 1
x2k
x1 x2

Una explicación del cambio de la función de dos variables, puede efectuarse considerando el
plano tangente a la superficie, en el punto (x10, x20) que pasa también por el punto (x11, x21).
Donde el punto 0 es el inicial, y el punto 1, se obtiene pasando un plano tangente a la superficie
en el punto 0.
x10
F1x1

x11 F1x2
x1
x21 x2

x20

Figura 23.9. Interpretación del Jacobiano de dos variables.

Aplicando la interpretación geométrica de las derivadas parciales, se tienen:

F1 ( x10 , x 20 ) F1x1
tg ( x1 )
x1 x10 x11
F1 ( x10 , x 20 ) F1x 2
tg ( x2 )
x2 x 20 x 21
El cambio total de la función, resulta:

F1 ( x10 , x20 ) F1 ( x10 , x20 )


F1x1 F1x 2 ( x10 x11 ) ( x 20 x21 )
x1 x2

Aplicando el método de Newton-Raphson, que consiste en asumir que el plano tangente pasa
por el punto que es una aproximación a la solución. Esto equivale a efectuar:

F1 ( x1k 1 , x 2k 1 )
0
F2 ( x1k 1 , x 2k 1 )

Profesor Leopoldo Silva Bijit 26-05-2008


34 Estructuras de Datos y Algoritmos
Entonces la fórmula de iteración, resulta:

F1 ( x1k , x 2 k ) F1 ( x1k , x 2 k )
x1 x2 x1k 1
x1k F1 ( x1k , x 2 k )
F2 ( x1k , x 2 k ) F2 ( x1k , x 2 k ) x 2k 1
x 2k F2 ( x1k , x 2 k )
x1 x2

Finalmente, despejando el nuevo punto:


1
F1 ( x1k , x 2k ) F1 ( x1k , x 2k )
x1k 1 x1k x1 x2 F1 ( x1k , x 2k )
x 2k 1 x 2k F2 ( x1k , x 2k ) F2 ( x1k , x 2k ) F2 ( x1k , x 2k )
x1 x2

La que expresada en términos de vectores y la matriz inversa del Jacobiano, resulta en general,
para n variables:

xk 1 xk J ( xk ) 1 F ( xk )

Una mejor visualización de la suma de los incrementos, se logra observando los triángulos
semejantes en la Figura 23.10.
Por el punto inicial (2, 2, 10) se pasa el plano z=2x+3y que también pasa por el punto (0, 0, 0).
Se han dibujado además los planos de z constante, z=4 y z=6.

z z z z
2, 3 x 4, y 6
x y x y

Figura 23.10. Variación total de función de dos variables.

Volviendo al caso de dos variables, considerando el álgebra de matrices, se tiene:

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 35
1
a b x 1 by dx
c d y ad bc cx ay

Entonces las fórmulas de iteración de Newton-Raphson para un sistema de ecuaciones no


lineales de dos variables, resultan:

F1 (k ) F2 (k )
( F2 (k ) F1 (k ))
x1k x1k x2 x2
1
F1 (k ) F2 (k ) F1 (k ) F2 (k )
x1 x2 x2 x1

F2 (k ) F1 (k )
( F1 (k ) F2 (k ))
x 2k x 2k x2 x1
1
F1 (k ) F2 (k ) F1 (k ) F2 (k )
x1 x2 x2 x1

En caso de mayores órdenes debe invertirse el Jacobiano, o alternativamente resolverse el


sistema lineal de ecuaciones, para las incógnitas xk 1 :

J ( xk )( xk 1 xk ) F ( xk )

Donde el vector de las funciones y el Jacobiano deben evaluarse para cada xk

Referencias.

Numerical Recipes In C: The Art of Scientific Computing. Cambridge University Press. 1992.

Profesor Leopoldo Silva Bijit 26-05-2008


36 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 23 ............................................................................................................................................1
ALGORITMOS NUMÉRICOS.................................................................................................................1
23.1. SOLUCIÓN DE SISTEMA SIMULTÁNEO DE ECUACIONES LINEALES. .....................................................1
23.1.1. Descomposición LU. ................................................................................................................1
23.1.2. Métodos iterativos. ...................................................................................................................7
23.2. SOLUCIÓN NUMÉRICA DE SISTEMAS DE ECUACIONES DIFERENCIALES. ...........................................10
23.2.1. Formulación de ecuaciones de estado. ..................................................................................11
23.2.2. Método de Euler. ....................................................................................................................11
23.2.3. Algoritmo de Euler. ................................................................................................................13
23.2.4. Algoritmo trapezoidal. ...........................................................................................................14
23.2.5. Algoritmo de Simpson. ...........................................................................................................15
23.4.6. Métodos multietapas. .............................................................................................................17
23.4.6.1. Método de Heun. ............................................................................................................................. 20
23.4.6.2. Método del punto medio. ................................................................................................................ 20
23.4.6.3. Método de Ralston. ......................................................................................................................... 20
23.4.7. Métodos de Runge-Kutta de cuarto orden. ............................................................................21
23.3. SOLUCIÓN DE ECUACIÓN NO LINEAL. ..............................................................................................28
23.3.1. Método de Newton-Raphson. .................................................................................................29
23.3.2. Generalización para sistemas de ecuaciones no lineales. .....................................................32
REFERENCIAS. .........................................................................................................................................35
ÍNDICE GENERAL. ....................................................................................................................................36
ÍNDICE DE FIGURAS. ................................................................................................................................37

Profesor Leopoldo Silva Bijit 26-05-2008


Algoritmos numéricos 37

Índice de figuras.

FIGURA 23.1. MÉTODO DE EULER. .............................................................................................................. 14


FIGURA 23.2. MÉTODO TRAPEZOIDAL. ........................................................................................................ 14
FIGURA 23.3. MÉTODO DE SIMPSON. .......................................................................................................... 15
FIGURA 23.4. MÉTODO DEL PUNTO MEDIO. ................................................................................................. 20
FIGURA 23.5. ITERACIÓN NEWTON-RAPHSON. ........................................................................................... 29
FIGURA 23.6. OSCILACIÓN. ......................................................................................................................... 30
FIGURA 23.7. DIVERGENCIA....................................................................................................................... 31
FIGURA 23.8. .............................................................................................................................................. 32
FIGURA 23.9. INTERPRETACIÓN DEL JACOBIANO DE DOS VARIABLES. ........................................................ 33
FIGURA 23.10. VARIACIÓN TOTAL DE FUNCIÓN DE DOS VARIABLES. .......................................................... 34

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 24

Transformada rápida de Fourier.

Es considerado un algoritmo clásico, que permite obtener a partir de una serie de valores
temporales las componentes espectrales en frecuencia, y viceversa con un costo O(n log n).

Como veremos es un caso particular del problema más general de calcular polinomios.

Existen dos formas de cálculo: una es evaluar un polinomio representado por sus coeficientes.
La otra es representar el polinomio por una serie de puntos.

24.1. Representación por n coeficientes.

Sea un polinomio de n coeficientes:


n 1
A( x) ajx j
j 0

Donde los coeficientes son los n valores de aj.

Si se evalúan las potencias de x a través de multiplicaciones se requieren (n-1) + (n-2) +..+1


multiplicaciones, originando un algoritmo de complejidad O(n2).

La evaluación de polinomios suele efectuarse empleando la regla de Horner:

A( xi ) a0 xi (a1 xi (a2 .... xi (an 2 xi (an 1 ))....))

Que es de complejidad O(n), ya que requiere (n-1) multiplicaciones. La siguiente rutina ilustra
el procedimiento de cálculo.

double Horner(int a[ ], int n, double x)


{ double A=a[n-1];
for( i=n-2; i>=0; i--) A=a[i] +x*A;
return(A);
}

Las operaciones de suma y resta de polinomios expresados por sus coeficientes son de
complejidad O(n), ya que basta sumar o restar los coeficientes.
Sin embargo el producto de polinomios es de complejidad O(n2).

Si A y B son de n coeficientes, el producto C(x) = A(x)B(x) tendrá (2n-1) coeficientes:

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos

2n 2
C ( x) cjx j
j 0

c0 = a0b0
c1 = a0b1 + a1b0
c2 = a0b2 + a1b1 + a2b0

j
cj ak b j k
k 0
Operación que se denomina convolución de los coeficientes.

La evaluación de los coeficientes de C es O(n), y debe repetirse n veces, lo cual implica O(n2),
para el costo de la multiplicación de polinomios.

24.2. Representación por n valores.

Se tienen los valores o muestras: yk A( xk ) con k = 0..n-1


y1 yn-1
y0
y2

x
x0 x1 x2 xn-1

Figura 24.1 Representación por muestras.

Si se conocen los puntos: (xk, yk ), se tienen n ecuaciones con n incógnitas (los coeficientes).

y0 a0 x00 a1 x01 a2 x02 .... an 1 x0n 1

y1 a0 x10 a1 x11 a2 x12 .... an 1 x1n 1

.....
yn 1 a0 xn0 1 a1 x1n 1 a2 xn2 1 .... an 1 xnn 11

Pueden existir diferentes representaciones por puntos, ya que sólo se requiere que los n puntos
xk sean diferentes. Como se verá la elección adecuada de los n puntos permitirá acelerar el
cálculo a O(n log n).

Esta representación puede obtenerse a partir de la representación por coeficientes con un costo
O(n2), ya que debe aplicarse n veces el algoritmo de Horner.

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 3
También es posible pasar de la representación por puntos a la de coeficientes, para esto es
preciso resolver el sistema de ecuaciones planteado antes. Este problema se conoce como
interpolación.

1 x0 x02 ... x0n 1


a0 y0
1 x1 x12 ... x1n 1
a1 y1

1 xn 1 xn2 1... xnn 11 an 1 yn 1

La matriz tiene inversa si el determinante no es cero. El determinante de la matriz anterior, que


se denomina de Vandermonde, puede calcularse según:

det ( xk x j ) ( x1 x0 )( x2 x1 )...( xn 1 xn 2 )
0 j k n 1

El costo de evaluar la matriz inversa es O(n2) y además se debe realizar n multiplicaciones del
vector de los yk por los renglones de la matriz inversa, con lo cual resulta O(n3).

Sin embargo los coeficientes pueden calcularse con costo O(n2) empleando la fórmula de
interpolación de polinomios de Lagrange:

( x x1 )( x x2 )...( x xn 1 ) ( x x0 )( x x2 )...( x xn 1 ) ( x x0 )( x x1 )...( x xn 2 )


A( x) y0 y1 .... yn 1
( x0 x1 )( x0 x2 )...( x0 xn 1 ) ( x1 x0 )( x1 x2 )...( x1 xn 1 ) ( xn 1 x0 )( xn 1 x1 )...( xn 1 xn 2 )

Que es un polinomio de n coeficientes que pasa por los n puntos (xk, yk).
O bien tal que A(xk) = yk para k=0..n-1, lo cual puede comprobarse en la fórmula anterior.

La ventaja de representación por puntos es que el cálculo del producto (también la suma y resta)
de polinomios es ahora O(n). Pero en caso de multiplicación, los polinomios deben
representarse con 2n puntos, ya que C, debe ser de 2n puntos.

Si se tienen los polinomios A y B, puede determinarse su producto C, según:


A( x) ( x0 , y A0 ),...( xn 1 , y An 1 ),...( x2 n 1 , y A2 n 1 )
B( x) ( x0 , yB 0 ),...( xn 1 , yBn 1 ),...( x2 n 1 , yB 2 n 1 )
C ( x) ( x0 , y A0 yB 0 ),...( xn 1 , y An 1 yBn 1 ),...( x2 n 1 , y A2 n 1 yB 2 n 1 )

24.3. Transformada discreta de Fourier.

Se tienen las transformadas de Fourier, directa e inversa, en el dominio de f y t, para la señal s(t)
y su densidad espectral S(f), definidas según:

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

j 2 ft
( s (t )) S( f ) s (t )e dt

1
( S ( f )) s (t ) S ( f )e j 2 ft df

Y las transformadas discretas de Fourier:


n 1
j 2 ik / n
S (k ) s (i )e
i 0
n 1
1
s(k ) S (i )e j 2 ik / n

n i 0
Con k=0..n-1

Si se define: wn=e-j2 /n
como la n-ava raíz compleja de la unidad, se tiene que:

e-j2 k/n
= (wn)k

El valor principal suele definirse según: ej2 /n, en el primer cuadrante.

Reemplazando S(k) por Sk, s(k) por sk, y (wn)k por x se tienen dos polinomios.
n 1 1 x0 x02 ... x0n 1
s0 S0
Sk si x i
i 0 1 x1 x12 ... x1n 1
s1 S1
1n 1
i
sk Si x
ni 0
1 xn 1 xn2 1... xnn 11 sn 1 Sn 1

Los polinomios se evalúan en las raíces complejas de la unidad. Si se reemplazan las potencias
de x se obtiene la relación que permite obtener el espectro a partir de las muestras temporales:

1 wn0*1 wn0*2 ... wn0*( n 1) s0 S0


1 w1*1
n w1*2
n ... w1*(
n
n 1)
s1 S1

1 wn( n 1)*1
wn( n 1)*2
... wn( n 1)*( n 1)
sn 1 Sn 1

Las figuras siguientes muestran los polinomios representados por puntos.

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 5
s1 S1
sn-1 Sn-1
s0 S0
s2 S2

t f
t0 t1 t2 tn-1 f0 f1 f2 fn-1

T=intervalo de muestreo fS=1/n T=separación en frecuencia


Figura 24.2 Representación de polinomios por puntos.

El elemento (r, c) de la matriz anterior es (wn)r*c con r y c variando entre 0 y (n-1).


Puede comprobarse que el elemento (r, c) de la matriz inversa es: (1/n)*(wn)-r*c

1 wn 0*1 wn 0*2 ... wn 0*( n 1)


S0 s0
1 1 wn 1*1 wn 1*2 ... wn 1*( n 1)
S1 s1
n
1 wn ( n 1)*1
wn ( n 1)*2
... wn ( n 1)*( n 1)
Sn 1 sn 1

Que permite obtener las muestras temporales a partir del espectro. Notamos que el problema es
similar al anterior, pero cambiando el signo de las n-avas potencias complejas de la unidad, y un
escalamiento dividiendo por n.

Conocidas las muestras temporales, se requieren n multiplicaciones para obtener una muestra
del espectro, y como se requieren n muestras en frecuencia, se obtiene un algoritmo de costo
cuadrático. Además deben calcularse las potencias complejas de las n-avas raíces de la unidad.

24.4. Desarrollo del algoritmo de transformada rápida de Fourier.

El algoritmo FFT permite calcular la DFT en O( n log n).


El polinomio S(x) puede separarse en dos, uno con las potencias pares de x; el otro, con las
potencias impares:
n 1
Sk S ( x) si x i
i 0

S p ( x) s0 s2 x ... sn 2 x ( n / 2) 1

Si ( x ) s1 s3 x ... sn 1 x ( n / 2) 1

Sk S ( x) S p ( x 2 ) xSi ( x 2 )

Los polinomios son de n/2 puntos cada uno. Esto considerando n par y más específicamente una
potencia de dos.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
El cálculo de los Sk para n puntos puede descomponerse en dos cálculos de polinomios pero con
la mitad de puntos cada uno. A su vez, cada uno de esos polinomios, puede ser calculado en
términos de dos polinomios con la cuarta parte de los puntos, y así sucesivamente.

Más adelante veremos cómo los Sk pueden calcularse recursivamente, para obtener esa relación
es preciso conocer algunas propiedades de las raíces complejas de la unidad.

24.5. n-avas raíces complejas de la unidad.

Existen n raíces n-avas complejas de la unidad: wn0, wn1, wn2,… wnn-1


Con : (wn)k = e-j2 k/n

Para n = 2, se tienen
w20=e-j 0 = cos(0) + j sen(0) = +1
w21=e-j 1 = cos(- ) + j sen(- ) = -1

w21 w20

Figura 24.3. Raíces complejas cuadradas de la unidad

Para n = 4, se tienen:
(w4) = e-j /2 = -j
w40 = +1 w43
w41 = -j
w42 w40
w42 = -1
w43= +j
Debe notarse que w44 = w40
w41

Figura 24.4. Raíces complejas cuartas de la unidad

Algunas propiedades:

Para n>=0, k>=0 y d>0:

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 7
dk j 2 / dn dk j2 /n k
wdn (e ) (e ) wnk
wnn / 2 (e j2 /n n/2
) e j
w2 1
( wnk n/2 2
) wn2 k n
wn2 k wnn ( wnk ) 2
j 2 / 2n j 2 / n 1/ 2
w2 n e (e ) wn
n 1 ( wnk ) n 1 ( wnn ) k 1 (1) k 1
( wnk )i 0
i 0 wnk 1 wnk 1 wnk 1
La última relación es una serie geométrica, se requiere n>=1, y que k sea positivo no divisible
por n, para que el denominador no sea cero.

Si se efectúa el producto de la matriz de Vandermonde por su inversa, el elemento (r, c) del


producto, puede expresarse según:

n 1 n 1
( wn kr / n)wnkc ( wn k ( c r)
/ n) la cual toma valor 1 para r=c, y cero en caso contrario. Lo
k 0 k 0
cual origina una matriz unitaria, comprobándose que la expresión para la inversa es correcta. Se
requiere que (c-r) no sea divisible por n, esto se cumple ya que fuera de la diagonal:

-(n-1) < (r-c) < (n-1).

24.6. Series discretas para dos y cuatro puntos.

Se tiene para n = 2:
S0 = s0 + x0s1
S1 = s0 + x1s1

Evaluando en las raíces cuadradas complejas de la unidad:


S0 = s0 + w20s1 = s0 + (+1)s1
S1 = s0 + w21s1 = s0 + (- 1)s1

Resultando:
S0 = s0 + s1
S1 = s0 - s1

Para n = 4:
Evaluando en las raíces cuartas complejas de la unidad, notando que todos los valores no son
diferentes, debido a las propiedades de las raíces cuartas complejas de la unidad, se obtienen:
S0 w40 s0 w40 s1 w40 s2 w40 s3
S1 w40 s0 w14 s1 w42 s2 w43 s3
S2 w40 s0 w42 s1 w44 s2 w46 s3
S3 w40 s0 w43 s1 w46 s2 w49 s3

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
Reemplazando por los valores numéricos:
S0 s0 s1 s2 s3
S1 s0 js1 s2 js3
S2 s0 s1 1s2 s3
S3 s0 js1 s2 js3

Finalmente, se advierte que para cuatro puntos, están incorporados los polinomios asociados a
dos puntos. Donde se ha definido: (S0p, S1p) y (S0i, S1i) como series de dos puntos.

S0 ( s0 s2 ) ( s1 s3 ) S0 p w40 S0i
S2 ( s0 s2 ) ( s1 s3 ) S0 p w40 S0i
S1 ( s0 s2 ) j ( s1 s3 ) S1 p w14 S1i
S3 ( s0 s2 ) j ( s1 s3 ) S1 p w14 S1i

24.7. Relaciones de recurrencia.

De la descomposición en dos polinomios y reemplazando los valores de evaluación con las n-


avas raíces complejas de la unidad, se desprenden dos relaciones de recurrencia para calcular los
Sk.
Una para los valores: S0, S1 , S2,…. , Sn/2 -1, y otra para: Sn/2, Sn/2 +1 , Sn/2 +2,…. , Sn-1

Sk S p ( x 2 ) xSi ( x 2 )
Sk S p ( wn2 k ) wnk Si ( wn2 k )
Sk n/2 S p ( wn2 k n ) wnk n/2
Si ( wn2 k n )

Empleando las propiedades de las raíces complejas, se obtienen:

Sk S p ( wn2 k ) wnk Si ( wn2 k ) S p ( wnk / 2 ) wnk Si ( wnk / 2 ) Skp wnk S ki


Sk n/2 S p ( wn2 k n ) wnk n/2
Si ( wn2 k n ) S p ( wn2 k ) wnk Si (wn2 k ) S p ( wnk / 2 ) wnk Si (wnk / 2 ) S kp wnk S ki

Donde se han definido los valores de la parte par e impar como una evaluación de un polinomio
de n/2 puntos.
S kp S p ( wnk / 2 )
S ki Si ( wnk / 2 )
Finalmente se obtienen:

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 9

Sk S kp wnk S ki Con k=0, 1, 2,… (n/2)- 1

Sk n/2 S kp wnk S ki

Asumiendo operaciones con números complejos, los n valores se calculan de a pares, en


términos de los valores asociados a dos series de n/2 puntos cada una:

for (k=0; k<= (n/2)-1; k++)


{ Sk = Skp + wnk Ski ;
Sk+n/2 = Skp - wnk Ski ;
}

Es usual calcular una sola vez, la expresión común dentro del for.

for (k=0; k<= (n/2)-1; k++)


{ t = wnk Ski ;
Sk = Skp + t ;
Sk+n/2 = Skp - t ;
}

El costo de calcular una potencia puede extraerse, a través de una multiplicación dentro del lazo:
wn = e-j2 /n ;
w = 1;
for (k=0; k<= (n/2)-1; k++)
{ t = w*Ski ;
Sk = Skp + t ; //Operación denominada mariposa. Butterfly.
Sk+n/2 = Skp - t ;
w * = wn ;
}

La codificación puede modificarse para tratar las variables complejas, por sus partes reales e
imaginarias.

Para un polinomio descrito por 8 puntos temporales. El cálculo puede visualizarse mediante la
descomposición en dos polinomios de 4 puntos, con los puntos pares en uno, y los impares en
otro. Un polinomio de 4 puntos, se puede calcular mediante la composición de dos polinomios
de dos puntos, donde nuevamente se separan los ubicados en posiciones pares e impares. No es
necesario descomponer polinomios de dos puntos, ya que su evaluación es sencilla. El siguiente
árbol ilustra la separación entre partes pares e impares.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

s0 s1 s2 s3 s4 s5 s6 s7

s0 s2 s4 s6 s1 s3 s5 s7

s0 s4 s2 s6 s1 s5 s3 s7

Figura 24.5. Descomposición en polinomios pares e impares.

Si cada vez se suprime el bit menos significativo que representa al índice en binario, se tendrá
que la división se efectúa entre pares e impares. Por ejemplo el grupo (0, 2, 4, 6) se transforma
al suprimir el bit menos significativo en (0, 1, 2, 3).

Esta visualización también posibilita un algoritmo iterativo en lugar de recursivo. En el nivel de


las hojas se aplica la operación mariposa a todos los grupos de a dos (pero en el orden que
figuran en las hojas), luego se evalúan polinomios de 4 puntos cada uno, considerando ahora las
raíces cuartas complejas de la unidad, y así sucesivamente ascendiendo de nivel.

24.8. Reordenamiento de los puntos.

Si se observa la siguiente tabla, donde se han colocado los valores de las muestras temporales en
un arreglo, junto a sus índices en binario y el arreglo modificado. Puede notarse que los índices
en binario del arreglo modificado son las imágenes especulares de los índices originales. Puede
también notarse que la última columna se puede visualizar como un contador binario inverso, es
decir los dígitos que primero cambian son los más significativos. En este caso sólo es preciso
intercambiar s1 con s4, y s3 con s6.

Arreglo original Contador binario Arreglo modificado Contador inverso


s0 000 s0 000
s1 001 s4 100
s2 010 s2 010
s3 011 s6 110
s4 100 s1 001
s5 101 s5 101
s6 110 s3 011
s7 111 s7 111

Figura 24.6. Contador binario directo e inverso.

Para 16 puntos, colocando los valores de los contadores binarios directo e inverso, en decimal,
puede notarse que bastaría generar las dos secuencias de valores siguientes:

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 11
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
j 0 8 4 12 2 10 6 14 1 9 5 13 3 11 7 15
ig x ig x ig x x x x ig

Figura 24.7. Contador binario inverso con cuentas en decimal.

Y sólo intercambiar cuando j> i. Esto evita los cambios en los casos identificados en el tercer
renglón con ig (por igualdad), y los cambios marcados con x (esto volvería a su lugar a los
elementos del arreglo). Además no es preciso revisar los casos con i= 0 e i=15.
Si el número de muestras es una potencia de dos, se tiene que el primer valor de j, de la
secuencia que debe generarse, será: j=n/2 o la expresión equivalente: j=n>>1.

La siguiente función intercambia los elementos de un arreglo x, generando las secuencias de


valores de i y j.

void bitreverse(int *x, int n)


{ int nmedios=n>>1, i, j, k, tx;
//i contador binario
for (i=1, j=nmedios; i<n-2; i++)
{
//printf("i=%d j=%d\n", i , j);
if (i < j) {
tx = x[i];
x[i] = x[j];
x[j] = tx;
}
//j contador binario inverso. Incrementa en uno la posición más significativa.
//y desplaza a la derecha las reservas.
for(k=nmedios; k <= j; k >>= 1) {j -= k;}
j+=k;
}
}

Ejemplos del contador binario inverso, con operaciones en decimal:


Si k y j tienen valor 8 ( 1000 en binario) se efectúa la resta (queda j=0) y la condición de reinicio
deja k=4 (0100), lo cual termina la iteración. Después del for se setea en uno el bit que marca k,
y deja j=4 (0100).

“Se cambian todos los unos más significativos por ceros, hasta encontrar el primer cero, el
cual se setea a uno”.

Si k=8 y j = 12 ( 1100 en binario) se efectúa la resta (queda j=4) y la condición de reinicio deja
k=4 (0100), se vuelve a iterar, quedando j=0 y k=2. Lo cual termina la iteración. Después del for
se setea en uno el bit que marca k, y deja j=4 (0010).

El siguiente ejemplo, busca el primer cero y lo cambia por uno, poniendo en cero el más
significativo.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
Si k=8 y j=10 (1010), la primera iteración deja j=2 (0010) y k = 4 (0100), terminando la
iteración. El nuevo valor de j será 6 (0110)

Otra alternativa de diseño es generando la imagen especular de i para formar j.

“Se detectan los unos de i, y se los copia, mediante la variable m, en la posición especular en
j”.

void contadorinverso(int *x, int n)


{ int nmedios=n>>1, i, j, m ,k ,tx;

for (i=1,j=nmedios; i<n-2; ) //el penúltimo es par


{
printf("i=%d j=%d\n",i,j);
if (i < j) {
tx = x[i];
x[i] = x[j];
x[j] = tx;
}
for(i++, k=nmedios, m=1, j=0; k >=1; k >>= 1, m<<=1)
{
if(i&k) j|=m;
}
}
}

24.9. Operación mariposa.

Para deducir el algoritmo iterativo, veremos algunos casos específicos.


Para una serie de dos puntos, se muestra la serie inicial de puntos temporales en un arreglo.
Luego de aplicar la operación mariposa, y finalmente la reinterpretación de los puntos en
frecuencia.

n=2
for (k=0; k<= (n/2)-1; k++) s0 s1
{ Sk = Skp + wnk Ski ;
Sk+n/2 = Skp - wnk Ski ;
} s0+s1 s0-s1

S0 = S0p + w20 S0i ;


S1 = S0p - w20 S0i ; S0 S1
Con w20 = 1

La composición de la serie de 4 puntos a partir de las series de dos puntos, se obtiene con:

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 13
n=4
for (k=0; k<= (n/2)-1; k++)
{ Sk = Skp + wnk Ski ;
Sk+n/2 = Skp - wnk Ski ;
}

S0 S0 p w40 S0i ( s0 s2 ) ( s1 s3 )
S2 S0 p w40 S0i ( s0 s2 ) ( s1 s3 )
S1 S1 p w14 S1i ( s0 s2 ) j ( s1 s3 )
S3 S1 p w14 S1i ( s0 s2 ) j ( s1 s3 )
Con: w40 = 1, w41 = - j = (w21)1/2 w4 = w2

Para una serie de cuatro puntos temporales, se procede al reordenamiento. A éstos se los trata
como series de dos puntos. Se aplica la operación mariposa, con n=2, a todos los pares:

s0 s2 s1 s3
Luego de la operación en los pares:
(0,1)(2,3) s0+s2 s0 -s2 s1 +s3 s1 -s3
Quedan las series en frecuencia de dos puntos
S0p S1p S0i S1i
Se aplica ahora la operación mariposa con n=4. A los pares (0,2) y (1,3).
Lo cual genera los cuatro puntos en frecuencia.

S0p +S0i S0p-S0i S1p-jS1i S1p+jS1i

Que puede leerse según: S0 S1 S2 S3


La composición de la serie de 8 puntos a partir de las series de cuatro puntos, se obtiene con:
n=8
for (k=0; k<= (n/2)-1; k++)
{ Sk = Skp + wnk Ski ;
Sk+n/2 = Skp - wnk Ski ;
}

Es decir:

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos

S0 S0 p w80 S0i
S4 S0 p w80 S0 i
S1 S1 p w81 S1i
S5 S1 p w81 S1i
S2 S2 p w82 S 2i
S6 S2 p w82 S 2i
S3 S3 p w83 S3i
S7 S3 p w83 S3i
1
Con: w80 =1, w81 = e-j /4
= (1 j ) w8 = w4
2

En el nivel 0: Se forman series de dos puntos, aplicando la operación mariposa con n=2, a los
pares: (0, 1), (0+2*1, 1+2*1), (0+2*2, 1+2*2), (0+2*3, 1+2*3)

En el nivel uno: Se forman series de 4 puntos. La composición de dos series de 2 puntos se logra
aplicando la operación con n=4, a los pares (0,2) (1,3) y también a los pares (0+4, 2+4) (1+4,
3+4) logrando dos series de cuatro puntos.

En el nivel dos: La composición de dos series de 4 puntos se logra aplicando la operación con
n=8. A los pares (0,4) (1,5) (2,6) (3, 7)

Dos series de 4 puntos S0p S1p S2p S3p S0i S1i S2i S3i

Serie de 8 puntos
S0 S1 S2 S3 S4 S5 S6 S7
Veamos ahora el caso general:
Si n es el número de puntos.
En el nivel 0, se tienen n/2 series de 2 puntos.
En el nivel 1, se tienen n/4 series de 4 puntos.
En el nivel 2, se tienen n/8 series de 8 puntos.
En el nivel j, se tienen n/2j+1 series de 2j+1 puntos.
En último nivel, el m-1, la única serie es de n= 2m puntos.

El algoritmo iterativo debe generar las series asociadas al nivel, y los pares de puntos de cada
serie y luego aplicarles la operación mariposa a cada par.

Es decir, a través de la variable j, se repite para cada grupo formado por nn puntos:

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 15
for(j=0; j<n; j+=nn)
for (k=0; k<= (nn/2)-1; k++)
{ Sk+j = Skp + wnk Ski ;
Sk+j+n/2 = Skp - wnk Ski ;
}

Falta aún una última iteración para procesar cada uno de los niveles.

El siguiente segmento genera los pares a los que debe aplicárseles la operación.

void mariposa(int m)
{ int n, k, j, nn, nivel;
n = 1;
for (k=0; k<m; k++) n *= 2; //n=2^m genera número de puntos

nn=1;
for (nivel=0; nivel<m; nivel++)
{ nn=2*nn; //series de nn puntos. Con nn=2^(nivel+1)
for(j=0; j<n; j=j+nn) //Para todos los n elementos en grupos de nn
{
for (k=0; k< nn/2; k++)//Se aplica mariposa a los pares (k+j, k+j+nn/2)
{ printf("(%d,%d)", k+j, k+j+nn/2); }
putchar('\n');
}
}
}

Para m= 4, se tienen 16 puntos, el programa genera los pares:


(0,1) 8 series de dos puntos
(2,3)
(4,5)
(6,7)
(8,9)
(10,11)
(12,13)
(14,15)
(0,2)(1,3) 4 series de 4 puntos
(4,6)(5,7)
(8,10)(9,11)
(12,14)(13,15)
(0,4)(1,5)(2,6)(3,7) 2 series de 8 puntos
(8,12)(9,13)(10,14)(11,15)
(0,8)(1,9)(2,10)(3,11)(4,12)(5,13)(6,14)(7,15) 1 serie de 16 puntos

La siguiente función resume las principales ideas. Sólo falta definir el tratamiento de los
números complejos y de los arreglos.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
void mariposa(double *S, int m)
{ int n, k, j, nn, nivel;
double w, wn, u, t;

for (k=0, n=1; k<m; k++) n *= 2; //n=2^m genera n, el número de puntos a partir de m
nn=1;
wn = w2 ; //Al inicio las series son de dos puntos
for (nivel=0; nivel<m; nivel++)
{ nn=2*nn; //series de nn puntos. Con nn=2^(nivel+1)
for(j=0; j<n; j=j+nn) //Para todos los n elementos en grupos de nn
{
w = 1;
for (k=0; k< nn/2; k++) //Se aplica mariposa a los pares (k+j, k+j+nn/2)
{
t = w*Sk+j+nn/2 ; //Con los valores de series de nn/2 puntos
u = Sk+j;
Sk+j = u + t ; //Se calculan con la operación mariposa, los
Sk+j+nn/2 = u - t ; //valores de las series de nn puntos.
w * = wn ;
}
}
wn = wn ; //Se duplica el número de puntos.
}
}

Al inicio, en el nivel cero, las series son de dos puntos; al subir de nivel debe calcularse la nueva
raíz compleja. Se ilustra, en el primer cuadrante la nueva raíz, que tiene la mitad del ángulo.

wn
w2n
n

n /2

Figura 24.8. Cálculo de la raíz cuadrada.

Para calcular la raíz compleja principal asociada a una serie de 2n puntos a partir de la raíz
compleja principal asociada a una serie de n puntos, pueden efectuarse las siguientes
definiciones:

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 17

wn xn jyn
w2 n x2 n jy2 n

Del diagrama, aplicando la identidad trigonométrica para la tangente del ángulo medio, se
obtiene una relación entre las coordenadas cartesianas de las raíces; la segunda relación se
cumple debido a que el módulo de las raíces es de magnitud unitaria.
sen
tg ( / 2)
1 cos
y2 n yn
x2 n 1 xn
x2 n 2 y2 n 2 1

Despejando, del sistema de ecuaciones, se obtienen las coordenadas cartesianas de w2n en


función de las coordenadas de wn.
1 xn
y2 n
2
1 xn
x2 n
2

El código completo de la función, con: s[i]=x[i] + jy[i], wn=wn1 +jwn2, w=w1 +jw2, además
se definen una serie de variables locales, para intercambios y para extraer constantes dentro de
los lazos. Se eliminan los llamados a función, para disminuir el costo de la creación de frames
en el stack. Se agrega el argumento dir, para obtener la transformada directa e inversa con la
misma función.

void FFT(short int dir, long m, double *x, double *y )


{ int n, nn, nmedios, nivel, i,j,k,i1;
double w1,w2, wn1,wn2,tx,ty,tw,t1,t2;

for (k=0,n=1; k<m; k++) n *= 2; //n=2^m genera número de puntos a partir de m

//void bitreverse(int *x, int n)


nmedios=n>>1;
for (i=1, j=nmedios; i<n-2; i++)
{
//printf("i=%d j=%d\n",i,j);
if (i < j) {
tx = x[i]; ty = y[i];
x[i] = x[j]; y[i] = y[j];
x[j] = tx; y[j] = ty;
}

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
//j contador binario. Incrementa en uno la posición más significativa.
//y desplaza a la derecha las reservas.
for(k=nmedios; k <= j; k >>= 1) {j -= k;}
j+=k;
}

/* Calcula FFT */

nn=1;
//wn = w2 = -1+j0; En el primer nivel son series de dos puntos
wn1 = -1.0;
wn2 = 0.0;

for (nivel=0; nivel<m; nivel++)


{ nn=2*nn; //series de nn puntos. Con nn=2^(nivel+1)
for(j=0; j<n; j=j+nn) //Para todos los n elementos en grupos de nn
{
//w = 1+j0;
w1 = 1.0; w2 = 0.0;
for (k=0; k< nn/2; k++) //Se aplica mariposa a los pares (k+j, k+j+nn/2)
{
i=k+j;
i1=i+nn/2;
//t = w*S[k+j+nn/2];
t1 = w1 * x[i1] - w2 * y[i1];
t2 = w1 * y[i1] + w2 * x[i1];
//u = S[k+j];
//S[k+j+nn/2] = u - t;
x[i1] = x[i] - t1;
y[i1] = y[i] - t2;
//S[k+j] = u + t ; //Operación mariposa.
x[i] += t1;
y[i] += t2;
//w * = wn ;
tw = w1 * wn1 - w2 * wn2;
w2 = w1 * wn2 + w2 * wn1;
w1 = tw;
}
}
//wn = sqrt(wn) ; Al subir de nivel se requiere la raíz cuadrada de la anterior.
wn2 = sqrt((1.0 - wn1) / 2.0);
if (dir == 1) wn2 = -wn2;
wn1 = sqrt((1.0 + wn1) / 2.0);
}
/* Escalamiento para transformada inversa */
if (dir == -1) for (i=0; i<n; i++) { x[i] /= n; y[i] /= n; }
}

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 19
24.10. Interpretación de la FFT.

24.10.1. Corriente continua.

Para una serie de 16 valores constantes en el tiempo se obtiene un espectro de un solo punto en
el origen.

Figura 24.9. Forma de onda y espectro para señal continua en tiempo.

Nótese el valor 16 en el valor máximo del espectro. En algunas definiciones se plantea la


transformada directa escalada en 1/n. En este caso el máximo valor del espectro será 1. Esto
mejora la interpretación como un impulso de Dirac de fuerza uno, asociado a la corriente
continua.

24.10.2 Primera armónica.

Para 16 muestras de la señal temporal: cos(2 n t/16), con n=0..15 se tiene la gráfica de la
señal, en incrementos de t. Su FFT, graficada como magnitudes de los valores complejos,
está asociada dos puntos diferentes de cero; uno en f=1 f, y otro en f=15 f.
Donde f = 1/ t.

Se toman 16 muestras en un período de la señal temporal. La gráfica temporal está en unidades


de t, que es el intervalo de muestreo. Se dice que es la primera armónica.

El espectro está en unidades de f. El punto espectral en 15 f es una frecuencia espejo relativa


a f=8 f, que se denomina frecuencia de Nyquist, para el caso de 16 muestras.

En el espectro, la más alta muestra en frecuencia (positiva o negativa) se denomina frecuencia


de Nyquist. Es la mayor componente de alta frecuencia que puede existir en la señal temporal.
Dicho de otra forma si la señal no contiene frecuencias superiores a la de Nyquist puede ser
reconstruida exactamente a partir de sus muestras.

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
Si la señal que se va a muestrear tiene una frecuencia máxima, para poder recuperar dicha señal,
a partir de sus muestras, debe muestreársela con una frecuencia que sea el doble de la frecuencia
máxima. Es decir dos muestras por período a lo menos. Si la señal varía más rápido que la mitad
de la frecuencia de muestreo no puede recuperársela a partir de las muestras.

Figura 24.10. Forma de onda y espectro para Primera armónica.

24.10.3. Segunda armónica.

Para 16 muestras de la señal temporal: cos(2 n t/8), con n=0..15 se tiene la gráfica de la señal,
en incrementos de t. Su FFT está asociada dos puntos diferentes de cero; uno en f=2 f, y otro
en f=14 f. Donde f = 1/ t. Se dice que es la segunda armónica.
El punto espectral en 14 es una frecuencia espejo relativa a la frecuencia de Nyquist.
Se toman ahora 8 muestras por período.

Figura 24.11. Forma de onda y espectro para Segunda armónica.

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 21
24.10.4. Tercera armónica.

Se dibuja ahora la señal temporal de tercera armónica junto a la primera o fundamental, en


forma continua. A la derecha se ilustran las 16 muestras de la tercera armónica. Se toman 16/3
muestras por período.

Figura 24.12. Forma de onda y muestras para Tercera armónica.

La trasformada rápida muestra el espectro, donde figura la tercera armónica, y su espejo en 13.

Figura 24.13. Espectro para Tercera armónica.

24.10.5. Séptima armónica.

Nótese que las muestras temporales difícilmente permiten visualizar la figura continua. Se
toman 16/7 muestras por período.

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos

Figura 24.14. Forma de onda y muestras para Séptima armónica.

La transformada rápida permite visualizar el contenido armónico de la séptima. Y el espejo en


9.

Figura 24.15. Espectro para Séptima armónica.

24.10.6 Octava armónica.

Al tomar muestras con la frecuencia de Nyquist, se toman dos muestras por período de la octava
armónica, que tiene período 2 t, ya que la fundamental tiene período 16 t.
Frecuencia de la fundamental = 1/16 t = f/16 = ff.
Frecuencia de la octava armónica es = f/2 = 8 ff .
Período de muestreo = t, frecuencia de muestreo =1/ t = f.
Frecuencia de Nyquist = f/2 = 8 ff .

Las muestras de la octava armónica, dos por período, se ilustran junto a una gráfica continua.

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 23

Figura 24.16. Forma de onda y muestras para Octava armónica.

Y el espectro de magnitudes obtenido mediante la FFT:

Figura 24.17. Espectro para Octava armónica.

Para la novena armónica, cuya frecuencia es mayor que la de Nyquist se obtiene un espectro
idéntico al de la séptima armónica. Lo cual no permite reconstruir la señal temporal a través de
su espectro.

24.10.7 Frecuencias no múltiplos enteros de la frecuencia de muestreo.

Los casos anteriores daban un espectro de tonos puros. Sin embargo una frecuencia levemente
diferente hace aparecer varias armónicas. Veamos por ejemplo una señal de 1,2 veces la
frecuencia fundamental (cos(2 n t*1,2/16) )

Se aprecia un valor de componente continua y presencia de segunda tercera y cuarta, en


magnitudes decrecientes. Siendo mayor la presencia de primera armónica.

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos

Figura 24.18. Forma de onda y espectro para no múltiplo de frecuencia de muestreo.

Para una señal con frecuencia 1,5 veces la de primera armónica, aparecen componentes
espectrales en toda la banda (de 0 a 8), siendo más importantes las contribuciones de la primera
y segunda.

Figura 24.19. Espectro para con frecuencia 1,5 veces la de primera armónica.

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 25
La mezcla de dos tonos puros: segunda de amplitud uno, y cuarta de amplitud 0,5.

Figura
24.20. Mezcla de frecuencias múltiples de la de muestreo.

La mezcla de un tercio de la 3,2 armónica con un quinto de la 5,5 armónica:

Figura 24.20. Mezcla de frecuencias no múltiplos de la de muestreo.

Se aprecia la dificultad para interpretar el contenido armónico en el espectro. Se detecta con


facilidad la importancia de la tercera, pero no la de la quinta o sexta.

24.11. Derivación de la FFT a partir de la transformada de Fourier.

j 2 ft
( s (t )) S( f ) s (t )e dt

1
( S ( f )) s (t ) S ( f )e j 2 ft df

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos
Si se aproxima la primera integral, asumiendo que s(t) es cero fuera del período fundamental
(i=0..n-1), por una sumatoria, considerando el tiempo como una variable discreta t=i t.
s1 sn-1
s0
s2

t
t0=0 t1=1 t t2=2 t tn-1

t=intervalo de muestreo

n t=Período fundamental

Figura 24.21. Aproximación de la integral temporal.

i n 1
j 2 fi t
S( f ) s (i t )e t
i 0

s (k t ) S ( f )e j 2 fk t
df
Para aproximar la segunda integral, se discretiza la variable f, en los puntos i f. Esto también
considera cero el espectro fuera del intervalo n f. Con i=0..n-1 y k=0..n-1.
i n 1
j 2 ki f t
S (k f ) s(i t )e t
i 0
i n 1
s(k t ) S (i f )e j 2 ki f t
f
i 0

Dividiendo la primera por t, y arreglando la segunda sumatoria, se obtiene:


i n 1
S (k f ) j 2 ki f t
s(i t )e
t i 0
i n 1
S (i f ) j 2 ki f t
s (k t ) f t e
i 0 t

Definiendo, con k=0..n-1:

S (k f )
S (k )
t
s(k ) s(k t )
1
f Silva Bijit
Profesor Leopoldo 26-05-2008
n t
Transformada rápida de Fourier 27

Se obtienen los polinomios:

i n 1
j 2 ki / n
S (k ) s(i )e
i 0

1i n 1
s(k ) S ( k )e j 2 ki / n

n i0

Referencias.

Thomas H. Cormen, Charles E. Leiserson, y Ronald L. Rivest, "Introduction to Algorithms",


Second Edition, McGraw-Hill, ISBN 0-07-013151-1, 2001.

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos
Índice general.

CAPÍTULO 24 ............................................................................................................................................1
TRANSFORMADA RÁPIDA DE FOURIER. .........................................................................................1
24.1. REPRESENTACIÓN POR N COEFICIENTES. ...........................................................................................1
24.2. REPRESENTACIÓN POR N VALORES. ..................................................................................................2
24.3. TRANSFORMADA DISCRETA DE FOURIER. .........................................................................................3
24.4. DESARROLLO DEL ALGORITMO DE TRANSFORMADA RÁPIDA DE FOURIER. .......................................5
24.5. N-AVAS RAÍCES COMPLEJAS DE LA UNIDAD. .....................................................................................6
24.6. SERIES DISCRETAS PARA DOS Y CUATRO PUNTOS. ............................................................................7
24.7. RELACIONES DE RECURRENCIA. .......................................................................................................8
24.8. REORDENAMIENTO DE LOS PUNTOS. ...............................................................................................10
24.9. OPERACIÓN MARIPOSA. ..................................................................................................................12
24.10. INTERPRETACIÓN DE LA FFT. .......................................................................................................19
24.10.1. Corriente continua. ..............................................................................................................19
24.10.2 Primera armónica. ................................................................................................................19
24.10.3. Segunda armónica................................................................................................................20
24.10.4. Tercera armónica. ................................................................................................................21
24.10.5. Séptima armónica. ...............................................................................................................21
24.10.6 Octava armónica. ..................................................................................................................22
24.10.7 Frecuencias no múltiplos enteros de la frecuencia de muestreo. ..........................................23
24.11. DERIVACIÓN DE LA FFT A PARTIR DE LA TRANSFORMADA DE FOURIER. ......................................25
REFERENCIAS. .........................................................................................................................................27
ÍNDICE GENERAL. ....................................................................................................................................28
ÍNDICE DE FIGURAS. ................................................................................................................................29

Profesor Leopoldo Silva Bijit 26-05-2008


Transformada rápida de Fourier 29

Índice de figuras.

FIGURA 24.1 REPRESENTACIÓN POR MUESTRAS. .......................................................................................... 2


FIGURA 24.2 REPRESENTACIÓN DE POLINOMIOS POR PUNTOS. ...................................................................... 5
FIGURA 24.3. RAÍCES COMPLEJAS CUADRADAS DE LA UNIDAD ..................................................................... 6
FIGURA 24.4. RAÍCES COMPLEJAS CUARTAS DE LA UNIDAD .......................................................................... 6
FIGURA 24.5. DESCOMPOSICIÓN EN POLINOMIOS PARES E IMPARES. ........................................................... 10
FIGURA 24.6. CONTADOR BINARIO DIRECTO E INVERSO.............................................................................. 10
FIGURA 24.7. CONTADOR BINARIO INVERSO CON CUENTAS EN DECIMAL. ................................................... 11
FIGURA 24.8. CÁLCULO DE LA RAÍZ CUADRADA. ........................................................................................ 16
FIGURA 24.9. FORMA DE ONDA Y ESPECTRO PARA SEÑAL CONTINUA EN TIEMPO. ....................................... 19
FIGURA 24.10. FORMA DE ONDA Y ESPECTRO PARA PRIMERA ARMÓNICA. ................................................. 20
FIGURA 24.11. FORMA DE ONDA Y ESPECTRO PARA SEGUNDA ARMÓNICA. ................................................ 20
FIGURA 24.12. FORMA DE ONDA Y MUESTRAS PARA TERCERA ARMÓNICA. ................................................ 21
FIGURA 24.13. ESPECTRO PARA TERCERA ARMÓNICA. ............................................................................... 21
FIGURA 24.14. FORMA DE ONDA Y MUESTRAS PARA SÉPTIMA ARMÓNICA. ................................................. 22
FIGURA 24.15. ESPECTRO PARA SÉPTIMA ARMÓNICA. ................................................................................ 22
FIGURA 24.16. FORMA DE ONDA Y MUESTRAS PARA OCTAVA ARMÓNICA. ................................................. 23
FIGURA 24.17. ESPECTRO PARA OCTAVA ARMÓNICA. ................................................................................ 23
FIGURA 24.18. FORMA DE ONDA Y ESPECTRO PARA NO MÚLTIPLO DE FRECUENCIA DE MUESTREO. ............ 24
FIGURA 24.19. ESPECTRO PARA CON FRECUENCIA 1,5 VECES LA DE PRIMERA ARMÓNICA. ......................... 24
FIGURA 24.20. MEZCLA DE FRECUENCIAS MÚLTIPLES DE LA DE MUESTREO. .............................................. 25
FIGURA 24.20. MEZCLA DE FRECUENCIAS NO MÚLTIPLOS DE LA DE MUESTREO. ........................................ 25
FIGURA 24.21. APROXIMACIÓN DE LA INTEGRAL TEMPORAL. ..................................................................... 26

Profesor Leopoldo Silva Bijit 26-05-2008


1

Capítulo 25.

Listas aleatorizadas. Skip lists.

Una lista de saltos (skip list) representa una lista ordenada simplemente enlazada, en la cual
cada nodo contiene un número variable de enlaces con otros nodos de la estructura. El n-avo
enlace de un nodo apunta a los nodos de n-enlaces siguiente de la lista, saltando los nodos
intermedios de menos de n enlaces. Como se muestra en la Figura 25.1.

6
10 15
8 3 3 31
3 12 23 43
3

Figura 25.1. Lista de saltos.

Puede estudiarse como una colección de listas enlazadas en diferentes niveles. Un recorrido por
la lista de menor nivel, el cero, pasa por todas las claves de la lista.
La estructura tiene además un nodo con el máximo número de niveles que sirve de encabezado,
y un solo nodo centinela, en el que se almacena un valor mayor que el máximo valor de clave
que puede almacenarse en la estructura. A ese nodo apuntan los últimos enlaces de cada lista.

La operación de búsqueda parte por la lista de mayor nivel, que tenga nodos con valores; hasta
que se encuentre el nodo con el valor o un nodo con una clave menor que el buscado. En este
último caso, en el nodo con clave menor se repite el procedimiento anterior, pero en la lista de
un nivel menor.

Por ejemplo para buscar el nodo con clave 12, se parte en la lista de nivel 3, se revisa el nodo
con clave 6 y se continúa con el siguiente, pero éste es el centinela que tiene clave mayor que
12. Entonces se desciende un nivel y se recorre ahora la lista del segundo nivel a partir del nodo
con clave 6. Se avanza al 10, saltándose el 8; pero como la clave 15 es mayor que la buscada, se
vuelve a descender un nivel y se repite la búsqueda a partir de la lista del primer nivel que
comienza en 10. Es preciso volver a descender un nivel y repetir la búsqueda, encontrándose
finalmente el 12. Si se hubiera buscado el valor 13, el proceso es similar al anterior, pero la
búsqueda falla, ya que luego del 12 se encuentra un nodo con clave mayor que el buscado.

La Figura 25.2, muestra el mismo conjunto de valores almacenados en la Figura 25.1, pero
ahora cada vez que se ubica un nodo con clave menor que el buscado, se descartan (o saltan) de

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
la búsqueda la mitad de los nodos restantes; esta distribución de los multinodos garantiza una
búsqueda con costo logarítmico, similar al costo de búsqueda en árboles binarios balanceados.

12
8 3 23
3 6 10 15 31 43
3

Figura 25.2. Lista de saltos.

En lugar de intentar generar una lista de saltos balanceada como la de la Figura 25.2, cada vez
que se inserta un nodo se le asigna su nivel de forma aleatoria. Por esto se dice que en un
algoritmo aleatorizado, ya que independiente del valor de las claves de entrada, y mediante un
generador de números aleatorios, se escoge el nivel del nodo que será insertado.

Luego de determinada la posición para insertar, mediante un generador de números aleatorios se


genera un número, si el último bit es uno se incrementa el nivel, y si es cero se elige el nivel 0,
en este caso la probabilidad de éxito es ½ ; si es uno se vuelve a repetir el procedimiento hasta
encontrar un cero o alcanzar el máximo nivel permitido. Este procedimiento implica que crear
nodos con altos niveles se efectúa con probabilidades cada vez más bajas.

Si se desea incrementar el nivel cada vez que se tenga probabilidad 3/4, se determina el número
binario formado por los dos últimos bits del número aleatorio, si es diferente de cero se
incrementa el nivel, y se repite hasta que el número sea cero. Se tienen los casos: 00, 01, 10 y
11; en tres de los cuatro casos el número será diferente de cero.

Si la probabilidad de aumentar los niveles es alta, aumenta el número de niveles de las listas,
con el consecuente aumento del número de punteros; además aumenta el tiempo de la búsqueda
ya que debe descenderse varios niveles hasta encontrar la clave en el nivel cero. El tener varias
listas aumenta el número de claves que se saltan y disminuye la búsqueda horizontal, en el
mismo nivel, de una de las listas.
Si la probabilidad de aumentar los niveles es baja, el número de niveles será bajo, lo que
disminuye el espacio ocupado por los punteros, y se desciende más rápido al nivel cero; pero se
alargan los recorridos horizontales, en el mismo nivel, hasta encontrar la clave. Si no se forman
niveles, resulta una lista simplemente enlazada, con costo O(n) para las operaciones.

Puede demostrarse que las operaciones de búsqueda y actualización tienen costo esperado
logarítmico respecto al número de nodos almacenados en la estructura, y que la probabilidad de
encontrar un peor caso, por ejemplo todos los nodos del mismo nivel, como puede calcularse, es
muy baja.

Más sorprendente aún es que puede demostrarse que el número esperado de punteros por nodo
no excede 1,3 punteros por clave, lo cual es un espacio menor de almacenamiento que el
requerido para almacenar la estructura en un árbol binario. Además no se requiere almacenar
información adicional de balance o color.

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 3

El autor del algoritmo plantea que el diseño de las listas de salto es más sencillo de codificar que
los empleados en algoritmos para árboles binarios balanceados.
Existen desarrollos teóricos más avanzados que justifican las bondades del algoritmo, pero son
más difíciles de seguir con el nivel de conocimientos que se tienen cuando se cursa una
asignatura básica de estructuras de datos. En forma experimental los treaps tienen mejor
comportamiento que las listas de salto, y emplean un número bastante menor de comparaciones
de claves.

25.1. Complejidad.

25.1.1. Número de niveles.

Si p es la probabilidad de obtener éxito en un experimento, la probabilidad de tener éxito en i


ensayos repetidos es p i . Entonces la probabilidad de insertar un nodo en una lista de nivel i será
p i . Esto implica que los elementos de las listas de mayores niveles son cada vez más cortas, a
medida que se asciende en niveles.

Si cada uno de los n elementos está presente en un conjunto con probabilidad p, entonces el
tamaño esperado del conjunto es np. Entonces ni , el tamaño esperado de la lista de nivel i es:
ni npi .
n n
Puede plantearse entonces: 1/ p i y despejando i, se obtiene: i log1/ p ( ) .
ni ni

Si se asume un peor caso, la lista de mayor nivel tiene un solo elemento y si la probabilidad p es
1/2, se tiene que el mayor nivel de la lista de saltos, queda dado por:

imax log 2 ( n) .

25.1.2. Tamaño del almacenamiento.

El largo esperado de las listas, que es una medida del tamaño dedicado al almacenamiento de la
lista de saltos de h niveles, está dado por:

i h
n
L npi (1 p h 1 )
i 0 1 p

Si p <1 y h >6, puede aproximarse por:


n
L
1 p
La Figura 25.3, ilustra que con p=1/2, el tamaño se mantiene menor que 2n. Con probabilidades
menores el tamaño disminuye y las listas de saltos son más cortas.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

Figura 25.3. Largo lista.

Con h=6: Para p=0,5 se tiene L=1,98. Para p=0,25 se tiene L=1,33. Para p=0,75 se tiene L=3,5.

Podemos calcular el número esperado de punteros por nodo, sumando los largos de las listas
ponderados con la probabilidad de que se encuentren presentes:

1 i h
1 p 2( h 1)
P (np i ) p i
n i 0 1 p2

Figura 25.4. Punteros por nodo.

Con h=6: Para p=0,5 se tiene P=1,33. Para p=0,25 se tiene P=1,06. Para p=0,75 se tiene P=2,24.

25.1.3. Costo de la búsqueda.

La Figura 25.5, muestra una situación de búsqueda, a partir del segundo nivel del nodo con
clave 6. Se tiene dos situaciones, una es seguir en el nivel, por ejemplo si se busca el 12; la otra
es descender de nivel, por ejemplo si se busca el 8.

Sea C(k) el costo de buscar en el nivel k. Sea C(0)=0, el costo cuando se está apuntado a la clave
buscada en el nivel cero, y estudiemos la ruta inversa a la de búsqueda.

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 5
C(k)

6
10 15
8 3 3
12

Figura 25.5. Búsqueda horizontal y descenso de nivel.

Si consideramos que p es la probabilidad que baje de nivel y siga buscando en el nivel (k-1).
Entonces (1-p) es que permanezca buscando en el nivel k. Hemos asumido p, ya que para subir
el nivel de un nodo se lo efectúa con esa probabilidad. Si consideramos costo unitario el
comparar las claves, puede plantearse la recurrencia:

C(k ) (1 p)(1 C(k )) p(1 C(k 1)

De la cual resulta, para k>0:


C(k ) C(k 1) 1/ p

La que requiere una condición inicial C(0) para ser resuelta. Se tienen:

C(1) C(0) 1/ p 1/ p
C(2) C(1) 1/ p 2/ p
Lo que permite obtener:
C (k ) k / p

Y utilizando la relación para el número de nodos en el nivel i-ésimo:

log1/ p (n / ni )
C (i )
p
Si empleamos para ni 1 para el máximo nivel, con p = 1/2, se tiene:

C (imax ) 2 log 2 (n) O(log 2 (n))

Resultado que muestra que el costo esperado de las búsquedas, que comienzan en el mayor
nivel, tendrán costo logarítmico.
Las demostraciones de que estos resultados se cumplen con “alta probabilidad” son complejos.

Los siguientes comandos Maple generan las gráficas de los crecimientos de las funciones en una
lista de saltos, comparadas con las de un árbol generado aleatoriamente (treaps) y un árbol
AVL.
> Cmax:=(ln(n/nm)/ln(1/p))/p;
> plot([1.386*ln(n)/ln(2),subs({p=1/2,nm=1}, Cmax),
1.44*ln(n)/ln(2)],n=10..1000,color=[red,black,blue],thickness=2);

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

AVL Lista de saltos. p=1/2

aleatorio

Figura 25.6. Comparación de lista de salto con árboles balaceados.

Mediante análisis experimental se tienen los siguientes datos, que muestran que la generación de
muchos niveles, aumenta el espacio de almacenamiento, disminuyendo las comparaciones. Si
son muy pocos niveles, disminuye el número de punteros por nodo, pero aumentan las
comparaciones. El caso en que se aumenta el nivel de un nodo en 3 de 4 casos, denominado 2 en
la tabla, produce el menor tiempo de ejecución, y para éste se diseñará la función aleatoria que
fija los niveles de los nodos al insertarlos.

Bits Niveles Punteros/nodo Comparaciones ticks


1 15 2,1 13 32
2 9 1,33 19 29
3 7 1,14 28 33

25.2. Análisis de las operaciones.

25.2.1. Inserción.

Si se desea insertar el nodo con clave 13, en la lista de saltos de la Figura 25.1, primero se ubica
la posición en que debe ser insertado. La Figura 25.7 muestra los cambios luego de la inserción,
cuando el generador aleatorio produjo un nivel superior al mayor existente.

b
a

6 13
10 15
8 3 3 31
3 12 23 43
3

Figura 25.7. Inserción de nodo con clave 13 en la lista de saltos de la Figura 25.1.

Es necesario disponer de un arreglo auxiliar, de tamaño igual al número de niveles, en el cual se


van almacenando las direcciones del nodo que contiene una clave mayor que la buscada en cada
nivel. Como la búsqueda se inicia en el nivel más alto en el que existen nodos, lo anterior

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 7
implica guardar los punteros desde el identificado con letra a hacia abajo. Luego de creado el
nuevo nodo con la clave y otros valores que contenga, dependiendo del nivel aleatorio que tenga
se continua el proceso de ligarlo con el resto de la estructura. Si el nivel k, del nuevo nodo es
menor que el nivel actual de la lista de saltos, sólo es necesario copiar los punteros hacia delante
almacenados en el arreglo, desde k hasta 0; representados con rojo en la Figura 25.7. También
es preciso reapuntar los nodos menores que la nueva clave, recorridos en la búsqueda, hacia el
nuevo nodo; se indican con azul en la Figura 25.7.

Si el nivel generado para el nuevo nodo es mayor que el máximo nivel actual de la lista de
saltos, se decide incrementar un nivel, y es preciso registrar en el arreglo auxiliar la dirección
del puntero denominado b, en la Figura 25.7. Y luego se procede de igual forma al caso anterior.

25.2.2. Descarte.

Es el proceso inverso a la inserción. Se busca el nodo que será descartado, almacenado en el


arreglo auxiliar, las direcciones de los punteros a nodos con claves mayores a las del que será
descartado. Si lo encuentra, copia los punteros hacia delante (los rojos), en los nodos con claves
menores al buscado y que fueros marcados en al búsqueda, en las casillas en que salen vínculos
azules. Luego de liberar el espacio ocupado por el nodo, es preciso revisar, a partir del la lista de
mayor nivel antes de descartar, si quedó vacía para ajustar, disminuyendo en uno, el nivel
máximo de la lista de saltos.

25.3. Generación aleatoria del nivel de un nodo.

Un ensayo de Bernoulli es un experimento que solamente puede tener dos resultados: un éxito,
el cual ocurre con probabilidad p, o un fallo, el que ocurre con probabilidad (1-p). Los ensayos
deben ser mutuamente independientes, de tal modo que cada uno de ellos tenga la misma
probabilidad p de éxito.
Cuando interesa determinar cuántos ensayos ocurren antes de tener un éxito, puede definirse una
variable aleatoria X, que sea el número de ensayos necesarios para obtener un éxito. X puede
tomar valores desde 1 hacia delante, y sea k un número mayor o igual a uno, entonces la
probabilidad de que X sea igual a k, tiene la siguiente distribución discreta:

Pr[ X k ] (1 p) k 1 p

Es decir la probabilidad de obtener (k-1) fracasos, seguido de un éxito. Esta distribución se


denomina geométrica, y se muestra en la Figura 8, para tres valores de p. La distribución sólo
está definida para valores enteros de k, pero se la ha dibujado como una función continua.

La Figura 25.8, ilustra cómo disminuye la probabilidad de crear nodos con niveles iguales a k,
en una lista de saltos. Cuando k=1, se introduce el nodo en el nivel 0; con k=2 el nodo es
asignado al nivel 1; y así sucesivamente.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos

1/2
1/4

1/8

Figura 25.8. Distribución geométrica.

El valor esperado cuando h tiende a infinito es 1/p. Es decir, en promedio se requieren 1/p
ensayos antes de obtener un éxito.
El valor esperado teniendo un nivel máximo h, se obtiene con la suma:

k h
1 (1 ph)(1 p) h
E[ X ] k (1 p ) k 1 p
k 1 p
Con h=16: Para p=1/2, se requieren 1,999 intentos en promedio para un cambio de nivel; si
p=1/4 se requieren 3,8 intentos para cambiar de nivel.

25.4. Estructuras de datos.

Se define como una constante el máximo número de niveles de la lista.

El macro newNodeOfLevel(k) crea un nodo, con espacio para la clave y el valor más el de un
arreglo con un número variable de punteros. Para esto considera el tamaño del nodo, que ya
contiene un arreglo de punteros con un solo nodo, y le suma el espacio de los k punteros a nodo
restantes.

#define false 0
#define true 1
typedef char boolean;

#define MaxNumberOfLevels 16 //sirve hasta para log(n)=16 => n=2^16


#define MaxLevel (MaxNumberOfLevels-1)
#define newNodeOfLevel(k) (pnodo)malloc(sizeof(nodo)+(k)*sizeof(pnodo ))

typedef int TipoClave;


typedef int TipoValor;

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 9
typedef struct nodeStructure
{ TipoClave clave;
TipoValor valor; //otro valor almacenado en el nodo.
struct nodeStructure *forward[1]; /* arreglo de punteros de largo variable */
} nodo, *pnodo ;

La estructura siguiente agrupa el nivel de la lista con un puntero al header de la lista de saltos.

typedef struct listStructure


{ int nivel; /* Niveles de la lista */
pnodo header; /* Apunta al nodo de encabezado que contiene MaxNumberOfLevels punteros
*/
} * list;

Variables globales.

pnodo NIL; //apunta a nodo centinela

#define BitsInRandom 15 //31 para enteros de 32


#define nbits 2
#define Mascara (1<<nbits)-1
int randomsLeft; //variables para el generador aleatorio de niveles.
int randomBits;

int mrandom()
{ return( (int) rand()); } //se usa el generador aleatorio de la biblioteca.

Inicio de variables globales.

void init(void)
{
NIL = newNodeOfLevel(0);
NIL->clave = INT_MAX; //0x7fffffff; //para enteros de 32 bits
NIL->valor = 666; //cualquier valor. Se inicia pero no se usa.
randomBits = mrandom(); //inicia variables del generador.
randomsLeft = BitsInRandom/2;
srand(1);
}

25.5. Creador y destructor.

list newList(void)
{ list sl;
int i;

sl = (list)malloc(sizeof(struct listStructure));
sl->nivel = 0;

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
sl->header = newNodeOfLevel(MaxNumberOfLevels);
sl->header->clave=-INT_MAX; //la deja en menos infinito.
sl->header->valor=0; //No es necesario iniciarlas ya que no se usan los valores del header.
for(i=0; i<MaxNumberOfLevels; i++) sl->header->forward[i] = NIL;

return(sl);
}

void freeList(list sl)


{ register pnodo p,q;
p = sl->header;
do { q = p->forward[0];
//printf("%d \n", p->clave);
free(p); //borra los nodos del nivel k=0.
//el primero que borra es el nodo header.
p = q;
}
while (p!=NIL);
free(sl); //solo queda por liberar el nodo centinela.
}

25.6. Listador.

void prtList(register list l)


{ register int k,cnt,punteros,acum;
register pnodo p,q;
int claves[MaxNumberOfLevels];
k = l->nivel;
punteros=0;
do
{ p = l->header; //imprime nodos de nivel k
if(k>=0 && p->forward[k]!=NIL) printf("k=%d ->", k);
cnt=0;
while (q = p->forward[k], q->clave !=INT_MAX)
{//printf("%d ", q->clave);
p = q; cnt++;
punteros++;
}
if(cnt==0) printf("Lista vacía\n"); else printf("N=%d \n", cnt);
//muestra el número de nodos en el nivel k
claves[k]=cnt;
k--;
}
while (k>=0);

if (cnt>0)
{ for(acum=0, k=l->nivel;k>=0;k--)

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 11
{printf("%d ",claves[k]-acum) ;acum=claves[k]; } //imprime nodos en cada nivel
putchar('\n');
}
if(punteros) printf("Punteros por clave=%f \n", (float) punteros/(float) cnt );
}

25.7. Genera nivel aleatorio.

Esta es la rutina que aleatoriza el algoritmo.

int randomLevel(void)
{register int nivel = 0;
register int b;
do { b = randomBits&Mascara; //inspecciona últimos nbits
if (!b) nivel++; //en (1<<nbits)-1 de (1<<nbits) casos incrementa nivel
randomBits>>=nbits;
randomsLeft--;
if (randomsLeft == 0) // Cuando se acaban, invoca al generador
{ randomBits = mrandom(); //recarga número aleatorio
randomsLeft = BitsInRandom/nbits; //reinicia pasadas
}
}
while (!b);

return(nivel>MaxLevel ? MaxLevel : nivel);


}

25.8. Insertar, descartar.

La rutina insertar puede compilarse para permitir o no duplicados. Se compila


condicionalmente, empleando la constante allowDuplicates.

pnodo update[MaxNumberOfLevels]; //arreglo global para mantener los enlaces

#ifdef allowDuplicates
void insertar(register list l, register TipoClave clave, register TipoValor valor)
#else
boolean insertar(register list l, register TipoClave clave, register TipoValor valor)
#endif
{ register int k;
register pnodo p, q;
p = l->header;
k = l->nivel;
do { while (q = p->forward[k], q->clave < clave)
{p = q; //operador coma.
}
update[k] = p; //salva enlaces hacia al nuevo multinodo.

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
k--;
}
while(k>=0);

#ifndef allowDuplicates
if (q->clave == clave)
{ q->valor = valor; //sobreescribe valor.
return(false);
}
#endif

k = randomLevel();
if (k > l->nivel) //si es mayor sólo aumenta al nivel siguiente
{ k = ++l->nivel;
update[k] = l->header;// hay que actualizar el nuevo nivel
}
q = newNodeOfLevel(k);
q->clave = clave;
q->valor = valor;
do { p = update[k]; //Inserta en todas las listas a partir de la k-ésima
q->forward[k] = p->forward[k]; //pega q con los proximos
p->forward[k] = q; //enlaza q con los anteriores
k--;
}
while(k>=0);
#ifndef allowDuplicates
return(true);
#endif
}

boolean descartar(register list l, register TipoClave clave)


{ register int k,m;
register pnodo p,q;
p = l->header;
k = m = l->nivel;
do { while (q = p->forward[k], q->clave < clave)
{p = q;
}
update[k] = p;
k--;
} while(k>=0);
if (q->clave == clave)
{ for(k=0; k<=m && (p=update[k])->forward[k] == q; k++)
p->forward[k] = q->forward[k]; //liga las listas
free(q);
if( l->header->forward[m] == NIL && m > 0 ) m--; //si lista vacía en nivel m (es if no
while.)

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 13
l->nivel = m; //fija el nivel
return(true);
}
else return(false); //si no encontró la clave
}

25.9. Buscar.

boolean buscar(register list l, register TipoClave clave, TipoValor *valorPointer)


{ register int k;
register pnodo p,q;
p = l->header;
k = l->nivel;
do
{ while (q = p->forward[k], q->clave < clave) p = q;
k--;
}
while (k>=0);
if (q->clave != clave) return(false);
*valorPointer = q->valor; //escribe por referencia el valor asociado a la clave
return(true);
}

25.10. Test de las funciones.

list lista; //variable del skip list


#define sampleSize 79 //65536

int main(void)
{
register int i,k;
TipoClave keys[sampleSize]; //arreglo con valores.
TipoValor v;
clock_t start, stop; //tipo definido en time.h
int totaltime = 0;
//printf("max=%d\n",INT_MAX);
printf("Prob=%f\n", ((float)Mascara)/((float)(Mascara+1)));
start = clock();

init();
lista= newList();

for(k=0; k<sampleSize;k++)
{ keys[k]=k; //puede también guardarse en forma descendente, o aleatoria.
insertar(lista, keys[k], keys[k]);
//prtList(lista);putchar('\n');
}

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
prtList(lista);putchar('\n');

for(k=0; k<sampleSize;k++)
{ if (! descartar(lista, keys[k])) printf("Error en descartar\n");
//prtList(lista); putchar('\n');
}
prtList(lista); putchar('\n'); //debe quedar vacía

for(k=0; k<sampleSize;k++)
{ keys[k]=k; //random();
insertar(lista, keys[k], keys[k]);
//prtList(lista);putchar('\n');
}

for(i=0;i<4;i++)
{ for(k=0; k<sampleSize;k++)
{ if (!buscar(lista,keys[k],&v)) printf("Error en búsqueda #%d,#%d\n", i, k);
if (v != keys[k]) printf("Búsqueda retorna valor errado\n");
};
for(k=0; k<sampleSize;k++)
{ if (! descartar(lista,keys[k])) printf("Error en descartar\n");
keys[k] = k; //random();
insertar(lista,keys[k],keys[k]);
}
}
//quedan los nodos en la lista
freeList(lista);
//debe quedar vacía.
free(NIL);
stop = clock();
totaltime += (stop-start); // Mantiene el tiempo usado por la acción. Aproximadamente.
printf("Tiempo acumulado = %d [ticks]\n", totaltime);
printf("\nFin de test.\n");
return (0);
}

Referencias.

William Pugh. “Skip Lists – A Probabilistic Alternative to Balanced Trees”, Communication of


ACM 33, 1990.
Dominique A. Heger. ”A Disquisition on The Performance Behaviour of Binary Search Tree
Data Structures” UPGRADE Vol. V, No. 5, October 2004.

Profesor Leopoldo Silva Bijit 26-05-2008


Listas aleatorizadas. Listas de salto. 15
Índice general.

CAPÍTULO 25. .......................................................................................................................................... 1


LISTAS ALEATORIZADAS. SKIP LISTS. ........................................................................................... 1
25.1. COMPLEJIDAD. ................................................................................................................................. 3
25.1.1. Número de niveles. .................................................................................................................. 3
25.1.2. Tamaño del almacenamiento. .................................................................................................. 3
25.1.3. Costo de la búsqueda. ............................................................................................................. 4
25.2. ANÁLISIS DE LAS OPERACIONES. ...................................................................................................... 6
25.2.1. Inserción. ................................................................................................................................. 6
25.2.2. Descarte. ................................................................................................................................. 7
25.3. GENERACIÓN ALEATORIA DEL NIVEL DE UN NODO. ......................................................................... 7
25.4. ESTRUCTURAS DE DATOS. ................................................................................................................ 8
Variables globales. .............................................................................................................................. 9
Inicio de variables globales. ............................................................................................................... 9
25.5. CREADOR Y DESTRUCTOR. ............................................................................................................... 9
25.6. LISTADOR. ..................................................................................................................................... 10
25.7. GENERA NIVEL ALEATORIO............................................................................................................ 11
25.8. INSERTAR, DESCARTAR. ................................................................................................................. 11
25.9. BUSCAR. ........................................................................................................................................ 13
25.10. TEST DE LAS FUNCIONES. ............................................................................................................. 13
REFERENCIAS. ........................................................................................................................................ 14
ÍNDICE GENERAL. ................................................................................................................................... 15
ÍNDICE DE FIGURAS................................................................................................................................. 16

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos
Índice de figuras.

FIGURA 25.1. LISTA DE SALTOS. ....................................................................................................................1


FIGURA 25.2. LISTA DE SALTOS. ....................................................................................................................2
FIGURA 25.3. LARGO LISTA. ..........................................................................................................................4
FIGURA 25.4. PUNTEROS POR NODO...............................................................................................................4
FIGURA 25.5. BÚSQUEDA HORIZONTAL Y DESCENSO DE NIVEL......................................................................5
FIGURA 25.6. COMPARACIÓN DE LISTA DE SALTO CON ÁRBOLES BALACEADOS.............................................6
FIGURA 25.7. INSERCIÓN DE NODO CON CLAVE 13 EN LA LISTA DE SALTOS DE LA FIGURA 25.1. ...................6
FIGURA 25.8. DISTRIBUCIÓN GEOMÉTRICA. ...................................................................................................8

Profesor Leopoldo Silva Bijit 26-05-2008


1

Apéndice 1

DESCRIPCION FORMAL DE
LENGUAJES

A1.1. Léxico, Sintaxis, Semántica.

Un programa es una secuencia de símbolos y puede considerarse como un texto.

Los símbolos de un lenguaje pertenecen a un conjunto que se denomina vocabulario o léxico.


Los símbolos también se denominan elementos léxicos o tokens. Léxico significa diccionario; y
aplicado en el ambiente de lenguajes de programación se utiliza para denotar los símbolos del
lenguaje. Estos símbolos, a su vez, están formados por secuencias de caracteres; y existen reglas
que determinan cómo puede generarse o producirse un símbolo a partir de caracteres.

Cada lenguaje de programación define reglas que permiten componer el texto de un programa
como una secuencia de símbolos. El conjunto de estas reglas se denomina gramática, o más
usualmente, la sintaxis del lenguaje. Sintaxis significa con orden. Cada regla establece una clase
definida de objetos o categorías sintácticas; como ejemplos pueden darse algunas partes típicas
de un programa: acciones, declaraciones, condiciones, expresiones, etc.

Asociado a cada palabra (símbolo) y a cada frase o sentencia (categoría sintáctica) debe existir
un significado, el cual se traduce en valores de los objetos (constantes y variables) de acuerdo a
sus tipos; o en nombres de objetos o grupos de acciones; o en la especificación de las
operaciones que deben efectuarse sobre esos objetos. Todas las reglas que aportan esta
información se denomina: Semántica del lenguaje.

Si bien las reglas para construir símbolos y frases son finitas, el conjunto de programas es
infinito.

Para describir con rigurosidad los lenguajes de programación se emplea una notación formal
que se denomina Metalenguaje.
El formulismo más conocido, y que emplearemos en la descripción, es el Formalismo
Extendido Backus-Nauer. (BNF, Backus Nauer Formalism).

Básicamente consiste en describir una frase, (categoría sintáctica) o parte abstracta de un


programa, mediante la secuencia de componentes, de menor categoría, que pueden reemplazar

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
dicha frase. Las reglas deben especificar hasta llegar al reemplazo por los símbolos que
componen el diccionario.

En este ambiente, los elementos léxicos del lenguaje se denominan símbolos terminales. Las
componentes estructurales del lenguaje que serán reemplazadas, se denominan símbolos no
terminales. La regla que establece el reemplazo de un símbolo no terminal por una secuencia de
símbolos terminales y no terminales se denomina producción.
Las reglas deben permitir verificar, con facilidad, si una secuencia de símbolos es o no una
sentencia correcta del lenguaje.

Un programador debe conocer cómo generar secuencias de símbolos terminales que cumplen
la gramática.

A1.2. Metalenguaje BNF.

A1.2.1. Producción.

Existe sólo una acción primitiva, es la producción. Es una ecuación sintáctica que permite
definir una categoría sintáctica, S; mediante una expresión E.

En símbolos:
<S> ::= <E>

Gráficamente:

S E

Figura A1.1. Producción

Los paréntesis de ángulo, delimitan los símbolos no terminales. En el gráfico esto se representa
por un rectángulo.

La secuencia ::= , es el metasímbolo para la producción. Y se lee: puede ser reemplazado por.

Una producción puede considerarse la instancia de definición de S en términos de E.

Lo usual es que S corresponda a una parte o concepto del lenguaje. Por ejemplo: acciones,
condiciones, tipos, etc.

La expresión E, debe especificar una secuencia de símbolos; éstos, a su vez, deben especificar,
en forma precisa, cómo se estructura una parte en términos de sus componentes.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 3
A1.2.2. Secuenciación de símbolos.

Los lenguajes estructurados suelen disponer tres formas básicas para establecer secuencias: La
alternativa, la concatenación, y la iteración.
A1.2.2.1. Alternativa.
Una expresión puede considerarse como una lista de términos sintácticos alternativos.

En símbolos:

<E> ::= <T1>|<T2>|--- |<Tn> n>0

Gráficamente:

T1

T2

E
Ti

Tn

Figura A1.2. Alternativa

El metasímbolo | se lee como o excluyente. La producción anterior explica que una expresión
puede remplazarse por uno (y sólo uno) de los términos de la lista.

Ejemplo:

<clase de almacenamiento> ::= 'auto ' | 'extern ' | 'register ' | 'static ' | 'typedef '

Nótese que los símbolos terminales, se indican por una secuencia de caracteres entre comillas
simples.
A1.2.2.2. Concatenación.
Cada término puede ser reemplazado por la concatenación (o producto) de factores sintácticos.

En símbolos:

<T> ::= <F1><F2> ---- <Fn> n>0

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

Gráficamente:

T F1 F2 Fi Fn

Figura A1.3. Concatenación

Ejemplo:

<acción repetitiva> ::= 'while' '(' <expresión> ')' <acción>

A1.2.2.3. Opción.
a) Opción. Una o ninguna.
Una forma de reemplazar un factor es mediante la opción.

En símbolos:

<F> ::= [ E ]

Gráficamente:

Figura A1.4. Opción.

Los paréntesis cuadrados son los metasímbolos empleados para la opción, se indica que el factor
puede ser reemplazado por la expresión o por nada. Se dice que [E] es opcional; puede estar o
no. Y si está, lo hace sólo una vez.

Ejemplo:
<signo> ::= [ '+' | '-' ]

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 5

+
Signo

-
Figura A1.5. Sintaxis de Signo

Debe notarse que los símbolos terminales se representan encerrados en ovoides o círculos, en
la descripción gráfica.

La ausencia de símbolo, suele ser tratada como la ocurrencia del símbolo vacío.

b) Repetición.
Un factor también puede ser reemplazado por la repetición, de cero o más veces, de una
expresión.

En símbolos:
<F> ::= { E }

Gráficamente:

Figura A1.6. Repetición

Los paréntesis de llave denotan la repetición.

Ejemplo:

<número entero sin signo> ::= <dígito>{dígito}

Para delimitar el número máximo de repeticiones, suele agregarse un valor entero como
superíndice.

Ejemplo: {digito}^3 indica a lo más 3 cifras.

c) Repetición de a lo menos una vez.

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
Una alternativa de reemplazo de factor, es la repetición, de una expresión, por lo menos una
vez.

En símbolos:

<F> ::= <E> {E}

La que es semánticamente equivalente a:

<F> ::= {E} <E>

Estableceremos la siguiente equivalencia, como convenio, para simplificar la notación:

<F> ::= {E*}

Gráficamente:

F E

Figura A1.7. Repetición de a lo menos una vez.

d) Lista.
Una construcción de uso frecuente es:

<lista> ::= { <entidad> <separador> } <entidad>

La lista está formada por una entidad a lo menos; en caso de existir varias entidades, éstas
aparecen separadas por el separador. Nótese que después de la última entidad no debe existir un
separador.

Debido a su frecuente uso, la sintaxis de lista puede convenirse en anotarla, en forma


abreviada, según:

<lista> ::= { <entidad>* <separador> }

A1.2.2.4. Agrupaciones.
En las ocasiones que sean necesarias pueden agruparse términos y factores sintácticos
mediante paréntesis redondos.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 7

Ejemplo:
<término simple> ::= ('A'|'B')('C'|'D')

Las siguientes secuencias cumplen la sintaxis de término simple:

AC AD BC BD
A1.2.2.5. Sintaxis de factor.
En símbolos:

<F> ::= <S>|<símbolo terminal>|(E)| E |[ E ]|{ E }

Los símbolos terminales son secuencias de símbolos tomados del vocabulario del lenguaje. Y
se representan entre comillas simples.

La entidad sintáctica <S> debe considerarse representada por su identificador o nombre; y


debe estar definida previamente.

A1.2.3. Descripción formal de BNF.

El BNF es un lenguaje formal, y como veremos puede emplearse para describirse a sí mismo.

Las siguientes producciones definen el formalismo:

<sintaxis> ::= <producción>

<producción> ::= <identificador> '::=' <expresión>

<expresión> ::= <término> { '|' <término> }

<término> ::= <factor> { <factor> }

<factor> ::= <identificador> |


<símbolo terminal> |
'(' <expresión> ')' |
<expresión> |
'[' <expresión> ']' |
'{' <expresión> '}'

El identificador denota una entidad sintáctica.

A1.2.4. Ejemplos.

a) 'A'['B']'C' genera ABC y AC


b) 'A'{'BA'} genera A ABA ABABA .......
c) {'A'|'B'}'C' genera C AC BC AAC

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
A1.2.5. Árboles de Derivación.

Una forma de ayudar a reconocer las frases y su estructura es el desarrollo de árboles de


derivación.

Ejemplo: Dadas las producciones:


<entero con signo>::=<signo><entero>
<signo>::='+'|'-'
<entero>::=<dígito><entero>|<dígito> (definición recursiva)
<dígito>::='0'|'1'

Verificar si +10 pertenece o no al lenguaje. Específicamente si es o no un entero con signo.

Una forma de verificación es la construcción de un árbol de derivación, que consiste en


representar gráficamente los reemplazos que se efectúan desde el símbolo de partida (raíz) hasta
llegar a elementos terminales (hojas). [top-down]

El entero con signo puede reemplazarse por la secuencia: signo entero.

enteroconsigno

signo entero

Figura A1.8. Secuencia signo entero

A su vez, signo se reemplaza por + (ya es terminal). Y entero por: dígito entero.

enteroconsigno

signo entero

dígito entero
+
Figura A1.9. Dígito entero

Dígito por el terminal 1, y entero por dígito. Finalmente dígito por el terminal 0.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 9

enteroconsigno

signo entero

dígito entero
+

1 dígito

0
Figura A1.10. Entero con signo

El procedimiento de reconocimiento puede también efectuarse a la inversa, desde las hojas


hacia la raíz. (bottom up)

A1.2.6. Recursividad en Producciones.

Consiste, en este caso, en definir un símbolo no terminal en términos de sí mismo. La


recursividad permite definir lenguajes con un número finito de producciones. La definición
recursiva permite generar símbolos de longitud variable.

El factor básico de repetición puede describirse explícitamente en forma recursiva. Se tiene


que:

<A>::={<B>} (i)

es equivalente a:

<A>::=<vacío>|<A><B> (ii)

En forma gráfica:

Figura A1.11. Repetición (i)

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos

A B

Figura A1.12. Repetición recursiva (ii)

Se dice que la definición ii) de A es recursiva. Puede comprobarse que existen otras formas
equivalentes. Son equivalentes porque generan las mismas secuencias; cuestión que puede
verificarse desarrollando árboles de derivación.

Otras formas equivalentes son:

<A> ::= <vacío>|<B><A> (iii)

<A> ::= <vacío>|<A><B><A> (iv)

La secuencia BBBB, puede lograrse para el caso (ii) según:

A es reemplazado por:
AB; luego A es nuevamente reemplazado por AB:
ABB; luego A por AB, queda:
ABBB; otra vez A por AB, queda:
ABBBB; finalmente A por el símbolo vacío.

Ejemplo: <S>::='z'|'y'<S>

S z

y S

Figura A1.13. Producción recursiva

Genera las secuencias:


z
yz
yyz
yyyz
...

Puede escribirse: <S>::={'y'}'z'

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 11

S z

Figura A1.14. Simplificación a no recursiva.

A1.2.7. Asociatividad en Producciones.

El uso de recursividad en las producciones, permite establecer reglas de asociatividad sin uso
de paréntesis.

Puede emplearse recursividad por la izquierda.


Ejemplo:
<expresión>::= <expresión><operador><variable>|<variable>
<operador> ::= '+'|'*'
<variable> ::= 'x'|'y'

La secuencia x+y*x+y se interpreta: ((x+y)*x)+y

Con la derivación:
exp

exp op var

exp op var + y

exp op var * x

var + y

Figura A1.15. Derivación

Se refleja la asociatividad, agrupando primero las hojas más alejadas de la raíz.

También puede emplearse recursividad por la derecha:

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
Ejemplo:
<expresión> ::=<variable><operador><expresión>|<variable>
<operador> ::='+'|'*'
<variable> ::='x'|'y'

La secuencia x+y*x+y se interpreta: x+(y*(x+y))

Con la derivación: exp

var op exp

x + var op exp

y * var op exp

x + var

Figura A1.16. Derivación

A1.2.8. Ambigüedad en Producciones.

Ciertas producciones pueden llevar a ambigüedades semánticas:

Ejemplo:
<expresión> ::=<expresión><operador><expresión>|<variable>
<operador> ::='+'|'*'
<variable> ::='a'|'b'|'c'

Puede comprobarse que la secuencia: a+b*c puede interpretarse como a+(b*c) y también como
(a+b)*c.

Comprobar lo anterior generando los árboles de derivación correspondientes. Obviamente en


una gramática deben evitarse producciones que generen ambigüedades.

A1.2.9. Precedencia de operadores en expresiones aritmético lógicas.

El formalismo BNF, permite reflejar el orden o jerarquía de los operadores en una expresión
aritmética o lógica.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 13

Por ejemplo: Si se tiene que el operador * tiene precedencia (o mayor jerarquía) que el
operador +; entonces la secuencia a+b*c se interpreta como:

a+(b*c)

Es decir, se realiza primero la multiplicación, luego la adición.

El uso cuidadoso de las reglas de composición de expresiones aritméticas y lógicas, permite


escribirlas sin usar paréntesis. Sin embargo, para mejorar la legibilidad en ciertos casos, suelen
emplearse éstos.

Cuando se escriben expresiones complejas empleando paréntesis, suele usarse la regla de


contar los paréntesis abiertos y los cerrados: deben estar balanceados.

A1.2.10. Reglas para construir Expresiones.

i) Las expresiones están formadas por operadores y operandos.


ii) Son fórmulas o reglas para computar valores.
iii) Precedencia de operadores
iv) Los grupos de operadores de igual precedencia se ejecutan de acuerdo a su regla de
asociatividad.

La siguiente lista muestra los operadores agrupados, en niveles de precedencia, en orden


descendente de ésta. Cada grupo está separado del siguiente por una línea en blanco. La
columna a la derecha establece la asociatividad del operador.

Operador Ejemplo notación Asociatividad


Post-incremento X++ desde izquierda hacia la derecha
Post-decremento X--
subíndice X[Y]
llamado a función X(Y)
selección de miembro X.Y
apunta a miembro X->Y

sizeof sizeof X desde derecha a izquierda


pre-incremento ++X
pre-decremento --X
dirección de &X
indirección *X
más +X
menos -X
NOT al bit ~X
NOT lógico !X
casteo de tipo (declaración)X

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
multiplicación X*Y desde izquierda a derecha
división X/Y
Resto (modulo) X%Y

suma X+Y desde izquierda a derecha


resta X–Y

Corrimiento a izquierda X << Y desde izquierda a derecha


Corrimiento a derecha X >> Y

Menor que X<Y desde izquierda a derecha


Menor o igual que X <= Y
Mayor que X>Y
Mayor o igual que X >= Y

Igualdad X == Y desde izquierda a derecha


No igualdad X != Y

AND al bit X&Y desde izquierda a derecha

OR exclusivo al bit X^Y desde izquierda a derecha

OR inclusivo al bit X|Y desde izquierda a derecha

AND lógico X && Y desde izquierda a derecha

OR lógico X || Y desde izquierda a derecha

Condicional Z?X:Y desde derecha a izquierda

Asignación X=Y desde derecha a izquierda


Multiplica y asigna X *= Y X=X*Y
Divide y asigna X /= Y X=X/Y
Resto y asigna X %= Y
Suma y asigna X += Y
Resta y asigna X -= Y
Left shift assign X <<= Y
right shift assign X >>= Y
AND al bit y asigna X &= Y
OR ex al bit y asigna X ^= Y
OR inclusive al bit asigna X |= Y

coma X, Y desde izquierda a derecha

Figura A1.17. Reglas de precedencia y asociatividad.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 15

La precedencia se refiere al orden de evaluación. Es decir, cuáles operadores reciben primero


sus operandos.

Ejemplos:

La semántica de a/b/c es (a/b)/c de acuerdo a la regla iv) ya que hay secuencia de operadores de
igual precedencia y se asocia desde izquierda a derecha.

En forma similar: a/b*c se interpreta: (a/b)*c

y=m*x+b se interpreta como y = ((m * x) + b)


*p++ = -X->Y se interpreta como (*(p++)) = (-(X->Y))

Para la expresión: a + b*c, aplicando la regla iii), la multiplicación tiene precedencia sobre la
suma, por lo tanto se ejecuta primero. Se interpreta: a + (b*c)

A1.3. Símbolos del lenguaje. Léxico.

El texto de un programa está formado por una secuencia de símbolos o elementos léxicos.

Los elementos léxicos, a su vez, están formados por secuencias de caracteres.

Existen reglas de composición que determinan cómo pueden generarse o producirse un


símbolo a partir de caracteres; que en el caso de programas de alto nivel, se consideran símbolos
terminales.

El conjunto de símbolos terminales es el vocabulario del lenguaje (reglas léxicas).

A1.3.1. Conjuntos de Caracteres.

Los caracteres están estandarizados y les corresponde un código único (de un código de 7
bits, ISO estándar). Con 7 bits pueden codificarse 128 caracteres diferentes.

Suele emplearse el código ASCII (Código Estándar Americano para el Intercambio de


Información), que usa 8 bits por carácter, dejando un bit para controlar la paridad. La paridad
consiste en dejar un número par o impar de unos en una palabra del código, y se emplea con
fines de detección de errores.

Los elementos del código que se representan (visualmente) mediante un símbolo gráfico se
denominan caracteres gráficos. Entre ellos 26 letras mayúsculas, 26 minúsculas, 10 dígitos
decimales, 32 caracteres especiales (signos), y el espacio.

El resto del código (33 símbolos, no gráficos) está formado por caracteres de control, que se
emplean para dar efectos de formato y para controlar la comunicación de caracteres entre un
equipo y otro. Se denominan de formato a: tabulación horizontal y vertical, retorno de carro,
alimentación de nueva línea y alimentación de nuevo formulario.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

En el texto de un programa sólo se permiten caracteres gráficos y de formato.

A1.3.2. Elementos Léxicos (tokens).

En lenguajes de alto nivel es usual definir las siguientes unidades:

i) Delimitadores
ii) Identificadores
iii) Números (literal numérico)
iv) Carácter
v) Strings
vi) Comentarios
vii) Separadores

El efecto de un programa depende solamente de la particular secuencia de elementos léxicos


que lo forman.

Existen reglas que determinan como puede generarse una frase a partir de los elementos
léxicos. El conjunto de reglas se denomina gramática del lenguaje.

La gramática o estructura del lenguaje permite determinar si una secuencia de palabras


(tokens) es una sentencia correcta o no. La estructura de las frases es esencial en el
reconocimiento del significado de éstas (semántica).

A1.3.3. Separadores

En algunos casos es necesario un separador explícito para separar elementos léxicos


adyacentes.

Se consideran separadores al espacio, a los caracteres con efecto de formato y el final de una
línea. Suele no definirse qué causa el fin de línea, en algunos sistemas pueden ser uno o más
caracteres de control.

Reglas para el uso de separadores:

a) Se requiere a lo menos un separador entre un identificador o número y el identificador o


número adyacente.

b) Se permite uno o más separadores entre dos elementos léxicos adyacentes. También se
acepta uno o más separadores, antes del primer elemento léxico del programa y después del
último. (Formato libre)

c) Los comentarios son considerados separadores.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 17
A1.3.4. Comentarios.

Los comentarios se emplean para mejorar la legibilidad del programa; y deben ocuparse con
frecuencia para aclarar el significado de variables, o de acciones. Los comentarios pueden
sacarse del texto sin alterar el significado del programa.

Cualquier secuencia de caracteres encerrados entre los símbolos /* y */ se denomina


comentario. Hay comentarios de fin de línea, de línea completa o de varias líneas.
Alternativamente un comentario puede comenzar con el doble símbolo: //.

A1.3.5. Carácter.

Un carácter literal se forma encerrando uno de los 95 caracteres gráficos (incluyendo el


espacio) entre comillas simples.

Ej: '*' '"' 'g' ' '

A1.3.6. Strings. ( tira, mensaje, texto, hileras, cadenas)

Son secuencias de caracteres gráficos entre comillas dobles. Para incluir una comilla doble, se
la precede con \. ”O/”Higgins”.

El largo del string es el número de caracteres que forman la secuencia. Se emplean para
intercalar texto (legible) en la salida. Pueden usarse para alertar al operador que se requiere una
entrada o una operación de su parte (prompts); o bien para hacer más comprensible la salida
(mensajes).

A1.3.7. Números.

Se habla de literal numérico cuando se quiere enfatizar que se está haciendo referencia a cómo
se escribe un número como una secuencia de caracteres.

Existen enteros y reales. Los reales incluyen un punto (la coma decimal). Entre los reales, se
habla de punto fijo y punto flotante (o formato científico y exponencial; llevan la letra E).

<signo> ::= ['+' | '-']


<constante entera sin signo> ::= { dígito* }
<constante entera> ::= <signo> <constante entera sin signo>

<real punto fijo> ::= {dígito*} '.' {dígito}


<exponente> ::= ('e'|'E')<constante entera>
<real punto flotante> ::= {dígito*} ['.'{dígito*}] <exponente>
<constante real> ::= <real punto fijo> |
<real punto flotante>

Un resumen de las producciones anteriores es:

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos

número + +
dígito . dígito E dígito
- -

Figura A1.18. Sintaxis de número

La representación anterior no considera aspectos prácticos como máximo número representable,


exactitud de representación, etc., que dependen de la instalación. La sintaxis de número se
abstrae de estos detalles y sólo explica cómo se escriben.

A1.3.8. Identificadores.

Se usan como nombres y también como palabras reservadas. Comienzan con una letra, que
puede ser seguida de cualquier combinación de letras y números. El espacio no se acepta dentro
de un identificador. Sirven para dar nombre a: constantes, tipos de datos, variables,
procedimientos, funciones.

letra
identificador
letra
dígito

Figura A1.19. Sintaxis de identificador

Las palabras reservadas tienen un significado especial en el lenguaje y no pueden emplearse


como identificadores de objetos.

Los siguientes símbolos son palabras reservadas:

auto double int struct


break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while

Estos símbolos también pueden ser clasificados como delimitadores.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 19
Existen también identificadores estándares que el lenguaje predefine. Por ejemplo, el nombre
de algunas funciones y constantes. Pueden usarse sin definirse previamente. Además podrían ser
redefinidos dentro del programa; es decir, cambiar el significado estándar.

A1.3.9. Delimitadores.

Es uno de los siguientes caracteres especiales:

~ + ; ] % , < ^
& - = { ( . > | ) / ? } ! * : [

O uno de los siguientes delimitadores, compuestos de dos caracteres especiales adyacentes:

... && -= >= <<= &= -> >>


>>= *= /= ^= != ++ << |=
%= += <= || -- ==

Algunos delimitadores se usan como operadores. Otros para establecer mecanismos de acceso
o selección de datos estructurados.

A1.3.10. Resumen.

<texto de un programa> ::= { { <separador> } <token> }

<separador> ::= <espacio> | <fin de línea> | <comentario>

<token> ::= <palabra reservada> |


<delimitador> |
<nombre> |
<constante>

<constante> ::= <contante entera> |


<constante real> |
<constante caracter> |
<string>

A1.3.11. Ejemplos.

A1.3.11.1. Indicar mediante paréntesis como se evalúan las expresiones:


a) 80/5/4
La expresión se evalúa ((80/5)/4)

b) sqrt(sqr(3)+11*5)
Se evalúa: sqrt(sqr(3)+(11*5))

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos

c) x =2*y-5.02
Se evalúa: x = ((2*y) - 5.02)
A1.3.11.2. Escribir todas las secuencias que cumplen la sintaxis dada por:
a) {'A'|'B'}'C'
b) 'A'{'BA'}
c) ('A'|'B')('C'|'D')
d) 'A'['B']('C'|'D')

Si el número de secuencias es mayor que 10, escribir por lo menos 5 de ellas.

a) {'A'|'B'}'C'

número de secuencias>10
1) C
2) AC C
3) BC
4) ABC A
5) AAC
B
b) 'A'{'BA'}

número de secuencias >10


1) A A
2) ABA
3) ABABA
4) ABABABA AB
5) ABABABABA

c) ('A'|'B')('C'|'D')

número de secuencias = 4
1) AC A C
2) AD
3) BC
4) BD B D

d) 'A'['B']('C'|'D')

número de secuencias = 4
1) AC C
2) AD A
3) ABC B D
4) ABD

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 1. Descripción formal de lenguajes 21

Índice general.

APÉNDICE 1 .............................................................................................................................................. 1
DESCRIPCION FORMAL DE LENGUAJES ........................................................................................ 1
A1.1. LÉXICO, SINTAXIS, SEMÁNTICA. ..................................................................................................... 1
A1.2. METALENGUAJE BNF. .................................................................................................................... 2
A1.2.1. Producción. ............................................................................................................................. 2
A1.2.2. Secuenciación de símbolos. ..................................................................................................... 3
A1.2.2.1. Alternativa........................................................................................................................................ 3
A1.2.2.2. Concatenación. ................................................................................................................................. 3
A1.2.2.3. Opción. ............................................................................................................................................. 4
a) Opción. Una o ninguna. .......................................................................................................................... 4
b) Repetición. .............................................................................................................................................. 5
c) Repetición de a lo menos una vez. .......................................................................................................... 5
d) Lista. ....................................................................................................................................................... 6
A1.2.2.4. Agrupaciones. .................................................................................................................................. 6
A1.2.2.5. Sintaxis de factor. ............................................................................................................................. 7
A1.2.3. Descripción formal de BNF. ................................................................................................... 7
A1.2.4. Ejemplos.................................................................................................................................. 7
A1.2.5. Árboles de Derivación. ........................................................................................................... 8
A1.2.6. Recursividad en Producciones. ............................................................................................... 9
A1.2.7. Asociatividad en Producciones. ............................................................................................ 11
A1.2.8. Ambigüedad en Producciones. .............................................................................................. 12
A1.2.9. Precedencia de operadores en expresiones aritmético lógicas. ........................................... 12
A1.2.10. Reglas para construir Expresiones. .................................................................................... 13
A1.3. SÍMBOLOS DEL LENGUAJE. LÉXICO. .............................................................................................. 15
A1.3.1. Conjuntos de Caracteres. ...................................................................................................... 15
A1.3.2. Elementos Léxicos (tokens). .................................................................................................. 16
A1.3.3. Separadores .......................................................................................................................... 16
A1.3.4. Comentarios. ......................................................................................................................... 17
A1.3.5. Carácter. ............................................................................................................................... 17
A1.3.6. Strings. ( tira, mensaje, texto, hileras, cadenas) ................................................................... 17
A1.3.7. Números. ............................................................................................................................... 17
A1.3.8. Identificadores. ..................................................................................................................... 18
A1.3.9. Delimitadores. ....................................................................................................................... 19
A1.3.10. Resumen. ............................................................................................................................. 19
A1.3.11. Ejemplos.............................................................................................................................. 19
A1.3.11.1. Indicar mediante paréntesis como se evalúan las expresiones:..................................................... 19
A1.3.11.2. Escribir todas las secuencias que cumplen la sintaxis dada por: .................................................. 20
ÍNDICE GENERAL. ................................................................................................................................... 21
ÍNDICE DE FIGURAS................................................................................................................................. 22

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos

Índice de figuras.

FIGURA A1.1. PRODUCCIÓN ..........................................................................................................................2


FIGURA A1.2. ALTERNATIVA ........................................................................................................................3
FIGURA A1.3. CONCATENACIÓN....................................................................................................................4
FIGURA A1.4. OPCIÓN. ..................................................................................................................................4
FIGURA A1.5. SINTAXIS DE SIGNO.................................................................................................................5
FIGURA A1.6. REPETICIÓN ............................................................................................................................5
FIGURA A1.7. REPETICIÓN DE A LO MENOS UNA VEZ. ....................................................................................6
FIGURA A1.8. SECUENCIA SIGNO ENTERO .....................................................................................................8
FIGURA A1.9. DÍGITO ENTERO.......................................................................................................................8
FIGURA A1.10. ENTERO CON SIGNO ..............................................................................................................9
FIGURA A1.11. REPETICIÓN (I) ......................................................................................................................9
FIGURA A1.12. REPETICIÓN RECURSIVA (II) ................................................................................................10
FIGURA A1.13. PRODUCCIÓN RECURSIVA....................................................................................................10
FIGURA A1.14. SIMPLIFICACIÓN A NO RECURSIVA. .....................................................................................11
FIGURA A1.15. DERIVACIÓN .......................................................................................................................11
FIGURA A1.16. DERIVACIÓN .......................................................................................................................12
FIGURA A1.17. REGLAS DE PRECEDENCIA Y ASOCIATIVIDAD. .....................................................................14
FIGURA A1.18. SINTAXIS DE NÚMERO .........................................................................................................18
FIGURA A1.19. SINTAXIS DE IDENTIFICADOR ..............................................................................................18

Profesor Leopoldo Silva Bijit 26-05-2008


1

Apéndice 2

Introducción al lenguaje C.

Al inicio se efectúa un breve repaso del lenguaje. A continuación se expone con mayor detalle
los tipos básicos y su manipulación; conceptualizando en el diseño de macros y funciones. Más
adelante se profundiza en la interfaz de entrada salida, y en el diseño de rutinas matemáticas.

1. Funciones.

1.1. Abstracción de acciones y expresiones.

Una función es una abstracción de una expresión.

Su objetivo es la realización de un grupo de acciones, que se reconocen por el nombre de la


función.

De este modo si el lenguaje no dispone de una determinada acción que se desee, se la puede
desarrollar como un grupo de las instrucciones que el lenguaje ya posee, e invocar la realización
de esta nueva acción por su nombre.

Luego de esto puede seguir construyéndose nuevas funciones empleando las ya definidas, en el
mismo sentido que ya Euclides desenvolvió para el desarrollo de la geometría a través de sus
Elementos.

De esta forma un gran programa puede estudiarse como un conjunto de funciones.


Y el desarrollo del programa como el diseño de las funciones individuales.

Si bien el lenguaje C tiene acciones muy primitivas, tiene una biblioteca (en inglés: library) muy
amplia. Las acciones básicas deben ser simples ya que el objetivo del lenguaje es lograr una
compilación eficiente; es decir, que la traducción a assembler sea siempre posible y eficiente
con los repertorios clásicos de los procesadores.

La biblioteca es un conjunto de funciones desarrolladas por especialistas y hábiles


programadores que suelen estar compiladas. Cuando se dispone de los programas fuentes de la
biblioteca se tiene un excelente material para aprender a programar.

Un ejemplo de la biblioteca estándar es la de entrada salida. El programador, por ejemplo,


puede desplegar un resultado en la salida mediante una invocación a la función printf.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
#include <stdio.h>
int x;

printf(" x= %d \n", x);

El nombre de la función recuerda que imprime con formato; es decir con determinado patrón
que se establece mediante el string de formato, que es el primer argumento de la función. En el
ejemplo mueve hacia la salida todos los caracteres del string, hasta encontrar un %. Luego de
acuerdo a la letra, después de cada signo % determina cómo imprimir el resto de los
argumentos. En este caso imprime el valor del entero almacenado en x, en forma decimal (por la
letra d).

1.2. Prototipo, definición, invocación.

Lo que se necesita para emplear una función de la biblioteca es conocer sus argumentos y el tipo
del valor de retorno, y la función que realiza. Esta especificación se conoce como el prototipo
de la función.

El conjunto de instrucciones que forman la función se denomina definición de la función.

La acción de usar la función, se conoce como invocación, y se hace de tal modo que los
argumentos actuales deben estar en correspondencia de número, tipo y orden de ocurrencia con
los parámetros o argumentos formales que tiene la definición de la función.
Tomemos un ejemplo del cálculo. Y calculemos la función: f(x) = 3 x2 +5

Entonces la definición, muestra: un argumento real y el tipo del valor de retorno, que también es
real.

float f ( float x) /* definición */


{
return ( 3*x*x + 5);
}

Luego de definida la función, se la puede invocar; es decir, si la invocación está más adelante,
en el texto del programa, que su definición.

float w, y;
y = 4.2;
w = f(y -3.0) + 3.3*y ; /* invocación */

Nótese que el argumento actual debe ser una expresión que tome un valor de tipo float. Y que
el valor de tipo float, retornado por la función puede emplearse, también dentro de una
expresión.

Antes de invocar, se calcula el valor del argumento actual (en este caso: 4.2-3.0) y con este
valor se inicializa la variable x (argumento actual) en el espacio de memoria asociado a la
función. Dentro del cuerpo de acciones de la función se realizan los cálculos y mediante la

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 3
palabra return, se devuelve el valor calculado. Este valor reemplaza a la función en la expresión
en que es invocada.

Si se desea invocar a la función con un argumento entero, debe especificarse una conversión
explícita de tipo, mediante un cast.

int j=0;
j = 5; w = f( (float) j*2 )

Si la invocación se realiza antes, en el texto del programa, que su definición, es preciso emplear
un prototipo de la función antes de invocarla. Nótese el punto y coma que termina el prototipo.

float f( float) ; /* prototipo */

Cuando se desea diseñar una función que sólo sea una agrupación de acciones, se define su
retorno vacío (void). También si no tiene argumentos, debe especificarse void, en el lugar de
los argumentos.

void asterisco(void)
{ putchar('*'); }

Como esta función usa una función de biblioteca, antes de definirla se debe incluir <stdio.h>
que contiene el prototipo de putchar.

Para invocarla:
asterisco( );

Una función con retorno vacío es una abstracción de procedimiento o acción y no de expresión.

1.3. Alcances del lenguaje C.

Parte importante de la habilidad que debe desarrollar un programador consiste en la forma en


que diseña las funciones que le permitirán resolver un determinado problema. Algunos
programadores generan sus propias bibliotecas, que pueden reutilizar en otros proyectos.

Por otro lado cuando la magnitud del problema es mayor, y deben resolverlo mediante un
equipo de programadores, la especificación de las funciones permite establecer la división del
trabajo. Todos deben conocer los prototipos.
Este modelo de programación considera las funciones como operaciones sobre los datos.
También es preciso que los miembros del equipo conozcan las estructuras de datos que
manipularán las funciones.

Sin embargo, el modelo anterior, aparentemente consistente, no se comportó bien en el tiempo.


En la actualidad se conciben las estructuras de datos y las acciones que las manipulan como un
todo, denominado objeto.
Si una persona desea aprender a programar es recomendable que se inicie con un lenguaje
orientado a objetos.

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos

La exposición del lenguaje C sigue empleándose como una descripción abstracta de las
capacidades de un procesador.

1.4. Paso de argumentos por valor.

Entonces, las funciones en C, tienen un diseño muy limitado. Sólo se le pueden pasar los
valores de los argumentos, y sólo se dispone de un valor de retorno.

Esto es muy limitado, ya que por ejemplo si el problema se puede modelar con vectores o
matrices, se requiere un vector o matriz de retorno. La función sólo puede retornar un valor de
un tipo determinado.

En ANSI C, se decidió permitir el retorno de una estructura. Lo cual permite retornar todos los
miembros de ésta.

El uso de punteros, que modelan las capacidades de direccionamiento indirecto o en base a


registros, permite pasar valores de punteros como argumentos y también retornar un valor de un
puntero. Posibilitando lo que se denomina paso por referencia y también el retorno de una
variable que apunta a una zona de la memoria donde pueden ubicarse múltiples valores de
retorno.

1.5. Paso por referencia.

Veamos un ejemplo de paso por referencia.

Deseamos incrementar en uno dos variables, en una sola operación, que identifican contadores.
int cnt1=0, cnt2=0, cnt=0;

La función:

int cnt( int i)


{ return (i++); }

sólo puede incrementar un contador a la vez.

Para incrementar dos contadores, se requiere:


cnt1=cnt(cnt1); cnt2=cnt(cnt2); /* cnt1++, cnt2++; */

El diseño:
void cnt( int * c1, int *c2)
{ (*c1)++; (*c2)++}

nos permite incrementar dos contadores:


cnt( &cnt1, &cnt2);

o bien cnt(&cnt1, &cnt3);

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 5

Al invocar se pasan los valores de las direcciones de las variables; en el cuerpo de la función
mediante indirección se escribe en las variables. Se pasa una referencia a las variables.

1.6. Frame.

Otro concepto relevante en el uso de funciones es el espacio de memoria que ésta ocupa para
almacenar sus variables. Este espacio se denomina frame y logra mantener en direcciones
contiguas de memoria a las variables que puede emplear la función mientras se ejecuta su
código.

Lo que se desea es tener localidad espacial. Es decir que las instrucciones que ejecuta la
función estén en direcciones contiguas de memoria y también las variables que ésta emplee.
Esto permite el empleo eficiente de las memorias caché, actualmente presentes en todos los
diseños actuales de procesadores.

Otra consideración de importancia es que sólo es necesario disponer del frame mientras la
función esté en ejecución, lo cual permite reutilizar las celdas ocupadas.
Las variables locales, las definidas dentro del cuerpo de acciones de la función, se guardan en el
frame. Los argumentos también se guardan en el frame. Si almacenamos en el frame la
dirección de la instrucción a la que debe retornarse, luego de ejecutada la función, podremos
invocar a funciones dentro de una función.

La organización anterior permite diseñar funciones recursivas; es decir, funciones que se


llamen a sí mismas. Cada vez que se llame a una función se crea un frame.
También se denomina diseño de funciones reentrantes a las que pueden ser llamadas de
diferentes partes y conservar su propio espacio de variables (en el frame).

Entonces, antes de llamar a una función se introducen en el frame, los valores de los argumentos
actuales en celdas contiguas de memoria; luego se introduce la dirección de retorno, y
finalmente se llama a la función. El código de la función crea el espacio para las variables
locales en el frame, en celdas contiguas. Antes de salir de la función, se retorna el valor; luego
se recupera la dirección de retorno y finalmente se desarma el frame.

Los procesadores mantienen en dos registros especiales la dirección de la próxima instrucción a


ejecutar (program counter PC) y la dirección de la última celda ocupada por el frame (stack
pointer SP).

Con esta disciplina el compilador lo único que requiere para traducir a código de máquina el
texto de una función es su prototipo. Ya que debe empujar dentro del frame los valores de los
argumentos, a su vez conoce ahora a los argumentos por los desplazamientos de éstos relativos
al stack pointer. Empleando mecanismos de direccionamiento indirecto o relativos a registros
puede leer y escribir en el espacio asignado a los argumentos. Lo mismo puede decirse de la
forma en que puede leer o escribir en las variables locales.

Sin embargo dotar a un lenguaje con la capacidad de invocar a funciones tiene un costo. Se
requieren varias instrucciones para crear el espacio, copiar los valores de los argumentos, iniciar

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos
las variables locales, y luego otra serie de instrucciones para desarmar el frame. Además,
obviamente, toda esta actividad requiere un tiempo para su realización.

No es razonable crear funciones cuyo código sea menor o similar al número de instrucciones
requeridas para administrar el frame. En estos casos se emplean macros; los cuales permiten la
abstracción de llamar por un nombre, sin el costo del frame.

Ojalá las cosas fueran simples....


Sin embargo este esquema que se ve simple y consistente, no es eficiente. Los accesos a
memoria siguen siendo el freno en la ejecución de programas. Las nuevas arquitecturas han
introducido máquinas con un número elevado de registros, los cuales pueden ser leídos y
escritos en menos tiempo que las celdas de las memorias caché.

El programador en assembler puede organizar sus funciones de tal modo de emplear


preferentemente registros. Lo cual logra funciones más rápidas.

Los compiladores actuales para un procesador determinado tienen una política para el uso de los
registros; intentan pasar un número fijo de argumentos y retornar un número fijo de valores en
registros, también intentan emplear registros para las variables locales. Y sólo emplean el frame
para los argumentos que no puedan almacenarse en registros.

Más aún: clasifican los registros en temporales y salvables. Usan indiscriminadamente los
temporales, para desarrollar el cuerpo de la función, y los salvables los emplean para las
variables locales. Y sólo salvan en el stack los valores de los registros salvables que sean
modificados por la función.

1.7. Algunos conceptos básicos

Como en cualquier lenguaje existe un aspecto léxico (vocabulario, las palabras que se pueden
escribir); un aspecto sintáctico (reglas gramaticales, para escribir correctamente); y finalmente
un aspecto semántico (que asigna un significado a las construcciones).

Un lenguaje de programación permite escribir programas que instruyen al computador sobre el


conjunto organizado de acciones (algoritmo) que deben efectuarse sobre los datos.
Las acciones y los datos deben describirse con rigurosidad para que puedan ser correctamente
interpretados por un autómata.
1.7.1. Datos.
Los datos básicos tienen un tipo asociado.
El tipo establece el conjunto de valores que puede tomar un dato de ese tipo y las acciones que
pueden efectuarse sobre valores de ese tipo.

Los tipos básicos son: enteros, reales, carácter y strings (secuencia de caracteres).
Enteros con signo.
En C, los enteros se representan en una palabra de la memoria, y sus valores dependen del
ancho de palabra que tenga la memoria (en la actualidad, 16 ó 32 bits)

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 7
Se emplea representación complemento a dos para representar números con signo.

Suponiendo un largo de palabra de la memoria de 3 bits, para simplificar la explicación:

Representación Equivalente Decimal Equivalente Decimal Entero sin


interna Complemento a dos. Complemento a uno. Signo
000 +0 +0 0
001 +1 +1 1
010 +2 +2 2
011 +3 +3 3
100 -4 -3 4
101 -3 -2 5
110 -2 -1 6
111 -1 -0 7

Para obtener un número en complemento a uno, basta cambiar sus unos por ceros y sus ceros
por unos. Si tenemos: 010 (el decimal 2) su complemento uno es 101 (con equivalente decimal
menos dos).

El complemento a dos es el complemento a uno más uno. (Sumarle uno en binario)

Si tenemos: 010, su complemento 1 es 101 y al sumarle uno queda:


101
+ 1
____
110
Nótese que en complemento a uno existen dos ceros; pero el rango de representación es
simétrico.
En complemento a dos, existe un cero, pero con rango de representación asimétrico.

Para 16 bits, en C-2, el rango de representación de enteros con signo es desde -(2^15) hasta
+(2^15 )-1
Lo cual equivale al intervalo desde –32.768 hasta +32.767

Definiciones de Datos.
a) Para declarar una variable entera, por ejemplo, la variable i como entera, se anota:

int i;

Esto reserva una palabra de la memoria y la reconoce con el nombre simbólico i.


Si se escribe: i = 5;

Se guarda, en representación interna: 0000.0000.0000.0101 en la dirección i, si el largo de la


palabra es 16 bits. Y 0000.0000.0000.0000.0000.0000.0000.0101 si es de 32 bits. Los
separadores cada cuatro cifras binarias son virtuales, permiten leer el número en hexadecimal.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
Más sencillamente se puede decir que quedó guardado un número entero 5 en la variable i.

Si, más adelante, se efectúa: i = i +1; quedará almacenado 6.


( en C se puede anotar: i++)

Pero si i tiene almacenado 32767 y se suma uno a i, quedará almacenado: -32768.


Cuestión que no debería sorprender, si se recuerda que se representan los números en
complemento a dos, y la aritmética es modular (en C no suele haber indicación de rebalse).

b) Si deseamos definir, dos variables enteras, i y j como enteros, puede anotarse:


int i;
int j;

más sencillamente, puede emplearse una lista:


int i, j;

c) También puede definirse y autoiniciarse con un valor:


int i = 5 ; /* define e inicia i */
Enteros sin signo.
unsigned int i = 3500u; /* 0 < i < 65535(en 16 bits) . Puede anotarse 3500U*/

Enteros Largos.
Ocupan el doble de largo que un entero común.

long int j = 123L;


/*Note la l o L, que se agrega al final, para especificar que el valor es entero largo. */

Rango en 16 bits. long int: -2.147.483.648 hasta +2.147.483.647


Largos sin signo.
unsigned long int i = 123ul; /*se agrega ul o UL Máximo 4.294.967.295 en 32 bits*/

Números Reales. ( float )


Representan números reales con una mantisa y un exponente.
Rango de representación entre 3.4e-38 hasta 3.4e+38 para el flotante de precisión simple.
Sólo se tienen 6 cifras decimales significativas, y se pueden escribir en formato fijo (123.456) o
bien en formato exponencial ( 1.23456e+2).

float x; /* define flotante */

double y; /*Define un real en doble precisión. (10 cifras decimales de precisión).*/

long double z; /* real de precisión extendida. */

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 9
Carácter.
Es un tipo adecuado para representar un carácter o símbolo alfanumérico (los que pueden
digitarse en un teclado).

La definición:

char ch = ‟a‟ ; /* define e inicia ch con el valor ASCII del símbolo a.*/

‟ ‟ el espacio equivale a 0x20


‟0‟ equivale a 0x30 ; el ‟1‟ a 0x31, ….
‟A‟ equivale a 0x41; ‟B‟ a 0x42; ….
‟a‟ equivale a 0x61: ….

Los caracteres toman valores enteros, y se definen entre comillas simples.


Por ejemplo:
int x = 0;
x= ‟1‟ - 1 ; /* toma valor 0x30 ; es decir 48 decimal. */

Algunos valores de caracteres no imprimibles:


„\n‟ es la forma de representar la función de newline. (El cursor se posiciona en el inicio de la
línea siguiente.
„\t‟ su envío hacia la salida produce una tabulación.
„\b‟ hace sonar una señal audible ( bell )

Strings.
Se los define como un arreglo de caracteres. La definición del arreglo, se indica con el valor
entero de las componentes entre paréntesis de tipo corchete.

Char a[10]; /*crea espacio para ingresar 9 caracteres. */


La última componente del arreglo, a[9] se inicia con valor nulo.

También se puede definir e iniciar: char a[ ] = “ este es un string “;


Se emplea una dimensión muda del arreglo, y ésta se ajusta automáticamente al largo de la
secuencia de caracteres encerrados entre comillas dobles.
La forma más empleada por los programadores en C, es mediante un puntero a carácter:
char *a = “ este es un string “;

En la representación interna se agrega un „\0‟ al final. (equivale a 0x00, y se denomina carácter


nulo)

1.7.2. Acciones.
La manera básica de organizar las acciones es hacerlo en una de las tres siguientes formas:
Secuencia.

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
Se realiza primero una acción y luego la siguiente:
Se anota:
{ acción1 ; acción2; }

Alternativa.
Si la condición es verdadera se realiza la acción1; si es falsa: la acción2.
Se anota:
if ( condición ) accion1; else acción2;

Repetición.
Mientras la condición sea verdadera se repite la acción.
Se anota:
while (condición ) acción;

Se ha comprobado que cualquier diagrama de flujo puede ser representado usando las tres
construcciones básicas anteriores. Una programación que emplee estos principios se denomina
estructurada. Permite leer un programa sin tener que volver hacia atrás.
For.
int i=10;
for (i=10; i>0 ; i--) putchar( ‟*‟);
putchar( ‟\n‟);

La construcción del for(inicio; condición; reinicio) ejecuta la expresión de inicio; luego evalúa
la condición, y si es verdadera(valor diferente de cero) ejecuta el bloque asociado; finalmente
evalúa la expresión de reinicio y vuelve a evaluar la condición y así sucesivamente hasta que
ésta es falsa(valor cero), situación en que termina el for.

Abstracción.
Una acción adicional es poder invocar a una función.
Como se verá una función permite agrupar a un número de instrucciones bajo un nombre.
Dotándola de una interfaz con el resto del programa, pasándole argumentos como valores de
entrada y tomando la función un valor de retorno.
Dado un problema, encontrar las funciones que permitan resolverlo es fundamental en
programación.

1.7.3. Entrada. Salida.


En el lenguaje C, no existen acciones de entrada y salida. Estas están desarrolladas como
invocaciones a funciones de biblioteca.

printf(“Escribe este texto hacia la salida estándar”);


printf(“\nEscribe este texto hacia la salida estándar”); /*avanza línea y escribe el texto*/

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 11
printf(“Escribe este texto hacia la salida estándar\n”); /*escribe el texto y avanza línea*/

Si i es de tipo entero:
printf(“%d”,i); /*escribe el entero en decimal*/
printf(“%o”,i); /*escribe el entero en octal*/
printf(“%x”,i); /*escribe el entero en hexadecimal*/
printf(“%6d”,i); /*escribe el entero en decimal, con largo de campo igual a 6*/
printf(“abcd%defgh”, i); /*escribe string abcd, el entero en decimal, y el string efgh*/

scanf(“%d”,&i) /*espera leer un entero en decimal, y lo almacena en variable i */


Debe notarse el empleo de & antes de la variable en la que se está depositando el valor.
Ejemplos.
#include <stdio.h>
int main(void)
{ int i,j,k; i=4; j=8; k=i*j;
printf("%d%d%d", i, j, k); /* en decimal */
printf("\n");
printf("i = %d j = %d k = %d\n", i, j, k); /* con string intercalados */
printf("-> i = %d(decimal) j = %o(octal) k = %x(hexadecimal)\n",i,j,k);
printf("--> i = %d j = %d k = %d\n", i, j, k);
return(0);
}

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos

2. Tipo char.

2.1. Valores.

Primero describiremos los valores que pueden tomar los elementos de tipo char.

Es un tipo básico del lenguaje. Las variables y constantes de tipo char ocupan un byte.
El tipo unsigned char tiene el rango 0 a 255.
El tipo char (o signed char, esto es por defecto) tiene el rango –128 a 127.
A las variables de tipo char se las puede tratar como si fueran de tipo entero, ya que son
convertidas automáticamente a ese tipo cuando aparecen en expresiones.

Una constante de tipo carácter se define como un carácter encerrado entre comillas simples. El
valor de una constante de tipo carácter es el valor numérico de ese carácter en la tabla o código
de caracteres. En la actualidad la tabla más empleada es el código ASCII.

2.1. Definición de variables y constantes de tipo char.

char ch; declara una variable de tipo char.


ch = „c‟; asigna la constante de tipo carácter c a la variable ch.

2.2. Caracteres ASCII.

ASCII son las iniciales de American Standard Code for Information Interchange.
Internamente, un carácter, se representa por una secuencia binaria de 8 bits. Un valor
perteneciente al código ASCII es la representación numérica de un carácter como '1' o '@' o de
una acción de control.

Se tienen 32 caracteres de control, que no son imprimibles o visualizables. En general puede


especificarse un carácter por su valor numérico equivalente expresado en octal, mediante '\ooo'
donde una, dos o las tres o deben ser reemplazadas por un dígito octal (dígitos entre 0 y 7). La
secuencia binaria de 8 unos seguidos, equivale a 377 en octal.

Alternativamente pueden emplearse dos cifras hexadecimales para representar un carácter, del
siguiente modo: '\xhh' La x indica que uno o los dos dígitos siguientes deben ser reemplazados
por una cifra hexadecimal (dígitos 0 a 9, y las letras A, B, C, D, F). La secuencia binaria de 8
unos seguidos, equivale a FF en hexadecimal.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 13
H D H D H D H D H D H D H D H D
00 NULL 00 10 DEL 16 20 32 30 0 48 40 @ 64 50 P 80 60 ` 96 70 p 112
01 SOH 01 11 DC1 17 21 ! 33 31 1 49 41 A 65 51 Q 81 61 a 97 71 q 113
02 STX 02 12 DC2 18 22 " 34 32 2 50 42 B 66 52 R 82 62 b 98 72 r 114
03 EXT 03 13 DC3 19 23 # 35 33 3 51 43 C 67 53 S 83 63 c 99 73 s 115
04 EOT 04 14 DC4 20 24 $ 36 34 4 52 44 D 68 54 T 84 64 d 100 74 t 116
05 ENQ 05 15 NAK 21 25 % 37 35 5 53 45 E 69 55 U 85 65 e 101 75 u 117
06 ACK 06 16 SYN 22 26 & 38 36 6 54 46 F 70 56 V 86 66 f 102 76 v 118
07 BEL 07 17 ETB 23 27 ' 39 37 7 55 47 G 71 57 W 87 67 g 103 77 w 119
08 BS 08 18 CAN 24 28 ( 40 38 8 56 48 H 72 58 X 88 68 h 104 78 x 120
09 TAB 09 19 EM 25 29 ) 41 39 9 57 49 I 73 59 Y 89 69 i 105 79 y 121
0a LF 10 1a SUB 26 2a * 42 3a : 58 4a J 74 5a Z 90 6a j 106 7a z 122
0b VT 11 1b ESC 27 2b + 43 3b ; 59 4b K 75 5b [ 91 6b k 107 7b { 123
0c FF 12 1c FS 28 2c , 44 3c < 60 4c L 76 5c \ 92 6c l 108 7c | 124
0d CR 13 1d GS 29 2d - 45 3d = 61 4d M 77 5d ] 93 6d m 109 7d } 125
0e SO 14 1e RS 30 2e . 46 3e > 62 4e N 78 5e ^ 94 6e n 110 7e ~ 126
0f SI 15 1f US 31 2f / 47 3f ? 63 4f O 79 5f _ 95 6f o 111 7f del 127

Figura A2.1. Tabla ASCCI.

Todos los valores de la tabla anterior son positivos, si se representan mediante un byte, ya que el
bit más significativo es cero.

Los caracteres que representan los dígitos decimales tienen valores asociados menores que las
letras; y si se les resta 0x30, los cuatro bits menos significativos representan a los dígitos
decimales en BCD (Binary Coded Decimal). Las letras mayúsculas tienen códigos crecientes en
orden alfabético, y son menores en 0x20 que las letras minúsculas.

En español suelen emplearse los siguientes caracteres, que se anteceden por su equivalente
decimal: 130 é, 144 É, 154 Ü, 160 á, 161 í, 162 ó, 163 ú, 164 ñ, 165 Ñ, 168 ¿, 173 ¡.
Los valores de éstos tienen el octavo bit (el más significativo en uno), y forman parte de los 128
caracteres que conforman un código ASCII extendido.

Los caracteres de control han sido designados por tres letras que son las primeras del significado
de la acción que tradicionalmente e históricamente se les ha asociado.

Por ejemplo el carácter FF (Form Feed) con valor 0x0c, se lo emplea para enviar a impresoras, y
que éstas lo interpreten con la acción de avanzar el papel hasta el inicio de una nueva página
(esto en impresoras que son alimentadas por formularios continuos).

Los teclados pueden generar caracteres de control (oprimiendo la tecla control y una letra). Por
ejemplo ctrl-S y ctrl-Q generan DC3 y DC1 (también son conocidos por X-on y Xoff), y han
sido usados para detener y reanudar largas salidas de texto por la pantalla de los terminales).
Varios de los caracteres se han usado en protocolos de comunicación, otros para controlar
modems.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
2.3. Secuencias de escape.

Algunos de los caracteres, debido a su frecuente uso, tienen una representación por secuencias
de escape. Se escriben como dos caracteres, pero representan el valor de uno de control. Los
más usados son:

\n representa a nueva línea (new line o line feed).


En Unix esto genera un carácter de control, en archivos de texto en PC, se generan dos: 0x0D
seguido de 0x0A.
\t tabulador horizontal.
\0 Nul representa el carácter con valor cero. El que se emplea como terminador de string.

Si se representan como constantes de tipo char, se definen entre comillas simples.


Por ejemplo:
#define EOS '\0' /* End of string */

Estas secuencias de escape pueden incorporarse dentro de strings. El \n (backslash n) suele


aparecer en el string de control de printf, para denotar que cuando se lo encuentre, debe
cambiarse de línea en el medio de salida.

Dentro de un string, suelen emplearse las siguientes secuencias para representar los caracteres ",
', \. Que no podrían ser usados ya que delimitan strings o caracteres o son parte de la secuencia
de escape.
\\ para representar la diagonal invertida
\" para representar la comilla doble, dentro del string.
\' para representar la comilla simple dentro del string.

Ejemplo:
Char esc = '\\';
"O\'Higgins" en un string.

2.4. Archivos de texto y binarios.

El siguiente texto,
se representa internamente
según:

45 6C 20 73 69 67 75 69 65 6E 74 65 20 74 65 78 74 6F 2C 20 0D 0A
73 65 20 72 65 70 72 65 73 65 6E 74 61 20 69 6E 74 65 72 6E 61 6D 65 6E 74 65 20 0D 0A
73 65 67 FA 6E 3A 0D 0A

La representación hexadecimal de los caracteres que forman el texto, muestra los dos caracteres
de control que representan el fin de línea (0x0D seguido de 0x0A). Cada carácter gráfico es
representado por su valor numérico hexadecimal. La primera representación (externa) se emplea
para desplegar la información en pantallas e impresoras; la segunda es una representación
interna (se suele decir binaria, pero representada en hexadecimal) y se emplea para almacenar

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 15
en memoria o en medios magnéticos u ópticos. Normalmente existen comandos (type, cat) y
programas (notepad, word) para desplegar archivos de texto.

2.5. Expresiones.

Un carácter en una expresión es automáticamente convertido a entero.

Así entonces la construcción de expresiones que involucren variables o constantes de tipo


carácter son similares a las que pueden plantearse para enteros. Sin embargo las construcciones
más frecuentes son las comparaciones.

La especificación de tipo char es signed char.

Puede verificarse cómo son tratados los enteros con signo negativo por el compilador que se
está empleando, observando los resultados de: printf(" %c\n",-23); Debe producir la letra
acentuada: é.

Un compilador moderno también debería imprimir las letras acentuadas, por ejemplo:
printf(" %c\n",'é');

El siguiente par de for anidados muestra 8 renglones de 16 caracteres cada uno, con los
caracteres que tienen valores negativos (el bit más significativo del byte es uno).

for (i = -128; i<0; i++)


{ for (j = 0; j<16; j++) printf("%c ", i++); putchar('\n'); }

Observando la salida, la que dependerá del compilador empleado, pueden comprobarse los
caracteres (con valores negativos) que serán representados gráficamente.

Cuando se desea tratar el contenido de un byte (independiente del valor gráfico) debe emplearse
unsigned char.

Si se desea obtener el entero i que es representado por un carácter c, conviene emplear:


i = c –'0'; en lugar de: i = c - 48; o i = c - 0x30; ya que el código generado resulta
independiente del set de caracteres (ASCII, EBCDIC). La primera expresión asume que los
valores de los caracteres, que representan dígitos decimales, están asociados a enteros
consecutivos y ordenados en forma ascendente.

Es preferible escribir: i = (int) (c – '0'); que destaca que se está efectuando un conversión de
tipos. Pero lo anterior no suele encontrarse en textos escritos por programadores
experimentados.

Si se desea comparar si el carácter c es igual a una determinada constante, conviene la


expresión: (c == 'b') en lugar de (c == 98).

La expresión ('a' - 'A') toma valor (97 – 65) = 32. Valor que expresado en binario es:
00100000 y en hexadecimal 0x20.

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

Si con esta máscara se efectúa un or con una variable c de tipo carácter: c | ('a' - 'A')
la expresión resultante queda con el bit en la quinta posición en uno(esto conviniendo, como es
usual, que el bit menos significativo ocupa la posición cero, el más derechista).
La expresión: c |= ('a' - 'A') si c es una letra la convierte en letra minúscula.

La expresión: c & ~ ('a' - 'A') forma un and con la máscara binaria 11011111 y la expresión
resultante deja un cero en el bit en la quinta posición.

La palabra máscara recuerda algo que se pone delante del rostro, y en este caso es una buena
imagen de la operación que realiza. Nótese que con un or con una máscara pueden setearse
determinadas posiciones con unos; y con un and, con una máscara, pueden dejarse determinados
bits en cero.

Las expresiones anteriores pueden emplearse para convertir letras minúsculas a mayúsculas y
viceversa.

for (c='a'; c<='z'; c++) { bloque }


permite ejecutar repetidamente un bloque de acciones, variando c desde la a hasta la z cada vez
que se realiza el bloque.
Esto asume que las letras recorridas en orden alfabético, están ordenadas en una secuencia
numérica ascendente.
En la parte de incremento del for, la conversión automática a entero no requiere un casteo
explícito: ( (int) c )++

La condición:
(c != ' ' && c != '\t' && c!= '\n')
es verdadera(toma valor 1) si c no es un separador.

Por De Morgan, la condición:


(c == ' ' || c == '\t' || c== '\n') es verdadera si c es separador.

Más adelante se ilustran otros ejemplos de expresiones de tipo char.

2.6. Entrada-Salida

En el lenguaje Pascal la entrada y salida son acciones. En C, son invocaciones a funciones o


macros de la biblioteca estándar.
Las siguientes descripciones son abstracciones (simplificaciones) del código que efectivamente
se emplea.

int putchar (int c)


{ return putc (c, stdout); }

Convierte c a unsigned char y lo escribe en la salida estándar (stdout); retorna el carácter escrito,
o EOF, en caso de error.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 17
int getchar (void)
{ return getc (stdin); }

Lee desde la entrada estándar el siguiente carácter como unsigned char y lo retorna convertido a
entero; si encuentra el fin de la entrada o un error retorna EOF.

La entrada estándar normalmente es el teclado, y la salida la pantalla. Sin embargo pueden


redirigirse la entrada y la salida desde o hacia archivos.

El valor de la constante EOF predefinida en <stdio.h> es un valor entero, distinto a los valores
de los caracteres que pueden desplegarse en la salida, suele ser –1. EOF recuerda a end of file
(debió ser fin del stream o flujo de caracteres). Por esta razón getchar, retorna entero, ya que
también debe detectar el EOF.

La condición: ( (c = getchar( ) ) != EOF ) con c de tipo int.

obtiene un carácter y lo asigna a c (la asignación es una expresión que toma el valor del lado
izquierdo); este valor es comparado con el EOF, si son diferentes, la condición toma valor 1,
que se interpreta como valor verdadero. Los paréntesis son obligatorios debido a que la
precedencia de != es mayor que la del operador =.

Sin éstos, la interpretación sería:


c = (getchar( ) != EOF), lo cual asigna a c sólo valores 0 ó 1.

Para copiar el flujo de entrada hacia la salida, puede escribirse:


while ((c = getchar( )) != EOF) putchar(c);

Para contar en n las veces que se presenta el carácter t, puede escribirse:


while ((c = getchar( )) != EOF) if (c == t ) n++;

Entrada y salida con formato.


La siguiente función ilustra el uso de printf para caracteres:

void print_char (unsigned char c)


{
if (isgraph(c)) printf("'%c'\n", c); else printf("'\\%.3o'\n", c);
}

Dentro del string de control del printf, una especificación de conversión se inicia con el carácter
% y termina con un carácter. El carácter c indica que una variable de tipo int o char se imprimirá
como un carácter. Nótese que los caracteres que están antes y después de la especificación de
conversión se imprimen de acuerdo a su significado.

Los siguientes valores del argumento actual son imprimibles (isgraph retorna verdadero).
print_char(0x40); imprime en una línea: '@'
print_char(65); imprime en una línea: 'A'

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos

Los siguientes valores del argumento actual se imprimen como tres cifras octales.
print_char(06); imprime en una línea: '\006'
print_char(0x15); imprime en una línea: '\025'

Entre el % y el carácter de conversión a octal o, pueden encontrarse otros especificadores:


- especifica ajuste a la izquierda.
n.m donde n es el número del ancho mínimo del campo y m el número máximo de caracteres
que será impreso. Si el campo es más ancho, se rellena con espacios.

Si el string de control del printf que se ejecuta asociado al else se modifica a:


"'\\x%.2x'\n" Se pasa hacia la salida '\x y luego dos caracteres hexadecimales
debido al carácter de conversión x.
Con esta modificación:
print_char( '\n'); imprime en una línea: '\x0a'
print_char(0x15); imprime en una línea: '\x15'

Debe notarse que el ancho y el máximo número de caracteres se refieren a la variable o


expresión que será convertida.
printf("'%4.1c'\n", 'A' +1) imprime ' B'
printf("'%-4.1c'\n",'3' -1) imprime '2 '

2.7. Funciones.

Las siguientes funciones transforman letras minúsculas a mayúsculas y viceversa.

char toupper(register int c)


{
if((char)c <= 'z' && (char)c >= 'a') c &= ~('a' - 'A');
return (char)c;
}

char tolower(int c)
{
if((char)c <= 'Z' && (char)c >= 'A') c |= ('a' - 'A');
return (char)c;
}

La siguiente es una función que retorna verdadero si el carácter es imprimible:


int isgraph (int c)
{
return((unsigned char)c >= ' ' && (unsigned char)c <= '~');
}

La siguiente es una función que retorna verdadero si el carácter es un dígito decimal:

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 19
int isdig(int c)
{
return((unsigned char)c >= '0' && (unsigned char)c <= '9');
}

La función recursiva printd imprime un número decimal.

Primero el signo, luego la cifra más significativa. Lo logra reinvocando a la función con un
argumento que trunca, mediante división entera la última cifra. De este modo la primera función
(de las múltiples encarnaciones) que termina, es la que tiene como argumento n a la cifra más
significativa, que es menor que 10, imprimiendo dicho valor, a través de la conversión de entero
a carácter. Es necesario tomar módulo 10, para imprimir las cifras siguientes.

Igual resultado se logra con: printf("%d", n).

void printd(int n)
{ if (n<0) putchar('-') n= -n;
if(n/10) printd(n/10);
putchar(n % 10 + '0');
}

La función prtbin imprime en binario un entero de 16 bits.


void prtbin(int i)
{ int j;
for (j=15; j>=0; j--) if(i &(1<<j) ) putchar('1'); else putchar('0');
}

La función prtstr imprime un string. Igual resultado se logra con printf("%s", s).
void prtstr(char *s)
{ while(*s) putchar(*s++); }

La traducción a assembler de putchar ocupa alrededor de 10 instrucciones, y la utilización de


printf emplea algunos cientos de instrucciones. Algunas de las rutinas anteriores pueden ser
útiles cuando no se dispone de una gran cantidad de memoria, como en el caso de
microcontroladores.

2.8. Macros.

Es un mecanismo que permite el reemplazo de símbolos en el texto fuente. Lo efectúa el


preprocesador antes de iniciar la compilación.

Pueden ser con y sin argumentos formales.


Cuando no se emplean argumentos, permite asignar un valor a una constante. Esto puede
emplearse para mejorar la legibilidad de un programa.

Se escriben:
#define <token> <string>

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos

Todas las ocurrencias del identificador <token> en el texto fuente serán reemplazadas por el
texto definido por <string>. Nótese que no hay signo igual, y que no se termina con punto y
coma.

También las emplea el sistema para representar convenientemente y en forma estándar algunos
valores.
Por ejemplo en limits.h figuran entre otras definiciones, las siguientes:
#define CHAR_BIT 8
#define SCHAR_MAX 127
#define SCHAR_MIN (-128)
#define UCHAR_MAX 255

En float.h, se definen entre otras:


#define DBL_MIN 2.2250738585072014E-308
#define FLT_MIN 1.17549435E-38F

En ctype.h, se encuentran entre otras:


#define _IS_SP 1 /* is space */
#define _IS_DIG 2 /* is digit indicator */
#define _IS_UPP 4 /* is upper case */
#define _IS_LOW 8 /* is lower case */
#define _IS_HEX 16 /* [0..9] or [A-F] or [a-f] */
#define _IS_CTL 32 /* Control */
#define _IS_PUN 64 /* punctuation */

En values.h, se encuentran entre otras:


#define MAXINT 0x7FFF
#define MAXLONG 0x7FFFFFFFL
#define MAXDOUBLE 1.797693E+308
#define MAXFLOAT 3.37E+38
#define MINDOUBLE 2.225074E-308

Si se desean emplear constantes predefinidas por el sistema, debe conocerse en cual de los
archivos del directorio include están definidas. Y antes de que sean usadas debe indicarse en una
línea la orden de inclusión, para que el preprocesador incorpore el texto completo de ese archivo
en el texto fuente previo al proceso de compilación.
Por ejemplo:

#include <ctype.h>
Los paréntesis de ángulo indican que el archivo está ubicado en el subdirectorio include. Si se
desea tener archivos definidos por el usuario, el nombre del archivo que debe incluirse debe
estar encerrado por comillas dobles.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 21
2.9. Macros con argumentos.

Se suelen emplear para definir macroinstrucciones (de eso deriva su nombre), es decir una
expresión en base a las acciones primitivas previamente definidas por el lenguaje. La macro se
diferencia de una función en que no incurre en el costo de invocar a una función (crear un frame
con espacio para los argumentos y variables locales en el stack, salvar registros y la dirección de
retorno; y luego recuperar el valor de los registros salvados, desarmar el frame y seguir la
ejecución).

Su real efectividad está limitada a situaciones en las que el código assembler que genera es
pequeño en comparación con el costo de la administración de la función equivalente. O también
cuando se requiere mayor velocidad de ejecución, no importando el tamaño del programa
ejecutable.

#define <macro> ( <arg1>, ... ) <string>

Los identificadores arg1, ... son tratados como parámetros del macro. Todas las instancias de los
argumentos son reemplazadas por el texto definido para arg1,.. cuando se invoca a la macro,
mediante su nombre. Los argumentos se separan por comas.

Por ejemplo:
#define ISLOWER(c) ('a' <= (c) )&& (c) <= 'z')
#define TOUPPER(c) (ISLOWER(c) ? 'A' + ((c) - 'a') : (c))

Si en el texto fuente aparece ISLOWER( 'A') dentro de una expresión, antes de la compilación
el preprocesador cambia el texto anterior por: ('a' <= ('A') && ('A') <= 'z'). El objetivo de esta
macro es devolver un valor verdadero (valor numérico 1) si el carácter c tiene un valor numérico
entre los valores del código asociados al carácter 'a' y al carácter 'z'; en caso contrario, la
expresión lógica toma valor falso (valor numérico 0).

Si aparece TOUPPER('d') éste es reemplazado por el texto:


(('a' <= ('d') && ('d') <= 'z') ? 'A' + (('d') - 'a') : ('d'))
TOUPPER convierte a mayúsculas un carácter ASCII correspondiente a una letra minúscula.

La definición debe estar contenida en una línea. En caso de que el string sea más largo, se
emplea el carácter \ al final de la línea.
#define ctrl(c) ((c) \
< ' ') /* string continua desde línea anterior */

Lo cual es equivalente a:

#define isctrl(c) ((c) < ' ')


Macro que toma valor 1 si el carácter c es de control.

El siguiente macro sólo puede aplicarse si se está seguro que el argumento representa un
carácter que es una letra. Entre 0x41 y 0x51. También da resultado correcto si la letra es
minúscula (entre 0x60 y 0x7a).

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos
#define TOLOWER(c) ( (c) | 0x20)

Otros ejemplos de macros:


#define isascii(c) ( !( (c) &~0x7F))
#define toascii(c) ( (c) & 0x7F)

Nótese que en el string que define el texto que reemplaza al macro, los argumentos se colocan
entre paréntesis.

2.10. Biblioteca. ctype.c

Prototipos en include/ctype.h

El diseño de la biblioteca ctype se efectúa mediante una tabla de búsqueda, en la cual se


emplean macros para clasificar un carácter. La tabla es un arreglo de bytes(unsigned char) en los
que se codifica en cada bit una propiedad del carácter asociado.

El nombre de cada macro pregunta si el carácter es de cierta clase, por ejemplo si es símbolo
alfanumérico el nombre es isalnum. Cada macro retorna un valor diferente de cero en caso de
ser verdadero y cero en caso de ser falso.

Se define el concepto asociado a cada bit.


#define _U 0x01 /* Upper. Mayúsculas */
#define _L 0x02 /* Lower. Minúsculas */
#define _N 0x04 /* Número decimal */
#define _S 0x08 /* Espacio */
#define _P 0x10 /* Puntuación */
#define _C 0x20 /* Control */
#define _X 0x40 /* Hex */

En funciones de biblioteca, se suelen preceder los identificadores por un _(underscore o línea de


subrayado); de este modo se evita el alcance de nombres con identificadores definidos por el
usuario (siempre que éste no emplee como primer símbolo para sus identificadores el subrayado
_).

El siguiente arreglo contiene la información de atributos de cada carácter, indexada por su valor
numérico ascii +1.

const unsigned char _ctype_[129] = {


0, /*retorna falso para EOF */
_C, _C, _C, _C, _C, _C, _C, _C,
_C, _C| _S, _C| _S, _C| _S, _C| _S, _C| _S, _C, _C, /* bs, tab, lf, vt, ff, cr, so, si */
_C, _C, _C, _C, _C, _C, _C, _C,
_C, _C, _C, _C, _C, _C, _C, _C,
_S, _P, _P, _P, _P, _P, _P, _P, /* space, !, ", #, $, %, &, ' */
_P, _P, _P, _P, _P, _P, _P, _P, /* (, ), *, +, , , -, . , / */
_N, _N, _N, _N, _N, _N, _N, _N, /* 0, 1, 2, 3, 4, 5, 6, 7 */

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 23
_N, _N, _P, _P, _P, _P, _P, _P, /* 8, 9, :, ;, <, =, >, ? */
_P, _U|_X, _U|_X, _U|_X, _U|_X, _U|_X, _U|_X, _U, /*@,A,B,C,D,E,F,G */
_U, _U, _U, _U, _U, _U, _U, _U, /* H, I, J, K, L, M, N, O */
_U, _U, _U, _U, _U, _U, _U, _U, /* P, Q, R, S, T, U, V, W */
_U, _U, _U, _P, _P, _P, _P, _P, /* X, Y, Z, [, \, ], [ , ^, _ */
_P, _L|_X, _L|_X, _L|_X, _L|_X, _L|_X, _L|_X, _L, /* `, a, b, c, d, e, f, g */
_L, _L, _L, _L, _L, _L, _L, _L, /* h, i, j, k, l, m, n, o */
_L, _L, _L, _L, _L, _L, _L, _L, /* p, q, r, s, t, u, v, w */
_L, _L, _L, _P, _P, _P, _P, _C /* x, y, z, {, |, }, ~, DEL */
};

Si el valor entero del carácter es –1(EOF), al sumarle 1, resulta índice 0 para la tabla de
búsqueda. Si se buscan los atributos en la entrada 0 del arreglo; se advierte que tiene definido
valor cero, resultando con retornos falsos de los macros para EOF.
Se escoge para EOF un valor numérico diferente a los imprimibles.

isascii está definida para valores enteros. El resto de los macros están definidos sólo cuando
isaccii es verdadero o si c es EOF.

#define isascii(c) ( (unsigned)(c) < 128)


#define iscntrl(c) (_ctype_[(unsigned char) (c) + 1]&_C)
#define isupper(c) (_ctype_[(unsigned char) (c) + 1]&_U)
#define islower(c) (_ctype_[(unsigned char) (c) + 1]&_L)
#define isalpha(c) (_ctype_[(unsigned char) (c) + 1]&(_U | _L))
#define isdigit(c) (_ctype_[(unsigned char) (c) + 1]&_N)
#define isxdigit(c) (_ctype_[(unsigned char) (c) + 1]&(_N | _X))
#define isalnum(c) (_ctype_[(unsigned char) (c) + 1]&(_U | _L | _N))
#define isspace(c) (_ctype_[(unsigned char) (c) + 1]&_S)
#define ispunct(c) (_ctype_[(unsigned char) (c) + 1]&_P)
#define isprint(c) (_ctype_[(unsigned char) (c) + 1]&(_P | _U | _L | _N | _S))
#define isgraph(c) (_ctype_[(unsigned char) (c) + 1]&(_P | _U | _L | _N))

Los caracteres considerados gráficos no contemplan la categoría espacio (_S); pero sí están
considerados en la de imprimibles.
En la categoría espacios se consideran los caracteres de control: tab, lf, vt, ff, cr
Las letras de las cifras hexadecimales se consideran en mayúsculas y minúsculas.

Con el macro siguiente, que pone en uno el bit en posición dada por bit:
# define _ISbit(bit) (1 << (bit))

La definición de atributos puede efectuarse según:


#define _U _ISbit(0) /* Upper. Mayúsculas */
#define _L _ISbit(1) /* Lower. Minúsculas */
#define _N _ISbit(2) /* Número decimal */
#define _S _ISbit(3) /* Espacio */
#define _P _ISbit(4) /* Puntuación */

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
#define _C _ISbit(5) /* Control */
#define _X _ISbit(6) /* Hex */

Los códigos de biblioteca suelen ser más complejos que los ilustrados, ya que consideran la
portabilidad. Por ejemplo si el macro que define un bit en determinada posición de una palabra,
se desea usar en diferente tipo de procesadores, se agrega texto alternativo de acuerdo a la
característica.

Las órdenes de compilación condicional seleccionan, de acuerdo a las condiciones, la parte del
texto fuente que será compilada.

Por ejemplo: Si se desea marcar uno de los bits de una palabra de 16 bits, el macro debe
considerar el orden de los bytes dentro de la palabra.

# if __BYTE_ORDER == __BIG_ENDIAN
# define _ISbit(bit) (1 << (bit))
# else /* __BYTE_ORDER == __LITTLE_ENDIAN */
# define _ISbit(bit) ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))
# endif

3. Strings.

Se describen las rutinas que manipulan strings, cuyos prototipos se encuentran en string.h

#include <string.h>
typedef unsigned size_t; /* define tipo requerido por sizeof */

#define NULL 0 /* terminador nulo */

3.1. Definición de string.

3.1.1. Arreglo de caracteres.


La siguiente definición reserva espacio para un string como un arreglo de caracteres.
La definición de un string como arreglo de caracteres, debe incluir un espacio para el carácter
fin de string (el carácter NULL). Quizás es mejor definir el terminador de string como null, para
evitar confusiones con el valor de un puntero nulo.

char string[6]; /*crea string con espacio para 6 caracteres. Índice varía entre 0 y 5 */
string[5] = NULL;

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 25

string

\0

Figura A2.2. Arreglo de caracteres.

El nombre del string es un puntero constante que apunta al primer carácter del string. Por ser
constante no se le puede asignar nuevos valores o modificar.
3.1.2. Puntero a carácter.
La definición de un string como un puntero a carácter, puede ser inicializada asignándole una
constante de tipo string. La que se define como una secuencia de cero o más caracteres entre
comillas dobles; el compilador agrega el carácter „\0‟ automáticamente al final.
Si dentro del string se desea emplear la comilla doble debe precedérsela por un \.
En caso de escribir, en el texto de un programa, un string de varias líneas, la secuencia de un \ y
el retorno de carro(que es invisible en la pantalla) no se consideran parte del string.

char * str1 = "abcdefghi"; /* tiene 10 caracteres, incluido el NULL que termina el string.*/

Un argumento de tipo puntero a carácter puede ser reemplazado en una lista de parámetros, en
la definición de una función por un arreglo de caracteres sin especificar el tamaño. En el caso
del ejemplo anterior: char str1[ ]. La elección entre estas alternativas suele realizarse según sea
el tratamiento que se realice dentro de la función; es decir, si las expresiones se elaboran en base
a punteros o si se emplea manipulación de arreglos.

3.2. Strcpy.

Copia el string fuente en el string destino.


Se detiene la copia después de haber copiado el carácter nulo del fin del string.
Retorna la dirección del string destino.

char *strcpy(char * destino, register const char * fuente)


{ register char * cp= destino;
while(*cp++ = *fuente++) continue;
return destino;
}

Profesor Leopoldo Silva Bijit 26-05-2008


26 Estructuras de Datos y Algoritmos

destino
cp fuente

Figura A2.3. Copia de strings.

El diagrama ilustra los punteros fuente y cp, después de haberse realizado la copia del primer
carácter. Se muestra el movimiento de copia y el de los punteros.
Cuando el contenido de *fuente es el carácter NULL, primero lo copia y la expresión resultante
de la asignación toma valor cero, que tiene valor falso para la condición, terminando el lazo
while.

La instrucción continue puede aparecer en el bloque de acciones de un while, do o for. Su


ejecución lleva a reevaluar la condición de continuación del bloque de repetición más interno
(en caso de bloques anidados). En el caso de la función anterior podría haberse omitido la
instrucción continue; ya que un punto y coma se considera una acción nula.

El operador de postincremento opera sobre un left value(que recuerda un valor que puede
colocarse a la izquierda de una asignación). Un lvalue es un identificador o expresión que está
relacionado con un objeto que puede ser accesado y cambiado en la memoria.

El uso de estos operadores en expresiones produce un efecto lateral, en el sentido que se


efectúan dos acciones. Primero se usa el valor del objeto en la expresión y luego éste es
incrementado en uno.

El operador de indirección ( el *) y el operador ++ tienen la misma precedencia, entonces se


resuelve cuál operador recibe primero el operando mediante su asociatividad, que en el caso de
los operadores unarios es de derecha a izquierda. Es decir *fuente++ se interpreta según: ( *
(fuente++) ) . La expresión toma el valor del puntero fuente y lo indirecciona, posteriormente
incrementa en uno al puntero. En la expresión (* fuente) ++, mediante el uso de paréntesis se
cambia la asociatividad, la expresión toma el valor del objeto apuntado por fuente, y luego
incrementa en uno el valor del objeto, no del puntero.

Puede evitarse la acción doble relacionada con los operadores de pre y postincremento, usando
éstos en expresiones que sólo contengan dichos operadores. En el caso de la acción de
repetición: while(*cp++ = *fuente++) continue;

Puede codificarse: while( *cp = *fuente) {cp++, fuente++};


Sin embargo los programadores no suelen emplear esta forma. Adicionalmente no producen
igual resultado, ya que en la primera forma los punteros quedan apuntando una posición más
allá de los caracteres de fin de string; la segunda forma deja los punteros apuntando a los
terminadores de los strings. Ambas formas satisfacen los requerimientos de srtcpy.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 27
La primera forma sólo tendría ventajas si el procesador tiene mecanismos de direccionamientos
autoincrementados, y si el compilador emplea dichos mecanismos al compilar la primera forma.

Cuando en la lista de parámetros de una función aparece la palabra reservada const precediendo
a una variable de tipo puntero, el compilador advierte un error si la función modifica la variable
a la que el puntero apunta. Además cuando se dispone de diferentes tipos de memorias (RAM,
EEPROM o FLASH) localiza las constantes en ROM o FLASH. Si se desea que quede en un
segmento de RAM, se precede con volatile, en lugar de const.

No se valida si el espacio a continuación de destino puede almacenar el string fuente sin


sobreescribir en el espacio asignado a otras variables. Este es un serio problema del lenguaje, y
se lo ha empleado para introducir código malicioso en aplicaciones que no validen el rebalse de
buffers.

Ejemplo:

#include <string.h>
#include <stdio.h>
char string[10]; /*crea string con espacio para 10 caracteres */
char * str1 = "abcdefghi"; /* tiene 10 caracteres, incluido el NULL que termina el string.*/

int main(void)
{ strcpy(string, str1); printf("%s\n", string);
return 0;
}

3.3. Strncpy.

strncpy copia n caracteres desde el string fuente hacia el string destino. Si el string fuente tiene
menos de n caracteres rellena con nulos hasta completar la copia de n caracteres.
Si el string fuente tiene n o más caracteres el string destino no queda terminado en un nulo.

char *strncpy(register char * destino, register const char * fuente, register size_t n )
{ register char * cp = destino;
while( n ) { n--; if (! (*cp++ = *fuente++) ) break; }
while( n--) *cp++ = 0;
return destino;
}

La sentencia break termina el bloque de repetición (más interno, si existen estructuras


repetitivas anidadas), y pasa a ejecutar la instrucción siguiente al bloque.
Si se copia el fin del string fuente se activa el break y comienza el segundo while que produce el
relleno de nulos. Si se copian n caracteres en el primer while, el segundo no se efectúa, ya que
se entra a éste con un valor cero de n.

Profesor Leopoldo Silva Bijit 26-05-2008


28 Estructuras de Datos y Algoritmos
3.4. Strcat.

Concatena una copia del string fuente luego del último carácter del string destino.
El largo del string resultante es la suma: strlen(dest) + strlen(src).
Retorna un puntero al string destino, que ahora contiene la concatenación.

char *strcat(register char * destino, register const char * fuente)


{ register char *cp= destino;
while(*cp) cp++;
while(*cp++ = *fuente++) continue; /*
return destino;
}

El primer while deja el puntero cp apuntando al carácter de fin del string destino. Luego el
segundo while efectúa la copia, sobreescribiendo el NULL del string destino, con el primer
carácter del string fuente.

La función también concatena un string fuente nulo.

Ejemplo:
#include <string.h>
#include <stdio.h>
char destino[25];
char *espacio = " ", *fin = "Final", *destino = "Inicio";

int main(void)
{ strcat(destino, espacio); strcat(destino, fin);
printf("%s\n", destino);
return 0; destino
}

cp fuente

Figura A2.4. Concatena strings.

3.5. Strncat.

strncat concatena al final del string destino a lo más n caracteres del string fuente.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 29
char *strncat(register char * destino, register const char * fuente, register size_t n )
{ register char * cp = destino;
while(*cp) cp++; /*apunta al final del string destino */
while( n && (*cp++ = *fuente++) ) n--;
if( n == 0) *cp = 0;
return destino;
}

Si fuente tiene menos de n caracteres, el segundo operador del and copia el fin de string. Si el
string fuente tiene n o más caracteres, es el primer operando del and es el que da por terminado
el segundo while; saliendo de éste con un valor cero de n. Es para este último caso que está el if
final que escribe el terminador del string destino.

El máximo largo del string destino resultante es strlen(destino al inicio) + n.


Agrega una porción del string fuente a continuación del string destino.

3.6. Strlen.

Largo de un string.
Retorna el número de caracteres del string s. No cuenta el carácter de terminación.

size_t strlen(const char * s)


{ register const char * cp= s;
while(*cp++) continue;
return (cp-1) - s;
}

El while termina cuando *cp tiene valor cero. Pero debido al operador de postincremento, al
salir del while, cp queda apuntado una posición más allá de la posición que ocupa el NULL.
Entonces cp-1, apunta al NULL. Y la resta de punteros, produce un entero como resultado; éste
da el número de caracteres del string. Si a s se le suman 5, se tiene el valor del puntero que
apunta al terminador del string, en el caso del ejemplo que se ilustra a continuación; entonces
(cp-1) – s resulta 5 en este caso.

\0
cp
Figura A2.5. Largo string.

3.7. Strcmp.

Compara dos strings.

Profesor Leopoldo Silva Bijit 26-05-2008


30 Estructuras de Datos y Algoritmos
La comparación comienza con el primer carácter de cada string y continua con los caracteres
subsecuentes hasta que los caracteres correspondientes difieren o hasta que se llegue al final de
uno de los strings.

int strcmp(register const char * s1, register const char * s2)


{ register signed char r;
while( !( r = *s1 - *s2++) && *s1++) continue;
return r;
}

Retorna un valor entero:


Menor que cero si s1 < s2
Igual a cero si s1 == s2
Mayor que cero si s1 > s2

Si los caracteres *s1 y *s2 son iguales, r es cero; y el valor lógico del primer operando del and
es verdadero. Si son diferentes, termina el lazo de repetición. Si el valor de *s2 es mayor que el
valor de *s1, r será negativo; implicando que el string s2 es "mayor" que el string s1. Si *s2 es
el carácter NULL, r será positivo si *s1 no es cero, terminando el while; implicando que s1 >
s2. Si *s1 es el carácter NULL, r será negativo si *s2 no es cero, terminando el while;
implicando que s1 < s2.

3.8. Strncmp.

Strncmp compara hasta n caracteres de los strings s1 y s2.


La comparación comienza con el primer carácter de cada string y continua con los caracteres
siguientes hasta que éstos difieran o hasta que se hayan revisado n.

int strncmp(register const char * s1, register const char * s2, size_t n)
{
while( n--) { if(*s1 == 0 || *s1 != *s2) return (*s1 - *s2); s1++; s2++; }
return 0;
}

Para los primeros n caracteres, si los caracteres difieren o se llegó al fin del string s1 se retorna
la resta del valor entero asociado a los caracteres.
Si s2 es más corto que s1, los caracteres difieren ya que *s2 es cero, y el retorno será positivo;
indicando s1 > s2.

3.9. Strstr.

Strstr encuentra la primera ocurrencia de un substring s2 en un string s1.


Si encuentra s2 en s1, retorna un puntero al carácter de s1 en que se inicia el substring que es
igual a s2; si no lo encuentra retorna un puntero nulo.
char *strstr(register const char * s1, register const char * s2)
{
while(s1 && *s1)

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 31
{ if(strncmp(s1, s2, strlen(s2)) == 0) return (char *)s1;
s1 =strchr(s1+1, *s2);
}
return (char *) 0;
}

La condición del if es verdadera si s2 está contenido en s1, con retorno exitoso. Si no lo


encuentra busca el primer carácter de s2(mediante strchr) a partir del siguiente carácter de s1; si
lo encuentra avanza s1 hasta esa coincidencia; si no lo encuentra s1 será un puntero nulo, dando
término al while(ya que el primer operando del and será falso). También termina el while si s1
es un string nulo.

3.10. Strchr.

Busca la primera ocurrencia de un carácter c en el string s.


Si lo encuentra retorna un puntero al carácter c; en caso contrario, si c no está presente en s,
retorna un puntero nulo.

char *strchr(register const char * s, int c)


{ while( *s ) { if( *s == (char) c ) return (char *) s; s++;}
return (char *) 0;
}

El tipo char es tratado como entero con signo de 8 bits(-128 a +127) y es promovido
automáticamente a tipo entero. Sin embargo se emplea un conversión explícita de entero a char
mediante el molde o cast: (char) c
Debido a que el parámetro formal se trata como puntero a una constante string(para evitar que la
función modifique a s), se debe efectuar una conversión del tipo de puntero para el retorno, se
pasa a puntero a carácter (char *) el puntero a string constante: const char *.
La búsqueda de c en s es hacia adelante (de izquierda a derecha, o de arriba hacia abajo).
Si s apunta al terminador del string, la expresión *s tiene valor cero, y la condición del while es
falsa, por lo tanto no busca el terminador del string.

El fin de string(terminador nulo) es parte del string. Si se desea que la búsqueda strchr(s, 0)
retorne un puntero al terminador nulo del string s, debe efectuarse la siguiente modificación:

char *strchr(register const char * s, int c)


{ while( *s ) { if( *s == (char) c ) return (char *) s; s++;}
if( *s == (char) c ) return (char *) s;
return (char *) 0;
}
La rutina modificada puede buscar el terminador del string incluso en un string nulo.

Profesor Leopoldo Silva Bijit 26-05-2008


32 Estructuras de Datos y Algoritmos
3.11. Strrchr.

strrchr encuentra la última ocurrencia del carácter c en el string s. Si encuentra c en s, retorna


un puntero al carácter encontrado; en caso contrario, un puntero nulo. La búsqueda la efectúa
en reversa.

char *strrchr(register const char * s, int c)


{ register const char * cp = s;
while(*s) s++; /* s queda apuntado al terminador */
while(s != cp) { s--; if(*s == (char)c ) return (char *)s;}
return (char *) 0;
}
cp

\0 s

Figura A2.6. Punteros después de primer while.

El diagrama ilustra los punteros una vez terminado el primer while.

El terminador nulo es considerado parte del string. Si se desea buscar el terminador del string
debe modificarse la rutina anterior, o utilizar strchar.

3.12. Strpbrk.

strpbrk busca en el string s1 la primera ocurrencia de cualquier carácter presente en s2; retorna
un puntero al primer encontrado, en caso que ningún carácter de s2 esté presente en s1 retorna
un puntero nulo.

char *strpbrk(register const char * s1, register const char * s2)


{
while(*s1) { if(strchr(s2, *s1)) return (char *)s1; s1++; }
return (char *)0;
}

3.13. Strcspn.

strcspn encuentra el segmento inicial del string s1 que no contiene caracteres que se encuentren
presentes en s2. Retorna el largo del segmento encontrado.

size_t strcspn(register const char * s1, register const char * s2)


{ register size_t i=0;
while(*s1 && !strchr(s2, *s1) ) {s1++; i++;}

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 33
return i;
}

A partir del primer carácter de s1 se revisa que éste no esté presente entre los caracteres que
forman s2.

3.14. Strspn.

strspn encuentra el segmento inicial del string s1 que solamente contiene caracteres que se
encuentren presentes en s2. Retorna el largo del segmento encontrado.

size_t strspn(register const char * s1, register const char * s2)


{ register size_t i = 0;
while(*s1 && strchr(s2, *s1) ) { s1++; i++;}
return i;
}

Se mantiene realizando el bloque mientras encuentre los caracteres de s1 en s2 y no sea fin de


string s1.

3.15. Strtok.

Strtok busca el primer substring de s1 que está antes del string s2 que se considera un
separador.

Se considera que s1 es un string formado por una secuencia de cero o más símbolos(tokens)
separados por el string s2.

La función strtkn permite obtener todos los tokens mediante llamados subsiguientes al primero.
Para esto el primer llamado debe retornar un puntero al primer token, y escribir un carácter
NULL en el string s1, inmediatamente después del último carácter del token que se retorna.
Los siguientes llamados deben realizarse con un puntero nulo en el primer argumento. El
separador s2 puede ser diferente para cada una de las siguientes invocaciones. Si no se
encuentran símbolos la función debe retornar un puntero nulo.

Como es una rutina que puede llamarse varias veces, su diseño debe incluir una variable estática
que permanezca entre llamados. Ya que los argumentos y variables locales sólo existen mientras
la función esté activa, debido a que se mantienen en un frame en el stack.
s1

t1 s2 t2 s2 t2 s2

sp

Figura A2.7. Strtok

Profesor Leopoldo Silva Bijit 26-05-2008


34 Estructuras de Datos y Algoritmos

El primer llamado a strtrk, debe retornar s1, colocando un nulo en el primer carácter de s2, y
fijar la posición de la estática sp inmediatamente después del terminador recién insertado.
Los llamados subsiguientes llevan un NULL en el primer argumento, correspondiente al puntero
al string s1; esto le indica a la función que debe emplear ahora sp para seguir buscando tokens.

El primer if, si s1 es un puntero nulo, fija s1 en el valor guardado en la estática sp por la


invocación anterior. Si el primer llamado se efectúa sobre un string nulo, debe estar inicializada
la estática sp.
El segundo if, retorna un puntero nulo, si el llamado inicial es con un puntero nulo, o si se agotó
la búsqueda de símbolos en llamados subsiguientes (encuentra sp con valor nulo).

char *strtok(register char * s1, register const char * s2)


{ static char * sp = NULL;
if(!s1) s1 = sp;
if(!s1) return NULL;
s1 += strspn(s1, s2); /* salta separador */
if(!*s1) return sp = NULL;
sp = s1 + strcspn(s1, s2);
if(*sp) *sp++ = '\0'; else sp = NULL;
return s1; /* puntero al token encontrado */
}

Mediante la función strspn se saltan los caracteres pertenecientes al separador s2, y s1 queda
apuntado al primer carácter siguiente al separador s2.
El tercer if, si se llegó al final del string, fija sp en puntero nulo, y retorna un puntero nulo.
No se efectúa la acción del tercer if, si quedan caracteres por escanear.
Mediante la función strcspn se fija sp una posición más allá del último carácter del token
encontrado.
El cuarto if, fija sp en puntero nulo, si se agotó la búsqueda (se efectúa el else); en caso,
contrario si quedan caracteres por escanear, coloca un carácter nulo para finalizar el token
encontrado, y avanza sp una posición más adelante.
El tipo del argumento de s1 no puede ser un puntero constante, ya que se escribe en el string.

Si por ejemplo el string s1 tiene el valor "abc, dd,efg" y si el separador s2 es el string ","; se
encuentra, después del primer llamado el token: abc. Luego del segundo (con primer argumento
NULL) el token: dd. Finalmente el símbolo: efg.

3.16. Strdup.

strdup crea un duplicado del string s, obteniendo el espacio con un llamado a malloc.

El espacio requerido es de (strlen(s) + 1) bytes, para incluir el terminador, el que es insertado


por strcpy.
Si malloc no puede asignar espacio retorna puntero nulo.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 35
La función retorna un puntero al duplicado; en caso que falle malloc retorna un puntero nulo. El
programador es responsable de liberar el espacio ocupado cuando ya no sea necesario,
empleando la función free.

char *strdup(register const char * s)


{ register char * cp;
if(cp = (char *) malloc(strlen(s)+1)) strcpy(cp, s);
return cp;
}

Con las siguientes definiciones:


char *duplicado;
char *string = "1234";

Un ejemplo de uso es: duplicado = strdup(string);


Y para liberar el espacio: free(duplicado);

El prototipo de malloc es: void *malloc(size_t size);

Malloc retorna un puntero de tipo void o un puntero genérico. Se emplean cuando el compilador
no puede determinar el tamaño del objeto al cual el puntero apunta. Por esta razón los punteros
de tipo void deben ser desreferenciados mediante casting explícito.

El siguiente ejemplo muestra que a un puntero tipo void se le puede asignar la dirección de un
entero o un real. Pero al indireccionar debe convertirse el tipo void al del objeto que éste
apunta.
int x;
float r;
void *p;

p = &x; * (int *) p = 2;
p = &r; *(float *)p = 1.1;

Bloques de memoria.
El conjunto de funciones cuyos prototipos se encuentran en string.h también incluye un grupo
de funciones que manipulan bloques de memoria.

Los bloques son referenciados por punteros de tipo void, en la lista de argumentos. Luego en las
funciones se definen como variables locales punteros a caracteres, que son iniciados con los
valores de los punteros genéricos amoldados a punteros a carácter.

3.17. Memcpy.

memcpy Copia un bloque de n bytes desde la dirección fuente hacia la dirección destino.

Profesor Leopoldo Silva Bijit 26-05-2008


36 Estructuras de Datos y Algoritmos
void *memcpy(void * destino, const void * fuente, register size_t n)
{ register char *d = (char *)destino; register const char *s= (char *)fuente;
while(n--) *d++ = *s++;
return destino;
}

Si fuente y destino se traslapan la conducta de memcpy es indefinida. Ya que no se puede


sobreescribir el bloque fuente, que se trata como puntero genérico constante. Dentro de la
función se emplean punteros locales a caracteres.

3.18. Memccpy.

Copia no más de n bytes desde el bloque apuntado por fuente hacia el bloque apuntado por
destino, deteniéndose cuando copia el carácter c.
Retorna un puntero al bloque destino, apuntando al byte siguiente donde se copió c. Retorna
NULL si no encuentra a c en los primeros n bytes del bloque fuente.

void *memccpy (void *destino, const void *fuente, int c, size_t n)


{
register const char *s = (char *)fuente;
register char *d = (char *)destino;
register const char x = c;
register size_t i = n;
while (i-- > 0) if ((*d++ = *s++) == x) return d;
return NULL;
}

Se emplean locales de tipo registro para acelerar la copia. Nótese que los punteros de tipo void
de los argumentos son los valores iniciales de los registros con punteros a caracteres.

3.19. Memmove.

Con memmove, si los bloques apuntados por fuente y destino se traslapan, los bytes ubicados en
la zona de traslapo se copian correctamente.

void *memmove(void * destino, void * fuente, register size_t n)


{ register char *d= (char *)destino; register char *s = (char *)fuente;
if(s < d && s+n >=d) { s += n; d += n; do *--d = *--s; while(--n); }
else if(n) do *d++ = *s++; while(--n);
return destino;
}

La condición de traslapo del bloque s sobre el bloque d, se ilustra en el diagrama de más a la


derecha. En este caso se efectúa la copia en reversa. Se sobreescriben los últimos elementos del
bloque apuntado por s; es decir los que primero fueron copiados.

La negación lógica de la condición de traslapo, por De Morgan, es: (s>=d || s+n<d )

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 37

Los dos casos del or se ilustran en las dos figuras de más a la izquierda. En el caso que ilustra la
figura central, no hay traslapo.

En el caso s>=d, si d+n>=s, se produce traslapo y se sobreescriben las primeras posiciones del
bloque s, las que primero fueron copiadas (cuando se ejecuta el else, avanzado los punteros
hacia direcciones cada vez mayores).

s>=d s + n <d s < d && s+n >= d


d s s

n n

s d d

Figura A2.8 Memmove.

Debido a que el bloque fuente puede ser sobrescrito, no puede ser un puntero vacío constante.
Los punteros a carácter s y d, son iniciados con los valores amoldados (cast) a punteros a
carácter de fuente y destino.

3.20. Memcmp.

memcmp compara los primeros n bytes de los bloques s1 y s2, como unsigned chars.

int memcmp(const void *s1, const void *s2, size_t n)


{ int i;
register const unsigned char *a1, *a2;
a1 = (unsigned char *)s1; a2 = (unsigned char *)s2;
while(n--) if( i = (int)(*a1++ - *a2++) ) return i;
return 0;
}

Valor de retorno menor que cero si s1 < s2


Valor de retorno igual a cero si s1 == s2
Valor de retorno mayor que cero si s1 > s2

Profesor Leopoldo Silva Bijit 26-05-2008


38 Estructuras de Datos y Algoritmos
3.21. Memset.

Rellena n bytes del bloque s con el byte c. Retorna puntero genérico al bloque.
void *memset(void * s, int c, register size_t n)
{ register char * p = (char *)s;
while(n--) *p++ = (char) c;
return s;
}

3.22. Movimientos de bloques, dependientes del procesador.

Efectuar movimientos de bloques orientados al carácter es ineficiente. Por esta razón las
funciones de movimiento tratan de mover palabras.
Primero se mueven los bytes parciales de una palabra, luego se pueden mover palabras
alineadas; para finalmente, copiar los bytes presentes en la última palabra.

Estas funciones, implementadas en base a macros para mejorar la velocidad, son dependientes
del procesador. Se requiere conocer el ancho de la palabra y el ordenamiento de los bytes dentro
de la palabra (little o big endian).

El siguiente ejemplo introduce en un entero de 32 bits, el carácter '4' en el byte más


significativo, luego el '3', después el '2', y el carácter '1' en el byte menos significativo. Luego
convierte la dirección de i en un puntero a carácter, e imprime el string de largo 4. En el string el
byte con la menor dirección queda más a la izquierda, y el byte con dirección mayor queda a la
derecha.

unsigned long int i;


if (sizeof (i) == 4)
{ i = (((((('4' << 8) + '3') << 8) + '2') << 8) + '1');
printf ("Orden de los Bytes = %.4s\n", (char *) &i);
} else printf (" \nNo es una máquina de 32 bits");

Los strings asociados reflejan el ordenamiento de los bytes dentro de la palabra. Suelen
denominarse:

LITTLE ENDIAN "1234"


BIG ENDIAN "4321"
PDP ENDIAN "3412"

El siguiente texto ilustra una función de movimiento por bloques, mostrando el grado de
complejidad de estas rutinas. Se invoca a varios macros, de los cuales sólo se da el nombre.

rettype
memmove (a1const void *a1, a2const void *a2, size_t len)
{
unsigned long int dstp = (long int) dest;
unsigned long int srcp = (long int) src;

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 39
/* This test makes the forward copying code be used whenever possible.
Reduces the working set. */
if (dstp - srcp >= len) /* *Unsigned* compare! */
{
/* Copy from the beginning to the end. */

/* If there not too few bytes to copy, use word copy. */


if (len >= OP_T_THRES)
{
/* Copy just a few bytes to make DSTP aligned. */
len -= (-dstp) % OPSIZ;
BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);

/* Copy whole pages from SRCP to DSTP by virtual address


manipulation, as much as possible. */

PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);

/* Copy from SRCP to DSTP taking advantage of the known


alignment of DSTP. Number of bytes remaining is put
in the third argument, i.e. in LEN. This number may
vary from machine to machine. */

WORD_COPY_FWD (dstp, srcp, len, len);

/* Fall out and copy the tail. */


}

/* There are just a few bytes to copy. Use byte memory operations. */
BYTE_COPY_FWD (dstp, srcp, len);
}
else
{
/* Copy from the end to the beginning. */
srcp += len;
dstp += len;

/* If there not too few bytes to copy, use word copy. */


if (len >= OP_T_THRES)
{
/* Copy just a few bytes to make DSTP aligned. */
len -= dstp % OPSIZ;
BYTE_COPY_BWD (dstp, srcp, dstp % OPSIZ);

/* Copy from SRCP to DSTP taking advantage of the known


alignment of DSTP. Number of bytes remaining is put
in the third argument, i.e. in LEN. This number may

Profesor Leopoldo Silva Bijit 26-05-2008


40 Estructuras de Datos y Algoritmos
vary from machine to machine. */

WORD_COPY_BWD (dstp, srcp, len, len);

/* Fall out and copy the tail. */


}

/* There are just a few bytes to copy. Use byte memory operations. */
BYTE_COPY_BWD (dstp, srcp, len);
}

RETURN (dest);
}

4. Rutinas de conversión.

4.1. De enteros a caracteres. Ltoa. Long to Ascii.

Pasar de un número en representación interna a una secuencia de caracteres, permite desplegar


en la salida los valores de las variables de un programa.

La siguiente rutina convierte un entero largo, en representación interna, en una secuencia de


dígitos. Se dispone, como argumento de la función, de la base numérica en la que los números
se representarán en forma externa.

La función ocupa un buffer estático de 65 bits, lo cual permite convertir enteros de 64 bits en
secuencias binarias. Se considera en el buffer espacio para el signo y el terminador del string.

 Para enteros de 16 bits, el rango de representación es: [-32768.. 32767] el cual requiere
de 5 char para representar mediante dígitos decimales.
 Para enteros de 32 bits: [-2147483648 .. +2147483647] se requieren 10 chars para
dígitos.
 Para enteros de 64 bits: [-9223372036854775808..+9223372036854775807] se
requieren 19 char para dígitos decimales. Para imprimir en binario se requieren 63
dígitos binarios.

El procedimiento consiste en sacar el módulo base del número, esto genera el último carácter
del número; es decir el menos significativo. Luego se divide en forma entera por la base,
quedando el resto; del cual se siguen extrayendo uno a uno los dígitos.

Por ejemplo para el entero 123 en base decimal, al sacar módulo 10 del número se obtiene el
dígito de las unidades, que es 3. Al dividir, en forma entera por la base, se obtiene el número de
decenas; es decir, 12. Sacando módulo 10 se obtiene 2; y al dividir por la base se obtiene el
número de centenas.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 41
Si la base que se pasa como argumento es menor o igual a cero, se asume base decimal. Si la
base es mayor que 36, también se asume base decimal. Con base 36 se tienen los 10 dígitos
decimales y todas las letras como los dígitos del sistema.

La función retorna un puntero al primer carácter de la secuencia. Al inicio de la rutina se apunta


al final del buffer, a la posición de las unidades, ya que los dígitos se generan a partir de las
unidades.
El carácter de fin de string se coloca automáticamente al definir el buffer estático, ya que éste es
inicializado con ceros.

La función permite desplegar secuencias binarias y hexadecimales.


Para convertir un número entero a un carácter de un dígito decimal se suma el valor del carácter
„0‟; el valor asociado a „0‟ es 0x30, al „1‟ está asociado el 0x31. Para números mayores que 9
se suma el valor del carácter „7‟ (que es 55 decimal); de esta manera para 10, se obtiene: 10 +
55 = 65 que es el equivalente a „A‟.

#define INT_DIGITOS 63
static char buf[INT_DIGITOS + 2];
/* Buffer para INT_DIGITS dígitos, signo - y fin de string '\0' */

char * ltoa(long int i, unsigned int base)


{
char *p = buf + INT_DIGITOS + 1; /* apunta a posición de unidades */
int dig, signo=0;
if (i<0) {signo=1;i = -i;}
if(base<=0 || base>36) base=10; /*evita división por cero */

do { dig=(i%base); if (dig <=9) *--p = '0' + dig; else *--p= '7'+ dig ; i /= base;}
while (i != 0);

if(signo) *--p = '-';


return p;
}

Para convertir enteros se emplea la misma rutina anterior, invocando con una conversión
explícita del entero a largo.

char * itoa(int i, unsigned int base)


{
return (ltoa((long)i, base));
}

La siguiente rutina permite el despliegue del string, mediante putchar.

#include <stdio.h>

Profesor Leopoldo Silva Bijit 26-05-2008


42 Estructuras de Datos y Algoritmos
void prtstr(char * p)
{
while(*p) putchar(*p++);
}

El siguiente ejemplo ilustra el uso de las rutinas de conversión.

int main(void)
{
int i=-31;
long int l= -2147483647L;

prtstr( itoa(i,32) ); putchar('\n');


prtstr( ltoa(l,2) ); putchar('\n');
return (0);
}

4.2. De secuencias de caracteres a enteros.

Si bien esta rutina efectúa el trabajo inverso de la anterior, su diseño es más complejo, ya que
debe operar con datos suministrados por un ser humano. La anterior saca algo que está en
formato interno y que está bien especificado.

En la conversión de un string en un long integer, debe asumirse que el usuario provee una
secuencia de caracteres que tiene el siguiente formato:

[blancos*] [signo] [0] [x|X] [ddd]

Los corchetes indican elementos opcionales, el asterisco la repetición de cero o más veces. De
esta forma podrían digitarse caracteres espacios o tabuladores antes de la secuencia; podría
indicarse el signo +, y también declarar números octales si el primer dígito es cero, o
hexadecimales si los primeros dígitos son 0x ó 0X.

En el diseño se decide terminar de leer cuando encuentra un carácter que no cumpla el formato.
Para permitir seguir analizando la secuencia de entrada se decide pasar a la función, además de
un puntero al inicio de la secuencia a analizar, y de la base, la referencia a un puntero a carácter.
Al salir de la función se escribe, en la variable pasada por referencia, el valor del puntero que
apunta al carácter que detuvo el scan, por no cumplir el formato. Esto debido a que en C, sólo se
puede retornar un valor desde la función.

Además la función debe resolver que el valor del número ingresado no exceda el mayor
representable. Como los números se representan con signo en complemento a la base, se tendrán
valores máximos diferentes(en la unidad) para el máximo positivo y el máximo negativo.

Por ejemplo para enteros largos de 32 bits, el rango asimétrico de representación en base
decimal es [-2147483648..2147483647].

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 43
Entonces debe considerarse que cuando se tenga ingresada la secuencia 214748364(corte), si el
siguiente dígito (límite) es mayor que 7, para positivos; y mayor que 8, para negativos se
tendrá rebalse. Y se debe notificar con el código de error estándar: error de rango.

Los valores máximos de las representaciones se almacenan en limits.h, mediante estas


constantes se pueden calcular la secuencia de corte y el dígito límite, que permiten determinar si
se sobrepasa o no el rango de representación.

En el código siguiente se incorporan el texto de las funciones, macros y constantes, y se


comentan la inclusión de los archivos que las contienen.
Los macros de ctype.h se han simplificado, ya que suelen estar implementados mediante una
tabla de búsqueda.
En la variable local acc se va formando el número. La variable any se coloca en uno si se han
consumido caracteres desde la secuencia de entrada; y se le da un valor negativo si se produce
rebalse.

Como se lee la secuencia de izquierda a derecha, el núcleo del algoritmo consiste en multiplicar
el número acumulado en acc, por la base y luego sumarle el valor numérico del dígito.
Por ejemplo, la secuencia 123 en decimal, es procesada según:
0*10 + 1 = 1
1*10 + 2 = 12
12*10 + 3 = 123

Nótese que el segundo argumento de la función es un puntero a un puntero a carácter. En caso


que a la función se le pase un puntero no nulo, retornará la posición donde se detuvo el scan, o
la dirección de inicio de la secuencia si se excede el rango de representación.

El código de la rutina pertenece a la Universidad de California.

/*#include <limits.h>*/
#define LONG_MAX 0x7FFFFFFFL
#define LONG_MIN ((long)0x80000000L)
/*#include <ctype.h>*/
#define islower(c) (('a' <= (c) ) && ((c) <= 'z'))
#define isupper(c) (('A' <= (c) ) && ((c) <= 'Z'))
#define isdigit(c) (('0' <= (c) ) && ((c) <= '9'))
#define isspace(c) ((c) == ' ' || (c) == '\t' || (c)== '\n')
#define isalpha(c) ((islower(c))||(isupper(c)))
/*#include <errno.h>*/
#define ERANGE 34 /* Resultado demasiado grande. Rebalse de representación. */
extern int errno;

/* Copyright (c) 1990 The Regents of the University of California.*/


long strtol(const char *nptr, char **endptr, int base)

{ register const char *s = nptr;

Profesor Leopoldo Silva Bijit 26-05-2008


44 Estructuras de Datos y Algoritmos
register unsigned long acc;
register int c;
register unsigned long cutoff;
register int neg = 0, any, cutlim;

do {c = *s++;} while (isspace(c)); /* salta blancos*/


if (c == '-') { neg = 1; c = *s++;} /* si negativo, registra signo en neg */
else if (c == '+') c = *s++; /* salta signo + */

if ( (base == 0 || base == 16) && c == '0' && (*s == 'x' || *s == 'X') )


{c = s[1];s += 2; base = 16;} /*si base es 0 ó 16 consume 0x ó 0X y lee hex*/

if (base == 0) base = c == '0' ? 8 : 10;


/*si la base es cero, si el primer digito es cero lee octal, sino asume decimal */

/*Calcula el corte y el dígito límite, a partir de los máximos */


cutoff = neg ? -(unsigned long) LONG_MIN : LONG_MAX;
cutlim = (int)(cutoff % (unsigned long) base);
cutoff /= (unsigned long) base;

for (acc = 0, any = 0; ; c = *s++) {


if (isdigit(c)) c -= '0';
else if (isalpha(c)) c -= (isupper(c)) ? 'A' - 10 : 'a' - 10; else break;

if (c >= base) break;


if (any < 0 || acc > cutoff || acc == cutoff && c > cutlim) any = -1;
else {any = 1; acc *= base; acc += c;}
}

if (any < 0) { acc = neg ? LONG_MIN : LONG_MAX; errno = ERANGE;}


else if (neg) acc = -acc;

if (endptr != 0) *endptr = any ? (char *)(s - 1) : (char *)nptr;

return (acc);
}

Las funciones atoi y atol se implementan en base a strtol.


En éstas, la base es 10, y no se pasa una referencia a un puntero a carácter.

/* Convierte un string en un entero. Ascii to integer. */


int atoi (const char *nptr)
{ return (int) strtol (nptr, (char **) NULL, 10); }

/* Convierte un string en un entero largo. Ascii to long */


long int atol (const char *nptr)

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 45
{ return strtol (nptr, (char **) NULL, 10); }

El siguiente ejemplo ilustra el uso de strtol, y la forma de emplear el segundo argumento.


Nótese que al invocar se pasa la dirección de un puntero a carácter; y que al salir, endptr queda
apuntando al carácter que no es válido en la secuencia.

/* strtol ejemplo */
#include <stdio.h>
int main(void)
{
char *string = "87654321guena", *endptr;
long lnumber;

/* strtol converts string to long integer */


lnumber = strtol(string, &endptr, 10);
printf("string = %s long = %ld\n", string, lnumber);
printf(" endptr = %s \n", endptr);
return 0;
}

Nótese que el segundo argumento actual, en la invocación, es &endptr; es decir, la dirección de


un puntero. Entonces en la definición de la función el tipo del segundo argumento debe un
puntero a puntero a carácter; es decir: char * * endptr.

4.3. De dobles a caracteres.

Resulta útil una función que convierta un número en punto flotante en una base b1 en otro
número punto flotante en base b2. Si b1 es 2 y b2 es 10, se convierte un número real en
representación interna en externa.

Se dice que en un sistema de base b, la mantisa m está normalizada si:

1/b <= m < 1

El algoritmo consiste en alternar las multiplicaciones (o divisiones) de m por b1, con las
divisiones (o multiplicaciones) por b2, de tal forma que m se mantenga en el rango:

1/b <= m < b con b = b1*b2

El algoritmo es ineficiente, pues el número de operaciones es proporcional a e1; además


introduce cierto error, debido a las numerosas operaciones de truncamiento para mantener la
mantisa dentro del rango anterior.

Debido a que la función entrega dos valores, se decide pasar por referencia el exponente e2.

Profesor Leopoldo Silva Bijit 26-05-2008


46 Estructuras de Datos y Algoritmos
/*Dado un flotante m1*pow(b1, e1) se desea obtener m2*pow(b2, e2) */
double convierta(double m, int e1, int b1, int *e2, int b2)
{
*e2=0;
if (e1>=0)
while (e1>0)
{ m*=b1;e1--;
while(m>=1) {m/=b2;(*e2)++;}
}
else
do
{ m/=b1;e1++;
while( m <(1/b2) ) {m*=b2;(*e2)--;}
}
while (e1!=0);
return(m);
}

Un ejemplo de uso se ilustra a continuación:

Se definen algunas variables:


double number, mantisa, m2;
int e1, int e2=0;

El siguiente llamado a la rutina frexp, declarada en math.h, retorna la mantisa y el exponente de


un número flotante doble, según:

mantisa * pow(2, exponente)

Con: 0.5 =< mantisa < 1

mantisa = frexp(number, &e1);

El llamado a convierta escribe en m2 y e2, el número doble mantisa e1.

m2 = convierta(mantisa, e1, 2, &e2, 10);


printf("m2 = %lf \n", m2);
printf("e2 = %d \n", e2);

4.4. Imprime mantisa.

La función prtmantisa imprime la parte fraccionaria o mantisa normalizada de un número u en


punto flotante de doble precisión, mediante putchar; empleando como base numérica a base, y
sacando un número de dígitos igual a ndig.

Sólo acepta bases positivas menores o iguales que 36; en caso de estar fuera de rango asume
base decimal.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 47
void prtmantisa(double u, int ndig, int base)
{
int i=0; int v;
putchar('.');
if(base<=0 || base>36) base=10;
for(i=0; i<ndig; i++)
{ u*=base; v=(int)u;
if (v <=9) putchar(v+'0'); else putchar(v+'7');
u-=v;
}
}

Multiplica la mantisa por la base, quedando de esta forma un número a la izquierda del punto
decimal. Dicho dígito puede obtenerse en v, truncando el doble; esto se logra mediante el cast
explícito a entero. Luego se puede enviar hacia la salida el carácter, considerando una
conversión a carácter, que toma en cuenta bases mayores a la decimal.
Antes de volver a seguir desplegando caracteres, le quita la parte entera al doble; de este modo
al inicio del bloque, u siempre es un número fraccionario puro.

Los siguientes ejemplos ilustran el uso de la función:


prtmantisa(mantissa, 18, 10); putchar('\n');
prtmantisa(mantissa, 52, 2); putchar('\n');
prtmantisa(mantissa, 8, 16); putchar('\n');

Nótese que no tiene sentido invocar la impresión binaria con más de 52 bits, ya que ese es el
número de bits de un doble. Tampoco tiene sentido invocar la impresión hexadecimal de la
mantisa con más de 7 cifras.

Puede verificarse que tampoco tiene sentido solicitar salidas en base decimal con más de 18
dígitos, ya que ésta es la precisión de un doble.

La impresión del exponente, mediante putchar, puede lograrse empleando itoa.

4.5. Rutinas más eficientes para convertir un número punto flotante binario a punto flotante
decimal.

4.5.1. Potencias de 10.


La siguiente función calcula una potencia de 10, mediante un switch.

/*Calcula pow(10, e) con 0 < e < 309 */


double ten(unsigned int e)
{
double t=1.0;
int i=0;
if (e<309)
while (e!=0)

Profesor Leopoldo Silva Bijit 26-05-2008


48 Estructuras de Datos y Algoritmos
{ if ( e&1)
switch (i)
{ case 0: t*=1.0e1;break;
case 1: t*=1.0e2;break;
case 2: t*=1.0e4;break;
case 3: t*=1.0e8;break;
case 4: t*=1.0e16;break;
case 5: t*=1.0e32;break;
case 6: t*=1.0e64;break;
case 7: t*=1.0e128;break;
case 8: t*=1.0e256;break;
}
e/=2;i++;
}
else t=1/0.0; /* return (INF ) */
return(t);
}

Pero es más eficiente emplear un arreglo estático:


double diezalai[9]={1.0e1,1.0e2,1.0e4,1.0e8,1.0e16,1.0e32,1.0e64,1.0e128,1.0e256};
/* pow(10,e) para double. Con arreglo. */

double ten2(unsigned int e)


{
double t=1.0;
int i=0;
if (e<309)
while (e!=0)
{ if ( e&1) t*= diezalai[i];
e/=2;i++;
}
else t=1/0.0; /* return (INF ) */
return(t);
}
4.5.2. Imprime exponente de flotante.
Rutina para imprimir, mediante putchar, el exponente de un número punto flotante.
Se emplean instrucciones con enteros.

/*Imprime exponente en flotante doble */


void prtexp(long int e)
{
static char buf[6];
char *p = buf; int e0,e1,e2;
*p++='e';
if (e<0) {*p++='-'; e=-e;} else *p++='+';
if (e<309)

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 49
{ e1=(int)(e*205)>>11; /* e/a = e*round(max/a)/max */
e2= (int)(e-(e1<<3)-(e1<<1)); /* e-10*e */
e0=(e1*205)>>11; /* e/10 = e1*round(2048/10)/2048 */
e1=e1-(e0<<3)-(e0<<1); /* e1=e1-e0*10 */
*p++=e0+'0';*p++=e1 +'0';*p++=e2 +'0';*p++='\0';}
else {*p++='I';*p++='N';*p++='F';*p++='\0';}
p=buf; while(*p) putchar(*p++);

}
/* con a de tipo float. round(2048/10) = 205. Funciona si e<1029 */

Para verificar la operación de redondeo empleando operaciones enteras, puede ejecutarse el


siguiente segmento:

long int i;
for(i=0; i<1030; i++)
/*realiza i/10 mediante multiplicaciones enteras. Para i<1029 */
if ( ((i*205)/2048)!=(i/10)) printf( " %d \n", i);
4.5.3. Redondeo de la mantisa.
double round(double t, unsigned int i)
{ /*no puede redondear a mas de 15 cifras */
if (i<16)
switch (i)
{ case 2: t+=0.5e-2;break;
case 3: t+=0.5e-3;break;
case 4: t+=0.5e-4;break;
case 5: t+=0.5e-5;break;
case 6: t+=0.5e-6;break;
case 7: t+=0.5e-7;break;
case 8: t+=0.5e-8;break;
case 9: t+=0.5e-9;break;
case 10: t+=0.5e-10;break;
case 11: t+=0.5e-11;break;
case 12: t+=0.5e-12;break;
case 13: t+=0.5e-13;break;
case 14: t+=0.5e-14;break;
case 15: t+=0.5e-15;break;
}
return(t);
}

La misma función anterior de Redondeo puede efectuarse mediante arreglos:

Profesor Leopoldo Silva Bijit 26-05-2008


50 Estructuras de Datos y Algoritmos
double roundalai[14]={0.5e-2,0.5e-3,0.5e-4,0.5e-5,0.5e-6,0.5e-7,0.5e-8,
0.5e-9,0.5e-10,0.5e-11,0.5e-12,0.5e-13,0.5e-14,0.5e-15};

double round2(double t, unsigned int i)


{ /*no puede redondear a mas de 15 cifras */
if ((i>1)||(i<16)) return(t+roundalai[i-2]);else return(t);
}
4.5.4. Convierta. Algoritmo dos.
/*Dado un flotante m=m1*pow(2,e1) se desea obtener m2*pow(10,e2) */
/* e2 = log(2)*e1 m2=m/pow(10,e2) */
/* log(2)=0.30103 es aprox 77/256 = 0,30078125 */
double convierta2(double m, long int e1, long int *e2)
{
m=m*pow(2,e1);
/* sin la funcion pow, se pasa el número y el exponente binario retornado por frexp */
if (e1>=0)
{ if(e1<1025)
{*e2=(e1*77/256)+1; m/=ten2((int)*e2);
while(m>=1.0) { m/=10.0; (*e2)++;}
while(m<0.1) {m*=10.0; (*e2)--;}
}
else printf("inf");
}
else
{*e2=(e1+1)*77/256; m*=ten2(-((int)*e2));
while(m<0.1) {m*=10.0; (*e2)--;}
while(m>=1.0) { m/=10.0; (*e2)++;}
}
return(m);
}

4.5.5. Imprime mantisa. Algoritmo dos.


Aplica redondeo a ndig
/* imprime mantisa normalizada u = .ddddd No chequea infinito*/
void prtmantisa(double u, int ndig, int base)
{
int i=0; int v;
putchar('0'); putchar('.');
if ((base<=0)||(base>36)) base=10; u=round2(u,ndig);
for(i=0; i<ndig; i++)
{ u*=base; v=(int)u;
if (v <=9) putchar(v+'0'); else putchar(v+'7');
u-=v;
}
}

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 51
5. Diseño de funciones con un número variable de argumentos.

Una función puede tener parámetros fijos y una lista de argumentos variables. El número y tipo
de argumentos no son conocidos en el momento que se diseña la función.

El siguiente prototipo de func indica que tiene un parámetro fijo, un puntero a entero, y los tres
puntos a continuación indican que se puede pasar a esta función un número variable de
argumentos.

int func( int *, …);

Por ejemplo se la puede invocar: x = func(p, a, b, c); o bien: x = func(p, a);

En la definición de la función debe existir alguna forma de conocer el número y tipo de los
parámetros variables. En la función printf el argumento fijo es el string de control que permite
determinar el número y tipo de argumentos.

Debe conocerse la estructura del frame de la función en el stack. Si suponemos que previo a la
invocación de una función se empujan los valores de los argumentos empezando por el último
argumento, se tendrá el siguiente esquema del frame, para la invocación a
func(p, a, b, c), después del llamado:

p
a
b
c

Las
direcciones
aumentan

Figura A2.9 Estructura frame.

Entonces el primer argumento variable tiene una dirección mayor que el último argumento fijo.
Como además los tamaños de los argumentos pueden ser diferentes, deben guardarse los
parámetros alineados.

Si p es un puntero a entero, se tendrá que la dirección de inicio de p es &p. La dirección de


inicio de a, será la dirección &p más el tamaño de un puntero a entero. Si se conoce la dirección
de a, puede obtenerse su valor según:

* (tipo de a *) (dirección de a)

Profesor Leopoldo Silva Bijit 26-05-2008


52 Estructuras de Datos y Algoritmos
Se convierte la dirección de a, en un puntero al tipo de a, y luego se indirecciona.

Para el diseño de este tipo de funciones, se dispone de herramientas de biblioteca estándar.


Básicamente consisten en dotar a la función de un puntero de un tipo adecuado, para tratar
argumentos de diferente tipo; de un mecanismo para fijar el puntero al primer argumento
variable, y de una función que extraiga el valor del argumento, según su tipo y avance el
puntero al siguiente argumento; y un mecanismo para evitar resultados catastróficos por un uso
inadecuado del puntero.

5.1. Argumentos estándar.

En <stdarg.h> se definen un tipo y tres macros.

typedef void *va_list;

#define __size(x) ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int)-1))


/* retorna el tamaño en bytes hasta el siguiente inicio de palabra alineada */

#define va_start(ap, parmN) ((void)((ap) = (va_list)((char *)(&parmN)+__size(parmN))))


#define va_arg(ap, type) (*(type *)(((*(char * *)&(ap))+=__size(type))-(__size(type))))
#define va_end(ap) ((void)((ap) = (va_list) 0) )

va_list es un tipo de puntero genérico; es decir puede apuntar a cualquier elemento de memoria,
no importando su tamaño. Más adelante se explicarán detalladamente los macros.

La inicialización del puntero, dentro de la función se logra con va_start, y debe usarse antes de
llamar a va_arg o va_end.
va_arg retorna el valor del argumento y mueve el puntero al inicio del siguiente argumento.
va_end desconecta el puntero, y debe emplearse una vez que va_arg haya leído todos los
argumentos.

El siguiente ejemplo ilustra el orden y uso de los macros.


#include <stdio.h>
#include <stdarg.h>

/* calcula la suma de una lista variables de enteros, terminada en 0 */


void sum(char *msg, ...)
/* Nótese que sum emplea un número variable de argumentos */
{
int total = 0; int arg;
va_list ap; /* 1. Se define el puntero */
va_start(ap, msg); /*2. Se inicia el puntero ap al primer argumento variable */

while ((arg = va_arg(ap,int)) != 0) { /* 3 Se buscan los argumentos, uno a la vez */


total += arg;
}
printf(msg, total);

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 53

va_end(ap); /* 4. Se aterriza el puntero ap */


}

int main(void) {
sum("El total de: 1+2+3+4 es igual a %d\n", 1, 2, 3, 4, 0);
return 0;
}

Para efectuar correctamente los movimientos del puntero, y para considerar el alineamiento en
el almacenamiento de los argumentos, se emplea el macro:

#define __size(x) ((sizeof(x) + sizeof(int) -1) & ~(sizeof(int) - 1))


/* retorna el tamaño en bytes hasta el siguiente inicio de palabra alineada */

Esto considera que un entero se guarda alineado, lo cual es una definición implícita en C. Lo
rebuscado, aparentemente, permite portar el código de estos macros a procesadores con
diferentes tamaños para el entero.
El operador sizeof retorna el número de bytes de la expresión o el tipo que es su argumento.

Para enteros de 16 bits, la máscara que se forma tomando el complemento a uno, resulta ser:
…1111110. Para enteros de 32 bits, la máscara es: ….1111100.
Para 64 bits: ….1111000.
Al tamaño de x en bytes se le suma el tamaño en bytes del entero menos uno, lo cual luego es
truncado por la máscara, obteniendo el tamaño de x en múltiplos del tamaño de un entero.

El siguiente segmento permite verificar en forma enumerativa, la función del macro que entrega
direcciones alineadas:
for(t=2;t<9;t*=2)
{ printf(" t=%d \n",t);
for( i=1; i<16;i++) printf(" i=%d largo=%d\n", i, (i+t-1)&~(t-1));

La macro que inicializa el puntero es:


#define va_start(ap, parmN) ((void)((ap) = (va_list)((char *)(&parmN) +__size(parmN))))

La dirección del último parámetro fijo se convierte a puntero a char(a un Byte) y se obtiene la
siguiente dirección (en Bytes) donde está almacenado el primer argumento variable, mediante:
( (char *)(&parmN) +__size(parmN) )

Luego esta dirección se convierte en la del tipo de ap, y se la escribe en ap. El primer void
indica que el macro no retorna valores.
Debe notarse que cuando se emplean macros, los argumentos deben colocarse entre paréntesis.

La macro que aterriza el puntero ap es:


#define va_end(ap) ((void)((ap) = (va_list) 0) )

Profesor Leopoldo Silva Bijit 26-05-2008


54 Estructuras de Datos y Algoritmos
El valor cero, que por definición es un puntero nulo, se lo convierte en puntero al tipo de ap,
antes de asignárselo. El primer void, indica que la función no retorna nada.

Finalmente la función que recorre la lista de argumentos:


#define va_arg(ap, type) (*(type *)(((*(char * *)&(ap))+=__size(type))-(__size(type))))

Es un tanto más compleja. Ya que efectúa dos cosas, retornar el valor del argumento y por otro
lado incrementar el puntero ap, de tal modo que apunte al próximo argumento.
Primero incrementa el puntero, luego obtiene el valor.

La dirección del puntero genérico se convierte en puntero a carácter mediante:


* (char * *) &(ap)
al cual se le suma el tamaño (en múltiplos del tamaño de un entero), de tal modo que apunte a la
dirección del próximo argumento. Esto se logra con:
( (* (char * *) &(ap)) +=__size(type) )
Luego a este valor se le resta su propio tamaño, para obtener la dirección del argumento actual,
la cual puede expresarse según:
( ((*(char * *)&(ap))+=__size(type)) - (__size(type) ) )
Se la convierte a un puntero al tipo del argumento, mediante:
(type *)( ((*(char * *)&(ap))+=__size(type)) - (__size(type) ) )
Y se indirecciona, para obtener el valor:
( * (type *)( ((*(char * *)&(ap))+=__size(type)) - (__size(type) ) ) )
Nótese que el retorno de este macro debe asignarse a una variable de tipo type.

La expresión *(char **)&ap podría haberse anotado más sencillamente: (char *) ap;

5.2. Estructura de printf.

Ver esquema de printf en KR 7.3, versión ANSI C.

El siguiente es un esquema para printf. Se traen los valores de los argumentos a variables
locales.

void printf(char * format,...)


{
va_list ap;
char *p, *sval ;
int ival ;
double dval;

va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') {putchar(*p); continue;}
switch( *++p) {
case 'd':
ival= va_arg(ap, int);
/* trae el argumento entero a la local ival */

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 55
/* acá debería formatearse el entero y desplegarlo */
break;
case 'f':
/* if ((int)ap&4) ap=(va_list)((int)ap +4); */
dval= va_arg(ap, double);
/* trae el argumento doble a la local dval */
/* acá debería formatearse el doble y desplegarlo */
break;
case 's':
sval= va_arg(ap, char *);
/* trae el puntero al string a la local sval */
/* acá debería desplegarse el string */
break;
}
}
va_end(ap);
}

Esta rutina es dependiente de la forma en que el compilador pasa los argumentos. Es decir si
pasa algunos argumentos en registros o los pasa en el stack. Además depende de la forma en
que el compilador almacena los diferentes tipos. Por ejemplo se ilustra una modificación a los
macros para corregir el valor de ap, en el caso que guarde alineados los dobles en direcciones
que son múltiplos de 8 Bytes. Si el tercer bit es 1, le suma 4 bytes a ap, de tal forma de alinear
correctamente al doble. Este es el caso del compilador lcc.

En la rutina se han empleado variables locales para depositar internamente los valores de los
argumentos, de acuerdo al tipo; y su objetivo es ilustrar el uso de va_arg.

Existen funciones como itoa que transforman un entero en una secuencia de caracteres.
Mediante estas funciones se puede traducir el despliegue de un entero, en determinada base, en
el despliegue de una secuencia de caracteres, lo cual se logra con putchar.

5.3. Estructura de scanf.

El siguiente esquema ilustra el diseño de scanf.

Se traen los valores de los argumentos a punteros locales a la rutina, con el fin de ilustrar el uso
de va_arg. Nótese que en este caso se traen punteros, y que puede cambiarse el valor de la
variable pasada por referencia, mediante indirección.

void scanf(char * format,...)


{
va_list ap;
char *p;
int * pi;
double *pd;
char *pc;

Profesor Leopoldo Silva Bijit 26-05-2008


56 Estructuras de Datos y Algoritmos

va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') continue;
switch( *++p) {
case 'd':
pi=(int *) va_arg(ap, int*);
/* trae a pi un puntero a entero */
/*acá debe ingresarse un entero y depositarlo en *pi */

break;
case 'l':
pd=(double *) va_arg(ap, double*);
/* trae a pd un puntero a doble */
/*acá debe ingresarse un doble y depositarlo en *pd */

break;
case 's':
pc=(char *) va_arg(ap, char *);
/* trae a pc un puntero a char */
/*acá debe ingresarse un string y copiarlo desde pc */

break;
}
}
va_end(ap);
}

Esta rutina debe cuidar que los caracteres ingresados correspondan a lo que se desea leer; que el
espacio asociado al string externo no sea excedido por el largo del string ingresado. También
dependerá de cómo se termine de ingresar los datos, o la acción que deberá tomarse si lo
ingresado no corresponde al tipo que se establece en el string de control.

Funciones como atoi y atof, pasan de secuencias de caracteres a enteros o flotantes; y mediante
éstas, puede implementarse scanf como una secuencia de llamados a getchar.

5.4. Salida formateada en base a llamados al sistema. SPIM.

Deseamos dotar a los programas compilados para MIPS, mediante el compilador lcc, de rutinas
de interfaz con los llamados al sistema que SPIM provee.

Para esto es preciso conocer la forma en que SPIM maneja la salida.


Se tiene cuatro llamados al sistema para implementar salidas.
Al ejecutar el siguiente código assembler, se imprime un entero en la consola de SPIM:
li $v0, 1 # código de llamado al sistema para print_int
li $a0, int # entero a imprimir se pasa en $a0
syscall # print it

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 57

Al ejecutar el siguiente código assembler, se imprime un float en la consola de SPIM:


li $v0, 2 # system call code for print_float
li $f12, float # float to print No se implementa, por el momento.
syscall # print it

Al ejecutar el siguiente código assembler, se imprime un double en la consola de SPIM:


li $v0, 3 # system call code for print_int
li $f12, double # double to print
syscall # print it

Al ejecutar el siguiente código assembler, se imprime un string en la consola de SPIM:


li $v0, 4 # system call code for print_str
la $a0, str # address of string to print
syscall # print the string

El siguiente código en C, muestra el diseño de la rutina printf, empleando los macros vistos
antes, ya que printf es una función con un número variable de argumentos:

void printf(char * format,...)


{
va_list ap;
char *p;

va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') {putchar(*p); continue;}
switch( *++p) {
case 'd':
syscall(va_arg(ap, int),1);
break;
case 'f':
if ((int)ap&4) ap=(va_list)((int)ap +4); /*align double */
syscall(va_arg(ap, double),3);
break;
case 's':
syscall(va_arg(ap, char *),4);
break;
}
}
va_end(ap);
}

La traducción de syscall se traduce en pasar en $a0 y $a1 los argumentos y luego efectuar un:
jal syscall.

Profesor Leopoldo Silva Bijit 26-05-2008


58 Estructuras de Datos y Algoritmos
La implementación de putchar, debe efectuarse en base al llamado al sistema que efectúa la
impresión de un string:

void putchar(char ch)


{char p[2];
p[0]=ch;
p[1]='\0';
syscall(p,4);
}

Debido a que el compilador lcc, introduce en el stack los dobles alineados en palabras dobles, se
requiere forzar el alineamiento en los casos que sea necesario.

Por ejemplo, la compilación de:

printf(" entero= %d doble= %f string=%s \n", 5,12.0000012,"hola");


se traduce en:

la $a0,formato
la $a1,5
l.d $f18,doble #carga en registro un valor doble
mfc1.d $a2, $f18 #mueve a $a2 y $a3 el valor
la $t8, stringhola
sw $t8,16($sp) #pasa el puntero en el stack.
jal printf

.data
.align 0
formato: .asciiz " entero= %d doble= %f string=%s \n"
.align 3
doble:
.word 0x2843ebe8
.word 0x40280000
.align 0
stringhola: .asciiz "hola"

Compilando las rutina de printf, vista antes, se logra:

.rdata
.align 2
_sss: .word 0 # espacio para almacenar un char terminado en \0.

.globl putchar
.text
.align 2
putchar: la $t8,_sss
sw $a0,0($t8)

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 59
move $a0,$t8
la $v0,4 # la $a1,4
syscall # jal syscall
j $ra

.globl printf
#void printf(char * format,...)

.text
.align 2
printf:
.frame $sp,32,$31
addu $sp,$sp,-32
.mask 0xc0800000,-8
sw $s7,16($sp)
sw $s8,20($sp)
sw $ra,24($sp)
sw $a0,32($sp)
sw $a1,36($sp)
sw $a2,40($sp)
sw $a3,44($sp)
#{
la $t8,4+32($sp)
sw $t8,-4+32($sp) # va_start(ap, format);
lw $s8,0+32($sp) # for(p = format; *p ; p++) {
b _tstcnd

_blkfor:lb $t8,($s8) # if( *p !='%') {putchar(*p); continue;}


la $t7,37 # '%'
beq $t8,$t7,_espc
lb $a0,($s8)
jal putchar
b _siga

_espc: la $t8,1($s8) # switch( *++p) {


move $s8,$t8
lb $s7,($t8)
la $t8,100 # 'd'
beq $s7,$t8,_esd
la $t7,102 # 'f'
beq $s7,$t7,_esf
blt $s7,$t8,_siga1
la $t8,115 # 's'
beq $s7,$t8,_ess
b _siga1

_esd: lw $t8,-4+32($sp) # syscall(va_arg(ap, int),1);

Profesor Leopoldo Silva Bijit 26-05-2008


60 Estructuras de Datos y Algoritmos
la $t8,4($t8)
sw $t8,-4+32($sp)
lw $a0,-4($t8)
la $v0,1 # la $a1,1
syscall # jal syscall
b _siga2 # break;

_esf:
lw $t8,-4+32($sp) # if ((int)ap&4) ap=(va_list)((int)ap +4);
and $t8,$t8,4
beq $t8,$0,_nosuma
lw $t8,-4+32($sp)
la $t8,4($t8)
sw $t8,-4+32($sp)
_nosuma:lw $t8,-4+32($sp) # syscall(va_arg(ap, double),3);
la $t8,8($t8)
sw $t8,-4+32($sp)
l.d $f12,-8($t8) #pasa eldoble en reg. doble $f12
la $v0,3 # la $a1,3
syscall # jal syscall
b _siga2 # break;

_ess: lw $t8,-4+32($sp) # syscall(va_arg(ap, char *),4);


la $t8,4($t8)
sw $t8,-4+32($sp)
lw $a0,-4($t8)
la $v0,4 # la $a1,4
syscall # jal syscall

_siga1: # break;
_siga2:
# }

_siga: la $s8,1($s8) # for(p = format; *p ; p++) {


_tstcnd:lb $t8,($s8)
bne $t8,$0,_blkfor

sw $0,-4+32($sp) # va_end(ap);
#}

lw $s7,16($sp)
lw $s8,20($sp)
lw $ra,24($sp)
addu $sp,$sp,32
j $ra
.end printf

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 61
Donde se han parchado los jal syscall por el llamado syscall, y se ha cuidado de pasar el
número del llamado en $v0. Las zonas de parches se destacan en rojo.

El siguiente código implementa, en C, la función scanf en base a los llamados al sistema de


SPIM. Se emplea el compilador lcc para pasar a assembler.

/* Macros for accessing variable arguments <stdarg.h>*/


typedef void *va_list;
#define __size(x) ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int)-1))

/* retorna el tamaño en bytes hasta el siguiente inicio de palabra alineada */

#define va_start(ap, parmN) ((void)((ap) = (va_list)((char *)(&parmN)+__size(parmN))))


#define va_arg(ap, type) (*(type *)(((*(char **)&(ap))+=__size(type))-(__size(type))))
#define va_end(ap) ((void)((ap) = (va_list)0) )
/*
.text
li $v0, 5 # system call code for read_int
syscall # read it. Retorno en $v0

li $v0, 7 # system call code for read_double


syscall # retorno en $f0

li $a1,largo #
li $v0, 8 # system call code for read_str
la $a0, str # buffer of string to read
syscall # read the string
*/

void scanf(char * format,...)


{
va_list ap;
char *p;
int * pi;
double *pd;
char *pc;
const char * cp;
int largo;

va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') continue;
switch( *++p) {
case 'd':
pi=(int *) va_arg(ap, int*);
*pi=syscall(5);
/*printf(" *pi=%d \n", *pi);*/

Profesor Leopoldo Silva Bijit 26-05-2008


62 Estructuras de Datos y Algoritmos
break;
case 'l':
pd=(double *) va_arg(ap, double*);
*pd=syscall(7);
/*printf(" *pd=%f \n", *pd);*/
break;
case 's':
pc=(char *) va_arg(ap, char *);
cp=pc; while(*cp++) continue; /*calcula largo buffer*/
largo=cp - pc;
syscall(pc,largo,8);
/*printf(" pc=%s \n", pc);*/
break;
}
}
va_end(ap);
}

Los syscall generan jal syscall, que deben parcharse, y también el paso de los argumento a esos
llamados.

.rdata
.align 2
_sss: .word 0

.globl getchar
.text
.align 2
getchar:
la $a0,_sss # syscall(pc,2,8);
li $a1,2
la $v0,8
syscall
la $t8,sss
lw $v0,0($t8)
j $ra

.globl scanf
.text
#void scanf(char * format,...)
.text
.align 2

scanf:
.frame $sp,56,$31
addu $sp,$sp,-56
.mask 0xc0e00000,-24

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 63
sw $s5,16($sp)
sw $s6,20($sp)
sw $s7,24($sp)
sw $s8,28($sp)
sw $ra,32($sp)
sw $a0,56($sp)
sw $a1,60($sp)
sw $a2,64($sp)
sw $a3,68($sp)
#{

la $t8,4+56($sp) # va_start(ap, format);


sw $t8,-4+56($sp)
# for(p = format; *p ; p++) {

lw $s8,0+56($sp)
b _tstscn

# if( *p !='%') continue;

_blkscn:lb $t8,($30)
la $t7,37 # '%'
beq $t8,$t7,_swscn
b _cntscn

_swscn:la $t8,1($s8) # switch( *++p) {


move $s8,$t8
lb $s5,($t8)
la $t8,108 # 'l'
beq $s5,$t8,_lfscn
bgt $s5,$t8,_tsts
la $t8,100 # 'd'
beq $s5,$t8,_dscn
b _brkscn
_tsts: la $t8,115 # 's'
beq $s5,$t8,_sscn
b _brkscn

_dscn: lw $t8,-4+56($sp) # pi=(int *) va_arg(ap, int*);


la $t8,4($t8)
sw $t8,-4+56($sp)
lw $t8,-4($t8)
sw $t8,-8+56($sp)
la $v0,5 # *pi=syscall(5);
syscall
lw $t7,-8+56($sp)
sw $v0,($t7)

Profesor Leopoldo Silva Bijit 26-05-2008


64 Estructuras de Datos y Algoritmos
b _brkscn # break;

_lfscn: lw $t8,-4+56($sp) # pd=(double *) va_arg(ap, double*);


la $t8,4($t8)
sw $t8,-4+56($sp)
lw $t8,-4($t8)
sw $t8,-12+56($sp)
la $v0,7 # *pd=syscall(7);
syscall
lw $t7,-12+56($sp)
s.d $f0,($t7) # retorna el doble en $f0
b _brkscn # break;

_sscn: lw $t8,-4+56($sp) # pc=(char *) va_arg(ap, char *);


la $t8,4($t8)
sw $t8,-4+56($sp)
lw $s6,-4($t8)

move $s7,$s6 # cp=pc; while(*cp++) continue;


_tsteos:move $t8,$s7
la $s7,1($t8)
lb $t8,($t8)
bne $t8,$0,_tsteos
la $t8,0($23) # largo=(cp) - pc;
move $t7,$22
subu $t8,$t8,$t7
sw $t8,-16+56($sp)
move $a0,$22 # syscall(pc,largo,8);
lw $a1,-16+56($sp)
la $v0,8
syscall
# break;
_brkscn:

# }

_cntscn:la $30,1($30) # for(p = format; *p ; p++) {


_tstscn:lb $24,($30)
bne $24,$0,_blkscn

sw $0,-4+56($sp) # va_end(ap);
#}

lw $s5,16($sp)
lw $s6,20($sp)
lw $s7,24($sp)
lw $s8,28($sp)

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 65
lw $ra,32($sp)
addu $sp,$sp,56
j $ra
.end scanf

Si estas rutinas se agregan al código del trap.handler de SPIM, pueden emplearse llamados a
printf, scanf, getchar y putchar, sin tener que incluir dichos códigos junto a los programas
fuentes.

5.5. Desarrollo de printf en base a putchar.

Puede implementarse printf en términos de putchar. Es decir una función que saca un carácter
hacia el medio de salida. La implementación de putchar suele efectuarse programando la puerta
serial de un microcontrolador, para esto bastan muy pocas instrucciones (no más de 10).

Entonces el núcleo de printf puede describirse según:

Dejando en la función _doprnt la tarea de desplegar en la salida estándar los diferentes


argumentos. Esta función inspecciona el string de control format, y de acuerdo a la
especificación de la letra ubicada después del carácter %, ubica el tipo del argumento, y usa
va_arg con dicho tipo para tomar el valor y convertirlo a secuencia de caracteres.

#include <stdarg.h>
int printf(const char *format, ...)
{
va_list ap;
int retval;

va_start(ap, format);

retval = _doprnt(format, ap, stdout);

va_end(ap);

return retval;
}

Se ilustra una rutina bajada de la red (de la cual perdí la referencia), con pequeñas
modificaciones que implementa printf. Lo cual muestra que la función printf está formada por
numerosas instrucciones, lo cual debe ser tenido en cuenta al emplearla en microcontroladores.
En estos casos existen versiones recortadas, mediante la no implementación de algunos
formatos.

#include <stdio.h> /* usa prototipo putchar y definición de NULL*/


#include <ctype.h> /* usa prototipo de isdigit */
#include <math.h> /* usa frexp */

Profesor Leopoldo Silva Bijit 26-05-2008


66 Estructuras de Datos y Algoritmos

/* Macros for accessing variable arguments <stdarg.h>*/


typedef void *va_list;
#define __size(x) ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int)-1))
/* retorna el tamaño en bytes hasta el siguiente inicio de palabra alineada */

#define va_start(ap, parmN) ((void)((ap) = (va_list)((char *)(&parmN)+__size(parmN))))


#define va_arg(ap, type) (*(type *)(((*(char **)&(ap))+=__size(type))-(__size(type))))
#define va_end(ap) ((void)((ap) = (va_list)0) )

/*
* This version only supports 32 bit floating point
*/
#define value long
#define NDIG 12 /* máximo número de dígitos ha ser impresos */
#define expon int
const static unsigned value
dpowers[] = {1, 10, 100, 1000, 10000, 100000L, 1000000L,
10000000L,10000000L,100000000L};
const static unsigned value
hexpowers[] = {1, 0x10, 0x100, 0x1000,0x10000L, 0x100000L,0x1000000L, 0x10000000L};

const static unsigned value


octpowers[] = {1, 010, 0100, 01000, 010000, 0100000, 01000000L,010000000L,
0100000000L, 01000000000L, 010000000000L};
static const double
powers[] ={1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e20,1e30,};
static const double
npowers[] ={1e-0,1e-1,1e-2,1e-3,1e-4,1e-5,1e-6,1e-7,1e-8,1e-9,1e-10,1e-20,1e-30,};

/* this routine returns a value to round to the number of decimal places specified */
double fround(unsigned char prec)
{ /* prec is guaranteed to be less than NDIG */
if(prec > 10) return 0.5 * npowers[prec/10+9] * npowers[prec % 10];
return 0.5 * npowers[prec];
}

/* this routine returns a scaling factor equal to 1 to the decimal power supplied */
static double scale(expon scl)
{ if(scl < 0) { scl = -scl;
if(scl > 10) return npowers[scl/10+9] * npowers[scl%10];
return npowers[scl];
}
if(scl > 10) return powers[scl/10+9] * powers[scl%10];
return powers[scl];
}

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 67
#define OPTSIGN 0x00
#define SPCSIGN 0x01
#define MANSIGN 0x02
#define NEGSIGN 0x03
#define FILL 0x04
#define LEFT 0x08
#define LONG 0x10
#define UPCASE 0x20
#define TEN 0x00
#define EIGHT 0x40
#define SIXTEEN 0x80
#define UNSIGN 0xC0
#define BASEM 0xC0
#define EFMT 0x100
#define GFMT 0x200
#define FFMT 0x400
#define ALTERN 0x800
#define DEFPREC 0x1000

#define pputc(c) if(pb->ptr) *pb->ptr++ = (c); else pb->func(c)

struct __prbuf
{ char * ptr;
void (* func)(char);
} pb;

void _doprnt(struct __prbuf * pb, const register char * f, va_list ap )


{ int prec;
char c;
int width;
unsigned flag;
double fval;
int exp;
union {
unsigned value _val;
struct { char * _cp;
unsigned _len;
} _str;
double _integ;
} _val;
#define val _val._val /*definiciones para los campos de la unión val */
#define cp _val._str._cp
#define len _val._str._len
#define integ _val._integ
flag = 0;
while(NULL != (c = (char)*f++)) {
if(c != '%') { pputc(c); continue; }

Profesor Leopoldo Silva Bijit 26-05-2008


68 Estructuras de Datos y Algoritmos
width = 0;
flag = 0;
for(;;) {
switch(*f) {
case '-':
flag |= LEFT;
f++;
continue;
case ' ':
flag |= SPCSIGN;
f++;
continue;
case '+':
flag |= MANSIGN;
f++;
continue;
case '#':
flag |= ALTERN;
f++;
continue;
case '0':
flag |= FILL;
f++;
continue;
}
break;
}
if(flag & MANSIGN) flag &= ~SPCSIGN;
if(flag & LEFT) flag &= ~FILL;
if(isdigit((unsigned)*f)) {
width = 0;
do width = width*10 + *f++ - '0';
while(isdigit((unsigned)*f));
}
else if(*f == '*') {
width = va_arg(ap, int);
f++;
}
if(*f == '.')
if(*++f == '*') {prec = va_arg(ap, int);f++;}
else {prec = 0;
while(isdigit((unsigned)*f)) prec = prec*10 + *f++ - '0';}
else {prec = 0;flag |= DEFPREC;}
loop:
switch(c = *f++) {
case 0: return;
case 'l': flag |= LONG; goto loop;

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 69
case 'f': flag |= FFMT; break;
case 'E': flag |= UPCASE;
case 'e': flag |= EFMT;break;
case 'g': flag |= GFMT;break;
case 'o': flag |= EIGHT;break;
case 'd':
case 'i': break;
case 'X':
case 'p': flag |= UPCASE;
case 'x': flag |= SIXTEEN;break;
case 's': cp = va_arg(ap, char *);
if(!cp) cp = "(null)";
len = 0;
while(cp[len])len++;
dostring:
if(prec && prec < len) len = prec;
if(width > len) width -= len; else width = 0;
if(!(flag & LEFT)) while(width--) pputc(' ');
while(len--) pputc(*cp++);
if(flag & LEFT) while(width--) pputc(' ');
continue;
case 'c': c=va_arg(ap, int);
default: cp = &c;len = 1; goto dostring;
case 'u': flag |= UNSIGN;break;
}
if(flag & (EFMT|GFMT|FFMT)) {
if(flag & DEFPREC) prec = 6;
fval=va_arg(ap, double);
if(fval < 0.0) {fval = -fval;flag |= NEGSIGN;}
exp = 0;
frexp(fval, &exp); /* get binary exponent */
exp--; /* adjust 0.5 -> 1.0 */
exp *= 3;
exp /= 10; /* estimate decimal exponent */
if(exp <= 0) c = 1; else c = exp;
if(!(flag & ALTERN) && flag & FFMT && prec == 0) {
val = (long)(fval + 0.5);
flag |= LONG;
goto integer;
}
if(!(flag & ALTERN) && flag & GFMT && exp >= 0 && c <= prec) {
integ = fval + fround(prec - c);
if(exp > sizeof dpowers/sizeof dpowers[0] ||
integ - (float)(unsigned long)integ < fround(prec-c-1)) {
val = (long)integ;
flag |= LONG;
prec = 0;

Profesor Leopoldo Silva Bijit 26-05-2008


70 Estructuras de Datos y Algoritmos
goto integer;
}
}
/* use e format */
if(flag & EFMT || flag & GFMT && (exp < -4 || exp >= (int)prec)) {
if(exp > 0) {
fval *= scale(-exp);
if(fval >= 10.0) {fval *= 1e-1; exp++;}
}
else if(exp < 0) { fval *= scale(-exp);
if(fval < 1.0) { fval *= 10.0;exp--;}
}
if(flag & GFMT) prec--;
fval += fround(prec);
if(flag & GFMT && !(flag & ALTERN)) {
/* g format, precision means something different */
if(prec > (int)(sizeof dpowers/sizeof dpowers[0]))
prec = sizeof dpowers/sizeof dpowers[0];
val = (long)(fval * scale(prec));
if(val) { while(val % 10 == 0) { prec--; val /= 10; }}
else prec = 0;
}
if(fval != 0.0) {
while(fval >= 10.0) { fval *= 1e-1; exp++; if(flag & EFMT)prec++;}
while(fval < 1.0) {fval *= 10.0; exp--; if(flag & EFMT) prec--;}
}
width -= prec + 5;
if(prec || flag & ALTERN) width--;
if(flag & (MANSIGN|SPCSIGN))width--;
if(exp >= 100 || exp <= -100) /* 3 digit exponent */ width--;
if(flag & FILL) {
if(flag & MANSIGN) pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN) pputc(' ');
while(width > 0) {pputc('0');width--;}
} else {
if(!(flag & LEFT)) while(width > 0) { pputc(' '); width--;}
if(flag & MANSIGN) pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)pputc(' ');
}
pputc((int)fval + '0');
if(prec || flag & ALTERN) {
if(prec > (int)(sizeof dpowers/sizeof dpowers[0]))
c = sizeof dpowers/sizeof dpowers[0];
else c = prec;
pputc('.');
prec -= c;
integ = (double)(unsigned long)fval;

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 71
val = (unsigned long)((fval - integ) * scale(c));
while(c) { pputc('0' + (int)((long)val/dpowers[--c]) % 10);}
while(prec) { pputc('0');prec--; }
}
if(flag & UPCASE) pputc('E');else pputc('e');
if(exp < 0) { exp = -exp;pputc('-');} else pputc('+');
if(exp >= 100) { pputc(exp / 100 + '0');exp %= 100;}
pputc(exp / 10 + '0');
pputc(exp % 10 + '0');
if((flag & LEFT) && width) do pputc(' ');while(--width);
continue;
}
/* here for f format */
frexp(fval, &exp); /* get binary exponent */
exp--; /* adjust 0.5 -> 1.0 */
exp *= 3;
exp /= 10; /* estimate decimal exponent */
if(flag & GFMT) {
if(exp < 0) prec -= exp-1;
val = (unsigned long)fval;
for(c = 1 ; c != sizeof dpowers/sizeof dpowers[0] ; c++)
if(val < dpowers[c]) break;
prec -= c;
val = (unsigned long)((fval-(double)val) * scale(prec)+0.5);
while(prec && val % 10 == 0) {val /= 10; prec--;}
}
if(prec <= NDIG) fval += fround(prec);
if(exp > (int)(sizeof dpowers/sizeof dpowers[0])) {
exp -= sizeof dpowers/sizeof dpowers[0];
val = (unsigned long)(fval * scale(-exp));
fval = 0.0;
} else { val = (unsigned long)fval; fval -= (float)val;exp = 0;}
for(c = 1 ; c != sizeof dpowers/sizeof dpowers[0] ; c++)
if(val < dpowers[c])break;
width -= prec + c + exp;
if(flag & ALTERN || prec)width--;
if(flag & (MANSIGN|SPCSIGN))width--;
if(flag & FILL) {
if(flag & MANSIGN)pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN) pputc(' ');
while(width > 0) { pputc('0'); width--;}
} else {
if(!(flag & LEFT))
while(width > 0) {pputc(' ');width--; }
if(flag & MANSIGN)pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)pputc(' ');
}

Profesor Leopoldo Silva Bijit 26-05-2008


72 Estructuras de Datos y Algoritmos
while(c--) pputc('0' + (int)((long)val/dpowers[c]) % 10);
while(exp > 0) { pputc('0');exp--;}
if(prec > (int)(sizeof dpowers/sizeof dpowers[0]))
c = sizeof dpowers/sizeof dpowers[0];else
c = prec;
prec -= c;
if(c || flag & ALTERN) pputc('.');
val = (long)(fval * scale(c));
while(c) {pputc('0' + (int)((long)val/dpowers[--c]) % 10);}
while(prec) {pputc('0'); prec--;}
if((flag & LEFT) && width) do pputc(' ');
while(--width); continue;
}
if((flag & BASEM) == TEN) {
if(flag & LONG) val=va_arg(ap, long);
else val=va_arg(ap, int);
if((value)val < 0) {flag |= NEGSIGN;val = ~val+1;}
} else {
if(flag & LONG) val=va_arg(ap, unsigned long);
else val= va_arg(ap, unsigned);
}
integer:
if(prec == 0 && val == 0) prec++;
switch((unsigned char)(flag & BASEM)) {
case TEN:
case UNSIGN:
for(c = 1 ; c != sizeof dpowers/sizeof dpowers[0] ; c++)
if(val < dpowers[c])break;
break;
case SIXTEEN:
for(c = 1 ; c != sizeof hexpowers/sizeof hexpowers[0] ; c++)
if(val < hexpowers[c]) break;
break;
case EIGHT:
for(c = 1 ; c != sizeof octpowers/sizeof octpowers[0] ; c++)
if(val < octpowers[c]) break;
break;
}
if(c < prec) c = prec; else if(prec < c) prec = c;
if(width && flag & NEGSIGN) width--;
if(width > prec) width -= prec; else width = 0;
if((flag & (FILL|BASEM|ALTERN)) == (EIGHT|ALTERN)) {
if(width) width--;
} else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {
if(width > 2) width -= 2; else width = 0;
}
if(flag & FILL) {

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 73
if(flag & MANSIGN) pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN) pputc(' ');
else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {
pputc('0');
pputc(flag & UPCASE ? 'X' : 'x');
}
if(width) do pputc('0'); while(--width);
}
else { if(width && !(flag & LEFT)) do pputc(' '); while(--width);
if(flag & MANSIGN) pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN) pputc(' ');
if((flag & (BASEM|ALTERN)) == (EIGHT|ALTERN)) pputc('0');
else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {
pputc('0');
pputc(flag & UPCASE ? 'X' : 'x');
}
}
while(prec > c) pputc('0');
while(prec--) {
switch((unsigned char)(flag & BASEM)) {
case TEN:
case UNSIGN:
c = (int)((long)val / dpowers[prec]) % 10 + '0';break;
case SIXTEEN:
c = (flag & UPCASE ? "0123456789ABCDEF" :
"0123456789abcdef")[(int)(val / hexpowers[prec]) & 0xF];
break;
case EIGHT: c = ( (int)((long)val / octpowers[prec]) & 07) + '0';
break;
}
pputc(c);
}
if((flag & LEFT) && width) do pputc(' '); while(--width);
}

void miputc(char ch)


{ putchar(ch);}

void mvprintf(const char * f, va_list ap)


{
pb.ptr = 0;
pb.func = miputc; /*putchar */
va_start(ap, f);
_doprnt(&pb, f, ap);
va_end(ap);

Profesor Leopoldo Silva Bijit 26-05-2008


74 Estructuras de Datos y Algoritmos
}

char * mvsprintf(char * wh, const char * f, va_list ap)


{ pb.ptr = wh;
pb.func = (void (*)(char))NULL;
va_start(ap, f);
_doprnt(&pb, f, ap);
*pb.ptr++ = 0;
va_end(ap);
return ( char *)(pb.ptr - wh);
}
void mprintf(const char * f, ...)
{ va_list ap;
struct __prbuf pb;
pb.ptr = 0;
pb.func = miputc;
va_start(ap, f);
_doprnt(&pb, f, ap);
va_end(ap);
}

/* mini test */
int main(void)
{ int x=15, y=2678; float f=3.2e-5;

mprintf(" x = %X y = %d\n", x, y);


mprintf(" f = %g \n", f);

return(0);
}

Al disponer del código fuente, éste puede adaptarse a las necesidades del usuario. En caso de ser
empleado en un microcontrolador, con el objeto de disminuir la memoria ocupada por printf, se
pueden recortar algunos modos que no se requieran.

6. Algunas rutinas matemáticas.

Se muestran algunos diseños de funciones matemáticas de biblioteca.

6.1. Trigonométricas.

Para desarrollar el algoritmo, consideremos la relación:


Sen(-x) = -sen(x) lo cual permite mediante un cambio de variable y signo efectuar cálculos
sólo para x>=0.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 75

La variable x se expresa en radianes, y es periódica. Se muestra la gráfica para un período.

Figura A2.10 Función seno.

plot(sin(x), x=0..2*Pi);
Si efectuamos el cambio de variable, w = x/2*Pi, tendremos:
plot(sin(2*Pi*w),w=0..1); cuya gráfica se ilustra a continuación:

Figura A2.11 Reducción a intervalo entre 0 y 1.

Reducción al primer período:


Para considerar la naturaleza periódica de la función, podemos considerar el cambio de
variables:
Z = w – floor(w), cuya gráfica se obtiene con plot(w - floor(w), w=0..5);

Profesor Leopoldo Silva Bijit 26-05-2008


76 Estructuras de Datos y Algoritmos

Figura A2.12. floor(w).

Que mapea los diferentes intervalos de w entre i e i+1 en el intervalo de z entre 0 y 1. La


función floor(w) trunca el número real al entero menor; en el caso de reales positivos, equivale
al truncamiento del número. Por ejemplo: floor(1.5) = 1.0

Después de este cambio de variables, los valores del argumento estarán acotados. De esta forma
cuando se calcule con valores reales elevados, éstos se reducen a valores entre 0 y 4, y no se
producirán errores cuando se calculen las potencias del argumento al evaluar la serie.
Si efectuamos: m= 4*(w-floor(w)) las variaciones de m serán en el intervalo entre 0 y 4,
cuando w cambia entre cualquier inicio de un período hasta el final de ese período.

Entonces para todos los reales positivos (representables) de w, se puede calcular en el primer
período, para valores de m entre 0 y 4:
plot( sin(2*Pi*m/4 ), m=0..4);

Figura A2.13. Reducción al primer período.

Reducción al primer cuadrante:


Para 4 > m > 2 se tiene que f(m) = - f(m-2) y si se efectúa m=m-2, se tendrá que 0<m<2.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 77
Ahora m está restringido a tomar valores entre 0 y 2.
Para 2> m > 1 se tiene f(m) = f(2-m) y si se efectúa m= 2-m, se tendrá que 0 < m < 1, lo cual
reduce los cálculos al primer cuadrante.
El intervalo donde se calculará el polinomio de aproximación se muestra en la siguiente gráfica:
plot( sin(2*Pi*m/4 ),m=0..1);

Figura A2.14. Reducción al primer cuadrante.

Entonces puede describirse el siguiente algoritmo:

signo = 1.0; /*describe signo positivo */


if(x < 0.0) { x = -x; signo = -signo; } /*Desde ahora sólo argumentos positivos */
x /= TWO_PI; /* 1 radian = 180/Pi Grados. Desde ahora: Inf > x > 0 */
x = 4.0 * (x - floor(x)); /* Reduce al primer período. Desde ahora 4 >= x >= 0 */
if(x > 2.0) { x -= 2.0; signo = -signo;} /* 2 >= x >=0 */
if( x > 1.0) x = 2.0 - x; /* Reduce al primer cuadrante. 1>= x >=0 */

Puede compararse la aproximación por series de potencia (de dos y tres términos) con el
polinomio de Pade, mediante:
plot([x-x^3/6,x-x^3/6+x^5/120, pade(sin(x),x=0,[9,6])], x=0.7..1.7,
y= 0.65..1,color=[red,blue,black], style=[point,line,point]);

Profesor Leopoldo Silva Bijit 26-05-2008


78 Estructuras de Datos y Algoritmos

Figura A2.15. Series y polinomio de Pade.

Cuando m varía entre 0 y 1, el x de la gráfica anterior varía entre 0 y 2*Pi/4 = 1,571


Se muestra a partir de la ordenada 0,65 para ampliar la zona en que las aproximaciones difieren.

Es preciso calcular polinomios, puede emplearse la función estándar poly, descrita en math.h
Si por ejemplo se desea calcular:

p(x) = d[4]*(x**4)+d[3]*(x**3)+d[2]*(x**2)+d[1]*(x)+d[0]

puede describirse por:

(((d[4]*x + d[3] )*x + d[2] )*x + d[1] )*x +d[0]

Con algoritmo: poli=d[n]; i=n; while (i >0) {i--; poli = poli* x + d[ i-1]};

Debido a que los polinomios son de potencias pares en el denominador, se efectúa el reemplazo
x por x*x. Y para obtener potencias impares en el numerador se multiplica el polinomio del
numerador por x.

El algoritmo completo es:

#include <math.h>
/*Calcula para n=4 el polinomio: d[4]*(x**4)+d[3]*(x**3)+d[2]*(x**2)+d[1]*(x)+d[0] */
double eval_poly(register double x, const double *d, int n)
{ int i;
register double res;
res = d[i = n];
while ( i ) res = x * res + d[--i];
return res;

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 79
}

#define PI 3.14159265358979
#define TWO_PI 6.28318530717958
double seno(double x)
{ static const double coeff_a[] = { 207823.68416961012, -76586.415638846949,
7064.1360814006881, -237.85932457812158, 2.8078274176220686 };
static const double coeff_b[] = { 132304.66650864931, 5651.6867953169177,
108.99981103712905, 1.0 };
register double signo, x2;
signo = 1.0;
if(x < 0.0) { x = -x; signo = -signo; } /*Solo argumentos positivos */
x /= TWO_PI; x = 4.0 * (x - floor(x));
if(x > 2.0) { x -= 2.0; signo = -signo;}
if( x > 1.0) x = 2.0 - x;
x2 = x * x;
return signo * x * eval_poly(x2, coeff_a, 4) / eval_poly(x2, coeff_b, 3);
}

Empleando Mapple puede obtenerse el polinomio de Pade, que aproxima a la función seno.
with(numapprox):
pade(sin(x), x=0, [9,6]);

1768969 9 36317 7 80231 5 8234 3


(-------------------- x - -------------- x + ------------- x - -------- x + x ) /
4763930371200 472612140 14321580 55083

631 2 3799 4 911 6


(1 + --------- x + ------------- x + --------------- x )
36722 28643160 1890448560

El siguiente comando dibuja el polinomio:


plot(pade(sin(x),x=0,[9,6]),x=0..10);

Figura A2.16. Polinomio de Pade.

Profesor Leopoldo Silva Bijit 26-05-2008


80 Estructuras de Datos y Algoritmos
Se aprecia que para x>6 la aproximación de la función seno no es buena.

Se requiere modificar el argumento de la función, de acuerdo al algoritmo:


a:=pade(sin(2*Pi*x/4), x=0, [9,6]);
evalf(denom(a)/10^11,17); Calcula el denominador, dividido por 10^11, con 17 cifras.

24391.323500544000+1034.1371819921864*x^2+19.695328959656098*x^4+
.17656643195797582*x^6
evalf(expand(numer(a)/10^11),17); Calcula el numerador, dividido por 10^11, con 17 cifras.

38313.801360320554*x-14131.500385136448*x^3+1306.7304862998132*x^5-
44.226197226558042*x^7+.52731372638787005*x^9

Los valores de los coeficientes son los que se emplean en la función.

La gráfica del polinomio es la zona donde será evaluado, se muestra a continuación:


plot(pade(sin(2*Pi*x/4),x=0,[9,6]), x=0..4);

Figura A2.17. Polinomio de Pade entre 0 y 4.

6.2. Manipulación de flotantes.

La función floor está basada en el truncamiento de la parte fraccionaria del número real.
Si se tiene: Double d, t ;
Entonces t = (double)(long)(d); es el número truncado, con parte fraccionaria igual a cero.

Primero el molde (long) transforma d a un entero, luego el molde o cast (double) transforma ese
entero a doble.
Si la cantidad de cifras enteras de un double, no pueden ser representadas en un entero largo, la
expresión será errónea. Por ejemplo si el entero largo tiene 32 bits, si las cifras enteras del doble
exceden a 231 -1 se tendrá error.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 81
Un doble IEEE 754 ocupa 64 bits, con un long double de 80 bits, no hay problemas en el
truncamiento.
Un double de 64 bits tiene el rango: 1.7 * (10**-308) to 1.7 * (10**+308) .
Un long double de 80 bits tiene el rango: 3.4 * (10**-4932) to 1.1 * (10**+4932) .

Double floor( double x)


{ double i;
i = (double)(long double)(x);
if(i > x) return i - 1.0;
return i;
}

Luego pueden derivarse el resto de las funciones trigonométricas.


La función coseno, se calcula

#define PImedio 1.570796326794895


double coseno(double x)
{ return seno(x + PImedio); }

La función tangente, se deriva de su definición:


double tangente(double x)
{ return seno(x)/coseno(x); }

El valor absoluto de un doble, se calcula según:

double fabs(double d)
{ if(d < 0.0) return -d; else return d; }

6.3. Acceso a los bits de un número.

En ocasiones resulta conveniente tener acceso a las representaciones internas de los números.
Los programas de este tipo deben considerar el ordenamiento de los bytes dentro de la palabra
de memoria; es decir si son de orden big-endian o little endian.

Estudiaremos varias alternativas de tratamiento. Desde la más simple de interpretar los bytes
dentro de la palabra, pasando por interpretar los enteros largos que constituyen una palabra
mayor; a métodos más generales que emplean uniones y campos; esta última no se recomienda
ya que es dependiente de la implementación del compilador.

Analizaremos la función estándar frexp.

La función frexp extrae la mantisa y el exponente de un real:

double frexp(double x, int * exponente)

Dado un número real de doble precisión x, la función frexp calcula la mantisa m (como un real
de doble precisión) y un entero n (exponente) tal que:

Profesor Leopoldo Silva Bijit 26-05-2008


82 Estructuras de Datos y Algoritmos

x = m * (2n) con: 0.5 =< m < 1

Como las funciones, en el lenguaje C, sólo retornan un valor, y si éste es el de la mantisa, debe
pasarse un segundo argumento por referencia: la dirección de un entero; y la función devolverá
el exponente escrito en el entero.
Por esta razón el segundo argumento es un puntero a entero. El valor de la mantisa es el valor
retornado por la función.

Se tiene la siguiente representación externa para un número real:

x = (-1)S 1.M2 2ee

Donde S es el bit del signo, M2 la mantisa binaria, y ee es la representación externa del


exponente, esto asumiendo representación de reales normalizados en formato IEEE 754.

Dividiendo y multiplicando por dos, obtenemos: x = (-1)S 1.M2 2 -1 2 ee + 1

Entonces el número real que debe retornar la función, como mantisa mayor que un medio y
menor que uno es:
mantisa = (-1)S 1.M2 2 -1

y el exponente, que retorna frexp, debe ser:


exponente = ee + 1.

La función debe extraer la representación interna del exponente, pasarla a representación


externa y sumarle uno para formar el exponente, que retorna la función. Por otra parte debe
convertir el exponente externo con valor menos uno a representación interna, y sobrescribirlo
en la parte binaria dedicada al exponente.

Se tiene que: exponente externo = exponente interno – polarización.

El exponente externo se representa como un número con signo en complemento a dos, y la


polarización es tal que el número más negativo (que tiene simétrico positivo) se represente
como una secuencia de puros ceros.

Para flotantes de simple precisión, que empleen 32 bits, se dedican 8 bits al exponente, el
mayor positivo en complemento a dos es, en decimal, 127; que equivale a 01111111 en binario.
El número más negativo, -127, se representa en complemento a dos como: 10000001,
cumpliéndose que, para este número, la representación interna es: 00000000. La polarización
para tipo float es 127, en decimal.

Para reales de precisión doble, se emplean 64 bits, y 11 para el exponente; en este caso la
polarización es 1023 en decimal, con representación binaria complemento a dos: 01111111111
(0x3FF en hexadecimal).

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 83
Entonces para doble precisión, para un exponente externo igual a menos uno, debe escribirse
en la parte que representa el exponente interno: -1 + 1023 = 1022 que equivale a
01111111110 (0x3FE).

Para el exponente retornado por la función se tiene: ee + 1 = ei – 1023 + 1 = ei –1022.


6.3.1. Acceso por caracteres (bytes).
Para extraer el exponente, supongamos que el puntero a carácter pc apunta al byte más
significativo del double; y que ps apunta al segundo.
unsigned char * pc;
unsigned char * ps;
unsigned int ei;
int exponente;

Los últimos 7 bits del primer byte (*pc & 0x7F) son los primeros siete del exponente (ya que el
primero se emplea para el signo del número).

Los primeros 4 bits del segundo, son los últimos 4 del exponente interno. Para esto es preciso
desplazar en forma lógica, en cuatro bits, esto se logra con: *ps>>4.

Para formar el exponente interno se requiere desplazar, en forma lógica, los primeros siete bits
en cuatro posiciones hacia la izquierda.

Entonces: ei = (*pc & 0x7F)<<4 | (*ps>>4) forma el exponente interno, como una secuencia
binaria de 11 bits. Al depositarlo en un entero sin signo, los primeros bits quedan en cero
(desde el doceavo hasta el largo del entero).
Finalmente, se logra:
exponente = ei –1022;

Para sobrescribir el número 0x3FE, en las posiciones en que va el exponente interno, se requiere
modificar los últimos siete bits del primer byte, para no alterar el signo del número. Esto se
logra haciendo un and con la máscara binaria 10000000(0x80) y luego un or con la máscara
binaria 00111111(0x3F)
Es decir:
*pc = (*pc & 0x80) | 0x3F;

Para el segundo byte, sólo se deben sobrescribir los primeros cuatro. Esto se logra haciendo un
and con la máscara binaria 00001111(0x0F) y luego un or con la máscara binaria
11100000(0xE0)
Es decir:
*ps = (*ps & 0x0F) | 0xE0;

El resto de los bits con la mantisa del número no deben modificarse.

Para apuntar a los primeros dos bytes, debe conocerse el orden de los bytes dentro de las
palabras de la memoria. Esto es dependiente del procesador. En algunos sistemas el primer byte
(el más significativo dentro del double) tiene la dirección menor, en otros es la más alta.

Profesor Leopoldo Silva Bijit 26-05-2008


84 Estructuras de Datos y Algoritmos

Como casi todos los tipos de datos que maneja un procesador suelen ser múltiplos de bytes,
para obtener la dirección de una variable de cierto tipo (en este caso de un double) en unidades
de direcciones de bytes puede escribirse:

unsigned char * pc = (unsigned char *)&number;


unsigned char * ps;

El moldeo (cast) convierte la dirección de la variable number en un puntero a carácter.

Luego de esto, considerando que un double está formado por 8 bytes se tiene: pc += 7; ps=pc-1;
para sistemas en que el byte más significativo tiene la dirección de memoria más alta.
O bien: ps = pc +1; si el byte más significativo tiene la dirección menor; en este caso, no es
preciso modificar pc.

Entonces el código completo de la función frexp puede escribirse:

double frexp(double number, int *exponent)


{
unsigned char * pc = (unsigned char *)&number;
unsigned char * ps;
unsigned int ei;
pc += 7; ps=pc-1; /* Big endian. O bien: ps=pc +1, para little endian*/
ei = ((*pc & 0x7F)<<4) | (*ps>>4); /*extrae exponente interno */
*exponent = ei - 1022; /*escribe en el entero que se pasa por referencia*/
*pc = (*pc & 0x80) | 0x3F; /*deja exponente igual a -1 */
*ps = (*ps & 0x0F) | 0xE0;
return( number);
}
Sin embargo esta rutina tiene varias limitaciones. No trata número sub-normales y no detecta
representaciones de infinito y NaN.
6.3.2. Uso de dos enteros largos sin signo, para representar los bits de un double.
Obviamente esto sólo puede aplicarse si los enteros largos son de 32 bits.

Considerando ei como el exponente interno y ee como el exponente externo, se tienen:

ee = ei -1023; ei = ee + 1023

Entonces, de acuerdo a la interpretación IEEE 754, se tiene que:

Con ei = 0 y M2 != 0 se tienen números subnormales que se interpretan según:


N = (-1)S*0.M2*pow(2, -1022)

Con ei = 0 y M2 == 0 se tienen la representación para el 0.0 según:


N = (-1)S*0.0

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 85
Con 0< ei < 2047 se logran en forma externa: -1023 < ee < 1024, se tienen
representaciones para números normales, según:
N = (-1)S*1.M2*pow(2, ee)

Con ei = 2047 y M2 == 0 (ee = 1024) se tiene la representación para el según:


N = (-1)S*INF

Con ei = 2047 y M2 != 0 se tienen la representación para el según:


N = NaN

El exponente interno se está considerando de 11 bits, sin signo. En la norma IEEE 754 debe
considerarse números con signo. Para los dos últimos casos esto implica ei = -1.
Entonces con las definiciones:
unsigned long int *pm2=(unsigned long int *)&number;
unsigned long int *pm1=pm2+1;

Podemos apuntar con pm1 al entero largo más significativo, donde se almacena el signo, los 11
bits del exponente y 4 bits de la mantisa.

Podemos conocer el signo del número mediante:


int signo=( int)((*pm1)>>31);
Dejando en signo un uno si el número es negativo; y cero si es positivo.

La extracción del exponente interno, sin signo, se logra con:


unsigned int ei= (unsigned int)(((*pm1)<<1)>>21);
Primero se le quita el signo, y luego se desplaza a la derecha en 21 bits.

Si se deseara manipular el exponente interno como número con signo, habría que definir:
int ei=(int) ( ( ( long int)((*pm1)<<1)) >>21);
Se corre a la derecha el largo con signo, y luego se convierte a entero.

Las siguientes definiciones, nos permiten extraer la parte más significativa de la mantisa en m1
(20 bits), y la menos significativa en m2:
unsigned long m1=(*pm1)&0x000FFFFFL;
unsigned long m2=*pm2;

Para tratar números subnormales es preciso normalizar la mantisa, corrigiendo el exponente. En


el código se multiplica por dos el número y se resta uno al exponente, mientras primer dígito de
la mantisa sea diferente de cero. Este primer dígito se detecta con la condición: (
(*pm1)&0x00080000L)==0

Setear el exponente externo en -1, para tener mantisa decimal que cumpla:
0.5 =< m < 1
se logra, como se explico antes, dejando el exponente interno en: 01111111110 (0x3FE).
Lo cual se logra con: ((*pm1)&0x800FFFFFL)| 0x3FE00000L

Profesor Leopoldo Silva Bijit 26-05-2008


86 Estructuras de Datos y Algoritmos
Para probar la rutina se pueden usar los siguientes valores:

Para comprobar el cero: number = 0.0;


Para verificar los subnormales: number = 0.125*pow(2,-1023);
Debe resultar como respuesta: 0.5*pow(2,-1025);
Para probar número grandes: number = 1.0*pow(2, 1023);
Para probar el infinito: number = 1/0.0;
Para probar un Not a Number: number = 0.0/0.0;

El código completo para la función:

double frexp(double number, int *exponent)


{
unsigned long int *pm2=(unsigned long int *)&number;
unsigned long int *pm1=pm2+1;
unsigned long m1=(*pm1)&0x000FFFFFL;
unsigned long m2=*pm2;
unsigned int ei= (unsigned int)(((*pm1)<<1)>>21);
if (ei==0)
{ if((m2|m1)==0) {*exponent=0;} /* 0.0 */
else {*exponent=-1022;
while( ((*pm1)&0x00080000L)==0) {number*=2;(*exponent)--;}
*pm1=((*pm1)&0x800FFFFFL) | 0x3FF00000L; number--;
}
else
if (ei==2047) {if ((m2|m1)==0) printf("infinito \n"); /*ei==-1 con signo*/
else printf("NaN \n");
*exponent = 1025;
*pm1=((*pm1)&0x800FFFFFL)| 0x3FE00000L;}
else
{ *exponent = ei - 1022; /*escribe en el entero que se pasa por referencia*/
*pm1=((*pm1)&0x800FFFFFL)| 0x3FE00000L;
}
return( number);
}

Nótese que la misma rutina que no trata los casos subnormales y el cero, podría escribirse:

double frexp(double number, int *exponent)


{
unsigned long int *pm1=((unsigned long int *)&number) +1;
*exponent = ( (unsigned int)(((*pm1)<<1)>>21)) - 1022;
*pm1=((*pm1)&0x800FFFFFL)| 0x3FE00000L;
return( number);
}

Que equivale al comportamiento de la primera rutina que manipulaba los bytes del double.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 87
En los ejemplos de uso de union y campos, se desarrollará la misma rutina anterior.
6.3.5. Uso de union.
Otra forma de accesar una zona de la memoria es a través de la estructura unión, que permite
definir variables que comparten una zona común del almacenamiento. La unión asigna a la
variable (de tipo union) un espacio de memoria suficiente para almacenar la variable de la union
de mayor tamaño.

En el ejemplo siguiente, la union denominada buffer, puede verse como un double o como una
estructura denominada pbs. Las variables anteriores tienen la misma dirección de memoria, y se
accesan de manera similar a una estructura. Si se escribe en una variable, se modifica la otra.

La estructura pbs, define 64 bits, el mismo tamaño que el double. Y permite identificar los dos
bytes más significativos del double, b0 y b1, en caso de que el byte más significativo esté
ubicado en la dirección menor. Y b6 y b7 si el más significativo del double está asociado a la
dirección mayor.

union buf
{ struct bts
{unsigned char b0;
unsigned char b1;
unsigned char b[4];
unsigned char b6;
unsigned char b7; /*el más significativo con dirección mayor*/
} pbs;
double d;
} buffer;

El valor +2.0 en doble precisión, equivale al valor 0x40000000 en formato IEEE 754. El signo
es cero, la mantisa normalizada es cero. Y el exponente externo es +1.

Para el exponente interno se cumple que: ei = ee + 1023


Empleando 11 bits en representación de números con signo polarizados, se tiene que 1023
decimal equivale a 0x3FF en hexadecimal.
Entonces ei = 00000000001 + 01111111111 = 10000000000 = 0x400 en hexadecimal. Y
resulta que el byte más significativo del double es 0x40, que equivale al binario: 01000000.

Con la siguiente asignación puede escribirse en el double de la unión:


buffer.d = 2.0;

Y leer los bytes de la unión, accesando por su nombre los bytes de la estructura pbs.
if (buffer.pbs.b7==0x40) printf("el byte más significativo del double tiene la dirección
mayor\n");
if (buffer.pbs.b0==0x40) printf("el byte más significativo del double tiene la dirección
menor\n");

Profesor Leopoldo Silva Bijit 26-05-2008


88 Estructuras de Datos y Algoritmos
El siguiente diseño genera una función frexp portable a plataformas que empleen big o little
endian para enumerar los bytes dentro de una palabra de memoria. La manipulación de los
bytes es similar al diseño basado en leer bytes de una variable de gran tamaño, en base a
punteros.

double frexp(double number, int *exponent)


{
union buf
{ struct bts
{unsigned char b0;
unsigned char b1;
unsigned char b[4];
unsigned char b6;
unsigned char b7; /*el más significativo con dirección mayor*/
} pbs;
double d;
} buffer;
unsigned int ei;
buffer.d=2.0;
if (buffer.pbs.b7==0x40)
{buffer.d = number;
ei=(unsigned int)(buffer.pbs.b7 & 0x7F)<<4|((unsigned int)(buffer.pbs.b6>>4));
*exponent=-1022+ei;
buffer.pbs.b7 = (buffer.pbs.b7 & 0x80)|0x3F;
buffer.pbs.b6 = (buffer.pbs.b6 & 0x0F)|0xE0;
return( buffer.d);
}
if (buffer.pbs.b0==0x40)
{buffer.d = number;
ei=(unsigned int)(buffer.pbs.b0 & 0x7F)<<4|((unsigned int)(buffer.pbs.b1>>4));
*exponent=-1022+ei;
buffer.pbs.b0 = (buffer.pbs.b0 & 0x80)|0x3F;
buffer.pbs.b1 = (buffer.pbs.b6 & 0x0F)|0xE0;
return( buffer.d);
}
*exponent = 0; /*no es little ni big endian */
return( number);
}
6.4.5. Uso de campos (fields)
El lenguaje C provee una estructura de campos de bits. Un campo de bits es un elemento de una
estructura que es definida en términos de bits. Es dependiente de la implementación del
lenguaje en un determinado procesador, pero asumiremos que está implementada con a lo
menos 16 bits de largo, en total.

Entonces en el ejemplo siguiente, dentro de la unión buffer, se tiene la estructura pbs, que a su
vez está formada por la estructura campos1, el arreglo de 4 caracteres b, y la estructura

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 89
campos2. Tanto la estructura pc1 como pc2 están formadas por campos de bits. Se han definido
de largo 11 los campos exp1 y exp2, que tratan como secuencias de bits a las posibles
ubicaciones del exponente de un double en formato IEEE 754.

union buf
{ struct bts
{ struct campos1
{ unsigned int signo1 :1;
unsigned int exp1 :11;
unsigned int man1 :4;
} pc1;
unsigned char b[4];
struct campos2
{ unsigned int man2 :4;
unsigned int exp2 :11;
unsigned int signo2 :1; /*el byte más significativo con dirección mayor*/
} pc2;
} pbs;
double d;
} buffer;

Con la siguiente asignación puede escribirse en el double de la unión:


buffer.d = 2.0;

Y leer los grupos de bits de la unión, accesando por su nombre los campos de la estructura pbs.
if (buffer.pbs.pc2.exp2==0x400) printf("el byte más significativo del double tiene la
dirección mayor\n");
if (buffer.pbs.pc1.exp1==0x400) printf("el byte más significativo del double tiene la
dirección menor\n");

El siguiente diseño implementa frexp usando estructuras de campos de bits (fields).

double pfrexp(double number,int *exponent)


{
union buf
{ struct bts
{ struct campos1
{ unsigned int signo1 :1;
unsigned int exp1 :11;
unsigned int man1 :4;
} pc1;
unsigned char b[4];
struct campos2
{ unsigned int man2 :4;
unsigned int exp2 :11;
unsigned int signo2 :1; /*el más significativo con dirección mayor*/
} pc2;

Profesor Leopoldo Silva Bijit 26-05-2008


90 Estructuras de Datos y Algoritmos
} pbs;
double d;
} buffer;
unsigned int ei;
buffer.d=2.0;
if (buffer.pbs.pc2.exp2==0x400)
{buffer.d = number;
ei=(unsigned int)(buffer.pbs.pc2.exp2);
*exponent=-1022+ei;
buffer.pbs.pc2.exp2 = 0x3FE;
return( buffer.d);
}
if (buffer.pbs.pc1.exp1==0x400)
{buffer.d = number;
ei=(unsigned int)(buffer.pbs.pc1.exp1);
*exponent=-1022+ei;
buffer.pbs.pc1.exp1 = 0x3FE;
return( buffer.d);
}
*exponent=0;
return( number);
}

double mfloor( double x)


{ double i;
int expon;
i= frexp(x, &expon);
if(expon < 0) return x < 0.0 ? -1.0 : 0.0;
/* pow(2,52) = 4503599627370496*/
if((unsigned) expon > 52) return x; /*se asume entero */
/* pow(2,31) = 2147483648 */
if (expon < 32 )
{i = (double)(long)(x); /* cabe en long x */
if(i > x) return i - 1.0;}
/*debe truncarse el double cuya parte entera no cabe en un long */

return i;
}

Referencias.

Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975.

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 91

Índice general.

APÉNDICE 2 .............................................................................................................................................. 1
INTRODUCCIÓN AL LENGUAJE C. ................................................................................................... 1
1. FUNCIONES. .......................................................................................................................................... 1
1.1. Abstracción de acciones y expresiones. ....................................................................................... 1
1.2. Prototipo, definición, invocación. ................................................................................................ 2
1.3. Alcances del lenguaje C. .............................................................................................................. 3
1.4. Paso de argumentos por valor. .................................................................................................... 4
1.5. Paso por referencia. ..................................................................................................................... 4
1.6. Frame. .......................................................................................................................................... 5
1.7. Algunos conceptos básicos ........................................................................................................... 6
1.7.1. Datos. .................................................................................................................................................... 6
Enteros con signo. ....................................................................................................................................... 6
Enteros sin signo. ....................................................................................................................................... 8
Enteros Largos. ........................................................................................................................................... 8
Largos sin signo. ......................................................................................................................................... 8
Números Reales. ( float ) ............................................................................................................................ 8
Carácter. ...................................................................................................................................................... 9
Strings. ........................................................................................................................................................ 9
1.7.2. Acciones. ............................................................................................................................................... 9
Secuencia. ................................................................................................................................................... 9
Alternativa. ............................................................................................................................................... 10
Repetición. ................................................................................................................................................ 10
For............................................................................................................................................................. 10
Abstracción. .............................................................................................................................................. 10
1.7.3. Entrada. Salida. .................................................................................................................................... 10
Ejemplos. .................................................................................................................................................. 11
2. TIPO CHAR. ......................................................................................................................................... 12
2.1. Valores. ...................................................................................................................................... 12
2.1. Definición de variables y constantes de tipo char. ..................................................................... 12
2.2. Caracteres ASCII. ...................................................................................................................... 12
2.3. Secuencias de escape.................................................................................................................. 14
2.4. Archivos de texto y binarios. ...................................................................................................... 14
2.5. Expresiones. ............................................................................................................................... 15
2.6. Entrada-Salida ........................................................................................................................... 16
Entrada y salida con formato. ........................................................................................................................ 17
2.7. Funciones. .................................................................................................................................. 18
2.8. Macros........................................................................................................................................ 19
2.9. Macros con argumentos. ............................................................................................................ 21
2.10. Biblioteca. ctype.c .................................................................................................................. 22
3. STRINGS.............................................................................................................................................. 24
3.1. Definición de string. ................................................................................................................... 24
3.1.1. Arreglo de caracteres. .......................................................................................................................... 24
3.1.2. Puntero a carácter................................................................................................................................. 25
3.2. Strcpy.......................................................................................................................................... 25
3.3. Strncpy........................................................................................................................................ 27

Profesor Leopoldo Silva Bijit 26-05-2008


92 Estructuras de Datos y Algoritmos
3.4. Strcat. ..........................................................................................................................................28
3.5. Strncat. ........................................................................................................................................28
3.6. Strlen. ..........................................................................................................................................29
3.7. Strcmp. ........................................................................................................................................29
3.8. Strncmp. ......................................................................................................................................30
3.9. Strstr............................................................................................................................................30
3.10. Strchr. .......................................................................................................................................31
3.11. Strrchr. ......................................................................................................................................32
3.12. Strpbrk.......................................................................................................................................32
3.13. Strcspn.......................................................................................................................................32
3.14. Strspn. .......................................................................................................................................33
3.15. Strtok. ........................................................................................................................................33
3.16. Strdup. .......................................................................................................................................34
3.17. Memcpy. ....................................................................................................................................35
3.18. Memccpy. ..................................................................................................................................36
3.19. Memmove. .................................................................................................................................36
3.20. Memcmp. ...................................................................................................................................37
3.21. Memset. .....................................................................................................................................38
3.22. Movimientos de bloques, dependientes del procesador. ..........................................................38
4. RUTINAS DE CONVERSIÓN. ..................................................................................................................40
4.1. De enteros a caracteres. Ltoa. Long to Ascii. ...........................................................................40
4.2. De secuencias de caracteres a enteros. ......................................................................................42
4.3. De dobles a caracteres. ...............................................................................................................45
4.4. Imprime mantisa. ........................................................................................................................46
4.5. Rutinas más eficientes para convertir un número punto flotante binario a punto flotante
decimal. ..............................................................................................................................................47
4.5.1. Potencias de 10..................................................................................................................................... 47
4.5.2. Imprime exponente de flotante. ............................................................................................................ 48
4.5.3. Redondeo de la mantisa........................................................................................................................ 49
4.5.4. Convierta. Algoritmo dos. .................................................................................................................... 50
4.5.5. Imprime mantisa. Algoritmo dos.......................................................................................................... 50
5. DISEÑO DE FUNCIONES CON UN NÚMERO VARIABLE DE ARGUMENTOS. ...............................................51
5.1. Argumentos estándar. .................................................................................................................52
5.2. Estructura de printf. ....................................................................................................................54
5.3. Estructura de scanf. ....................................................................................................................55
5.4. Salida formateada en base a llamados al sistema. SPIM. ..........................................................56
5.5. DESARROLLO DE PRINTF EN BASE A PUTCHAR. ................................................................................65
6. ALGUNAS RUTINAS MATEMÁTICAS......................................................................................................74
6.1. Trigonométricas. .........................................................................................................................74
6.2. Manipulación de flotantes. ..........................................................................................................80
6.3. Acceso a los bits de un número. ..................................................................................................81
6.3.1. Acceso por caracteres (bytes). .............................................................................................................. 83
6.3.2. Uso de dos enteros largos sin signo, para representar los bits de un double. ........................................ 84
6.3.5. Uso de union. ....................................................................................................................................... 87
6.4.5. Uso de campos (fields) ......................................................................................................................... 88
REFERENCIAS. .........................................................................................................................................90
ÍNDICE GENERAL. ....................................................................................................................................91
ÍNDICE DE FIGURAS. ................................................................................................................................93

Profesor Leopoldo Silva Bijit 26-05-2008


Apéndice 2. Introducción al lenguaje C. 93

Índice de figuras.

FIGURA A2.1. TABLA ASCCI. .................................................................................................................... 13


FIGURA A2.2. ARREGLO DE CARACTERES. .................................................................................................. 25
FIGURA A2.3. COPIA DE STRINGS. ............................................................................................................... 26
FIGURA A2.4. CONCATENA STRINGS........................................................................................................... 28
FIGURA A2.5. LARGO STRING. .................................................................................................................... 29
FIGURA A2.6. PUNTEROS DESPUÉS DE PRIMER WHILE. ................................................................................ 32
FIGURA A2.7. STRTOK ................................................................................................................................ 33
FIGURA A2.8 MEMMOVE. ........................................................................................................................... 37
FIGURA A2.9 ESTRUCTURA FRAME. ............................................................................................................ 51
FIGURA A2.10 FUNCIÓN SENO. ................................................................................................................... 75
FIGURA A2.11 REDUCCIÓN A INTERVALO ENTRE 0 Y 1. .............................................................................. 75
FIGURA A2.12. FLOOR(W). .......................................................................................................................... 76
FIGURA A2.13. REDUCCIÓN AL PRIMER PERÍODO........................................................................................ 76
FIGURA A2.14. REDUCCIÓN AL PRIMER CUADRANTE. ................................................................................. 77
FIGURA A2.15. SERIES Y POLINOMIO DE PADE. .......................................................................................... 78
FIGURA A2.16. POLINOMIO DE PADE. ......................................................................................................... 79
FIGURA A2.17. POLINOMIO DE PADE ENTRE 0 Y 4. ..................................................................................... 80

Profesor Leopoldo Silva Bijit 26-05-2008


1

Apéndice 3

Introducción a la estructura y operación


de analizadores léxicos.

A3.1. Estructura de un lenguaje de programación.

Un lenguaje está basado en un vocabulario, o léxico, el que está compuesto por palabras, o
más precisamente por símbolos.

Ciertas secuencias de palabras son reconocidas como sintácticamente bien formadas o correctas.
La gramática o sintaxis o estructura del lenguaje queda descrita por una serie de reglas o
fórmulas que definen si una secuencia de símbolos es una sentencia correcta. La estructura de
las sentencias establece el significado o semántica de ésta.

Ejemplo.
<sentencia> ::= <sujeto><predicado>
<sujeto>::= árboles|arbustos
<predicado>::=grandes|pequeños

La semántica de las líneas anteriores es la siguiente:


Una sentencia está formada por un sujeto seguido de un predicado. Un sujeto es la palabra
árboles o arbustos. Un predicado consiste de una sola palabra, la cual puede ser grandes o
pequeños.
El lenguaje anterior, genera cuatro sentencias correctas.

Una sentencia bien formada puede ser derivada a partir del símbolo de partida, <sentencia> en
el caso del ejemplo, por la repetida aplicación de reglas de reemplazo o producciones (reglas
sintácticas). Se denominan símbolos no terminales, o categorías sintácticas, a las sentencias
definidas entre paréntesis de ángulo, que figuran a la derecha en las producciones; los símbolos
terminales (vocabulario) figuran a la derecha de las producciones y se representan a sí mismos.

Estas reglas para definir lenguajes se denomina formulismo de Backus-Nauer. Los paréntesis
de ángulo, y los símbolos ::= (que se lee: puede ser reemplazado por) y | (que se lee como: o
excluyente) son denominados metasímbolos.

Las producciones son libres al contexto, si en éstas figura a la izquierda un solo símbolo no
terminal S, que puede ser reemplazado en función de símbolos terminales s, no importando el
contexto en el que ocurra S.

Profesor Leopoldo Silva Bijit 26-05-2008


2 Estructuras de Datos y Algoritmos
La producción: ASB::=AsB define que S puede ser reemplazado por s, siempre que ocurra
entre A y B; por lo cual es sensible al contexto.

El uso de la recursión al definir producciones, permite generar un infinito número de sentencias


a partir de un número finito de producciones.
S::=aA
A::=b|cA

La categoría A, está definida en términos de sí misma. En el ejemplo, los símbolos terminales se


representan con minúsculas, los no terminales con mayúsculas.
A partir de S, se generan: ab, acb, accb, acccb, …..

A3.2. Analizador léxico. (parser)

El diseño de un reconocedor de sentencias correctas está basado en encontrar algoritmos que


sean de complejidad n, donde n es el largo de la sentencia a analizar. Es decir en cada paso del
algoritmo se depende solamente del estado actual y del siguiente símbolo; y no es necesario
volver atrás. Obviamente la estructura del lenguaje debe permitir esta forma de análisis.

El método jerárquico o top-down, de análisis de sentencias (parsing) consiste en reconstruir el


árbol de derivación desde el símbolo de partida hasta la sentencia final.

Ejemplo: Se da la sentencia: árboles grandes, y se desea determinar si pertenece al lenguaje


definido en un ejemplo anterior.
Se parte del símbolo de partida, <sentencia> y se lee el primer símbolo del texto que se desea
analizar: árboles.

Se reemplaza <sentencia> por <sujeto><predicado>, se ve si es posible reemplazar <sujeto>; se


verifica que puede ser reemplazado por árboles. En este momento, puede avanzarse al siguiente
símbolo de la secuencia de entrada, que en el caso del ejemplo es grandes. Al mismo tiempo se
avanza al siguiente de los símbolos no terminales. Ahora la tarea restante es verificar si
<predicado> puede generar el símbolo grandes. Como esto es así, se avanza en la secuencia de
entrada, y se observa que no quedan más símbolos. Con lo cual el análisis termina reconociendo
cómo válida la construcción.

Cada paso del análisis está basado solamente en el siguiente símbolo de la secuencia de
símbolos no terminales que se está analizando.

Puede ilustrarse el algoritmo mediante la siguiente tabla. La columna a la izquierda representa la


tarea de reconocimiento pendiente y la de la derecha la secuencia de símbolos terminales que
aún no se leen. Se desea validar accb como perteneciente a S.

S::=aA
A::=b|cA

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 3
S accb Inicio. Se lee a
aA accb Se reemplaza S. Se reconoce a.
A ccb Se acepta a. Se lee c
cA ccb Se reemplaza A. Se reconoce c.
A cb Se acepta c. Se lee c.
cA cb Se reemplaza A. Se reconoce c.
A b Se acepta c. Se lee b.
b b Se reemplaza A. Se reconoce b.
- - Se acepta la frase como correcta.

Figura A3.1. Análisis sin volver atrás.

No es necesario volver atrás, y el análisis está basado en la lectura de un símbolo terminal por
adelantado.

Para que esto sea posible, los símbolos iniciales de símbolos no terminales alternativos que
figuran a la derecha en las producciones, deben ser diferentes.

El siguiente ejemplo ilustra reglas que no cumplen el principio anterior. Ya que A y B (símbolos
no terminales alternativos en S), tienen iguales símbolos iniciales, ambos son x.
S::=A|B
A::=xA|y
B::=xB|z

Si se desea analizar la secuencia xxxz, se tendrá la dificultad que no es posible discernir (sólo
leyendo el primer símbolo por adelantado) si S debe ser reemplazado por A o por B. Si se
eligiera al azar, reemplazar S por A, luego de unos pasos el análisis falla, y se debería volver
atrás, e intentar reemplazar por B, en lugar de A, y volver a intentar.
Las reglas:
S::=C|xS
C::=y|z

Son equivalentes a las anteriores y cumplen el principio anterior.

En variadas construcciones de los lenguajes se acepta símbolos opcionales. Es decir la


alternativa entre un símbolo terminal y una secuencia nula de símbolos. Ejemplo de esto es la
asignación: x= +a; el símbolo + es opcional.

Sea nula la secuencia nula.


Entonces las reglas:
S::=Ax
A::=x|nula

Si se desea reconocer x, si luego de reemplazar S por Ax, se intenta reemplazar A por x el


análisis falla, se logra xx en la derivación. Con lo cual puede reconocerse el primer x, y luego al
intentar leer el próximo, como no quedan símbolos no terminales que leer y queda pendiente un

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
x que derivar, se concluye que el análisis falló. Lo que se debió realizar era reemplazar A por
nula.
Para evitar la vuelta atrás en el reconocimiento, se impone una regla adicional para las
producciones que generen la secuencia nula:

Para una secuencia A que genera la secuencia nula, los símbolos iniciales que genera A deben
se disjuntos con los símbolos que siguen a cualquier secuencia generada por A.

En el ejemplo anterior, S dice que la sentencia A tiene a x como símbolo siguiente. Y la


producción que define A, indica que el primer símbolo que puede generar A es también x.
Como los iniciales generados por A son iguales a los siguientes a A, se viola la regla anterior.

La repetición de construcciones, que también es muy frecuente en los lenguajes, suele definirse
empleando recursión.
Por ejemplo la repetición de una o más veces del elemento B, puede anotarse:
A::= B|AB

Pero el primero de B y el primero de AB no es el vacío, y no cumple la primera de las reglas.


Si se cambia la definición de A por:
A::=nula|AB
A genera: nula, B, BB, BBB, … y se tendrá que el primero de A y el siguiente a A serán B,
violando la segunda regla.

Lo cual permite visualizar que no puede emplearse recursión por la izquierda.

La recursión por la derecha, cumple las reglas anteriores:


A::=nula|BA
Esta última producción también genera la repetición de cero, una o más veces del elemento B.
La frecuencia de construcciones repetitivas que generen la secuencia nula lleva a definir los
siguiente metasímbolos:
A::={B}
Que genera: nula, B, BB, BBB, …

Esta definición sólo simplifica la notación, pero aún es preciso revisar que se cumpla la segunda
regla, para emplear algoritmos basados en leer un símbolo por adelantado y sin volver atrás.

A3.3. Reglas de análisis.

Debido a lo abstracto del formalismo de Backus-Nauer se ha desarrollado una representación


gráfica de las reglas. En los grafos sintéticos se emplean los siguientes símbolos:

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 5

Símbolo terminal.

Figura A3.2. Símbolo terminal.

Corresponde a un reconocimiento del símbolo terminal x, en la producción de la que forma


parte, y al avanzar en la lectura del siguiente símbolo en la secuencia de entrada.

Es importante asociar estos grafos a elementos de un lenguaje de programación que


implementará el reconocedor sintáctico basado en diagramas.

Para el elemento terminal, puede traducirse:


if (ch== „x‟) ch=lee(stream); else error(n);

Donde stream es el descriptor del archivo que contiene el texto que será analizado. La función
lee, trae el siguiente carácter. En este nivel los caracteres individuales del texto se consideran
símbolos terminales. La función de error, debería generar un mensaje asociado a la detección de
una sentencia mal formada.

Símbolo no terminal.

Figura A3.3. Símbolo no terminal.

Cuando aparece este diagrama en una producción, corresponde a la activación de un


reconocedor del símbolo no terminal B.

En el reconocedor, se activa una invocación al procedimiento reconocedor de B.


B( );

Alternativa.

La producción: A::=B1|B2|…|Bn
Se representa:

Profesor Leopoldo Silva Bijit 26-05-2008


6 Estructuras de Datos y Algoritmos

B1
A
B2

Bn

Figura A3.4. Alternativa.

En el reconocedor, puede implementarse, mediante la sentencia switch.

switch (ch){
case L1: B1(); break;
case L2: B2(); break;
….
case Ln: Bn(); break;
}

Donde los Li serían los conjuntos de los símbolos iniciales de los Bi.

Es preferible describir la alternativa, explicitando el primer símbolo:

b1 B1

A
b2 B2

bn Bn

Figura A3.5. Alternativa, con primer símbolo explícito.

switch (ch){
case „b1‟: {ch=lee(stream); B1(); break;}
case „b2‟: {ch=lee(stream); B2(); break;}
….
case „bn‟: {ch=lee(stream); Bn(); break;}
}

Concatenación.

La producción: A::=B1B2…Bn
Se representa:

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 7

A
B1 B2 Bn
Figura A3.6. Concatenación.

En el reconocedor: {B1( ); B2( );…Bn( );}

Repetición.

La producción: A::={B}

Se representa:

Figura A3.7. Repetición.

En el reconocedor, se implementa:
while( esta_en(L, ch) ) B( );

Donde la función esta_en retorna verdadero si ch pertenece al conjunto L de los primeros


caracteres generados por B.

Es preferible, representar, el forma explícita el primer carácter:

B b

Figura A3.8. Repetición, con primer símbolo explícito.

De este modo el reconocedor se implementa:


while( ch==‟b‟) {ch=lee(stream); B( );}

La repetición, de a lo menos una vez, puede implementarse con una sentencia while.

Cada uno de los bloques anteriores puede ser reemplazado por alguna de las construcciones
anteriores. Por ejemplo: la repetición puede ser una serie de acciones concatenadas.

Resumen.

Para una gramática dada, pueden construirse grafos sintácticos a partir de las producciones
descritas en BNF, y viceversa.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
Y de éstas derivar el código del reconocedor.

Los grafos deben cumplir las siguientes dos reglas, para que se puedan recorrer leyendo un
símbolo por adelantado y sin volver atrás.

Los primeros símbolos en las alternativas deben ser diferentes. De tal forma que la
bifurcación solo pueda escogerse observando el siguiente símbolo de esa rama.
Si un grafo reconocedor de una sentencia A, puede generar la secuencia nula, debe
rotularse con todos los símbolos que puedan seguir a A. Ya que ingresar al lazo puede
afectar el reconocimiento de lo que viene a continuación.

Una vez definido el lenguaje a reconocer, mediante sus grafos, debe verificarse el cumplimiento
de las dos reglas anteriores. Un sistema de grafos que cumplan las reglas anteriores se denomina
determinista y puede ser recorrido sin volver atrás y solo leyendo un símbolo por adelantado.

Esta restricción no es una limitante en los casos prácticos.

Ejemplo A3.1. Reconocedor simple.

Generar reconocedor para sentencias que cumplan las siguientes producciones.


A::=x|(B)
B::=AC
C::={+A}

Algunas sentencias válidas, de este lenguaje, son:


x, (x), (x+x), ((x)), (x+x+x+x+x+x+x+x+x),….

Pueden plantearse los grafos sintácticos para cada una de las producciones. Posteriormente, es
posible reducir los grafos, mediante substituciones. Luego de esto se obtiene:
A
( A )

A +

Figura A3.9. Grafo del reconocedor.

El grafo permite revisar el cumplimiento de las dos reglas.


La bifurcación tiene intersección vacía de los primeros elementos de cada rama:
{ „(„ } { „x‟ } =

La producción que genera la secuencia nula tiene intersección vacía del primer elemento de la
repetición y del símbolo que sigue a esa repetición:
{ „+„ } { „)‟ } =

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 9

El siguiente programa implementa un reconocedor, para sentencias que cumplan la sintaxis


descrita por el grafo. Lo más importante es notar que el código para el reconocedor de
sentencias A, puede ser escrito a partir del diagrama anterior. Cada conjunto de reglas da origen
a un programa determinado. Se han agregado las funciones que abren y leen el archivo con el
texto que será analizado, para ilustrar los detalles del entorno. Se da término a las sentencias del
archivo con un asterisco, en la primera posición de una línea. Cada vez que termina el análisis
de una sentencia avisa si la encontró correcta.

Se emplea una variable global ch, para disminuir el número de argumentos. Se destaca que debe
leerse un símbolo por adelantado, antes de invocar al reconocedor.
Si el descriptor del archivo se deja como variable global, pueden disminuirse aún más los
argumentos de las funciones, simplificándolas.

#include <stdio.h>

void error(int e)
{ printf("Error %d\n", e);}

char lee(FILE *stream)


{ return( fgetc(stream)); }

char ch='\0';

void A(FILE *stream)


{
if (ch=='x') ch=lee(stream);
else
if (ch=='(' )
{ ch=lee(stream); A(stream);
while(ch=='+') {ch=lee(stream); A(stream);}
if ( ch==')' ) ch=lee(stream); else error(1);
}
else
error(2);
}

/* Analiza archivo de texto */


int parser(void)
{
FILE *stream;
if ((stream = fopen("inparser.txt", "r")) == NULL) {
fprintf(stderr, "No pudo abrir archivo de entrada.\n");
return 1;
}

/* lee hasta encontrar el final del stream */

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
while(!feof(stream))
{
ch=lee(stream); if(ch=='*') break; //lee ch por adelantado.
A(stream);
printf("%s\n","ok");
}
printf("%s\n","fin");

fclose(stream);
return 0;
}

int main(void)
{
parser();
return 0;
}

Ejemplo A3.2. Parser BNF.

Se desea reconocer sentencias descritas por la siguiente gramática:

producción ::= <símbolo no terminal> „=‟ <expresión> „.‟


expresión ::= <término> {„,‟ <termino>}
término ::= <factor> { <factor> }
factor ::= <símbolo terminal> | <símbolo no terminal> | „( „ <expresión> „)‟
símbolo no terminal ::= Letra mayúscula
símbolo terminal ::= Letra minúscula

Los símbolos terminales requeridos por las reglas se han colocado entre comillas simple. Nótese
que cada producción termina en el carácter punto.
La serie de producciones se termina cuando se encuentra un asterisco:
<texto de programa> ::= {producción} „*‟

El parser genera algunos comentarios de error, hacia la salida estándar, indicando la línea y la
posición del carácter que no cumple las reglas.

#include <stdio.h>
#include <stdlib.h> //malloc
#include <string.h>
#include <ctype.h>

void expresion(void); //prototipo. Factor requiere expresión.

char simbolo='\0';
int nl=1; //contador de líneas
int nc=0; //contador de caracteres en la línea.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 11
FILE *stream;

void error(int tipo)


{
putchar('\n');printf("(%d,%d): ",nl, nc+1);
switch (tipo)
{
case 1: printf("%s\n", "Esperaba símbolo no terminal");break;
case 2: printf("%s\n", "Esperaba signo igual"); break;
case 3: printf("%s\n", "Esperaba cierre paréntesis"); break;
case 4: printf("%s\n", "Esperaba abre paréntesis"); break;
case 5: printf("%s\n", "Esperaba punto"); break;
}
}

void getch(void)
{
if(!feof(stream))
{ simbolo = fgetc(stream); nc++;
if(símbolo == '\n') {nl++; nc=0;}
putchar(simbolo); //eco en salida estándard
}
}

void getsimbolo(void)
{
getch();
while(isspace(simbolo)) getch(); //descarta blancos
}

//factor ::= <símbolo terminal> | <símbolo no terminal> | „( „ <expresión> „)‟


void factor(void)
{
if (isalpha(simbolo) ) getsimbolo();
else
if (simbolo == '(' )
{ getsimbolo(); expresion();
if(símbolo == ')' ) getsimbolo(); else error(3);
}
else error(4);
}

// término ::= <factor> { <factor> }


void termino(void)
{

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
factor();
while( (isalpha(simbolo)) || (símbolo == '(' ) ) factor();
}
Notar que la repetición de factor es precedida por la revisión de los primeros caracteres de
factor: es decir que sea un símbolo terminal o no terminal o el comienzo de una expresión, que
debe comenzar por paréntesis abierto.

//expresión ::= <término> {„,‟ <termino>}


void expresion(void)
{
termino();
while(símbolo == ',') {getsimbolo(); termino();}
}

// producción ::= <símbolo no terminal> „=‟ <expresión> „.‟


void produccion()
{
if(isupper(simbolo)) getsimbolo(); else error(1);
if (símbolo == '=') getsimbolo(); else error(2);
expresion();
if (simbolo != '.') error(5);
}

/* Lectura de archivo de texto */


int bnfparser(void)
{
/* Abre stream para lectura, en modo texto. */
if ((stream = fopen("bnfparser.txt", "r")) == NULL) {
fprintf(stderr, "No pudo abrir archivo de entrada.\n");
return 1;
}

/* lee hasta encontrar el final del stream */


while(!feof(stream))
{
getsimbolo(); if(simbolo=='*') break;
produccion();
}
printf("%s numero de lineas =%d\n","fin de archivo", nl);

fclose(stream); /* close stream */


return 0;
}

int main(void)
{
bnfparser();

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 13

return 0;
}

Para el siguiente archivo de entrada:

A = C.
B=x,A.
B=x,A,B,C-
C=x(B,D.
D=(A).
*

Se genera la siguiente salida:


A = C.
B=x,A.
B=x,A,B,C-
(3,10): Esperaba punto

C=x(B,D.
(4,8): Esperaba cierre paréntesis

D=(A).
*fin de archivo número de líneas =5

Ejemplo A3.3. Reconocedor de identificador.

Un identificador es una secuencia de caracteres, donde el primero debe ser letra, y los que
siguen letras o números.

Un identificador puede aparecer entre espacios, tabs, retornos; o estar entre caracteres no
alfanuméricos. Si ab y cd son identificadores, las siguientes líneas muestran posibles instancias
de éstos:
ab
(ab + cd)
ab= cd;
ab = cd+5;

Una alternativa al diseño de reconocedores es el diseño basado en diagramas de estados. Se


ilustra un ejemplo, basado en análisis de líneas. Más adelante en A3.4 se esbozan
procedimientos para leer archivos de texto por líneas.

El diagrama de estados de la Figura A3.10, muestra que deben descartarse los espacios (se
simboliza por el círculo 0), y comenzar a almacenar el identificador, cuando se encuentra una
letra; luego se siguen almacenado los caracteres del identificador (círculo 1) hasta que llegue un
carácter no alfanumérico, en que se vuelve a esperar identificadores.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
Si lo único que se desea es extraer los identificadores, si no llega una letra cuando se espera una,
puede descartársela y continuar el análisis.

Si es letra
Si es espacio
Si es alfanumérico

0 1

No es letra

Si no es alfanumérico

Figura A3.10. Estados de reconocedor de identificador.

Asumiendo que se tiene una línea almacenada en buffer, la siguiente función forma en el string
id, el identificador.

La estructura del código está basada en el diagrama anterior, y en las funciones cuyos prototipos
se encuentran en ctype.h

#define LARGOLINEA 80
#define Esperando_letra 0
#define Almacenando_id 1

getword(char *buffer, int nl)


{
char id[LARGOLINEA];
int i, j, estado;
for(i=0, estado=0, j=0; i<strlen(buffer); i++)
{
switch (estado){
case Esperando_letra:
if (isspace(buffer[i]) ) continue;
else
if (isalpha(buffer[i]))
{ estado=Almacenando_id;
id[j]=buffer[i]; j++;
}
else ; //No es letra. Descarta char.
break;
case Almacenando_id:
if (isalnum(buffer[i]))

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 15
{id[j]=buffer[i]; j++;} //forma id
else
if(!isalnum(buffer[i]))
{ estado = Esperando_letra;
id[j]='\0'; //termina string
j=0; //reset posición
//printf("%s %d\n", id, nl); //muestra los identificadores y la línea

// Aquí debería hacerse algo con el identificador


root=insert(id, root, nl); //Ejemplo: lo inserta en árbol
}
break;
}
}
}

Ejemplo A3.4. Reconocedor de una definición.

El siguiente ejemplo es una elaboración del anterior, y su código se realiza apoyándose en el


diagrama de estados de la Figura A3.11.

Se desea analizar un texto de programa y reconocer el identificador y su definición que figuran


en una línea que comienza con #define.

En el siguiente diagrama de estados, se pasa al estado 3, si se reconoce el identificador define,


luego del símbolo #. A la salida del estado 4, se tiene el identificador para el cual se está
definiendo una equivalencia. A la salida del estado 6, se tiene el identificador con el valor.
Luego del estado 6 debe regresar al estado 0.

Se emplean las definiciones:


#define LARGOID 20 //máximo largo identificadores
#define EsperaID 0 //estados
#define GetId 1
#define BuscaDefine 2
#define EsperaDef 3
#define GetDef 4
#define EsperaEquiv 5
#define GetEquiv 6

Profesor Leopoldo Silva Bijit 26-05-2008


16 Estructuras de Datos y Algoritmos

alfanum
6 5 isspace
!alfanum
alfanum
!alfanum

es alfanumérico id ==”define”
3 4
2
alfanum

Es # id != “define” Si es espacio alfanum

Si es letra Si es alfanumérico
Si es espacio

0 1

No es letra ni #
Si no es alfanumérico

Figura A3.11. Estados de reconocedor de definiciones.

La función puede escribirse, basándose en el diagrama:


FILE *streami,*streamo;
char ch;
#define p() putc(ch,streamo);
#define g() ch=getc(streami)
void parser(void)
{
char word[LARGOID];
char equiv[LARGOID];
int j, state;
pcelda p;
for(state=0, j=0;!feof(streami);g())
{
switch (state){
case EsperaID:
if (isspace(ch) ) { p(); continue;}
else if (isalpha(ch)) {state=GetId; word[j++]=ch;}
else if(ch=='#'){state=BuscaDefine; j=0;}
else p(); //se traga hasta final de línea
break;
case GetId:
if (isalnum(ch)) {word[j++]=ch;}
else
{ if(!isalnum(ch))
{state=EsperaID; word[j]='\0'; j=0;

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 17
//printf("%s\n",word);
if( (p=buscar(word))!=NULL ) //busca identificador
{
//printf("%s -> %s\n", p->definicion, p->equivalencia);
fprintf(streamo, p->equivalencia); //lo reeemplaza
}
else fprintf(streamo,word);
}
p();
}
break;
case BuscaDefine:
if (isalnum(ch)) {word[j++]=ch;}
else
if(!isalnum(ch))
{word[j]='\0';j=0;
if (strcmp(word,"define")==0)
{ state=EsperaDef;
//printf("pre=%s\n",word);
}
else {state=EsperaID; putc('#', streamo); fprintf(streamo,word);p();}
}
break;
case EsperaDef:
if (isspace(ch) ) continue;
else if (isalpha(ch)) {state=GetDef; word[j++]=ch;}
break;
case GetDef:
if (isalnum(ch)) {word[j++]=ch;}
else if(!isalnum(ch))
{state=EsperaEquiv; word[j]='\0'; j=0;
//printf("Definición =%s\n",word);
}
break;
case EsperaEquiv:
if (isspace(ch) ) continue;
else if (isgraph(ch)) {state=GetEquiv; equiv[j++]=ch;}
break;
case GetEquiv:
if (isgraph(ch)) {equiv[j++]=ch;}
else if(!isgraph(ch))
{ state=EsperaID; equiv[j]='\0';j=0;
//printf("insertar valor equivalente en tabla=%s\n", equiv);
// Aquí debería insertar la palabra word y su equivalencia.
if( (p=buscar(word))!=NULL ) descarte(word); //permite redefinición
inserte(word, equiv);
}

Profesor Leopoldo Silva Bijit 26-05-2008


18 Estructuras de Datos y Algoritmos
break;
}
}
}

El siguiente segmento, abre los archivos de entrada y salida.


int procesa_archivos(void)
{
/* Abre stream para lectura, en modo texto. */
if ((streami = fopen("input6.c", "r")) == NULL) {
fprintf(stderr, "No pudo abrir archivo de entrada.\n");
return 1;
}
/* Abre stream para escritura, en modo texto. */
if ((streamo = fopen("output6.c", "w")) == NULL) {
fprintf(stderr, "No pudo abrir archivo de salida.\n");
return 1;
}
/* lee hasta encontrar el final del stream */
while(!feof(streami))
{
g(); //lee uno por adelantado
if(!feof(streami))
{
parser();
//putchar(ch);
}
else break;
}
fclose(streami); /* close stream */
fclose(streamo);
return 0;
}

int main(void)
{ makenull();
procesa_archivos();

return 0;
}

A3.4. Manipulación de archivos en C.

En la etapa de prueba de los algoritmos es necesario ingresar datos. Si éstos son numerosos es
recomendable efectuar esta operación leyendo los datos desde un archivo.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 19
También es preciso poder extraer resultados, para posterior análisis, escribiéndolos en un
archivo.

Escritura de archivos de texto, con estructura de líneas.

Consideremos primero la escritura de archivos de texto con formato.


Esto puede lograrse con un editor, siguiendo ciertas convenciones para separar los elementos de
datos. Es recomendable ingresar por líneas, y en cada línea separar los ítems mediante espacios
o tabs.

Es importante saber el número y tipos de datos de los ítems de cada línea, ya que debe
confeccionarse un string de formato con dicha estructura.

En caso de escritura de un archivo, mediante un programa, también debe tenerse en cuenta la


estructura de la línea.

Veamos un ejemplo.
Se tiene un entero y un carácter por línea, que configuran un dato con la siguiente estructura:

struct mystruct
{
int i;
char ch;
};

La confección con un editor, podría generar el siguiente texto, donde los espacios previos al
número pueden ser reemplazados por varios espacios o tabs. La separación entre el número y el
carácter debe ser un espacio o un tab. Luego pueden existir varios espacios o tabs, seguidos de
un retorno.
11 a
22 b
333 c
4d
5e

Escritura de archivo, desde un programa.

La manipulación de archivos, requiere la invocación a funciones de <stdio.h>, para abrir, cerrar,


leer o escribir en el archivo.

El siguiente segmento abre para escritura el archivo testwr.txt, que debe estar ubicado en el
mismo directorio que el programa ejecutable; en caso de otra ubicación, es preciso preceder el
nombre con el sendero de la ruta.
El modo “w” establece modo escritura, es decir sobreescribe si el archivo existe, y lo crea en
caso contrario.

La variable que permite manipular un archivo es de tipo FILE, y se la define según:

Profesor Leopoldo Silva Bijit 26-05-2008


20 Estructuras de Datos y Algoritmos
FILE *stream;

Se suele proteger la apertura, en caso de falla, mediante:

if ((stream = fopen("testwr.txt", "w")) == NULL)


{
fprintf(stderr, "No puede abrir archivo de salida.\n");
return 1;
}

El stream o flujo de salida stderr suele ser la pantalla.

La siguiente instrucción escribe en el stream 4 caracteres por línea, más el terminador de línea,
eol; que suele ser uno o dos caracteres. La estructura de la línea: dd<sp>c<eol>.

fprintf(stream, "%2d %c\n", s.i, s.ch);

Una vez completada la escritura de todas las líneas, se cierra el archivo, mediante:
fclose(stream);

Lectura de archivos de texto con estructura de líneas.

Se incluye un programa para leer el archivo, ya sea generado con un editor o por un programa,
mediante la función fscanf. Se ha usado la función feof, para determinar si se llegó al final del
archivo. La acción que se realiza con los datos es simplemente desplegar en la pantalla, los
datos formateados.

/* Ejemplo con streams. Lectura de archivo de texto formateado */

#include <stdio.h>
//El archivo testwr.txt debe estar estructurado en líneas.
//Cada línea debe estar formateada según: <sep>entero<sep>char<eol>
//Donde <sep> pueden ser espacios o tabs.
//El entero debe ser de dos dígitos, si en el string de formato del fscanf figura como %2d.

FILE *stream;

int main(void)
{ int jj; char cc;

/* Abre stream para lectura, en modo texto. */


if ((stream = fopen("testwr.txt", "r")) == NULL){
fprintf(stderr, "No pudo abrir archivo de entrada.\n");
return 1;
}

/* lee hasta encontrar el final del stream */

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 21
for(;;)
{
fscanf(stream, "%d %c", &jj, &cc); //lee variables según su tipo y estructura de línea.
if(feof(stream)) break;
printf("%d %c\n", jj, cc); //sólo muestra las líneas del archivo
}

fclose(stream); /* close stream */


return 0;
}

Si se intenta leer más allá del fin de archivo la función feof, retorna verdadero.

Llenar un arreglo a partir de un archivo.

El siguiente ejemplo llena un arreglo de estructuras.


/* Ejemplo con streams. Con datos de archivo se escribe un arreglo */

#include <stdio.h>
//El archivo testwr.txt debe estar estructurado en lineas.
//Cada linea debe estar formateada según: <sep>entero<sep>char<eol>
//Donde <sep> pueden ser espacios o tabs.
//El entero debe ser de dos dígitos, si en el string de formato del fscanf figura como %2d.

struct mystruct
{
int i;
char ch;
};
#define ITEMS 20
struct mystruct arr[ITEMS];
FILE *stream;

int main(void)
{
int i, jj; char cc;
/* Abre stream para lectura, en modo texto. */
if ((stream = fopen("testwr.txt", "r")) == NULL) {
fprintf(stderr, "No pudo abrir archivo de entrada.\n");
return 1;
}
for(i=0; i< ITEMS; i++)
{
fscanf(stream, "%d %c", &jj, &cc); //lee variables según tipo.
if(feof(stream)) break;
arr[i].i=jj; arr[i].ch=cc; //llena items del arreglo
}

Profesor Leopoldo Silva Bijit 26-05-2008


22 Estructuras de Datos y Algoritmos

fclose(stream); /* close stream */

for(i=0;i< ITEMS;i++)
{
printf("%d %c\n", arr[i].i, arr[i].ch);// muestra el arreglo
}
return 0;
}

Escritura y lectura de archivos binarios.

El siguiente ejemplo, ilustra la escritura y lectura de archivos binarios, no de texto. Se emplean


ahora las funciones: fwrite, fseek y fread.

* Ejemplo con streams. Escritura y luego lectura de archivo binario */

#include <stdio.h>

struct mystruct
{
int i;
char ch;
};

int main(void)
{
FILE *stream;
struct mystruct s;
int j;
/* sobreescribe y lo abre para escritura o lectura en modo binario */
if ((stream = fopen("TEST.bin", "w+b")) == NULL) {
fprintf(stderr, "No se puede crear archivo de salida.\n");
return 1;
}

for(j=0;j<20;j++)
{
s.i = j;
s.ch = 'A'+j;
fwrite(&s, sizeof(s), 1, stream); /* write struct s to file */
}

/* seek to the beginning of the file */


fseek(stream, SEEK_SET, 0);

/* lee y despliega los datos */

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 23
for(j=0; j<20;j++)
{
fread(&s, sizeof(s), 1, stream);
printf("%d %c\n", s.i, s.ch);
}

fclose(stream); /* close file */


return 0;
}

El archivo TEST.bin, no puede ser visualizado con un editor de texto. Para su interpretación
debe usarse un editor binario.

Compilación y ejecución en ambiente UNIX.

Para programas sencillos, como los ilustrados, puede generarse el ejecutable en ambiente UNIX,
mediante el comando: make <nombre de archivo c, sin extensión>

Esto crea un ejecutable de igual nombre al programa en C.


Para su ejecución basta escribir su nombre.

Escritura y lectura de archivos por líneas.

#define LARGOLINEA 80 //máximo largo de línea igual a 80 caracteres


char buffer[LARGOLINEA];

int lee_archivo(void)
{
FILE *stream;
/* Abre stream para lectura, en modo texto. */
if ((stream = fopen("input.txt", "r")) == NULL) {
fprintf(stderr, "No pudo abrir archivo de entrada.\n");
return 1;
}
while(!feof(stream)) /* lee hasta encontrar el final del stream */
{
fgets(buffer, LARGOLINEA, stream); //carga buffer
if(!feof(stream))
{

// Aquí debería procesarse la línea


//printf("%s ", buffer); //muestra las líneas del archivo de entrada
}
else break;
}
fclose(stream); /* close stream */
return 0;

Profesor Leopoldo Silva Bijit 26-05-2008


24 Estructuras de Datos y Algoritmos
}

int escribe_archivo(void)
{
FILE *stream;

/* Abre stream para escritura, en modo texto. */


if ((stream = fopen("output.txt", "w")) == NULL) {
fprintf(stderr, "No pudo crear archivo de salida.\n");
return 1;
}

// Aquí debería escribirse el el archivo..


//fputs(buffer, stream); imprime línea
//fprintf(stream, "%d\t", 5); salida formateada
// fputc('\n', stream); salida caracteres

fclose(stream); /* close stream */


return 0;
}

Referencias.

Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 25
Índice general.

APÉNDICE 3 .............................................................................................................................................. 1
INTRODUCCIÓN A LA ESTRUCTURA Y OPERACIÓN DE ANALIZADORES LÉXICOS. ...... 1
A3.1. ESTRUCTURA DE UN LENGUAJE DE PROGRAMACIÓN. ...................................................................... 1
A3.2. ANALIZADOR LÉXICO. (PARSER) ..................................................................................................... 2
A3.3. REGLAS DE ANÁLISIS. ..................................................................................................................... 4
Símbolo terminal. ................................................................................................................................ 5
Símbolo no terminal. ........................................................................................................................... 5
Alternativa. .......................................................................................................................................... 5
Concatenación. .................................................................................................................................... 6
Repetición. ........................................................................................................................................... 7
Resumen. ............................................................................................................................................. 7
EJEMPLO A3.1. RECONOCEDOR SIMPLE. ................................................................................................... 8
EJEMPLO A3.2. PARSER BNF. ............................................................................................................... 10
EJEMPLO A3.3. RECONOCEDOR DE IDENTIFICADOR. .............................................................................. 13
EJEMPLO A3.4. RECONOCEDOR DE UNA DEFINICIÓN. ............................................................................ 15
A3.4. MANIPULACIÓN DE ARCHIVOS EN C. ............................................................................................. 18
Escritura de archivos de texto, con estructura de líneas. .................................................................. 19
Escritura de archivo, desde un programa. ........................................................................................ 19
Lectura de archivos de texto con estructura de líneas. ..................................................................... 20
Llenar un arreglo a partir de un archivo. ......................................................................................... 21
Escritura y lectura de archivos binarios. .......................................................................................... 22
Compilación y ejecución en ambiente UNIX. .................................................................................... 23
Escritura y lectura de archivos por líneas. ....................................................................................... 23
REFERENCIAS. ........................................................................................................................................ 24
ÍNDICE GENERAL. ................................................................................................................................... 25
ÍNDICE DE FIGURAS................................................................................................................................. 25

Índice de figuras.

FIGURA A3.1. ANÁLISIS SIN VOLVER ATRÁS. ................................................................................................ 3


FIGURA A3.2. SÍMBOLO TERMINAL. .............................................................................................................. 5
FIGURA A3.3. SÍMBOLO NO TERMINAL. ........................................................................................................ 5
FIGURA A3.4. ALTERNATIVA. ....................................................................................................................... 6
FIGURA A3.5. ALTERNATIVA, CON PRIMER SÍMBOLO EXPLÍCITO. ................................................................. 6
FIGURA A3.6. CONCATENACIÓN. .................................................................................................................. 7
FIGURA A3.7. REPETICIÓN. ........................................................................................................................... 7
FIGURA A3.8. REPETICIÓN, CON PRIMER SÍMBOLO EXPLÍCITO. ..................................................................... 7
FIGURA A3.9. GRAFO DEL RECONOCEDOR. ................................................................................................... 8
FIGURA A3.10. ESTADOS DE RECONOCEDOR DE IDENTIFICADOR. ............................................................... 14
FIGURA A3.11. ESTADOS DE RECONOCEDOR DE DEFINICIONES................................................................... 16

Profesor Leopoldo Silva Bijit 26-05-2008

Vous aimerez peut-être aussi