Manual - Universidad Tecnológica de Izúcar de Matamoros

Anuncio
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Objetivo de la
unidad
Introducción a la
asignatura
Desglose de
temas
Glosario
MANUAL DEL DOCENTE
ESTRUCTURA DE DATOS I
SECRETARÍA DE EDUCACIÓN PÚBLICA
SUBSECRETARÍA DE EDUCACIÓN SUPERIOR E INVESTIGACIÓN CIENTÍFICA
SUBSISTEMA DE UNIVERSIDADES TECNOLÓGICAS
COORDINACIÓN GENERAL DE UNIVERSIDADES TECNOLÓGICAS
UNIVERSIDAD TECNOLÓGICA DE IZÚCAR DE MATAMOROS
ELABORÓ:
APROBÓ:
LIC. IVAN ANTONIO FLORES
TRUJILLO
LIC. ALEJANDRO SALVADOR
VARGAS
CUERPO COLEGIADO TIC-SI
REVISÓ:
CUERPO COLEGIADO TIC-SI
FECHA DE ENTRADA
EN VIGOR:
SEPTIEMBRE 2004
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
I. DIRECTORIO
Dr. Reyes Taméz Guerra
Secretario de Educación Pública
Dr. Julio Rubio Oca
Subsecretario de Educación Superior e Investigación Científica
DR. Arturo Nava Jaimes
Coordinador General de Universidades Tecnológicas
Reconocimientos
Universidad Tecnológica Izúcar de Matamoros
División de TIC-SI
Estructuras de Datos D.R.  2005
ESTA OBRA, SUS CARACTERÍSTICAS Y DERECHOS SON PROPIEDAD DE LA: COORDINACIÓN
GENERAL DE UNIVERSIDADES TECNOLÓGICAS (CGUT) FRANCISCO PETRARCA No. 321, COL.
CHAPULTEPEC MORALES, MÉXICO D.F. LOS DERECHOS DE PUBLICACIÓN PERTENECEN A LA CGUT.
QUEDA PROHIBIDA SU REPRODUCCIÓN PARCIAL O TOTAL POR CUALQUIER MEDIO, SIN
AUTORIZACIÓN PREVIA Y POR ESCRITO DEL TITULAR DE LOS DERECHOS.
ISBN (EN TRÁMITE)
IMPRESO EN MÉXICO.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
ÍNDICE
Preámbulo…………………………………………………………………………………………………………5
1. Antecedentes……………………………………………………………………………9.
1.1. Introducción a la orientación a objetos……………………………………………9
1.2. Tipos de datos abstractos…………………………………………………………...11
1.3. Definición de estructuras de datos………………………………………………….14
1.4. Acceso directo y Secuencial a los datos………………………………… 16
1.5. Iteradores……………………………………………………………………………………………… 16
1.6. Apuntadores o punteros…………………………………………………………...16
1.7. Plantillas (Templates)……………………………………………………………..34
1.8. La biblioteca STL…………………………………………………………………38
2. Arreglos………………………………………………………………………………..34
2.1. Introducción…..…………………………………………………………………...34
2.2. Arreglos dinámicos………………………………………………………………..36
2.3. La clase VECTOR………………………………………………………………...36
3. Listas…………………………………………………………………………………...44
3.1. Definición de lista…………………………………………………………………44
3.2. Operaciones básicas con listas…………………………………………………….45
3.2.1. Insertar un elemento en la lista……………………………………………..45
3.2.2. Localizar un elemento en la lista…………………………………………...47
3.2.3. Eliminar elementos de la lista……………………………………………...48
3.2.4. Moverse a través de una lista………………………………………………50
3.2.5. Borrar una lista completa…………………………………………………..50
3.2.6. Ejemplo de lista Ordenada………………………………………………....50
3.2.7. Ejemplo de lista en C++ usando clases…………………………………….53
3.3. La clase LIST de STL……………………………………………………………..54
4. Pilas…………………………………………………………………………………….58
4.1. Definición de pilas……………………………………………………………...…58
4.2. Operaciones básicas con pilas……………………………………………………..59
4.2.1. Push, insertar elemento…………………………………………………….59
4.2.2. Pop, leer y eliminar un elemento…………………………………………...60
4.3. Implementación de pilas…………………………………………………………..60
4.3.1. Ejemplo de pilas en C++ usando clases……………………………………61
4.4. La clase STACK de STL………………………………………………………….62
5. Colas…...………………………………………………………………………………65
5.1. Definición de colas………………………………………………………………..65
5.2. Operaciones básicas con colas…………………………………………………….66
5.2.1. Añadir un elemento………………………………………………………...66
5.2.2. Leer un elemento…………………………………………………………..67
5.3. Implementación de colas………………………………………………………….69
5.3.1. Ejemplo de una cola en C++ usando clases………………………………..70
5.4. La clase QUEUE de STL………………………………………………………….71
6. Árboles…….…………………………………………………………………………..73
6.1. Definición de árboles……………………………………………………………...73
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
6.2. Árboles binarios…………………………………………………………………...75
6.3. Árboles de Búsqueda binaria……………………………………………………...76
6.3.1. Operaciones en ABB. ……………………………………………………..76
6.3.1.1. Buscar un elemento…………………………………………………77
6.3.1.2. Insertar un elemento………………………………………………...77
6.3.1.3. Eliminar un elemento……………………………………………….78
6.3.2. Ejemplos de eliminación en un ABB………………………………………79
6.4. Aplicaciones de árboles…………………………………………………………...81
Bibliografía……………………………...…………………………………...……………82
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Preámbulo.
Resumidamente, el ANSI define un conjunto de reglas. Cualquier compilador de C
o de C++ debe cumplir esas reglas, si no, no puede considerarse un compilador de
C o C++. Estas reglas definen las características de un compilador en cuanto a
palabras reservadas del lenguaje, comportamiento de los elementos que lo
componen, funciones externas que se incluyen, etc. Un programa escrito en ANSI
C o en ANSI C++, podrá compilarse con cualquier compilador que cumpla la
norma ANSI. Se puede considerar como una homologación o etiqueta de calidad
de un compilador.
Todos los compiladores incluyen, además del ANSI, ciertas características no
ANSI, por ejemplo librerías para gráficos. Pero mientras no usemos ninguna de
esas características, sabremos que nuestros programas son transportables, es
decir, que podrán ejecutarse en cualquier ordenador y con cualquier sistema
operativo.
Este curso es sobre C++, con respecto a las diferencias entre C y C++, habría
mucho que hablar, pero no es este el momento adecuado. Pero para comprender
muchas de estas diferencias necesitarás cierto nivel de conocimientos de C++.
Probablemente este es el lugar más adecuado para explicar cómo se obtiene un
fichero ejecutable a partir de un programa C++.
Para empezar necesitamos un poco de vocabulario técnico. Veremos algunos
conceptos que se manejan frecuentemente en cualquier curso de programación y
sobre todo en manuales de C y C++.
Archivos fuente y programa o código fuente:
Los programas C y C++ se escriben con la ayuda de un editor de textos del mismo
modo que cualquier texto corriente. Los ficheros que contiene programas en C o
C++ en forma de texto se conocen como ficheros fuente, y el texto del programa
que contiene se conoce como programa fuente. Nosotros siempre escribiremos
programas fuente y los guardaremos en ficheros fuente.
Archivos objeto, código objeto y compiladores:
Los programas fuente no pueden ejecutarse. Son ficheros de texto, pensados para
que los comprendan los seres humanos, pero incomprensibles para los
ordenadores.
Para conseguir un programa ejecutable hay que seguir algunos pasos. El primero
es compilar o traducir el programa fuente a su código objeto equivalente. Este es
el trabajo que hacen los compiladores de C y C++. Consiste en obtener un fichero
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
equivalente a nuestro programa fuente comprensible para el ordenador, este
fichero se conoce como fichero objeto, y su contenido como código objeto.
Los compiladores son programas que leen un fichero de texto que contiene el
programa fuente y generan un fichero que contiene el código objeto.
El código objeto no tiene ningún significado para los seres humanos, al menos no
directamente. Además es diferente para cada ordenador y para cada sistema
operativo. Por lo tanto existen diferentes compiladores para diferentes sistemas
operativos y para cada tipo de ordenador.
Librerías:
Junto con los compiladores de C y C++, se incluyen ciertos ficheros llamados
librerías. Las librerías contienen el código objeto de muchos programas que
permiten hacer cosas comunes, como leer el teclado, escribir en la pantalla,
manejar números, realizar funciones matemáticas, etc. Las librerías están
clasificadas por el tipo de trabajos que hacen, hay librerías de entrada y salida,
matemáticas, de manejo de memoria, de manejo de textos, etc.
Hay un conjunto de librerías muy especiales, que se incluyen con todos los
compiladores de C y de C++. Son las librerías ANSI o estándar. Pero también hay
librerías no estándar, y dentro de estas las hay públicas y comerciales. En este
curso sólo usaremos librerías ANSI.
Archivos ejecutables y enlazadores:
Cuando obtenemos el fichero objeto, aún no hemos terminado el proceso. El
fichero objeto, a pesar de ser comprensible para el ordenador, no puede ser
ejecutado. Hay varias razones para eso:
1.
Nuestros programas usaran, en general, funciones que estarán incluidas en
librerías externas, ya sean ANSI o no. Es necesario combinar nuestro fichero
objeto con esas librerías para obtener un ejecutable.
2.
Muy a menudo, nuestros programas estarán compuestos por varios ficheros
fuente, y de cada uno de ellos se obtendrá un fichero objeto. Es necesario unir
todos los ficheros objeto, más las librerías en un único fichero ejecutable.
3.
Hay que dar ciertas instrucciones al ordenador para que cargue en memoria
el programa y los datos, y para que organice la memoria de modo que se
disponga de una pila de tamaño adecuado, etc. La pila es una zona de
memoria que se usa para que el programa intercambie datos con otros
programas o con otras partes del propio programa. Veremos esto con más
detalle durante el curso.
Existe un programa que hace todas estas cosas, se trata del "link", o enlazador. El
enlazador toma todos los ficheros objeto que componen nuestro programa, los
combina con los ficheros de librería que sea necesario y crea un fichero ejecutable.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Una vez terminada la fase de enlazado, ya podremos ejecutar nuestro programa.
Errores:
Por supuesto, somos humanos, y por lo tanto nos equivocamos. Los errores de
programación pueden clasificarse en varios tipos, dependiendo de la fase en que
se presenten.
Errores de sintaxis: son errores en el programa fuente. Pueden deberse a palabras
reservadas mal escritas, expresiones erróneas o incompletas, variables que no
existen, etc. Los errores de sintaxis se detectan en la fase de compilación. El
compilador, además de generar el código objeto, nos dará una lista de errores de
sintaxis. De hecho nos dará sólo una cosa o la otra, ya que si hay errores no es
posible generar un código objeto.
Avisos: además de errores, el compilador puede dar también avisos (warnings).
Los avisos son errores, pero no lo suficientemente graves como para impedir la
generación del código objeto. No obstante, es importante corregir estos avisos, ya
que el compilador tiene que decidir entre varias opciones, y sus decisiones no
tienen por qué coincidir con lo que nosotros pretendemos, se basan en las
directivas que los creadores del compilador decidieron durante su creación.
Errores de enlazado: el programa enlazador también puede encontrar errores.
Normalmente se refieren a funciones que no están definidas en ninguno de los
ficheros objetos ni en las librerías. Puede que hayamos olvidado incluir alguna
librería, o algún fichero objeto, o puede que hayamos olvidado definir alguna
función o variable, o lo hayamos hecho mal.
Errores de ejecución: incluso después de obtener un fichero ejecutable, es posible
que se produzcan errores. En el caso de los errores de ejecución normalmente no
obtendremos mensajes de error, sino que simplemente el programa terminará
bruscamente. Estos errores son más difíciles de detectar y corregir. Existen
programas auxiliares para buscar estos errores, son los llamados depuradores
(debuggers). Estos programas permiten detener la ejecución de nuestros
programas, inspeccionar variables y ejecutar nuestro programa paso a paso. Esto
resulta útil para detectar excepciones, errores sutiles, y fallos que se presentan
dependiendo de circunstancias distintas.
Errores de diseño: finalmente los errores más difíciles de corregir y prevenir. Si nos
hemos equivocado al diseñar nuestro algoritmo, no habrá ningún programa que
nos pueda ayudar a corregir los nuestros. Contra estos errores sólo cabe practicar
y pensar.
Propósito de C/C++
¿Qué clase de programas y aplicaciones se pueden crear usando C y C++?
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
La respuesta es muy sencilla: TODOS.
Tanto C como C++ son lenguajes de programación de propósito general. Todo
puede programarse con ellos, desde sistemas operativos y compiladores hasta
aplicaciones de bases de datos y procesadores de texto, pasando por juegos,
aplicaciones a medida, etc.
Oirás y leerás mucho sobre este tema. Sobre todo diciendo que estos lenguajes
son complicados y que requieren páginas y páginas de código para hacer cosas
que con otros lenguajes se hacen con pocas líneas. Esto es una verdad a medias.
Es cierto que un listado completo de un programa en C o C++ para gestión de
bases de datos (por poner un ejemplo) puede requerir varios miles de líneas de
código, y que su equivalente en Visual Basic sólo requiere unos pocos cientos.
Pero detrás de cada línea de estos compiladores de alto nivel hay cientos de líneas
de código en C, la mayor parte de estos compiladores están respaldados por
enormes librerías escritas en C. Nada te impide a ti, como programador, usar
librerías, e incluso crear las tuyas propias.
Una de las propiedades de C y C++ es la reutilización del código en forma de
librerías de usuario. Después de un tiempo trabajando, todos los programadores
desarrollan sus propias librerías para aquellas cosas que hacen frecuentemente. Y
además, raramente piensan en ello, se limitan a usarlas.
Además, los programas escritos en C o C++ tienen otras ventajas sobre el resto.
Con la excepción del ensamblador, generan los programas más compactos y
rápidos. El código es transportable, es decir, un programa ANSI en C o C++ podrá
ejecutarse en cualquier máquina y bajo cualquier sistema operativo. Y si es
necesario, proporcionan un acceso a bajo nivel de hardware sólo igualado por el
ensamblador.
Otra ventaja importante, C tiene más de 30 años de vida, y C++ casi 20 y no
parece que su uso se debilite demasiado. No se trata de un lenguaje de moda, y
probablemente a ambos les quede aún mucha vida por delante. Sólo hay que
pensar que sistemas operativos como Linux, Unix o incluso Windows se escriben
casi por completo en C.
Por último, existen varios compiladores de C y C++ gratuitos, o bajo la norma
GNU, así como cientos de librerías de todo propósito y miles de programadores en
todo el mundo, muchos de ellos dispuestos a compartir su experiencia y
conocimientos.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
1. Antecedentes
1.1. Introducción a la orientación a objetos.
La programación orientada a objetos (POO) es una nueva manera de
enfocar la programación. Desde sus comienzos, la programación ha estado
gobernada por varias metodologías. En cada punto crítico de la evolución de la
programación se creaba un nuevo enfoque para ayudar al programador a manejar
programas cada vez más complejos. Los primeros programas se crearon mediante
un proceso de cambio de los conmutadores del panel frontal de la computadora.
Obviamente, este enfoque solo es adecuado para programas pequeños. A
continuación se invento el lenguaje ensamblador que permitió escribir programas
más largos. El siguiente avance ocurrió en los años 50 cuando se invento el primer
lenguaje de alto nivel (FORTRAN).
Mediante un lenguaje de alto nivel, un programador estaba capacitado para
escribir programas que tuvieran una longitud de varios miles de líneas. Sin
embargo, el método de programación usado en el comienzo era un enfoque adhoc
que no solucionaba mucho. Mientras que esto esta bien para programas
relativamente cortos, se convierte en “código espagueti” ilegible y difícil de tratar
cuando se aplica a programas más largos. La eliminación del código espagueti se
consiguió con la creación de los lenguajes de programación estructurados en los
años sesenta. Estos lenguajes incluyen ALGOL y PASCAL. En definitiva, C es un
lenguaje estructurado, y casi todos los tipos de programas que se han estado
haciendo se podrían llamar programas estructurados.
Los programas estructurados se basan en estructuras de control bien
definidas, bloques de código, la ausencia del GOTO, y subrutinas independientes
que soportan recursividad y variables locales. La esencia de la programación
estructurada es la reducción de un programa a sus elementos constitutivos.
Mediante la programación estructurada un programador medio puede crear y
mantener programas de una longitud superior a 50,000 líneas.
Aunque la programación estructurada nos ha llevado a excelentes
resultados cuando se ha aplicado a programas moderadamente complejos, llega a
fallar en algún punto cuando el programa alcanza un cierto tamaño. Para poder
escribir programas de mayor complejidad se necesitaba de un nuevo enfoque en la
tarea de programación. A partir de este punto nace la programación orientada a
objetos (POO). La POO toma las mejores ideas incorporadas en la programación
estructurada y las combina con nuevos y potentes conceptos que permiten
organizar los programas de forma más efectiva. La POO permite descomponer un
problema en subgrupos relacionados. Cada subgrupo pasa a ser un objeto
autocontenido que contiene sus propias instrucciones y datos que le relacionan
con ese objeto. De esta manera, la complejidad se reduce y el programador puede
tratar programas más largos.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Todos los lenguajes de POO comparten tres características: encapsulación,
polimorfismo y herencia.
Encapsulación.
La encapsulación es el mecanismo que agrupa el código y los datos que
maneja y los mantiene protegidos frente a cualquier interferencia y mal uso. En un
lenguaje orientado a objetos, el código y los datos suelen empaquetarse de la
misma forma en que se crea una “caja negra” autocontenida. Dentro de la caja son
necesarios tanto el código como los datos. Cuando el código y los datos están
enlazados de esta manera, se ha creado un objeto. En otras palabras, un objeto es
el dispositivo que soporta encapsulación.
En un objeto, los datos y el código, o ambos, pueden ser privados para ese
objeto o públicos. Los datos o el código privado solo los conoce o son accesibles
por otra parte del objeto. Es decir, una parte del programa que esta fuera del
objeto no puede acceder al código o a los datos privados. Cuando los datos o el
código son públicos, otras partes del programa pueden acceder a ellos, incluso
aunque este definido dentro de un objeto. Normalmente, las partes públicas de un
objeto se utilizan para proporcionar una interfaz controlada a las partes privadas
del objeto.
Para todos los propósitos, un objeto es una variable de un tipo definido por
el usuario. Puede parecer extraño que un objeto que enlaza código y datos se
pueda contemplar como una variable. Sin embargo, en programación orientada a
objetos, este es precisamente el caso. Cada vez que se define un nuevo objeto, se
esta creando un nuevo tipo de dato. Cada instancia específica de este tipo de dato
es una variable compuesta.
Polimorfismo.
Polimorfismo (del Griego, cuyo significado es “muchas formas”) es la
cualidad que permite que un nombre se utilice para dos o más propósitos
relacionados pero técnicamente diferentes. El propósito del polimorfismo aplicado
a la POO es permitir poder usar un nombre para especificar una clase general de
acciones. Dentro de una clase general de acciones, la acción específica a aplicar
está determinada por el tipo de dato. Por ejemplo, en C, que no se basa
significativamente en el polimorfismo, la acción de valor absoluto requiere tres
funciones distintas: abs(), labs() y fabs(). Estas tres funciones calculan y devuelven
el valor absoluto de un entero, un entero largo y un valor real, respectivamente. Sin
embargo, en C++, que incorpora polimorfismo, a cada función se puede llamar
abs().
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
El tipo de datos utilizado para llamar a la función determina que versión
específica de la función se esta usando, es decir, es posible usar un nombre de
función para propósitos muy diferentes. Esto se llama sobrecarga de funciones.
De manera general, el concepto de polimorfismo es la idea de “una interfaz,
múltiples métodos”. Esto significa que es posible diseñar una interfaz genérica
para un grupo de actividades relacionadas. Sin embargo, la acción específica
ejecutada depende de los datos. La ventaja del polimorfismo es que ayuda a
reducir la complejidad permitiendo que la misma interfaz se utilice para especificar
una clase general de acción. Es trabajo del compilador seleccionar la acción
específica que se aplica a cada situación. El programador no necesita hacer esta
selección manualmente, solo necesita recordar y utilizar la interfaz general.
El polimorfismo se puede aplicar tanto a funciones como a operadores,
prácticamente todos los lenguajes de programación contienen una aplicación
limitada de polimorfismo cuando se relaciona con los operadores aritméticos, por
ejemplo, en C, el signo + se utiliza par añadir enteros, enteros largos, caracteres y
valores reales. En estos casos, el compilador automáticamente sabe que tipo de
aritmética debe aplicar, en C++, se puede ampliar este concepto a otros tipos de
datos que se definan, este tipo de polimorfismo se llama sobrecarga de
operadores.
Herencia.
La herencia es el proceso mediante el cual un objeto puede adquirir las
propiedades de otro. Mas en concreto, un objeto puede heredar un conjunto
general de propiedades a alas que puede añadir aquellas características que son
específicamente suyas. La herencia es importante porque permite que un objeto
soporte el concepto de clasificación jerárquica. Mucha información se hace
manejable gracias a esta clasificación, por ejemplo, la descripción de una casa.
Una casa es parte de una clase general llamada edificio, a su vez, edificio es una
parte de la clase mas general estructura, que es parte de la clase aun más general
de objetos que se puede llamar obra-hombre.
En cualquier caso, la clase hija hereda todas las cualidades asociadas con
la clase padre y le añade sus propias características definitorias. Sin el uso de
clasificaciones ordenadas, cada objeto tendría que definir todas las características
que se relacionan con él explícitamente.
Sin embargo, mediante el uso de la herencia, es posible describir un objeto
estableciendo la clase general (o clases) a las que pertenece, junto con aquellas
características específicas que le hacen único.
1.2 Tipos de datos abstractos.
Los tipos de datos abstractos (TDA) encapsulan datos y funciones que
trabajan con estos datos. Los datos no son visibles para el usuario en un tipo de
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
dato abstracto y el acceso a los datos es exclusivamente bajo el llamado a
funciones, también llamadas métodos. Así, el tipo de dato abstracto es
especificado por los métodos, no por los datos. En C++, los tipos de datos
abstractos son representados por clases, las cuales presentan a pequeña
deficiencia: el dato que representa el estado de un objeto de este tipo de dato
abstracto es visible (algunas veces no accesible) en la parte private de la clase
declarada para cada programa, la clase es reconocida mediante la vía # include.
Ejemplos de tipos de datos abstractos son: stack, queue, etc.
Los TDA por lo general manejan memoria dinámica, esto es, la asignación
dinámica de memoria es una característica que le permite al usuario crear tipos de
datos y estructuras de cualquier tamaño de acuerdo a las necesidades que se
tengan en el programa, para ello se emplean funciones típicas como malloc y free.
1.3 Definición de estructuras de datos.
En programación, una estructura de datos es una forma de organizar un
conjunto de datos elementales (un dato elemental es la mínima información que se
tiene en el sistema) con el objetivo de facilitar la manipulación de estos datos como
un todo y/o individualmente.
Una estructura de datos define la organización e interrelacionamiento de
estos, y un conjunto de operaciones que se pueden realizar sobre él. 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 se 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 las
frecuencias y el orden en que se realiza cada operación sobre los datos.
Algunas estructuras de datos utilizadas en programación son:

Arrays (Arreglos)
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
o
o



MANUAL DEL DOCENTE
Vectores
Matrices
Listas Enlazadas
o Listas Simples
o Listas Dobles
o Listas Circulares
Pilas (stack)
Colas (queue)

Árboles
o Árboles Binarios
 Árbol binario de búsqueda
 Árbol binario de búsqueda autoajustable
 Árboles Biselados (Árboles Splay)
o Árboles Multicamino (Multirrama)
 Árboles B
 Árboles B+
 Árboles B*

Conjuntos (set)

Grafos

Montículos (o heaps)
1.4 Acceso Directo y Secuencial a los datos

Secuencial. Para acceder a un objeto se debe acceder a los objetos
almacenados previamente en el archivo. El acceso secuencial exige
elemento a elemento, es necesario una exploración secuencial
comenzando desde el primer elemento.

Directo o Aleatorio. Se accede directamente al objeto, sin recorrer los
anteriores. El acceso directo permite procesar o acceder a un elemento
determinado haciendo una referencia directamente por su posición en el
soporte de almacenamiento.
1.5 Iteradores
Un iterador es una especie de puntero utilizado por un algoritmo para recorrer los
elementos almacenados en un contenedor. Dado que los distintos algoritmos
necesitan recorrer los contenedores de diversas maneras para realizar diversas
operaciones, y los contenedores deben ser accedidos de formas distintas, existen
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
diferentes tipos de iteradores. Cada contenedor de la Librería Estándar puede
generar un iterador con funcionalidad adecuada a la técnica de almacenamiento
que utiliza. Es precisamente el tipo de iterador requerido como argumento, lo que
distingue que algoritmos STL pueden ser utilizados con cada clase de contenedor.
Por ejemplo, si un contenedor solo dispone de iteradores de acceso secuencial, no
pueden utilizarse con algoritmos que exijan iteradores de acceso aleatorio.
1.6 Apuntadores o punteros.
Un puntero es una variable destinada a contener una dirección de memoria.
Esta dirección generalmente corresponde a otra variable, decimos entonces que el
puntero contiene la dirección de la variable o que “apunta” a ésta.
En el siguiente esquema suponemos que la variable puntero está cargada
con el número 4A20. Decimos, entonces, que la dirección 0x4A20 es apuntada por
el puntero.
Usos y ventajas de los punteros





Permiten el acceso a cualquier posición de la memoria, para ser leída o
para ser escrita (en los casos en que esto sea posible).
Permiten la transferencia de argumentos a las funciones, de modo que
puedan retener un valor nuevo, que resulta de aplicarles la función.
Permiten solicitar memoria que no fue reservada al inicio del programa.
Esto es el uso de memoria dinámica.
Son el soporte de enlace que utilizan estructuras avanzadas de datos en
memoria dinámica como las listas, pilas, colas y árboles.
Operan más eficientemente en los arrays, en comparación con el uso de
subíndices.
Declaración del tipo puntero
Sintaxis:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
tipo * nombre_puntero;
nombre_puntero es el identificador de la variable.
El * es el operador de indirección que nos indica que la variable es de tipo puntero.
tipo indica el tipo de la variable apuntada por el puntero (se lo denomina tipo base).
Ejemplo 1:
En la siguiente declaración, p es un puntero a float, i es una variable tipo int y q es
un puntero a int.
int i, *q;
float *p;
Operadores para punteros
Operador &: Es un operador unario que devuelve la dirección de memoria de su
operando. A través de este operador podemos relacionar a los punteros con las
variables a ser apuntadas por los mismos. No puede aplicarse a expresiones,
constantes o variables tipo registro.
Operador *: Es el operador de indirección. También es unario y es relativamente
complementario del anterior. Da acceso a la variable señalada por el puntero.
Ejemplo 2:
int A, *p;
p = &A;
A partir de esta última sentencia, p se carga con la dirección de A, es decir,
p apunta a A. Y al suceder esto, *p es el contenido de lo apuntado por p, o sea, *p
es equivalente a A.
Recordar siempre que en la declaración “int A, *p” se está expresando que la
variable A corresponde a entero y que *p también corresponde a entero. Cuando
se declara un puntero, éste contiene “basura”y por lo tanto no apunta a nada. Es
necesario inicializarlo con el valor adecuado.
Asignación de punteros
Un puntero puede ser asignado de 3 maneras:
A través de otro puntero.
Ejemplo 3:
int A, *p, *q;
p = &A;
q = p;
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
En esta última sentencia, p apunta a A y a q se le carga el valor que
contiene p (que es justamente la dirección de memoria de A), por lo tanto *p es
equivalente a *q.
Con la dirección de una variable.
Es el caso de la segunda línea del ejemplo anterior.
Directamente con la dirección que deba contener.
Esta asignación se realiza entregándole un valor entero. Por ejemplo, al
realizar la asignación p = 10; p contendrá 0x000A (si la expresamos en el sistema
de numeración hexadecimal). Lo habitual es entregarle la dirección expresada
directamente en hexadecimal, aunque esto es sólo por comodidad, pues las
direcciones son de por sí números binarios. Si realizamos la asignación p =
0x0B75; p contendrá 0x0B75
Punteros y argumentos de funciones
Los punteros son útiles a la hora de hacer un paso de argumentos a una
función, en caso de ser necesario que la función modifique el valor de la variable
transferida. Esto es similar a lo que sería, en otros lenguajes, un paso por
referencia.
Ejemplo 10:
La siguiente función pretende intercambiar los valores de dos variables de tipo
float:
void swap(float x, float y)
{
int aux;
aux = x;
x = y;
y = aux;
}
La forma de invocar esta función podría ser swap(a, b), siendo a y b las
variables cuyos valores queremos intercambiar. De todas maneras, la función no
altera los valores de a y de b porque sólo intercambia “copias” de estas variables.
Hace un pasaje de argumentos por valor. Por lo tanto, esto no es correcto.
Ejemplo 11:
Luego de analizar el ejemplo anterior, observemos la siguiente función.
void swap(float *x, float *y)
{
int aux;
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
aux = *x;
*x = *y;
*y = aux;
}
Con esta función sí es posible intercambiar los valores de dos variables, y la
forma de invocarla es swap(&a, &b). Dentro de esta función, los parámetros se
declaran para ser punteros, y al invocarla se pasan las direcciones de a y de b.
Tipos de datos apuntados por punteros
Los tipos base apuntados pueden ser todos los provistos de manera
standard por el C++: int, char, long, float, double. También puede apuntar a un tipo
no especificado de dato como void, o a otro puntero.
Los punteros también pueden apuntar a tipos de datos creados por el
programador como cadenas, estructuras y uniones. No pueden apuntar a campos
de bit pues no pueden contener direcciones no enteras.
Punteros a caracteres
Sintaxis:
char * nombre_cadena;
Ejemplo 13:
El siguiente programa imprime en pantalla la frase “color azul”.
char * frase = “color azul”;
printf (“%s” , frase);
En su representación interna, las cadenas (y también los arreglos) terminan
con el carácter nulo „\0‟,de tal manera que sea posible encontrar el fin.
Punteros a estructura
Sintaxis:
struct tipo_estructura * nombre_estructura;
tipo_estructura es un rótulo de una estructura.
nombre_estructura es el identificador del puntero.
Ejemplo 17:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Se crea un tipo de dato estructura con el nombre fecha. Luego se declara una
variable de tipo estructura-fecha con el nombre hoy, y finalmente un puntero a
estructura-fecha llamado F.
struct fecha {
int día;
int mes;
int anio;
};
struct fecha hoy;
struct fecha * F;
F todavía no apunta a una dirección válida. Hacemos que F apunte a hoy,
cargándolo con la dirección de esa variable tipo estructura-fecha.
F = &hoy;
Observe la presencia del operador &. Es necesario no confundir una
estructura con un arreglo. En el caso de las estructuras, el nombre es el
identificador de la variable y representa a la variable misma, no a su dirección. Por
eso usamos el operador mencionado.
1.7 Plantillas (Templates).
La generalidad es una propiedad que permite definir una clase o una
función sin tener que especificar el tipo de todos o alguno de sus miembros. Esta
propiedad no es imprescindible en un lenguaje de programación orientado a
objetos y ni siquiera es una de sus características. Esta característica del C++
apareció mucho más tarde que el resto del lenguaje, al final de la década de los
ochenta. Esta generalidad se alcanza con las plantillas (templates).
La utilidad principal de este tipo de clases o funciones es la de agrupar
variables cuyo tipo no esté predeterminado . Así el funcionamiento de una pila, una
cola, una lista, un conjunto, un diccionario o un array es el mismo
independientemente del tipo de datos que almacene (int, long, double, char, u
objetos de una clase definida por el usuario). En definitiva estas clases se definen
independientemente del tipo de variables que vayan a contener y es el usuario de
la clase el que debe indicar ese tipo en el momento de crear un objeto de esa
clase.
Plantillas de funciones
Supóngase que se quiere crear una función que devolviese el mínimo entre
dos valores independientemente de su tipo (se supone que ambos tienen el mismo
tipo). Se podría pensar en definir la función tantas veces como tipos de datos se
puedan presentar (int, long, float, double, etc.). Aunque esto es posible, éste es un
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
caso ideal para aplicar plantillas de funciones. Esto se puede hacer de la siguiente
manera:
// Declaración de la plantilla de función
template <class T> T minimo( T a, T b);
En ese caso con <classT> se está indicando que se trata de una plantilla
cuyo parámetro va a ser el tipo T y que tanto el valor de retorno como cada uno de
los dos argumentos va a ser de este tipo de dato T. En la definición y declaración
de la plantilla puede ser que se necesite utilizar mas de un tipo de dato e incluido
algún otro parámetro constante que pueda ser utilizado en las declaraciones. Por
ejemplo, si hubiera que pasar dos tipos a la plantilla, se podría escribir:
// Declaración de la plantilla de función con dos tipos de datos
template <class T1, class T2> void combinar(T1 a, T2 b);
Podría darse el caso también de que alguno de los argumentos o el valor de
retorno fuese de un tipo de dato constante y conocido. En ese caso se indicaría
explícitamente como en una función convencional. La definición de la plantilla de
función es como sigue:
// Definición de la plantilla de función
template <class T> T minimo(T a, T b)
{
if(a <= b)
return a;
else
return b;
}
A continuación se presenta un programa principal que utiliza la plantilla de
función recién definida:
#include <iostream.h>
template <class T> T minimo(T a, T b);
void main(void)
{
int euno=1;
int edos=5;
cout << minimo(euno, edos) << endl;
long luno=1;
long ldos=5;
cout << minimo(luno, ldos) << endl;
char cuno='a';
char cdos='d';
cout << minimo(cuno, cdos) << endl;
double duno=1.8;
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
double ddos=1.9;
cout << minimo(duno, ddos) << endl;
}
La ejecución del programa anterior demuestra que el tipo de los argumentos
y el valor de retorno de la función minimo() se particularizan en cada caso a los de
la llamada. Es obvio también que se producirá un error si se pasan como
argumentos dos variables de distinto tipo, por lo que el usuario de la plantilla de
función debe ser muy cuidadoso en el paso de los argumentos. Seguidamente se
presenta un nuevo ejemplo de función para permutar el valor de dos variables:
#include <iostream.h>
template <class S> void permutar(S&, S&);
void main(void)
{
int i=2, j=3;
cout << "i=" << i << " " << "j=" << j << endl;
permutar(i, j);
cout << "i=" << i << " " << "j=" << j << endl;
double x=2.5, y=3.5;
cout << "x=" << x << " " << "y=" << y << endl;
permutar(x, y);
cout << "x=" << x << " " << "y=" << y << endl;
}
template <class S> void permutar(S& a, S& b)
{ S temp;
temp = a;
a = b;
b = temp;
}
Plantillas de clases
De una manera semejante a como se hace para las funciones se puede
generalizar para el caso de las clases por medio de plantillas de clases. Se definirá
un parámetro que indicará el tipo de datos con los que más adelante se crearán
los objetos. Se presenta a continuación un ejemplo completo de utilización de
plantillas de clases basado en una pila muy simple (sin listas vinculadas y sin
reserva dinámica de memoria):
// fichero Pila.h
template <class T>
// declaración de la clase
class Pila
{
public:
Pila(int nelem=10); // constructor
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
void Poner(T);
void Imprimir();
private:
int nelementos;
T* cadena;
int limite;
};
// definición del constructor
template <class T> Pila<T>::Pila(int nelem)
{
nelementos = nelem;
cadena = new T(nelementos);
limite = 0;
};
// definición de las funciones miembro
template <class T> void Pila<T>::Poner(T elem)
{
if (limite < nelementos)
cadena[limite++] = elem;
};
template <class T> void Pila<T>::Imprimir()
{
int i;
for (i=0; i<limite; i++)
cout << cadena[i] << endl;
};
El programa principal puede ser el que sigue:
#include <iostream.h>
#include "Pila.h"
void main()
{
Pila <int> p1(6);
p1.Poner(2);
p1.Poner(4);
p1.Imprimir();
Pila <char> p2(6);
p2.Poner('a');
p2.Poner('b');
p2.Imprimir();
}
En este programa principal se definen dos objetos p1 y p2 de la clase Pila.
En p1 el parámetro T vale int y en p2 ese parámetro vale char. El funcionamiento
de todas las variables y funciones miembro se particulariza en cada caso para
esos tipos de variable. Es necesario recordar de nuevo que el usuario de este tipo
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
de clases debe poner un muy especial cuidado en pasar siempre el tipo de
argumento correcto.
Puede pensarse que las plantillas y el polimorfismo son dos utilidades que
se excluyen mutuamente. Aunque es verdad que el parecido entre ambas es
grande, hay también algunas diferencias que pueden hacer necesarias ambas
características. El polimorfismo necesita punteros y su generalidad se limita a
jerarquías. Recuérdese que el polimorfismo se basa en que en el momento de
compilación se desconoce a qué clase de la jerarquía va a apuntar un puntero que
se ha definido como puntero a la clase base. Desde este punto de vista las
plantillas pueden considerarse como una ampliación del polimorfismo. Una
desventaja de las plantillas es que tienden a crear un código ejecutable grande
porque se crean tantas versiones de las funciones como son necesarias.
1.8 La biblioteca STL.
La Biblioteca Estándar de Patrones, comúnmente conocida en inglés por
sus siglas STL (Standard Template Library) es una biblioteca de C++ que incluye
la mayoría de algoritmos y estructuras de datos que se suelen utilizar en
Informática. Básicamente está compuesta de:



clases contenedoras, es decir, patrones (templates) que permiten
almacenar objetos de muy diversos tipos,
algoritmos de uso frecuente, e
iteradores, que nos permitirán recorrer los elementos incluidos en los
contenedores.
También incluye "objetos función", que son generalizaciones de funciones
(clases útiles por el procesamiento que realizan y no por los datos que contienen),
y "adaptadores", que modifican el interfaz de ciertos contenedores e iteradores de
forma puedan ser manejados más fácilmente.
Esta biblioteca es genérica en tanto sus componentes están altamente
parametrizados, ya que casi todos son patrones, lo que permite su instancia con
cualquier otro tipo de objeto, lo que hace que la STL se configure como una gran
herramienta para la programación de aplicaciones en C++.
Los tipos de contenedores que nos podemos encontrar en la biblioteca son
los siguientes:

Secuenciales:
o Vectores: contienen elementos contiguos almacenados al estilo de
un array o vector del lenguaje C++.
o Listas: secuencias de elementos almacenados en una lista enlazada.
o Deques: contenedores parecidos a los vectores, excepto que
permiten inserciones y borrados en tiempo constante tanto al
principio como al final.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I


MANUAL DEL DOCENTE
Adaptadores:
o Colas: contenedores que ofrecen la funcionalidad de listas " primero
en entrar, primero en salir".
o Pilas: contenedores asociados a listas " primero en entrar, último en
salir".
o Colas con prioridad: en este caso, los elementos de la cola salen de
ella de acuerdo con una prioridad (que se estableció en la inserción).
Asociativos.
o Conjuntos de bits: contenedor para almacenar bits.
o Mapas: almacenan pares "clave, objeto", es decir, almacenan objetos
referidos mediante un identificador único.
o Multimapas: mapas que permiten claves duplicadas.
o Conjuntos: conjuntos ordenados de objetos únicos.
o Multiconjuntos: conjuntos ordenados de objetos que pueden estar
duplicados.
Todas estas clases de objetos tienen la posibilidad de poder ser "recorridas"
utilizan iteradores. Para ello, cada una de ellas tiene una subclase (o varias
subclases) que nos permiten colocarnos en el primer elemento de un objeto,
avanzar al siguiente elemento, ver el elemento (o modificarlo) y comprobar si ya
hemos llegado hasta el final.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
2.
MANUAL DEL DOCENTE
Arreglos
2.1 Introducción.
Los arreglos (arrays) permiten agrupar datos usando un mismo
identificador. Todos los elementos de un array son del mismo tipo, y para acceder
a cada elemento se usan subíndices.
Sintaxis:
<tipo> <identificador>[<núm_elemen>][[<núm_elemen>]...];
Los valores para el número de elementos deben ser constantes, y se
pueden usar tantas dimensiones como queramos, limitado sólo por la memoria
disponible.
Cuando sólo se usa una dimensión se suele hablar de listas o vectores,
cuando se usan dos, de tablas.
Ahora podemos ver que las cadenas de caracteres son un tipo especial de
arrays. Se trata en realidad de arrays de una dimensión de objetos de tipo char.
Los subíndices son enteros, y pueden tomar valores desde 0 hasta <número
de elementos>-1. Esto es muy importante, y hay que tener mucho cuidado, por
ejemplo:
int Vector[10];
Creará un array con 10 enteros a los que accederemos como Vector[0] a Vector[9].
Como subíndice podremos usar cualquier expresión entera.
En general C++ no verifica el ámbito de los subíndices. Si declaramos un
array de 10 elementos, no obtendremos errores al acceder al elemento 11. Sin
embargo, si asignamos valores a elementos fuera del ámbito declarado,
estaremos accediendo a zonas de memoria que pueden pertenecer a otras
variables o incluso al código ejecutable de nuestro programa, con consecuencias
generalmente desastrosas.
Ejemplo:
int Tabla[10][10];
char DimensionN[4][15][6][8][11];
...
DimensionN[3][11][0][4][6] = DimensionN[0][12][5][3][1];
Tabla[0][0] += Tabla[9][9];
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Cada elemento de Tabla, desde Tabla[0][0] hasta Tabla[9][9] es un entero.
Del mismo modo, cada elemento de DimensionN es un carácter.
Inicialización de arrays.
Los arrays pueden ser inicializados en la declaración.
Ejemplos:
float R[10] = {2, 32, 4.6, 2, 1, 0.5, 3, 8, 0, 12};
float S[] = {2, 32, 4.6, 2, 1, 0.5, 3, 8, 0, 12};
int N[] = {1, 2, 3, 6};
int M[][3] = { 213, 32, 32, 32, 43, 32, 3, 43, 21};
char Mensaje[] = "Error de lectura";
char Saludo[] = {'H', 'o', 'l', 'a', 0};
En estos casos no es obligatorio especificar el tamaño para la primera
dimensión, como ocurre en los ejemplos de las líneas 2, 3, 4, 5 y 6. En estos casos
la dimensión que queda indefinida se calcula a partir del número de elementos en
la lista de valores iniciales.
En el caso 2, el número de elementos es 10, ya que hay diez valores en la lista.
En el caso 3, será 4.
En el caso 4, será 3, ya que hay 9 valores, y la segunda dimensión es 3: 9/3=3.
Y en el caso 5, el número de elementos es 17, 16 caracteres más el cero de fin de
cadena.
Operadores con arrays
Ya hemos visto que se puede usar el operador de asignación con arrays para
asignar valores iniciales. El otro operador que tiene sentido con los arrays es
sizeof.
Aplicado a un array, el operador sizeof devuelve el tamaño de todo el array en
bytes. Podemos obtener el número de elementos dividiendo ese valor entre el
tamaño de uno de los elementos.
#include <iostream>
using namespace std;
int main()
{
int array[231];
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
}
MANUAL DEL DOCENTE
cout << "Número de elementos: "
<< sizeof(array)/sizeof(int) << endl;
cout << "Número de elementos: "
<< sizeof(array)/sizeof(array[0]) << endl;
cin.get();
return 0;
Las dos formas son válidas, pero la segunda es, tal vez, más general.
2.2 Arreglos dinámicos.
Si al iniciar un programa no se sabe el número de elementos del que va a
constar el array, o no se quiere poner un límite predetermiado, lo que hay que
hacer es definir el array dinámicamente. Para hacer esto, primero se define un
puntero, que señalará la dirección de memoria del primer elemento del array:
tipo_de_elemento *nombre_de_array;
y luego se utiliza la función malloc (contenida en stdlib.h) para reservar memoria:
nombre_de_array=(tipo_de_elemento *)malloc(tamaño);
donde tamaño es el número de elementos del array por el tamaño en bytes de
cada elemento. La función malloc devuelve un puntero void, que indica la posición
del primer elemento. Antes de asignarlo a nuestro puntero, hay que convertir el
puntero que devuelve el malloc al tipo de nuestro puntero (ya que no se pueden
igualar punteros de distintos tipos).
Para arrays bidimensionales, hay que hacerlo dimensión a dimensión; primero se
define un puntero de punteros:
int **mapa;
Luego se reserva memoria para los punteros:
mapa=(int **)malloc(sizeof(int *)*N1);
y, por último, para cada puntero se reserva memoria para los elementos:
for(i1=0;i1<N1;i1++)
mapa[i1]=(int *)malloc(sizeof(int)*N2);
Ya se puede utilizar el array normalmente. Para arrays de más de dos
dimensiones, se hace de forma similar.
2.3 La clase VECTOR.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Aunque cada uno de los contenedores que ofrece la STL tienen
características diferentes, en esta sección presentaremos el contenedor vector. El
contenedor vector permite almacenar cero o más objetos del mismo tipo, pudiendo
acceder a ellos individualmente mediante un índice, es decir, acceso aleatorio. En
este sentido, es una extensión del vector o array que ofrece C++, aunque en este
caso el número de elementos de un objeto vector puede variar dinámicamente. La
gestión de la memoria se hace de manera totalmente transparente al usuario. Se
define como una clase patrón, lo que implica que puede albergar objetos de
cualquier tipo. En cuanto a las operaciones más frecuentes, ofrece un tiempo
constante en inserción y borrados de elementos al final, y lineal al comienzo o en
la mitad del vector.
La declaración más común de un objeto de tipo vector se realiza de la
siguiente manera:
vector<tipo> objeto;
donde tipo puede ser cualquier tipo o clase de los que ofrece C++, así como
cualquier otra clase implementada por un usuario. Así, podríamos declarar los
siguientes vectores:
vector<double> vectorReales; // De números reales.
vector<string> vectorCadenas; // De cadenas de caracteres.
vector<MiClase> vectorObj; // Contendrá objetos de una clase construida por
un usuario.
Una declaración:
vector <int> vectorEnteros(10);
crea un vector de diez enteros. Si deseamos inicializarlo a algún valor en
concreto, entonces añadimos un argumento más al constructor:
vector <int> vectorEnteros(10,-1);
De manera general las funciones miembro y operadores manejados por
vector mas utilizadas son:
size
size_type size() const; Devuelve el número de elementos
almacenados en el vector. El tipo size_type es un entero sin signo.
empty
bool empty() const; Devuelve true si el número de elementos es cero y
false en caso contrario.
void push_back(const T& x); Añade un elemento x al final del vector. T
push_back es el tipo de dato de los elementos del vector.
vector<int> a; a.push_back(5);
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
begin
iterator begin(); Devuelve un iterador que referencia el comienzo del
vector.
end
iterator end(); Devuelve un iterador que referencia la posición
siguiente al final del vector.
erase
void erase(iterator first, iterator last); Borra los elementos del vector
que estén situados entre los iteradores first y last. Por ejemplo, para
borrar todos los elementos de un vector, se puede utilizar:
vector<int> a; a.erase(a.begin(),a.end()); // Se borran todos los
elementos entre la primera y la última posición.
capacity
size_type capacity() const; Devuelve el número de elementos con que
se ha creado el vector. Siempre es mayor o igual que size.
void clear (); Borra todos los elementos de un vector.
clear
vector<int> a; a.clear(); // Se borran todos los elementos.
Algunos de los operadores de esta clase son:
=
El operador de asignación sustituye el contenido de un vector por el de otro.
vector<int> a;
vector<int> b;
a.push_back(5);
a.push_back(10);
b.push_back(3);
b = a; // El vector b contiene dos elementos: 5 y 10 (los mismos que contenía el
vector b).
==
Comprueba si dos vectores contienen los mismos elementos. Para ello lleva a
cabo una comparación elemento a elemento.
[]
El operador de subíndice devuelve una referencia a un elemento del vector.
Una referencia con un subíndice igual a cero devuelve el primer elemento del
vector. Así, el rango del subíndice debe estar entre cero y size()-1.
vector<double> vec;
vec.push_back(1.2);
vec.push_back(4.5);
vec[1] = vec[0] + 5.0; vec[0] = 2.7; // El vector contiene ahora los elementos
2.7, 6.2
Veamos algunos ejemplos. El siguiente programa muestra dos constructores
simples:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
#include <iostream>
#include <cassert>
#include <vector>
using namespace std;
int main()
{
cout << "Demostrando los constructores más simple del vector" << endl;
vector<char> vector1, vector2(3, 'x');
assert (vector1.size() == 0);
assert (vector2.size() == 3);
assert (vector2[0] == 'x' && vector2[1] == 'x' &&
vector2[2] == 'x');
assert (vector2 == vector<char>(3, 'x') &&
vector2 != vector<char>(4, 'x'));
cout << " --- Ok." << endl;
return 0;
}
Otro constructor, el de copia, crea un vector a partir de un "trozo" de un array u
otro vector, o de un vector completo. Veamos el siguiente ejemplo:
#include <iostream>
#include <cassert>
#include <vector>
using namespace std;
int main()
{
cout << "Demostrando el constructor de copia del vector." << endl;
int numeros[] = {0,1, 2, 3, 4, 5, 6, 7, 8, 9};
vector<int> otroVector(&(numeros[0]),&(numeros[6]));
vector<int> tercerVector(otroVector.begin(), otroVector.end());
assert (tercerVector == otroVector);
vector<int> hijito1(otroVector);
assert (hijito1 == otroVector);
vector<int> hijito2 = otroVector;
assert (hijito2 == tercerVector);
return 0;
}
Este otro programa muestra el uso de algunas de las funciones miembro
anteriormente descritas:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<int> v(5);
int x;
int cont = 0;
cout << "tam v:" <<
v.size() << " y tam.max.: " << v.capacity()
<< endl;
do {
cin >> x;
if (x!=-1) { // Lo añadimos
if (cont<v.size())
v[cont] = x;
else {
if (v.size()>=v.capacity()) //Hacemos v + grande
v.reserve(2*v.capacity());
v.push_back(x);
}
cont++;
cout << "Añadido: " << v[cont-1] << ", tam v:" <<
v.size() << " y tam.max.: " << v.capacity()
<< endl;
}
} while (x!=-1);
}
cout << "Vector: ";
for (vector<int>::const_iterator i= v.begin();
i!=v.end(); i++)
cout << *i << " ";
cout << endl;
return 0;
Por último, veamos algunos algoritmos que pueden ser útiles:
find -> Busca un elemento, pasado como tercer argumento, en los
elementos de un contenedor incluidos en el rango especificado por un iterador de
inicio y otro de fin (dos primeros argumentos).
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
#include <iostream>
#include <cassert>
#include <vector>
#include <algorithm> // Biblioteca para find
using namespace std;
int main()
{
vector<char> vectorCar;
vectorCar.push_back('h');
vectorCar.push_back('o');
vectorCar.push_back('l');
vectorCar.push_back('a');
vectorCar.push_back(' ');
vectorCar.push_back('q');
vectorCar.push_back('u');
vectorCar.push_back('e');
vectorCar.push_back(' ');
vectorCar.push_back('t');
vectorCar.push_back('a');
vectorCar.push_back('l');
cout << "Demostración de la función find con un vector de caracteres. " << endl;
// Búsqueda del 5.:
vector<char>::iterator donde = find(vectorCar.begin(), vectorCar.end(), 'q');
assert (*donde == 'q' && *(donde + 1) == 'u');
cout << " --- Ok." << endl;
return 0;
}
reverse -> Invierte los elementos de un contenedor contenidos en un rango
determinado por dos iteradores, uno de inicio y otro de final.
#include <iostream>
#include <vector>
#include <cassert>
#include <algorithm> // reverse
using namespace std;
int main()
{
vector<char> vectorCar;
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
vectorCar.push_back('h');
vectorCar.push_back('o');
vectorCar.push_back('l');
vectorCar.push_back('a');
vector<char> vectorCarAlReves;
vectorCarAlReves.push_back('a');
vectorCarAlReves.push_back('l');
vectorCarAlReves.push_back('o');
vectorCarAlReves.push_back('h');
cout << "Invirtiendo un vector. " << endl;
reverse(vectorCar.begin(), vectorCar.end());
assert (vectorCar == vectorCarAlReves);
cout << " --- Ok." << endl;
return 0;
}
sort -> ordena de manera creciente los elementos de un contenedor
incluidos en el rango establecido por un iterador inicial y otro final, pasados como
argumentos.
#include <iostream>
#include <algorithm>
#include <vector>
#include <cassert>
using namespace std;
int main()
{
vector<int> v(1000);
for (int i = 0; i < 1000; ++i)
v[i] = 1000 - i - 1;
sort(v.begin(), v.end());
for (int i = 0; i < 1000; ++i)
assert (v[i] == i);
cout << " --- Ok." << endl;
return 0;
}
forma:
También tenemos la opción de ordenador en orden inverso de la siguiente
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
#include <iostream>
#include <algorithm>
#include <vector>
#include <cassert>
using namespace std;
bool descendente(int a, int b){ return a < b; }
int main()
{
vector<int> v(1000);
for (int i = 0; i < 1000; ++i)
v[i] = i;
sort(v.begin(), v.end(), descendente);
}
for (int i = 999; i >= 0; --i)
assert (v[i] == i);
cout << " --- Ok." << endl;
return 0;
copy -> Recibe tres argumentos: dos iteradores indicando principio y fin del
vector origen y un tercero que indica el inicio en el destino.
#include <iostream>
#include <cassert>
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main()
{
cout << "Ejemplo de copia genérica." << endl;
string s("abcdefghihklmnopqrstuvwxyz");
vector<char> vector1(s.begin(), s.end());
vector<char> vector2(vector1.size()); //Con el mismo tamaño que vector1
// Copy vector1 to vector2:
copy(vector1.begin(), vector1.end(), vector2.begin());
assert (vector1 == vector2);
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
cout << " --- Ok." << endl;
return 0;
}
3 Listas
3.1 Definición de lista
Una lista es un conjunto de elementos del mismo tipo. Una propiedad
importante con la que se caracteriza a las listas es que su longitud puede
aumentar o disminuir, según se requiera. Es más, podemos insertar o eliminar
elementos en cualquier posición de una lista.
Las listas se pueden implementar de 2 modos distintos: mediante vectores o
mediante estructuras dinámicas enlazadas por apuntadores. El trabajo de listas
con vectores tiene una limitante: las listas tienen una longitud variable (dinámicas)
y los vectores tienen longitud estática. No podríamos definir un arreglo de tamaño
N donde solamente utilicemos los primero N-m cajones. O viceversa, declarar un
arreglo demasiado pequeño para nuestras necesidades. Esto es el porque de las
estructuras dinámicas de datos.
La forma más simple de estructura dinámica es la lista. En esta forma los
nodos se organizan de modo que cada uno apunta al siguiente, y el último no
apunta a nada, es decir, el puntero del nodo siguiente vale NULL.
En las listas abiertas existe un nodo especial: el primero. Normalmente
diremos que nuestra lista es un puntero a ese primer nodo y llamaremos a ese
nodo la cabeza de la lista. Eso es porque mediante ese único puntero podemos
acceder a toda la lista.
Cuando el puntero que usamos para acceder a la lista vale NULL, diremos
que la lista está vacía. El nodo típico para construir listas tiene esta forma:
struct nodo {
int dato;
struct nodo *siguiente;
};
En el ejemplo, cada elemento de la lista sólo contiene un dato de tipo
entero, pero en la práctica no hay límite en cuanto a la complejidad de los datos a
almacenar.
Normalmente se definen varios tipos que facilitan el manejo de las listas, la
declaración de tipos puede tener una forma parecida a esta:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
typedef struct _nodo {
int dato;
struct _nodo *siguiente;
} tipoNodo;
typedef tipoNodo *pNodo;
typedef tipoNodo *Lista;
Donde:
tipoNodo es el tipo para declarar nodos.
pNodo es el tipo para declarar punteros a un nodo.
Lista es el tipo para declarar listas.
Como puede verse, un puntero a un nodo y una lista son la misma cosa. En
realidad, cualquier puntero a un nodo es una lista, cuyo primer elemento es el
nodo apuntado, por ejemplo:
Es muy importante que un programa nunca pierda el valor del puntero al
primer elemento, ya que si no existe ninguna copia de ese valor, y se pierde, será
imposible acceder al nodo y no podremos liberar el espacio de memoria que
ocupa.
3.2 Operaciones básicas con listas.
Con las listas tendremos un pequeño repertorio de operaciones básicas que
se pueden realizar:




Añadir o insertar elementos.
Buscar o localizar elementos.
Borrar elementos.
Moverse a través de una lista, anterior, siguiente, primero.
Cada una de estas operaciones tendrá varios casos especiales, por
ejemplo, no será lo mismo insertar un nodo en una lista vacía, o al principio de una
lista no vacía, o la final, o en una posición intermedia.
3.2.1 Insertar elementos en una lista.
Insertar un elemento en una lista vacía:
Este es, evidentemente, el caso más sencillo. Partimos de que ya tenemos
el nodo a insertar y, por supuesto un puntero que apunte a él, además el puntero a
la lista valdrá NULL:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
El proceso es muy simple, bastará con que:
1. nodo->siguiente apunte a NULL.
2. Lista apunte a nodo.
Insertar un elemento en la primera posición de una lista:
Podemos considerar el caso anterior como un caso particular de éste, la
única diferencia es que en el caso anterior la lista es una lista vacía, pero siempre
podemos, y debemos considerar una lista vacía como una lista.
De nuevo partimos de un nodo a insertar, con un puntero que apunte a él, y
de una lista, en este caso no vacía:
El proceso sigue siendo muy sencillo:
1. Hacemos que nodo->siguiente apunte a Lista.
2. Hacemos que Lista apunte a nodo.
Insertar un elemento en la última posición de una lista:
Este es otro caso especial. Para este caso partimos de una lista no vacía:
El proceso en este caso tampoco es excesivamente complicado:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
1. Necesitamos un puntero que señale al último elemento de la lista. La
manera de conseguirlo es empezar por el primero y avanzar hasta que
el nodo que tenga como siguiente el valor NULL.
2. Hacer que nodo->siguiente sea NULL.
3. Hacer que ultimo->siguiente sea nodo.
Insertar un elemento a continuación de un nodo cualquiera de una lista:
De nuevo podemos considerar el caso anterior como un caso particular de
este. Ahora el nodo "anterior" será aquel a continuación del cual insertaremos el
nuevo nodo:
Suponemos que ya disponemos del nuevo nodo a insertar, apuntado por
nodo, y un puntero al nodo a continuación del que lo insertaremos.
El proceso a seguir será:
1. Hacer que nodo->siguiente señale a anterior->siguiente.
2. Hacer que anterior->siguiente señale a nodo.
3.2.2 Localizar elementos en una lista.
Muy a menudo necesitamos recorrer una lista, ya sea buscando un valor
particular o un nodo concreto. Las listas abiertas sólo pueden recorrerse en un
sentido, ya que cada nodo apunta al siguiente, pero no se puede obtener, por
ejemplo, un puntero al nodo anterior desde un nodo cualquiera si no se empieza
desde el principio.
Para recorrer una lista procederemos siempre del mismo modo, usaremos
un puntero auxiliar como índice:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
1. Asignamos al puntero índice el valor de Lista.
2. Abriremos un bucle que al menos debe tener una condición, que el
índice no sea NULL.
3. Dentro del bucle asignaremos al índice el valor del nodo siguiente al
índice actual.
Por ejemplo, para mostrar todos los valores de los nodos de una lista,
podemos usar el siguiente bucle:
typedef struct _nodo {
int dato;
struct _nodo *siguiente;
} tipoNodo;
typedef tipoNodo *pNodo;
typedef tipoNodo *Lista;
...
pNodo indice;
...
indice = Lista;
while(indice) {
printf("%d\n", indice->dato);
indice = indice->siguiente;
}
...
Supongamos que sólo queremos mostrar los valores hasta
encontremos uno que sea mayor que 100, podemos sustituir el bucle por:
que
...
indice = Lista;
while(indice && indice->dato <= 100) {
printf("%d\n", indice->dato);
indice = indice->siguiente;
}
...
Si analizamos la condición del bucle, tal vez encontremos un posible error:
¿Qué pasaría si ningún valor es mayor que 100, y alcancemos el final de la lista?.
Podría pensarse que cuando indice sea NULL, si intentamos acceder a indice>dato se producirá un error.
En general eso será cierto, no puede accederse a punteros nulos. Pero en
este caso, este acceso está dentro de una condición y forma parte de una
expresión "and". Recordemos que cuando se evalúa una expresión "and", se
comienza por la izquierda, y la evaluación se abandona cuando una de las
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
expresiones resulta falsa, de modo que la expresión "indice->dato <= 100" nunca
se evaluará si indice es NULL.
Si hubiéramos escrito la condición al revés, el programa nunca funcionaría
bien. Esto es algo muy importante cuando se trabaja con punteros.
3.2.3 Eliminar elementos en una lista.
Nuevamente podemos encontrarnos con varios casos, según la posición del
nodo a eliminar.
Eliminar el primer nodo de una lista abierta:
Es el caso más simple. Partimos de una lista con uno o más nodos, y
usaremos un puntero auxiliar, nodo:
1. Hacemos que nodo apunte al primer elemento de la lista, es decir a
Lista.
2. Asignamos a Lista la dirección del segundo nodo de la lista: Lista>siguiente.
3. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
Si no guardamos el puntero al primer nodo antes de actualizar Lista,
después nos resultaría imposible liberar la memoria que ocupa. Si liberamos la
memoria antes de actualizar Lista, perderemos el puntero al segundo nodo.
Si la lista sólo tiene un nodo, el proceso es también válido, ya que el valor
de Lista->siguiente es NULL, y después de eliminar el primer nodo la lista quedará
vacía, y el valor de Lista será NULL.
De hecho, el proceso que se suele usar para borrar listas completas es
eliminar el primer nodo hasta que la lista esté vacía.
Eliminar un nodo cualquiera de una lista abierta:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
En todos los demás casos, eliminar un nodo se puede hacer siempre del
mismo modo. Supongamos que tenemos una lista con al menos dos elementos, y
un puntero al nodo anterior al que queremos eliminar. Y un puntero auxiliar nodo.
El proceso es parecido al del caso anterior:
1. Hacemos que nodo apunte al nodo que queremos borrar.
2. Ahora, asignamos como nodo siguiente del nodo anterior, el siguiente al
que queremos eliminar: anterior->siguiente = nodo->siguiente.
3. Eliminamos la memoria asociada al nodo que queremos eliminar.
Si el nodo a eliminar es el último, es procedimiento es igualmente válido, ya
que anterior pasará a ser el último, y anterior->siguiente valdrá NULL.
3.2.4 Moverse a través de una lista.
Sólo hay un modo de moverse a través de una lista abierta, hacia delante.
Aún así, a veces necesitaremos acceder a determinados elementos de una lista
abierta. Veremos ahora como acceder a los más corrientes: el primero, el último, el
siguiente y el anterior.
Primer elemento de una lista:
El primer elemento es el más accesible, ya que es a ese a que apunta el
puntero que define la lista. Para obtener un puntero al primer elemento bastará con
copiar el puntero Lista.
Elemento siguiente a uno cualquiera:
Supongamos que tenemos un puntero nodo que señala a un elemento de
una lista. Para obtener un puntero al siguiente bastará con asignarle el campo
"siguiente" del nodo, nodo->siguiente.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Elemento anterior a uno cualquiera:
Ya hemos dicho que no es posible retroceder en una lista, de modo que
para obtener un puntero al nodo anterior a uno dado tendremos que partir del
primero, e ir avanzando hasta que el nodo siguiente sea precisamente nuestro
nodo.
Último elemento de una lista:
Para obtener un puntero al último elemento de una lista partiremos de un
nodo cualquiera, por ejemplo el primero, y avanzaremos hasta que su nodo
siguiente sea NULL.
Saber si una lista está vacía:
Basta con comparar el puntero Lista con NULL, si Lista vale NULL la lista está
vacía.
3.2.5 Borrar una lista completa.
El algoritmo genérico para borrar una lista completa consiste simplemente
en borrar el primer elemento sucesivamente mientras la lista no esté vacía.
3.2.6 Ejemplo de lista ordenada.
Supongamos que queremos construir una lista para almacenar números
enteros, pero de modo que siempre esté ordenada de menor a mayor. Para hacer
la prueba añadiremos los valores 20, 10, 40, 30. De este modo tendremos todos
los casos posibles. Al comenzar, el primer elemento se introducirá en una lista
vacía, el segundo se insertará en la primera posición, el tercero en la última, y el
último en una posición intermedia.
Insertar un elemento en una lista vacía es equivalente a insertarlo en la
primera posición. De modo que no incluiremos una función para asignar un
elemento en una lista vacía, y haremos que la función para insertar en la primera
posición nos sirva para ese caso también.
Algoritmo de inserción:
1. El primer paso es crear un nodo para el dato que vamos a insertar.
2. Si Lista es NULL, o el valor del primer elemento de la lista es mayor que
el del nuevo, insertaremos el nuevo nodo en la primera posición de la
lista.
3. En caso contrario, buscaremos el lugar adecuado para la inserción,
tenemos un puntero "anterior". Lo inicializamos con el valor de Lista, y
avanzaremos mientras anterior->siguiente no sea NULL y el dato que
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
contiene anterior->siguiente sea menor o igual que el dato que
queremos insertar.
4. Ahora ya tenemos anterior señalando al nodo adecuado, así que
insertamos el nuevo nodo a continuación de él.
Algoritmo para borrar un elemento:
Después probaremos la función para buscar y borrar, borraremos los
elementos 10, 15, 45, 30 y 40, así probaremos los casos de borrar el primero, el
último y un caso intermedio o dos nodos que no existan.
Recordemos que para eliminar un nodo necesitamos disponer de un
puntero al nodo anterior.
1. Lo primero será localizar el nodo a eliminar, si es que existe. Pero sin
perder el puntero al nodo anterior. Partiremos del nodo primero, y del
valor NULL para anterior. Y avanzaremos mientras nodo no sea NULL o
mientras que el valor almacenado en nodo sea menor que el que
buscamos.
2. Ahora pueden darse tres casos:
3. Que el nodo sea NULL, esto indica que todos los valores almacenados
en la lista son menores que el que buscamos y el nodo que buscamos
no existe. Retornaremos sin borrar nada.
4. Que el valor almacenado en nodo sea mayor que el que buscamos, en
ese caso también retornaremos sin borrar nada, ya que esto indica que
el nodo que buscamos no existe.
5. Que el valor almacenado en el nodo sea igual al que buscamos.
6. De nuevo existen dos casos:
7. Que anterior sea NULL. Esto indicaría que el nodo que queremos borrar
es el primero, así que modificamos el valor de Lista para que apunte al
nodo siguiente al que queremos borrar.
8. Que anterior no sea NULL, el nodo no es el primero, así que asignamos
a anterior->siguiente la dirección de nodo->siguiente.
9. Después de 7 u 8, liberamos la memoria de nodo.
void Borrar(Lista *lista, int v) {
pNodo anterior, nodo;
nodo = *lista;
anterior = NULL;
while(nodo && nodo->valor < v) {
anterior = nodo;
nodo = nodo->siguiente;
}
if(!nodo || nodo->valor != v) return;
else { /* Borrar el nodo */
if(!anterior) /* Primer elemento */
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
*lista = nodo->siguiente;
else /* un elemento cualquiera */
anterior->siguiente = nodo->siguiente;
free(nodo);
}
}
3.2.7 Ejemplo de lista en C++ usando clases.
Usando clases el programa cambia bastante, aunque los algoritmos son los
mismos.
Para empezar, necesitamos dos clases, una para nodo y otra para lista. Además la
clase para nodo debe ser amiga de la clase lista, ya que ésta debe acceder a los
miembros privados de nodo.
class nodo {
public:
nodo(int v, nodo *sig = NULL) {
valor = v;
siguiente = sig;
}
private:
int valor;
nodo *siguiente;
};
friend class lista;
typedef nodo *pnodo;
class lista {
public:
lista() { primero = actual = NULL; }
~lista();
void Insertar(int v);
void Borrar(int v);
bool ListaVacia() { return primero == NULL; }
void Mostrar();
void Siguiente();
void Primero();
void Ultimo();
bool Actual() { return actual != NULL; }
int ValorActual() { return actual->valor; }
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
};
MANUAL DEL DOCENTE
private:
pnodo primero;
pnodo actual;
Hemos hecho que la clase para lista sea algo más completa que la equivalente en
C, aprovechando las prestaciones de las clases. En concreto, hemos añadido
funciones para mantener un puntero a un elemento de la lista y para poder
moverse a través de ella.
3.3 La clase LIST de STL
El contenedor de decencia LIST cuenta con una eficiente combinación para las
operaciones de inserción y eliminación en cualquier posición del contenedor. La
clase LIST se implementa como una lista doblemente enlazada: cada nodo en una
lista contiene un apuntador al nodo anterior y al nodo siguiente de esta lista. Esto
permite a la clase LIST soportar iteradotes bidireccionales que permiten que el
contenedor se recorra tanto hacia delante como hacia atrás.
Además de las funciones miembro de todos los contenedores de la STL y de
las funciones miembro comunes de todos los contenedores de secuencia, la clase
LIST cuenta con otras ocho funciones miembro:
splice: elimina elementos de una estructura y los almacena en otra, utiliza
distintos formatos.
remove: elimina el elemento especificado de la lista.
unique: elimina elementos duplicados de una lista.
merge: elimina elementos de una estructura y los almacena en otra, de
manera ordenada.
reverse: invierte los elementos de una lista.
sort: ordena los elementos de la lista.
push_front: inserta un elemento en la parte frontal de la lista.
pop_front: elimina un elemento de la parte frontal de la lista.
4 Pilas
4.1 Definición de pilas
Una pila es un tipo especial de lista abierta en la que sólo se pueden
insertar y eliminar nodos en uno de los extremos de la lista. Estas operaciones se
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
conocen como "push" y "pop", respectivamente "empujar" y "tirar". Además, las
escrituras de datos siempre son inserciones de nodos, y las lecturas siempre
eliminan el nodo leído.
Estas características implican un comportamiento de lista LIFO (Last In First
Out), el último en entrar es el primero en salir. La más sencilla de las estructuras
dinámicas de datos es la pila, las pilas son utilizadas sobre todo por los sistemas
operativos y los controladores de los lenguajes de alto nivel. Una pila es dinámica:
crece y encoge a medida que es necesario.
Una manera de ver esta estructura es pensar en las pilas como si fuese una
pila de bandejas de un autoservicio. Las bandejas se ponen en la pila por arriba, la
bandeja de arriba se la lleva de la pila un cliente que este en la cola. Este
escenario se denomina modelo del último en llegar-primero en salir: la última
bandeja que se ponga en la pila será la primera que se lleven.
El nodo típico para construir pilas es el siguiente:
struct nodo {
int dato;
struct nodo *siguiente;
};
Los tipos que se definen normalmente para manejar pilas son casi los
mismos que para manejar listas, tan sólo se cambian algunos nombres de la
siguiente manera:
typedef struct _nodo {
int dato;
struct _nodo *siguiente;
} tipoNodo;
typedef tipoNodo *pNodo;
typedef tipoNodo *Pila;
Donde:
tipoNodo es el tipo para declarar nodos, evidentemente.
pNodo es el tipo para declarar punteros a un nodo.
Pila es el tipo para declarar pilas.
Así una manera grafica de ver la estructura sería:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Es evidente, a la vista del gráfico, que una pila es una lista abierta. Así que
sigue siendo muy importante que un programa nunca pierda el valor del puntero al
primer elemento, igual que pasa con las listas abiertas.
Es necesario tener en cuenta que las inserciones y eliminaciones en una
pila se hacen siempre en un extremo, es decir, lo que consideramos como el
primer elemento de la lista es en realidad el último elemento de la pila.
4.2 Operaciones básicas con pilas.
Las pilas tienen un conjunto de operaciones muy limitado, sólo permiten las
operaciones de "push" y "pop":


Push: Añadir un elemento al final de la pila.
Pop: Leer y eliminar un elemento del final de la pila.
4.2.1. Push, insertar elemento:
Las operaciones con pilas son muy simples, no hay casos especiales, salvo
que la pila esté vacía.
Push en una pila vacía:
Partiremos de que ya tenemos el nodo a insertar y, por supuesto un puntero
que apunte a él, además el puntero a la pila valdrá NULL:
El proceso es muy simple, bastará con que:
1. nodo->siguiente apunte a NULL.
2. Pila apunte a nodo.
Push en una pila no vacía:
Podemos considerar el caso anterior como un caso particular de éste, la
única diferencia es que podemos y debemos trabajar con una pila vacía como con
una pila normal. De nuevo partiremos de un nodo a insertar, con un puntero que
apunte a él, y de una pila, en este caso no vacía:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
El proceso sigue siendo muy sencillo:
1. Hacemos que nodo->siguiente apunte a Pila.
2. Hacemos que Pila apunte a nodo.
4.2.2. Pop, leer y eliminar un elemento.
Ahora sólo existe un caso posible, ya que sólo podemos leer desde un
extremo de la pila. Partiremos de una pila con uno o más nodos, y usaremos un
puntero auxiliar, nodo:
1. Hacemos que nodo apunte al primer elemento de la pila, es decir a Pila.
2. Asignamos a Pila la dirección del segundo nodo de la pila: Pila>siguiente.
3. Guardamos el contenido del nodo para devolverlo como retorno,
recuerda que la operación pop equivale a leer y borrar.
4. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
Si la pila sólo tiene un nodo, el proceso sigue siendo válido, ya que el valor
de Pila->siguiente es NULL, y después de eliminar el último nodo la pila quedará
vacía, y el valor de Pila será NULL.
4.3 Implementación de pilas.
Supongamos que queremos construir una pila para almacenar números
enteros.
Algoritmo de la función "push":
1. Creamos un nodo para el valor que colocaremos en la pila.
2. Hacemos que nodo->siguiente apunte a Pila.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
3. Hacemos que Pila apunte a nodo.
void Push(Pila *pila, int v) {
pNodo nuevo;
/* Crear un nodo nuevo */
nuevo = (pNodo)malloc(sizeof(tipoNodo));
nuevo->valor = v;
}
/* Añadimos la pila a continuación del nuevo nodo */
nuevo->siguiente = *pila;
/* Ahora, el comienzo de nuestra pila es en nuevo nodo */
*pila = nuevo;
Algoritmo de la función "pop":
1. Hacemos que nodo apunte al primer elemento de la pila, es decir, a Pila.
2. Asignamos a Pila la dirección del segundo nodo de la pila: Pila>siguiente.
3. Guardamos el contenido del nodo para devolverlo como retorno.
4. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
int Pop(Pila *pila) {
pNodo nodo; /* variable auxiliar para manipular nodo */
int v;
/* variable auxiliar para retorno */
}
/* Nodo apunta al primer elemento de la pila */
nodo = *pila;
if(!nodo) return 0; /* Si no hay nodos en la pila retornamos 0 */
/* Asignamos a pila toda la pila menos el primer elemento */
*pila = nodo->siguiente;
/* Guardamos el valor de retorno */
v = nodo->valor;
/* Borrar el nodo */
free(nodo);
return v;
4.3.1 Ejemplo de pila en C++ usando clases.
Las clases para pilas son versiones simplificadas de las mismas clases que
usamos para listas. Para empezar, necesitaremos dos clases, una para nodo y
otra para pila. Además la clase para nodo debe ser amiga de la clase pila, ya que
ésta debe acceder a los miembros privados de nodo.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
class nodo {
public:
nodo(int v, nodo *sig = NULL) {
valor = v;
siguiente = sig;
}
private:
int valor;
nodo *siguiente;
friend class pila;
};
typedef nodo *pnodo;
class pila {
public:
pila() : ultimo(NULL) {}
~pila();
void Push(int v);
int Pop();
private:
pnodo ultimo;
};
4.4 La clase STACK de STL
La clase STACK permite insertar a, y eliminar de, la estructura de datos
subyacente en un extremo. Una pila puede implementarse con cualquiera de los
contenedores de secuencia; vector, list y deque.
De manera predeterminada, una pila se implementa con un deque. Las
operaciones de la pila son: push para insertar un elemento en la parte superior,
pop para eliminar el elemento de la pila, top para obtener una referencia al
elemento de la pila, empty para determinar si la pila se encuentra vacía o no y size
para obtener el número de elementos de la pila.
Para poder hacer uso de la clase STACK es necesario incluir el archivo de
encabezado <stack>, por ejemplo, el siguiente codigo crea tres pilas de enteros
utilizando cada uno de los contenedores de secuencia de la STL como estructura
de datos subyacente para representar al adaptador STACK.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
// Programa de prueba para el adaptador stack de la Biblioteca estándar.
#include <iostream>
using std::cout;
using std::endl;
#include <stack> // definición del adaptador stack
#include <vector> // definición de la plantilla de clase vector
#include <list> // definición de la plantilla de clase list
// prototipo de la plantilla de función sacarElementos
template< class T >
void sacarElementos( T &stackRef );
int main()
{
// pila con deque subyacente predeterminado
std::stack< int > intDequePila;
// pila con vector subyacente
std::stack< int, std::vector< int > > intVectorPila;
// pila con lista subyacente
std::stack< int, std::list< int > > intListaPila;
// meter los valores 0-9 en cada pila
for ( int i = 0; i < 10; ++i ) {
intDequePila.push( i );
intVectorPila.push( i );
intListaPila.push( i );
} // fin de instrucción for
// mostrar y eliminar elementos de cada pila
cout << "Sacando de intDequePila: ";
sacarElementos( intDequePila );
cout << "\nSacando de intVectorPila: ";
sacarElementos( intVectorPila );
cout << "\nSacando de intListaPila: ";
sacarElementos( intListaPila );
cout << endl;
return 0;
} // fin de main
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
// sacar elementos del objeto pila al que hace referencia stackRef
template< class T >
void sacarElementos( T &stackRef )
{
while ( !stackRef.empty() ) {
cout << stackRef.top() << ' '; // ver elemento superior
stackRef.pop();
// eliminar elemento superior
} // fin de instrucción while
} // fin de la función sacarElementos
5 Colas
5.1 Definición de colas
Una cola es un tipo especial de lista abierta en la que sólo se puede insertar
nodos en uno de los extremos de la lista y sólo se pueden eliminar nodos en el
otro. Además, como sucede con las pilas, las escrituras de datos siempre son
inserciones de nodos, y las lecturas siempre eliminan el nodo leído.
Este tipo de lista es conocido como lista FIFO (First In First Out), el primero
en entrar es el primero en salir.
Un ejemplo cotidiano es una cola para comprar, por ejemplo, las entradas
del cine. Los nuevos compradores sólo pueden colocarse al final de la cola, y sólo
el primero de la cola puede comprar la entrada.
El nodo típico para construir colas es el mismo para la construcción de listas
y pilas:
struct nodo {
int dato;
struct nodo *siguiente;
};
Los tipos que se definen normalmente para manejar colas son casi los
mismos que para manejar listas y pilas, tan sólo se cambian algunos nombres:
typedef struct _nodo {
int dato;
struct _nodo *siguiente;
} tipoNodo;
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
typedef tipoNodo *pNodo;
typedef tipoNodo *Cola;
Donde:
tipoNodo es el tipo para declarar nodos.
pNodo es el tipo para declarar punteros a un nodo.
Cola es el tipo para declarar colas.
De manera similar a las pilas, una cola se puede representar gráficamente
como:
Es evidente que una cola es una lista abierta. Así que sigue siendo muy
importante que un programa nunca pierda el valor del puntero al primer elemento,
igual que pasa con las listas abiertas. Además, debido al funcionamiento de las
colas, también se debe mantener un puntero para el último elemento de la cola,
que será el punto donde insertemos nuevos nodos.
Teniendo en cuenta que las lecturas y escrituras en una cola se hacen
siempre en extremos distintos, lo más fácil será insertar nodos por el final, a
continuación del nodo que no tiene nodo siguiente, y leerlos desde el principio, hay
que recordar que leer un nodo implica eliminarlo de la cola.
5.2 Operaciones básicas con colas.
De nuevo nos encontramos ante una estructura con muy pocas operaciones
disponibles. Las colas sólo permiten añadir y leer elementos:


Añadir: Inserta un elemento al final de la cola.
Leer: Lee y elimina un elemento del principio de la cola.
5.2.1 Añadir un elemento.
Las operaciones con colas son muy sencillas, prácticamente no hay casos
especiales, salvo que la cola esté vacía.
Añadir elemento en una cola vacía:
Partimos de que ya tenemos el nodo a insertar y, por supuesto un puntero
que apunte a él, además los punteros que definen la cola, primero y ultimo que
valdrán NULL:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
El proceso es muy simple, bastará con que:
1. nodo->siguiente apunte a NULL.
2. Y que los punteros primero y ultimo apunten a nodo.
Añadir elemento en una cola no vacía:
Nuevamente partimos de un nodo a insertar, con un puntero que apunte a
él, y de una cola, en este caso, al no estar vacía, los punteros primero y ultimo no
serán nulos:
El proceso sigue siendo muy sencillo:
1. Hacemos que nodo->siguiente apunte a NULL.
2. Después que ultimo->siguiente apunte a nodo.
3. Y actualizamos ultimo, haciendo que apunte a nodo.
Añadir elemento en una cola, caso general:
Para generalizar el caso anterior, sólo necesitamos añadir una operación:
1.
2.
3.
4.
Hacemos que nodo->siguiente apunte a NULL.
Si ultimo no es NULL, hacemos que ultimo->siguiente apunte a nodo.
Y actualizamos ultimo, haciendo que apunte a nodo.
Si primero es NULL, significa que la cola estaba vacía, así que haremos
que primero apunte también a nodo.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
5.2.2 Leer un elemento.
Ahora, también existen dos casos, que la cola tenga un solo elemento o que
tenga más de uno.
Leer un elemento en una cola con más de un elemento:
Usamos un puntero a un nodo auxiliar:
1. Hacemos que nodo apunte al primer elemento de la cola, es decir a
primero.
2. Asignamos a primero la dirección del segundo nodo de la pila: primero>siguiente.
3. Guardamos el contenido del nodo para devolverlo como retorno.
4. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
Leer un elemento en una cola con un solo elemento:
También necesitamos un puntero a un nodo auxiliar:
1. Hacemos que nodo apunte al primer elemento de la pila, es decir a
primero.
2. Asignamos NULL a primero, que es la dirección del segundo nodo
teórico de la cola: primero->siguiente.
3. Guardamos el contenido del nodo para devolverlo como retorno.
4. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
5. Hacemos que ultimo apunte a NULL, ya que la lectura ha dejado la cola
vacía.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Leer un elemento en una cola caso general:
1. Hacemos que nodo apunte al primer elemento de la pila, es decir a
primero.
2. Asignamos a primero la dirección del segundo nodo de la pila: primero>siguiente.
3. Guardamos el contenido del nodo para devolverlo como retorno.
4. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
5. Si primero es NULL, hacemos que ultimo también apunte a NULL, ya
que la lectura ha dejado la cola vacía.
5.3 Implementación de colas.
Supóngase que construimos una cola para almacenar números enteros.
Algoritmo de la función "Añadir":
1.
2.
3.
4.
5.
Creamos un nodo para el valor que colocaremos en la cola.
Hacemos que nodo->siguiente apunte a NULL.
Si "ultimo" no es NULL, hacemos que ultimo->siguiente apunte a nodo.
Actualizamos "ultimo" haciendo que apunte a nodo.
Si "primero" es NULL, hacemos que apunte a nodo.
void Anadir(pNodo *primero, pNodo *ultimo, int v) {
pNodo nuevo;
/* Crear un nodo nuevo */
nuevo = (pNodo)malloc(sizeof(tipoNodo));
nuevo->valor = v;
/* Este será el último nodo, no debe tener siguiente */
nuevo->siguiente = NULL;
/* Si la cola no estaba vacía, añadimos el nuevo a continuación de ultimo */
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
if(*ultimo) (*ultimo)->siguiente = nuevo;
/* Ahora, el último elemento de la cola es el nuevo nodo */
*ultimo = nuevo;
/* Si primero es NULL, la cola estaba vacía, ahora primero apuntará también al
nuevo nodo */
if(!*primero) *primero = nuevo;
}
Algoritmo de la función "leer":
1. Hacemos que nodo apunte al primer elemento de la cola, es decir a
primero.
2. Asignamos a primero la dirección del segundo nodo de la cola: primero>siguiente.
3. Guardamos el contenido del nodo para devolverlo como retorno.
4. Liberamos la memoria asignada al primer nodo, el que queremos
eliminar.
5. Si primero es NULL, haremos que último también apunte a NULL, ya
que la cola habrá quedado vacía.
int Leer(pNodo *primero, pNodo *ultimo) {
pNodo nodo; /* variable auxiliar para manipular nodo */
int v;
/* variable auxiliar para retorno */
/* Nodo apunta al primer elemento de la pila */
nodo = *primero;
if(!nodo) return 0; /* Si no hay nodos en la pila retornamos 0 */
/* Asignamos a primero la dirección del segundo nodo */
*primero = nodo->siguiente;
/* Guardamos el valor de retorno */
v = nodo->valor;
/* Borrar el nodo */
free(nodo);
/* Si la cola quedó vacía, ultimo debe ser NULL también*/
if(!*primero) *ultimo = NULL;
return v;
}
5.3.1 Ejemplo de cola en C++ usando clases.
Ya hemos visto que las colas son casos particulares de listas abiertas, pero
más simples. Como en los casos anteriores, veremos ahora un ejemplo de cola
usando clases.
Para empezar, y como siempre, necesitamos dos clases, una para nodo y
otra para cola. Además la clase para nodo debe ser amiga de la clase cola, ya que
ésta debe acceder a los miembros privados de nodo.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
class nodo {
public:
nodo(int v, nodo *sig = NULL) {
valor = v;
siguiente = sig;
}
private:
int valor;
nodo *siguiente;
friend class cola;
};
typedef nodo *pnodo;
class cola {
public:
cola() : ultimo(NULL), primero(NULL) {}
~cola();
void Anadir(int v);
int Leer();
private:
pnodo primero, ultimo;
};
5.4 La clase QUEUE de STL.
La clase QUEUE permite inserciones en la parte final de la estructura de datos
subyacentes y eliminaciones en la parte inicial de la misma. Una cola puede
implementarse con la estructura de datos list o deque de la STL, de manera
predeterminada una cola se implementa con deque.
Las operaciones comunes de un adaptador queue son push para insertar
un elemento en su parte final, pop para eliminar el elemento en la parte inicial de la
cola, front para obtener una referencia al primer elemento de la cola, back para
obtener una referencia al último elemento de la cola, empty para determinar si la
cola esta o no vacía y size para obtener el número de elementos de la cola.
El archivo de encabezado <queue> debe incluirse para poder utilizar esta
clase. Por ejemplo, el siguiente codigo muestra el uso de la clase queue.
// Programa de prueba para el adaptador queue de la Biblioteca estándar.
#include <iostream>
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
using std::cout;
using std::endl;
#include <queue> // definición del adaptador queue
int main()
{
std::queue< double > valores;
// meter elementos en la cola valores
valores.push( 3.2 );
valores.push( 9.8 );
valores.push( 5.4 );
cout << "Sacando de valores: ";
while ( !valores.empty() ) {
cout << valores.front() << ' '; // ver elemento inicial
valores.pop();
// eliminar elemento
} // fin de instrucción while
cout << endl;
return 0;
} // fin de main
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
6 Árboles
6.1 Definición de árboles
Un árbol es una estructura no lineal en la que cada nodo puede apuntar a uno o
varios nodos. También se suele dar una definición recursiva: un árbol es una
estructura en compuesta por un dato y varios árboles, la forma gráfica se puede
apreciar como sigue:
En relación con nodos se pueden definir conceptos como:


Nodo hijo: cualquiera de los nodos apuntados por uno de los nodos del
árbol. En el ejemplo, 'L' y 'M' son hijos de 'G'.
Nodo padre: nodo que contiene un puntero al nodo actual. En el
ejemplo, el nodo 'A' es padre de 'B', 'C' y 'D'.
En cuanto a la posición dentro del árbol, encontramos:



Nodo raíz: nodo que no tiene padre. Este es el nodo que usaremos
para referirnos al árbol. En el ejemplo, ese nodo es el 'A'.
Nodo hoja: nodo que no tiene hijos. En el ejemplo hay varios: 'F', 'H', 'I',
'K', 'L', 'M', 'N' y 'O'.
Nodo rama: aunque esta definición apenas la usaremos, estos son los
nodos que no pertenecen a ninguna de las dos categorías anteriores.
En el ejemplo: 'B', 'C', 'D', 'E', 'G' y 'J'.
Un árbol en el que en cada nodo o bien todos o ninguno de los hijos existe,
se llama árbol completo. Los árboles se parecen al resto de las estructuras que
hemos visto: dado un nodo cualquiera de la estructura, podemos considerarlo
como una estructura independiente, es decir, un nodo cualquiera puede ser
considerado como la raíz de un árbol completo.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Existen otros conceptos que definen las características del árbol, en relación
a su tamaño:

Orden: es el número potencial de hijos que puede tener cada elemento
de árbol. De este modo, diremos que un árbol en el que cada nodo
puede apuntar a otros dos es de orden dos, si puede apuntar a tres
será de orden tres, etc.

Grado: el número de hijos que tiene el elemento con más hijos dentro
del árbol. En el árbol del ejemplo, el grado es tres, ya que tanto 'A'
como 'D' tienen tres hijos, y no existen elementos con más de tres
hijos.

Nivel: se define para cada elemento del árbol como la distancia a la
raíz, medida en nodos. El nivel de la raíz es cero y el de sus hijos uno.
Así sucesivamente. En el ejemplo, el nodo 'D' tiene nivel 1, el nodo 'G'
tiene nivel 2, y el nodo 'N', nivel 3.

Altura: la altura de un árbol se define como el nivel del nodo de mayor
nivel. Como cada nodo de un árbol puede considerarse a su vez como
la raíz de un árbol, también podemos hablar de altura de ramas. El
árbol del ejemplo tiene altura 3, la rama 'B' tiene altura 2, la rama 'G'
tiene altura 1, la 'H' cero, etc.
Los árboles de orden dos son bastante especiales, de hecho les
dedicaremos varios capítulos. Estos árboles se conocen también como árboles
binarios.
Frecuentemente, aunque no es estrictamente necesario, para hacer más
fácil el moverse a través del árbol, se añade un puntero a cada nodo que apunte al
nodo padre. De este modo podremos avanzar en dirección a la raíz, y no sólo
hacia las hojas.
Es importante conservar siempre el nodo raíz ya que es el nodo a partir del
cual se desarrolla el árbol, si perdemos este nodo, perderemos el acceso a todo el
árbol. El nodo típico de un árbol difiere de los nodos que hemos visto hasta ahora
para listas, aunque sólo en el número de nodos. Veamos un ejemplo de nodo para
crear árboles de orden tres:
struct nodo {
int dato;
struct nodo *rama1;
struct nodo *rama2;
struct nodo *rama3;
};
O generalizando más:
#define ORDEN 5
struct nodo {
int dato;
struct nodo *rama[ORDEN];
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
};
Para C y C++, y basándonos en la declaración de nodo que hemos visto
anteriormente, se pueden definir los siguientes tipos:
typedef struct _nodo {
int dato;
struct _nodo *rama[ORDEN];
} tipoNodo;
typedef tipoNodo *pNodo;
typedef tipoNodo *Arbol;
Al igual que con las listas, declaramos un tipo tipoNodo para declarar
nodos, y un tipo pNodo para es el tipo para declarar punteros a un nodo. Arbol es
el tipo para declarar árboles de orden ORDEN.
El movimiento a través de árboles será siempre partiendo del nodo raíz
hacia un nodo hoja. Cada vez que lleguemos a un nuevo nodo podremos optar por
cualquiera de los nodos a los que apunta para avanzar al siguiente nodo.
Operaciones básicas con árboles
Salvo que trabajemos con árboles especiales, como los que veremos más
adelante, las inserciones serán siempre en punteros de nodos hoja o en punteros
libres de nodos rama. Con estas estructuras no es tan fácil generalizar, ya que
existen muchas variedades de árboles.
De nuevo tenemos casi el mismo repertorio de operaciones de las que
disponíamos con las listas:





Añadir o insertar elementos.
Buscar o localizar elementos.
Borrar elementos.
Moverse a través del árbol.
Recorrer el árbol completo.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Los algoritmos de inserción y borrado dependen en gran medida del tipo de árbol
que estemos implementando, de modo que por ahora los pasaremos por alto y nos
centraremos más en el modo de recorrer árboles.
Recorridos por los árboles
El modo evidente de moverse a través de las ramas de un árbol es siguiendo los
punteros, del mismo modo en que nos movíamos a través de las listas.
Esos recorridos dependen en gran medida del tipo y propósito del árbol, pero hay
ciertos recorridos que usaremos frecuentemente. Se trata de aquellos recorridos
que incluyen todo el árbol.
Hay tres formas de recorrer un árbol completo, y las tres se suelen implementar
mediante recursividad. En los tres casos se sigue siempre a partir de cada nodo
todas las ramas una por una.
Supongamos que tenemos un árbol de orden tres, y queremos recorrerlo por
completo.
Partiremos del nodo raíz:
RecorrerArbol(raiz);
La función RecorrerArbol, aplicando recursividad, será tan sencilla como invocar
de nuevo a la función RecorrerArbol para cada una de las ramas:
void RecorrerArbol(Arbol a) {
if(a == NULL) return;
RecorrerArbol(a->rama[0]);
RecorrerArbol(a->rama[1]);
RecorrerArbol(a->rama[2]);
}
Lo que diferencia los distintos métodos de recorrer el árbol no es el sistema de
hacerlo, sino el momento que elegimos para procesar el valor de cada nodo con
relación a los recorridos de cada una de las ramas.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Los tres tipos son:
Pre-orden:
En este tipo de recorrido, el valor del nodo se procesa antes de recorrer las ramas:
void PreOrden(Arbol a) {
if(a == NULL) return;
Procesar(dato);
RecorrerArbol(a->rama[0]);
RecorrerArbol(a->rama[1]);
RecorrerArbol(a->rama[2]);
}
Si seguimos el árbol del ejemplo en pre-orden, y el proceso de los datos es
sencillamente mostrarlos por pantalla, obtendremos algo así:
ABEKFCGLMDHIJNO
In-orden:
En este tipo de recorrido, el valor del nodo se procesa después de recorrer la
primera rama y antes de recorrer la última. Esto tiene más sentido en el caso de
árboles binarios, y también cuando existen ORDEN-1 datos, en cuyo caso
procesaremos cada dato entre el recorrido de cada dos ramas (este es el caso de
los árboles-b):
void InOrden(Arbol a) {
if(a == NULL) return;
RecorrerArbol(a->rama[0]);
Procesar(dato);
RecorrerArbol(a->rama[1]);
RecorrerArbol(a->rama[2]);
}
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Si seguimos el árbol del ejemplo en in-orden, y el proceso de los datos es
sencillamente mostrarlos por pantalla, obtendremos algo así:
KEBFALGMCHDINJO
Post-orden:
En este tipo de recorrido, el valor del nodo se procesa después de recorrer todas
las ramas:
void PostOrden(Arbol a) {
if(a == NULL) return;
RecorrerArbol(a->rama[0]);
RecorrerArbol(a->rama[1]);
RecorrerArbol(a->rama[2]);
Procesar(dato);
}
Si seguimos el árbol del ejemplo en post-orden, y el proceso de los datos es
sencillamente mostrarlos por pantalla, obtendremos algo así:
KEFBLMGCHINOJDA
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:
1.
2.
3.
4.
Buscar el nodo padre del que queremos eliminar.
Buscar el puntero del nodo padre que apunta al nodo que queremos borrar.
Liberar el nodo.
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.
2.
3.
4.
Buscar el nodo padre del que queremos eliminar.
Buscar el puntero del nodo padre que apunta al nodo que queremos borrar.
Podar el árbol cuyo padre es nodo.
padre->nodo[i] = NULL;.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
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á:
KEFyB
Á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, que veremos a continuación:





árboles binarios de búsqueda (ABB): son árboles de orden 2 que mantienen
una secuencia ordenada si se recorren en inorden.
árboles AVL: 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 M1 claves.
6.2 Árboles binarios.
Un árbol binario esta vacío o consta de un nodo denominado raíz junto con
dos árboles binarios llamados subárbol izquierdo y subárbol derecho de la raíz.
Además de que los árboles binarios se emplean para búsquedas, la
recuperación de información es una de las aplicaciones más importantes, y para
ello existen los árboles de búsqueda binaria.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
La única manera de construir un árbol binario con un nodo consiste en
hacer que el nodo sea su raíz y que estén vacíos sus subárboles izquierdo y
derecho (representan un apuntador a NULL), esto significa que se trata de un árbol
ordinario.
Con dos nodos en el árbol, uno de ellos será la raíz y el otro estará en un
subárbol. Así, uno de los subárboles de la izquierda o derecha debe estar vacío y
el otro contendrá un nodo. De ahí que haya dos árboles binarios diferentes con dos
nodos.
En el caso de un árbol binario con tres nodos, uno de estos será la raíz y los
otros dos se dividirán entre los subárboles de la izquierda y derecha en una de las
siguientes formas: 2 + 0, 1 + 1 y 0 + 2.
Como hay dos árboles binarios con dos nodos y solo un árbol vacío, en el
primer caso da dos árboles binarios. El tercero también lo hace. En el segundo
caso, los subárboles izquierdo y derecho tienen un nodo, y solo hay un árbol
binario con un nodo y por eso hay uno en el segundo. Así pues, en total existen
cinco árboles binarios con tres nodos.
Un árbol binario tiene una representación natural en el almacenamiento de
listas ligadas.
6.3 Árboles de búsqueda binaria (ABB).
Se trata de árboles de orden 2 en los que se cumple que para cada nodo, el
valor del nodo raíz del subárbol izquierdo es menor que el valor del nodo raíz y que
el valor del nodo raíz del subárbol derecho es mayor que el valor del nodo raíz.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
6.3.1 Operaciones en ABB.
El conjunto de operaciones que se pueden realizar sobre un ABB es similar
al que se realiza sobre otras estructuras de datos, más alguna otra propia de
árboles:
Buscar un elemento.
Insertar un elemento.
Eliminar un elemento.
Movimientos a través del árbol:
o Izquierda.
o Derecha.
o Raíz.
 Información:
o Comprobar si un árbol está vacío.
o Calcular el número de nodos.
o Comprobar si el nodo es hoja.
o Calcular la altura de un nodo.
o Calcular la altura de un árbol.




6.3.1.1
Buscar un elemento.
Partiendo siempre del nodo raíz, el modo de buscar un elemento se define
de forma recursiva como:




Si el árbol está vacío, terminamos la búsqueda: el elemento
el árbol.
Si el valor del nodo raíz es igual que el del elemento que
terminamos la búsqueda con éxito.
Si el valor del nodo raíz es mayor que el elemento que
continuaremos la búsqueda en el árbol izquierdo.
Si el valor del nodo raíz es menor que el elemento que
continuaremos la búsqueda en el árbol derecho.
Estructura de Datos.
no está en
buscamos,
buscamos,
buscamos,
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
El valor de retorno de una función de búsqueda en un ABB puede ser un
puntero al nodo encontrado, o NULL, si no se ha encontrado.
6.3.1.2
Insertar un elemento.
Para insertar un elemento nos basamos en el algoritmo de búsqueda. Si el
elemento está en el árbol no lo insertaremos. Si no lo está, lo insertaremos a
continuación del último nodo visitado. Para ello, se necesita un puntero auxiliar
para conservar una referencia al padre del nodo raíz actual. El valor inicial para
ese puntero es NULL.



Padre = NULL
nodo = Raiz
Bucle: mientras actual no sea un árbol vacío o hasta que se encuentre el
elemento.
o Si el valor del nodo raíz es mayor que el elemento que buscamos,
continuaremos la búsqueda en el árbol izquierdo:
Padre=nodo, nodo=nodo->izquierdo.
o
Si el valor del nodo raíz es menor que el elemento que buscamos,
continuaremos la búsqueda en el árbol derecho:
Padre=nodo, nodo=nodo->derecho.
Si nodo no es NULL, el elemento está en el árbol, por lo tanto salimos.
Si Padre es NULL, el árbol estaba vacío, por lo tanto, el nuevo árbol sólo
contendrá el nuevo elemento, que será la raíz del árbol.
 Si el elemento es menor que el Padre, entonces insertamos el nuevo
elemento como un nuevo árbol izquierdo de Padre.
 Si el elemento es mayor que el Padre, entonces insertamos el nuevo
elemento como un nuevo árbol derecho de Padre.


Este modo de actuar asegura que el árbol sigue siendo ABB.
6.3.1.3
Eliminar un elemento.
Para eliminar un elemento también nos basamos en el algoritmo de
búsqueda. Si el elemento no está en el árbol no lo podremos borrar. Si está, hay
dos casos posibles:
1. Se trata de un nodo hoja: en ese caso lo borraremos directamente.
2. Se trata de un nodo rama: en ese caso no podemos eliminarlo, puesto que
perderíamos todos los elementos del árbol de que el nodo actual es padre.
En su lugar buscamos el nodo más a la izquierda del subárbol derecho, o el
más a la derecha del subárbol izquierdo e intercambiamos sus valores. A
continuación eliminamos el nodo hoja.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Necesitamos un puntero auxiliar para conservar una referencia al padre del
nodo raíz actual. El valor inicial para ese puntero es NULL.
Padre = NULL
Si el árbol está vacío: el elemento no está en el árbol, por lo tanto salimos
sin eliminar ningún elemento.
 (*) Si el valor del nodo raíz es igual que el del elemento que buscamos,
estamos ante uno de los siguientes casos:
o El nodo raíz es un nodo hoja:
 Si 'Padre' es NULL, el nodo raíz es el único del árbol, por lo
tanto el puntero al árbol debe ser NULL.
 Si raíz es la rama derecha de 'Padre', hacemos que esa rama
apunte a NULL.
 Si raíz es la rama izquierda de 'Padre', hacemos que esa rama
apunte a NULL.
 Eliminamos el nodo, y salimos.
o El nodo no es un nodo hoja:
 Buscamos el 'nodo' más a la izquierda del árbol derecho de
raíz o el más a la derecha del árbol izquierdo. Hay que tener
en cuenta que puede que sólo exista uno de esos árboles. Al
mismo tiempo, actualizamos 'Padre' para que apunte al padre
de 'nodo'.
 Intercambiamos los elementos de los nodos raíz y 'nodo'.
 Borramos el nodo 'nodo'. Esto significa volver a (*), ya que
puede suceder que 'nodo' no sea un nodo hoja.
 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.


6.3.2 Ejemplos de eliminación en un ABB.
Ejemplo 1: Eliminar un nodo hoja
En el árbol de ejemplo, eliminar el nodo 3.
1. Localizamos el nodo a borrar, al tiempo que mantenemos un puntero a
'Padre'.
2. Hacemos que el puntero de 'Padre' que apuntaba a 'nodo', ahora apunte a
NULL.
3. Borramos el 'nodo'.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Ejemplo 2: Eliminar un nodo rama con intercambio de un nodo hoja.
En el árbol de ejemplo, eliminar el nodo 4.
1. Localizamos el nodo a eliminar (nodo raíz).
2.
Buscamos el nodo más a la derecha del árbol izquierdo de 'raíz', en
este caso el 3, al tiempo que mantenemos un puntero a 'Padre' a 'nodo'.
3. Intercambiamos los elementos 3 y 4.
4. Hacemos que el puntero de 'Padre' que apuntaba a 'nodo', ahora apunte a
NULL.
5. Borramos el 'nodo'.
Ejemplo 3: Eliminar un nodo rama con intercambio de un nodo rama.
Para este ejemplo usaremos otro árbol. En éste borraremos el elemento 6.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
1. Localizamos el nodo a eliminar (nodo raíz).
2.
Buscamos el nodo más a la izquierda del árbol derecho de 'raíz', en
este caso el 12, ya que el árbol derecho no tiene nodos a su izquierda, si
optamos por la rama izquierda, estaremos en un caso análogo. Al mismo
tiempo que mantenemos un puntero a 'Padre' a 'nodo'.
3. Intercambiamos los elementos 6 y 12.
4.
Ahora tenemos que repetir el bucle para el nodo 6 de nuevo, ya que
no podemos eliminarlo.
5. Localizamos de nuevo el nodo a eliminar (nodo raíz).
6.
Buscamos el nodo más a la izquierda del árbol derecho de 'raíz', en
este caso el 16, al mismo tiempo que mantenemos un puntero a 'Padre' a
'nodo'.
7. Intercambiamos los elementos 6 y 16.
8. Hacemos que el puntero de 'Padre' que apuntaba a 'nodo', ahora apunte a
NULL.
9. Borramos el 'nodo'.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
Este modo de actuar asegura que el árbol sigue siendo ABB.
Movimientos a través de un árbol
No hay mucho que contar. Nuestra estructura se referenciará siempre mediante un
puntero al nodo Raíz, 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
Raíz 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.
Comprobar si el nodo es hoja.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
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.
7.8 á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:
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
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.
6.4 Aplicaciones de árboles.
Un ejemplo de estructura en árbol es el sistema de directorios y ficheros de
un sistema operativo. Aunque en este caso se trata de árboles con nodos de dos
tipos, nodos directorio y nodos archivo, podríamos considerar que los nodos hoja
son archivos y los nodos rama son directorios.
Otro ejemplo podría ser la tabla de contenido de un libro, por ejemplo de
este mismo manual, dividido en capítulos, y cada uno de ellos en subcapítulos.
Aunque el libro sea algo lineal, como una lista, en el que cada capítulo sigue al
anterior, también es posible acceder a cualquier punto de él a través de la tabla de
contenido.
También se suelen organizar en forma de árbol los organigramas de mando
en empresas o en el ejército, y los árboles genealógicos.
Estructura de Datos.
UTIM
ESTRUCTURA DE DATOS I
MANUAL DEL DOCENTE
BIBLIOGRAFIA
Como programar C++
Deitel & Deitel
4ª Ed. Pearson/Prentice Hall
Desarrollo de algoritmos y sus aplicaciones en Basic, Cobol y C.
Guillermo Correa Uribe.
3ª Ed. McGraw Hill.
Estructuras de datos y algoritmos.
Alfred V. Aho, John E. Hopcroft, Jeffrey D. Ullman
Addison-Wesley Iberoamericana.
Estructuras de datos. Algoritmos, abstracción y objetos.
Luis Joyanes Aguilar, Ignacio Zahonero Martínez.
McGraw Hill.
Guia de Autoenseñanza C++
Herbert Schildt
McGraw Hill.
Manual de Estructuras dinámicas de datos.
Salvador Pozo.
Estructura de Datos.
UTIM
Descargar