estructura de datos

122
Introducción……………………………………………………………………...........……………… 2 Capitulo I Análisis y Diseño de Algoritmos Algoritmo…………………………………………………………….………………………………….4 Características de los Algoritmos…………………………………………………………………….6 Análisis De Algoritmos……………………………………………………………………………...7 Recursos De Computadores Y Complejidad…………………………………………………….7 Recursos consumidos……………………………………………………………………………...8 Contenido de los datos de entrada………………………………………………………………….8 Diseño del Algoritmo………………………………………………………………………………....9 Técnica de diseño de Algoritmos…………………………………………………………………...9 Método de fuerza bruta……………………………………………………………………………...11 Divide y vencerás………………………………………………………………………………….13 Método voraz………………………………………………………………………………………....13 Algoritmos voraces………………………………………………………………………………..15 Desglose en monedas………………………………………………………………………………16 Multiplicación de Matrices………………………………………………………………………...19 Algoritmos De Vuelta Atrás…………………………………………………………………....20 Ramificación (Bifurcacion) Y Acotación………………………………………………………...21 Algoritmos Heuristicos…………………………………………………………………………....21 Algoritmos De Aproximación………………………………………………………………….……23 Algoritmos de búsqueda y ordenación……………………………………………………....24 Algoritmos De Búsqueda………………………………………………………………………...25 Algoritmo de Quick Sort………………………………………………………………………….26 Estructura de Datos UCENM

Upload: josue-rafael-montalvan-henriquez

Post on 27-Oct-2015

111 views

Category:

Documents


5 download

TRANSCRIPT

Page 1: Estructura de Datos

Introducción……………………………………………………………………...........……………… 2 Capitulo I Análisis y Diseño de Algoritmos Algoritmo…………………………………………………………….………………………………….4 Características de los Algoritmos…………………………………………………………………….6 Análisis De Algoritmos…………………………………………………………………………….…..7 Recursos De Computadores Y Complejidad…………………………………………………….…7 Recursos consumidos……………………………………………………………………………..….8 Contenido de los datos de entrada………………………………………………………………….8 Diseño del Algoritmo………………………………………………………………………………....9 Técnica de diseño de Algoritmos…………………………………………………………………...9 Método de fuerza bruta……………………………………………………………………………...11 Divide y vencerás………………………………………………………………………………….…13 Método voraz………………………………………………………………………………………....13 Algoritmos voraces……………………………………………………………………………….….15 Desglose en monedas………………………………………………………………………………16 Multiplicación de Matrices………………………………………………………………………..….19 Algoritmos De Vuelta Atrás………………………………………………………………….….…..20 Ramificación (Bifurcacion) Y Acotación………………………………………………………..….21 Algoritmos Heuristicos………………………………………………………………………….…...21 Algoritmos De Aproximación………………………………………………………………….……23 Algoritmos de búsqueda y ordenación…………………………………………………….….…..24 Algoritmos De Búsqueda………………………………………………………………………..….25 Algoritmo de Quick Sort………………………………………………………………………….…26 Estructura de Datos UCENM

Page 2: Estructura de Datos

Estructura de Datos UCENM 2

Semántica…………………………………………………………………………………….…..…27 Semántica operacional……………………………………………………………………….…....28 Semántica denotacional………………………………………………………………………..……28 Recursividad……………………………………………………………………………………..……29 Semántica axiomática…………………………………………………………………………..……29 Diseño De Algoritmos Recursivos……………………………………………………………..…...30 Recursividad no lineal…………………………………………………………………………..……30 … Recursividad mutua………………………………………………………………………...………..30 Recursividad estructural……………………………………………………………………….…….30 Recursión bien fundada………………………………………………………………………..…….31 Diseño De Algoritmos Iterativos……………………………………………………………..……...31 Programación Dinámica………………………………………………………………….……….…33 Capitulo II Tipos de Datos Abstractos Abstracción de datos…………………………………………………………………………………35 Separación de la interfaz e implementación…………………………………………………...….37 Tipos de Abstracción que Podemos Encontrar en un Programa………………………………..40 Capitulo III Árboles Definición no recursiva…………………………………………………………………………..…..45 Definición recursiva…………………………………………………………………………….……45 Nomenclatura sobre árboles…………………………………………………………………..……46 Uso de los árboles……………………………………………………………………………….….49 Estructura de Datos UCENM

Page 3: Estructura de Datos

Estructura de Datos UCENM 3

Árboles ordenados………………………………………………………………….…………..……50 Árboles degenerados…………………………………………………………………………..…….53 Árboles binarios…………………………………………………………………………………..…..53 Recorridos sobre árboles binarios……………………………………………………………….…53 Construcción de un árbol binario…………………………………………………………………...56 Árboles binarios de búsqueda………………………………………………………………………57 Árboles binarios perfectamente equilibrados……………………………………………………...62 Árboles rojinegros……………………………………………………………………………………64 Árboles B……………………………………………………………………………………………..67 Capitulo IV Teoría de Grafos Lazos o bucles………………………………………………………………………………….…..73 Tipos de grafos………………………………………………………………………………….….73 Grafo no dirigido……………………………………………………………………………………73 Grafo dirigido…………………………………………………………………………………….….74 Pseudografo………………………………………………………………………………………...75 Estructura de lista…………………………………………………………………………………..77 Vértice……………………………………………………………………………………………….77 Subgrafo…………………………………………………………………………………………….78 Ciclos y caminos hamiltonianos…………………………………………………………………..79 Grafo nulo o vacío………………………………………………………………………………….80 Grafos simples……………………………………………………………………………………...80 Grafo trivial………………………………………………………………………………………….80 Estructura de Datos UCENM

Page 4: Estructura de Datos

Estructura de Datos UCENM 4

Grafos conexos……………………………………………………………………………………80 Grafos completos………………………………………………………………………………….81 Grafos bipartitos…………………………………………………………………………………...82 Grafos ponderados………………………………………………………………………………..83 Grafos planos……………………………………………………………………………………...83 Teorema de los cuatro colores…………………………………………………………………..85 Coloración de grafos……………………………………………………………………………...86 Capitulo V Métodos de Clasificación (Ordenamiento) Clasificación de Métodos………………………………………………………………………..98 Comparación de los Métodos de Ordenamiento………………………………………………99 Algoritmos de ordenamiento…………………………………………………………………….100 Algoritmos de inserción………………………………………………………………………….101 Algoritmos de intercambio……………………………………………………………………….101 Algoritmos de selección………………………………………………………………………….101 Algoritmos de enumeración……………………………………………………………………...101 Método de Inserción………………………………………………………………………………102 Procedimiento Insertion Sort…………………………………………………………………….102 Método de selección……………………………………………………………………………..103 Método burbuja……………………………………………………………………………………104 Método de shell……………………………………………………………………………………105 Archivos……………………………………………………………………………………………107

Bases de Datos…………………………………………………………………………………...112

Page 5: Estructura de Datos

Estructura de Datos UCENM 5

Introducción

En programación, una estructura de datos es una forma de organizar un conjunto de datos elementales con el objetivo de facilitar su manipulación. Un dato elemental es la mínima información que se tiene en un sistema.

Las estructuras de datos son una colección de datos cuya organización se caracteriza por las funciones de acceso que se usan para almacenar y acceder a elementos individuales de datos.

Una estructura de datos se caracteriza por lo siguiente:

Pueden descomponerse en los elementos que la forman.

La manera en que se colocan los elementos dentro de la estructura afectará la forma en que se realicen los accesos a cada elemento.

La colocación de los elementos y la manera en que se accede a ellos puede ser encapsulada.

Una estructura de datos define la organización e interrelación de éstos y un conjunto de operaciones que se pueden realizar sobre ellos. Las operaciones básicas son:

Alta, adicionar un nuevo valor a la estructura. Baja, borrar un valor de la estructura. Búsqueda, encontrar un determinado valor en la estructura para realizar una

operación con este valor, en forma SECUENCIAL o BINARIO (siempre y cuando los datos estén ordenados)...

Otras operaciones que se pueden realizar son:

Ordenamiento, de los elementos pertenecientes a la estructura. Apareo, dadas dos estructuras originar una nueva ordenada y que contenga a las

apareadas.

Cada estructura ofrece ventajas y desventajas en relación a la simplicidad y eficiencia para la realización de cada operación. De esta forma, la elección de la estructura de datos apropiada para cada problema depende de factores como la frecuencia y el orden en que se realiza cada operación sobre los datos.

Cuando hablamos de tipos de datos básicos nos referimos a un conjunto de valores más sus operaciones asociadas, por ejemplo, dentro del computador un número entero se representa con un par de bytes (16 bits), con ello, sólo puede almacenar valores en un rango de [-2 16/2, +216/2] y disponer de los operadores aritméticos: +, -, *, / y mod. Extendiendo el concepto, si agrupamos un conjunto de valores de igual o distinto tipo de dato básico y enseguida definimos la manera de cómo operar sobre ellos, es decir, sus métodos de acceso, estaríamos en presencia de una ESTRUCTURA DE DATOS.

Page 6: Estructura de Datos

Estructura de Datos UCENM 6

La definición de una Estructura de Datos posee: Primer nivel: De abstracción en donde simplemente se identifica la colección de elementos a agrupar y sus operaciones de acceso. En un segundo nivel: El de implementación, ya pensamos en un lenguaje de programación específico y es ahí donde surgen preguntas como ¿cuál es la estructura óptima? o ¿qué funciones y/o procedimientos.definir? Como ya sabemos, las computadoras fueron diseñadas o ideadas como una herramienta mediante la cual podemos realizar operaciones de cálculo complicadas en un lapso de mínimo tiempo. Pero la mayoría de las aplicaciones de este fantástico invento del hombre, son las de almacenamiento y acceso de grandes cantidades de información. La información que se procesa en la computadora es un conjunto de datos, que pueden ser simples o estructurados. Los datos simples son aquellos que ocupan sólo una localidad de memoria, mientras que los estructurados son un conjunto de casillas de memoria a las cuales hacemos referencia mediante un identificador único. Debido a que por lo general tenemos que tratar con conjuntos de datos y no con datos simples (enteros, reales, booleanos, etc.) que por sí solos no nos dicen nada, ni nos sirven de mucho, es necesario tratar con estructuras de datos adecuadas a cada necesidad.

Page 7: Estructura de Datos

Estructura de Datos UCENM 7

Capitulo I

Análisis y Diseño de Algoritmos Algoritmo Un algoritmo es un conjunto de operaciones y procedimientos que deben seguirse para resolver un problema. Es un método para resolver un problema mediante una serie de datos precisos, definidos y finitos.

Marco Histórico

La palabra algoritmo se deriva del nombre latinizado del gran Matemático Árabe Mohamed Ibn Al Kow Rizmi, el cual escribió sobre los años 800 y 825 su obra Quitad Al Mugabala, donde se recogía el sistema de numeración hindú y el concepto del cero. Fue Fibinacci, el que tradujo la obra al latín y el inicio con la palabra: Algoritmi Dicit. El lenguaje algorítmico es aquel por medio al cual se realiza un análisis previo del problema a resolver y encontrar un método que permita resolverlo. El conjunto de todas las operaciones a realizar y el orden en que se deben efectuarse, se le denomina algoritmo. Es un método para resolver un problema mediante una serie de datos precisos, definidos y finitos.

Generalidades

El programador de computadoras es ante que nada una persona que resuelve problemas, por lo que para llegar a ser un programador eficaz se necesita aprender a resolver problemas de un modo riguroso y sistemático. A la metodología necesaria para resolver problemas mediante programas se denomina Metodología de la Programación. El eje central de esta metodología es el concepto, algoritmo. Un algoritmo es un método para resolver un problema. Aunque la popularización del término ha llegado con el advenimiento de la era informática, algoritmo proviene de Mohammed al-Khowarizmi, matemático persa que vivió durante el siglo IX y alcanzo gran reputación por el enunciado de las reglas para sumar, restar, multiplicar y dividir números decimales; la traducción al latín del apellido de la palabra algorismus derivo posteriormente en algoritmo. Euclides, el gran matemático griego (del siglo IV antes de Cristo) que invento un método para encontrar el máximo común divisor de dos números, se considera con Al-Khowarizmi el otro gran padre de la algoritmia (ciencia que trata de los algoritmos).

Page 8: Estructura de Datos

Estructura de Datos UCENM 8

El profesor Niklaus Wirth, inventor de Pascal, Modula-2 y Oberon, titulo uno de sus mas famosos libros, Algoritmos + Estructuras de Datos = Programas, significándonos que solo se puede llegar a realizar un buen programa con el diseño de un algoritmo y una correcta estructura de datos. Esta ecuación será de una de las hipótesis fundamentales consideradas en esta obra. La resolución de un problema exige el diseño de un algoritmo que resuelva el problema propuesto. Los pasos para la resolución de un problema son:

Diseño de algoritmo, que describe la secuencia ordenada de pasos que conducen a la solución de un problema dado. (Análisis del problema y desarrollo del algoritmo).

Expresar el algoritmo como un programa de lenguaje de programación adecuado. (Fase de codificación.)

Ejecución y validación del programa por la computadora. Para llegar a la realización de un programa es necesario el diseño previo de algoritmo, de modo que sin algoritmo no puede existir un programa. Los algoritmos son independientes tanto del lenguaje de programación en que se expresan como de la computadora que lo ejecuta. En cada problema el algoritmo se puede expresar en un lenguaje diferente de programación y ejecutarse en una computadora distinta; sin embargo, el algoritmo será siempre el mismo. Así, por ejemplo, en una analogía con la vida diaria, una receta de un plato de cocina se puede expresar en español, ingles o francés, pero cualquiera que sea el lenguaje, los pasos para la elaboración del plato se realizaran sin importar el idioma del cocinero. En la ciencia de la computación y en la programación, los algoritmos son más importantes que los lenguajes de programación o las computadoras. Un lenguaje de programación es tan solo un medio para expresar un algoritmo y una computadora es solo un procesador para ejecutarlo. Tanto el lenguaje de programación como la computadora son los medios para obtener un fin: conseguir que el algoritmo se ejecute y se efectúe el proceso correspondiente.

Problema

Diseño del

algoritmo

Programa de

computadora

Page 9: Estructura de Datos

Estructura de Datos UCENM 9

Dada la importancia del algoritmo en la ciencia de la computación, un aspecto muy importante será el diseño de algoritmos. El diseño de la mayoría de los algoritmos requiere creatividad y conocimientos profundos de la técnica de la programación. En esencia, la solución de un problema se puede expresar mediante un algoritmo.

Características de los Algoritmos Las características fundamentales que debe cumplir todo algoritmo son:

Un algoritmo debe ser preciso e indicar el orden de realización de cada paso.

Un algoritmo debe estar definido. Si se sigue un algoritmo dos veces, se debe obtener el mismo resultado cada vez.

Un algoritmo debe ser finito. Si se sigue un algoritmo se debe terminar en algún momento; o sea, debe tener un numero finito de pasos.

La definición de un algoritmo debe definir tres partes: Entrada, Proceso y Salida. En el algoritmo de receta de cocina citado anteriormente se tendrá: Entrada: ingrediente y utensilios empleados. Proceso: elaboración de la receta en la cocina. Salida: terminación del plato (por ejemplo, cordero).

Ejemplo de Algoritmo:

Un cliente ejecuta un pedido a una fábrica. Esta examina en su banco de datos la ficha del cliente; si el cliente es solvente entonces la empresa acepta el pedido; en caso contrario rechazara el pedido. Redactar el algoritmo correspondiente. Los pasos del algoritmo son: 1. inicio 2. leer el pedido 3. examinar la ficha del cliente 4. si el cliente es solvente aceptar pedido; en caso contrario, rechazar pedido 5. fin

Page 10: Estructura de Datos

Estructura de Datos UCENM 10

Análisis De Algoritmos

El análisis de algoritmos es una parte importante de la Teoría de complejidad computacional más amplia, que provee estimaciones teóricas para los recursos que necesita cualquier algoritmo que resuelva un problema computacional dado. Estas estimaciones resultan ser bastante útiles en la búsqueda de algoritmos eficientes.

A la hora de realizar un análisis teórico de algoritmos es corriente calcular su complejidad en un sentido asintótico, es decir, para un tamaño de entrada suficientemente grande.

La medida exacta (no asintótica) de la eficiencia a veces puede ser computada pero para ello suele hacer falta aceptar supuestos acerca de la implementación concreta del algoritmo, llamada modelo de computación. Un modelo de computación puede definirse en términos de una ordenador abstracto, como la Máquina de Turing, y/o postulando que ciertas operaciones se ejecutan en una unidad de tiempo.

Las estimaciones de tiempo dependen de cómo definamos un paso. Para que el análisis tenga sentido, debemos garantizar que el tiempo requerido para realizar un paso esté acotado superiormente por una constante.

Recursos De Computadores Y Complejidad

Algoritmo: Conjunto de reglas para resolver un problema. Su ejecución requiere unos recursos.

Un algoritmo es mejor cuantos menos recursos consuma, su facilidad de programarlo, corto, fácil de entender, robusto, etc. Criterio empresarial: Maximizar la eficiencia.

ALGORITMO 0 ó más entradas

1 ó más salidas

Memoria E/S

Comunicación

Page 11: Estructura de Datos

Estructura de Datos UCENM 11

Eficiencia: Relación entre los recursos consumidos y los productos conseguidos. Recursos consumidos: Tiempo de ejecución. Memoria principal: Entradas/salidas a disco. Comunicaciones, procesadores, etc. Lo que se consigue: Resolver un problema de forma exacta, forma aproximada o algunos casos.

Recursos consumidos Ejemplo. ¿Cuántos recursos de tiempo y memoria consume el siguiente algoritmo sencillo? i: = 0 a [n+1]:= x repetir i: = i + 1 hasta a[i] = x Respuesta: Depende. ¿De qué depende? De lo que valga n y x, de lo que haya en a, de los tipos de datos, de la máquina... En general los recursos dependen de:Factores externos.

El ordenador donde lo ejecutemos.

El lenguaje de programación y el compilador usado.

La implementación que haga el programador del algoritmo. En particular, de las estructuras de datos utilizadas.

Tamaño de los datos de entrada. Ejemplo. Calcular la media de una matriz de NxM.

Contenido de los datos de entrada. Mejor caso. El contenido favorece una rápida ejecución. Peor caso. La ejecución más lenta posible. Caso promedio. Media de todos los posibles contenidos. Los factores externos no aportan información sobre el algoritmo. Normalmente usaremos la notación T(N)=..., pero ¿qué significa T(N)? Tiempo de ejecución en segundos. T(N) = bN + c. Suponiendo que b y c son constantes, con los segundos que tardan las operaciones básicas correspondientes.

Page 12: Estructura de Datos

Estructura de Datos UCENM 12

Diseño del Algoritmo En la etapa de análisis del proceso de programación se determina que hace el programa. En la etapa de diseño se determina como hace el programa la tarea solicitada. Los métodos mas eficaces para el proceso de diseño se basan en el conocido por Divide y Vencerás, es decir, la resolución de un problema complejo se realiza dividiendo el problema en sub problemas y a continuación dividir estos sub problemas en otros de nivel mas bajo, hasta que pueda ser implementada una solución en la computadora. Este método se conoce técnicamente como diseño descendente (Top Down) o modular. El proceso de romper el problema en cada etapa y expresar cada paso en forma más detallada se denomina refinamiento sucesivo. Cada sub programa es resuelto mediante un modulo (sub programa) que tiene un solo punto de entrada y un solo punto de salida. Cualquier programa bien diseñado consta de un programa principal (el modulo de nivel mas alto) que llama a sub programas (módulos de nivel mas bajo) que a su vez pueden llamar a otros sub programas. Los programas estructurados de esta forma se dice que tienen un diseño modular y el método de romper el programa en módulos más pequeño se llama Programación Modular. Los módulos pueden ser planeados, codificados, comprobados y depurados independientemente (incluso por diferentes programadores) y a continuación combinarlos entre si. El proceso implica la ejecución de los siguientes pasos hasta que el programa se termina:

programar modulo.

Comprobar el modulo.

Si es necesario, depurar el modulo.

Combinar el modulo con los módulos anteriores. El proceso que convierte los resultados del análisis del problema en un diseño modular con refinamiento sucesivo que permitan una posterior traducción al lenguaje se denomina diseño de algoritmo. El diseño del algoritmo es independiente del lenguaje de programación en el que se vaya a codificar posteriormente.

Técnica de diseño de Algoritmos Hasta ahora se han realizado algunos comentarios respecto a la necesidad de diseñar algoritmos correctos y eficientes utilizando los elementos de un lenguaje de programación .Es necesario en este momento mencionar algo sobre como hacerlo. El acto de diseñar un algoritmo puede considerarse como una tarea que difícilmente podrá ser del todo automatizada.

Page 13: Estructura de Datos

Estructura de Datos UCENM 13

Todo problema algorítmico es un reto para su diseñador, algunos resultan inmediatos de resolver, otros son bastante complejos. La investigación en esta área ha permitido descubrir un conjunto de métodos o esquemas de diseño hacia los cuales puede orientarse la realización de muchos algoritmos. No obstante, y a pesar de que resulta mas adecuado en bastantes casos utilizar alguno de estos esquemas que realizar un diseño desde cero, idear un algoritmo continua siendo una labor bastante creativa donde los conocimientos y la experiencia del propio diseñador tiene un papel fundamental. El diseño de un algoritmo que resuelva un problema es, en general, una tarea difícil. Una forma de facilitar esta labor consiste en recurrir a técnicas conocidas de diseño de algoritmos, se decir, a esquemas muy generales que pueden adaptarse a un problema particular al detallar las partes generales del esquema. Muchos problemas pueden resolverse buscando una solución fácil y directa pero, a la vez bastante ineficiente. Este método, llamado de fuerza bruta, puede ser muy directo, pero con un poco de análisis puede encontrarse algoritmos más eficientes. El esquema mas sencillo quizás sea el llamado divide y vencerás, basado en la descomposición de un problema en sub problemas. Otros esquemas requieren un análisis minucioso del problema de forma que la solución se vaya construyendo en etapas. Si puede preverse que decisión conviene en cada etapa para producir cierto tipo de mejor resultado, tenemos una solución voraz, si la decisión en una etapa, solo puede tomarse tras considerar varias soluciones de otras etapas mas simples, la solución es dinámica. Aun así, hay problemas cuya solución no puede hallarse sino mediante un proceso de búsqueda, a pesar de lo complejas que son las operaciones de búsqueda, su uso adecuado mediante el esquema de búsqueda con retroceso (o backtracking) permite ganar gran eficiencia respecto a soluciones de fuerza bruta. Por ultimo, conviene conocer otros métodos de diseño de algoritmos que también resultan de utilidad práctica. Nos estamos refiriendo a métodos basados en la mejora de la eficiencia (por ejemplo, el uso de parámetros de acumulación al resolver problemas utilizando divide y vencerás, y el empleo de tablas como estructura auxiliar para la resolución eficiente de problemas donde se aplica programación dinámica), y a métodos basados en transformaciones del dominio para encontrar una solución mas fácilmente a un problema en un dominio transformado, siendo dicha solución finalmente adaptada al dominio original.

Consideraciones generales

Si el hábil programador dispone de un recetario de algoritmos de donde poder seleccionar el más adecuado para cada problema, su tarea se simplifica. Supongamos que disponemos de una especificación precisa, completa y consistente del problema a resolver y queremos obtener un algoritmo en el que, dados uno datos de entrada valido, se produzca cierto resultado. Si no nos importa la eficiencia del algoritmo, podríamos

Page 14: Estructura de Datos

Estructura de Datos UCENM 14

utilizar un algoritmo general llamado algoritmo del museo británico. Se programa un computador de manera que parta de un conjunto de axioma matemáticos y los que use para reducir aleatoriamente teoremas validos. Aprender los principios básicos del diseño de algoritmos podemos preguntarnos por un método aceptable. El mas entendido, y quizás el mejor, es organizar el diseño sobre un esquema de algoritmo o una técnica de diseño que haya demostrado su utilidad para otros problemas. Este método de trabajo es practicable, puesto que existe un número reducido de esquema y técnicas de diseño. El conocimiento de técnicas de diseño es solo un primer paso para el diseñador, que debe completarse con otros conocimientos y, sobre todo, con la experiencia.

Método de fuerza bruta

Comenzamos el estudio de esquemas algorítmicos con un método sencillo, pero que debe evitarse siempre que se pueda, dad su ineficacia; la fuerza bruta. En realidad, no es un esquema algorítmico, si no mas bien calificativo. Para una forma de diseñar algoritmos: tomar una solución directa, poco reflexionada. En principio, esto no es malo, pero dado que no se ha analizado apenas el problema, es muy probable que no se hayan aprovechado propiedades deducibles del problema y que la solución sea terriblemente ineficiente. Una solución por fuerza bruta también puede resultar adecuada como primera aproximación a la solución final, porque su desarrollo puede permitir profundizar más sobre el problema y conocer propiedades que sean utilizadas para obtener otra versión más eficiente. Por ejemplo: Algunos algoritmos de búsqueda de un elemento en un vector. Uno de ellos realizaba una búsqueda secuencial con complejidad lineal sobre el tamaño del vector y podía usarse con cualquier vector. Otro algoritmo realizaba un búsqueda dicotomica o binaria, con complejidad logarítmica, y solo se podía usar cuando el vector estuviese ordenado. El algoritmo primero responde a un razonamiento más sencillo, por lo que uno puede sentirse tentado a usar siempre. Esta es la solución de fuerza bruta: una solución directa, pero poco reflexionada. Lo más razonable es comprobar si el vector esta ordenado y, en caso positivo, aprovechar esta circunstancia para usar el algoritmo más eficiente: el de búsqueda binaria.

Técnicas de los Parámetros Acumuladores y de Tabulacion

La recurcion es un mecanismo que permite obtener, en combinación con otras construcciones, una solución funcional a muchos problemas. Muchos algoritmos recursivos

Page 15: Estructura de Datos

Estructura de Datos UCENM 15

resultan eficientes, pero no todos: hay algunos fácilmente formulables, pero muy ineficientes. En estos casos, dichos algoritmos pueden servir como una primera aproximación al algoritmo definitivo, pero debe mejorar su rendimiento para que sea práctico. Veremos dos parámetros para la mejora de eficiencia de algoritmos recursivos: el uso de parámetros acumuladores y el uso de tablas. Cada una se ilustra con un ejemplo distinto.

Parámetros Acumuladores

Veamos primero una solución ineficiente que intentaremos mejorar. Ejemplo: Números de Fibonacci Los números de fibonacci suele especificarse como: Fib(0)=1 Fib(1)1 Fib(n+2)=fib(n)+fib(n+1) Esta especificación de los números de fibonacci tiene una formulación recursiva inmediata en estilo funcional. Un modo de evitar problema lo proporciona la técnica de los parámetros acumuladores, cuya idea básica se expone a continuación. La función principal usa una función auxiliar que tiene los parámetros de aquellas más algunos adicionales. La función principal simplemente realiza una llamada a esta función auxiliar en los que los parámetros de aquellas se modifican y los parámetros nuevos toman un valor inicial adecuado. Los parámetros adicionales tienen como misión ir acumulando resultados principales durante el proceso recursivo.

Tabulacion

No todos los algoritmos recursivos ineficientes pueden optimizarse con la técnica de los parámetros acumuladores. Otra técnica útil es el uso de tablas. La intención es que la primera vez que se realiza un cálculo, se almacena en una tabla, donde puede consultarse otras veces que se necesite. Esta técnica también se suele emplear con la programación dinámica. Ejemplo:

Page 16: Estructura de Datos

Estructura de Datos UCENM 16

Sea el problema de la competición. Hay dos participantes (deportistas o equipos, no importa que), A,B, que juegan una competición que es ganada por el primero que venza en n partidos, siendo ( n ) mayor que( 0 ). Por sencillez , se supone que ambos participantes tienen cualidades y preparación similar . De forma que cada uno tiene un 50% de posibilidades de ganar cada partido. De todas formas, la modificación para incorporar probabilidades diferentes es evidente y no complica el problema.

Divide y vencerás

Consiste en descomponer un problema en un subproblema, resolver independientemente los subproblemas para luego combinar sus soluciones y obtener la solución del problema original. Esta técnica se puede aplicar con éxito a problemas como la multiplicación de matrices, la ordenación de vectores, la búsqueda en estructuras ordenadas, etc. Ejemplo. Búsqueda de una palabra en un diccionario Como ejemplo sencillo de aplicación de esta estrategia puede considerarse la búsqueda de una palabra en un diccionario de acuerdo con el siguiente criterio. Se abre el diccionario por la pagina centrar (quedando dividido en dos mitades) y se comprueba si la palabra aparece allí o si es léxico gráficamente anterior o posterior. Si no ha encontrado y es anterior se procede a buscarla en la primera mitad., si es posterior, se buscara en la segunda mitad. El procedimiento se repite sucesivamente hasta encontrar la palabra o decidir que no aparece.

Método voraz Este método trata de producir tipo de mejor resultado a partir de conjunto de opciones candidatas .Para ello, se va procedimiento paso a paso realizándose la mejor elección (usando una función objetivo que respeta un conjunto de restricciones ) de entre las posibles. Puede emplearse en problemas de optimización, como el conocido de la mochila, en la búsqueda de caminos mínimos sobre grafos, la planificación en el orden de la ejecución de unos programas en un computador, etc. Ejemplo. Dar un cambio utilizando el menor número de monedas Considérese ahora el problema de la devolución del cambio al realizar una compra (por ejemplo, en una maquina expendedora de tabaco). Suponiendo que se disponga de cantidad

Page 17: Estructura de Datos

Estructura de Datos UCENM 17

suficiente de ciertos tipos diferentes de monedas de curso legal, se trata de dar como cambio la menor cantidad posible usando estos tipos de monedas. La estrategia voraz aplicada comienza devolviendo, cuando se pueda, la moneda de mayor valor ( es decir, mientras el valor de dicha moneda sea mayor o igual al cambio que resta por dar), continua aplicándose el mismo criterio para la segunda moneda mas valiosa, y así sucesivamente. El proceso finaliza cuando se ha devuelto todo el cambio.

Consideraciones y Criterios para Diseñar Algoritmos

Algunas consideraciones estilísticas pueden contribuir a mejor la calidad de los algoritmos (y programas ) mediante la reducción del numero de errores que aparecen al desarrollar los. También influyen haciendo que nuestro algoritmo resulte más fácil de leer y entender para otras personas. Los criterios de estilo pueden reflejarse en un conjunto de normas de estilo de codificación. Ello asegura que tanto algoritmos como programa resulten legibles y puedan modificarse fácilmente en caso de necesidad. Generalmente, estas normas de estilo se dirigen hacia aspectos como la forma de construir los nombres de variables o tipo de datos que aparezcan., la tipografía seguida ala hora de escribir nombres de variables, subprogramas, palabras claves, etc. El modo de encolumnar las distintas partes de un algoritmo para facilitar su lectura y comprensión, y la normas sobre como y donde deben de introducirse los comentarios. Estilo y calidad de los programas van fuertemente unidos. Ante la pregunta ¿Cuáles son las característica de un buen algoritmo?, las siguientes respuestas reflejan, cierta medida, los factores que identifican la calidad en ellos .

Corrección, el algoritmo debe funcionar.

Nunca se debe olvidar que la característica más simple e importante de un algoritmo es que funcione. Pude aparecer obvio, pero resulta difícil de asegurar en algoritmos complejos.

Eficiencia, el algoritmo no debe desaprovechar recursos. La eficiencia de un algoritmo se mide por los recursos que este consume. En particular, se habla de la memoria y del tiempo de ejecución. A pesar de que con la reducción de los costes del hardware es posible diseñar computadores más rápidos y con más memoria, no hay que desperdiciar estos recursos y tratar de desarrollar algoritmos más eficientes.

Claridad, el algoritmo debe estar bien documentación. La documentación ayuda a comprender el funcionamiento de los algoritmos. Ciertos detalles o algunas partes especiales de los mismos pueden olvidarse fácilmente o quedar oscura si no están adecuadamente comentadas.

En realidad, y de acuerdo con los puntos de vista anteriores, la calidad de un algoritmo tiene muchas facetas y todas ellas importantes. Resumiendo, lo ideal es que nuestro algoritmo resulte correcto, eficiente, claro, fiable y fácil de mantener.

Page 18: Estructura de Datos

Estructura de Datos UCENM 18

Algoritmos voraces

Esquema voraz: Hay muchos problemas en los que se pretende obtener un subconjunto de n elementos que satisfaga ciertas restricciones y que optimice alguna medida. Se supone que un problema de esta clase tiene al menos una solución. Puede haber varias soluciones optimas, en cuyo caso no importa cual se elija. Por ejemplo, sea el problema de encontrar un subconjunto de los arcos de un grafo. Otro ejemplo se da cuando, dados unos ficheros almacenados en una cinta de que el tiempo de recuperación de un fichero cualquiera sea el mínimo en promedio. A menudo, el problema incluye restricciones adicionales que limitan el número posible de soluciones. Normalmente, estos problemas no se intentan resolver “de golpe “, encontrando de una sola vez la solución completa y óptima. Es más frecuente que el subconjunto de la solución se vaya formando paso a paso, analizando durante cada etapa que elemento conviene añadir a la solución parcial ya existente. La dificultad principal para resolver esta clase de problemas estriba en el análisis necesario para poder formular un algoritmo que halle la solución en varios pasos. Un algoritmo voraz sigue el esquema anterior, pero con la fortuna de que cada vez que añade un elemento a la solución se tiene la certeza de haber realizado la mejor elección posible. Esta característica hace que aunque el análisis del problema sea arduo, la solución voraz siempre resulte sencilla. La única complicación es comprobar que se siguen satisfaciendo las restricciones del problema. Por lo que se ha descrito del esquema voraz, éste es un proceso repetitivo sencillo que trata sucesivamente los diferentes elementos del problema. Para facilitar la descripción de este proceso, puede llamarse candidato al elemento tratado en cada paso. Inicialmente, el conjunto de candidatos que forman la solución está vacío. En cada paso se intenta añadir el mejor de los candidatos restantes a dicha solución parcial. Si este conjunto ampliado sigue siendo válido, es decir, si satisface las restricciones del problema y, por tanto, permite formar una solución del problema, el candidato se incorpora definitivamente. Al contrario, si dicho conjunto no es válido, se desecha el candidato. Si el algoritmo voraz se ha diseñado correctamente, la primera solución encontrada es óptima. Por tanto, la dificultad principal al diseñar un algoritmo voraz reside en encontrar un criterio en encontrar un criterio de selección que garantice la optimalidad de la solución. Según esta descripción, el problema parte de:

Una función objetivo que da el valor de una solución. Obviamente, ésta es la función por optimizar.

Un conjunto de restricciones sobre el valor de los datos de entrada y sobre la solución final del problema.

A su vez, la solución consta de:

Page 19: Estructura de Datos

Estructura de Datos UCENM 19

Un conjunto de candidatos

Una función de selección que en cada momento determine que candidato de los aún no usados parece ser el mejor.

Una función que determine si cierto conjunto de candidatos es válido; es decir, si permite formar alguna solución del problema.

Obsérvese que las funciones de validez y completitud no se preocupan de la optimalidad del la solución, pero si la función de selección es la adecuada, cada solución válida y completa es optima. Podemos representar el esquema voraz de la siguiente forma funcional: FUNCTION Voraz ( candidatos: ( 1..n ) : ( 1..n) -> FUNCTION VorazAcumulador ( candidatos : (1..n), Solución : (1..n) : (1..n) -> Cadidatos = ( ) v EsSolución ( solución)-> Value siguiente -> seleccionar ( candidatos ) IN EsVálida (solución v ( siguiente)) => VorazAcumulador (candidatos – (solución), solución v (siguiente)) VorazAcumulador (candidatos – (siguiente), solución) VorazAcumulador (candidatos, ( ) ) Puede verse por qué estos algoritmos se llaman “voraces “: en cada paso toman el mejor trozo de la solución; es decir, el mejor candidato. Además, nunca cambian de opinión: una vez que un candidato es aceptado o rechazado en la solución, la decisión, es definitiva. La función objetivo no suele aparecer en el algoritmo final, sino que se utiliza durante el análisis del problema y es determinante en la elección de la función de selección. De todas formas, debe recordarse que puede haber varios criterios alternativos de selección y que de su correcta elección depende que la solución calculada por el algoritmo sea optima. Como ejercicio, se puede intentar encontrar una solución voraz del problema del calendario. Es fácil encontrar una solución si en cada etapa se genera el subcalendario correspondiente a un equipo; es decir, la tabla de competición se va completando por filas. Como fila primera se toma la secuencia de los índices de los participantes en cualquier orden. Cada fila resultante puede tener una complejidad de o (n2). Además, este algoritmo tiene la ventaja de valer para las situaciones en que el número de participantes no es una potencia de dos.

Desglose en monedas

Como primer ejemplo introductorio sencillo al que puede aplicarse la técnica voraz, se considera el problema de un cambio o desglose en monedas. Hay que desglosar una cantidad en un conjunto de monedas tratando de cumplir alguna condición; en este caso, utilizar el menor número de monedas. Para ello, se parte de un conjunto de tipos de monedas válidas, de las que se supone que hay cantidad suficiente para realizar el desglose,

Page 20: Estructura de Datos

Estructura de Datos UCENM 20

y de un importe. Se trata de indicar la cantidad (menor) de monedas de los tipos considerados, tales que sumados sus valores equivalgan al importe. Para simplificar, suponemos que manejamos dinero español y, en particular, podemos utilizar sólo monedas de 500, 100, 50, 25, 5 y 1 pesetas para el desglose. Estos valores se definen por medio de un tipo enumerado MONEDAS. Asimismo, se declaran los tipos VALORES y CANTIDADES para representar el valor asignado a cada unidad monetaria y la cantidad de cada tipo de moneda que se devolverá en el desglose. Su declaración es la siguiente: TYPE Monedas -> M500 I M100 I M50 I M25 I M5 I M1, Valores -> Integer M500…M1 Cantidades -> Integer M500….M1 Se supone inicialmente asignados los valores a cada uno de los tipos de monedas. Los elementos de la técnica voraz están presentes en este problema de la siguiente forma:

El conjunto de candidatos está constituido por cada una de las monedas de los diferentes tipos que se pueden usar para realizar el desglose del importe dado.

Una solución viene dad por un conjunto de monedas devuelto tras el desglose, y cuyo valor total es igual al importe a desglosar.

La condición de factibilidad de la solución siendo construida establece en el desglose debe ser menor o igual que el importe a desglosar.

La función de selección establece que hay que elegir, mientras sea posible, la moneda de mayor valor de entre las candidatas.

La función objetivo cosiste en minimizar la cantidad total de monedas utilizadas en el desglose.

Con esta información se puede comprobar que en este problema están presentes los distintos elementos de la técnica voraz. Además, cuando un candidato (moneda) se incorpora al conjunto solución, éste no será nunca excluido de él. .

Ordenación de un Vector por Mezcla La ordenación de un vector es un problema que se presta fácilmente a la aplicación de la técnica de divide y vencerás. El caso básico corresponde a un subvector de un solo elemento, que obviamente ya esta ordenado. Para el caso general, sea vi..s un vector de índice inferior i e índice superior s. La partición puede hacerse por la mitad si se toma un índice m=[(i+s)/2] y dos subvectores vi..m y vm+1..s . La combinación de los dos subvectores ya ordenados es fácil. basta con mezclar los dos subvectores, mediante comparaciones de sus elementos sucesivos, para obtener un único vector ordenado. Este proceso de mezcla es realizado por un procedimiento auxiliar. El algoritmo resultante es: PROCEDURE Ordenar (INOUT v : INTEGER1..N) -> (* ordenación por mezcla *) PROCEDURE OrdenarAux (INOUT Vector : INTEGER1..N,1..N, IN inf, sup : 1..N) ->

Page 21: Estructura de Datos

Estructura de Datos UCENM 21

VAR Medio : 1..N IF inf < sup THEN medio: = (inf+sup) Div 2; OrdenarAux (vector, inf, medio); OrdenarAux (vector, medio+1, sup); Mezclar (vector, inf, medio, sup) OrdenarAux (v, 1, N) El procedimiento para realizar la mezcla de los subvectores ordenados es: PROCEDURE Mezclar (IN inf: INTEGER, IN medio: INTEGER, IN sup: INTEGER, INOUT vector: INTEGER1.N) -> VAR vectorAux : INTEGER1..N, i1, i2, j : INTEGER, índice : INTEGER i1 := inf; i2 := medio + 1; j := inf; WHILE (i1<=medio) ^ (i2<=sup) DO IF vectori1 << vectori2 THEN vectorAuxj :=vectori1; i1 :=i1 + 1

ELSE

vectorAuxj ;= vectori2; i2 := i2 + 1 j := j + 1 FOR índice IN i1..medio DO vectorAuxj := vectorindice; J := j + 1 FOR índice IN i2..sup DO vectorAuxj := vectorindice ; J := j + 1 FOR índice In inf..sup DO vectorindice := vectorAuxindice El algoritmo resultante es sencillo conceptualmente. Es fácil analizar la complejidad del algoritmo para un vector de longitud n. La operación de mezcla es proporcional a n, de forma que las ecuaciones de recurrencia de la función de tiempo son:

Page 22: Estructura de Datos

Estructura de Datos UCENM 22

T(n) = a, n=1, a=cte

2T(n/2) + bn, n>1, b=cte Si n es una potencia de 2; es decir, n =2k para algún k, las ecuaciones anteriores se resuelven por sustituciones sucesivas, resultando: T(n) = 2T(n/2) + bn=…=2K T(n/2K) + kbn = an + bn log2 n El algoritmo de ordenación por mezcla es óptimo en tiempo de ejecución. Los únicos inconvenientes que presenta es que el procedimiento de mezcla necesita gran capacidad de almacenamiento (para dos copias del vector) y que, además de mezclar, necesita copiar el vector auxiliar completo en el principal. Puede diseñarse un algoritmo de mezcla más complejo que mejore ambos aspectos, pero mantenga la complejidad asintótica calculada.

Multiplicación de Matrices Sean dos matrices, A y B, de dimensión nxn. La matriz producto C=AxB también es una matriz de nxn cuyo elemento (i,j)-esimo se forma multiplicando cada elemento de la final i-esima de A por el elemento correspondiente de la columna j-esima de B y sumando los productos parciales. El cálculo de cada elemento Cij requiere n multiplicaciones. La matriz C tiene n2 elementos, así que el tiempo total del algoritmo de multiplicación es de orden O(n3). El algoritmo anterior, que podemos llamar algoritmo convencional de multiplicación de matrices, proviene directamente de la definición matemática del producto de matrices. Sin embargo, la técnica de divide y vencerás sugiere un algoritmo distinto. Supongamos, por sencillez, que n es una potencia de dos; es decir, que existe un entero no negativo k tal que n=2k. (Si n no es un potencia de dos, pueden añadirse las filas y columnas de ceros necesarias para formar una dimensión que sea potencia de dos.) las submatrices A y B pueden partirse en cuatro submatrices de dimensión (n/2)x(n/2). Si el producto AxB tiene la forma: A11 A12 B11 B12 C11 C12 A21 A22 B21 B22 C21 C22 Entonces: C11 = A11*B11 + A12*B21 C12 = A11*B12 + A12*B22 C21 = A21*B11 + A22*B21 C22 = A21*B12 + A22*B22 Para n=2, los elementos Cij se calculan mediante algunas multiplicaciones y sumas de números, pero para n>2 las submatrices Cij se calculan mediante multiplicaciones (recursivas) y sumas de submatrices de dimensión (n/2)x(n/2). Dos submatrices de (n/2)x(n/2) pueden sumarse en un tiempo bn2, siendo b alguna constante.

Page 23: Estructura de Datos

Estructura de Datos UCENM 23

La resolución de este sistema de ecuaciones nos dice que O(T(n))=OT(n3), de forma que no se ha conseguido ningún ahorro sustancial de tiempo. Sin embargo, interesa encontrar algoritmos mas eficientes, porque la multiplicación esta relacionada con otras operaciones sobre matrices mas usuales, como inversión de una matriz o hallar su determinante. La existencia de un algoritmo eficiente para la multiplicación (en realidad, para cualquier operación de las anteriores) significaría la existencia de un algoritmo similar para las demás. Podría conseguirse mas eficiencia si lográramos realizar menos multiplicaciones de matrices, aunque fuera a costa de un mayor numero de sumas de matrices, dado que la complejidad respectiva de estas operaciones es O(n3)n y o(n2). El algoritmo de Strassen calcula las cuatro submatrices Cij empleando 7 multiplicaciones y 18 sumas o restas de matrices.

Algoritmos De Vuelta Atrás

Existen un alto número de problemas que pueden formularse como la búsqueda de la mejor solución o del conjunto de todas las soluciones que satisfacen ciertas condiciones. Además, cada solución es el resultado de una secuencia de decisiones. Por supuesto, debe existir una función de criterios que debe ser satisfecha por cada secuencia solución u optimizada por dichas secuencias solución si solo queremos la mejor. En algunos problemas de optimización se conoce un criterio óptimo de selección que puede usarse de forma voraz. Otros problemas satisfacen el principio de optimalidad, pudiéndose aplicar la técnica de programación dinámica. Sin embargo, todavía hay otros problemas peores que no queda mas remedio que realizar una búsqueda de la solución.

Esquema de Algoritmos de Vuelta Atrás Sea (x1,...,xi) el camino desde la raíz hasta un nodo de un árbol del espacio de estado. Sea G(x1...xi) el conjunto de todos los valores posibles de xi+1 tales que (x1,...,xi+1) es un camino hasta el estado del problema. Supongamos que existe algún predicado acotador A tal que A(x1,...,xi+1) es falso si el camino (xi,...,xi+1) no puede extenderse para alcanzar un nodo de respuesta. Por lo tanto, los candidatos para la posición i+1 del vector desolucion x1..n son aquellos valores generados por G que satisfacen A. Supongamos que también existe algún predicado R que determina si un camino (x1,...,xi+1) termina en un nodo de respuesta. El Algoritmo de Vuelta Atrás se especifica de la forma siguiente: PROCEDURE Retroceso (IN k : INTEGER, INOUT solución : elemento1...n) -> VAR nodo: elemento FOR noso IN G(solucion, 1, k-1) DO Solucion k := nodo; IF R(solucion, 1, k) THEN

Page 24: Estructura de Datos

Estructura de Datos UCENM 24

IF R(solucion, 1,k) THEN << guardar „solucion‟ >>; Retroceso (k+1, solucion) La llamada inicial del algoritmo es Retroceso(1, solucion). El procedimiento no hace ninguna llamada recursiva cuando k = N+1 o cuando ningún nodo generado por G satisface el elemento posible que satisfacen A se añade una solución particular, se comprueba si se ha encontrado una solucion. Después simplemente se llama recursivamente al algoritmo para generar los estados descendientes. Se sale del bucle FOR cuando no quedan mas valores para solución terminando la llamada actual al algoritmo.

Ramificación (Bifurcacion) Y Acotación

Los métodos de Ramificación y Acotación constituyen un a variante de las técnicas de retroceso para problemas donde se trata de encontrar el valor máximo o mínimo de cierta función objeto (esto suele suceder en los problemas de programación lineal entera). La técnica de ramificación y acotacotacion aplica de la siguiente manera: Supóngase que al recorrer un árbol y alcanza una hoja se tiene una solucion con k colores, y que al seguir avanzando en el árbol (mediante la aplicación de varios pasos de retrocesos) se alcanza un nodo que requiere k+1 colores. En este punto podemos retroceder (y no seguir avanzando por mas ramas), pues tenemos ya una solucion mayor. Así, k sirve de cota inferior al retroceso. Este mismo proceso se repite en el resto de nodos del árbol, evitando así la exploración de gran parte de al estructura.

Algoritmos Heuristicos

Existen muchos problemas para los cuales no se conocen algoritmos que puedan encontrar la solución de forma eficiente: problemas NP-completos. La solución exacta puede requerir un orden factorial o exponencial: el problema de la explosión combinatoria. Se hace necesario utilizar algoritmos heurísticos: Un algoritmo heurístico (o simplemente heurística) puede producir una buena solución (puede que la óptima) pero también puede que no produzca ninguna solución o dar una solución no muy buena. Normalmente, se basa en un conocimiento intuitivo del programador sobre un determinado problema. La estructura de algoritmo voraz se puede utilizar para construir procedimientos heurísticos: hablamos de heurísticas voraces. Objetivo: obtener buenas soluciones en un tiempo de ejecución corto.

Page 25: Estructura de Datos

Estructura de Datos UCENM 25

El problema del viajante

Problema: Dado un grafo no dirigido, completo y ponderado G = (V, A), encontrar un ciclo simple de costo mínimo que pase por todos los nodos.

Es un problema NP, pero necesitamos una solución eficiente. Problema de optimización, donde la solución está formada por un grupo de elementos en cierto orden: podemos aplicar el esquema voraz. Posibilidades:

Los nodos son los candidatos. Empezar en un nodo cualquiera. En cada paso moverse al nodo no visitado más próximo al último nodo seleccionado.

Las aristas son los candidatos. Hacer igual que en el algoritmo de Kruskal, pero garantizando que se forme un ciclo.

Heurística voraz 1 Una solución será un cierto orden en el conjunto de nodos (c1, c2, ..., cn), el orden de visita

de los nodos. Inicialización: seleccionar un nodo cualquiera. Función de selección: de los nodos candidatos seleccionar el más próximo al último (o al primero) de la secuencia actual (c1, c2, ..., ca).

Acabamos cuando tengamos n nodos. Ejemplo. Empezando en el nodo 1.

15

30

20

25

50

45

10

35

40 55

5

4

3

1

2

Page 26: Estructura de Datos

Estructura de Datos UCENM 26

Solución: (1, 4, 5, 3, 2) Coste: 30+15+25+10+45=125 Empezando en el nodo 3. Solución: (5, 4, 3, 2, 1) Coste: 15+20+10+45+50=140 Heurística voraz 2 Una solución será un conjunto de aristas (a1, a2, ..., an-1) que formen un ciclo hamiltoniano,

sin importar el orden. Empezar con un grafo sin aristas. Selección: seleccionar la arista candidata de menor coste. Factible: una arista se puede añadir a la solución actual si no se forma un ciclo (excepto para la última arista añadida) y si los nodos unidos no tienen grado mayor que 2. Ejemplo. Solución: ((2, 3), (4, 5), (3, 4), (1, 2), (1, 5)) Coste = 10+15+20+45+50 = 140 Conclusión Ninguno de los dos algoritmos garantiza una solución óptima. Sin embargo, normalmente ambos dan soluciones buenas, próximas a la óptima. Posibles mejoras: buscar heurísticas mejores; repetir la heurística 1 con varios orígenes; ó bien, a partir de la solución del algoritmo intentar hacer modificaciones locales para mejorar esa solución.

Algoritmos De Aproximación Dado un problema NP completo, es probable que no sepamos resolverlo de manera precisa y completa utilizando un algoritmo polinomico en tiempo. Para este tipo de problemas, los algoritmos que no conducen a una solución óptima se llaman algoritmos de aproximación. Sin embargo, resulta parcialmente interesante que estos garanticen una cota en el margen de imprecisión. A continuación se ilustra este tipo de tratamiento de problemas al problema de recubrimiento de un grafico: Dado un grafo G=(V,A), se trata de encontrar un conjunto con el menor numero de vértices tal que toda arista sea incidente por lo menos de un vértice de V. Este problema se puede resolver a través de otro aproximado, como es calcular el ajuste maximizal del grafo G. Se trata de calcular un subconjunto A‟ de aristas tal que dos aristas

Page 27: Estructura de Datos

Estructura de Datos UCENM 27

cualquiera de A‟ no tengan ningún vértice común y toda arista de A-A‟ comparta algún vértice común con una arista de A‟. Este nuevo problema garantiza conseguir un recubrimiento que contiene no más de dos vértices del recubrimiento mínimo. El procedimiento para construir un ajuste maximizal de un grafo G consistiría en ir tomando aristas de G, de una en una y en cualquier orden e ir eliminando las incidentes al conjunto que se esta construyendo hasta recubrir todo en grafo. Para poder aplicar el nuevo problema aproximado, seria necesario demostrar que el conjunto de todos los vértices inciden a las aristas de un ajuste máximas M para un grafo G es un recubrimiento con no mas de dos veces el numero de veces el recubrimiento de tamaño mínimo. Esto es evidente, ya que por la definición de ajuste máximas, los vértices incidentes a las aristas de M son un recubrimiento de G. también por la propia definición, ningún vértice perteneciente a M puede recubrir a mas de una arista en M. En consecuencia, por lo menos la mitad de los vértices de M deben pertenecer a un recubrimiento.

Algoritmos de búsqueda y ordenación

En muchas situaciones de programación es necesario repetir ciertos procedimientos hasta alcanzar un punto en que no se puede o no se desea continuar. Esta repetición de tareas puede llevarse a cabo básicamente de dos maneras diferentes: la iteración y la recursión. Como se supone que en cada repetición se procesa un estado diferente de cosas -sin lo cual el proceso tendería al infinito-, ambos métodos presentan también alguna forma de control de fin de tarea. La idea básica en un algoritmo iterativo es que la repetición de la tarea se controla desde afuera. Se ejecuta un conjunto de acciones en forma completa, se verifica la condición de salida y si es necesario se vuelve a ejecutar el conjunto de acciones en forma completa. El orden en que se ejecuta y evalúa determina que el algoritmo sea de evaluación previa (primero se evalúa la condición de salida y luego se ejecutan las acciones) o de evaluación posterior (primero se ejecutan las acciones y luego se evalúa el resultado). En ambos casos, sin embargo, el control de las repeticiones es exterior al grupo principal de acciones. En un algoritmo recursivo, en cambio, la tarea se controla desde adentro. Se comienza a ejecutar un conjunto de acciones, pero antes de finalizar se evalúa si se ha llegado a la condición de salida; si no es así, se continúa ordenando una nueva ejecución del mismo conjunto de acciones. Finalmente se concluye con la tarea iniciada. Dicho en otros términos, el procedimiento se llama repetidas veces a sí mismo, y el control de esas llamadas forma parte del grupo principal de acciones. Por otra parte, si bien hay problemas que se resuelven más directamente en forma iterativa y otros que son más naturalmente recursivos, ambas técnicas son compatibles e intercambiables, por lo que todo algoritmo recursivo puede transformarse en iterativo y viceversa.

Page 28: Estructura de Datos

Estructura de Datos UCENM 28

Algoritmos De Búsqueda

Cuando se trata de buscar un valor en un arreglo ordenado de datos, el algoritmo de búsqueda binaria es el más frecuentemente utilizado. La idea central de este algoritmo es comparar el elemento ubicado en el lugar central del arreglo con el valor buscado. Si el elemento central es igual al valor buscado la búsqueda finaliza con éxito. Si no es así, puede ocurrir o bien que el elemento central sea mayor que el buscado en cuyo caso el elemento coincidente debe estar en la mitad inferior del arreglo- o bien que sea menor y el elemento coincidente se encuentra en la mitad superior. En ambos casos se prosigue la búsqueda en la mitad que corresponde, si es que quedan elementos en esa dirección, o bien se finaliza la búsqueda sin éxito, en caso contrario. Existe naturalmente una solución recursiva, ya que si el valor buscado no es hallado en la posición del centro se repite el mismo procedimiento con una de las mitades del arreglo, y así hasta que se encuentre el valor o no queden más "mitades". Compárese esto con el problema de las bolillas dentro de las cajas, en el cual la "bolilla blanca" sería el valor buscado y la "caja interior" sería la mitad que se debe seguir examinando. En ocasiones es necesario determinar no sólo si el valor buscado se halla en el arreglo, sino además saber en qué posición está (o debería estar, si es que no existe). Por ejemplo, si se desea insertar un nuevo valor en un arreglo ordenado, una solución eficaz es "buscar" el valor en el arreglo (aunque se sepa de antemano que no se encuentra) para determinar la posición correcta en la que debe ser insertado. En esos casos se debe informar por algún medio (generalmente un puntero pasado como parámetro o una variable global) cuál es la posición lógica del elemento buscado dentro del arreglo.

Ejemplo de un Algoritmo de Búsqueda

A modo de ejemplo se presenta una versión de la función int busbin (int *vec, unsigned tam, int val, unsigned *ord); Ésta realiza una búsqueda binaria del elemento val en el vector de enteros apuntado por vec de tam elementos y deja en la memoria apuntada por ord la posición lógica que tiene (o debería tener) el elemento buscado. Retorna 1 si el elemento es hallado o 0 en caso contrario. Para calcular el elemento mitad del vector se desplaza tam un bit a la derecha, lo que equivale a dividirlo en dos, pero resulta mucho más rápido para el procesador que la operación de división. int busbin (int *vec, unsigned tam, int val, unsigned *ord) {if (!(vec && tam && ord)) return 0; unsigned mitad = tam >> 1; // Divide tam en 2 desplazando un bit a la// derecha. Es más rápido que tam / 2. if (vec [mitad] == valor) { *ord += mitad; return 1; } if (vec [mitad] < valor) { mitad++; *ord += mitad; vec += mitad; tam -= mitad; } else tam = mitad;

Page 29: Estructura de Datos

Estructura de Datos UCENM 29

return tam? busbin (vec, tam, va, ord): 0;}

Algoritmo de Quick Sort La idea central de este algoritmo es la siguiente: si se toma un conjunto de elementos desordenados y se ubica uno cualquiera de ellos -llamado pivote- en una posición tal que todos los que estén antes sean menores o iguales y todos los que estén después sean mayores, entonces esa posición particular sería la correcta para ese elemento si el conjunto estuviera ordenado. Asimismo se verifica que el conjunto ha quedado dividido en dos partes: en la primera están todos los elementos menores o iguales al pivote y en la segunda todos los mayores. Esto permite aplicar recursivamente el mismo procedimiento en ambas partes del conjunto hasta que éste quede completamente ordenado. La tarea de dividir el conjunto de datos en dos grupos en torno al pivote es accesoria y puede ser realizada por una función auxiliar. Esta función auxiliar se puede implementar de varias maneras, pero todas ellas deben informar de algún modo en qué posición ha quedado ubicado el pivote. Al igual que busbin (), la función qsort () es recursiva, y cada llamada provocará que la función se llame "internamente" a sí misma varias veces. Los valores de los parámetros vec y tam seguramente variarán entre una llamada y otra, ya que son los que definen la parte del arreglo que se ordenará cada vez. Sin embargo, tanto los controles de que los parámetros sean legales como el retorno de la función con la dirección original del vector deben hacerse sólo una vez. Para evitar tener que hacer estos controles redundantes en cada llamada recursiva, la tarea se divide entre dos funciones. La primera está declarada públicamente como int *qsort (int *vec, unsigned tam); No es recursiva y sólo realiza las tareas que deben ser ejecutadas una sola vez: verifica que vec y tam sean legales y llama a la segunda función, que es la que completa la tarea. Si vec o tam son ilegales la función retorna vec sin hacer nada más. La segunda función, definida como void _qsort (int *vec, unsigned tam);

Es estática (por lo tanto inaccesible desde fuera del módulo), y es la que realmente lleva a cabo la tarea recursiva de ordenamiento. Recibe los parámetros vec y tam, que varían entre una llamada y otra, pero que tienen siempre valores legales.

Page 30: Estructura de Datos

Estructura de Datos UCENM 30

A continuación se presentan dos pequeños módulos de librería con una versión optimizada de la función: El módulo qsort.h contiene simplemente la declaración, El módulo qsort.cpp incluye la implementación de la función principal int *qsort (int *vec, unsigned tam); y una implementación de las dos funciones auxiliares int dividir (int *vec, unsigned tam); void _qsort (int *vec, unsigned tam); La función qsort () verifica la validez de los parámetros vec y tam y llama a _qsort () antes de retornar vec. La función _qsort () ordena por el algoritmo de quick sort el vector de enteros apuntado por vec de tamaño tam, llamando a su vez a la función dividir (). Ésta divide los tam elementos del vector apuntado por vec en dos grupos alrededor de un pivote y retorna la cantidad de elementos del primer grupo -que incluye al pivote. Ambas funciones auxiliares son estáticas -esto es, de uso "privado" dentro del módulo donde están insertas- ya que la tarea que realizan es subsidiaria y dependiente de la función qsort ().

Verificación y derivación de programas

Conceptos Básicos

La definición del significado de un elemento del lenguaje se puede realizar de distintas formas, cada una de las cuales define una semántica diferente del lenguaje. En esta lección se van a introducir los conceptos más importantes de algunas de estas formas semánticas, y se van a tratar más extensamente los conceptos de corrección, verificación y prueba, ya mencionados en la lección.

Semántica Dependiendo del objetivo prioritario que se presenta cubrir al dar el significado de un lenguaje podemos encontrar diversas aproximaciones. Entre todas ellas, las más frecuentemente utilizadas son la semantica operacional, la semantica declarativa y la semítica axiomática. Veamos a continuación una peque son la semántica operacional, la

Page 31: Estructura de Datos

Estructura de Datos UCENM 31

semítica declarativa y la semítica axiomática. Veamos a continuación una pequeña introducción a cada una de estas aproximaciones.

Semántica operacional

La semántica operacional define el significado de un lenguaje de programación en términos de los cambios de estado que producen las instrucciones primitivas del lenguaje. Estos cambios no se reflejan directamente en la maquina real, sino en una maquina (virtual) abstracta asociada que sirve como instrumento de conexión con aquella. Expresado de otra forma, podemos decir que la semántica operacional define el significado de un programa en términos del efecto producido por la ejecución paso a paso del mismo, de tal modo que la especificación de las instrucciones del lenguaje mediante instrucciones primitivas de la maquina abstracta es, precisamente, la definición semántica del mismo. A pesar de la aparente simplicidad de este formalismo, este tipo de semántica no describe con igual claridad todo tipo de lenguaje de programación. El motivo es que el mecanismo que emplean los distintos lenguajes de programación para realizar un cómputo no siempre puede expresarse de una manera clara, comprensible y concisa.

Semántica denotacional

La semántica denotacional define unas aplicaciones (funciones) de valoración semántica que asignan a cada construcción denotada tal objeto matemático que modela su significado. Se dice que la construcción denota tal objeto o que este objeto es la denotación de dicha construcción. En otras palabras, la semántica denotacional indica que función matemática se obtiene a la salida ante unas entradas del programa, sin preocuparse de la ejecución paso a paso del programa. Existe una variante de esta semántica que es la semántica algebraica, en la que se utiliza conceptos algebraicos a la hora de modelar el significado de las construcciones. El primer paso a realizar en la definición de la semántica denotacional de un determinado lenguaje es el establecimiento de un dominio semantico al que pertenecerán los resultados obtenidos de la evaluación de las construcciones del lenguaje. Esta evaluación es proporcionada por un conjunto de funciones de significado cuyo dominio esta constituido por el conjunto de construcciones del lenguaje y cuyo rango (o imagen) viene dado por el dominio semantico. Este tipo de semántica dotan de significado a los elementos del lenguaje de una manera mas formal y abstracta, pero sin embargo también necesitan un mayor conocimiento de conceptos matemáticos, que no tienen por que ser básicos ni elementales.

Page 32: Estructura de Datos

Estructura de Datos UCENM 32

Recursividad Una función recursiva es una función que se define en términos de si misma., es decir, en su cuerpo aparece alguna aplicación suya. La recursividad es un mecanismo que se usa cuando hay que repetir cierto tratamiento o cálculo pero el número de repeticiones es variable. De hecho ya se han visto definiciones recursivas, el tipo suma permite definir tipos de datos recursivos, como el tipo de lista. La definición del tipo lista permite repetir la adición de elementos a una lista. el número de adicciones es variable y determina el número final de elementos de lista.

Semántica axiomática

La semántica axiomática asociada a cada construcción del lenguaje un axioma lógico que relaciona el estado del computo (valores que tienen las variables utilizadas) antes y después de ejecutar esta construcción. Existen distintas semánticas, axiomática, de la que la mas conocida es la introducida por el sistema formal de hoare y que es utilizada para definir el significado de lenguaje imperativos (por ejemplo, pascal). El método axiomático expresa la semántica de un lenguaje de programación asociado al lenguaje una teoría matemática o sistema formal que permita demostrar propiedades de los programas escritos en ese lenguaje. Esta aproximación formal contrasta con el método denotacional mostrando anteriormente, que asocia a cada construcción del lenguaje una denotación (significado) en un intento de encontrar un modelo matemático (colección abstracta de objetos matemáticos) del lenguaje. Un sistema formal puede entenderse como un metalenguaje que pose su propia sintaxis y semántica. Desde el punto de vista sintáctico, es necesario definir una gramática en la que se reflejen las construcciones validas del sistema formal, normalmente se suele emplear la sintaxis sde la lógica de predicado de primer orden ampliada com. expresiones aritméticas y de teoría de conjuntos. Una vez fijada la sintaxis a emplear, un sistema formal se define mediante un conjunto de axiomas (propiedades ciertas escritas en forma de sentencias lógicas) que expresen las propiedades de las construcciones básicas y un conjuntos de reglas de inferencias (forma de deducir una propiedades de otras) que permitan deducir las propiedades de construcciones mas complejas. Las definiciones de sistemas formales para lenguajes funcionales suele hacerse fundamentalmente para demostrar propiedades que tiene que ver con el tipo de una expresión y de las subexpresiones que la componen.

Page 33: Estructura de Datos

Estructura de Datos UCENM 33

Diseño De Algoritmos Recursivos

Las definiciones recursivas pueden resultar sorprendentes a primera vista.Incluso puede dar lugar a funciones que nunca terminen (inútiles como algoritmos), Sin embargo, usadas juiciosamente son un instrumento potentísimo de diseño de algoritmos. En primer lugar, hay que identificar el proceso repetitivo por realizar. En segundo lugar, hay que considerar que una función recursiva solo es útil si su evaluación termina.

Clasificación

Según el número de llamadas recursivas realizadas, se distinguen tres clases: Recursividad lineal: El cuerpo de la función contiene una llamada recursiva. Son las funciones recursivas. Son las funciones recursivas más sencillas. Todas las funciones especificadas en este capitulo son recursivas lineales. Un caso importante de recursividad lineal aparece cuando la expresión más externa del caso recursivo de la función es la misma llamada recursiva. Esta clase de recursividad se llama recursividad de cola.

Recursividad no lineal El cuerpo de la función contiene varias llamadas recursivas. Lo mas frecuente que contenga dos llamadas recursivas, en cuyo caso se habla de recursividad binaria.

Recursividad mutua Es un caso curioso de recurcion, una función no contiene ninguna llamada recursiva, pero durante la evaluación de su aplicación surgen llamadas recursivas. Auque a primera vista parezca imposible obtenérsete comportamiento, la recursividad puede conseguirse indirectamente si hay dos funciones que se llaman entre si. Otra clasificación basada en el formato de los parámetros de las llamadas recursivas.

Recursividad estructural Las sucesivas llamadas recursivas sobre un dato se hacen siguiendo la estructura recursiva del mismo. En el caso de los elementos enteros, una recurcion estructurar significa que el valor se vaya decrementando en uno.

Page 34: Estructura de Datos

Estructura de Datos UCENM 34

Recursión bien fundada Aunque hablando con propiedad, cualquier recursividad esta bien fundada, suele utilizarse este término para aquellas recursividades que no son estructurales. Un ejemplo de función recursiva bien es invertir Entero, donde el parámetro entero se divide entre diez. La primera clasificación dada es útil para decidir si es conveniente usar una definición recursiva o iterativa en los algoritmos imperactivo. La segunda clasificación es útil para decidir el principio de inducción necesaria para necesaria para verificar el algoritmo.

Diseño De Algoritmos Iterativos Cada clase de bucle resulta útil en situaciones diferentes. El bucle FOR se utiliza cuando se conoce el número de iteraciones que se quiere realizar. El bucle WHILE es usado en las otras situaciones en que no se conoce el número de iteraciones. El bucle LOOP es útil cuando puede haber varias condiciones de salida del bucle, situadas en diferentes partes del cuerpo, o cuando la condición de salida no esta al comienzo del bucle. Sin embargo, conocer la instrucción iterativa que mas conviene para un algoritmo es solo el comienzo de la construcción de la instrucción. Es conveniente tener algunas guías para el diseño de algoritmos iterativos. El diseño de un algoritmo iterativo se hace sobre una base distinta de la de uno recursivo. En los algoritmos imperativos se tiene disponible toda la información del problema en las variables. Por esta razón, es útil partir los datos del problema en dos:

La parte que representa el problema aun no resuelto.

La parte que representa el problema ya resuelto; es decir, el resultado ya calculado. Una vez que se han distinguido estas dos partes, el bucle se construye a partir de tres decisiones hechas sobre dicha partición:

Fijar la situación inicial. El problema a resolver se encuentra en los parámetros de entrada o de entrada/salida; si estos datos quieren modificarse durante el algoritmo, deben copiarse en variables auxiliares. también hay que dar un valor inicial a las variables que representan la solución vacía.

Formar el cuerpo del bucle. Para ello puede usarse un razonamiento parecido al recursivo. Se supone, por generalidad, que la iteración se encuentra en un estado intermedio. Debe entonces determinarse que hacer en una iteración para avanzar en la resolución del problema.

Determinar la condición de terminación. Corresponde a la situación en que se ha hallado la solución completa. Esta parte esta totalmente relacionada con la elección de la instrucción iterativa. Según la forma de la iteración, se elige un bucle FOR, WHILE o LOOP, como se describió antes.

Page 35: Estructura de Datos

Estructura de Datos UCENM 35

Normalmente, la terminación del bucle puede especificarse de forma sencilla, incluso en casos relativamente complicados. Por ejemplo, cuando se tiene un bucle WHILE con una condición compuesta es posible que pueda usarse la técnica del centinela para obtener una especificación más sencilla. Algo con lo que hay que tener especial cuidado es la terminación de los bucles. Esta es fácil de determinar en un algoritmo recursivo porque basta con tomar una medida de los parámetros y comprobar que disminuye en cada llamada recursiva, de forma que se acerca al tamaño de los casos básicos. En una solución iterativa se razona igual, pero la tarea es algo mas complicada porque el control del proceso repetitivo no tiene parámetros, sino que se hace a partir de la información contenida en las variables.

Page 36: Estructura de Datos

Estructura de Datos UCENM 36

Programación Dinámica

Principios de programación dinámica

Se ha visto que la técnica voraz se aplica a problemas cuya solución puede formularse como el resultado de una secuencia de decisiones. El método es eficiente porque una vez que se toma una decisión en un paso, no se reconsidera en el futuro, conduciendo de forma directa a la solución. Sin embargo, no todos los problemas pueden resolverse de esta manera, asegurando que la secuencia de decisiones es la mejor de las posibles. La programación dinámica (también llamada planificación dinámica) es una técnica de programación que también permite resolver problemas mediante una secuencia de decisiones, pero de una manera menos directa que en el caso voraz. Esta vez se necesita producir varias secuencias de decisiones. Solamente al final se sabe cuál es la mejor de todas. No es fácil establecer una definición de la programación dinámica; una característica es que el programa “aprende “dinámicamente de las decisiones que toma. Además, todo problema resoluble con esta técnica debe de satisfacer el principio de optimalidad. Este principio establece que “una secuencia óptima de decisiones que resuelve un problema debe cumplir la propiedad de que cualquier subsecuencia de decisiones también debe ser óptima respecto al subproblema que resuelva “. Usando una técnica de fuerza bruta, el número de secuencias de decisión es exponencial sobre el número de decisiones, porque si hay d opciones para cada una de las n decisiones, resultará un total de d secuencias posibles de decisión. En la programación dinámica todos los subproblemas se resuelven de acuerdo con criterio de tamaño creciente y los resultados de subproblemas más pequeños se almacenan en algún tipo de estructura de datos (normalmente tablas) para facilitar la solución de los problemas más grandes. De esta forma se reduce al número total de subsecuencias generadas, evitándose una explosión combinatoria en la producción de las secuencias y consiguiéndose soluciones más eficientes en cuanto a tiempo de ejecución. Podemos formalizar algo más la idea básica. Supongamos que tenemos un problema que satisface el principio de optimalidad, tal que Eo es el estado inicial del problema y deben tomarse n decisiones d, 1<i<n. sea D = { v1…..vn} el conjunto de valores de decisión posibles para la decisión d1. sea, asimismo, Eli el estado del problema tras la elección del valor vli 1<i<n1 y Sli una secuencia óptima de decisiones respecto al estao Eli. Entonces, una secuencia óptima de decisiones respecto a E0 es la mejor secuencias de decisión { Vli Sli }, 1<i<N1. El razonamiento anterior se refiere a la primera decisión d1 tomada desde el estado inicial E0 sin embargo, puede generalizarse la formulación del problema a cualquier subsecuencia de decisiones dk…..,dl, 1<k<n, partiendo como estado inicial de Ek-1. si este subproblema de simboliza como problema (k,l), entonces el problema completo es problema ( l,n ). Tiene sentido centrarse en un subproblema del problema inicial porque éste satisface el principio

Page 37: Estructura de Datos

Estructura de Datos UCENM 37

de optimalidad pero, además, tiene la ventaja ( quizás paradójica al tratar de un problema más pequeño ) de que proporciona una visión más general del problema en cuestión. ( Obsérvese que vamos a usar la técnica de resolución de problemas por generalización para después poder realizar una particularización de la solución obtenida.) Una solución dinámica para problema ( k,1 ) debe expresarse en términos de los valores de decisión existente para decisiones d1 y el subproblema problema ( k+1,1 ) resultante de aplicar cada valor de decisión. La expresión inicial de la ecuación de recurrencia, hay un caso en que la decisión d1 no va seguida por ninguna secuencia de decisiones, que es problema ( n,n ). En resumen, la aplicación de la técnica de programación dinámica a un problema significa comprobar primero el principio de optimalidad y desarrollar después unas ecuaciones recurrentes del estilo de (1) y (2). La ecuación general relaciona la secuencia óptima en una etapa i con la decisión tomada en la etapa i y la subsecuencia óptima en la etapa posterior i+1. la ecuación de base establece el valor para la etapa n+1 en que no queda ninguna decisión Xi, 1<i<n, a tomar. La ecuación de recurrencia puede formularse de dos formas: delantera o trasera. Sea X1…..Xn la secuencia de decisiones necesaria para resolver el problema. La formulación delantera expresa la decisión de Xl , 1<i<n, a partir de la secuencia de decisiones Xi+1……Xn ( es la clase de formulación adoptada hasta ahora ). La formulación trasera expresa la decisión de Xi, 1<i<n , a partir de la recurrentes con formulación trasera es igual que e la formulación delantera, sólo que en orden contrario. La elección de una formulación delantera o trasera depende del problema considerado o, sencillamente, del gusto del programador.

Page 38: Estructura de Datos

Estructura de Datos UCENM 38

Capitulo II

Tipos de Datos Abstractos

En el mundo de la programación existen diversos lenguajes que se han ido creando con el paso del tiempo y que se han perfeccionado debido a las necesidades de los programadores de la época a la que pertenecen. Los primeros lenguajes de programación eran de tipo lineales, ya que un programa se recorría desde un punto marcado como Inicio hasta llegar a un punto Fin. Con el tiempo se fueron creando nuevos lenguajes y en nuestros días los más utilizados son los llamados “Orientados a Objetos”.

Los Lenguajes Orientados a Objetos (LOO) tienen la característica de que no son lenguajes lineales, sino que se forman de diversas funciones, las cuales son llamadas en el orden en que el programa mismo las pide o el usuario determina. Para entender mejor cómo funcionan los Lenguajes Orientados a Objetos, vamos a introducir un concepto fundamental en las Estructuras de Datos denominado Abstracción de Datos y que es parte importante de estos Lenguajes y de la manera en que funciona la mayoría del software comercial de nuestros días.

Historia

El concepto de tipo de dato abstracto (TDA, Abstract Data Types ), fue propuesto por primera vez hacia 1974 por John Guttag y otros, pero no fue hasta 1975 que por primera vez Liskov lo propuso para el lenguaje CLU.

El lenguaje Turbo Pascal fue determinante para la común aceptación de los TDAs con la introducción de las Units. Si bien estas no cumplen con las características básicas de un Tipo de dato Abstracto, como por ejemplo: la encapsulación de los datos. El lenguaje ADA pudo implementar exitosamente los TDAs con sus Packages. Vale recordar que estos dos últimos lenguajes soportan formalmente la Programación modular.

Abstracción de datos

Un sistema de gestión de bases de datos es una colección de archivos interrelacionados y un conjunto de programas que permiten a los usuarios acceder y modificar esos archivos. Un objetivo importante de un SDBDD es proporcionar a los usuarios una visión abstracta de los datos. Es decir, el sistema esconde ciertos detalles de cómo se almacenan y mantienen los datos.

Page 39: Estructura de Datos

Estructura de Datos UCENM 39

Existen tres niveles de abstracción:

Nivel físico. El nivel más bajo de abstracción describe cómo se almacena realmente los datos. En el nivel físico, se describen en detalle las estructuras de datos complejas del nivel bajo.

Nivel conceptual. El siguiente nivel más alto de abstracción describe qué datos son realmente almacenados en la base de datos y las relaciones que existen entre los datos. Aquí se describen la base de datos completa en términos de un número pequeño de estructuras relativamente. Aunque la implementación de las estructuras sencillas del nivel conceptual puede implicar estructuras complejas del nivel físico, el usuario no necesita darse cuenta de esto. Este nivel es usado por los administradores de bases de datos, quienes deben decidir qué información se va a guardar en la base de datos.

Nivel de visión. El nivel más alto de abstracción describe sólo parte de la base de datos completa. A pesar del uso de estructuras más sencillas en el nivel conceptual, permanece algo de complejidad debido al gran tamaño de la base de datos. Muchos usuarios del sistema de bases de datos no se interesarán por toda la información. En cambio, dichos usuarios sólo necesitan una parte de la base de datos. Para simplificar su interacción con el sistema, se define el nivel de abstracción de visión. El sistema puede proporcionar muchas visiones de la misma base de datos.

¿Qué implica el concepto abstracción? El concepto abstracción implica concentrarse en las cualidades esenciales de un objeto. En la abstracción se suprimen los detalles irrelevantes y se aísla la esencia del objeto que se maneja. En la vida diaria, a menudo usamos este concepto aunque no nos demos cuenta de ello. Cada vez que usamos un automóvil, lo hacemos sin preocuparnos de la organización interna del vehículo ni de cómo trabajan los controles del mismo (guía, pedales, etc.). Sabemos lo que hacen y cómo utilizarlos pero probablemente desconocemos cómo estos controles realizan su tarea asignada. Otro ejemplo: aún cuando la mayoría de nosotros no sabemos de que está hecho un televisor ni sabemos al detalle como se genera la imagen en la pantalla, esto no evita que lo utilicemos todos los días confiados de que trabajará como es debido. Este concepto es fundamental en el estudio de las Ciencias de Computadoras ya que es la base para entender el diseño contemporáneo del software, en especial el diseño orientado a objetos (object-oriented design).

Como ya se mencionó, los Lenguajes de Programación Orientados a Objetos son lenguajes formados por diferentes métodos o funciones y que son llamados en el orden en que el programa lo requiere, o el usuario lo desea. La abstracción de datos consiste en ocultar las características de un objeto y obviarlas, de manera que solamente utilizamos el nombre del objeto en nuestro programa. Esto es similar a una situación de la vida cotidiana.otro ejemplo es cuando se dice la palabra “perro”, usted no necesita que se le diga lo que hace el perro.

Page 40: Estructura de Datos

Estructura de Datos UCENM 40

Usted ya sabe la forma que tiene un perro y también sabe que los perros ladran. De manera que se abstraen todas las características de todos los perros en un solo término, al cual llamo “perro”. A esto se le llama „Abstracción‟ y es un concepto muy útil en la programación, ya que un usuario no necesita mencionar todas las características y funciones de un objeto cada vez que éste se utiliza, sino que son declaradas por separado en el programa y simplemente se utiliza el término abstracto (“perro”) para mencionarlo.

En el ejemplo anterior, “perro” es un Tipo de Dato Abstracto y todo el proceso de definirlo, implementarlo y mencionarlo es a lo que llamamos Abstracción de Datos.

Vamos a poner un ejemplo real de la programación. Supongamos que en algún Lenguaje de Programación Orientado a Objetos un pequeño programa saca el área de un rectángulo de las dimensiones que un usuario decida. Pensemos también que el usuario probablemente quiera saber el área de varios rectángulos. Sería muy tedioso para el programador definir la multiplicación de „base‟ por „altura‟ varias veces en el programa, además que limitaría al usuario a sacar un número determinado de áreas. Por ello, el programador puede crear una función denominada „Área‟, la cual va a ser llamada el número de veces que sean necesitadas por el usuario y así el programador se evita mucho trabajo, el programa resulta más rápido, más eficiente y de menor longitud. Para lograr esto, se crea el método Área de una manera separada de la interfaz gráfica presentada al usuario y se estipula ahí la operación a realizar, devolviendo el valor de la multiplicación. En el método principal solamente se llama a la función Área y el programa hace el resto.

Al hecho de guardar todas las características y habilidades de un objeto por separado se le llama Encapsulamiento y es también un concepto importante para entender la estructuración de datos. Es frecuente que el Encapsulamiento sea usado como un sinónimo del Ocultamiento de Información, aunque algunos creen que no es así

Separación de la interfaz e implementación

Cuando se usa en un programa de computación, un TDA es representado por su interfaz, la cual sirve como cubierta a la correspondiente implementación. Los usuarios de un TDA tienen que preocuparse por la interfaz, pero no con la implementación, ya que esta puede cambiar en el tiempo y afectar a los programas que usan el TDA. Esto se basa en el concepto de Ocultamiento de Información, una protección para el programa de decisiones de diseño que son objeto de cambio.

La solidez de un TDA reposa en la idea de que la implementación está escondida al usuario. Solo la interfaz es pública. Esto significa que el TDA puede ser implementado de diferentes formas, pero mientras se mantenga consistente con la interfaz, los programas que lo usan no se ven afectados.

Hay una diferencia, aunque a veces sutil, entre el Tipo de Dato Abstracto y la [[Estructura de Dato] usada en su implementación. Por ejemplo, un TDA de una lista puede ser

Page 41: Estructura de Datos

Estructura de Datos UCENM 41

implementado mediante un Arreglo o una Lista Enlazada o hasta un Árbol binario de búsqueda. Una lista es un Tipo de Dato Abstracto con operaciones bien definidas (agregar elemento, agregar al final, agregar al principio, recuperar, eliminar, etc) mientras una Lista Enlazada es una estructura de datos basada en punteros o referencias (dependiendo del lenguaje) que puede ser usada para crear una representación de una Lista. La Lista Enlazada es comúnmente usada para representar una TDA Lista, y a veces, hasta confundida. Un ejemplo es la clase Linked List de Java, la cual ofrece una gran cantidad de métodos que no corresponden a una Lista Enlazada "pura", sino a un fuerte TDA.

De forma similar, un TDA Árbol binario de búsqueda puede ser representado de muchas maneras: Árbol binario, Árbol AVL, Árbol rojo-negro, Arreglo, etc. A pesar de la implementación un Árbol binario siempre tiene las mismas operaciones (insertar, eliminar, encontrar, etc.)

Separar la interfaz de la implementación no siempre significa que el usuario ignora totalmente la implementación de la rutina, pero lo suficiente para no depender de ningún aspecto de la implementación. Por ejemplo, un TDA puede ser creado usando un Lenguaje Script o cualquiera que pueda ser descompilado (como C).

En la terminología de Lenguaje Orientado a Objeto, un TDA es una clase; un instancia de un TDA o clase, es un objeto.

Caracterización

Un TDA está caracterizado por un conjunto de operaciones (funciones) al cual le denominaron usualmente como su interfaz pública y representan el comportamiento del TDA; mientras que la implementación como la parte privada del TDA está oculta al programa cliente que lo usa. Todos los lenguajes de alto nivel tienen predefinidos TDA; que son los tipos denominados simples y las estructuras predefinidas, y estos tienen sus interfaces públicas que incluyen las operaciones como la +, -, *, etc.

En un TDA no se necesita conocer como actúan tales operadores sobre la representación interna de los tipos definidos, que además, suele ser una implementación bastante dependiente de la máquina sobre la que trabaje el compilador. Lo interesante es que los lenguajes actuales nos van a permitir ampliar los TDA predefinidos con otros que serán definidos por el propio programador para adecuar así los tipos de datos a las necesidades de los programas.

Los TDA que nos van a interesar de ahora en adelante son aquellos que reflejen cierto comportamiento organizando cierta variedad de datos estructuradamente. A esta forma estructurada de almacenar los datos será a la que nos refiramos para caracterizar cada TDA.

Los TDA que tienen informaciones simples pero dependientes de un comportamiento estructural serán llamados polilíticos y aquellos TDA simples, como son los tipos predefinidos

Page 42: Estructura de Datos

Estructura de Datos UCENM 42

donde la información no es relacionada mediante ninguna estructura y no admiten más que un valor en cada momento serán denominados TDA monolíticos.

Nótese que cuando hablemos de un TDA no haremos ninguna alusión al tipo de los elementos sino tan sólo a la forma en que están dispuestos estos elementos. Sólo nos interesa la estructura que soporta la información y sus operaciones. Para determinar el comportamiento estructural basta con observar la conducta que seguirán los datos.

Caractericemos entonces los TDA. Un TDA tendrá una parte que será invisible al usuario la cual hay que proteger y que se puede decir que es irrelevante para el uso del usuario y está constituida tanto por la maquinaria algorítmica que implemente la semántica de las operaciones como por los datos que sirvan de enlace entre los elementos del TDA, es decir, información interna necesaria para la implementación que se esté haciendo para ese comportamiento del TDA. Resumiendo podemos decir, que tanto la implementación de las operaciones como los elementos internos del TDA serán privados al acceso externo y ocultos a cualquier otro nivel.

Un TDA representa una abstracción:

Se destacan los detalles (normalmente pocos) de la especificación (el qué). Se ocultan los detalles (casi siempre numerosos) de la implementación (el cómo).

Entonces la abstracción, es una de las herramientas que más nos ayuda a la hora de solucionar un problema, es un mecanismo fundamental para la comprensión de problemas y fenómenos que poseen una gran cantidad de detalles, su idea principal consiste en manejar un problema, fenómeno, objeto, tema o idea como un concepto general, sin considerar la gran cantidad de detalles que estos puedan tener. El proceso de abstracción presenta dos aspectos complementarios.

1. Destacar los aspectos relevantes del objeto. 2. Ignorar los aspectos irrelevantes del mismo (la irrelevancia depende del nivel de

abstracción, ya que si se pasa a niveles más concretos, es posible que ciertos aspectos pasen a ser relevantes).

De modo general podemos decir que la abstracción permite establecer un nivel jerárquico en el estudio de los fenómenos, el cual se establece por niveles sucesivos de detalles. Generalmente, se sigue un sentido descendente de detalles, desde los niveles más generales a los niveles más concretos.

Por ejemplo: los lenguajes de programación de alto nivel permiten al programador abstraerse del sin fin de detalles de los lenguajes ensambladores. Otro ejemplo, la memoria de la computadora es una estructura unidimensional formada por celdas y sin embargo trabajamos como si fuera única. La abstracción nos brinda la posibilidad de ir definiendo una serie de refinamientos sucesivos a nuestro TDA y entiéndase bien que cuando decimos refinamientos sucesivos nos estamos refiriendo a la estrategia que se utiliza para descomponer un

Page 43: Estructura de Datos

Estructura de Datos UCENM 43

problema en subproblemas. Conforme evoluciona el diseño de software a cada nivel de módulos se representa un refinamiento en el nivel de abstracción. Esto es, incluir detalles que fueron obviados en un nivel superior, en un nivel más bajo de la jerarquía.

Tipos de Abstracción que Podemos Encontrar en un Programa

Abstracción funcional:

Crear procedimientos y funciones e invocarlos mediante un nombre donde se destaca qué hace la función y se ignora cómo lo hace. El usuario sólo necesita conocer la especificación de la abstracción (el qué) y puede ignorar el resto de los detalles (el cómo).

Abstracción de datos:

Tipo de datos: proporcionado por los leguajes de alto nivel. La representación usada es invisible al programador, al cual solo se le permite ver las operaciones predefinidas para cada tipo.

Tipos definidos: por el programador que posibilitan la definición de valores de datos más cercanos al problema que se pretende resolver.

TDA: para la definición y representación de tipos de datos (valores + operaciones), junto con sus propiedades.

Objetos: Son TDA a los que se añade propiedades de reutilización y de compartición de código.

Si profundizamos más al mundo de la programación y sus conceptos, existen dos de estos conceptos que no se deben confundir, ellos son: tipo de datos y estructura de datos.

Un tipo de dato, en un lenguaje de programación, define un conjunto de valores que una determinada variable puede tomar, así como las operaciones básicas sobre dicho conjunto. Ahora veamos como se van relacionando estos conceptos. Los tipos de datos constituyen un primer nivel de abstracción, ya que no se tiene en cuenta cómo se implementan o se representan realmente la información sobre la memoria de la máquina. Para el usuario, el proceso de implementación o representación es invisible.

Veamos entonces que son las estructuras de datos. Las estructuras de datos son colecciones de variables, no necesariamente del mismo tipo, relacionadas entre sí de alguna forma. Las estructuras de datos están caracterizadas por el tipo de dato de los elementos guardados en la estructura y por la relación definida sobre estos elementos.

Page 44: Estructura de Datos

Estructura de Datos UCENM 44

Al nivel de las estructuras de datos son totalmente irrelevantes las operaciones sobre un elemento en particular, solamente tienen carácter relevante las operaciones que envuelvan la estructura de forma global.

Cuando un sistema se diseña modularmente, éste es dividido en subsistemas llamados módulos. Cada uno de esos módulos comienza como una "caja negra". A medida que se progresa en el diseño, cada una de esas "cajas negras" se refina hasta que se puede codificar usando un lenguaje de programación. El propósito de una "caja negra" es especificar qué hace el módulo pero no cómo lo hace. El propósito del módulo se mantiene separado del algoritmo que lo realiza. A este concepto se le conoce como abstracción de procesos. Esto permite que un programador pueda usar una rutina creada por otro programador conociendo solamente la tarea que realiza el módulo y la descripción de sus insumos y productos (sus parámetros), aún cuando desconozca los detalles del algoritmo implementado en el módulo. Un ejemplo de este concepto lo vemos en los algoritmos de ordenamiento (sorting) y en los de búsqueda (searching).

En un algoritmo de ordenamiento, los datos de una lista de artículos son ordenados ascendente o descendentemente de acuerdo a algún atributo. La lista original es recibida por el módulo, éste la ordena y la devuelve ordenada. Existen muchos tipos de algoritmos de ordenamiento pero todos realizan la misma función. Es posible sustituir en un programa una rutina que implemente un algoritmo particular, digamos el de la burbuja (bubble sort), por un algoritmo más eficiente (por ejemplo, el quick sort). Aún cuando el programador no entienda el algoritmo específico de ordenamiento, esto no evita que pueda invocar la rutina ya que sabe que ésta trabajará como es debido. En un algoritmo de búsqueda, un objeto es buscado en una lista de acuerdo a algún atributo. En caso de encontrar el objeto, el algoritmo devuelve un indicador de la posición en la que lo encontró. Aquí ocurre una situación semejante a la de los algoritmos de ordenamiento. Es posible sustituir en un programa una rutina que implemente el algoritmo de búsqueda secuencial (linear o sequential search) por el algoritmo de búsqueda binaria (binary search) con un mínimo de esfuerzo ya que ambos realizan la misma función y tienen los mismos parámetros.

Otro concepto importante es la abstracción de datos. Esto significa que la representación interna de un artículo es secundaria al concepto que representa. El programador debe poder usar estos artículos aún cuando no conozca su organización interna. Por ejemplo, un programador de C++ puede utilizar números enteros en su programa aún cuando desconozca cuál es la representación interna de ese entero (ya sea signo/magnitud, complemento a uno, complemento a dos, etc.). Lo importante es que reconozca qué artículo representa un número entero (en este caso, el artículo sería un número en base diez que no contenga punto decimal) y qué operaciones puede hacer con artículos de tipo entero (en este caso, las operaciones definidas para artículos de tipo entero son las operaciones aritméticas (+, -, *, /, %) y las operaciones de comparación o relacionales (<, >, <=, >=, = =, !=).

Los conceptos de abstracción de datos y abstracción de procesos son necesarios para entender lo que es un tipo de datos abstracto. Un tipo de datos abstracto (abstract data type

Page 45: Estructura de Datos

Estructura de Datos UCENM 45

o ADT, por sus siglas en inglés) se define como una colección de artículos que están definidos a base de sus atributos y aquellas operaciones que trabajan con esos atributos. Los atributos sólo deben ser accesibles mediante las operaciones definidas para ese tipo de datos abstracto. Los atributos en un ADT se implementan utilizando una estructura de datos. Las operaciones definidas para manipular los atributos se implementan utilizando rutinas de programación. El programador debe poder utilizar artículos que pertenezcan al ADT, pero sólo deberá poder manipular estos artículos utilizando las operaciones definidas para ese ADT. Un ejemplo de este concepto es el tipo de datos abstracto que describe una lista.

El ADT Lista reconoce las siguientes operaciones: crear la lista, revisar si la lista está vacía o no, revisar si la lista está llena o no, añadir un elemento, eliminar un elemento, buscar un elemento y recorrer la lista. El programador que desee utilizar una lista debe ser capaz de poder declarar variables del tipo Lista y poder manipular los elementos de la lista mediante las operaciones definidas para el ADT Lista. Es más: este programador no debería preocuparse de saber cómo es la estructura interna de la lista, ni cómo están implementadas las operaciones. En otras palabras, la estructura de datos que representa la lista y los algoritmos que implementan las operaciones no son imprescindibles para poder utilizar la lista. Solamente el programador que desee implementar una lista es el que debe preocuparse de la representación interna de la lista (la estructura de datos) y de los algoritmos específicos para manejar esa lista.

Cuando se describe un tipo de datos abstracto es necesario especificar tanto la estructura interna del ADT como sus operaciones asociadas. Es muy común que las operaciones en un ADT sean descritas utilizando precondiciones y postcondiciones. Una precondición es un enunciado que debe ser cierto para que la operación se pueda llevar a cabo. Una postcondición es un enunciado que debe ser cierto después de que la operación se lleve a cabo. Por ejemplo, la operación para revisar si la lista está vacía o no tiene como precondición que la lista exista. La postcondición de esta operación es que se devolvió el valor cierto si la lista está vacía o se devolvió el valor falso si la lista no está vacía (tiene por lo menos un elemento).

¿Cómo se implementa un ADT en un lenguaje de programación real? Existen dos enfoques principales:

1. El programador no se preocupa del concepto del ADT y simplemente hace su programa asumiendo que la estructura de datos seleccionada y los algoritmos correspondientes no cambiarán. Por ejemplo, el programador puede asumir que la lista está implementada como un arreglo y desarrolla las rutinas de su programa asumiendo que siempre trabajará con un arreglo. ¿Qué ocurre si se desea cambiar la estructura de datos de la lista para que en lugar de ser un arreglo sea otra cosa, por ejemplo una estructura dinámica que usa apuntadores? Este programador tendría que probablemente cambiar muchas de las rutinas de su programa.

Page 46: Estructura de Datos

Estructura de Datos UCENM 46

2. En el segundo enfoque, el programador separa la implementación del ADT del programa que usa el ADT. Para esto, el programador utiliza una clase para implementar el ADT. Una clase es un mecanismo que permite encapsular la estructura de datos de un objeto junto con las operaciones usadas para crear, acceder o para transformar los datos del objeto. La definición de una clase se compone de dos porciones: la porción pública y la porción privada, y se mantiene normalmente en un archivo aparte del código fuente de la aplicación que use la clase. La porción pública puede ser accesada por el programador de una aplicación que desee solicitar los servicios de la clase. Esta incluye los prototipos de las operaciones especificadas para el ADT. La porción privada sólo la puede acceder los programadores que implementan la clase. Esta incluye la estructura de datos del ADT y las instrucciones que implementan las operaciones. Un programador de aplicaciones que utiliza los servicios de una clase no puede acceder a la estructura de datos que define la clase si no es por medio de llamadas a las operaciones definidas en la porción pública ya que el compilador no se lo permitirá. Este es un enfoque que permite un manejo más eficiente y mejor controlado de la complejidad de los programas ya que fomenta tanto la abstracción de procesos como la abstracción de datos.

Una clase permite definir un nuevo tipo de datos y cualquier variable que se declare usando esta definición es conocida como un objeto. Esta es la esencia de la programación orientada a objetos: un programa se compone de objetos que interactúan entre sí mediante el uso de las operaciones públicas (los métodos) de esos objetos. El proceso de diseñar clases promueve la abstracción de procesos y la abstracción de datos. Inclusive, se pueden diseñar clases que sean subtipos de clases más fundamentales. Los objetos de estas clases derivadas heredarán los atributos y las operaciones definidas en la clase base, además de que podrán contener atributos y operaciones propias. Es importante recalcar que los lenguajes de programación más populares hoy en día (C++, Java, Visual Basic, Delphi, C#, etc.) son todos lenguajes que permiten la programación orientada a objetos. Por lo tanto, es un concepto del cual conviene estar familiarizado.

Algunos ejemplos de utilización de TDAs en programación son:

Conjuntos: Implementación de conjuntos con sus operaciones básicas (unión, intersección y diferencia), operaciones de inserción, borrado, búsqueda...

Árboles Binarios de Búsqueda: Implementación de árboles de elementos, utilizados para la representación interna de datos complejos.

Pilas y Colas: Implementación de los algoritmos FIFO y LIFO. Grafos: Implementación de grafos; una serie de vértices unidos mediante una serie de

arcos o aristas.

Page 47: Estructura de Datos

Estructura de Datos UCENM 47

Capitulo III

Árboles

Un árbol es una estructura de datos ampliamente usada que emula la forma de un árbol (un conjunto de nodos conectados). Un nodo es la unidad sobre la que se construye el árbol y puede tener cero o más nodos hijos conectados a él. Se dice que un nodo a es padre de un nodo b si existe un enlace desde a hasta b (en ese caso, también decimos que b es hijo de a). Sólo puede haber un único nodo sin padres, que llamaremos raíz. Un nodo que no tiene hijos se conoce como hoja. Los demás nodos (tienen padre y uno o varios hijos) se les conoce como rama.

También podemos definirlo como:

Estructura no lineal y dinámica de datos. Dinámica: puede cambiar durante la ejecución de un programa. No lineal: a cada elemento del árbol pueden seguirle varios elementos. Están formados por un conjunto de nodos y un conjunto de aristas que conectan pares de nodos.

Page 48: Estructura de Datos

Estructura de Datos UCENM 48

Definición no recursiva

Conjunto de nodos y conjunto de aristas que conectan pares de nodos con las siguientes características:

Se distingue un nodo raíz (no tiene padre). A cada nodo c (excepto la raíz) le llega una arista desde exactamente un nodo p

diferente a c, al cual se le llama padre de c. Hay un único camino desde la raíz hasta cada nodo. La longitud del camino es su

número de aristas.

Definición recursiva

Un árbol es o bien vacío o consiste en una raíz y cero o más subárboles no vacíos T1, T2,…, Tn, cada una de cuyas raíces está conectada por medio de una arista con la raíz.

Definiciones

Los nodos que no tienen hijos se denominan hojas. Un árbol con N nodos debe tener (N-1) aristas. La profundidad de la raíz es 0 y la de cualquier nodo es la de su padre más 1. La altura de un nodo es 1 más que la mayor altura de un hijo suyo. La altura de un

árbol es la altura de la raíz. Los nodos que tienen el mismo padre son hermanos. Si hay un camino del nodo u al nodo v, u es ascendiente de v y v es descendiente de

u. Si u v son propios. El tamaño de un nodo es el número de descendientes (incluido él mismo). El tamaño

de un árbol es el tamaño de su raíz.

Formas de representación

Mediante un grafo:

Page 49: Estructura de Datos

Estructura de Datos UCENM 49

Figura 1

Ejemplo de árbol (binario).

Mediante un diagrama encolumnado: a b d c e f

En la computación se utiliza mucho una estructura de datos, que son los árboles binarios. Estos árboles tienen 0, 1 ó 2 descendientes como máximo.

Nomenclatura sobre árboles

Raíz: es aquel elemento que no tiene antecesor.

Rama: arista entre dos nodos.

Antecesor: un nodo X es antecesor de un nodo Y si por alguna de las ramas de X se puede llegar a Y.

Sucesor: un nodo X es sucesor de un nodo Y si por alguna de las ramas de Y se puede llegar a X.

Grado de un nodo: el número de descendientes directos que tiene. Ejemplo: c tiene grado 2, d tiene grado 0, a tiene grado 2.

Hoja: nodo que no tiene descendientes: grado 0. Ejemplo: d

Nodo interno: aquel que tiene al menos un descendiente.

Nivel: número de ramas que hay que recorrer para llegar de la raíz a un nodo.

Page 50: Estructura de Datos

Estructura de Datos UCENM 50

Altura: el nivel más alto del árbol.

Anchura: es el mayor valor del número de nodos que hay en un nivel.

Grado de árbol: Cantidad máxima de hijos posibles de asociar a un nodo del árbol

Clasificación

Según Número de Hijos

Page 51: Estructura de Datos

Estructura de Datos UCENM 51

Según Estructura de Niveles

Árbol completo: Es un árbol binario en el cual cada nodo es una hoja o posee exactamente 2 hijos.

Árbol lleno: Es un árbol binario con hojas en a lo más dos niveles adyacentes l-1 y l, en las cuales los nodos terminales se encuentran ubicados en las posiciones de más a la izquierda del árbol.

Si un árbol binario es completo, necesariamente es lleno

Formalmente, podemos definir un árbol de la siguiente forma:

Caso base: un árbol con sólo un nodo (es a la vez raíz del árbol y hoja).

Un nuevo árbol a partir de un nodo nr y k árboles de raíces

con elementos cada uno, puede construirse estableciendo una relación padre-hijo entre nr y cada una de las raíces de los k

árboles. El árbol resultante de nodos tiene como raíz el

nodo nr, los nodos son los hijos de nr y el conjunto de nodos hoja está formado por la unión de los k conjuntos hojas iniciales. A cada uno de los árboles Ai se les denota ahora subárboles de la raíz.

Tipos de árboles

Árboles Binarios Árbol de búsqueda binario auto-balanceable Árboles B Árboles Multicamino

Page 52: Estructura de Datos

Estructura de Datos UCENM 52

Operaciones de árboles. Representación

Las operaciones comunes en árboles son:

Enumerar todos los elementos. Buscar un elemento. Dado un nodo, listar los hijos (si los hay). Borrar un elemento. Eliminar un subárbol (algunas veces llamada podar). Añadir un subárbol (algunas veces llamada injertar). Encontrar la raíz de cualquier nodo.

Por su parte, la representación puede realizarse de diferentes formas. Las más utilizadas son:

Representar cada nodo como una variable en el heap, con punteros a sus hijos y a su padre.

Representar el árbol con un array donde cada elemento es un nodo y las relaciones padre-hijo vienen dadas por la posición del nodo en el array.

Uso de los árboles

Usos comunes de los árboles son:

Representación de datos jerárquicos. Como ayuda para realizar búsquedas en conjuntos de datos Partición binaria del espacio

Implementación primer hijo - siguiente hermano

Consiste en mantener los hijos de cada nodo en una lista enlazada. Cada nodo tiene dos referencias: una a su hijo más a la izquierda y otra a su hermano de la derecha.

Eliminar nodos en un árbol:

El proceso general es muy sencillo en este caso, pero con una importante limitación, sólo podemos borrar nodos hoja:

El proceso sería el siguiente:

Page 53: Estructura de Datos

Estructura de Datos UCENM 53

1. Buscar el nodo padre del que queremos eliminar. 2. Buscar el puntero del nodo padre que apunta al nodo que queremos borrar. 3. Liberar el nodo. 4. padre->nodo[i] = NULL;.

Cuando el nodo a borrar no sea un nodo hoja, diremos que hacemos una "poda", y en ese caso eliminaremos el árbol cuya raíz es el nodo a borrar. Se trata de un procedimiento recursivo, aplicamos el recorrido PostOrden, y el proceso será borrar el nodo.

El procedimiento es similar al de borrado de un nodo:

1. Buscar el nodo padre del que queremos eliminar. 2. Buscar el puntero del nodo padre que apunta al nodo que queremos borrar. 3. Podar el árbol cuyo padre es nodo. 4. padre->nodo[i] = NULL;.

En el árbol del ejemplo, para podar la rama 'B', recorreremos el subárbol 'B' en postorden, eliminando cada nodo cuando se procese, de este modo no perdemos los punteros a las ramas apuntadas por cada nodo, ya que esas ramas se borrarán antes de eliminar el nodo.

De modo que el orden en que se borrarán los nodos será:

K E F y B

Árboles ordenados:

A partir del siguiente capítulo sólo hablaremos de árboles ordenados, ya que son los que tienen más interés desde el punto de vista de TAD, y los que tienen más aplicaciones genéricas.

Un árbol ordenado, en general, es aquel a partir del cual se puede obtener una secuencia ordenada siguiendo uno de los recorridos posibles del árbol: inorden, preorden o postorden.

En estos árboles es importante que la secuencia se mantenga ordenada aunque se añadan o se eliminen nodos.

Existen varios tipos de árboles ordenados:

árboles binarios de búsqueda (ABB):

son árboles de orden 2 que mantienen una secuencia ordenada si se recorren en inorden.

árboles AVL:

Page 54: Estructura de Datos

Estructura de Datos UCENM 54

son árboles binarios de búsqueda equilibrados, es decir, los niveles de cada rama para cualquier nodo no difieren en más de 1.

árboles perfectamente equilibrados:

son árboles binarios de búsqueda en los que el número de nodos de cada rama para cualquier nodo no difieren en más de 1. Son por lo tanto árboles AVL también.

árboles 2-3:

son árboles de orden 3, que contienen dos claves en cada nodo y que están también equilibrados. También generan secuencias ordenadas al recorrerlos en inorden.

árboles-B:

caso general de árboles 2-3, que para un orden M, contienen M-1 claves.

Movimientos a través del árbol

No hay mucho que contar. Nuestra estructura se referenciará siempre mediante un puntero al nodo Raiz, este puntero no debe perderse nunca.

Para movernos a través del árbol usaremos punteros auxiliares, de modo que desde cualquier puntero los movimientos posibles serán: moverse al nodo raíz de la rama izquierda, moverse al nodo raíz de la rama derecha o moverse al nodo Raiz del árbol.

Información

Hay varios parámetros que podemos calcular o medir dentro de un árbol. Algunos de ellos nos darán idea de lo eficientemente que está organizado o el modo en que funciona.

Comprobar si un árbol está vacío.

Un árbol está vacío si su raíz es NULL.

Calcular el número de nodos.

Tenemos dos opciones para hacer esto, una es llevar siempre la cuenta de nodos en el árbol al mismo tiempo que se añaden o eliminan elementos. La otra es, sencillamente, contarlos.

Para contar los nodos podemos recurrir a cualquiera de los tres modos de recorrer el árbol: inorden, preorden o postorden, como acción sencillamente incrementamos el contador.

Page 55: Estructura de Datos

Estructura de Datos UCENM 55

Comprobar si el nodo es hoja.

Esto es muy sencillo, basta con comprobar si tanto el árbol izquierdo como el derecho están vacíos. Si ambos lo están, se trata de un nodo hoja.

Calcular la altura de un nodo.

No hay un modo directo de hacer esto, ya que no nos es posible recorrer el árbol en la dirección de la raíz. De modo que tendremos que recurrir a otra técnica para calcular la altura.

Lo que haremos es buscar el elemento del nodo de que queremos averiguar la altura. Cada vez que avancemos un nodo incrementamos la variable que contendrá la altura del nodo.

Empezamos con el nodo raíz apuntando a Raiz, y la 'Altura' igual a cero. Si el valor del nodo raíz es igual que el del elemento que buscamos, terminamos la

búsqueda y el valor de la altura es 'Altura'. Incrementamos 'Altura'. Si el valor del nodo raíz es mayor que el elemento que buscamos, continuaremos la

búsqueda en el árbol izquierdo. Si el valor del nodo raíz es menor que el elemento que buscamos, continuaremos la

búsqueda en el árbol derecho.

Calcular la altura de un árbol.

La altura del árbol es la altura del nodo de mayor altura. Para buscar este valor tendremos que recorrer todo el árbol, de nuevo es indiferente el tipo de recorrido que hagamos, cada vez que cambiemos de nivel incrementamos la variable que contiene la altura del nodo actual, cuando lleguemos a un nodo hoja compararemos su altura con la variable que contiene la altura del árbol si es mayor, actualizamos la altura del árbol.

Iniciamos un recorrido del árbol en postorden, con la variable de altura igual a cero. Cada vez que empecemos a recorrer una nueva rama, incrementamos la altura para

ese nodo. Después de procesar las dos ramas, verificamos si la altura del nodo es mayor que la

variable que almacena la altura actual del árbol, si es así, actualizamos esa variable.

Page 56: Estructura de Datos

Estructura de Datos UCENM 56

Árboles degenerados

Los árboles binarios de búsqueda tienen un gran inconveniente. Por ejemplo, supongamos que creamos un ABB a partir de una lista de valores ordenada:

2, 4, 5, 8, 9, 12

Difícilmente podremos llamar a la estructura resultante un árbol:

Esto es lo que llamamos un árbol binario de búsqueda degenerado, y en el siguiente capítulo veremos una nueva estructura, el árbol AVL, que resuelve este problema, generando árboles de búsqueda equilibrados.

Árboles binarios

Un árbol binario es o bien vacío o consta de una raíz, un hijo árbol binario izquierdo y otro derecho. Los árboles binarios de búsqueda permiten inserciones y acceso a los elementos en tiempo logarítmico. Los árboles binarios llamados colas con prioridad soportan acceso y eliminación del mínimo de una colección de elementos.

Recorridos sobre árboles binarios

Se consideran dos tipos de recorrido: recorrido en profundidad y recorrido en anchura o a nivel. Puesto que los árboles no son secuenciales como las listas, hay que buscar estrategias alternativas para visitar todos los nodos.

Page 57: Estructura de Datos

Estructura de Datos UCENM 57

Recorridos en profundidad:

Recorrido en preorden: consiste en visitar el nodo actual (visitar puede ser simplemente mostrar la clave del nodo por pantalla), y después visitar el subárbol izquierdo y una vez visitado, visitar el subárbol derecho. Es un proceso recursivo por naturaleza. Si se hace el recorrido en preorden del árbol de la figura 1 las visitas serían en el orden siguiente: a,b,d,c,e,f.

void preorden(tarbol *a) { if (a != NULL) { visitar(a); preorden(a->izq); preorden(a->der); } }

Recorrido en in-orden u orden central:

se visita el subárbol izquierdo, el nodo actual, y después se visita el subárbol derecho. En el ejemplo de la figura 1 las visitas serían en este orden: b,d,a,e,c,f.

void inorden(tarbol *a) { if (a != NULL) { inorden(a->izq); visitar(a); inorden(a->der); } }

Recorrido en post-orden:

se visitan primero el subárbol izquierdo, después el subárbol derecho, y por último el nodo actual. En el ejemplo de la figura 1 el recorrido quedaría así: d,b,e,f,c,a.

void postorden(arbol *a) { if (a != NULL) { postorden(a->izq); postorden(a->der); visitar(a); } }

Page 58: Estructura de Datos

Estructura de Datos UCENM 58

La ventaja del recorrido en postorden es que permite borrar el árbol de forma consistente. Es decir, si visitar se traduce por borrar el nodo actual, al ejecutar este recorrido se borrará el árbol o subárbol que se pasa como parámetro. La razón para hacer esto es que no se debe borrar un nodo y después sus subárboles, porque al borrarlo se pueden perder los enlaces, y aunque no se perdieran se rompe con la regla de manipular una estructura de datos inexistente. Una alternativa es utilizar una variable auxiliar, pero es innecesario aplicando este recorrido.

Recorrido en amplitud:

Consiste en ir visitando el árbol por niveles. Primero se visitan los nodos de nivel 1 (como mucho hay uno, la raíz), después los nodos de nivel 2, así hasta que ya no queden más. Si se hace el recorrido en amplitud del árbol de la figura una visitaría los nodos en este orden:.a,b,c,d,e,f En este caso el recorrido no se realizará de forma recursiva sino iterativa, utilizando una cola como estructura de datos auxiliar. El procedimiento consiste en encolar (si no están vacíos) los subárboles izquierdo y derecho del nodo extraído de la cola, y seguir desencolando y encolando.hasta.que.la.cola.esté.vacía. En la codificación que viene a continuación no se implementan las operaciones sobre colas.

void amplitud(tarbol *a) { tCola cola; /* las claves de la cola serán de tipo árbol binario */ arbol *aux; if (a != NULL) { CrearCola(cola); encolar(cola, a); while (!colavacia(cola)) { desencolar(cola, aux); visitar(aux); if (aux->izq != NULL) encolar(cola, aux->izq); if (aux->der != NULL) encolar(cola, aux->der); } } }

Por último, considérese la sustitución de la cola por una pila en el recorrido en amplitud. ¿Qué tipo de recorrido se obtiene?

Finalmente, puede decirse que esta estructura es una representación del concepto de árbol en teoría de grafos. Un árbol es un grafo conexo y acíclico

Page 59: Estructura de Datos

Estructura de Datos UCENM 59

Construcción de un árbol binario

Partiendo de los recorridos preorden e inorden del árbol de la figura 1 puede determinarse que la raíz es el primer elemento del recorrido en preorden. Ese elemento se busca en el array inorden. Los elementos en el array inorden entre izq y la raíz forman el subárbol izquierdo. Asimismo los elementos entre der y la raíz forman el subárbol derecho. Por tanto se tiene este árbol:

A continuación comienza un proceso recursivo. Se procede a crear el subárbol izquierdo, cuyo tamaño está limitado por los índices izq y der. La siguiente posición en el recorrido en preorden es la raíz de este subárbol. Queda esto:

El subárbol b tiene un subárbol derecho, que no tiene ningún descendiente, tal y como indican los índices izq y der. Se ha obtenido el subárbol izquierdo completo de la raíz a, puesto que b no tiene subárbol izquierdo:

Page 60: Estructura de Datos

Estructura de Datos UCENM 60

Después seguirá construyéndose el subárbol derecho a partir de la raíz a.

La implementación de la construcción de un árbol partiendo de los recorridos en preorden y en inorden puede consultarse aquí (en C).

Declaración de árbol binario

Se definirá el árbol con una clave de tipo entero (puede ser cualquier otra tipo de datos) y dos hijos: izquierdo (izq) y derecho (der). Para representar los enlaces con los hijos se utilizan punteros. El árbol vacío se representará con un puntero nulo.

Un árbol binario puede declararse de la siguiente manera:

typedef struct tarbol { int clave; struct tarbol *izq,*der; } tarbol;

Árboles binarios de búsqueda

Un árbol binario de búsqueda es aquel que es:

Una estructura vacía o

Page 61: Estructura de Datos

Estructura de Datos UCENM 61

Un elemento o clave de información (nodo) más un número finito -a lo sumo dos- de estructuras tipo árbol, disjuntos, llamados subárboles y además cumplen lo siguiente:

o Todas las claves del subárbol izquierdo al nodo son menores que la clave del nodo.

o Todas las claves del subárbol derecho al nodo son mayores que la clave del nodo.

o Ambos subárboles son árboles binarios de búsqueda.

Un ejemplo de árbol binario de búsqueda:

Figura 2

Al definir el tipo de datos que representa la clave de un nodo dentro de un árbol binario de búsqueda es necesario que en dicho tipo se pueda establecer una relación de orden. Por ejemplo, suponer que el tipo de datos de la clave es un puntero (da igual a lo que apunte). Si se codifica el árbol en Pascal no se puede establecer una relación de orden para las claves, puesto que Pascal no admite determinar si un puntero es mayor o menor que otro.

En el ejemplo de la Figura 2 las claves son números enteros. Dada la raíz 4, las claves del subárbol izquierdo son menores que 4, y las claves del subárbol derecho son mayores que 4. Esto se cumple también para todos los subárboles. Si se hace el recorrido de este árbol en orden central se obtiene una lista de los números ordenada de menor a mayor.

Una ventaja fundamental de los árboles de búsqueda es que son en general mucho más rápidos para localizar un elemento que una lista enlazada. Por tanto, son más rápidos para insertar y borrar elementos. Si el árbol está perfectamente equilibrado (esto es, la diferencia entre el número de nodos del subárbol izquierdo y el número de nodos del subárbol derecho es a lo sumo 1, para todos los nodos) entonces el número de comparaciones necesarias para localizar una clave es aproximadamente de logN en el peor caso. Además, el algoritmo de inserción en un árbol binario de búsqueda tiene la ventaja sobre los arrays ordenados, donde se emplearía búsqueda dicotómica para localizar un elemento- de que no necesita hacer una reubicación de los elementos de la estructura para que esta siga ordenada

Page 62: Estructura de Datos

Estructura de Datos UCENM 62

después de la inserción. Dicho algoritmo funciona avanzando por el árbol escogiendo la rama izquierda o derecha en función de la clave que se inserta y la clave del nodo actual, hasta encontrar su ubicación; por ejemplo, insertar la clave 7 en el árbol de la Figura 2 requiere avanzar por el árbol hasta llegar a la clave 8, e introducir la nueva clave en el subárbol izquierdo.a.8.

El algoritmo de borrado en árboles es algo más complejo, pero más eficiente que el de borrado en un array ordenado.

Operaciones básicas sobre árboles binarios de búsqueda

Búsqueda

Si el árbol no es de búsqueda, es necesario emplear uno de los recorridos anteriores sobre el árbol para localizarlo. El resultado es idéntico al de una búsqueda secuencial. Aprovechando las propiedades del árbol de búsqueda se puede acelerar la localización. Simplemente hay que descender a lo largo del árbol a izquierda o derecha dependiendo del elemento que se busca.

boolean buscar(tarbol *a, int elem) { if (a == NULL) return FALSE; else if (a->clave < elem) return buscar(a->der, elem); else if (a->clave > elem) return buscar(a->izq, elem); else return TRUE; }

Inserción

La inserción tampoco es complicada. Es más, resulta prácticamente idéntica a la búsqueda. Cuando se llega a un árbol vacío se crea el nodo en el puntero que se pasa como parámetro por referencia, de esta manera los nuevos enlaces mantienen la coherencia. Si el elemento a insertar ya existe entonces no se hace nada.

void insertar(tarbol **a, int elem) { if (*a == NULL) { *a = (arbol *) malloc(sizeof(arbol)); (*a)->clave = elem; (*a)->izq = (*a)->der = NULL; } else if ((*a)->clave < elem) insertar(&(*a)->der, elem); else if ((*a)->clave > elem) insertar(&(*a)->izq, elem); }

Page 63: Estructura de Datos

Estructura de Datos UCENM 63

Borrado

La operación de borrado si resulta ser algo más complicada. Se recuerda que el árbol debe seguir siendo de búsqueda tras el borrado. Pueden darse tres casos, una vez encontrado el nodo a borrar: 1) El nodo no tiene descendientes. Simplemente se borra. 2) El nodo tiene al menos un descendiente por una sola rama. Se borra dicho nodo, y su primer descendiente se asigna como hijo del padre del nodo borrado. Ejemplo: en el árbol de la Figura 2 se borra el nodo cuya clave es -1. El árbol resultante es:

3) El nodo tiene al menos un descendiente por cada rama. Al borrar dicho nodo es necesario mantener la coherencia de los enlaces, además de seguir manteniendo la estructura como un árbol binario de búsqueda. La solución consiste en sustituir la información del nodo que se borra por el de una de las hojas, y borrar a continuación dicha hoja. ¿Puede ser cualquier hoja?

No, debe ser la que contenga una de estas dos claves:

La mayor de las claves menores al nodo que se borra. Suponer que se quiere borrar el nodo 4 del árbol de la Figura 2. Se sustituirá la clave 4 por la clave 2.

La menor de las claves mayores al nodo que se borra. Suponer que se quiere borrar el nodo 4 del árbol de la Figura 2. Se sustituirá la clave 4 por la clave 5.

El algoritmo de borrado que se implementa a continuación realiza la sustitución por la mayor de las claves menores, (aunque se puede escoger la otra opción sin pérdida de generalidad). Para lograr esto es necesario descender primero a la izquierda del nodo que se va a borrar, y después avanzar siempre a la derecha hasta encontrar un nodo hoja. A continuación se muestra gráficamente el proceso de borrar el nodo de clave 4:

Page 64: Estructura de Datos

Estructura de Datos UCENM 64

Codificación: el procedimiento sustituir es el que desciende por el árbol cuando se da el caso del nodo con descendientes por ambas ramas.

void borrar(tarbol **a, int elem) { void sustituir(tarbol **a, tarbol **aux); tarbol *aux; if (*a == NULL) /* no existe la clave */ return; if ((*a)->clave < elem) borrar(&(*a)->der, elem); else if ((*a)->clave > elem) borrar(&(*a)->izq, elem); else if ((*a)->clave == elem) { aux = *a; if ((*a)->izq == NULL) *a = (*a)->der; else if ((*a)->der == NULL) *a = (*a)->izq; else sustituir(&(*a)->izq, &aux); /* se sustituye por la mayor de las menores */ free(aux); } }

operación “borrado”

El nodo a borrar debe ser reemplazado por el nodo más a la derecha en el subárbol izquierdo o el nodo más a la izquierda en el subárbol derecho (el nodo más a la derecha del subárbol izquierdo será mayor o igual que cualquier otro nodo de ese subárbol y menor que todos los del subárbol derecho, y el nodo más a la izquierda del subárbol derecho será menor que todos los demás nodos de ese subárbol y mayor que todos los del subárbol izquierdo). Para

Page 65: Estructura de Datos

Estructura de Datos UCENM 65

el caso en el que el nodo elegido tengo un subárbol, hay por lo menos tres soluciones posibles:

La primera consiste en conservar la estructura del subárbol, y colgar del elemento ubicado en el extremo (el elemento menor o mayor) correspondiente al subárbol donde se encuentra el elemento a promover hacia la raíz (en este ejemplo, el subárbol izquierdo, por lo cual se buscará el elemento más a la izquierda), lo cual es consistente, porque todos los elementos en el subárbol promovido serán mayores que los del subárbol del cual estaban colgados a la derecha. El inconveniente que presenta esta solución es que debe utilizarse una función encontrarMínimo() o encontrarMáximo().

La segunda solución consiste en colgar del padre del nodo promovido hacia la raíz, el subárbol remanente. Esto es consistente, porque todo elemento del subárbol derecho de un nodo será mayor que el valor de ese nodo, y viceversa. Estas soluciones aprovechan la ventaja de contar con que el nodo promovido tiene, a lo sumo, un subárbol.

Un hueco dejado por un nodo promovido también puede pensarse como una eliminación.

Árboles binarios perfectamente equilibrados

La eficiencia de las operaciones depende exclusivamente de la altura del árbol. Para un árbol de N nodos perfectamente equilibrado el coste de acceso es de orden logarítmico: O(log N). Sin embargo, si el árbol crece o decrece descontroladamente, el rendimiento puede disminuir considerablemente, siendo para el caso más desfavorable (insertar un conjunto de claves ordenadas en forma ascendente o descendente) el coste de acceso: O(N). En un árbol binario perfectamente equilibrado, el número de nodos en el subárbol izquierdo y el número de nodos en el subárbol derecho, difieren como mucho en una unidad, y los subárboles son también equilibrados.

Árboles equilibrados

Un procedimiento de inserción que siempre restaure la estructura del árbol a un equilibrio perfecto es poco eficiente. Se usa una formulación menos estricta de “equilibrio”: Un árbol está equilibrado si para cada uno de sus nodos ocurre que las alturas de sus dos subárboles difieren como mucho en 1.

Page 66: Estructura de Datos

Estructura de Datos UCENM 66

Factor de equilibrio (FE) de un nodo

El FE es la altura del subárbol izquierdo menos la altura del subárbol derecho. Los valores que puede tomar son -1, 0, 1. Si llegara a tomar los valores -2 o 2 debe reestructurarse el árbol. Todos los árboles perfectamente equilibrados son AVL. La longitud de camino media es prácticamente idéntica a la de un árbol perfectamente equilibrado. En un AVL se puede realizar con complejidad del O(log N) las siguientes operaciones:

Encontrar un nodo con una clave dada. Insertar un nodo con una clave dada. Borrar un nodo con una clave dada. Un árbol AVL de altura H tiene por lo menos

(Fibonacci(H+3) -1) nodos. Los pasos necesarios para insertar un nodo en un árbol AVL son:

Agregar el nodo como en un árbol binario de búsqueda. En el regreso por el camino de búsqueda se comprueba el FE de los nodos. Si un nodo presenta un FE incorrecto (2 o -2) se reestructura el árbol y se continúa el

ascenso hasta llegar a la raíz. Casos en situación de reestructurar:

1. Una inserción en el subárbol izquierdo del hijo izquierdo de X. 2. Una inserción en el subárbol derecho del hijo izquierdo de X. 3. Una inserción en el subárbol izquierdo del hijo derecho de X. 4. Una inserción en el subárbol derecho del hijo derecho de X.

Inserciones en los “márgenes”

1 y 4: inserciones en los “márgenes”: rotación simple: intercambia los papeles de los padres y de los hijos, manteniendo la ordenación.

// Rotación izquierda - izquierda private static NodoAVL rotarConHijoIzq(NodoAVL A) { NodoAVL B = (NodoAVL) A.getIzq(); //Asigna nombre A.setIzq(B.getDer()); B.setDer(A); return B; }

Inserciones por “dentro”

2 y 3: inserciones por “dentro”: rotación doble. Notar que una rotación simple no resuelve el problema, ya que la rama que provocó el desequilibrio se descuelga del nodo promovido y se cuelga al nodo que desciende un nivel, de manera que se mantiene con la misma profundidad, que es la que provocó el desequilibrio. Por lo tanto, antes de rotar, debe

Page 67: Estructura de Datos

Estructura de Datos UCENM 67

desplazarse el desequilibrio a la rama correspondiente, es decir, transformamos el caso de una inserción por dentro, a un caso de inserción en el margen, utilizando una rotación.

// Rotación izquierda - derecha private static NodoAVL rotarDobleConHijoIzq(NodoAVL A) { NodoAVL B = (NodoAVL) A.getIzq(); //Asigna nombre A.setIzq(rotarConHijoDer((NodoAVL) B)); return rotarConHijoIzq(A); }

Como se ve, el problema se convirtió en un problema igual al del primer caso. Los pasos necesarios para suprimir un nodo en un árbol AVL son:

Suprimir el nodo como en un árbol binario de búsqueda. En el regreso por el camino de supresión se comprueba el FE de los nodos. Si un nodo presenta un FE incorrecto (2 o -2) se reestructura el árbol y se continúa el

ascenso hasta llegar a la raíz. La reestructuración se efectúa cuando al regresar por el camino de búsqueda después de una inserción o una supresión se comprueba que la condición del FE se ha violado. Una supresión puede provocar varias reestructuraciones. En la práctica se utilizan otros esquemas de equilibrio como los árboles rojinegros: como en los AVL las operaciones son logarítmicas en el peor caso. La ventaja es que las inserciones y eliminaciones pueden realizarse con un único recorrido descendente.

Árboles rojinegros

Árbol binario de búsqueda, donde cada nodo está coloreado con los colores rojo o negro, y se verifican las siguientes propiedades:

1. La raíz es negra.

2. Si un nodo es rojo, sus hijos deben ser negros.

3. Todos los caminos desde un nodo a un nodo vacío deben contener el mismo número de nodos negros.

Las condiciones (2) y (3) aseguran que el árbol nunca esté demasiado desbalanceado. (2) asegura que no puedan haber demasiados nodos rojos, y (3) dice que, despreciando el número de nodos rojos, que es limitado, el árbol es perfectamente balanceado. La condición (1) es trivial: si la raíz es roja, simplemente se colorea negra, ya que esto no violará ninguna regla. En los ARN la operación eliminar se complica. Cuando se necesitan árboles equilibrados y se requieren muchas eliminaciones se emplean los AA-Árboles que añaden una condición adicional a las impuestas por los ARN:

Page 68: Estructura de Datos

Estructura de Datos UCENM 68

4. Los hijos izquierdos no pueden ser rojos.

Altura de un ARN

En un ARN con n nodos, la altura h será:

Demostración

La condición (3) nos permite asegurar que, despreciando el número de nodos rojos, el árbol

es perfectamente balanceado, y, en virtud de esa característica, su altura . La condición (2) evita que haya nodos rojos consecutivos, como máximo, la mitad de los nodos de un camino -que constituirán una altura- serán rojos.

Operaciones

Se pueden realizar operaciones de búsqueda con complejidad O(log N), por lo expuesto anteriormente. Al realizar una inserción, la complejidad de búsqueda será O(log N), pero aparece un problema: el resultado será un árbol de búsqueda binario, pero no necesariamente un ARN. Si coloreamos el nuevo nodo rojo, el balance de negros quedará intacto, pero se puede incurrir en una violación rojo-rojo. Si lo coloreamos negro, no se incurrirá en una violación rojo-rojo, pero en este caso, siempre alteraremos el balance de negros. Al eliminar, si el nodo a eliminar es negro, ambas violaciones pueden aparecer.

Reparación del balance del árbol

Una vez detectada una violación, se deben tomar medidas que reparen el balance del árbol. Estas medidas utilizan dos herramientas: rotaciones y cambios de color.

Inserción ascendente

Sea X la nueva hoja añadida, P su padre, S el hermano de P (si existe) y G el abuelo.

1. Los nuevos nodos se insertan en el árbol como hojas de color rojo.

2. Si el padre es negro, hemos acabado.

Page 69: Estructura de Datos

Estructura de Datos UCENM 69

3. Si el padre es rojo violamos la regla 2, entonces debemos modificar el árbol de forma que se cumpla la regla (2) sin introducir violaciones de la propiedad (3).

4. Si el padre P es la raíz, se colorea negro. La altura negra aumenta en 1, pero el balance se preserva. Hemos acabado.

Reparación del balance

Asumiendo que P no es la raíz

5. Si S es rojo, se puede aplicar un cambio de color: Se elimina así la violación, y el balance de negros se mantiene. ¿Qué pasa si el padre de G es también rojo? Solución: propagar este procedimiento hacia arriba hasta conseguir que no haya dos nodos rojos consecutivos o alcanzar la raíz. Esta propagación es análoga a la que se hace en los árboles AVL.

6. Si S es negro, tenemos dos casos, con sus simétricos asociados: violación en el margen, y violación por dentro.

7. Si es una violación es por dentro, la convertimos a una violación en el margen haciendo una rotación: Como X y P son rojos, no añaden nada en el balance de negros a los caminos que pasan por g, de manera que el balance se preserva.

8. Teniendo el caso de violación por dentro, se efectúa una rotación simple. La rama izquierda de desbalancea, le falta un nodo negro para todos sus caminos, y aún tenemos la violación rojo-rojo. Solucionamos ambos problemas haciendo un cambio de color.

Inserción descendente

Objetivo: garantizar que en el momento de la inserción S no es rojo, de manera que sólo haya que añadir una hoja roja y, si fuere necesario, realizar una rotación (simple o doble). En el camino descendente, si un nodo X tiene dos hijos rojos, el color de X cambia a rojo y el de sus dos hijos a negro. El número de nodos negros en los caminos por debajo de X permanece inalterable. Si X es la raíz, la convertiríamos en roja, hay que volver a negro (esto no puede violar ninguna de las reglas). Si el padre de X es rojo, hay que aplicar rotación simple o doble. ¿Qué pasa si el hermano del padre de X es también rojo? Esta situación NO puede darse, gracias al proceso efectuado en el camino descendiente.

Page 70: Estructura de Datos

Estructura de Datos UCENM 70

Árboles B

Mientras que la altura de un árbol binario completo es, aproximadamente, log2N, la altura de un árbol M-ario completo es, más o menos, logMN. Un B-árbol de orden M es un árbol M-ario que verifica:

Cada página, excepto la página raíz y las páginas hojas, tienen entre M/2 y M descendientes, y entre (M/2 -1) y (M-1) elementos.

La página raíz, o es una hoja o tiene entre 2 y M descendientes. Las páginas hojas están todas al mismo nivel.

Relación entre los árboles B y los árboles rojinegros

Si juntamos un nodo negro con sus hijos rojos, si los hubiere, en un mismo nodo, se obtiene un árbol no binario con altura igual a la altura negra, con un máximo de 3 elementos y 4 hijos, y un mínimo de un elemento; en definitiva, es un árbol B de orden 4.

Búsqueda

Debe tenerse en memoria principal la página sobre la cual vamos a buscar. Considérese el elemento a buscar x. Si la búsqueda es infructuosa dentro de la página se estará en una de las siguientes situaciones:

ki < x < ki + 1 para 1 ≤i < n. La búsqueda continúa en la página ri. kn < x. La búsqueda continúa en la página rn. x < k1. La búsqueda continúa en la páginar0. Si en algún caso la referencia es nula, es

decir, si no hay página descendiente, entonces no hay ningún elemento x en todo el árbol y se acaba la búsqueda.

Inserción

Siempre se inserta en los nodos hojas. Primero se comprueba que la clave no se encuentre en el árbol. Si la cantidad de elementos es menor que 2n:

Se inserta en forma secuencial la clave. Si la cantidad de elementos es 2n: Los 2n+1 elementos se dividen en dos páginas, excluyendo la clave del medio. La clave del medio se inserta en el nodo padre.

Page 71: Estructura de Datos

Estructura de Datos UCENM 71

Borrado

Si la clave a ser borrada no está en una hoja, su predecesor inmediato tiene que estar en una hoja (esto se debe a que todas las hojas tienen el mismo nivel, de manera que si existe un valor menor en un nodo más abajo, también tiene que haber uno mayor), y se puede promover, y así borrar el espacio que estaba en la hoja. Si la hoja queda con menos de n elementos, se comprueba si se puede promover un elemento de un hermano adyacente a su padre, y bajar el del padre a la hoja. Si el hermano tiene sólo n elementos, las dos hojas y la clave del medio se unen, y la clave del medio se elimina del nodo padre.

Árboles B+

Las diferencias con los árboles B son que:

Sólo los nodos hoja apuntan a los registros o cubetas del fichero. Existe un orden lineal entre las hojas, que están encadenadas mediante punteros para

permitir un eficiente acceso secuencial.

Búsqueda por clave

Buscar en la raíz el valor ki más pequeño mayor que la clave x. La búsqueda sigue por el puntero pi hasta que llegue a un nodo hoja, que será donde esté el puntero al bloque o cubeta (cuando un elemento se encuentre en una página raíz o interior la búsqueda continuará por la rama derecha de dicha clave, hasta llegar a una hoja).

Inserción

Se busca el nodo hoja correspondiente y se inserta la clave si no está allí. Si tiene lugar una partición, se inserta una clave en el nodo padre, que será duplicada si la partición ocurre en una hoja.

Eliminación

Se busca el nodo hoja correspondiente y se elimina la clave. Si al eliminar una clave, n queda menor a (M/2 -1), entonces debe realizarse una redistribución de claves, tanto en el índice como en las páginas hojas.

Aplicación práctica de un árbol

Se tiene un fichero de texto ASCII. Para este propósito puede servir cualquier libro electrónico de la librería Gutenberg o Cervantes, que suelen tener varios cientos de miles de palabras. El objetivo es clasificar todas las palabras, es decir, determinar que palabras

Page 72: Estructura de Datos

Estructura de Datos UCENM 72

aparecen, y cuantas veces aparece cada una. Palabras como 'niño'-'niña', 'vengo'-'vienes' etc., se consideran diferentes por simplificar el problema.

Escribir un programa, que recibiendo como entrada un texto, realice la clasificación descrita anteriormente.

Ejemplo:

Texto: "a b'a c. hola, adiós, hola" La salida que produce es la siguiente: a 2 adiós 1 b 1 c 1 hola 2

Nótese que el empleo de una lista enlazada ordenada no es una buena solución. Si se obtienen hasta 20.000 palabras diferentes, por decir un número, localizar una palabra cualquiera puede ser, y en general lo será, muy costoso en tiempo. Se puede hacer una implementación por pura curiosidad para evaluar el tiempo de ejecución, pero no merece la pena.

La solución pasa por emplear un árbol binario de búsqueda para insertar las claves. El valor de Log (20.000) es aproximadamente de 14. Eso quiere decir que localizar una palabra entre 20.000 llevaría en el peor caso unos 14 accesos. El contraste con el empleo de una lista es simplemente abismal. Por supuesto, como se ha comentado anteriormente el árbol no va a estar perfectamente equilibrado, pero nadie escribe novelas manteniendo el orden lexicográfico (como un diccionario) entre las palabras, así que no se obtendrá nunca un árbol muy degenerado. Lo que está claro es que cualquier evolución del árbol siempre será mejor que el empleo de una lista.

Por último, una vez realizada la lectura de los datos, sólo queda hacer un recorrido en orden central del árbol y se obtendrá la solución pedida en cuestión de segundos.

Una posible definición de la estructura árbol es la siguiente:

typedef struct tarbol { char clave[MAXPALABRA]; int contador; /* numero de apariciones. Iniciar a 0 */ struct tarbol *izq, *der; } tarbol;

Page 73: Estructura de Datos

Estructura de Datos UCENM 73

EJEMPLOS DE ÁRBOLES

Page 74: Estructura de Datos

Estructura de Datos UCENM 74

Capitulo IV

Teoría de Grafos

En matemáticas y ciencias de la computación, la teoría de grafos estudia las propiedades de los grafos, que son colecciones de objetos llamados vértices (o nodos) conectados por líneas llamadas aristas (o arcos) que pueden tener orientación (dirección asignada). Típicamente, un grafo está diseñado por una serie de puntos (los vértices) conectados por líneas (las aristas).

Historia

Los siete puentes de Königsberg.

El primer artículo científico relativo a grafos fue escrito por el matemático suizo Leonhard Euler en 1736. Euler se basó en su artículo en el problema de los puentes de Königsberg. La ciudad de Kaliningrado, originalmente Königsberg, es famosa por sus siete puentes que unen ambas márgenes del río Pregel con dos de sus islas. Dos de los puentes unen la isla mayor con la margen oriental y otros dos con la margen occidental. La isla menor está conectada a cada margen por un puente y el séptimo puente une ambas islas. El problema planteaba lo siguiente: ¿es posible, partiendo de un lugar arbitrario, regresar al lugar de partida cruzando cada puente una sola vez?

Abstrayendo este problema y planteándolo con la (entonces aún básica) teoría de grafos, Euler consigue demostrar que el grafo asociado al esquema de puentes de Königsberg no tiene solución, es decir, no es posible regresar al vértice de partida sin pasar por alguna arista dos veces.

Page 75: Estructura de Datos

Estructura de Datos UCENM 75

De hecho, Euler resuelve el problema más general: ¿qué condiciones debe satisfacer un grafo para garantizar que se puede regresar al vértice de partida sin pasar por la misma arista más de una vez?

El trabajo de Leonhard Euler, en 1736, sobre el problema de los puentes de Königsberg es considerado como uno de los primeros resultados de la teoría de grafos. También se considera uno de los primeros resultados topológicos en geometría (que no depende de ninguna medida). Este ejemplo ilustra la profunda relación entre la teoría de grafos y la topología.

En 1845 Gustav Kirchhoff publicó sus leyes de los circuitos para calcular el voltaje y la corriente en los circuitos eléctricos.

En 1852 Francis Guthrie planteó el problema de los cuatro colores que plantea si es posible, utilizando solamente cuatro colores, colorear cualquier mapa de países de tal forma que dos países vecinos nunca tengan el mismo color. Este problema, que no fue resuelto hasta un siglo después por Kenneth Appel y Wolfgang Haken, puede ser considerado como el nacimiento de la teoría de grafos. Al tratar de resolverlo, los matemáticos definieron términos y conceptos teóricos fundamentales de los grafos.

Definición:

Un grafo es la representación por medio de conjuntos de relaciones arbitrarias entre objetos

En matemáticas y ciencias de la computación, un grafo (del griego grafos: dibujo, imagen) es el principal objeto de estudio de la teoría de grafos.

Informalmente, un grafo es un conjunto de objetos llamados vértices o nodos unidos por enlaces llamados aristas o arcos, que permiten representar relaciones binarias entre elementos de un conjunto.

Típicamente, un grafo se representa gráficamente como un conjunto de puntos (vértices o nodos) unidos por líneas (aristas).

Desde un punto de vista práctico, los grafos permiten estudiar las interrelaciones entre unidades que interactúan unas con otras. Por ejemplo, una red de computadoras puede representarse y estudiarse mediante un grafo, en el cual los vértices representan terminales y las aristas representan conexiones (las cuales, a su vez, pueden ser cables o conexiones inalámbricas).

Prácticamente cualquier problema puede representarse mediante un grafo, y su estudio trasciende a las diversas áreas de las ciencias duras y las ciencias sociales.

Un grafo G es un par ordenado G = (V,E), donde:

V es un conjunto de vértices o nodos, y

Page 76: Estructura de Datos

Estructura de Datos UCENM 76

E es un conjunto de arcos o aristas, que relacionan estos nodos.

Normalmente V (y por tanto también E) suele ser finito. Muchos resultados importantes sobre grafos no son aplicables para grafos infinitos. Se llama orden de G a su número de vértices, |

V | .

Lazos o bucles

Un lazo o bucle es una arista que relaciona al mismo nodo; es decir, una arista donde el nodo inicial y el nodo final coinciden.

Tipos de grafos

Existen dos tipos de grafos según la relación entre los objetos sea unívoca o biunívoca. Los primeros forman los grafos dirigidos o dígrafos y los segundos los grafos no dirigidos o simplemente grafos.

Grafo no dirigido

Un grafo no dirigido, o grafo, al igual que un dígrafo consiste de un conjunto de vértices V y un conjunto de arcos A. La diferencia consiste en que la existencia de aRb presupone que bRa también existe y además que son iguales. De este modo es indistinto hablar del arco (a,b) o (b,a), tampoco tiene sentido hablar de la cabeza o la cola del arco. Los grafos representan como lo indica la figura , donde los círculos representan los vértices y las líneas representan los arcos.

. Grafo no dirigido

Un grafo no dirigido o grafo propiamente dicho es un grafo G = (V,E) donde:

es un conjunto de pares no ordenados de elementos de .

Page 77: Estructura de Datos

Estructura de Datos UCENM 77

Un par no ordenado es un conjunto de la forma {a,b}, de manera que {a,b} = {b,a}. Para los grafos, estos conjuntos pertenecen al conjunto potencia de V de cardinalidad 2, el cual se

denota por .

Un nodo b se dice que es adyacente al nodo a si existe el arco (a, b), tómese en cuenta que para un grafo no dirigido necesariamente a es también adyacente a b. Esto no ocurre en los grafos dirigidos donde la existencia de (a, b) no implica que (b, a) también existe. Este concepto es de particular importancia dado que los grafos suelen representarse en la computadora por medio de listas o matrices de adyacencias.

Un arco (a,b) incide en el nodo b, de igual modo en grafo no dirigido dicho arco también incide en el nodo a debido a que también existe (b, a). El número de arcos que inciden en un nodo le otorga el grado a dicho nodo. El nodo con mayor grado en el grafo le indica el grado de dicho grafo. También se acostumbra representar a un grafo por medio de listas o matrices de incidencias.

Grafo dirigido

Un grafo dirigido o dígrafo consiste de un conjunto de vértices V y un conjunto de arcos A. Los vértices se denominan nodos o puntos; los arcos también se conocen como aristas o líneas dirigidas que representan que entre un par de vértices existe una relación unívoca aRb pero no bRa. De modo que los arcos se representan comúnmente por medio de pares ordenados (a,b), donde se dice que a es la cabeza y b la cola del arco y a menudo se representa también por medio de una flecha, tal como se muestra en la figura .

. Grafo dirigido

Un grafo dirigido o digrafo es un grafo G = (V,E) donde:

es un conjunto de pares ordenados de elementos de .

Dada una arista (a,b), a es su nodo inicial y b su nodo final.

Por definición, los grafos dirigidos no contienen bucles.

Page 78: Estructura de Datos

Estructura de Datos UCENM 78

Un vértice que solo tiene arcos saliendo de él se denomina fuente y un vértice que solo tiene arcos dirigidos hacia él se denomina sumidero. Dicha nomenclatura es importante cuando los dígrafos se usan para resolver problemas de flujos.

Pseudografo

Un pseudografo es un grafo G = (V,E) donde:

es un conjunto de pares no ordenados de elementos de .

Es decir, un pseudografo es un grafo no dirigido que acepta bucles en .

Pseudografo dirigido

Un pseudografo dirigido es un grafo G = (V,E) donde:

es un conjunto de pares ordenados y etiquetados de elementos de

Es decir, un pseudografo dirigido es un grafo dirigido que acepta bucles en .

Variantes sobre las principales definiciones

Algunas aplicaciones requieren extensiones más generales a las dos propuestas clásicas de grafos. Aunque la definición original los permite, según la aplicación concreta pueden ser válidos o no. A veces V o E pueden ser un multiconjunto, pudiendo haber más de una arista entre cada par de vértices. La palabra grafo (a secas) puede permitir o no múltiples aristas entre cada par de vértices, dependiendo del autor de la referencia consultada. Si se quiere remarcar la inexistencia de múltiples aristas entre cada par de vértices (y en el caso no dirigido, excluir bucles) el grafo puede llamarse simple. Por otra parte, si se quiere asegurar la posibilidad de permitir múltiples aristas, el grafo puede llamarse multigrafo (a veces se utiliza el término pseudografo para indicar que se permiten tanto bucles como múltiples aristas entre cada par de vértices).

Page 79: Estructura de Datos

Estructura de Datos UCENM 79

Propiedades

Adyacencia: dos aristas son adyacentes si tienen un vértice en común, y dos vértices son adyacentes si una arista los une.

Incidencia: una arista es incidente a un vértice si ésta lo une a otro. Ponderación: corresponde a una función que a cada arista le asocia un valor (costo,

peso, longitud, etc.), para aumentar la expresividad del modelo. Esto se usa mucho para problemas de optimización, como el del vendedor viajero o del camino más corto.

Etiquetado: distinción que se hace a los vértices y/o aristas mediante una marca que los hace unívocamente distinguibles del resto.

Ejemplo

Grafo etiquetado con 6 vértices y 7 aristas.

La imagen es una representación del siguiente grafo:

V:={1,2,3,4,5,6} E:={{1,2},{1,5},{2,3},{2,5},{3,4},{4,5},{4,6}}

El hecho que el vértice 1 sea adyacente con el vértice 2 puede ser denotado como 1 ~ 2.

En la Teoría de las categorías una categoría puede ser considerada como un multigrafo dirigido, con los objetos como vértices y los morfismos como aristas dirigidas.

En ciencias de la computación los grafos dirigidos son usados para representar máquinas de estado finito y algunas otras estructuras discretas.

Una relación binaria R en un conjunto X es un grafo dirigido simple. Dos vértices a, b en X están conectados por una arista dirigida ab si aRb.

Page 80: Estructura de Datos

Estructura de Datos UCENM 80

Estructuras de datos en la representación de grafos

Existen diferentes formas de almacenar grafos en una computadora. La estructura de datos usada depende de las características del grafo y el algoritmo usado para manipularlo. Teóricamente se pueden distinguir las estructuras de listas y las de matrices, pero usualmente, lo mejor es una combinación de ambas. Las listas son preferidas en grafos dispersos porque tienen un eficiente uso de la memoria. Por otro lado, las matrices proveen acceso rápido, pero pueden consumir grandes cantidades de memoria.

Estructura de lista

lista de incidencia: Las aristas son representadas con un vector de pares (ordenados, si el grafo es dirigido) de vértices que conecta esa arista.

lista de adyacencia: Cada vértice tiene una lista de vértices los cuales son adyacentes a él. Esto causa redundancia en un grafo no dirigido (ya que A existe en la lista de adyacencia de B y viceversa), pero las búsquedas son más rápidas, al costo de almacenamiento extra.

Estructuras matriciales

Matriz de incidencia: El grafo está representado por una matriz de A (aristas) por V (vértices), donde [arista, vértice] contiene la información de la arista (1 - conectado, 0 - no conectado)

Matriz de adyacencia: El grafo está representado por una matriz cuadrada M de tamaño n, donde n es el número de vértices. Si hay una arista entre un vértice x y un vértice y, entonces el elemento mx,y es 1, de lo contrario, es 0.

Vértice

Un vértice es la unidad fundamental de la que están formados los grafos. Los vértices son tratados como un objeto indivisible y sin propiedades, aunque puedan tener una estructura adicional dependiendo de la aplicación por la cual se usa el grafo; por ejemplo, una red semántica es un grafo en donde los vértices representan conceptos o clases de objetos.

Page 81: Estructura de Datos

Estructura de Datos UCENM 81

En la figura, V = { a, b, c, d, e, f }, y A = { ab, ac, ae, bc, bd, df, ef }.

Un grafo es una pareja de conjuntos G = (V,A), donde V es el conjunto de vértices, y A es el conjunto de aristas, este último es un conjunto de subconjuntos de la forma (u,v) tal que

, tal que . Para simplificar, notaremos la arista {a,b} como ab.

En teoría de grafos, sólo queda lo esencial del dibujo: la forma de las aristas no son relevantes, sólo importa a qué vértices están unidas. La posición de los vértices tampoco importa, y se puede variar para obtener un grafo más claro. Generalmente, se considera que colocar los vértices en forma de polígono regular da grafos muy legibles.

Prácticamente cualquier red puede ser modelada con un grafo: una red de carreteras que conecta ciudades, una red eléctrica o la red de drenaje de una ciudad.

Subgrafo

Un subgrafo de un grafo G es un grafo cuyos conjuntos de vértices y aristas son subconjuntos de los de G. Se dice que un grafo G contiene a otro grafo H si algún subgrafo de G es H o es isomorfo a H (dependiendo de las necesidades de la situación)

El subgrafo inducido de G es un subgrafo G' de G tal que contiene todas las aristas adyacentes al subconjunto de vértices de G.

Aristas dirigidas y no dirigidas

En algunos casos es necesario asignar un sentido a las aristas, por ejemplo, si se quiere representar la red de las calles de una ciudad con sus inevitables direcciones únicas. El conjunto de aristas será ahora un subconjunto de todos los posibles pares ordenados de

Page 82: Estructura de Datos

Estructura de Datos UCENM 82

vértices, con (a, b) ≠ (b, a). Los grafos que contienen aristas dirigidas se denominan grafos orientados.

Las aristas no orientadas se consideran bidireccionales para efectos prácticos (equivale a decir que existen dos aristas orientadas entre los nodos, cada una en un sentido).

En el grafo anterior se ha utilizado una arista que tiene sus dos extremos idénticos: es un lazo (o bucle), y aparece también una arista bidireccional, y corresponde a dos aristas orientadas.

Aquí V = { a, b, c, d, e }, y A = { (a, c), (d, a), (d, e), (a, e), (b, e), (c, a), (c, c), (d, b) }.

Se considera la característica de "grado" (positivo o negativo) de un vértice v (y se indica como (v)), como la cantidad de aristas que llegan o salen de él; para el caso de grafos no orientados, el grado de un vértice es simplemente la cantidad de aristas que tocan este vértice. Por ejemplo, el grado positivo (salidas) de d es 3, mientras que el grado negativo (llegadas) de b es 1.

A un vértice del que sólo salen aristas se le denomina fuente (en el ejemplo anterior, el vértice d); tiene grado negativo 0. Por el contrario, a aquellos en los que sólo entran aristas se les denomina pozo o sumidero (en el caso anterior, el vértice e); tiene grado positivo 0.

Ciclos y caminos hamiltonianos

Ejemplo de un ciclo hamiltoniano.

Page 83: Estructura de Datos

Estructura de Datos UCENM 83

Un ciclo es un camino, es decir una sucesión de aristas adyacentes, donde no se recorre dos veces la misma arista, y donde se regresa al punto inicial. Un ciclo hamiltoniano tiene además que recorrer todos los vértices exactamente una vez (excepto el vértice del que parte y al cual llega).

Por ejemplo, en un museo grande, lo idóneo sería recorrer todas las salas una sola vez, esto es buscar un ciclo hamiltoniano en el grafo que representa el museo (los vértices son las salas, y las aristas los corredores o puertas entre ellas).

Se habla también de camino hamiltoniano si no se impone regresar al punto de partida, como en un museo con una única puerta de entrada. Por ejemplo, un caballo puede recorrer todas las casillas de un tablero de ajedrez sin pasar dos veces por la misma: es un camino hamiltoniano. Ejemplo de un ciclo hamiltoniano en el grafo del dodecaedro.

Caracterización de grafos

Grafo nulo o vacío

Aquel que no tiene vértices ni aristas. No hay un actual consenso sobre si deba seguir existiendo, ya que no cumple con muchas propiedades.

Grafos simples

Un grafo es simple si a lo sumo sólo 1 arista une dos vértices cualesquiera. Esto es equivalente a decir que una arista cualquiera es la única que une dos vértices específicos.

Un grafo que no es simple se denomina complejo.

Grafo trivial

Aquel que tiene un vértice y ninguna arista.

Grafos conexos

Un grafo es conexo si cada par de vértices está conectado por un camino; es decir, si para cualquier par de vértices (a, b), existe al menos un camino posible desde a hacia b.

Page 84: Estructura de Datos

Estructura de Datos UCENM 84

Un grafo es fuertemente conexo si cada par de vértices está conectado por al menos dos caminos disjuntos; es decir, es conexo y no existe un vértice tal que al sacarlo el grafo resultante sea disconexo.

En términos matemáticos la propiedad de un grafo de ser (fuertemente) conexo permite establecer en base a él una relación de equivalencia para sus vértices, la cual lleva a una partición de éstos en "componentes (fuertemente) conexas", es decir, porciones del grafo, que son (fuertemente) conexas cuando se consideran como grafos aislados. Esta propiedad es importante para muchas demostraciones en teoría de grafos.

Grafos completos

Un grafo simple es completo si existen aristas uniendo todos los pares posibles de vértices. Es decir, todo par de vértices (a, b) debe tener una arista e que los une.

El conjunto de los grafos completos es denominado usualmente , siendo el grafo completo de n vértices.

Un Kn, es decir, grafo completo de n vértices tiene exactamente aristas.

La representación gráfica de los Kn como los vértices de un polígono regular da cuenta de su peculiar estructura.

Grafo bipartito completo

sea (W,X) una partición del conjunto de vértices V, es aquel donde cada vértice en W es adyacente sólo a cada vértice en X, y viceversa.

Page 85: Estructura de Datos

Estructura de Datos UCENM 85

Grafos bipartitos

Un grafo G es bipartito si puede expresarse como (es decir, sus vértices son la unión de dos grupos de vértices), bajo las siguientes condiciones:

V1 y V2 son disjuntos y no vacíos. Cada arista de A une un vértice de V1 con uno de V2. No existen aristas uniendo dos elementos de V1; análogamente para V2.

Bajo estas condiciones, el grafo se considera bipartito, y puede describirse informalmente como el grafo que une o relaciona dos conjuntos de elementos diferentes, como aquellos resultantes de los ejercicios y puzzles en los que debe unirse un elemento de la columna A con un elemento de la columna B.

Operaciones en Grafos

Subdivisión elemental de una arista

Se convierte en

Se reemplaza la arista por dos aristas y un vértice w.

Después de realizar esta operación, el grafo queda con un vértice y una arista más.

Remoción débil de un vértice

Si y g(v) = 2 (Sea v un vértice del grafo y de grado dos) removerlo débilmente significa reemplazarlo por una arista que une los vértices adyacentes a v.

Se convierte en

Entonces e' y e'' desaparecen y aparece

Homeomorfismos de grafos

Dos grafos G1 y G2 son homeomorfos si ambos pueden obtenerse a partir del mismo grafo con una sucesión de subdivisiones elementales de aristas.

Page 86: Estructura de Datos

Estructura de Datos UCENM 86

Grafos ponderados

En muchos casos, es preciso atribuir a cada arista un número específico, llamado valuación, ponderación o coste según el contexto, y se obtiene así un grafo valuado. Formalmente, es un grafo con una función v: A → R+.

Por ejemplo, un representante comercial tiene que visitar n ciudades conectadas entre sí por carreteras; su interés previsible será minimizar la distancia recorrida (o el tiempo, si se pueden prever atascos). El grafo correspondiente tendrá como vértices las ciudades, como aristas las carreteras y la valuación será la distancia entre ellas. Y, de momento, no se conocen métodos generales para hallar un ciclo de valuación mínima, pero sí para los caminos desde a hasta b, sin más condición.

Grafos planos

Un grafo es plano si se puede dibujar sin cruces de aristas.

Cuando un grafo o multigrafo se puede dibujar en un plano sin que dos segmentos se corten, se dice que es plano.

Un juego muy conocido es el siguiente: Se dibujan tres casas y tres pozos. Todos los vecinos de las casas tienen el derecho de utilizar los tres pozos. Como no se llevan bien en absoluto, no quieren cruzarse jamás. ¿Es posible trazar los nueve caminos que juntan las tres casas con los tres pozos sin que haya cruces?

Cualquier disposición de las casas, los pozos y los caminos implica la presencia de al menos un cruce.

Sea Kn el grafo completo con n vértices, Kn, p es el grafo bipartito de n y p vértices.

Page 87: Estructura de Datos

Estructura de Datos UCENM 87

El juego anterior equivale a descubrir si el grafo bipartito completo K3,3 es plano, es decir, si se puede dibujar en un plano sin que haya cruces, siendo la respuesta que no. En general, puede determinarse que un grafo no es plano, si en su diseño puede encontrase una estructura análoga (conocida como menor) a K5 o a K3,3.

Establecer qué grafos son planos no es obvio, y es un problema tiene que ver con topología.

Diámetro

En la figura se nota que K4 es plano (desviando la arista ab al exterior del cuadrado), que K5 no lo es, y que K3,2 lo es también (desvíos en gris).

En un grafo, la distancia entre dos vértices es el menor número de aristas de un recorrido entre ellos. El diámetro, en una figura como en un grafo, es la menor distancia entre dos puntos de la misma.

El diámetro de los Kn es 1, y el de los Kn,p es 2. Un diámetro infinito puede significar que el grafo tiene una infinidad de vértices o simplemente que no es conexo. También se puede considerar el diámetro promedio, como el promedio de las distancias entre dos vértices..

En el mundo real hay una analogía: tomando al azar dos seres humanos del mundo, ¿En cuántos saltos se puede pasar de uno a otro, con la condición de sólo saltar de una persona a otra cuando ellas se conocen personalmente? Con esta definición, se estima que el diámetro de la humanidad es de... ¡ocho solamente!

Este concepto refleja mejor la complejidad de una red que el número de sus elementos.

Page 88: Estructura de Datos

Estructura de Datos UCENM 88

Teorema de los cuatro colores

En 1852 Francis Guthrie planteó el problema de los cuatro colores.

Otro problema famoso relativo a los grafos: ¿Cuántos colores son necesarios para dibujar un mapa político, con la condición obvia que dos países adyacentes no puedan tener el mismo color? Se supone que los países son de un solo pedazo, y que el mundo es esférico o plano.

Cuatro colores son siempre suficientes para colorear un mapa.

La forma precisa de cada país no importa; lo único relevante es saber qué país toca a qué otro. Estos datos están incluidos en el grafo donde los vértices son los países y las aristas conectan los que justamente son adyacentes. Entonces la cuestión equivale a atribuir a cada vértice un color distinto del de sus vecinos.

Hemos visto que tres colores no son suficientes, y demostrar que con cinco siempre se llega, es bastante fácil. Pero el teorema de los cuatro colores no es nada obvio. Prueba de ello es que se han tenido que emplear ordenadores para acabar la demostración.

Page 89: Estructura de Datos

Estructura de Datos UCENM 89

Coloración de grafos

Colores en los vértices.

Definición: Si G=(V,E) es un grafo no dirigido, una coloración propia de G, ocurre cuando coloreamos los vértices de G de modo que si {a, b} es una arista en G entonces a y b tienen diferentes colores. (Por lo tanto, los vértices adyacentes tienen colores diferentes). El número mínimo de colores necesarios para una coloración propia de G es el número cromático de G y se escribe como C(G). Sea G un grafo no dirigido sea λ el número de colores disponibles para la coloración propia de los vértices de G. Nuestro objetivo es encontrar una función polinomial P(G,λ), en la variable λ, llamada polinomio cromático de G , que nos indique el número de coloraciones propias diferentes de los vértices de G, usando un máximo de λ colores.

Descomposición de polinomios cromáticos. Si G=(V,E) es un grafo conexo y e pertenece a Ε , entonces: P(Ge,λ)=P(G,λ)+P(Ge,λ)

Para cualquier grafo G, el término constante en P(G,λ) es 0

Sea G=(V,E) con |E|>0 entonces, la suma de los coeficientes de P(G,λ) es 0.

Sea G=(V,E), con a, b pertenecientes al conjunto de vértices V pero {a, b}=e, no perteneciente a al conjunto de aristas E. Escribimos G+e para el grafo que se obtiene de G al añadir la arista e={a, b}. Al identificar los vértices a y b en G, obtenemos el subgrafo G++e de G.

Page 90: Estructura de Datos

Estructura de Datos UCENM 90

Listas de Adyacencia.

En esta estructura de datos la idea es asociar a cada vertice i del grafo una lista que contenga todos aquellos vértices j que sean adyacentes a él. De esta forma sóllo reservará memoria para los arcos adyacentes a i y no para todos los posibles arcos que pudieran tener como origen i. El grafo, por tanto, se representa por medio de un vector de n componentes (si |V|=n) donde cada componente va a ser una lista de adyacencia correspondiente a cada uno de los vertices del grafo. Cada elemento de la lista consta de un campo indicando el vértice adyacente. En caso de que el grafo sea etiquetado, habrá que añadir un segundo campo para mostrar el valor de la etiqueta.

Page 91: Estructura de Datos

Estructura de Datos UCENM 91

Esta representación requiere un espacio proporcional a la suma del número de vértices, más el numero de arcos, y se suele usar cuando el número de arcos es mucho menor que el número de arcos de un grafo completo. Una desventaja es que puede llevar un tiempo O(n) determinar si existe un arco del vértice i al vértice j, ya que puede haber n vertices en la lista de adyacencia asociada al vértice i.

Mediante el uso del vector de listas de adyacencias sólo se reserva memoria para los arcos existentes en el grafo con el consiguiente ahorro de la misma. Sin embargo, no permite que haya vértices que puedan ser añadidos o suprimidos del grafo, debido a que la dimensión del grafo debe ser predeterminada y fija. Para solucionar esto se puede usar una lista de listas de adyacencia. Sólo los vértices del grafo que sean origen de algún arco aparecerán en la lista. De esta forma se pueden añadir y suprimir arcos sin desperdicio de memoria ya que simplemente habrá que modificar la lista de listas para reflejar los cambios.

Page 92: Estructura de Datos

Estructura de Datos UCENM 92

Como puede verse en el ejemplo de las figuras anteriores tanto el vector de listas de adyacencias como en la lista de listas se ha razonado en función de los vértices que actúan como orígenes de los arcos. Análogamente se podía haber hecho con lod vertices destino, y combinando ambas representaciones podría pensarse en utilizar dos vectores de listas de adyacencia o dos listas de listas de adyacencia.

Representación Propuesta.

La elección de una estructura idónea para representar el TDA grafo no es una tarea fácil ya que existen dos representaciones totalmente contrapuestas: por un lado tenemos la matriz de adyacencias que es muy eficiente para comprobar si existe una arista uniendo dos vertices sin embargo desperdicia una gran cantidad de espacio si el grafo no es completo o esta lejos de serlo, además no tiene la posibilidad de añadir nuevos vértices; y por otra parte está la lista de adyacencias que no tiene el problema de la anterior respecto al espacio pero que sin embargo no es tan eficiente a la hora de ver si existe una arista entre dos nodos determinados.

Page 93: Estructura de Datos

Estructura de Datos UCENM 93

Teniendo en cuenta estas consideraciones se ha optado por realizar una mezcla de ambas representaciones intentando aprovechar de alguna forma las ventajas que ambas poseen. Por otra parte siguiendo con la idea de tratar tanto los grafos dirigidos como los no dirigidos bajo una misma estructura, la estructura elegida posee dos apariencias ligeramente diferentes para tratar de forma adecuada cada uno de estos dos tipos de grafos.

La estructura consiste (en el caso de que tengamos un grafo dirigido en una lista de vértices donde cada uno de estos posee dos listas, una de aristas incidentes a él y otra de adyacentes. Cada vez que se añade una arista al grafo se inserta en la lista de aristas adyacentes del vertice origen y en la de incidentes del vértice destino. De esta forma la estructura desplegada se asemejaría a una matriz de adyacencia en la cual hay una arista por cada 1 y el índice de la matriz es la posición dentro de la lista de vertices.

Gráficamente la estructura para un grafo dirigido queda como se puede apreciar en la siguiente figura. El puntero que de la estructura arco que apunta al destino se ha sustituido por la etiqueta del nodo destino en el grafico para simplificarlo y hacerlo mas claro.

Page 94: Estructura de Datos

Estructura de Datos UCENM 94

Esta estructura no seria la más idonea si trabajamos con solo con grafos no dirigidos ya que por cada arista no dirigida tendríamos que insertar en la estructura una misma arista dirigida repetida dos veces (una con un vértice como origen y el otro como destino y al contrario). En

Page 95: Estructura de Datos

Estructura de Datos UCENM 95

muchos problemas si asumimos el desperdicio de espacio podría, de todas formas, resultar interesante representar un grafo no dirigido como un grafo dirigido simétrico, el problema se presenta cuando al tener dos aristas dirigidas esto supone la presencia de un ciclo en el grafo que realmente no existe.

Teniendo en cuenta el razonamiento anterior, en el caso de que queramos manejar grafos no dirigido la estructura consistiría en tener una lista de adyacencia para cada uno de los vertices pero tratando aquellas aristas que aparecen en la lista de adyacencia de dos vertices distintos y que unen ambos vértices como una única arista lógica (a estas dos aristas que forman una misma arista lógica las llamaremos aristas gemelas).

Conclusión:

Gracias a la teoría de grafos se pueden resolver diversos problemas como por ejemplo la síntesis de circuitos secuenciales, contadores o sistemas de apertura.

Los grafos se utilizan también para modelar trayectos como el de una línea de autobús a través de las calles de una ciudad, en el que podemos obtener caminos óptimos para el trayecto aplicando diversos algoritmos como puede ser el algoritmo de Floyd.

La teoría de grafos también ha servido de inspiración para las ciencias sociales, en especial para desarrollar un concepto no metafórico de red socia que sustituye los nodos por los actores sociales y verifica la posición, centralidad e importancia de cada actor dentro de la red. Esta medida permite cuantificar y abstraer relaciones complejas, de manera que la estructura social puede representarse gráficamente. Por ejemplo, una red socia puede representar la estructura de poder dentro de una sociedad al identificar los vínculos (aristas), su dirección e intensidad y da idea de la manera en que el poder se transmite y a quiénes.

Algunas definiciones básicas en grafos

Orden de un grafo: es el número de nodos (vértices) del grafo. Grado de un nodo: es el número de ejes (arcos) que inciden sobre el nodo Grafo simétrico: es un grafo dirigido tal que si existe la relación entonces existe , con

u, v pertenecientes a V. Grafo no simétrico: es un grafo que no cumple la propiedad anterior. Grafo reflexivo: es el grafo que cumple que para todo nodo u de V existe la relación (u,

u) de A. Grafo transitivo: es aquél que cumple que si existen las relaciones (u, v) y (v, z) de A

entonces existe (u, z) de A. Grafo completo: es el grafo que contiene todos los posibles pares de relaciones, es

decir, para cualquier par de nodos u, v de V, (u <> v), existe (u, v) de A. Camino: un camino en el grafo G es una sucesión de vértices y arcos: v(0), a(1), v(1),

a(2), v(2), ... , a(k), v(k); tal que los extremos del arco a(i) son los vértices v(i-1) y v(i).

Page 96: Estructura de Datos

Estructura de Datos UCENM 96

Longitud de un camino: es el número de arcos que componen el camino. Camino cerrado (circuito): camino en el que coinciden los vértices extremos (v(0) =

v(k)). Camino simple: camino donde sus vértices son distintos dos a dos, salvo a lo sumo los

extremos. Camino elemental: camino donde sus arcos son distintos dos a dos. Camino euleriano: camino simple que contiene todos los arcos del grafo. Grafo euleriano: es un grafo que tiene un camino euleriano cerrado. Grafo conexo: es un grafo no dirigido tal que para cualquier par de nodos existe al

menos un camino que los une. Grafo fuertemente conexo: es un grafo dirigido tal que para cualquier par de nodos

existe un camino que los une. Punto de articulación: es un nodo que si desaparece provoca que se cree un grafo no

conexo. Componente conexa: subgrafo conexo máximas de un grafo no dirigido (parte más

grande de un grafo que sea conexa).

Page 97: Estructura de Datos

Estructura de Datos UCENM 97

Ejemplos de grafos:

Page 98: Estructura de Datos

Estructura de Datos UCENM 98

Grafo dirigido

Page 99: Estructura de Datos

Estructura de Datos UCENM 99

Ejercicios:

G1 = (V1, A1) V1 = {1, 2, 3, 4} A1 = {(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)}

G2 = (V2, A2) V2 = {1, 2, 3, 4, 5, 6} A2 = {(1, 2), (1, 3), (2, 4), (2, 5), (3, 6)}

Page 100: Estructura de Datos

Estructura de Datos UCENM 100

G3 = (V3, A3) V3 = {1, 2, 3} A3 = {<1, 2>, <2, 1>, <2, 3>}

Capitulo V

Métodos de Clasificación (Ordenamiento)

Un método de ordenamiento consiste en un algoritmo que recibe como entrada a un conjunto de datos que son necesarios de ordenar según cierto(s) criterio(s). El objetivo fundamental de éstos métodos es el de facilitar la búsqueda de datos según estos mismos criterios.

¿Qué es ordenamiento? Es la operación de arreglar los registros de una tabla en algún orden secuencial de acuerdo a un criterio de ordenamiento. El ordenamiento se efectúa con base en el valor de algún campo en un registro. El propósito principal de un ordenamiento es el de facilitar las búsquedas de los miembros del conjunto ordenado.

Page 101: Estructura de Datos

Estructura de Datos UCENM 101

Ej. de ordenamientos: Dir. telefónico, tablas de contenido, bibliotecas y diccionarios, etc. El ordenar un grupo de datos significa mover los datos o sus referencias para que queden en una secuencia tal que represente un orden, el cual puede ser numérico, alfabético o incluso alfanumérico, ascendente o descendente. ¿Cuándo conviene usar un método de ordenamiento? Cuando se requiere hacer una cantidad considerable de búsquedas y es importante el factor tiempo.

Clasificación de Métodos

Según lugar físico donde residen los datos

Ordenamiento Interno:

Es aquel que ocurre sobre estructuras de datos residentes en memoria principal. Entre otros se tiene al de Inserción directa, Selección directa.

Ordenamiento Externo:

Es aquel que ocurre sobre estructuras de datos residentes en memoria secundaria. Entre otros se tiene al método de Intercalación, Mezcla directa, Mezcla Equilibrada.

Según movimientos de claves de ordenamiento

Ordenamiento Estable:

Es aquel que una vez efectuado mantiene el orden relativo de dos o más registros cuyo criterio de ordenamiento es el mismo. Es beneficioso para ordenamientos en donde se utilice más de un criterio de ordenamiento.

Ordenamiento No estable:

Es aquel que una vez efectuado pierde el orden relativo de dos o más registros cuyo criterio de ordenamiento es el mismo.

Page 102: Estructura de Datos

Estructura de Datos UCENM 102

Comparación de los Métodos de Ordenamiento

Una alternativa sería comparar tiempos de ejecución, sin embargo, esto sólo sería una medida de eficiencia sobre un computador en particular. (dependiendo la arquitectura de la máquina, set de instrucciones etc.).

Mayor validez tendría aislar la operación básica realizada en el algoritmo y determinar el n° de veces que se realiza. Por ejemplo, si estuviéramos sumandos los elementos de un arreglo podríamos contabilizar el n¼ de operaciones realizadas para un arreglo de tamaño n, así como en este caso el número de operaciones realizadas es función del número de elementos del arreglo.

Usualmente se utiliza la notación O( ) para indicar el grado u orden del algoritmo que está dado en términos de comparaciones realizadas. Esto representa cuantitativamente la complejidad de un algoritmo.

Las complejidades más usuales son O(n), O(n2), O(n3), O(log2n). Para que se entienda mejor:

O(n): Llenar un arreglo lineal O(n2):Llenar un arreglo bidimensional O(n3):Llenar un arreglo Tridimensional O(log2n):Recorrer un árbol binario de búsqueda

Búsqueda Secuencial

Comenzando desde el inicio de la lista se busca registro a registro (elemento a elemento) hasta encontrar el elemento buscado o en su defecto terminar de revisar toda la estructura. Este tipo de búsqueda es aplicable a listas secuenciales(arreglos) y listas enlazadas.

El siguiente algoritmo realiza la búsqueda de un campo clave sobre un arreglo de registros.

Procedure Busqueda_Secuencial(Lista: Tipo_array;Numelementos: integer; Valclave: Tipo_clave; var posición: integer); Var índice: integer; Encontrado: boolean; Begin

Indice:=1; Encontrado:= FALSE;

Page 103: Estructura de Datos

Estructura de Datos UCENM 103

While NOT ENCONTRADO AND indice <= Numelementos do begin if lista[indice].clave = valclave then ENCONTRADO:= TRUE else indice:=indice+1; end;

if ENCONTRADO then Posicion :=indice else posicion:=0;

end;

Consideraciones

En el peor de los casos, deberíamos buscar en el último registro de la lista o sobre uno no existente. O(N)

En promedio, si supusiéramos igual probabilidad de búsqueda para cualquier item, se harían N/2 comparaciones (mitad de la lista)

Si la lista estuviese ordenada (en base al campo clave) la eficiencia del algoritmo anterior se vería mejorada ya que detendríamos nuestra búsqueda cuando se haya sobrepasado su posición lógica dentro de la lista.

Algoritmos de ordenamiento: Internos:

Inserción directa. o Inserción directa. o Inserción binaria.

Selección directa. o Selección directa.

Intercambio directo. o Burbuja. o Shake.

Inserción disminución incremental. o Shell.

Ordenamiento de árbol. o Heap. o Tournament.

Sort particionado. o Quick sort.

Merge sort.

Radix sort.

Cálculo de dirección. Externos:

Page 104: Estructura de Datos

Estructura de Datos UCENM 104

Straight merging.

Natural merging.

Balanced multiway merging.

Polyphase sort.

Distribution of initial runs.

Clasificación de los algoritmos de ordenamiento de información: El hecho de que la información está ordenada, nos sirve para poder encontrarla y accesarla de manera más eficiente ya que de lo contrario se tendría que hacer de manera secuencial. A continuación se describirán 4 grupos de algoritmos para ordenar información:

Algoritmos de inserción: En este tipo de algoritmo los elementos que van a ser ordenados son considerados uno a la vez. Cada elemento es INSERTADO en la posición apropiada con respecto al resto de los elementos ya ordenados. Entre estos algoritmos se encuentran el de INSERCION DIRECTA, SHELL SORT, INSERCION BINARIA y HASHING.

Algoritmos de intercambio: En este tipo de algoritmos se toman los elementos de dos en dos, se comparan y se INTERCAMBIAN si no están en el orden adecuado. Este proceso se repite hasta que se ha analizado todo el conjunto de elementos y ya no hay intercambios. Entre estos algoritmos se encuentran el BURBUJA y QUICK SORT.

Algoritmos de selección: En este tipo de algoritmos se SELECCIONA o se busca el elemento más pequeño (o más grande) de todo el conjunto de elementos y se coloca en su posición adecuada. Este proceso se repite para el resto de los elementos hasta que todos son analizados. Entre estos algoritmos se encuentra el de SELECCION DIRECTA.

Algoritmos de enumeración: En este tipo de algoritmos cada elemento es comparado contra los demás. En la comparación se cuenta cuántos elementos son más pequeños que el elemento que se está

Page 105: Estructura de Datos

Estructura de Datos UCENM 105

analizando, generando así una ENUMERACION. El número generado para cada elemento indicará su posición. Los métodos simples son: Inserción (o por inserción directa), selección, burbuja y shell, en dónde el último es una extensión al método de inserción, siendo más rápido. Los métodos más complejos son el quick-sort (ordenación rápida) y el heap sort. A continuación se mostrarán los métodos de ordenamiento más simples.

Método de Inserción. Este método toma cada elemento del arreglo para ser ordenado y lo compara con los que se encuentran en posiciones anteriores a la de él dentro del arreglo. Si resulta que el elemento con el que se está comparando es mayor que el elemento a ordenar, se recorre hacia la siguiente posición superior. Si por el contrario, resulta que el elemento con el que se está comparando es menor que el elemento a ordenar, se detiene el proceso de comparación pues se encontró que el elemento ya está ordenado y se coloca en su posición (que es la siguiente a la del último número con el que se comparó).

Procedimiento Insertion Sort Este procedimiento recibe el arreglo de datos a ordenar a[] y altera las posiciones de sus elementos hasta dejarlos ordenados de menor a mayor. N representa el número de elementos que contiene a[]. paso 1: [Para cada pos. del arreglo] For i <- 2 to N do paso 2: [Inicializa v y j] v <- a[i] j <- i. paso 3: [Compara v con los anteriores] While a[j-1] > v AND j>1 do paso 4: [Recorre los datos mayores] Set a[j] <- a[j-1], paso 5: [Decrementa j] set j <- j-1. paso 5: [Inserta v en su posición] Set a[j] <- v. paso 6: [Fin] End. Ejemplo: Si el arreglo a ordenar es a = ['a','s','o','r','t','i','n','g','e','x','a','m','p','l','e'], el algoritmo va a recorrer el arreglo de izquierda a derecha. Primero toma el segundo dato 's' y lo asigna a v y i toma el valor de la posición actual de v.

Page 106: Estructura de Datos

Estructura de Datos UCENM 106

Luego compara esta 's' con lo que hay en la posición j-1, es decir, con 'a'. Debido a que 's' no es menor que 'a' no sucede nada y avanza i. Ahora v toma el valor 'o' y lo compara con 's', como es menor recorre a la 's' a la posición de la 'o'; decrementa j, la cual ahora tiene la posición en dónde estaba la 's'; compara a 'o' con a[j-1] , es decir, con 'a'. Como no es menor que la 'a' sale del for y pone la 'o' en la posición a[j]. El resultado hasta este punto es el arreglo siguiente: a = ['a','o','s','r',....] Así se continúa y el resultado final es el arreglo ordenado : a = ['a','a','e','e','g','i','l','m','n','o','p','r','s','t','x']

MÉTODO DE SELECCIÓN. El método de ordenamiento por selección consiste en encontrar el menor de todos los elementos del arreglo e intercambiarlo con el que está en la primera posición. Luego el segundo mas pequeño, y así sucesivamente hasta ordenar todo el arreglo.

Procedimiento Selection Sort

paso 1: [Para cada pos. del arreglo] For i <- 1 to N do paso 2: [Inicializa la pos. del menor] menor <- i paso 3: [Recorre todo el arreglo] For j <- i+1 to N do paso 4: [Si a[j] es menor] If a[j] < a[menor] then paso 5: [Reasigna el apuntador al menor] min = j paso 6: [Intercambia los datos de la pos. min y posición i] Swap(a, min, j). paso 7: [Fin] End. Ejemplo: El arreglo a ordenar es a = ['a','s','o','r','t','i','n','g','e','x','a','m','p','l','e']. Se empieza por recorrer el arreglo hasta encontrar el menor elemento. En este caso el menor elemento es la primera 'a'. De manera que no ocurre ningún cambio. Luego se procede a buscar el siguiente elemento y se encuentra la segunda 'a'. Esta se intercambia con el dato que está en la segunda posición, la 's', quedando el arreglo así después de dos recorridos: a = ['a','a','o','r','t','i','n','g','e','x','s','m','p','l','e'].

Page 107: Estructura de Datos

Estructura de Datos UCENM 107

El siguiente elemento, el tercero en orden de menor mayor es la primera 'e', la cual se intercambia con lo que está en la tercera posición, o sea, la 'o'. Le sigue la segunda 's', la cual es intercambiada con la 'r'. El arreglo ahora se ve de la siguiente manera: a = ['a','a','e','e','t','i','n','g','o','x','s','m','p','l','r']. De esta manera se va buscando el elemento que debe ir en la siguiente posición hasta ordenar todo el arreglo. El número de comparaciones que realiza este algoritmo es : Para el primer elemento se comparan n-1 datos, en general para el elemento i-ésimo se hacen n-i comparaciones, por lo tanto, el total de comparaciones es: la sumatoria para i de 1 a n-1 (n-i) = 1/2 n (n-1).

MÉTODO BURBUJA. El bubble sort, también conocido como ordenamiento burbuja, funciona de la siguiente manera: Se recorre el arreglo intercambiando los elementos adyacentes que estén desordenados. Se recorre el arreglo tantas veces hasta que ya no haya cambios. Prácticamente lo que hace es tomar el elemento mayor y lo va recorriendo de posición en posición hasta ponerlo en su lugar.

Procedimiento Bubble Sort

paso 1: [Inicializa i al final de arreglo] For i <- N down to 1 do paso 2: [Inicia desde la segunda pos.] For j <- 2 to i do paso 4: [Si a[j-1] es mayor que el que le sigue] If a[j-1] < a[j] then paso 5: [Los intercambia] Swap(a, j-1, j). paso 7: [Fin] End. Tiempo de ejecución del algoritmo burbuja:

Para el mejor caso (un paso) O(n)

Peor caso n(n-1)/2

Promedio O(n2)

Page 108: Estructura de Datos

Estructura de Datos UCENM 108

MÉTODO DE SHELL. Ordenamiento de disminución incremental. Nombrado así debido a su inventor Donald Shell. Ordena subgrupos de elementos separados K unidades (respecto de su posición en el arreglo) del arreglo original. El valor K es llamado incremento. Después de que los primeros K subgrupos han sido ordenados (generalmente utilizando INSERCION DIRECTA), se escoge un nuevo valor de K más pequeño, y el arreglo es de nuevo partido entre el nuevo conjunto de subgrupos. Cada uno de los subgrupos mayores es ordenado y el proceso se repite de nuevo con un valor más pequeño de K. Eventualmente el valor de K llega a ser 1, de tal manera que el subgrupo consiste de todo el arreglo ya casi ordenado. Al principio del proceso se escoge la secuencia de decrecimiento de incrementos; el último valor debe ser 1. "Es como hacer un ordenamiento de burbuja pero comparando e intercambiando elementos." Cuando el incremento toma un valor de 1, todos los elementos pasan a formar parte del subgrupo y se aplica inserción directa. El método se basa en tomar como salto N/2 (siendo N el número de elementos) y luego se va reduciendo a la mitad en cada repetición hasta que el salto o distancia vale 1.

Procedimiento Shell Sort; const MAXINC = _____; incrementos = array[1..MAXINC] of integer; var j,p,num,incre,k:integer; begin for incre := 1 to MAXINC do begin /* para cada uno de los incrementos */

Page 109: Estructura de Datos

Estructura de Datos UCENM 109

k := inc[incre]; /* k recibe un tipo de incremento */ for p := k+1 to MAXREG do begin /* inserción directa para el grupo que se encuentra cada K posiciones */ num := reg[p]; j := p-k; while (j>0) AND (num < reg[j]) begin reg[j+k] := reg[j]; j := j - k; end; reg[j+k] := num; end end end; Ejemplo: Para el arreglo a = [6, 1, 5, 2, 3, 4, 0] Tenemos el siguiente recorrido:

Recorrido Salto Lista Ordenada Intercambio

1 3 2,1,4,0,3,5,6 (6,2), (5,4), (6,0)

2 3 0,1,4,2,3,5,6 (2,0)

3 3 0,1,4,2,3,5,6 Ninguno

4 1 0,1,2,3,4,5,6 (4,2), (4,3)

5 1 0,1,2,3,4,5,6 Ninguno

Page 110: Estructura de Datos

Estructura de Datos UCENM 110

ARCHIVOS

Definición y Componentes

Es una es estructura de datos que reside en memoria secundaria o almacenamiento permanente (cinta magnética, disco magnético, disco óptico, disco láser, etc.). La forma de clasificación más básica se realiza de acuerdo al formato en que residen estos archivos, de esta forma hablamos de archivos ASCII (de texto) y archivos binarios.

Definición archivo binario:

Estructura de datos permanente compuesto por registros (filas) y éstos a su vez por campos (columnas). Se caracteriza por tener un tipo de dato asociado, el cual define su estructura interna.

Definición archivo texto:

Estructura de datos permanente no estructurado formado por una secuencia de caracteres ASCII.

Tipos de Acceso a los Archivos

Secuencial

Se accesan uno a uno los registros desde el primero hasta el último o hasta aquel que cumpla con cierta condición de búsqueda. Se permite sobre archivos de Organización secuencial y Secuencial Indexada.

Random

Se accesan en primera instancia la tabla de índices de manera de recuperar la dirección de inicio de bloque en donde se encuentra el registro buscado. Se permite para archivos con Organización Sec.Indexada.

Dinámico

Se accesan en primera instancia la tabla de índices de manera de recuperar la dirección de inicio de bloque en donde se encuentra el registro buscado. Se permite para archivos con Organización Sec.Indexada.

Page 111: Estructura de Datos

Estructura de Datos UCENM 111

Directo

Es aquel que utiliza la función de Hashing para recuperar los registros. Sólo se permite para archivos con Organización Relativa.

Clasificación de los Archivos Según su Uso

La siguiente tabla resume las distintas denominaciones dadas a los archivos según la utilización dada:

Tipo Definición Ejemplo

Maestros Perduran durante todo el ciclo de vida de los sistemas.

Archivo de empleados en un sistema de Remuneraciones.

Transaccionales Se utilizan para actualizar otros archivos. Pueden ser eliminados al término de este proceso o conservados como respaldos.

Archivo de ventas diarias en un sistema de ventas.

De respaldo Son copias de seguridad de otros archivos

De paso Son creados y eliminados entro de un proceso computacional.

Históricos Registran acontecimientos a través del tiempo.

Registros de movimientos diarios de CC en un Sistema Bancario de CC.

De referencia Corresponden a los archivos de consultas de parámetros.

Registro % imposiciones % descuentos isapre, etc.

Informes o Reportes

Son respuestas del sistema computacional cuyos contenidos deben ser interpretados por personas. Pueden ser en forma escrita, por pantalla e incluso hacia algún archivo con formato editable.

Planillas de sueldos en un sistema de Remuneraciones.

Page 112: Estructura de Datos

Estructura de Datos UCENM 112

Organizaciones Básicas de Archivos

Secuencial

Es la organización más común. Los registros se almacenan uno tras otro por orden de llegada. Para acceder un registro determinado se deben leer todos los registros que están almacenados antes que él.

Se recomienda: para archivos de procesamiento batch (aquellos para los cuales se debe realizar la misma operación sobre la mayor parte de sus registros)

Secuencial Indexada

Los registros son almacenados en una distribución tal que permiten ser consultados a través de índices. En ellos se distinguen 3 áreas de almacenamiento:

De índices: Mecanismo de acceso a los registros de datos.

Primaria o de datos: En donde se encuentran los datos propiamente tal.

De overflow: Encargada de recibir aquellos registros que no pueden ser almacenados en el área primaria.

Se recomienda : Para archivos de consulta

Relativa

Existe una relación directa entre la ubicación lógica de un registro y su ubicación física. Para ello necesita una función matemática que asigne a un valor de clave una dirección física única. El encontrar una Fh óptima es tarea compleja por lo que esta organización es poco utilizada.

Se recomienda: Para archivos en donde sea crucial minimizar tiempos de acceso.

Operaciones Básicas de Archivos

Consiste ordenar el archivo a partir de uno o más criterios impuestos sobre sus campos. Es normal que los archivos estén ordenados por clave o por algún campo alfabético ascendente o descendentemente

Page 113: Estructura de Datos

Estructura de Datos UCENM 113

Concatenación

Consiste en unir 2 ó más archivos en uno (uno a continuación del otro).

Antes de...

Después de...

Intercalación

Consiste en generar un archivo ordenado según algún criterio preestablecido a partir de dos ó más archivos que pueden o no encontrarse en la misma secuencia.

Page 114: Estructura de Datos

Estructura de Datos UCENM 114

Antes de...

Después de...

Pareamiento

Consiste en ubicar registros correspondientes entre 2 ó más archivos. Consiste en ubicar aquellos pares de registros pertenecientes a dos archivos diferentes que cumplen una cierta relación de correspondencia.

Edición

Consiste en preparar los datos que constituyen a uno ó más archivos para que sean interpretados por personas. Se asocia generalmente a la generación de listados en papel impreso.

Page 115: Estructura de Datos

Estructura de Datos UCENM 115

Validación

Consiste en asegurar la veracidad e integridad de los datos que ingresan a un archivo. Existen numerosas técnicas de validación tales como: Dgito verificador, chequeo de tipo, chequeo de rango.

Actualización

Consiste en agregar y eliminar registros desde un archivo, así como también modificar el valor de uno ó más campos. También se conoce como mantenimiento de uno o más archivos.

Bases de Datos

Una base de datos es un conjunto de archivos o tablas relacionadas almacenados en forma estructurada. Está formada además por consultas e índices.

Existen ciertos enfoques para el diseño de BD: Relacional, Jerárquica y de Red.

Las aplicaciones que permiten administrar una base de datos se denominan DBMS (Data Base Manager System), entre ellos podemos citar a: Access, SQL Server, ORACLE Server etc. Estas ultimas se conocen también como RDBMS (Relational Data Base Manager System) ya que siguen el enfoque relacional.

En empresas de gran envergadura, se habla de Base de Datos Corporativa conformada por múltiples modelos de bases de datos pertenecientes a distintas instancias e interrelacionados entre sí.

En general, el diseño de las bases de datos se rige según ciertas normas, algunas creadas por E. Codd permiten evitar redundancias, e inconsistencias como consecuencia del proceso de actualización. Estas reglas son las denominadas FORMAS NORMALES.

Ejemplo: Esquema físico relacional

P: Primary Key F: Foreing Key

Page 116: Estructura de Datos

Estructura de Datos UCENM 116

Notar que existe una relación entre las tablas reparación y producto representada la tabla relación prod_reparpor, debido a que en una reparación se pueden utilizar varios productos como insumos y éstos a su vez pueden participar en más de una reparación.

Formas Normales

Las formas normales definidas en la teoría de bases de datos relacionales representan lineamientos para el diseño de registros. Las reglas de normalización están dirigidas a la prevención de anomalías de actualización y de inconsistencias en los datos. Ellas no reflejan ninguna consideración de rendimiento. En cierta forma, pueden ser visualizadas como orientadas por el supuesto de que todos los campos no-clave serán actualizados frecuentemente. No hay obligación de normalizar completamente todas las relaciones cuando se deben considerar aspectos de rendimiento (tiempo de acceso por consulta).

Finalmente, podemos decir que estas formas normales también son aplicables al diseño de archivos tradicionales con clave, con lo que abarcaríamos todos los sistemas.

Aquí enunciamos las 2 primeras formas normales:

Primera forma normal

Bajo la primera formal normal, todas las ocurrencias de un registro deben contener el mismo número de campos. La primera forma normal excluye campos de repetición variable.

Ejemplo: Archivo = LIBROS Registro = Reg_libros

Este registro transgrede la primera forma normal, debido a que pueden existir varios autores para un mismo libro (igual sucede con el campo materia). Una solución a este problema separa los campos "conflictivos", agrupándolos en otras tablas:

Cod_lib Nom_lib Autor Num_cop Materia Idioma Precio

Cod_lib Nom_lib Num_cop Materia Idioma Precio

Cod_lib Autor

Page 117: Estructura de Datos

Estructura de Datos UCENM 117

Segunda forma normal

Todo campo no clave debe depender directamente del campo clave.

Ejemplo:

Articulo Bodega Cantidad Direc_bod

La clave está compuesta de 2 campos ARTICULO y BODEGA, sin embargo, DIRECCION-BODEGA, está relacionado exclusivamente con BODEGA. Los problemas básicos de diseño son: La dirección de la bodega se repite en cada registro para cada parte almacenada en esa bodega (redundancia).Si la dirección de la bodega cambia, cada registro que se refiera a una parte almacenada en esa bodega debe ser actualizado. Debido a la redundancia, los datos pueden llegar a ser inconsistentes, con diferentes registros indicando diferentes direcciones para la misma bodega (integridad). Si en algún momento no hubiera partes almacenadas en alguna bodega, no habría un registro para anotar la dirección de la bodega (anomalía).

Page 118: Estructura de Datos

Estructura de Datos UCENM 118

Glosario A ASCII "Código Americano de Normalización para el Intercambio de Información". Un sistema de codificación para convertir caracteres del teclado e instrucciones en el código de número binario, el que entiende el computador. B BIBLIOTECA Es el archivo que contiene las funciones estándares que se pueden usar en los programas. Estas funciones incluyen todas las operaciones de entrada/salida (E/S), manejo de pantalla en modo texto o modo gráfico así como otras rutinas útiles. BIT La porción más pequeña de información "representable" en un computador. Es un 0 ó 1. Mediante el lenguaje de los computadores se interpretan series de 1 y 0's para formar símbolos, signos de puntuación, caracteres y números. BYTE Corresponde a un set de 8 bits que representa un carácter de dato. Es la unidad más pequeña "referenciable" en un computador. C CACHÉ Una porción de memoria en donde se ubica la información utilizada con mayor frecuencia para permitir un acceso más rápido. CASE "Computer Aided Software Design". Herramienta Computacional que apoya a alguna de las fases del desarrollo del Sw CÓDIGO FUENTE Es el texto que un usuario puede leer. Normalmente considerado como el programa.

Page 119: Estructura de Datos

Estructura de Datos UCENM 119

CÓDIGO OBJETO Es la traducción del código fuente de un programa a código máquina, que es el que la Computadora puede leer y ejecutar directamente. El Código Objeto es la entrada al enlazador. CUADRO DE DIÁLOGO Ventana que aparece y requiere entrada de datos por parte del usuario (las más básicas muestran sólo un mensaje). Cuando el usuario completa la información necesaria o hace clic en los botones correctos, el cuadro de diálogo desaparece. E EFICACIA Se dice que un proceso es eficaz, si capaz de cumplir sus objetivos. EFICIENCIA Se dice que un proceso es eficiente, si es eficaz y además utiliza un mínimo de recursos. ENLAZADOR Es un programa que enlaza funciones compiladas por separado para producir un solo programa (Combina las funciones de biblioteca estándar con el código que se ha escrito). La salida del Enlazador es un programa ejecutable. G GIGA BYTE (GB) 1024 x 1024 x 1024 bytes I IDENTIFICADOR Nombre asignado a una variable, procedimiento o función dentro del código fuente. M MEGA BYTE (MB) 1024 x 1024 bytes.

Page 120: Estructura de Datos

Estructura de Datos UCENM 120

O OOP (OBJECT ORIENTED PROGRAMMING) La programación orientada a objetos es una modalidad de programación usada para escribir programas como una colección de módulos y objetos. En lugar de escribir los programas línea por línea, se escriben programas con módulos prescritos. Los módulos disimulan la complejidad del lenguaje de programación y facilitan el proceso de copiar las instrucciones de un programa a otro. A pesar de que la programación orientada a objetos es mucho más sencilla de aprender y aplicar que otros lenguajes, es común que estos programas sean un poco más lentos, aunque con las rápidas computadoras de hoy, la diferencia en velocidad cada vez es menos notoria. p PORTABILIDAD [TRANSPORTABILIDAD]: Capacidad que tiene un programa o lenguaje para ser cambiado de plataforma con el mínimo número de modificaciones. Tiene relación directa con la independencia que posee el lenguaje de la máquina. PROGRAMACIÓN MODULAR Los programas complejos se descomponen en módulos (partes independientes), que estos a su vez se analizan, codifican y se verifican por separado. Su codificación se realiza mediante programación estructurada u orientada al objeto. PANEL DE CONTROL En plataforma Windows de Microsoft y en Macintosh, se refiere a una ventana en la cual se pueden hacer selecciones sobre ciertas características del ambiente en el que se trabaja. Por ejemplo, definir el color del fondo en la pantalla o la velocidad del cursor, así como elegir qué figuras queremos que aparezcan en el monitor mientras la computadora está prendida en los momentos en que no estamos trabajando. PATH Un mapa que le dice a su computadora dónde encontrar cierta información. Usando el sistema operativo DOS, usted puede utilizar esta instrucción para ejecutar un programa desde cualquier directorio sin necesidad de hallarse físicamente en el directorio donde se encuentra el programa. PROGRAMAS DE UTILERIAS (Utilitarios) Son programas auxiliares que ayudan a manejar archivos, a obtener información acerca de la computadora, a diagnosticar o reparar fallas comunes, o bien a mantener el sistema

Page 121: Estructura de Datos

Estructura de Datos UCENM 121

Operativo operando eficientemente. Los programas más populares de este tipo son el PC Tools y el Norton Utilities. PROTOCOLO Normas y estándares para transferir información entre computadores conectados en red. PRUEBA ALFA El primer pasó de prueba de verificación para productos de cómputo antes de que éstos sean lanzados al mercado. Normalmente, los fabricantes de equipo o bien los encargados de la mercadotecnia de los programas son los responsables de llevar a cabo estas pruebas. Más adelante, se aplica la prueba beta en la cual ya participa un grupo seleccionado de usuarios. T TERA BYTE (TB) 1024 x 1024 x 1024 x 1024 bytes TIEMPO DE EJECUCIÓN Se refiere a lo que sucede mientras el programa se está ejecutando realmente. Normalmente, estos términos aparecerán cuando se consideren posibles errores, encontrándose como "Errores de tiempo de Compilación" y "Errores de tiempo de ejecución". TRADUCTOR Se refiere a un programa que se encarga de convertir lenguajes de un nivel superior a otro inferior. Según el tipo de lenguaje se puede tratar de un traductor: Compilador o un traductor: intérprete.

Page 122: Estructura de Datos

Estructura de Datos UCENM 122

BIBLIOGRAFÍA

Nombre del libro: Matemáticas discretas y combinatoria.

Autor: Ralph P. Grimaldi

Editorial: Addison Wesley Longman

Sitios en Internet:

www.altavista.com

www.elrincondelvago.com

www.aulaclick.com

www.monografias.com

www.wikipedia.com

http://dgicii.mty.itesm.mx:8095/%7Empadilla/Estructura/abstrac.htm http://guiafe.com.ar/aedd/teoria.htm http://www.programmersheaven.com http://www.cc.ece.ntua.gr/%7Egkaval/applets/sortdemo.html