v 1.0b
Leandro Rabindranath León [email protected]
Centro de Estudios en Microelectrónica y Sistemas Distribuidos Universidad de Los Andes
Quisiera que este texto se considerase como un vademecum en el diseño efectivo y en la programación de algoritmos y de estructuras de datos que soporten programas computa-cionales. Vademécum es una palabra latina construida mediante el imperativo latín vade que connota ven, anda o camina, y mecum, que signica conmigo. Aprender es parecido a transitar un camino; enseñar es parecido a mostrárselo a un peregrino. En-señar proviene del latín insign are, verbo compuesto de in (en) y signare (señalar); la expresión aún se emplea cuando se señala con el dedo una senda a seguir. Así, un vade-mecum pretende ser una guía de tránsito por una senda de aprendizaje; en este caso, la de programación de computadoras.
Modo y medios
Permítaseme introducir el modo de enseñanza de este texto, expresado por este antiquísimo proverbio oriental:
Cuando escucho, olvido, Cuando veo, recuerdo, Cuando hago, entiendo.
La apropiación efectiva de un conocimiento ocurre cuando éste se entiende. El en-tendimiento se torna realidad cuando un aprendiz consuma la hechura de cosas caracterís-ticas de su práctica haciéndolas él mismo. Dicho de otra manera, el modo de enseñanza es mostrar enteramente cómo se elabora una estructura de datos y un algoritmo. Bajo este modo intentaré guiar a un potencial aprendiz por el camino de la práctica de la programación avanzada. Para ello empleo algunos medios.
C++
Para representar los algoritmos y las especicaciones de estructuras de datos empleo el
lenguaje de programación C++.
Esta decisión no se tomó sin dignas objeciones, las cuales, resumidamente, se pueden clasicar en dos grupos. El primero cuestiona el eventual desconocimiento del lector
sobre el lenguaje C++. A esto debo replicar que no sólo C++ es uno de los lenguajes más
populares e importantes de la programación de sistemas, sino que su sintaxis y semántica son reminiscentes a la mayoría de los otros lenguajes procedurales y a objetos. De hecho, en
el quórum actual de los lenguajes de programación procedurales no sólo domina C++, sino
que el resto de los lenguajes se inspiran en él o en su precursor, el lenguaje C; por instancias, java, python, perl, C# y D.
El segundo tipo de objeción denuncia que la comprensión se torna más dicultosa, a lo cual replico con dos argumentos. El primero es que los lenguajes de programación se
pensaron también en un estilo coloquial1 respecto a la programación. Consideremos,
por ejemplo, el siguiente pseudoprograma en un pseudolenguaje castellano:
Repita mientras m > 0 y m < n si a[m] < a[x] entonces
m = m/2; de lo contrario
m = 2*m fin si
Fin repita mientras
el cual es equivalente a la traducción literal, coloquial, del siguiente bloque en C++:
while (m > 0 and m < n) { if (a[m] < a[x]) m = m/2; else m = 2*m; }
Aunque un ejemplo no basta para generalizar mi defensa sobre el uso del lenguaje C++,
el hecho es que cualquiera que considere seriamente la programación tendrá que progra-mar y esto deberá hacerlo en un lenguaje de programación, el cual, para bien o mal, fue pensado en lengua inglesa. Por tanto, inevitablemente, un aprendiz hispanoparlante de-berá programar en un lenguaje de programación anglizado. Hecha la acotación anterior,
también podemos decir que el programa en castellano y su equivalente en C++ tienen el
mismo signicado. Así pues, es dudoso, cuando menos, armar que la comprensión se torna más dicultosa.
Una bondad que se atribuye al uso de un pseudolenguaje es que éste es independiente de la implementación. En mi criterio, esto es parcialmente correcto. Por ejemplo, las
plantillas en C++ plantean un problema de portatibilidad en otros lenguajes que no las
tienen. Alguien podría aducir que el uso de plantillas son crípticas para el aprendiz. Pero esta objeción sólo tiene sentido si no se comprende el concepto y n de una plantilla, cual no es otro que la genericidad. Entendido esto, un programa con plantillas es tanto o más genérico que uno expresado en un pseudolenguaje; y resulta que ½este es el principal argumento de quienes deenden la enseñanza de programación en un pseudolenguaje!. De todos modos, si se usase un pseudolenguaje, entonces también habría que plantearse el
problema de traducir a un lenguaje de programación real2. Mi segundo argumento estriba
en que, habida cuenta de la popularidad y transcendencia del C++, creo que es más fácil
traducir un programa bien estructurado en C++ hacia otro lenguaje, por ejemplo, java,
que uno realizado en un pseudolenguaje3.
1Es imposible ser completamente coloquial en un lenguaje de programación. 2Una traducción con más esfuerzo si se trata de un pseudolenguaje en castellano.
3De hecho, muchos textos de programación aparecen en diferentes ediciones para distintos lenguajes.
En muchos de estos casos, el autor escribe los programas en un solo lenguaje y luego emplea traductores hacia el otro lenguaje. Tales son los casos de Sedgewick o Weiss y sus series en C, C++y java
Biblioteca ALEPH
Este texto contiene en sí mismo una implementación concreta de una biblioteca, libre, llamada ALEPH, contentiva de todas las estructuras de datos y algoritmos tratados en este texto.
La lectura posibilita la del fuente de la biblioteca y revela aspectos de su instrumenta-ción. Salvo errores aún no descubiertos o mejoras que cualquiera desee proponer, el lector tiene la posibilidad de apoyarse en una implantación concreta que le facilite la apropiación de sus conocimientos.
Los códigos fuentes de ALEPH están disponibles en:
http://webdelprofesor.ula.ve/ingenieria/lrleon/aleph.tbz Ellos fueron automáticamente indentados con bcpp [17].
A lo largo de mi experiencia como enseñante he intentado recompensar a los descubri-dores de errores y proponentes de mejoras con una ponderación en su calicación. No veo ningún impedimento a que un tercero aplique lo mismo si este texto se usa como material instruccional. Si algún lector fuera de mi círculo de enseñanza desea reportar algún error o hacer alguna mejora, entonces le agradezco que lo reporte al email [email protected].
La documentación de la biblioteca está disponible en el enlace: http://webdelprofesor.ula.ve/ingenieria/lrleon/aleph/html Ésta fue escrita para procesarse con el excelente programa doxygen [38].
Programación literaria
En la elaboración de este texto se utilizó un sistema llamado noweb [147, 86], el cual es
un sistema programado que genera este texto y los fuentes en C++ a partir de un solo
archivo fuente. El fuente entrelaza prosa explicativa con bloques de código de la implantación de la biblioteca.
Este estilo de escritura de programas se denomina programación literaria y fue pro-puesto por Knuth [96]. La idea es presentar un estilo más coloquial para escribir programas que el mero estilo deformado de un lenguaje de programación, junto con la ganancia de que la documentación y la última versión del programa (presumiblemente correcta) residan en un sólo fuente.
Aparte de código en C++, los bloques pueden contener referencias a otros bloques. Una
denición de bloque comienza por su nombre entre paréntesis angulares. Por ejemplo, consideremos determinar si un número n es o no primo. Para ello, podemos denir el siguiente bloque:
iii hCalcular si n es primo iiii≡
if (n <= 2)
// n es un número primo
const int raíz_de_n = static_cast<int>(ceil(sqrt(n))); for (int i = 3; i < raíz_de_n; i += 2)
if (n % i == 0) // n no es primo // n es un número primo
Los bloques se enumeran según el número de la página en que se denen por primera vez. Si hay más de un bloque en una página, entonces a éste se le añade una letra que, en el orden alfabético, se corresponde con su orden de aparición. Algunos bloques referencian a otros bloques o variables.
Los bloques están escritos en un orden que -se cree- es preferible para la comprensión que el mero listado de código. Así, agradeceré toda crítica que se me pueda hacer sobre el estilo y orden de presentación de una estructura de dato o algoritmo en programación li-teraria, pero solicito al crítico un esfuerzo primigenio por comparar el estilo noweb con el listado plano de código y, entonces, bajo esa consideración, hacer su crítica.
Un instrumento que podría ser útil es el índice de identicadores de código (pag. ??), el cual que se encuentra en el apéndice y contiene los identicadores de clases y métodos denidos a lo largo del texto. Para cada identicador, se ubica en subrayado el número de página dónde fue denido y los números de las páginas de segmentos de código que le hacen referencia.
Puede decirse que este texto contiene la implantación de cuanta estructura de datos o algoritmo se estudie. Empero, tomarse esto al pie de la letra acarrearía bastante papel; además de que haría este texto más voluminoso y repetitivo. En ese sentido, en pro del espacio, se han hecho cortes de dos tipos:
1. Manejo de excepciones: aunque este aspecto constituye una de las bondades más
elegantes de C++, este texto casi no los presenta.
2. Métodos de clases repetitivos o muy simples.
En ambos casos, el código de la biblioteca, directamente generado a partir del fuente noweb de este texto, está completo; es decir, la biblioteca contiene los manejadores de excepciones y métodos omitidos en este texto.
Ejercicios
Cualquiera que sea la práctica, no hay otra manera de que un practicante consume sus conocimientos que no sea haciendo práctica. Por más esfuerzo al orientarle y enseñarle, el aprendiz debe, preferiblemente guiado por un maestro, ejercitar por sí solo lo aprendido.
Hay tres maneras, que no deben ser excluyentes, de llevar a cabo lo anterior:
1. Resolución de ejercicios propuestos: en este sentido, al nal de cada capítulo se presenta un conjunto de ejercicios destinados primordialmente a la resolución en solitario por parte del aprendiz.
Algunos ejercicios son teóricos en el sentido de que no necesariamente requieren es-cribir un programa compilable. Por supuesto, no está prohibido sentarse frente al computador e intentar concretar el ejercicio mediante un programa. Otros ejercicios son prácticos y consisten en extensiones a la biblioteca. Estos ejercicios son distin-guibles de los teóricos porque enuncian su resultado en términos de un objeto de la biblioteca.
Los ejercicios están clasicados según una dicultad subjetiva juzgada por mí. No es fácil ponderar el tiempo de resolución de un ejercicio, pues éste depende del estudiante, pero he aquí mi clasicación:
(a) Ninguna cruz denota a un ejercicio considerado sencillo. Los teóricos deberían de resolverse en el orden de cinco minutos, mientras que los prácticos en el de un día. (b) Una cruz (+) expresa un tiempo estimado de una a dos horas en el caso de un
ejercicio teórico y de dos días en el práctico.
(c) Dos cruces (++) expresan ya una dicultad mucho mayor. Por lo general, esta clase de ejercicios plantean al aprendiz un descubrimiento o revelación de un truco o técnica que, una vez revelada, debe reducir la complejidad del ejercicio a ninguna cruz.
El tiempo de un ejercicio teórico debe ser al menos de un día, mientras que el de uno práctico será de tres (3).
(d) Tres cruces (+++) representan una ejercicio de élite, difícil aún para un maestro, cuyo tiempo de resolución no está delimitado.
2. Ejecución de ejercicios guiados en laboratorio: uno de los grandes obstáculos que lo abstracto plantea al aprendiz -uno diría que ello ocurre en cualquier práctica-, es que su condición novicia le diculta aceptar que lo que para él puede ser en principio abstracto se convertirá, a través del ejercicio, en concreto.
Dicho lo anterior, puede decirse que el sentido de un ejercicio en laboratorio es permitirle al estudiante iniciar la concreción de sus conocimientos. A tal n, en el laboratorio, pueden llevarse a cabo tres actividades:
(a) Contemplación de un problema computacional, junto con su solución, en el cual se haga uso de alguna estructura de datos o algoritmo de estudio.
El guía de laboratorio puede enunciar el problema y presentar la instrumentación de su solución. Luego, invitar al estudiante a revisar los fuentes e inferir, antes de su ejecución, las técnicas y estilos utilizados en la instrumentación.
(b) Contemplación de la ejecución del programa para diversas combinaciones de en-trada. Esta es la fase en la cual el aprendiz comienza a sentir concreción; es decir, que los conceptos abstractos desemboquen en soluciones concretas.
Durante esta actividad puede ser recomendable la utilización de un depurador. (c) Finalmente, resolución de una variante del problema a partir de la solución
prim-igenia. En este punto es muy importante que se trabaje sobre el mismo problema y fuentes, pero con alguna variante; una ampliación para resolver otras clases de entradas, por ejemplo.
3. Trabajos prácticos: indudablemente, en el espíritu del proverbio citado, esta es la fase que encaja con el hacer para entender.
¾Cómo debe ser el proyecto? Debe corresponder a un problema original para y con sentido al contexto en el cual se enseñe, es decir, formulado por y para usarse en la comunidad en donde se imparta este curso. Por ejemplo, si aledaño al lugar de enseñanza se padecen de problemas de tráco, entonces puede plantearse un conjunto amplísimo de proyectos en torno a la simulación del tráco y al estudio de diversas técnicas para evitar su congestión. Un proyecto de este tipo requeriría, por ejemplo, la modelización con grafos de las vías de circulación, de mecanismos de control como los semáforos y de agentes circulantes como los automóviles.
So riesgo de ser excesivamente repetitivo, debo insistir en la importancia de los ejercicios. Esto ya debe ser obvio desde la perspectiva del estudiante, pues es el único medio de ejercitarse. Al instructor, por otra parte, le permite enriquecer la enseñanza cuando se plantea ejercicios que completan o complementan el conocimiento impartido, así como, mediante la corrección, supervisar el nivel de sus estudiantes.
Estructura del texto
Por razones didácticas y de espacio, este texto está dividido en dos tomos.
El primero de ellos es fundamental y comprende un curso mediano y riguroso de algoritmos y estructuras de datos, así como técnicas para criticar su efectividad y eciencia. Este tomo puede emplearse como libro texto o de referencia para un curso de estructuras de datos, diseño de algoritmos o programación mediana.
El segundo tomo es ya un texto avanzado y se consagra a dos temas principales: técnicas avanzadas de recuperación de información en memoria primaria y grafos. Los grafos conforman uno de los mundos más complejos de la computación y de la optimización, y su aplicabilidad es vasta para otros dominios que transcienden las ciencias computacionales. En pos del mejor rendimiento de algoritmos sobre grafos, a menudo es necesario emplear técnicas de alto rendimiento para recuperar información en memoria primaria, pero estas técnicas también son requeridas en otros mundos algorítmicos. De ahí el porqué de su inclusión.
La comprensión de tomo II exige un nivel de programación avanzado y buenas cuali-dades en el diseño y análisis de algoritmos y estructuras de datos. Este tomo puede usarse como texto o referencia de un curso avanzado de algoritmos o de grafos, tanto en el ámbito de pregrado como en el de postgrado.
Ambos tomos pueden fungir de referencia para el quehacer ingenieril.
Estructura de este tomo
El presente tomo comienza por abordar en el capítulo 1 los fundamentos de abstracción empleados en el resto del texto.
El capítulo 2 se adentra profundamente en la idea de secuencia, ubicua e indispensable en la programación.
Una vez dominada e instrumentada cabalmente la idea de secuencia, el capítulo 3 estudia las técnicas conocidas para criticar un algoritmo o estructuras de datos, tanto desde la perspectiva de la eciencia (análisis) como desde la de la efectividad (correctitud). Finalmente, el capítulo 4 se consagra a estudiar la estructura de datos árbol, cual también es ampliamente usada en la computación.
Historia
Comencé la elaboración de la biblioteca ALEPH en mayo de 1998. En aquel entonces me consagré a escribir clases de objetos para representar listas (las jerarquías Slink y Dlink), árboles binarios (las jerarquías BinNode<Key> y Avl_Tree<Key>) y tablas hash (la clase LhashTable<Key>).
Partes de este texto comenzaron a aparecer a mediados de 1999, luego de que algunos estudiantes me observasen que les sería útil leer algoritmos en código y que éstos fuesen bien comentados. Fue entonces cuando apelé a noweb, cuya magistral efectividad ya había conocido cuando, estudiando generación dinámica de código, me fue oportunísimo leer el libro de compiladores de Hanson y Fraser [69]. En ese tiempo escribí parte de lo que hoy es el capítulo 2, concerniente a secuencias. A mediados del 2000 comencé el capítulo 4 sobre árboles y parte del 5, referente a las tablas hash. El capítulo 4 ha tenido bastantes modicaciones a su contenido original y añadiduras, ocurridas en su mayor parte en el largo período comprendido entre 2001 y 2004.
En noviembre del 2001 redacté casi enteramente lo que actualmente es el capítulo 6 sobre equilibrio de árboles.
A mediados del 2004 inicié la extensión de la biblioteca hacia grafos, tema presentado en el capítulo 7 . Las estructuras y algoritmos en torno a este dominio han variado bastante en forma y no ha sido hasta agosto del 2007 cuando han tomado una versión estable. Creo que en este dominio es donde este texto plasma sus principales aportes.
Desde febrero de 2006 me planteé el esfuerzo de revisar y unicar los capítulos bajo lo que hoy conforma este texto. Escribí enteramente los capítulos 1, sobre abstracción de datos, y el 3, concerniente a la crítica de algoritmos y estructuras de datos.
El septiembre de 2007 culminé el capítulo 5 referente a las tablas hash.
La mayoría de las guras de este libro fue generada automáticamente mediante pro-gramas elaborados con la propia biblioteca ALEPH. En ese sentido, hay tres propro-gramas:
1. btreepic para dibujar árboles binarios
2. ntreepic para dibujar arborescencias y árboles en general 3. graphpic para dibujar grafos
El primer programa, btreepic, fue producto de mi insatisfacción con los dibujos de árboles binarios generados con programas especiales tales como Xfig y dia. A esto se aunó la imposibilidad material de dibujar árboles enormes -de cientos o miles de nodos-. En virtud de esto solicité un trabajo escolar al respecto, cuyos resultados, a pesar de satisfacerme escolarmente, distaron de serme sucientes. A raíz de esa experiencia decidí por mi propia cuenta programar btreepic. Cuando tuve que dibujar árboles generales solicité el mismo tipo de programa; entonces apareció un estudiante excelso, llamado José Brito, quien forjó una primera versión llamada xtreepic y que está distribuida en ALEPH. Lamentable-mente, esta versión operaba sobre una versión de árboles que a la época de mi necesidad y revisión ya era caduca, por lo que preferí rehacer enteramente el programa bajo el nombre de ntreepic. Para la elaboración del programa me fue muy útil el hermoso texto sobre dibujado de grafos de Kozo Sugiyama [165]. Finalmente, cuando me encontré en la misma necesidad respecto a los grafos, aproveché la ocasión para desarrollar casi enteramente el corpus algorítmico en geometría computacional. El programa resultante, graphpic, es aún muy simple y evade toda la algorítmica vinculada al dibujado automático de grafos; de hecho, las decisiones sobre dibujado son tomadas por el usuario, pero podría decir que graphpic tiene la virtud de operar enteramente sobre geometría computacional y que ello no sólo valida este campo, sino que abre futuros desarrollos.
En Marzo de 2008 inicié la escritura de la documentación de la biblioteca. Me enfrenté a un problema: ¾Cómo integrar la documentación en el fuente de este texto sin que ésta
aparezca en el texto? Requería escribirla en el mismo sitio donde escribí este libro porque de ese modo, bajo la misma doctrina de noweb, una modicación de la biblioteca se podría actualizar rápidamente en la documentación. Decidí entonces diseñar un ltro de texto que reconociese los bloques de documentación dentro del fuente noweb y los eliminase
de manera que no apareciesen en la salida LATEX. Tal ltro se llama deldoxygen y fue
escrito con el generador de analizadores lexicográcos flex y C++. Alguien dirá que
pude haberlo hecho en treinta minutos con uno de los lenguajes de scripting modernos (perl, python, etc.). Probablemente sea cierto si yo fuese maestro en alguno de aquellos lenguajes, pero, en añadidura, déjeseme replicar con tres hechos. Primero, demoré un par de horas en escribir deldoxygen porque tenía quince años sin usar flex y no recordaba bien el lenguaje; no es pues mucha la diferencia. Segundo, mi ltro tiene muchas más posibilidades de ser correcto, pues fue especicado -no programado- bajo el formalismo de las expresiones regulares y los autómatas; en un lenguaje de scripting, esta correctitud tenía que programarse y no especicarse como fue el caso. Finalmente, el ltro debe ser considerablemente más veloz que una contraparte scripting; yo diría, cuando menos, en dos órdenes de magnitud.
Cuando me encontraba en la edición nal de esta versión (Marzo 2008) hube de aprehender que el texto contenía (y aún contiene) bloques noweb con pedazos de código sin mucho valor didáctico, por ejemplo, repetir funciones cuyo sentido ya fue explicado en otro lugar para otra clase de objeto. Decidí entonces diseñar otro ltro de texto que reconociere delimitadores dentro del fuente noweb y que se invocase antes de noweb. El
ltro se denomina nobook, fue escrito también en flex y elimina, para la salida LATEX, los
bloques delimitados.
Cosas que faltan y sobran
Desde muchas perspectivas, siempre un texto adolece de falta de algo. Aquí deseo referirme a lo que, creo, hubiera debido perfectamente hacer y a lo que, con certitud, no debí hacer.
Creo sinceramente que las estructuras de datos y algoritmos fundamentales están pre-sentes en este texto, aunque, con seguridad, algún par discrepará. Por otra parte, material que a mi juicio es de alto interés, que está presente en la biblioteca ALEPH, no está pre-sente en este texto. Los ejemplos más notables de eso son las listas skip, coloreados de grafos, caminos eulerianos y hamiltonianos, estructuras de grafos concurrentes, de agentes y de simulación, estructuras de grafos y sus algoritmos basados en colonias de hormigas y geometría computacional. Me habría gustado incluirlas en este libro, pero ya me sobrepasó el momento y agoté el límite de espacio.
Aspectos no desarrollados en ALEPH y que serían dignos de incluirse son los heaps de Fibonacci, las familias de árboles dedicados a sistemas de archivo y búsqueda en memoria
secundaria (B+ y derivados) y los árboles quadtrees.
En este texto se apela al lenguaje de modelado UML para facilitar la compresión de relaciones entre objetos. Creo sinceramente que UML tiene mucho valor para comprender sistemas complejos, ya desarrollados, y un poco menos de valor, aunque apreciable, para diseñarlos. Pero en lo que atañe a este texto y sus cursos derivados, no es de trascendente utilidad.
Deudas
Sea cual sea la obra, ésta se circunscribe gracias a y para una cultura. La primera deuda, pues, que cualquier individuo adquiere con una obra es hacia su cultura, la cual le entrega el trasfondo circunstancial, en conocimientos y sentimientos, que posibilitan e inspiran la obra en cuestión. En este mismo espíritu, una vez entregada la obra, otra adquirida es hacia la cultura que recibe y reconoce la obra.
Parafraseando a Ortega y Gasset, uno es uno y su circunstancia, pero cualquiera que ésta sea, en ella siempre se encuentra la inuencia abrumadora del otro. Me siento, pues, en franca deuda hacia quienes siento que debo lo que soy y, en lo particular de este texto, hacia aquellos que incidieron muy directamente en su elaboración.
Víctor Bravo, Carlos Nava, Juan Luís Chaves y Juan Carlos Vargas fueron mis primeros discípulos en esta y el área de sistemas distribuidos. Víctor instrumentó la primera versión de la clase LinearHashTable<Key> presentada en 5.1.7 (Pág. 412). Carlos instrumentó la primera versión de un sistema comunicacional de envergadura sustentado en el uso de ALEPH; el sistema aún es operativo hoy en día. Juan Carlos instrumentó los árboles AVL hilados y con rangos, los cuales, si bien no están presentes en este texto, su instrumentación ayudó a mejorar y depurar la clase Avl_Tree<Key>.
Andrés Arcia fue un usuario intensivo de ALEPH durante de su tesis de maestría, lo cual me permitió ver aspectos que luego incidieron en extensiones y mejoras de la biblioteca.
Leonardo Zúñiga, Bladimir Contreras y Carlos Acosta diseñaron y programaron treepic, precursor de btreepic, un programa para dibujar los árboles binarios de este libro. José Brito escribió xtreepic, precursor de ntreepic, usado para dibujar árboles y arborescencias generales.
Jorge Redondo y Tomás López hicieron las primeras pruebas de desempeño sobre los diversos árboles binarios de búsqueda.
Jesús Sánchez hizo parte de la implantación parcial de la biblioteca estándar C++ bajo
ALEPH. En pruebas de desempeño tradicionales, la biblioteca estándar bajo ALEPH es de mejor desempeño que la de GNU.
Juan Fuentes instrumentó parte de y depuró la clase genérica de árbol Tree_Node . Orlando Vicuña y Alejandro Mujica encontraron errores importantes en los árboles y grafos, así como plantearon sugerencias muy apreciables sobre el estilo de implantación.
En la confección de este texto se han empleado enteramente programas libres:
LATEX [106], TEX [169], noweb, BIBTEX [22], gnu make [30], imake [82], gnuplot [61],
R [146], Maxima [121], Xfig [185], dia [36], Umbrello [170], doxygen [38], bcpp [17], graphviz [58, 42, 43], entre otros. Este texto y los programas fueron editados con gnu Emacs [44]. Los programas fueron manejados con todos los utilitarios GNU [60].
Juan Acevedo, profesor de la Facultad de Humanidades de la Universidad de Los Andes, me supervisó los comentarios etimológicos en latín y griego.
Luís Paniagua, corrector del Consejo de Publicaciones de la Universidad de Los An-des, se ha tomado muy gentilmente su deber de revisar concienzudamente este texto. A ese tenor, debo expresarle mi gratitud por sus numerosas y sorprendentes correcciones y enseñanzas.
Algunas veces, al observar algunos gestos y actitudes en discípulos me parece notarles algunas de mis enseñanzas, lo cual evoca la lejana posibilidad de mi impronta. Pero a ese
tenor debo aclarar que soy yo, más bien, quien porta sus lecciones y, por tanto, quien les expresa gratitud.
Mis amadísimos padres, Nelly y Adelis, han contribuido a lo que me atribuiría como una sensibilidad particular hacia la tecnología. Mi madre leyó y corrigió enteramente este trascrito, así como ellos, otrora mi adolescencia, me enseñaron a escribir un poco.
He observado que casi todo autor de libro texto técnico expresa agradecimientos a su familia (esposo(a) e hijo(a)(s)). En el transcurso de esta edición me percaté de que ello probablemente obedezca a que a ellos, en mi caso con descarada e irresponsable negligencia, uno les olvida. Por razones muy íntimas, para nada técnicas, no puedo medir ni expresar cómo y cuánto soy gracias a mi esposa, Magdiel, pero sí puedo clamar que no sería nada sin ella. Sea pues con y hacia ella, por su amor y su perdón, mi mayor y principal deuda . . . y gratitud.
1 Abstracción de datos 1
1.1 Especicaciones de datos . . . 3
1.1.1 Tipo abstracto de dato . . . 4
1.1.2 Noción de clase de objeto . . . 5
1.1.3 Lo subjetivo de un objeto . . . 6 1.1.4 Un ejemplo de TAD . . . 7 1.1.5 El lenguaje UML . . . 10 1.2 Herencia . . . 11 1.2.1 Tipos de herencia . . . 12 1.2.2 Multiherencia . . . 12 1.2.3 Polimorsmo . . . 13
1.3 El problema fundamental de estructuras de datos . . . 17
1.3.1 Comparación general entre claves . . . 19
1.3.2 Operaciones para conjuntos ordenables . . . 19
1.3.3 Circunstancias del problema fundamental . . . 19
1.3.4 Presentaciones del problema fundamental . . . 20
1.4 Diseño de datos y abstracciones . . . 20
1.4.1 Tipos de abstracción . . . 21 1.4.2 El principio n-a-n . . . 22 1.4.3 Inducción y deducción . . . 22 1.4.4 Ocultamiento de información . . . 23 1.5 Notas bibliográcas . . . 24 1.6 Ejercicios . . . 25 2 Secuencias 27 2.1 Arreglos . . . 28
2.1.1 Operaciones básicas con Arreglos . . . 29
2.1.2 Manejo de memoria para arreglos . . . 32
2.1.3 Arreglos dinámicos . . . 34 2.1.4 El TAD DynArray<T> . . . 34 2.1.5 Arreglos de bits . . . 53 2.2 Arreglos multidimensionales . . . 58 2.3 Iteradores . . . 59 2.4 Listas enlazadas . . . 61
2.4.1 Listas enlazadas y el principio n a n . . . 64
2.4.2 El TAD Slink (enlace simple) . . . 65 xi
2.4.3 El TAD Snode<T> (nodo simple) . . . 68
2.4.4 El TAD Slist<T> (lista simplemente enlazada) . . . 69
2.4.5 Iterador de Slist<T> . . . 70
2.4.6 El TAD DynSlist<T> . . . 71
2.4.7 El TAD Dlink (enlace doble) . . . 72
2.4.8 El TAD Dnode<T> (nodo doble) . . . 83
2.4.9 El TAD DynDlist<T> . . . 84
2.4.10 Aplicación: aritmética de polinomios . . . 91
2.5 Pilas . . . 98
2.5.1 Representaciones de una pila en memoria . . . 99
2.5.2 El TAD ArrayStack<T> (pila vectorizada) . . . 101
2.5.3 El TAD ListStack<T> (pila con listas enlazadas) . . . 103
2.5.4 El TAD DynListStack<T> . . . 105
2.5.5 Aplicación: un evaluador de expresiones aritméticas injas . . . 106
2.5.6 Pilas, llamadas a procedimientos y recursión . . . 112
2.6 Colas . . . 122
2.6.1 Variantes de las colas . . . 123
2.6.2 Aplicaciones de las colas . . . 123
2.6.3 Representaciones en memoria de las colas . . . 123
2.6.4 El TAD ArrayQueue<T> (cola vectorizada) . . . 124
2.6.5 El TAD ListQueue<T> (cola con listas enlazadas) . . . 128
2.6.6 El TAD DynListQueue<T> (cola dinámica con listas enlazadas) . . . 130
2.7 Estructuras de datos combinadas - Multilistas . . . 131
2.8 Notas bibliográcas . . . 133
2.9 Ejercicios . . . 135
3 Crítica de algoritmos 145 3.1 Análisis de algoritmos . . . 147
3.1.1 Unidad o paso de ejecución . . . 148
3.1.2 Aclaratoria sobre los métodos de ordenamiento . . . 150
3.1.3 Ordenamiento por selección . . . 150
3.1.4 Búsqueda secuencial . . . 154
3.1.5 El problema fundamental y los arreglos dinámicos . . . 155
3.1.6 Búsqueda de extremos . . . 156
3.1.7 Notación O . . . 157
3.1.8 Ordenamiento por inserción . . . 162
3.1.9 Búsqueda binaria . . . 165
3.1.10 Errores de la notación O . . . 166
3.1.11 Tipos de análisis . . . 167
3.2 Algoritmos dividir/combinar . . . 168
3.2.1 Ordenamiento por mezcla . . . 169
3.2.2 Ordenamiento rápido (Quicksort) . . . 173
3.3 Análisis amortizado . . . 187
3.3.1 Análisis potencial . . . 189
3.3.2 Análisis contable . . . 191
3.4 Correctitud de algoritmos . . . 193
3.4.1 Planteamiento de una demostración de correctitud . . . 193
3.4.2 Tipos de errores . . . 194
3.4.3 Prevención y detección de errores . . . 194
3.5 Ecacia y eciencia . . . 210
3.5.1 La regla del 80-20 . . . 212
3.5.2 ¾Cuándo atacar la eciencia? . . . 212
3.5.3 Maneras de mejorar la eciencia . . . 213
3.5.4 Perlaje (proling) . . . 214 3.5.5 Localidad de referencia . . . 214 3.5.6 Tiempo de desarrollo . . . 215 3.6 Notas bibliográcas . . . 216 3.7 Ejercicios . . . 217 4 Árboles 223 4.1 Conceptos básicos . . . 226 4.2 Representaciones de un árbol . . . 228 4.2.1 Conjuntos anidados . . . 228 4.2.2 Secuencias parentizadas . . . 229 4.2.3 Indentación . . . 230 4.2.4 Notación de Deway . . . 231
4.3 Representaciones de árboles en memoria . . . 231
4.3.1 Listas enlazadas . . . 231
4.3.2 Arreglos . . . 232
4.4 Árboles binarios . . . 233
4.4.1 Representación en memoria de un árbol binario . . . 234
4.4.2 Recorridos sobre árboles binarios . . . 234
4.4.3 Un TAD genérico para árboles binarios . . . 238
4.4.4 Contenedor de funciones sobre árboles binarios . . . 242
4.4.5 Recorridos recursivos . . . 243
4.4.6 Recorridos no recursivos . . . 246
4.4.7 Cálculo de la cardinalidad . . . 249
4.4.8 Cálculo de la altura . . . 249
4.4.9 Copia de árboles binarios . . . 250
4.4.10 Destrucción de árboles binarios . . . 250
4.4.11 Comparación de árboles binarios . . . 250
4.4.12 Recorrido por niveles . . . 251
4.4.13 Construcción de árboles binarios a partir de recorridos . . . 253
4.4.14 Conjunto de nodos en un nivel . . . 254
4.4.15 Hilado de árboles binarios . . . 255
4.4.16 Recorridos pseudohilados . . . 258
4.4.17 Correspondencia entre árboles binarios y m-rios . . . 260
4.5 Un TAD genérico para árboles . . . 264
4.5.1 Observadores de Tree_Node . . . 266
4.5.2 Modicadores de Tree_Node . . . 267
4.5.4 Recorridos sobre Tree_Node . . . 270
4.5.5 Destrucción de Tree_Node . . . 270
4.5.6 Búsqueda por número de Deway . . . 271
4.5.7 Cálculo del número de Deway . . . 272
4.5.8 Correspondencia entre Tree_Node y árboles binarios . . . 273
4.6 Algunos conceptos matemáticos de los árboles . . . 275
4.6.1 Altura de un árbol . . . 275
4.6.2 Longitud del camino interno/externo . . . 277
4.6.3 Árboles completos . . . 280 4.7 Heaps . . . 282 4.7.1 Inserción en un heap . . . 284 4.7.2 Eliminación en un heap . . . 285 4.7.3 Colas de prioridad . . . 288 4.7.4 Heapsort . . . 289
4.7.5 Aplicaciones de los heaps . . . 291
4.7.6 El TAD BinHeap<Key> . . . 293
4.8 Enumeración y códigos de árboles . . . 302
4.8.1 Números de Catalan . . . 307
4.8.2 Almacenamiento de árboles binarios . . . 311
4.9 Árboles binarios de búsqueda . . . 313
4.9.1 Búsqueda en un ABB . . . 315
4.9.2 El TAD BinTree<Key> . . . 319
4.9.3 Inserción en un ABB . . . 320
4.9.4 Partición de un ABB por clave (split) . . . 321
4.9.5 Unión exclusiva de ABB (join exclusivo) . . . 323
4.9.6 Eliminación en un ABB . . . 324
4.9.7 Inserción en raíz de un ABB . . . 326
4.9.8 Unión de ABB (join) . . . 327
4.9.9 Análisis de los árboles binarios de búsqueda . . . 329
4.10 El TAD DynMapTree . . . 332
4.11 Extensiones a los árboles binarios . . . 335
4.11.1 Selección por posición . . . 337
4.11.2 Cálculo de la posición inja . . . 338
4.11.3 Inserción por clave en árbol binario extendido . . . 338
4.11.4 Partición por clave . . . 339
4.11.5 Inserción en raíz . . . 340
4.11.6 Partición por posición . . . 340
4.11.7 Inserción por posición . . . 341
4.11.8 Unión exclusiva de árboles extendidos . . . 342
4.11.9 Eliminación por clave en árboles extendidos . . . 342
4.11.10 Eliminación por posición en árboles extendidos . . . 343
4.11.11 Desempeño de las extensiones . . . 343
4.12 Rotación de árboles binarios . . . 344
4.12.1 Rotaciones en árboles binarios extendidos . . . 345
4.13 Códigos de Human . . . 345
4.13.2 Decodicación . . . 349
4.13.3 Algoritmo de Human . . . 350
4.13.4 Denición de símbolos y frecuencias . . . 352
4.13.5 Codicación de texto . . . 353
4.13.6 Optimación de Human . . . 355
4.14 Árboles estáticos óptimos . . . 358
4.14.1 Objetivo . . . 360 4.14.2 Implantación . . . 360 4.15 Notas bibliográcas . . . 363 4.16 Ejercicios . . . 365 5 Tablas hash 379 5.1 Manejo de colisiones . . . 381
5.1.1 El problema del cumpleaños . . . 381
5.1.2 Estrategias de manejo de colisiones . . . 382
5.1.3 Encadenamiento . . . 383
5.1.4 Direccionamiento abierto . . . 393
5.1.5 Reajuste de dimensión en una tabla hash . . . 409
5.1.6 El TAD DynLhashTable (cubetas dinámicas) . . . 410
5.1.7 Tablas hash lineales . . . 412
5.2 Funciones hash . . . 423
5.2.1 Interfaz a la función hash . . . 423
5.2.2 Holgura de dispersión . . . 423
5.2.3 Plegado o doblado de clave . . . 424
5.2.4 Heurísticas de dispersión . . . 424
5.2.5 Dispersión de cadenas de caracteres . . . 429
5.2.6 Dispersión universal . . . 430
5.2.7 Dispersión perfecta . . . 431
5.3 Otros usos de las tablas hash y de la dispersión . . . 431
5.3.1 Identicación de cadenas . . . 431
5.3.2 Supertraza . . . 434
5.3.3 Cache (el TAD Hash_Cache ) . . . 435
5.4 Notas bibliográcas . . . 445
5.5 Ejercicios . . . 446
6 Árboles de búsqueda equilibrados 451 6.1 Equilibrio de árboles . . . 452
6.2 Árboles aleatorizados . . . 455
6.2.1 El TAD Rand_Tree<Key> . . . 455
6.2.2 Análisis de los árboles aleatorizados . . . 460
6.3 Treaps . . . 464
6.3.1 El TAD Treap<Key> . . . 465
6.3.2 Inserción en un treap . . . 467
6.3.3 Eliminación en treap . . . 468
6.3.4 Análisis de los treaps . . . 470
6.3.5 Prioridades implícitas . . . 471
6.4.1 El TAD Avl_Tree<Key> . . . 472
6.4.2 Análisis de los árboles AVL . . . 483
6.5 Árboles rojo-negro . . . 490
6.5.1 El TAD Rb_Tree<Key> . . . 491
6.5.2 Análisis de los árboles rojo-negro . . . 503
6.6 Árboles splay . . . 507
6.6.1 El TAD Splay_Tree<Key> . . . 509
6.6.2 Análisis de los árboles splay . . . 513
6.7 Conclusión . . . 519
6.8 Notas bibliográcas . . . 523
6.9 Ejercicios . . . 525
7 Grafos 535 7.1 Fundamentos . . . 537
7.2 Estructuras de datos para representar grafos . . . 542
7.2.1 Matrices de adyacencia . . . 542
7.2.2 Listas de adyacencia . . . 544
7.3 Un TAD para grafos (List_Graph) . . . 545
7.3.1 Grafos . . . 546
7.3.2 Digrafos (List_Digraph) . . . 547
7.3.3 Nodos . . . 547
7.3.4 Arcos . . . 551
7.3.5 Atributos de control de nodos y arcos . . . 555
7.3.6 Macros de acceso a nodos y arcos . . . 561
7.3.7 Construcción y destrucción de List_Graph . . . 562
7.3.8 Operaciones genéricas sobre nodos . . . 563
7.3.9 Operaciones genéricas sobre arcos . . . 563
7.3.10 Implantación de List_Graph . . . 564
7.4 TAD camino sobre un grafo (Path<GT>) . . . 578
7.5 Recorridos sobre grafos . . . 581
7.5.1 Iteradores ltro . . . 582
7.5.2 Recorrido en profundidad . . . 585
7.5.3 Conectividad entre grafos . . . 589
7.5.4 Recorrido en amplitud . . . 589
7.5.5 Prueba de ciclos . . . 592
7.5.6 Prueba de aciclicidad . . . 593
7.5.7 Búsqueda de caminos por profundidad . . . 596
7.5.8 Búsqueda de caminos por amplitud . . . 600
7.5.9 Árboles abarcadores de profundidad . . . 603
7.5.10 Árboles abarcadores de amplitud . . . 605
7.5.11 Árboles abarcadores en arreglos . . . 606
7.5.12 Conversión de un árbol abarcador a un Tree_Node . . . 608
7.5.13 Componentes inconexos de un grafo . . . 610
7.5.14 Puntos de articulación de un grafo . . . 613
7.5.15 Componentes conexos de los puntos de corte . . . 622
7.6.1 El TAD Ady_Mat . . . 629
7.6.2 El TAD Bit_Mat_Graph<GT> . . . 633
7.6.3 Algoritmo de Warshall . . . 635
7.7 Grafos dirigidos . . . 637
7.7.1 Conectividad entre digrafos . . . 638
7.7.2 Inversión de un digrafo . . . 638
7.7.3 Componentes fuertemente conexos de un digrafo . . . 639
7.7.4 Prueba de aciclicidad . . . 648
7.7.5 Cálculo de ciclos en un digrafo . . . 650
7.7.6 Prueba de conectividad . . . 652
7.7.7 Digrafos acíclicos (DAG) . . . 653
7.7.8 Planicación de tareas . . . 654
7.7.9 Ordenamiento topológico . . . 655
7.8 Árboles abarcadores mínimos . . . 660
7.8.1 Manejo de los pesos del grafo . . . 660
7.8.2 Algoritmo de Kruskal . . . 661 7.8.3 Algoritmo de Prim . . . 666 7.9 Caminos mínimos . . . 674 7.9.1 Algoritmo de Dijkstra . . . 675 7.9.2 Algoritmo de Floyd-Warshall . . . 685 7.9.3 Algoritmo de Bellman-Ford . . . 692
7.9.4 Discusión sobre los algoritmos de caminos mínimos . . . 707
7.10 Redes de ujo . . . 708
7.10.1 Deniciones y propiedades fundamentales . . . 708
7.10.2 El TAD Net_Graph . . . 710
7.10.3 Manejos de varios fuentes o sumideros . . . 711
7.10.4 Operaciones topológicas sobre una red capacitada . . . 713
7.10.5 Cortes de red . . . 716
7.10.6 Flujo máximo/corte mínimo . . . 718
7.10.7 Caminos de aumento . . . 720
7.10.8 Cálculo de la red residual . . . 723
7.10.9 Cálculo de caminos de aumento . . . 725
7.10.10 Incremento del ujo por un camino de aumento . . . 725
7.10.11 El algoritmo de Ford-Fulkerson . . . 726
7.10.12 El algoritmo de Edmonds-Karp . . . 731
7.10.13 Algoritmos de empuje y preujo . . . 736
7.10.14 Cálculo del corte mínimo . . . 766
7.10.15 Aumento o disminución de ujo de una red . . . 769
7.11 Reducciones al problema del ujo máximo . . . 773
7.11.1 Flujo máximo en redes no dirigidas . . . 773
7.11.2 Capacidades en nodos . . . 774
7.11.3 Flujo factible . . . 774
7.11.4 Máximo emparejamiento bipartido . . . 777
7.11.5 Circulaciones . . . 783
7.11.6 Conectividad de grafos . . . 785
7.12 Flujos de coste mínimo . . . 792
7.12.1 El TAD Net_Max_Flow_Min_Cost . . . 796
7.12.2 Algoritmos de máximo ujo con coste mínimo mediante eliminación de ciclos . . . 798
7.12.3 Análisis de los algoritmos basados en eliminación de ciclos negativos 802 7.12.4 Problemas que se reducen a enunciados de ujo máximo a coste mínimo . . . 802
7.13 Programación lineal . . . 810
7.13.1 Forma estándar de un programa lineal . . . 812
7.13.2 Un ejemplo de estandarización . . . 813
7.13.3 Forma holgada de un programa lineal . . . 814
7.13.4 El método simplex . . . 815
7.13.5 Conclusión sobre la programación lineal . . . 822
7.14 Redes de ujo y programación lineal . . . 822
7.14.1 Conversión de una red capacitada de costes a un programa lineal . . 822
7.14.2 Redes generalizadas . . . 823
7.14.3 Capacidades acotadas . . . 824
7.14.4 Redes con restricciones laterales . . . 824
7.14.5 Redes multiujo . . . 825
7.14.6 Redes de procesamiento . . . 825
7.14.7 Conclusión sobre el problema del ujo máximo a coste mínimo . . . 828
7.15 Notas bibliográcas . . . 829
7.16 Ejercicios . . . 831
Abstracción de datos
Este texto concierne al diseño e implantación de estructuras de datos y algoritmos que instrumenten soluciones a problemas mediante programas de computador.
Un algoritmo es una secuencia nita de instrucciones que acomete la consecución de un n. Usamos algoritmos en diversos contextos de la vida; por ejemplo, cuando preparamos un plato de comida según alguna receta. La cultura nos ha inculcado algunos algoritmos, culturales, no naturales, para desenvolvernos socialmente; por ejemplos, el algoritmo de conducir un automóvil o el algoritmo para cruzar una calle. En ambos ejemplos, el n que se plantea es arribar a un sitio.
Según Knuth [97], el término algoritmo proviene del nombre del ancestral matemático al-Khw arizm , de la Persia, parte del actual Irán, región del planeta muy amenazada de ser borrada del mapa. De al-Khw arizm también proviene la palabra álgebra, dominio descubierto por vez primera en el actual invadido y devastado Irak.
Para la consecución de un n se emplean medios. En el caso de un plato, los medios que se utilizan son los instrumentos de cocina e ingredientes; el cocinero, quien puede interpretarse también como un medio, conjuga, en un orden especíco, los ingredientes mediante los instrumentos. La secuencia de ejecución, o sea, el algoritmo, es fundamental para conseguir el plato en cuestión. Una alteración del orden acarreará posiblemente una alteración sobre el sabor del plato.
Durante la preparación de un plato, el cocinero requiere percibir y recordar la secuencia de ejecución. Él requiere, por ejemplo, sofreír algunos aliños antes de mezclarlos con la carne. Según la experiencia y la complejidad, es posible que el cocinero lleve notas que memoricen el estado de preparación; por ejemplo, anotar la hora en que comenzó a hornear. En todo momento, con notas o sin ellas, el cocinero requiere tener consciencia del estado en el cual se ubica la preparación respecto a su receta. Para eso se sirve de su memoria.
En el caso de un programa, el computador funge de cocinero, el cual ejecuta elmente las recetas que se le proporcionan. El computador es entonces un ejecutor que organiza y usa algunos medios para alcanzar la solución de algún problema, según alguna receta llamada programa y que recuerda estados de cálculos mediante una memoria.
Aparte del CPU, quien funge de cocinero, el computador se vale de un medio funda-mental: la memoria. De por sí, la memoria en bruto es una secuencia de ceros y unos cuyo sentido lo imparte el programador en forma de datos.
Cualquier programa que opere en un computador puede dividirse en dos partes: la secuencia de instrucciones de ejecución y los datos. Las instrucciones son resultado de aquello que escribimos como código fuente. En ocasiones, las instrucciones pueden gene-rarse durante la ejecución del programa. Los datos representan el estado de cálculo en
algún momento del tiempo de ejecución.
La palabra dato proviene del latín datum, participio pasado de d o (dar). Dato connota, pues, algo que fue dado en el pasado y que nos interesa recordar; ese es el sentido de que sea memorizado. En el caso de la programación, un dato recuerda una parte del estado de ejecución del programa.
El mínimo nivel de organización de un dato es su tipo o clase. Un tipo de dato dene un conjunto compuesto por todos los valores que puede adquirir una instancia del dato.
Un tipo de dato puede conformarse por varios tipos de datos. En este caso lo tipicamos de dato estructurado o estructura de datos. En algunos casos, los datos pueden ser
recursivos (recurrentes1); es decir, según el tipo de dato se recurren a sí mismos o entre
ellos.
Algunos ejemplos en C++ podrán dar luz de este asunto.
Para representar números enteros se utiliza el tipo de dato int, el cual indica que su valor pertenece al conjunto Z. En este caso no se dice nada acerca de cómo se representa el tipo int en la memoria de un computador; bien pudiera tratarse de 19, de 937 o de
2535301200456458802993406410049, entre innitos valores posibles.
Para representar números reales que pertenecen a R, usamos el tipo float, más in-teresante que el anterior porque trasluce parte de su implantación en la memoria de un computador cuando expresa, mediante el nombre float, que la representación del número es en punto otante, es decir, está estructurada en tres campos de forma similar a la siguiente:
001011110010110001001010
0 00001000
Exponente Parte fraccional Signo
El sentido de esta estructura es hacer rápidamente sumas mediante ajuste del expo-nente y de la parte fraccional. Aunque no conocemos el tamaño de los campos anteriores, el conocimiento de la estructura y de la manera de manipularla nos alerta sobre el célebre e inevitable error de redondeo que ocurre cuando trabajamos con aritmética otante.
Para ilustrar un dato recurrente nos valdremos del tipo hElemento de secuencia 2i,
cuya especicación en C++ puede plantearse como sigue:
2 hElemento de secuencia 2i≡ struct Elemento { int dato; Elemento * siguiente_elemento; };
hElemento de secuencia2i modeliza un elemento entero perteneciente a una secuencia.
Notemos que el atributo siguiente_elemento se reere a un struct Elemento e indica la dirección en memoria del siguiente elemento en la secuencia.
Diversos intereses inciden en el diseño de una estructura de datos. Entre los más típicos podemos destacar: la comprensión y manipulación del programa por parte del programador, el desempeño y la adaptación a un algoritmo o concepto particular. En el ejemplo del tipo float hay dos características decisivas. La primera es que las operaciones
1Según su raíz latina, el término recurrir recurr o, connota volver, regresar. En inglés, la raíz es
la misma, pero basada en recursus, que signica vuelta, retorno. En este texto se da preferencia a recursión en lugar de recurrencia.
aritméticas son muy rápidas. De hecho, siempre toman tiempo constante e independiente del valor particular del dato. La segunda característica concierne al espacio, es decir, cada dato en punto otante siempre ocupa la misma cantidad de espacio, cuestión que no sucedería si usáramos aritmética arbitraria.
Para los tipos de datos que acabamos de ejemplicar (int y float) disponemos de un fondo cultural, matemático y de programación que nos permite señalarlos y comprender-los sin necesidad de detallar minuciosamente en qué consisten. Sabemos, sin tener que indicarlo explícitamente, que existen las operaciones aritméticas tradicionales de suma, resta, producto y división, así como qué hacen y cuáles son sus resultados.
1.1 Especicaciones de datos
Supongamos que un cocinero consumado desea escribir una de sus recetas para divulgarla entre otros. En esta situación, según el corpus cognitivo de su experiencia, el cocinero asume que sus lectores poseen un lenguaje común que les facilitará entender las instruc-ciones de su receta. Por ejemplo, se requiere que el cocinero y sus lectores tengan el mismo concepto de lo que es una olla.
En el marco de ese lenguaje común, la receta debe ser precisa; no debe contener am-bigüedades que bloqueen al ejecutante. Además, debe ser completa para que el ejecutante prepare plenamente el plato en cuestión.
En la percepción del aprendiz de programación existe una diferencia esencial entre ela-borar un plato de cocina y ejecutar un programa. En cocina se opera sobre cosas concretas para la percepción humana. En programación, el computador opera sobre datos concretos en su memoria, pero abstractos para nuestra percepción. Preguntémosnos: ¾existe un número?, ¾existe un arreglo? En nuestra mente, un arreglo constituye una abstracción que no se capta con nuestra percepción sensorial. En el computador no tiene sentido la abstracción arreglo, pues éste no entiende lo que es un número o arreglo. En el caso de la programación, así como en otros dominios derivados de la matemática, un número o un arreglo son conceptos abstractos que conforman un lenguaje común para comunicarlos y permitir la construcción de más conceptos.
Entre programadores, así como en el resto de las prácticas, es muy importante disponer de un corpus común, sobre la manera de abstraer, que permita la comunicación de manera homogénea.
Consideremos una situación en la que deseemos disponer de un nuevo tipo de dato. Planteémosnos dos clases de preguntas en el siguiente orden:
1. P1: ¾Cuál es su n? o, dicho de otra manera, ¾para qué puede servir? 2. P2: ¾Cómo se puede denir?, ¾qué representa el dato?
La primera pregunta nos indica la clase de problema para el cual el tipo de dato se circunscribe como parte de la solución. Si esto no está denido, entonces no tiene ningún sentido considerar el tipo de dato. La segunda pregunta nos expresa qué es el tipo de dato, pero ese qué-es depende del para qué éste se usa. Si tenemos claro el n, entonces un dato se dene según las operaciones permisibles y sus resultados.
Por ejemplo, si nos encontramos en una situación en la cual requiramos cálculo de variable compleja, entonces es esencial tener un tipo de dato número complejo. LLámese
hComplejo 5i a este tipo y denámoslo como una suma cr + ci i | cr, ci ∈ R, donde
el coeciente cr es llamado parte real y, ci parte imaginaria; este último representa
una fracción del número imaginado √−1 . Al igual que con los tipos anteriores, esta
denición asume que el lector cuenta con una cultura matemática en la cual tienen sentido los números complejos.
Como operaciones establecemos la consulta de la parte real e imaginaria respectiva-mente.
1.1.1 Tipo abstracto de dato
Hemos dicho que un tipo de dato representa un conjunto. Bajo la presunción de una base
cultural matemática, la cual comprende al concepto de conjunto, el tipo hComplejo 5i se
dene en torno a la noción matemática de número complejo que le brinda su comprensión. Bajo ese lenguaje, el ejemplo anterior satisface las preguntas P1 y P2 respectivamente. Si no dispusiéramos del corpus matemático de número complejo, entonces nos sería muy
difícil interpretar el sentido del tipo hComplejo 5i.
La matemática, siempre y cuando se haya pasado por su entrenamiento, dene un cor-pus cognitivo, bastante abstracto por cierto, que nos permite denir el nuevo tipo de dato. Si nos remitimos a la denición de tipo de dato, entonces, según la matemática, denir un tipo de dato estriba en denir un conjunto de las dos maneras que tiene la matemática: por extensión o por comprensión. Denir un conjunto por extensión es muy objetivo, pero también muy arduo y, en muchos casos, imposible cuando el conjunto es innito, por ejemplo. En el caso de la programación, un tipo de dato se dene, paradójicamente, por comprensión mediante una forma metodológica denominada tipo abstracto de dato o TAD, la cual, en la versión de este texto, consta de las siguientes partes:
1. Una descripción del n para el cual se destina el tipo de dato.
2. Un conjunto de axiomas y precondiciones que denen el dominio del tipo2.
3. Una interfaz denida por todas las operaciones posibles sobre el TAD en la cual, por cada operación, se establezcan dos tipos de especicaciones:
Especicación sintáctica: nombre de la operación, tipo de resultado y nombres y tipos de los parámetros.
Especicación semántica: descripción de lo que hace la operación sobre el estado del TAD.
En esta parte puede ser útil indicar axiomas, precondiciones y postcondiciones. Esencial destacar que, conocido, entendido y aceptado el n, adquieren completo sen-tido las especicaciones sintáctica y semántica de un TAD.
En este texto nos valdremos del concepto de clase de objeto para llevar a cabo parte de la especicación.
1.1.2 Noción de clase de objeto
La palabra objeto proviene del latín objectum (ob-jectum u ob-iectum), composición de ob, que signica sobre el (la), frente a, y jectum, que es el participio pasado de iac ere, étimo directo de yacer y que signica tender, echar. Así pues, objectum (obiectum) es lo que yace al frente, lo que es visible de una cosa, su cara externa. En similar contraste, la palabra sujeto, también proveniente del latín subjectum, cuyo prejo latino sub señala debajo, signicaba lo que está debajo de la cosa, oculto, que le es interno; dicho de otro
modo, invisible en apariencia3.
En programación, así como en otras ingenierías, lo objetivo, o sea, la cara visible, se denomina interfaz; de inter-faz; es decir, lo que está entre la cara; lo que se ofrece al exterior.
En el contexto de la programación, una clase de objeto, o simplemente clase, es una representación objetiva de un tipo abstracto de dato que dene su especicación sintáctica. Por objetiva pretendemos decir que sólo nos referimos a las partes externas, visibles, que tendría un objeto perteneciente a una clase dada. En el caso de un tipo de dato, las partes visibles las conforman el nombre del tipo, los nombres de las operaciones, los nombres de los parámetros, los tipos de dato de los parámetros y los resultados de las operaciones, es decir, su especicación sintáctica.
Para satisfacer la objetividad de la especicación sintáctica es necesario acordar un lenguaje común entre los programadores, pues de lo contrario sería muy difícil interpretar la interfaz. Un automóvil, por ejemplo, tiene una interfaz de uso consistente, entre otras cosas, de los pedales, el volante y el tablero. Para poder conducirlo se requiere que el conductor esté entrenado en la utilización de esa interfaz. Del mismo modo, para que un programador comprenda una especicación sintáctica, éste debe entender el lenguaje en que se especica la clase o TAD. En el caso de este texto haremos especicaciones
sintácticas de TAD en el lenguaje C++ o en diagramas de clases UML.
En C++, el ejemplo del TAD hComplejo 5i podría modelizarse del siguiente modo:
5 hComplejo5i≡
struct Complejo {
Complejo(float r, float i); Complejo(const Complejo & c); float & obtenga_parte_real(); float & obtenga_parte_imag(); };
Esta denición establece objetivamente la especicación sintáctica del TAD hComplejo5i,
la cual, aunada al lenguaje y al corpus matemático cultural de la noción de número com-plejo, completa la especicación sintáctica del TAD.
¾Qué sucedió con la especicación semántica?, ¾está completa la especicación? Si
asumimos que el lector de la especicación del TAD hComplejo 5i conoce su matemática
inherente, entonces los nombres de las operaciones permiten comprender directamente qué hace cada operación sin necesidad de explicitarlo. ¾Existe alguna duda sobre lo que hace la operación obtenga_parte_real()? La respuesta depende, entre otros factores, del grado de entendimiento matemático que tenga el cuestionante. Si el lector no conoce la noción
de número complejo, entonces se requerirá una especicación semántica que le imparta lo que es un complejo.
Lo objetivo posibilita un acuerdo común entre diferentes personas acerca de la inter-pretación de un tipo de dato. Para que este acuerdo ocurra, es necesario que las personas en cuestión vean o interpreten homogéneamente al objeto. Consecuentemente, la inter-pretación de un TAD depende del grado en que sus interesados compartan el lenguaje con que se exprese su especicación.
1.1.3 Lo subjetivo de un objeto
Si tratamos a un dato en términos objetivos, entonces, ¾en qué consiste tratarlo en términos subjetivos? Grosso modo, la respuesta es que la subjetividad de un TAD se trata durante su especicación semántica.
Existen, básicamente, tres fuentes de subjetividad.
La visión, interpretación y, en consecuencia, el sentido de un TAD, dependen de la experiencia y conocimiento que tenga la persona que utilice el TAD. Pero esto es muy subjetivo, pues cada quien tiene su propia experiencia, la cual no debe tratarse obje-tivamente. La primera fuente de subjetividad es entonces la interpretación del usuario del TAD acerca de su sentido. Quienes hayan estudiado cabalmente los números
com-plejos tendrán una compresión del TAD hComplejo 5i más homogénea que quienes no lo
hayan estudiado. Para estos últimos puede ser necesario complementar la especicación.
En el caso del TAD hComplejo5i es muy conveniente tener una trayectoria de estudios
matemáticos. ¾Puede un programador sin esta trayectoria manejar el TAD hComplejo 5i?
Enfrentar esta pregunta revela perspectivas contradictorias.
En primer lugar, si el usuario del TAD hComplejo 5i acepta la interfaz, entonces, el
desarrollo de programas que usen números complejos permite ganar comprensión acerca de la matemática compleja. Empero, este usuario, al no disponer del corpus matemático requerido, es más propenso a utilizar la interfaz para un n diferente al que fue concebido
el TAD hComplejo5i; por ejemplo, para representar puntos en el plano cartesiano. Si bien
esto puede representar un ahorro de código, puede acarrear también grandes confusiones entre los programadores y mantenedores.
La segunda fuente de subjetividad proviene del mismo diseñador del TAD. El objeto resultante depende también de la experiencia del diseñador. Personas diferentes tienden a proponer interfaces diferentes.
La última fuente de subjetividad se reere a la implantación del TAD. Distintos pro-gramadores harán implantaciones diferentes. Si bien esta es primariamente la subjetividad que pretende esconder un TAD, puede ser esencial considerarla por dos razones: el tiempo de implantación y el tiempo de ejecución.
El tiempo de desarrollo de un TAD puede ser tan extenso que comprometa un proyecto. Análogamente, el tiempo de ejecución del programa resultante puede ser tan lento que haga necesario repetir la implantación del TAD. En cualquiera de estas situaciones puede ser conveniente indicar los aspectos generales de la implantación.
En resumen, las fuentes de subjetividad se tipican como sigue: 1. Subjetividad de interpretación del usuario
3. Subjetividad de implantación
Por más énfasis que se le haga a la orientación a objetos, los programadores que se circunscriban en desarrollos cooperativos deben estar conscientes de estas subjetividades al momento de hacer la especicación semántica de un TAD. La idea en la especicación semántica es entonces adecuarse a la expectativa cognitiva del grupo de personas involu-cradas en el desarrollo y uso de un TAD. Si aquel grupo, por instancia, comprende la matemática compleja, entonces no sólo es innecesario ahondar en una especicación semán-tica que explique la noción de numero complejo, sino que también puede tornarse muy tedioso. En este caso, la fuente de subjetividad sólo es de implantación, la cual es precisa-mente la subjetividad que pretende ocultar un TAD.
Por la razón anterior, la especicación semántica no es objetiva. Ahora bien, ¾cómo llevar a cabo una especicación semántica efectiva, es decir, que logre el efecto de aceptarse entendida por un grupo de programadores? Respuesta resumida: mediante un lenguaje adecuado. Para disertar en torno a esta cuestión, es apropiado imaginar cómo dos inter-locutores tratan la noción de lo objetivo y subjetivo acerca de una cosa de programación y un TAD.
En el sentido en que lo hemos tratado, lo objetivo de la cosa programada es percep-tible a la visión común de los interlocutores, mientras que lo subjetivo les está oculto, al menos a la mirada de uno de ellos, o de ambos. Por visión, el lector no debe asumir el mero sentido sensorial, sino la capacidad de percibir una cosa o fenómeno mediante las abstracciones y construcciones intelectuales que la experiencia de los interlocutores les permita. Como parte de la experiencia, es crítico que los interlocutores tengan destrezas de programación equiparables.
Supongamos que un interlocutor A le presenta un TAD a otro B. Dos situaciones iniciales son posibles: (1) B ve e interpreta el TAD de la misma manera que A y (2) B no lo interpreta igual. En cualquiera de los dos casos, es esencial que A conozca la opinión de B para poder determinarse cuál de las dos situaciones ocurre.
Cuando ocurre la primera situación, los interlocutores tienen entonces una mirada homogénea del TAD y la subjetividad que queda es de implantación, la cual, en el estadio de diseño, casi siempre es bueno ocultarla.
Si ocurre la segunda situación, entonces A y B deben homogeneizar la visión e in-terpretación del TAD de forma que sus subjetividades de inin-terpretación lleguen a ser objetivas. La única manera hasta ahora conocida de hacerlo es mediante el diálogo. Por eso el lenguaje es fundamental en la especicación de un TAD. Pero el lenguaje no es me-ramente unidireccional. A no tiene ninguna forma de corroborar si B comparte su mirada si no escucha la interpretación que tenga B acerca del TAD. Por tanto, cuando se diseña un nuevo TAD, es esencial que el diseñador lo exponga ante los interesados e inicie un proceso de diálogo que dure hasta que no hayan subjetividades de interpretación y sólo queden las de implantación.
1.1.4 Un ejemplo de TAD
Experiencias adquiridas en el desarrollo de programas de dibujado permiten modelizar un
TAD, llamado hFigure 8ai, cuyo n es generalizar operaciones inherentes al dibujado de
que hagan dibujos, y una propuesta de denición es como sigue: 8a hFigure 8ai≡ struct Figure { hConstructores de Figure 8bi hObservadores de Figure 9bi hModicadores de Figure 9ci }; hFiguras concretas 12i Denes:
Figure, used in chunks 8b, 9a, 12, and 15a.
El TAD hFigure8ai modeliza una gura general que se dibujaría en algún medio de
contraste. Es general en el sentido de que sólo abstraemos operaciones generales sobre
una gura geométrica cualquiera, es decir, las operaciones son generales4 porque operan
sobre cualquier gura independientemente de su particularidad. Por cualquier gura pretendemos expresar que su forma, cuadrática, triangular, etcétera, no nos importa, sólo nos interesa una gura como abstracción general para dibujar y la manera general de operar sobre ella a través de operaciones generales comunes a todas las guras existentes. No se debe, y es preferible asumir que no se puede, denir una abstracción sin conocer el para qué de tal denición. Por esa razón, cuando diseñamos un TAD debemos ase-gurarnos de tener claro el n que perseguimos. En este sentido, en lo que concierne al
TAD hFigure 8ai, el n es dibujar guras en algún medio de contraste. Si este n no está
claro, no tiene sentido hablar de guras y de sus operaciones.
La especicación sintáctica del TAD hFigure 8ai está dada por su denición en C++.
Una operación sobre una clase se denomina método, término proveniente del latín methodus, el cual proviene del griego µèθοδοσ (meta - hodos). En este caso meta connota fuera, más allá y hodos signica camino. Método quiere decir, pues, un camino hacia el n [que está fuera]; es decir, un camino con destino, con sentido.
Para denir la semántica de cada operación, debemos denotar el TAD Point,
pues lo referencian algunas operaciones del TAD hFigure 8ai. Supeditado al n del
TAD hFigure 8ai, un punto destina la ubicación de la gura al momento de su
dibu-jado y no nos conviene, por ahora, denirla más, pues no tenemos idea -y es por ahora también preferible no tenerla- del medio en el cual se dibujarían las guras; por ejemplos, un medio planar: papel o pantalla; o un medio tridimensional: proyector tridimensional u holografía. Para el primer tipo de medio el punto requiere dos coordenadas, mientras que para el segundo tres.
Hay dos formas de construir una gura abstracta expresadas por los siguientes cons-tructores:
8b hConstructores de Figure 8bi≡ (8a) 9a .
Figure(const Point & point); Figure(const Figure & figure);
Uses Figure 8a.
El primer constructor requiere un punto; el segundo copia la gura a partir del punto donde se encuentre otra gura.
El destructor del TAD hFigure 8ai debe ser virtual:
9a hConstructores de Figure 8bi+≡ (8a) / 8b
virtual ~Figure();
Uses Figure 8a.
pues de esa manera se garantiza la invocación de cualquier destructor asociado a una gura particular.
A un método que no altere o modique el estado del objeto suele llamársele obser-vador. En este sentido, una gura tiene un solo observador:
9b hObservadores de Figure9bi≡ (8a) 16 .
const Point & get_point() const;
el cual observa su punto de referencia en el plano. En C++, el calicador const sobre un
método indica al compilador que el método no altera el estado del objeto.
A un método que altera o modica el estado de un objeto se le calica de modicador o, a veces actuador. Los actuadores de una gura son los siguientes:
9c hModicadores de Figure 9ci≡ (8a)
virtual void draw() = 0;
virtual void move(const Point & point) = 0; virtual void erase() = 0;
virtual void scale(const Ratio & ratio) = 0; virtual void rotate(const Angle &angle) = 0;
Los nombres de métodos draw(), move() y erase() indican claramente su función5.
El método scale() ajusta el tamaño (escala) de una gura según un radio dado. El tipo Ratio especica una magnitud de escala que signica la proporción en que la escala se modica; si éste es menor que uno, entonces la gura se achica, de lo contrario se agranda. Finalmente, el método rotate() gira o rota la gura en el medio según un ángulo de tipo Angle.
Los hModicadores de Figure 9ci representan operaciones generales sobre una gura.
Podemos dibujarla, moverla hacia otro punto, borrarla, escalarla según alguna magnitud, o rotarla según algún ángulo en radianes cuyo signo indica el sentido de rotación. En todos los casos tratamos con guras abstractas, no concretas.
Al igual que con el tipo Point, en este estadio no es conveniente pensar en las im-plantaciones de los tipos Ratio y Angle. Sólo basta con conocer su utilización con objetos de tipo Figure circunscrita a n de dibujarlas.
Mención particular merecen dos calicadores sintácticos del C++. El primero lo
con-forma el prejo reservado virtual, el cual indica que la operación puede implantarse según la particularidad de la gura; por ejemplo, un cuadrado se dibuja diferente que un círculo. El segundo está dado por el hecho de inicializar la operación con el valor cero.
Esta es la sintaxis de C++ para denir un método virtual puro, el cual, a su vez, dene
una clase abstracta, o sea, abstracción pura, sin ningún carácter concreto, pues si no, la clase no sería abstracta. Para aprehender esta observación, comencemos por preguntarnos
¾a cuál gura se reere el TAD hFigure8ai?. La respuesta correcta es que no lo sabemos,
pues se trata de una clase abstracta, no de una concreta.
Los métodos virtuales puros tienen que implantarse en clases derivadas de la clase Figure que concretan implantaciones particulares de una gura abstracta. Por