SlideShare une entreprise Scribd logo
1  sur  39
Télécharger pour lire hors ligne
TEMA 7

ESTRUCTURAS DE DATOS: ESTRUCTURAS DINÁMICAS

       1. Asignación dinámica de memoria. Punteros.
       2. Estructuras dinámicas lineales
          2.1 Listas.
          2.2 Pilas.
          2.3 Colas.
       3. Estructuras dinámicas no lineales
          3.1 Árboles.
          3.2 Grafos.

1.- Asignación dinámica de memoria. Punteros

Una declaracion de variable como:
                int var;
produce una asociación entre el nombre 'var' y un espacio de almacenamiento en
memoria. Por lo tanto hay dos elementos relacionados con el nombre 'var': un valor que
se puede almacenar allí una dirección de memoria para la variable, algunos autores se
refieren a estos dos aspectos como el "rvalue" y "lvalue" de la variable.
Además del identificador "var", tenemos la palabra "int" que nos indica el TIPO (type)
de la variable. El tipo nos indica:
1-CUANTAS CELDAS DE MEMORIA (bytes) se asocian a ese nombre de variable.
2-DE QUE MODO SERÁN INTERPRETADOS los datos que se encuentren en un
lugar de la memoria.
        Un byte es la menor unidad de información que puede direccionarse en la
mayoría de las computadoras. En la mayoría de las arquitecturas el tipo char ocupa un
solo byte, por lo tanto es la unidad mínima. Un boolean admite sólo dos valores
diferentes, pero es almacenado como un byte(en C no existe). El tipo integer ocupa
generalmente 2 bytes, un long 4, double 8, y así con el resto de los tipos.
        El otro punto, es la relación entre LO QUE HAY en una celda de memoria y
CÓMO ES INTERPRETADO. Lo que hay en un celda cuya extensión es un byte es
simplemente un conjunto de ocho estados posibles (8 bits) que a nivel hardware admiten
dos estados diferenciales, estados que pueden ser interpretados como 'verdadero/falso',
0/1, o cualquier otro par de valores. Una celda de memoria del sector de datos, podría
contener algo como lo siguiente:
En binario                                   o en hexadecimal 61 y en decimal 97.
         El contenido, depende en gran parte del TIPO (type) que hayamos asociado a esa
celda (y suponiendo que exista tal asociación). Ese valor interpretado como un
hexadecimal es 0x61, en decimal es 97, y si fue asociada al tipo char representará la
letra 'a', cuyo ASCII es igual a 97. En ninguna localidad de memoria hay algo como la
letra 'a', lo que encontramos son valores binarios que en caso de estar asociados a char y
en caso de que lo saquemos en pantalla como char hará que veamos encendidos ciertos
pixeles de pantalla, en los cuales reconoceremos una representacion de la letra 'a'.
         La representación binaria de datos ocupa demasiado espacio, por ese motivo es
preferible utilizar el sistema hexadecimal, además de ser muy fácil de traducir a binario
es más económico que éste o el decimal. Observar los bytes de un sector de memoria de


                                                                                        1
un programa facilita la comprensión sobre el modo en que cada tipo (type) se asocia a
direcciones de memoria.
       Supongamos un programa que declara, define e inicializa, las siguientes
variables:
               int main()
               {
               int a = 5;
               long b = 8;
               char cad[ ]= "abcd";
               char ch = '6';
               char hh = 77;
               …
               }


La representación de estos datos en memoria, en el segmento de datos, tendría el
siguiente aspecto(cada dato, 8 bits=1 byte, se representa en headecimal con dos dígitos):

ffd0 ........................   20 20 20 20 00 8F 12 00 00 00 F6 FF BC 04 00 FF
ffe0 .... hhchcadba             F6 F6 00 00 F6 FF C7 04 4D 36 61 62 63 64 00 00
fff0 ....................…      08 00 00 00 05 00 00 00 0B 01 00 00 00 00 00 00

        Los datos que se han declarado primero en el código (int a) figuran al final, los
bytes 05 00 son la representacion de la variable entera a de valor : 5, los cuatro bytes 08
00 00 00 lo son del long b: 8, luego sigue 61 62 63 64 00 00 que es el array
cad:"abcd"(con final de cadena 0 más otro para que el nº de bytes sea par), el char ch:'6'
que corresponde con el hexadecimal 0x36, y por último un char hh con el valor entero:
77,(0x4D en hexadecimal, carácter M en ASCII).
        Además podemos realizar las siguientes observaciones:
        1- Que el segmento de datos almacena los datos comenzando desde el final
(0xffff). La primera variable declarada y definida es el entero 'a', que no está
verdaderamente en el final del segmento, es así porque esos valores (como 0B 01)
guardan valores de STACK (pila) para restablecer algunos registros cuando el programa
salga de main() y termine. Sobrescribir ese valor podría producir un error.
        2- Que la variable entera de valor 5 guarda este valor ubicando los bytes al
revés. Lo lógico sería que la representación fuera 00 05, pero los bytes están invertidos,
esto es una norma general de la mayoría de los procesadores y responde a una pauta de
mayor eficiencia en la lectura de variables numéricas.
        3- El array cad, se declara de modo implícito con 5 bytes, las cuatro letras mas el
caracter terminador '0'. Se ocupa un byte más porque un número par de bytes es más
eficiente. Obsérvese que un array no invierte la posición de sus elementos.
        4- Un char ocupa exactamente un byte. El primer char está definido con el
caracter '6' que corresponde al ASCII 0x36, la segunda variable char hh es inicializada a
partir de un valor entero 77, lo que genera una conversión implícita de tipos.

       Podríamos profundizar más para ver que funciones gestionan este sector de
memoria, su relación con la pila (STACK) y los modelos de memoria. Más adelante
veremos qué funciones gestionan el uso de memoria dinámica. Por ahora es importante
tener en cuenta la relación entre el tipo (type) usado para declarar una variable y el




                                                                                         2
modo en que se almacena en memoria. En la siguiente tabla se encuentran algunos
ejmplos más :




                                              Representacion
DECLARACION                  Inicializacion                    Numero de bytes
                                              en memoria
int N;                       N = 5;           05 00                     2
char letra;                  letra = 'L';     4C                        1
char cad[]="hola";                   -        68 6F 6C 61 00            5
long a;                      a=4              04 00 00 00               4
long a;                      a=0x1234         34 12 00 00               4
long a;                      a = 65535        ff ff 00 00               4

       Cuando en el flujo de un programa se asigna un valor a una variable lo que
sucede es que el lugar (o lugares) de memoria asociadas a la variables son inicializadas
con tal valor. La asociación entre posiciones de memoria y variable no siempre existe
desde el comienzo al final de un programa. Las variables declaradas como 'locales' a
una función sólo tienen asociado un lugar de memoria, mientras el flujo del programa se
encuentra en tal función, al salir de la misma tales posiciones serán usadas por otros
datos. En cambio las variables 'globales' o las declaradas como 'static' conservan sus
posiciones de memoria durante toda la ejecución del programa.
       Un array es una colección ordenada de elementos del mismo tipo (type), estos
tipos pueden ser los que proporciona el lenguaje, como char, int, float, long int, etc., o
bien puede tratarse de un tipo definido por el programador, como una estructura o una
clase.
       Estos elementos se encuentran ordenados en celdas consecutivas de memoria.
Veamos los siguientes ejemplos:

Declaracion e inicializacion       Representacion en memoria            Bytes
int a []= {3, 345, 54, 4};         03 00 63 01 72 01 03 27              2x4=8
int a[4]={2};                      02 00 00 00 00 00 00 00              2x4=8
char a [] = {"Mensaje 1"};         4d 65 6e 73 61 6a 65 20 31 00        9+1= 10
char a [8] = {hola};               68 6F 6C 61 00 00 00 00              7+1 = 8
long a [] = {9, 16, 0x23b2a};      09 00 00 00 12 00 00 00 2a 3b 02 00 3 x 4 = 12

        El tipo (type) del array determina cuántos bytes ocupa cada uno de sus
elementos, y también de qué modo se almacena el dato.
        Es importante mencionar que este modo de inicialización es sólo posible cuando
se realiza en la misma línea que en la declaración, no es posible inicializar al mismo
tiempo varios elementos de un array si lo hacemos en una línea diferente a la de la
declaración. También hay que mencionar el hecho de que si damos más elementos
inicializadores que los que figuran entre corchetes se genera un error de compilación, si
damos menos elementos el compilador inicializa el resto de los elementos con el valor
'0'.



                                                                                        3
Una cadena en C/C++, es representada internamente como un array de tipo char
y utiliza un carácter 'terminador' (0), para indicar el fin de la cadena, ese carácter es el
correspodiente al ASCII = 0.

       Un puntero es un tipo especial de variable, que almacena el valor de una
dirección de memoria, esta dirección puede ser la de una variable individual, pero mas
frecuentemente será la de un elemento de un array, una estructura u objeto de una clase.
Los punteros, al igual que una variable común, pertenecen a un tipo (type), se dice que
un puntero 'apunta a' ese tipo al que pertenece.

Ejemplos:

float * Preal ;        //Declara un puntero a un real
int * Pentero;        //Declara un puntero a entero
char * Pcaracter;     //Puntero a char
fecha * Pfecha;       //Puntero a objeto de clase fecha

        Independientemente del tamaño (sizeof) del objeto apuntado, el valor
almacenado por el puntero será el de una única dirección de memoria. En sentido
estricto un puntero no puede almacenar la dirección de memoria de 'un array'
(completo), sino la de un elemento de un array, y por este motivo no existen diferencias
sintácticas entre punteros a elementos individuales y punteros a arrays. La declaración
de un puntero a char y otro a array de char es igual.
        Al definir variables o arrays hemos visto que el tipo (type) modifica la cantidad
de bytes que se usarán para almacenar tales elementos, así un elemento de tipo 'char'
utiliza 1 byte, y un entero 2 o 4. No ocurre lo mismo con los punteros, el tipo no influye
en la cantidad de bytes asociados al puntero, pues todas las direcciones de memoria se
pueden expresar con sólo 2 bytes (o 4 si es una dirección de otro segmento)
        Veamos los efectos de un código como el siguiente, en la zona de
almacenamiento de datos:

char SALUDO[] = "hola";
char * p;
p = SALUDO;           //Puntero 'p' apunta a la cdena 'SALUDO'




       El puntero está en la dirección 0xffee por tanto el valor que hay en esa localidad
de memoria es otra dirección, los bytes "F0 FF" indican que el puntero apunta a FF F0
(recordemos que los bytes numéricos se guardan en sentido inverso), donde comienza la
cadena de caracteres 'SALUDO' con el contenido “hola”, más el cero de fin de cadena.
       En las líneas de código no hemos indicado a qué carácter del array apunta el
puntero, pero esa notación es equivalente, como ya sabemos a:
p = &cad[0];




                                                                                           4
que indica de modo más explícito que se trata de la dirección del primer elemento de ese
array de caracteres. El juego con las direcciones puede ilustrarse también del siguiente
modo:
ffee   F0    <----- El puntero ocupa dos bytes para representar la direccion FFF0, dirección a la que 'apunta'.

ffef   FF

fff0   68   <------ cad[0] = ‘h’ (Primer char del array de caracteres, dirección apuntada por el puntero)

fff1   6f   <------ cad[1] = ‘o’

fff2   6C   <------ cad[2] = ‘l’

fff3   61   <------ cad[3] = ‘a’

fff4   00 <------ cad[4] = ‘0’ (Fin del array, caracter ascii = 0 de fin de cadena)

        Puesto que un puntero tiene como valor una dirección de memoria, es lógico que
al llamar a funciones de impresión con un puntero como argumento, la salida en
pantalla sea la de una dirección de memoria.
        En este caso se trata de un puntero que almacena en 2 bytes una dirección de
memoria, la cual es FFF0.(16 bits, 4 para cada dígito hexadecimal))
        La salida en pantalla de un puntero a char es diferente, pues es tratado como
apuntando a una cadena de caracteres, en tal caso no sale en pantalla una dirección de
memoria, sino un conjunto de caracteres hasta encontrar el '0'.
        Un puntero puede almacenar la dirección de ("apuntar a") muy diferentes
entidades: una variable, un objeto, una función, un miembro de clase, otro puntero, o un
array de cada uno de estos tipos de elementos, también puede contener un valor que
indique que no apunta actualmente a ningún objeto (puntero nulo).
        Tipos como 'int' o 'char', son "tipos predefinidos", pertenecientes al lenguaje. En
C/C++ al igual que otros lenguajes, es posible definir tipos nuevos. Las enumeraciones,
uniones, estructuras , son tipos nuevos que implementa el programador.

        La declaración de un tipo no produce ningún efecto en memoria, no hay ningún
identificador donde almacenar un dato, por esa razón no tendría sentido, dentro de la
definición de una estructura o clase , intentar dar un valor a sus datos, sería lo mismo
que intentar dar un valor a un tipo predefinido, por ejemplo:
        int = 7; long = 83453; float = 2.3 ;char =’s’; //errores
        Para asignar un valor necesitamos un objeto, pues un objeto implica una región
de memoria donde almacenar un valor.
     El almacenamiento en memoria de una unión, enumeración o estructura (C), no
presenta importantes cambios respecto a los tipos predefinidos, sus elementos se
ordenaran de modo consecutivo de acuerdo a su tamaño ('sizeof'). (Respecto a C, C++
aporta un nuevo tipo predefinido, las clases, entidad que no sólo es un agregado de
datos sino también de funciones, y que por ello presenta novedades de importancia
respecto a los tipos anteriores.)


       Todas las variables, arrays, punteros y objetos en general tienen una duración
determinada en el transcurso del programa. Tales objetos son 'creados' y 'destruidos', o
en otros términos: se asocian sus nombres (identificadores) a una zona de memoria en la
cual no puede asentarse otro objeto, y tales zonas de memoria son liberadas para el uso
de otros objetos.


                                                                                                                  5
La existencia de tales objetos está determinada según tres formas básicas de usar
la memoria (en C++,C):

1-Memoria estática
       Los objetos son creados al comenzar el programa y destruidos sólo al finalizar el
mismo. Mantienen la misma localización en memoria durante todo el transcurso del
programa. Estos objetos son almacenados (en compiladores Borland) al principio del
segmento de datos. Los objetos administrados de este modo son: variables globales,
variables estáticas de funciones (static), miembros static de clases, y literales de
cualquier tipo (arrays, cadenas,...).

2- Memoria automática
       Los objetos son creados al entrar en el bloque en que están declarados, y se
destruyen al salir del bloque. Se trata de un proceso dinámico pero manejado de modo
automático por el compilador (no confundir con memoria dinámica). Tales objetos se
almacenan en la pila o stack al entrar en la función o bloque.
       Este procedimiento se aplica a: variables locales y argumentos de función.

3-Memoria dinámica
        En este caso tanto la creación como destrucción de los objetos están en manos
del programador. El sitio donde se almacenan tales objetos se suele denominar en ingles
'heap' o 'free store', traducido como 'montículo' o 'memoria libre'. Pero el sitio preciso
donde se encuentre tal 'montículo' depende del compilador y el tipo de puntero utilizado
en la reserva de memoria dinámica. Cualquier tipo de objeto puede ser creado y
destruido a través de este procedimiento.
     En C y C++ la administración explícita de memoria por parte del programador
juega un rol muy importante, no es así en otros lenguajes (Basic, Smalltalk, Perl) donde
la gestión principal es automática. La administración 'manual' permite un mayor grado
de flexibilidad pero también multiplica la posibilidad de errores. Un modo de gestionar
memoria dinámica en C,C++, aprovechando las ventajas de la memoria automática, es
la implementación de destructores que sean llamados de modo automático al salir de un
bloque, y que se encarguen de la liberación de memoria dinámica.

     Según lo visto hasta ahora, la reserva o asignación de memoria para vectores y
matrices se hace de forma automática con la declaración de dichas variables, asignando
suficiente memoria para resolver el problema de tamaño máximo, dejando el resto sin
usar para problemas más pequeños. Así, si en una función encargada de realizar un
producto de matrices, éstas se dimensionan para un tamaño máximo (100, 100), con
dicha función, se podrá calcular cualquier producto de un tamaño igual o inferior, pero
aún en el caso de que el producto sea por ejemplo de tamaño (3, 3), la memoria
reservada corresponderá al tamaño máximo (100, 100). Es muy útil el poder reservar
más o menos memoria en tiempo de ejecución, según el tamaño del caso concreto que
se vaya a resolver. A esto se llama reserva o gestión dinámica de memoria.

     La memoria es una colección de celdas contiguas con la capacidad de almacenar
valores. Cada celda de memoria es localizada por una 'dirección', que consta de un valor
de segmento y otro de offset (desplazamiento dentro del segmento). Los detalles de
como opera la cpu en relación a la memoria dependen del tipo de procesador, si éste
funciona en modo 'real' o 'protegido' (forma en que accede a las posiciones de
memoria), del sistema operativo y de otros factores.


                                                                                        6
Cada segmento tiene una capacidad de 64 Kb. Una importante directiva en todos
los programas es la que determina el MODELO DE MEMORIA que utilizará el
programa al ejecutarse. El default suele ser el modelo 'small', pero existen varios
modelos más, sus principales diferencias están en el modo en que utilizan los segmentos
para almacenar código, datos o ubicar la pila (stack).
     Al compilar y ejecutar un programa, podemos examinar los registros de la CPU
para datos, codigo y stack, estas serían las siglas de tales registros:

                        CS (code segment)    Segmento de código
                        DS (data segment)    Segmento de datos
                        SS (stack segment)   Segmento de pila
                        ES (extra segment)   Segmento extra

       El modelo de memoria utilizado por nuestro programa determinará cuanto
espacio (por segmentos) se usará para código, datos y stack (pila). El siguiente
cuadro sintetiza las distintas opciones:

Modelos de
              Segmentos       Comentarios
memoria
                              Código, datos y stack utilizan un único segmento, por lo
Tiny          cs = ds = ss
                              tanto el ejecutable no podrá ser mayor a 64 Kb.
              cs              Un segmento para código y uno para datos y stack(pila).
Small
              ds = ss         Es el modelo default utilizado, si no se especifica otro.
                              El código usa múltiples segmentos, datos y pila
              cs
Medium                        comparten uno. Es el modelo de elección si hay gran
              ds = ss
                              cantidad de código y pocos datos.
                              Un segmento para código y múltiples segmentos para
              cs              datos y stack. Modelo apropiado cuando hay poco código
Compact
              ds = ss         pero gran cantidad de datos. Los datos son referenciados
                              por punteros 'far'.
                              Múltiples segmentos para código y múltiples segmentos
              cs
Large                         para código y stack. Se usan punteros 'far' para código y
              ds = ss
                              para datos.
              cs
Huge                          Similar a 'large'.
              ds = ss
              cs              Usa punteros 'near' como el modelo 'small', pero hecho a
Flat
              ds = ss         medida para sistemas operativos de 32 bits.

       Estas categorías no son especificas de un lenguaje de programación, la mayoría
de los compiladores de los diferentes lenguajes permiten optar por estos diferentes
modelos de memoria.
       Distinguimos entre código y datos de forma natural, la sintaxis de C obliga a
declarar, en una función, primero todos los datos antes de realizar cualquier operación
(código). Pero la noción de STACK (PILA) tiene una correspondencia menos obvia con
lo que observamos en un lenguaje de alto nivel, se trata de algo manejado de modo
automático por el compilador. A lo sumo aparecerá en relación a mensajes de error
como 'Stack overflow' o 'Desbordamiento de pila' (también 'volcado de pila', en


                                                                                         7
Windows). En programación de bajo nivel (ensamblador) podemos operar directamente
sobre la propia pila.
        La pila es una zona de memoria requerida por todo programa para un uso
especial. Su función, es la de servir para el intercambio dinámico de datos durante la
ejecución de un programa, principalmente para la reserva y liberación de variables
locales y paso de argumentos entre funciones.
       El espacio utilizado para uso de la pila variará según el modelo de memoria que
utilice nuestro programa. Cuando un programa utliliza el modelo de memoria
SMALL usa un mismo segmento para datos y stack, 64 Kb entre ambos.
       Suponiendo que nuestro programa opera con tal modelo de memoria, en la
mayoría de los compiladores de BorlandC++, el segmento de datos/stack presentará el
siguiente aspecto, después de entrar en la función main() de un programa cualquiera:

                                                                 El inicio del segmento(0x0000)
                                                               contiene una cadena de Copyright de
                                                               Borland que no debe ser sobreescrita
                                                               (pues daría el mensaje "Null pointer
                                                               assignment"), luego se ubican las
                                                               variables globales y constantes. Los
                                                               literales, sean 'de cadena' o
                                                               'numericos' son tratados como
                                                               constantes y almacenados en la parte
                                                               baja. Al final de la pila (desde
                                                               0xFFFF)       se     guardan     datos
                                                               fundamentales para una buena salida
                                                               del programa, y debajo se extiende
                                                               una zona usada para almacenar
                                                               variables locales y datos pasados
                                                               como parámetros, por lo tanto es la
                                                               parte mas dinámica del segmento
                                                               (en el grafico la parte en blanco).


        El espacio total del segmento es de 64 Kb, esto significa que el montón de datos
que podemos pasar a una función será un poco menor pues hay espacio ocupado por
otros elementos. Esta limitación se podría salvar utilizando otro modelo de memoria,
pero por ahora nos centraremos en nuestro ejemplo con modelo small.
        Entonces:
        1º) El código del programa se ubica en un lugar de la memoria perfectamente
conocido en el momento del enlazado.
        2º) Los datos estáticos (variables globales, estáticas (static) y constantes) que
están presentes durante todo el tiempo de ejecución, aparecen en la primera parte del
segmento, después del copyright de Borland, seguidos de los literales (cadenas,
arrays,...).
        3º) A continuación aparecen los datos automáticos que no pueden tener una
longitud conocida pues se crean al entrar en un módulo o función (parámetros), y se
destruyen al salir de él, así como variables locales. Ésta es la zona de pila (stack) y es la
parte más dinámica.
        4º) Los datos dinámicos tampoco tienen una longitud conocida y su creación y
liberación depende de las solicitudes realizadas en tiempo de ejecución. La gestión de
estos datos no puede realizarse como una pila y por ello su gestión es independiente de
la gestión de los datos de tipo automático. Se emplea para ello un montículo (heap) en
el que Turbo C++ intenta asignar o liberar memoria en función de las necesidades. La
gestión del montículo precisa conocer las direcciones de los huecos de memoria
utilizados.


                                                                                                    8
Cuando se guardan en la pila más valores de los que caben se produce un 'stack
overflow', un desbordamiento de pila. Las funciones recursivas trabajan haciendo una
copia de sí mismas y guardándolas en la pila, por esa causa es frecuente provocar
desbordamientos de pila de ese modo. Hay muchos motivos para utilizar la pila del
modo más económico posible, y los punteros cumplen una gran utilidad en este caso,
por ejemplo, al pasar arrays, estructuras u objetos entre funciones a través de una
dirección (sólo 2 bytes).
        Otros detalles en relación a punteros. Todo puntero que esté dentro de este
segmento y apunte a otra dirección del mismo segmento será un puntero 'near', para
apuntar a un segmento diferente deberemos (en modelo small) explicitar un puntero
'far'. Una cuestión interesante es la de si la memoria dinámica se almacena en este
segmento o en algún otro. Los detalles en la implementación de memoria dinámica son
en general bastante oscuros y dependen mucho del compilador utilizado, pero si el
espacio reservado se asocia a un puntero 'near' es claro que la memoria reservada estará
dentro de este mismo segmento. Para estudiar este aspecto es recomendable ejecutar el
programa consultando los datos del puntero, el valor de segmento donde se encuentra y
el valor de segmento a donde apunta.

Funciones para la asignación dinámica de memoria. Uso de punteros.

        Los punteros también se utilizan en la reserva dinámica de memoria,
utilizándose para la creación de estructuras cuyo tamaño se decide en tiempo de
ejecución (son creadas y destruidas cuando el programa las necesite), adecuado para
programas donde no se pueda hacer una estimación inicial eficiente de necesidades de
memoria.
        Como ya habíamos anunciado el lenguaje C, utiliza para la reserva dinámica de
memoria una zona de espacio diferente del segmento de datos y de la pila del programa,
llamada montículo (heap), para que ésta no dependa de los movimientos de la pila del
programa y se puedan reservar bloques de cualquier tamaño.
      Existen en C dos funciones que reservan la cantidad de memoria deseada en
tiempo de ejecución. Dichas funciones devuelven –es decir, tienen como valor de
retorno– un puntero a la primera posición de la zona de memoria reservada. Estas
funciones se llaman malloc() y calloc(), y sus declaraciones, que están en la librería
stdlib.h, son como sigue:
                      void *malloc(int n_bytes)
                      void *calloc(int n_datos, int tamaño_dato)

       La función malloc() busca en la memoria el espacio requerido, lo reserva y
devuelve un puntero al primer elemento de la zona reservada.
       La función calloc() necesita dos argumentos:
       - el nº de celdas de memoria deseadas
       - el tamaño en bytes de cada celda
 se devuelve un puntero a la primera celda de memoria.
 La función calloc() tiene una propiedad adicional: inicializa todos los bloques a cero.

        Existe también una función llamada free() que deja libre la memoria reservada
por malloc() o calloc() y que ya no se va a utilizar. Esta función usa como argumento el
puntero devuelto por calloc() o malloc(). La memoria no se libera por defecto, sino que
el programador tiene que liberarla explícitamente con la función free(). El prototipo de
esta función es el siguiente:



                                                                                      9
void free(void *)

función malloc( )

void * malloc( size );

       La función malloc (memory allocate - asignar memoria) reserva una parte de la
memoria y devuelve la dirección del comienzo de esa parte. Esta dirección podemos
almacenarla en un puntero y así podemos acceder a la memoria reservada. La función
malloc tiene el siguiente formato:
          puntero = (tipo_de_variable *) malloc( número de bytes a reservar );

    •   puntero: es una variable tipo puntero que almacena la dirección del bloque de
        memoria reservado. Puede ser un puntero a char, int, float,...
    •   (tipo_de_variable *): es lo que se llama un molde. La función malloc nos
        reserva una cierta cantidad de bytes y devuelve un puntero del tipo void (que es
        uno genérico). Con el molde le indicamos al compilador que lo convierta en un
        puntero del mismo tipo que la variable puntero. Esto no es necesario en C, ya
        que lo hace automáticamente, aunque es aconsejable acostumbrarse a usarlo.

     Una vez reservada la memoria y guardada su dirección en un puntero podemos usar
ese puntero como hemos visto y hecho hasta ahora.
     Si no había suficiente memoria libre, malloc devolverá el valor NULL. El puntero
por tanto apuntará a NULL. Es muy importante comprobar siempre si se ha podido
reservar memoria o no, comprobando el valor de puntero:
if (puntero)
se cumple (puntero es una dirección de memoria no nula) si hay memoria suficiente, en
caso contrario es falso (puntero señala a la dirección NULL, cero).
Ejemplo:

 float *p;
 p = (float *) malloc( sizeof(float));
if (p)
   *p = 5.0;
else
  printf(“No se ha podido reservar memoria”);


función calloc( )

void * calloc( size_t n, size_t size );

      Reserva un bloque de memoria para ubicar “n” elementos contiguos de
tamaño "size” bytes cada uno (ej: un vector).

int *p;
p = (int *) calloc( 5, sizeof(int));
p[3] = 2;

función realloc( )



                                                                                     10
void * realloc( void *pant, size_t size );

       Cambia el tamaño de un área de memoria reservada con anterioridad, a un
tamaño de "size" bytes contiguos. Si size es mayor que el anterior tamaño, realloc()
busca una nueva zona, copia allí los datos y destruye la zona anterior. Si size es menor,
truncará el bloque actual al nuevo tamaño.
Ejemplo:

int *p, *nuevo;
p = (int *) calloc( 5, sizeof(int));
nuevo = realloc(p, 10);


función free( )

void free( void *punt );

        Cuando ya no necesitemos más el espacio reservado debemos liberarlo, es decir,
indicar al ordenador que puede destinarlo a otros fines. Si no liberamos el espacio que
ya no necesitamos corremos el peligro de agotar la memoria del ordenador. Para ello
usamos la función free, que funciona de la siguiente manera:
                free ( puntero );
      Donde puntero, es un puntero que apunta al comienzo del bloque que habíamos
reservado. Es muy importante no perder la dirección del comienzo del bloque, pues de
otra forma no podremos liberarlo.
      Tras el uso de free es recomendable anular el puntero poniéndolo a NULL. La
función free(), no puede usarse para liberar la memoria de las variables globales o
locales (están en el segmento de datos o pila, no en el montículo).
Ejemplo:

float *p;
p = (float *) malloc( sizeof(float));
// liberamos la zona apuntada por p
free(p);
p=NULL;

Ejemplo:

#include <stdio.h>
#include <stdlib.h>
void main()
{
  unsigned long bytes;
  char *frase;
  printf("¿Cuantos bytes vamos a reservar?: ");
  scanf("%lu",&bytes);
  frase = (char *) malloc(bytes);
  /* Verificamos el éxito de la operación */
     if (frase)
     {


                                                                                      11
printf("La cantidad de memoria reservada es: n");
        printf("%lu bytes = %lu kbn", bytes,bytes/1024);
        printf("El bloque reservado empieza en la direccion: %pn", frase);
        /* Ahora liberamos la memoria */
       free( frase );
       /*posicionamos el puntero*/
       frase=NULL;
     }
    else
        printf("No se ha podido reservar memorian");
}

Este programa pregunta cuánta memoria se quiere reservar. Si se consigue reservar
memoria, se indica cuánta memoria se ha reservado y dónde comienza el bloque. Si no
se consigue se indica mediante el mensaje: "No se ha podido reservar memoria".
Si por ejemplo reservamos 2500 bytes:

¿Cuantos bytes quieres reservar?: 2500
La cantidad de memoria reservada es: 2500 bytes = 2 kbytes = 0 Mbytes
El bloque reservado empieza en la dirección: 1234:0004

Si lo volvemos a ejecutar incrementando el tamaño de la memoria que queremos
reservar y suponiendo por ejemplo que tenemos 32Mbytes de memoria RAM, al teclear
lo siguiente:
¿Cuantos bytes quieres reservar?: 32000000

Nos dará el siguiente mensaje:
No se ha podido reservar memoria

        La función malloc() no ha podido reservar tanta memoria, ya que se trata de casi
el 100% de la memoria del ordenador, y devuelve (NULL) por lo que se nos avisa de
que la operación no se ha podido realizar.
        Es muy recomendable mantener siempre una referencia (puntero) al inicio del
vector para poder volver al principio en cualquier momento y liberarlo, así como una
variable que almacene la cantidad de elementos del vector:

Ejemplo:
#define n 10                   // nº de bytes a reservar
int *p, *q;
p = (int *) calloc(n, sizeof(int));
q = p;                         //guardamos posición inicial
for(i=0; i<n; i++)             //escribimos 1 en todas las posiciones de
{                              //de memoria usando q.
  *q = 1;
   q++;
}
free(p);             //liberamos p
p=NULL;              //posicionamos el puntero
Ejemplo:
        Calculemos el producto Y de una matriz A por un vector X: {y}=[A]{x}. Hay
que tener en cuenta que reservando memoria por separado para cada fila de la matriz,


                                                                                     12
no se garantiza que las filas estén contiguas en la memoria. Por otra parte, de esta
forma se pueden considerar filas de distinto tamaño. El nombre de la matriz se declara
como puntero a vector de punteros (indirección doble), y los nombres de los vectores
como punteros. Supóngase que N es una constante simbólica predefinida con el número
de filas y de columnas, ya que en este caso vamos a suponer que se trata de una matriz
cuadrada. Los datos serán enteros:

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void main()
{
         int **A, *x, *y;
         int N,i,j;
         void prod(int dim, int ** A, int * x, int * y);
         printf ("Dimensión de la matriz cuadrada : ");
         scanf("%d",&N);
         // reserva de memoria para la matriz A
                  //reservamos memoria a N punteros a enteros (fila)
         A = (int **)calloc(N, sizeof(int *));
         for (i=0; i<N; i++)
                  //memoria para N enteros (columnas de cada fila)
         A[i]=(int *) calloc(N, sizeof(int));
         // reserva de memoria para los vectores x e y
         x = (int *) calloc(N, sizeof(int));
         y = (int *) calloc(N, sizeof(int));
         for(i=0;i<N;i++)
                   for (j=0;j<N;j++)
                  {
                  printf("nElemento de la matriz A [%d][%d]: ",i+1,j+1);
                   //scanf("%d",(*(A+i)+j));
                   //scanf("%d",A[i]+j);
                  scanf("%d",&A[i][j]);
                   //printf ("%dn",*(&A[i]+j));
                   //printf("%d",*(*(A+i)+j));
                   printf("%d",A[i][j]);
                   }
                  for(i=0;i<N;i++)
                  {
                  printf("nElemento del vector x [%d]: ",i+1);
                  scanf("%d",x+i);
                  }
                  prod(N, A, x, y);
                  for(i=0;i<N;i++)
                           printf("y[%d]= %dn",i+1,*(y+i));
                  getch();
}
void prod(int N,int **mat,int *x,int *y)
{
int i,j,suma;


                                                                                   13
for(i=0;i<N;i++)
  {
       suma=0;
        for (j=0;j<N;j++)
               suma = suma + (*(*(mat+i)+j))*(*(x+j));
       *(y+i)=suma;
  }
}

Ejemplo:
Vamos a sacar por pantalla una matriz unidad cuadrada, de orden N. N es solicitado al
usuario del programa con lo que tenemos que asignar memoria dinámica.

#include <stdio.h>
#include <stdlib.h>
void main ()
{
  int *p,n,i,j,k;
  printf( "Orden de la matriz: ");
  fflush(stdin);
  scanf ("%d",&n);
  p=(int *)calloc(n*n,sizeof(int));
         for (i=0;i<n;i++)
    for (j=0;j<n;j++)
       if (i==j)
         *(p+(n*i)+j)=1;
       else
         *(p+(n*i)+j)=0;
  for (k=0;k<(n*n);k++)
    {
    if (k%n==0)
         printf ("n");
    printf ("%d ",*(p+k));
    }
}
         Una estructura se convierte en dinámica cuando sus elementos pueden ser
insertados o extraídos sin necesidad de algoritmos complejos, durante la ejecución del
programa. Cada elemento de una estructura dinámica se denomina nodo. Son
extremadamente flexibles.
         Podemos clasificarlas en dos grandes grupos: lineales y no lineales.




2. Estructuras dinámicas lineales




                                                                                   14
Las estructuras dinámicas lineales de datos son básicamente: las listas enlazadas,
las pilas y las colas. Analicemos cada una de ellas.

       2.1 LISTAS

      Una lista es un conjunto de elementos (nodos) de un determinado tipo,
compuestos por campos, y que puede variar en número, de tal forma que tendremos:

       -   listas lineales si sus elementos se almacenan en memoria de forma
           secuencial, es decir contiguos, uno detrás de otro de forma sucesiva. Aquí
           podemos incluir los arrays, cadenas y los ficheros secuenciales que se
           gestionan en tiempo real. En este caso operaciones como búsqueda o
           modificación de elementos no es tan difícil, pero se complica la eliminación
           o inserción de elementos salvo quizá en la última posición de la lista, ya que
           habría que realizar desplazamientos y reorganizar la lista para realizar dichas
           operaciones.

       -   listas enlazadas si cada uno de sus elementos o nodos, posee al menos dos
           campos: uno para dato o información y otro para enlace (puntero). El último
           nodo de la lista enlazada, por convenio apunta al valor nulo, nil. En las listas
           enlazadas no es necesario que los elementos se almacenen consecutivamente
           en memoria ya que cada nodo indica a través del puntero, donde se encuentra
           el siguiente elemento de la lista. Por tanto se eliminan los problemas que
           aparecen en las listas lineales a la hora de insertar, borrar, etc. los elementos.
                        SIG           INFO   SIG          INFO   SIG          INFO   SIG

                                ...                 ...                  ..          NIL




       -   listas circulares, cuando apuntamos el último nodo de la lista hacia el
           primer nodo, tenemos una lista circular. Ahora podemos acceder a cualquier
           nodo desde cualquier posción de la lista, el problema es que se pueden
           producir fácilmente bucles infinitos. Por ello el primer nodo se identifica con
           el nombre de cabezera y contiene información adicional para diferenciarlo
           del resto, a la vez que nos sirve de referencia a la hora de recorrer la lista
           circular.

           CABEZERA     SIG           INFO   SIG          INFO   SIG          INFO   SIG

                              ......               .. ...              . ..



       -   listas doblemente enlazadas, si cada uno de sus nodos poseen tres campos:
           el de información (INFO), y otros dos que apuntan al nodo anterior (ANT) y
           posterior (SIG) respectivamente. Ahora la ventaja es que podemos acceder a
           cualquier elemento desde otra posición cualquiera de la lista, avanzando o
           retrocediendo nodos según sea necesario.

Las operaciones que podemos realizar en una lista son:

       -   Inserción, eliminación y búsqueda de elementos.


                                                                                           15
-   Determinar el tamaño (nº de elementos) de la lista.
       -   Recorrer la lista con algún propósito.
       -   Ordenar la lista ascendente o descendentemente.
       -   Unir varias listas en una sola.
       -   Copiar o duplicar la lista.
       -   Comparar dos listas.
       -   Borrar la lista.

       Una lista contigua o densa almacena los elementos, como hemos dicho, de forma
contigua en memoria y se complican las eliminaciones e inserciones, ya que hay que
desplazar todos sus elementos. En cambio en las listas enlazadas, el almacenamiento es
encadenado y su tratamiento es mucho más amplio y flexible. Es por ello, que las listas
contiguas se usan muy poco frente a las enlazadas.

       Veamos por ejemplo, como insertamos o borramos un elemento de una lista
enlazada:

                                                                      .......
       A               B               C               D                        Z




       A               B               C               D                        Z


La eliminación del elemento C, consiste en dirigir el puntero del elemento anterior a C
hacia el elemento siguiente a C.
        La inserción de un elemento AA, consiste en dirigir el puntero del elemento que
le va a preceder hacia AA y dirigir el puntero de AA hacia el elemento al que apuntaba
el elemento que le va a preceder.


               A


       A               B               C               D              ......    Z

Ejemplo: Crear una lista
Entorno
       Entero L
       Puntero P          (lista) //array de enteros de longitud L indicada por el usuario
fin_entorno
Inicio
       Escribir “Dar la longitud del la lista”
       Leer L
       Reservar (P)
              ..............
              operaciones con la lista
              ...............
       Liberar (P)


                                                                                             16
fin

Ejemplo: Determinar cual es elemento j-ésimo de una lista :
     Si L=0 entonces
             Escribir “lista vacía”
     si_no
             Si ((0<=j)y(j<=L-1)) entonces
                    Escribir “B=”,P(j)
             si_no
                    Escribir “Elemento no existente”
             fin_si
     fin_si

Ejemplo: Borrar un elemento j de la lista:

       Si L=0 entonces
              Escribir “lista vacía”
       si_no
              Si ((0<=J)y(J<=L-1)) entonces
                     Para I=J hasta L-2 incremento 1
                              P(I)=P(I+1)
                     fin_para
                     L=L-1
              si_no
                     Escribir “Elemento no existente”
              fin_si
       fin_si

       Como vemos el tratamiento es similar al de los arrays, con la única diferencia de
que el espacio de memoria se asigna dinámicamente. Es el usuario, el que determina en
tiempo de ejecución, cuanto espacio se va a reservar para la lista (array). Por tanto para
crear una lista contigua, usamos la función calloc(), vista anteriormente. Esta función
nos permite reservar, como vimos en los ejemplos, de forma dinámica L elementos de
un tamaño concreto (sizeof), a través de un puntero P :

                              P = (tipo *) calloc (L, sizeof(elemento)).

        Para la gestión de listas enlazadas, el procedimiento es distinto ya que los
elementos no están contiguos. En este caso no reservamos un bloque de memoria, ya
que es el propio compilador, el que en tiempo de ejecución va buscando los espacios
libres para ir insertando los elementos que deben estar enlazados entre sí a través de un
puntero. Llamaremos P al puntero, accederemos al contenido de la dirección apuntada
con Val-P, y en el caso de estructuras accedemos a las direcciones de los campos con
P(CAMPO) y a sus contenidos con Val-P(CAMPO).



Ejemplo: Crear lista vacía
Entorno
      Definir registro nodo


                                                                                       17
Cadena INFO
                   Puntero a registro nodo SIG
       fin_registro
       Puntero a registro nodo PRIMERO, ULTIMO
fin_entorno
Inicio
       PRIMERO(SIG)=nulo
       ULTIMO(SIG)=nulo
       ..............
       operaciones con la lista
       ...............
fin

Ejemplo: Insertar elemento al final de lista enlazada
Inicio
       Puntero a registro nodo NUEVO
       Reservar(NUEVO)               //reservamos memoria dinámica para el puntero
       Escribir “Información del nuevo nodo”
       Leer Val-NUEVO(INF)
       NUEVO(SIG)=nulo
       Si PRIMERO(SIG)=nulo entonces
              PRIMERO=NUEVO
       si_no
              ULTIMO=PRIMERO
              Mientras (ULTIMO(SIG)!=nulo) hacer
                      ULTIMO=ULTIMO(SIG)
              ULTIMO=NUEVO
       fin_si
fin


       Vamos a ver un ejemplo global para observar como se trabajaría con listas
enlazadas en C. Este programa crea una lista y permite insertar, borrar y buscar
elementos dentro de la misma:

//lista enlazada
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>

struct agenda
{
  char nombre[15];
  char tel[10];
  struct agenda * sig;
 };
typedef struct agenda nodo;

void visualizar(void);


                                                                                     18
void buscar(void);
void insertar(void);
void borrar(void);
nodo *primero,*ultimo;

void main()
{
char opc;
primero=(nodo *)NULL;
ultimo=(nodo *)NULL;
if(primero==NULL)
         printf ("La lista esta vacia.");
do {
  gotoxy(15,5);printf("0. Visualizar elementos de la lista.");
  gotoxy(15,6);printf("1. Buscar elemento de lista.");
  gotoxy(15,7);printf("2. Insertar elemento de lista.");
  gotoxy(15,8);printf("3. Borrar elemento de lista.");
  gotoxy(15,9);printf("4. Terminar.");
  gotoxy(10,11);printf("Elegir opcion: ");
  scanf("%c%",&opc);
  fflush(stdin);
  printf("%c%",opc);
  fflush(stdin);
  clrscr();
  switch (opc)
         {
          case '0': visualizar();break;
          case '1': buscar();break;
          case '2': insertar();break;
          case '3': borrar();break;
          case '4': clrscr();exit(0);
       default:
       printf ("ntOpcion no valida.Pulsar tecla");
       getchar();
       clrscr();
      }
   }while (opc!=4);
}

void visualizar()
{
  nodo * auxiliar=primero;
  printf ("Elementos de la lista: n");
  if (auxiliar==NULL)
         printf ("No hay elementos en la lista");
  else
     {
       while (auxiliar!=NULL)
          {
           printf ("t%s t%sn",auxiliar->nombre,auxiliar->tel);


                                                                    19
auxiliar=auxiliar->sig;
        }
    }
getchar();
clrscr();
}

void buscar()
{
int i=1,sw=0;
char nom[15];
nodo * auxiliar;
printf ("Nombre a buscar: ");
gets(nom);
auxiliar=primero;
while(auxiliar!=NULL)
 {
      if (strcmp(auxiliar->nombre,nom)==0)
         {
           printf ("nombre : %s en posicion %dn",auxiliar->nombre,i);
           sw=1;
         }
      auxiliar=auxiliar->sig;
      i++;
 }
 if (sw==0)
         printf ("Elemento no encontrado.n");
getchar();
clrscr();
}

void insertar()
{
nodo * nuevo;
nuevo=(nodo *)malloc(sizeof(nodo));
printf ("Nombre : ");
fflush(stdin);
gets(nuevo->nombre);
printf ("telefono : ");
gets(nuevo->tel);
nuevo->sig=NULL;
if (primero==NULL)
   {
   printf ("Primer elemento de la lista.n");
   primero=nuevo;
   }
else
  {
   ultimo=primero;
   while (ultimo->sig!=NULL)


                                                                         20
ultimo=ultimo->sig;
  printf ("Siguiente elemento de la lista.n");
  ultimo->sig=nuevo;
  }
getchar();
clrscr();
}

//lista enlazada
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>

struct agenda
        {
        char nombre[15];
    char tel[10];
    struct agenda * sig;
  };
typedef struct agenda nodo;

void visualizar(void);
void buscar(void);
void insertar(void);
void borrar(void);
nodo *primero,*ultimo;
void main()
{
char opc;
primero=(nodo *)NULL;
ultimo=(nodo *)NULL;
if(primero==NULL)
       printf ("La lista esta vacia.");

do {
        gotoxy(15,5);printf("0. Visualizar elementos de la lista.");
  gotoxy(15,6);printf("1. Buscar elemento de lista.");
  gotoxy(15,7);printf("2. Insertar elemento de lista.");
  gotoxy(15,8);printf("3. Borrar elemento de lista.");
  gotoxy(15,9);printf("4. Terminar.");
  gotoxy(10,11);printf("Elegir opcion: ");
  fflush(stdin);
  scanf("%c",&opc);
  fflush(stdin);
  printf("%c",opc);
  fflush(stdin);
  clrscr();
  switch (opc)
        {


                                                                       21
case '0': visualizar();break;
        case '1': buscar();break;
      case '2': insertar();break;
      case '3': borrar();break;
      case '4': clrscr();exit(0);
      default:
       printf ("ntOpcion no valida.Pulsar tecla");
       getchar();
       clrscr();
     }
    }while (opc!=4);
}

void visualizar()
{
  nodo * auxiliar=primero;
  printf ("Elementos de la lista: n");
    if (auxiliar==NULL)
         printf ("No hay elementos en la lista");
    else
    {
     while (auxiliar!=NULL)
          {
       printf ("t%s t%sn",auxiliar->nombre,auxiliar->tel);
       auxiliar=auxiliar->sig;
      }
     }
getchar();
clrscr();
}


void buscar()
{
int i=1,sw=0;
char nom[15];
nodo * auxiliar;
printf ("Nombre a buscar: ");
gets(nom);
auxiliar=primero;
while(auxiliar!=NULL)
 {
  if (strcmp(auxiliar->nombre,nom)==0)
          {
               printf ("nombre : %s en posicion %dn",auxiliar->nombre,i);
     sw=1;
   }
  auxiliar=auxiliar->sig;
  i++;
 }


                                                                             22
if (sw==0)
        printf ("Elemento no encontrado.n");
getchar();
clrscr();
}

void insertar()
{
nodo * nuevo;
nuevo=(nodo *)malloc(sizeof(nodo));
printf ("Nombre : ");
fflush(stdin);
gets(nuevo->nombre);
printf ("telefono : ");
gets(nuevo->tel);
nuevo->sig=NULL;
if (primero==NULL)
         {
   printf ("Primer elemento de la lista.n");
   primero=nuevo;
   }
else
         {
   ultimo=primero;
   while (ultimo->sig!=NULL)
         ultimo=ultimo->sig;
   ultimo->sig=nuevo;
         }
getchar();
clrscr();
}


void borrar()
{
nodo * penult;
char nom[15];
int enc=0;
printf ("Nombre a borrar: n");
gets(nom);
if (primero==NULL)
   printf ("No hay elementos para eliminarn");
else
          {
   if (strcmp(primero->nombre,nom)==0)
      {
          if(primero->sig==NULL)
          {
                 primero=NULL;
                 printf("No quedan registros en la lista.n");


                                                                 23
}
     else
       {
       primero=primero->sig;
       }
    }
 else
  {
  ultimo=primero;
  penult=primero;
  while (strcmp(ultimo->nombre,nom)!=0)
      {
       if(ultimo->sig==NULL)
         {
         printf("Elemento no halladon");
         enc=0;
         break;
         }
       else
         {
         penult=ultimo;
         ultimo=ultimo->sig;
         enc=1;
         }
       }
 if (enc)
         penult->sig=ultimo->sig;
 }
}
getchar();
clrscr();
}

Otra forma muy usual de gestionar listas, es a través de indirección doble, pasando la
dirección del nodo que está en la primera posición de la lista (al tratarse de un puntero
que apunta a una zona de memoria, para pasar la dirección de la variable (que es un
puntero) necesitamos una indirección doble.

Ejemplo:

#include <stdio.h>
#include <stdlib.h>
typedef struct n
    {
      int inf;
      struct n *sig;
    } nodo;

void main()
{


                                                                                      24
void insertar(nodo **pri,nodo *d);
  void mostrar (nodo *pri);
                 //creamos la lista vacía;
   nodo *n1=NULL,*dato,*ult;
                 //pedimos datos del nuevo registro
  do
  {
    printf ("Introducir informacion: ");
                //reservamos memoria para el nodo
    dato=(nodo *)malloc(sizeof(nodo));
    scanf ("%d",&dato->inf);
    if (dato->inf==0)
         break;
    dato->sig=NULL;
                 //insertamos el elemento
    insertar (&n1,dato);
  }while (dato->inf!=0);
mostrar(n1);
printf ("El primer elemento de la lista es : %dn",n1->inf);
ult=n1;
while (ult->sig!=NULL)
         ult=ult->sig ;
printf ("El ultimo elemento de la lista es : %d",ult->inf);
free(dato);
}

void mostrar (nodo * pri)
{
   int i=1;
   while (pri!=NULL)
        {
        printf ("Elemento %d: %dn",i,pri->inf);
        pri=pri->sig ;
        i++;
      }
}

void insertar (nodo **pri,nodo *dato)
{
  nodo *aux;
  aux=*pri;
  if(aux==NULL)
     {
     aux=malloc(sizeof(nodo));
        printf ("Primer elemento de la lista.n");
                *aux=*dato;
     *pri=aux;
     }
  else
    {


                                                               25
while (aux->sig!=NULL)
       aux=aux->sig ;
    aux->sig=dato;
    }
}

Observamos que en todo momento sabemos cuáles son y dónde están, el primer
elemento de la lista, así como el último. De esta forma podríamos hacer inserciones o
borrar elementos de posiciones intermedias. En estos ejemplos insertamos y borramos
por el final de la lista, pero se podrían modificar las funciones, solicitando una posición
a insertar o borrar un elemento de la lista. Como además, sabemos almacenar
información en un fichero, podríamos almacenar la lista en un fichero, etc..

2.2 PILAS

        Las pilas también denominadas stacks son un tipo de listas lineales en las que la
inserción y extracción de elementos, se realiza por uno de los extremos, denominado
cima o tope (top). Ejemplos muy usuales de la vida real serían: una pila de platos, una
pila de libros, un conjunto de monedas apiladas, una bandeja encima de otra en un
comedor, etc. Para poder acceder a cualquier elemento, es necesario ir extrayendo los
que se han colocado posteriormente.
        Las dos operaciones más usuales asociadas a las pilas son:
        - Push (meter o poner): operación de inserción de un elemento en la pila.
        - Pop (sacar o quitar): operación de eliminación de un elemento de la pila.
        Por tanto los elementos se extraen en orden inverso al de inserción y se trata de
una estructura LIFO (Last In- First Out), último en entrar- primero en salir.



                                            aa

                                           ¨¨¨¨¨¨
cima           dd             cima          cc             aa bb cc dd
               cc                           dd
               .....                                              cima

               aa

Las pilas se gestionan a través de un puntero P denominado stack pointer, a través del
cual realizamos las inserciones y eliminaciones de elementos. P se dirige al elemento de
la pila que se situa en la cima. Al principio, cuando la pila está vacia, el puntero es nulo
P=0 (NULL).
         Es usual y conveniente, para la gestión de las pilas, al igual que de las listas y
colas, crear subprogramas para PONER (push) y QUITAR(pop) elementos en la misma.
También es usual crear una variable o función booleana VACIA, para determinar si la
pila está vacía, ya que no podemos eliminar elementos de una pila vacía. Por otro lado
si la pila se crea reservando una cantidad fija de memoria de longitud l, se producirá un
desbordamiento cuando se intente sobrepasar dicho límite, intentando insertar un
elemento más allá del espacio reservado.



                                                                                         26
Las aplicaciones de las pilas son múltiples, son usadas muy frecuentemente por
compiladores, sistemas operativos y programas de aplicación. Por ejemplo, cuando en
un programa se hace una llamada a un subprograma, es necesario almacenar el lugar
dónde se hizo la llamada y los datos que se estaban usando para que cuando el programa
principal retorne tras la ejecución del subprograma, sepa encontrar dichos elementos.
Imaginemos además, que estas llamadas son sucesivas, de unos módulos a otros, y que
no sólo almacenemos los últimos datos a recuperar, sino las sucesivas informaciones
relativas a cada una de las llamadas de unos módulos a otros. La pila en este caso
almacenaría primero los datos de la función principal F1 para ejecutar la función F2, a
continuación cuando F2 llama a otra función F3, se almacenan los datos de F2 para
ejecutar la función F3, etc. Cuando se ejecute la última función, que no llama a ningún
otro módulo, se va recuperando la información en orden inverso a su almacenamiento
hasta llegar a la función principal.

Veamos algún ejemplo de cómo podemos gestionar una pila. Ahora no necesitamos
guardar la posición del primer elemento y el último, como en las listas, ya que las
inserciones o eliminaciones se realizan por el final. Luego sólo necesitamos saber,
dónde se ubica el último elemento, e ir añadiendo o eliminando a partir de él, siempre y
cuando la lista no esté vacía (comprobación a realizar cada vez que se realiza una
operación) en cuyo caso sólo se podrían insertar elementos.

Ejemplo:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>


typedef struct n
       {
       int inf;
       struct n *ant;
       }nodo;

nodo *ultimo=NULL,*dato;

void main()
{
 void insertar(void);
 void sacar(void);
 void mostrar(void);

char res;
do
       {
       dato=malloc(sizeof(nodo));
       printf ("nIntroducir o eliminar, finalizar con S:(I/E) ");
       fflush(stdin);
       scanf("%c",&res);


                                                                                     27
if(toupper(res)=='S')
                 break;
        if (toupper(res)=='I')
                 {
                 printf ("Introducir informacion:n");
                 scanf("%d",&dato->inf);
                 clrscr();
                  insertar();
                 }
        else if (toupper(res)=='E')
                 sacar();
                 mostrar();
} while (toupper(res)!='S');
printf ("La pila queda al final:n");
mostrar();
}



void sacar(void)
{
 if (ultimo==NULL)
  printf ("No hay elementos en la pilan");
 else
  ultimo=ultimo->ant;
}




void mostrar(void)
 {
 nodo *aux=ultimo;
 while (aux!=NULL)
   {
    printf ("%dn",aux->inf);
    aux=aux->ant;
   }
 }



void insertar(void)
 {
 if (ultimo==NULL)
   {
   ultimo=malloc(sizeof(nodo));
   ultimo=dato;
   ultimo->ant=NULL;
   }


                                                         28
else
         {
    dato->ant=ultimo;
    ultimo=malloc(sizeof(nodo));
    *ultimo=*dato;
    }
}

        Como vemos, para la gestión de una pila sólo es necesario contar con dos
operaciones: “sacar” e “introducir” (push, pop) , ya que siempre estamos al final, en el
último elemento introducido. Además no es necesario saber donde está ubicado el
primer elemento, ya que siempre nos movemos desde el último, por tanto sólo es
necesario un puntero, en nuestro caso llamado “ultimo”, que nos da la posición del
último elemento. El único cuidado, es saber en todo momento si la pila se ha quedado
vacía, ya que entonces no se podrán eliminar elementos.

2.3 COLAS

        Se trata de otro tipo de estructura lineal similar a las pilas, con la única
diferencia de que el modo de inserción y extracción de elementos, se realiza de forma
diferente. La inserción de un nuevo elemento en una cola (queue), se realiza como en
las pilas, por el final de la estructura, después del último elemento insertado. Mientras
que la extracción de un elemento, se realiza por la cabecera o inicio de la estructura.
Tenemos también claros ejemplos en la vida real: la cola de espera de un cine, etc.
        Se trata pues, de unas estructuras de tipo FIFO (First In-First Out), primero en
entrar- primero en salir.
        Por tanto usaremos colas, cuando necesitemos una estructura en la que se han de
procesar los datos según su orden de aparición o inserción.
        Existen casos particulares, por ejemplo la bicola o doble cola, que es una cola en
la que las inserciones y extracciones se relizan por ambos extremos.

Sabiendo cual es el mecanismo de inserción, borrado o modificación en una lista y una
pila, gestionar una cola es muy sencillo, ya que la única modificación hay que realizarla
a la hora de eliminar un elemento, que ha de realizarse por el principio de la estructura.
Por tanto, habrá que tener constancia en todo momento, de cual es el primer elemento
de la cola para eliminar elementos, cual es el último elemento para introducir elementos
y si la cola se queda vacía tras realizar una extracción.

Ejemplo:

//Gestion de una cola

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <ctype.h>

typedef struct n
       {
       int dato;


                                                                                       29
struct n *sig;
       } nodo;

nodo *primero=NULL,*ultimo=NULL;

void main()
{
void sacar();
void introducir();
void mostrar();
char OPC;
do
  {
     printf ("nElegir opcion: n");
     printf ("tA) Introducir n");
     printf ("tB) Sacar          n");
     printf ("tC) Salir      n");
     fflush(stdin);
     OPC=getch();
     OPC=toupper(OPC);
     switch (OPC)
         {
       case 'A': introducir();
            mostrar();
            break;
       case 'B': sacar();
            mostrar();
            break;
       default: if(OPC!='C')
                    printf ("Eleccion no validan");
       }
    }while (OPC!='C');
  printf ("La cola queda finalmente:n");
  mostrar();
}


void introducir()
{
  nodo * aux;
  aux=malloc(sizeof(nodo));
  printf ("nIntroducir nuevo dato: ");
  fflush(stdin);
  scanf("%d",&aux->dato);
  aux->sig=NULL;
  if(primero==NULL)
        {
        primero=aux;
        }
  else


                                                       30
{
        ultimo=malloc(sizeof(nodo));
        ultimo=primero;
        while (ultimo->sig!=NULL)
               ultimo=ultimo->sig;
       ultimo->sig=aux;
       }
}

void sacar()
{
  if(primero==NULL)
        printf ("No hay elementos en la colan");
  else
        primero=primero->sig;
}

void mostrar()
{
   nodo *aux=primero;
   while (aux!=NULL)
         {
     printf ("%dt",aux->dato);
         aux=aux->sig;
         }
  }


3. Estructuras dinámicas no lineales

       Las estructuras dinámicas no lineales son: los árboles y los grafos.
       Estas estructuras son mucho más difíciles de manejar, es por ello y sobre todo
porque no hay tiempo suficiente, por lo que sólo vamos a comentar como se organizan.
Lo que dará una clara idea de la complejidad a la hora de gestionarlas.

       3.1 LOS ÁRBOLES

        Es una de las estructuras más importantes de las ciencias de la computación.
Permiten la representación natural de muchas clases de información y datos, además de
permitir la resolución algorítmica de muchos problemas.
       Las estructuras en árbol se usan cuando representamos información, en la que
existe una relación jerárquica entre sus elementos. En el mundo real, tenemos claros
ejemplos: árbol genealógico, ejemplos de combinatoria matemática, etc.
       Los elementos de un árbol son:
       - Elementos constituyentes:
            Nodos: cada uno de los elementos del árbol (vértices), que constituye una
            información.
            Conexiones: cada una de las líneas que expresan una relación entre dos
            nodos.
                                            A

                                                                                   31
B                                        nodos
                                               C

             D           E          F
                                                                  conexiones



-   Estructura del árbol:
    Nodo raíz : nodo más alto de la jerarquía a partir del cual, se conectan los
    demás. Ej: A.
    Nodo terminal u hoja : el que no contiene ningún subárbol . Ej: C.
    Nodo interno : aquél que tiene padre e hijos. No es ni raíz, ni terminal.
    Hijos de un nodo : aquellos nodos conectados hacia abajo, en la jerarquía, de
    forma directa. Ej: B y C son hijos de A.
    Descendientes: subárbol o conjunto de nodos de un nodo, que encontramos
    hacia abajo, en la jerarquía del árbol.
    Ascendientes : subárbol o conjunto de nodos de un nodo, que encontramos
    hacia arriba en la jerarquía del árbol.
    Padre : antecesor directo de un nodo. Todos los nodos tiene padre salvo el
    ríz. Ej: B es padre D, E y F.
    Hermanos : nodos hijos del mismo padre.
    Bosque : una colección de dos o más árboles.

-   Topología del árbol:
    Grado de un nodo: número máximo de hijos del nodo.
    Grado de un árbol: grado máximo de los nodos del árbol.
    Camino entre dos nodos: es la sucesión de nodos que hay que visitar para ir
    desde el nodo inicial hasta el final. Ej.: {(A,B), (B,E)} camino de A a E.
    Rama : camino que finaliza en un nodo terminal u hoja.
    Nivel de un nodo: Es la longitud del camino que inicia en el nodo raíz (nivel
    0) y acaba en el propio nodo. Ej: B y C tienen nivel 1.
    Altura o profundidad de un árbol : es el nivel máximo del árbol más uno.
    Ej: el árbol del ejemplo tiene altura 3.


Árboles binarios

  Es un conjunto finito, de cero o más nodos que cumplen:
- cada nodo puede tener 0, 1 ó 2 subárboles conocidos como subárbol
  izquierdo y derecho.
- El número de nodos en el nivel i puede ser como máximo 2i .
- El número de nodos del árbol de altura k, puede ser como máximo 2k –1.




                                                                               32
En el caso de que todos los nodos de un árbol binario tengan dos subárboles, se dice que
es además un árbol binario completo. Entonces, por ejemplo, el nº máximo de nodos n
de un árbol binario de altura h, sería nh –1 (nº de nodos si es completo).

Representación de árboles binarios completos mediante vectores:

Sea v[i] un vector que represente el árbol mediante el siguiente orden:
       ---------------------------------
       |   |   |   |   |   |   |   |   |
       ---------------------------------
v        0   1   2   3   4   5   6   7

Si consideramos desde i=1 como primer elemento, ubicaríamos los elementos del árbol
de la siguiente manera:

Nodo raíz             => v[1]
Hijo izquierdo de v[i] => v[2i]     si 2i <= n
Hijo derecho de v[i] => v[2i+1] si 2i+1 <= n
Padre de             v[i] => v[i/2]  si i > 1
( v[i] es una hoja si 2i > n )

v[1]   nodo raíz
v[2]   hijo izdo. del raíz
v[3]   hijo dcho. del raíz
v[4]   hijo izdo. de v[2]
v[5]   hijo dcho. de v[2]
v[6]   hijo izdo. de v[3]
v[7]   hijo izdo. de v[3], etc

       Para que coincidan los elementos con los índices del vector bastaría iniciar en
i=0, para ello basta restar una unidad a todos los índices obtenidos (2i-1 y 2i).

        Un árbol es vacío si no posee nodos ni conexiones y es lleno, si todos sus niveles
están llenos (2 hijos), salvo el último nivel.

      Otra forma de almacenar y recorrer un árbol sería usando asignación dinámica
mediante dos punteros que almacenan la información de los dos hijos de cada nodo:

typedef struct nodoarbolbin
{
  tipo_info clave;
  struct nodoarbolbin *izq;
  struct nodoarbolbin *der;
} NODOARBOL;

NODOARBOL *arbol;

        Al igual que hacíamos con las listas, basta guardar la posición del nodo raíz a
partir del cual podemos acceder mediante aritmética de punteros a los nodos sucesivos.




                                                                                       33
Métodos de recorrido del árbol binario:

        Entendemos por recorrer un árbol visitar (y procesar) cada uno de los nodos que
lo componen, una sola vez y en un determinado orden. Existen 3 métodos, los cuales
difieren en el orden en que procesamos los nodos, y que comúnmente a la hora de entrar
en nuevos nodos comienzan por el subárbol izquierdo:



Método preorden:

a). Visitar el nodo (y procesarlo).
b). Recorrer el subárbol izq en preorden
c). Recorrer el subárbol der en preorden.

Método inorden:

a). Recorrer el subárbol izq en inorden
b). Visitar el nodo (y procesarlo).
c). Recorrer el subárbol der en inorden.

Método postorden:

a). Recorrer el subárbol izq en postorden
b). Recorrer el subárbol der en postorden.
c). Visitar el nodo (y procesarlo).

Por ejemplo:

void PreOrden( NODOARBOL *nodo)
{
  if( nodo!=NULL)
  {
     procesar(nodo);
     PreOrden(nodo->izq);
     PreOrden(nodo->der);
  }
}

Esta función recursiva trata el método preorden para recorrer y leer el árbol.

Ejemplos:



                                                                   *


Recorrido pre-orden      *+xyz                            +                z
Recorrido in-orden       x+y*z


                                                                                    34
Recorrido post-orden    xy+z*
                                                  x           y




Recorrido pre-orden MEBADLPNVTZ
                                                          M
Recorrido in-orden   ABDELMNPTVZ
Recorrido post-orden ADBLENTZVPM              E                           P

                                   B                  L           N           V


                            A             D                           T           Z




Ejemplo:


Vamos a crear el primer árbol del ejemplo anterior y vamos a recorrerlo usando los tres
métodos expuestos anteriormente: pre-orden, in-orden y post-orden.


#include <stdio.h>
#include <stdlib.h>
#include <conio.h>

typedef struct nodo
{
       char dato;
       struct nodo *hijo_izq;
       struct nodo *hijo_der;

} ARBOL;

void PreOrden(ARBOL *ptr);
void Inorden(ARBOL *ptr);
void PostOrden(ARBOL *ptr);

void main()
{
      ARBOL *n1, *n2, *n3, *n4, *n5;

       n1 = malloc(sizeof(ARBOL));
       n2 = malloc(sizeof(ARBOL));
       n3 = malloc(sizeof(ARBOL));


                                                                                      35
n4 = malloc(sizeof(ARBOL));
         n5 = malloc(sizeof(ARBOL));
         n1->dato = '*';
         n1->hijo_izq = n2;
         n1->hijo_der = n3;
         n2->dato = '+';
         n2->hijo_izq = n4;
         n2->hijo_der = n5;
         n3->dato = 'z';
         n3->hijo_izq = NULL;
         n3->hijo_der = NULL;
         n4->dato = 'x';
         n4->hijo_izq = NULL;
         n4->hijo_der = NULL;
         n5->dato = 'y';
         n5->hijo_izq = NULL;
         n5->hijo_der = NULL;
         printf("Recorido en preorden => ");
         PreOrden(n1);
         printf("nRecorrido en orden simetrico => ");
         Inorden(n1);
         printf("nRecorrido en postorden => ");
         PostOrden(n1);
    getch();
}


void PreOrden(ARBOL *ptr)
{
       if(ptr != NULL)
       {
                printf("%c",ptr->dato);
                PreOrden(ptr->hijo_izq);
                PreOrden(ptr->hijo_der);
       }
}

void Inorden(ARBOL *ptr)
{
       if(ptr != NULL)
       {
                OrdenSimetrico(ptr->hijo_izq);
                printf("%c",ptr->dato);
                OrdenSimetrico(ptr->hijo_der);
       }
}

void PostOrden(ARBOL *ptr)
{
       if(ptr != NULL)


                                                         36
{
              PostOrden(ptr->hijo_izq);
              PostOrden(ptr->hijo_der);
              printf("%c",ptr->dato);
       }
}

       3.2 LOS GRAFOS

        Los árboles anteriormente comentados, son estructuras complejas, pero por lo
menos, tienen una jerarquía bien diferenciada, que permite ir avanzando desde el nodo y
raíz elegido un método de recorrido, por algunos de los caminos descendentes( según
nuestra representación). En el mejor de los casos puede tratarse de un árbol binario, que
podría incluso convertirse en un array si se trata de un árbol binario completo.

        Sin embargo los grafos , que son estructuras no lineales que tienen un gran
número de aplicaciones, eliminan las restricciones que tienen los árboles, permitiendo
que cualquier nodo, enlace con cualquiera de los demás. De este modo no existen padres
e hijos, sino relaciones entre nodos, que complican extremadamente la gestión de dichas
estructuras.

       Elementos constituyentes de los grafos:


                                     Zaragoza
                     Madrid                                       Barcelona
           Sevilla            Jaen
                                                 Soria
                 Avila                                       Cuenca
                                            Avila


       -   nodos o vértices, cada uno de los elementos de información.
       -   líneas, aristas o arcos, uniones entre dos nodos.

       Un grafo se denomina sencillo, si no existe un nodo que enlaza consigo mismo,
       y sólo existe un arco o línea entre dos nodos.




              Un camino es una secuencia de dos o más arcos o líneas que conectan
       dos nodos entre sí.
              Ej.: C(Madrid,Barcelona)= (Madrid, Zaragoza) (Zaragoza, Barcelona).

              Dos grafos son adyacentes si existe un arco que los une. Por otro lado los
       arcos pueden indicar una dirección en cuyo caso tendremos los grafos dirgidos.
       En otros casos puede que cada línea tenga un peso (valor establecido). En
       nuestro ejemplo de ciudades, podemos indicar la dirección de las líneas entre


                                                                                      37
cada dos nodos y podríamos establecer un peso o valor para cada una de ellas
por ejemplo la distancia, tiempo de recorrido, etc.


                                      6
                    A                             B
                        7                     3
                                C

        Por tanto una vez expuestas las características básicas de los grafos,
podemos hacernos una idea de la complejidad que pueden tener los algoritmos
que gestionen la creación y el posterior acceso a un grafo para realizar cualquier
actualización de l mismo. Suelen construirse las matrices de adyacencia, en las
que se representan las conexiones de cada uno de los nodos con todos los demás.
A través de las mismas podemos gestionarlos.



                                                           6
                        2

                                          3


         1                                                      5
                        4
(Grafo no dirigido)



Matriz de adyacencia

   IJ          1           2             3           4         5           6
    1           0           1             0           0         0           0
    2           1           1             1           0         0           0
    3           0           1             0           1         1           1
    4           0           0             1           0         0           0
    5           0           0             1           0         0           0
    6           0           0             1           0         0           0

A partir de aquí podemos crear unas listas de adyacencias, que podrían
gestionarse mediante punteros:
   Elementos
                    1             2   NULO
                                 NUNUL
                    2             1                2                  3 NULO

                                 2                                    5              6 NULO
                    3                              4
                                                  NUNUL              NUNUL
                                  3   NULO
                    4
                                 NUNUL
                                  3   NULO


                                                                                38
5

6
    3




        39

Contenu connexe

Tendances

Tendances (17)

18 tipos-de-datos
18 tipos-de-datos18 tipos-de-datos
18 tipos-de-datos
 
Lenguaje C para Administradores de Red - Script I
Lenguaje C para Administradores de Red - Script ILenguaje C para Administradores de Red - Script I
Lenguaje C para Administradores de Red - Script I
 
Tipos de datos_en_java
Tipos de datos_en_javaTipos de datos_en_java
Tipos de datos_en_java
 
Ambiente
 Ambiente Ambiente
Ambiente
 
Ambiente de programación en pascal
Ambiente de programación en pascalAmbiente de programación en pascal
Ambiente de programación en pascal
 
Ambiente de programacin en pascal
Ambiente de programacin en pascalAmbiente de programacin en pascal
Ambiente de programacin en pascal
 
Luis hernandez 22310621
Luis hernandez   22310621Luis hernandez   22310621
Luis hernandez 22310621
 
Gestión Dinámica de la Memoria
Gestión Dinámica de la MemoriaGestión Dinámica de la Memoria
Gestión Dinámica de la Memoria
 
Cadena De Caracteres_adrian
Cadena De Caracteres_adrianCadena De Caracteres_adrian
Cadena De Caracteres_adrian
 
Tema 2 tipos de datos y expresiones en java por gio
Tema 2   tipos de datos y expresiones en java por gioTema 2   tipos de datos y expresiones en java por gio
Tema 2 tipos de datos y expresiones en java por gio
 
Material iii parcial
Material iii parcialMaterial iii parcial
Material iii parcial
 
Manejo de memoria
Manejo de memoriaManejo de memoria
Manejo de memoria
 
Transparencias4
Transparencias4Transparencias4
Transparencias4
 
Manual de c c++
Manual de c c++Manual de c c++
Manual de c c++
 
Manejo De Memoria
Manejo De MemoriaManejo De Memoria
Manejo De Memoria
 
Estructuras en C
Estructuras en CEstructuras en C
Estructuras en C
 
Arrays
ArraysArrays
Arrays
 

En vedette (9)

FIT Plansbook
FIT PlansbookFIT Plansbook
FIT Plansbook
 
Fiber optics
Fiber opticsFiber optics
Fiber optics
 
Hypothesis
HypothesisHypothesis
Hypothesis
 
Ondernemen
OndernemenOndernemen
Ondernemen
 
Atacocha
AtacochaAtacocha
Atacocha
 
Research
 Research Research
Research
 
Respiratory system
Respiratory systemRespiratory system
Respiratory system
 
Chapter 11 Presentation
Chapter 11 PresentationChapter 11 Presentation
Chapter 11 Presentation
 
Calentamiento global
Calentamiento globalCalentamiento global
Calentamiento global
 

Similaire à Tema7 dinamicas

Similaire à Tema7 dinamicas (20)

Funciones de entrada y salida
Funciones de entrada y salidaFunciones de entrada y salida
Funciones de entrada y salida
 
Tipos de datos en java
Tipos de datos en javaTipos de datos en java
Tipos de datos en java
 
Expocicion sabado
Expocicion sabadoExpocicion sabado
Expocicion sabado
 
Apuntadores
Apuntadores Apuntadores
Apuntadores
 
6.2 cadenas de caracteres
6.2 cadenas de caracteres6.2 cadenas de caracteres
6.2 cadenas de caracteres
 
Fpr Tema6 www.fresymetal.com
Fpr Tema6 www.fresymetal.comFpr Tema6 www.fresymetal.com
Fpr Tema6 www.fresymetal.com
 
Presentación 2014 profe gabriel
Presentación 2014 profe gabrielPresentación 2014 profe gabriel
Presentación 2014 profe gabriel
 
Arreglos
ArreglosArreglos
Arreglos
 
5-EDA-teo.ppt
5-EDA-teo.ppt5-EDA-teo.ppt
5-EDA-teo.ppt
 
5-EDA-teo.ppt
5-EDA-teo.ppt5-EDA-teo.ppt
5-EDA-teo.ppt
 
5-EDA-teo.ppt
5-EDA-teo.ppt5-EDA-teo.ppt
5-EDA-teo.ppt
 
estructura de datos y algoritmos repaso
estructura de  datos y algoritmos repasoestructura de  datos y algoritmos repaso
estructura de datos y algoritmos repaso
 
Tipos de datos en C
Tipos de datos en CTipos de datos en C
Tipos de datos en C
 
Arrays C++
Arrays C++Arrays C++
Arrays C++
 
Implementacion de punteros
Implementacion de punterosImplementacion de punteros
Implementacion de punteros
 
Actividades 1-7
Actividades 1-7Actividades 1-7
Actividades 1-7
 
Actividades 1 7
Actividades 1 7Actividades 1 7
Actividades 1 7
 
Actividades 1 7
Actividades 1 7Actividades 1 7
Actividades 1 7
 
Actividades 1 7
Actividades 1 7Actividades 1 7
Actividades 1 7
 
Segundo trabajo
Segundo trabajoSegundo trabajo
Segundo trabajo
 

Tema7 dinamicas

  • 1. TEMA 7 ESTRUCTURAS DE DATOS: ESTRUCTURAS DINÁMICAS 1. Asignación dinámica de memoria. Punteros. 2. Estructuras dinámicas lineales 2.1 Listas. 2.2 Pilas. 2.3 Colas. 3. Estructuras dinámicas no lineales 3.1 Árboles. 3.2 Grafos. 1.- Asignación dinámica de memoria. Punteros Una declaracion de variable como: int var; produce una asociación entre el nombre 'var' y un espacio de almacenamiento en memoria. Por lo tanto hay dos elementos relacionados con el nombre 'var': un valor que se puede almacenar allí una dirección de memoria para la variable, algunos autores se refieren a estos dos aspectos como el "rvalue" y "lvalue" de la variable. Además del identificador "var", tenemos la palabra "int" que nos indica el TIPO (type) de la variable. El tipo nos indica: 1-CUANTAS CELDAS DE MEMORIA (bytes) se asocian a ese nombre de variable. 2-DE QUE MODO SERÁN INTERPRETADOS los datos que se encuentren en un lugar de la memoria. Un byte es la menor unidad de información que puede direccionarse en la mayoría de las computadoras. En la mayoría de las arquitecturas el tipo char ocupa un solo byte, por lo tanto es la unidad mínima. Un boolean admite sólo dos valores diferentes, pero es almacenado como un byte(en C no existe). El tipo integer ocupa generalmente 2 bytes, un long 4, double 8, y así con el resto de los tipos. El otro punto, es la relación entre LO QUE HAY en una celda de memoria y CÓMO ES INTERPRETADO. Lo que hay en un celda cuya extensión es un byte es simplemente un conjunto de ocho estados posibles (8 bits) que a nivel hardware admiten dos estados diferenciales, estados que pueden ser interpretados como 'verdadero/falso', 0/1, o cualquier otro par de valores. Una celda de memoria del sector de datos, podría contener algo como lo siguiente: En binario o en hexadecimal 61 y en decimal 97. El contenido, depende en gran parte del TIPO (type) que hayamos asociado a esa celda (y suponiendo que exista tal asociación). Ese valor interpretado como un hexadecimal es 0x61, en decimal es 97, y si fue asociada al tipo char representará la letra 'a', cuyo ASCII es igual a 97. En ninguna localidad de memoria hay algo como la letra 'a', lo que encontramos son valores binarios que en caso de estar asociados a char y en caso de que lo saquemos en pantalla como char hará que veamos encendidos ciertos pixeles de pantalla, en los cuales reconoceremos una representacion de la letra 'a'. La representación binaria de datos ocupa demasiado espacio, por ese motivo es preferible utilizar el sistema hexadecimal, además de ser muy fácil de traducir a binario es más económico que éste o el decimal. Observar los bytes de un sector de memoria de 1
  • 2. un programa facilita la comprensión sobre el modo en que cada tipo (type) se asocia a direcciones de memoria. Supongamos un programa que declara, define e inicializa, las siguientes variables: int main() { int a = 5; long b = 8; char cad[ ]= "abcd"; char ch = '6'; char hh = 77; … } La representación de estos datos en memoria, en el segmento de datos, tendría el siguiente aspecto(cada dato, 8 bits=1 byte, se representa en headecimal con dos dígitos): ffd0 ........................ 20 20 20 20 00 8F 12 00 00 00 F6 FF BC 04 00 FF ffe0 .... hhchcadba F6 F6 00 00 F6 FF C7 04 4D 36 61 62 63 64 00 00 fff0 ....................… 08 00 00 00 05 00 00 00 0B 01 00 00 00 00 00 00 Los datos que se han declarado primero en el código (int a) figuran al final, los bytes 05 00 son la representacion de la variable entera a de valor : 5, los cuatro bytes 08 00 00 00 lo son del long b: 8, luego sigue 61 62 63 64 00 00 que es el array cad:"abcd"(con final de cadena 0 más otro para que el nº de bytes sea par), el char ch:'6' que corresponde con el hexadecimal 0x36, y por último un char hh con el valor entero: 77,(0x4D en hexadecimal, carácter M en ASCII). Además podemos realizar las siguientes observaciones: 1- Que el segmento de datos almacena los datos comenzando desde el final (0xffff). La primera variable declarada y definida es el entero 'a', que no está verdaderamente en el final del segmento, es así porque esos valores (como 0B 01) guardan valores de STACK (pila) para restablecer algunos registros cuando el programa salga de main() y termine. Sobrescribir ese valor podría producir un error. 2- Que la variable entera de valor 5 guarda este valor ubicando los bytes al revés. Lo lógico sería que la representación fuera 00 05, pero los bytes están invertidos, esto es una norma general de la mayoría de los procesadores y responde a una pauta de mayor eficiencia en la lectura de variables numéricas. 3- El array cad, se declara de modo implícito con 5 bytes, las cuatro letras mas el caracter terminador '0'. Se ocupa un byte más porque un número par de bytes es más eficiente. Obsérvese que un array no invierte la posición de sus elementos. 4- Un char ocupa exactamente un byte. El primer char está definido con el caracter '6' que corresponde al ASCII 0x36, la segunda variable char hh es inicializada a partir de un valor entero 77, lo que genera una conversión implícita de tipos. Podríamos profundizar más para ver que funciones gestionan este sector de memoria, su relación con la pila (STACK) y los modelos de memoria. Más adelante veremos qué funciones gestionan el uso de memoria dinámica. Por ahora es importante tener en cuenta la relación entre el tipo (type) usado para declarar una variable y el 2
  • 3. modo en que se almacena en memoria. En la siguiente tabla se encuentran algunos ejmplos más : Representacion DECLARACION Inicializacion Numero de bytes en memoria int N; N = 5; 05 00 2 char letra; letra = 'L'; 4C 1 char cad[]="hola"; - 68 6F 6C 61 00 5 long a; a=4 04 00 00 00 4 long a; a=0x1234 34 12 00 00 4 long a; a = 65535 ff ff 00 00 4 Cuando en el flujo de un programa se asigna un valor a una variable lo que sucede es que el lugar (o lugares) de memoria asociadas a la variables son inicializadas con tal valor. La asociación entre posiciones de memoria y variable no siempre existe desde el comienzo al final de un programa. Las variables declaradas como 'locales' a una función sólo tienen asociado un lugar de memoria, mientras el flujo del programa se encuentra en tal función, al salir de la misma tales posiciones serán usadas por otros datos. En cambio las variables 'globales' o las declaradas como 'static' conservan sus posiciones de memoria durante toda la ejecución del programa. Un array es una colección ordenada de elementos del mismo tipo (type), estos tipos pueden ser los que proporciona el lenguaje, como char, int, float, long int, etc., o bien puede tratarse de un tipo definido por el programador, como una estructura o una clase. Estos elementos se encuentran ordenados en celdas consecutivas de memoria. Veamos los siguientes ejemplos: Declaracion e inicializacion Representacion en memoria Bytes int a []= {3, 345, 54, 4}; 03 00 63 01 72 01 03 27 2x4=8 int a[4]={2}; 02 00 00 00 00 00 00 00 2x4=8 char a [] = {"Mensaje 1"}; 4d 65 6e 73 61 6a 65 20 31 00 9+1= 10 char a [8] = {hola}; 68 6F 6C 61 00 00 00 00 7+1 = 8 long a [] = {9, 16, 0x23b2a}; 09 00 00 00 12 00 00 00 2a 3b 02 00 3 x 4 = 12 El tipo (type) del array determina cuántos bytes ocupa cada uno de sus elementos, y también de qué modo se almacena el dato. Es importante mencionar que este modo de inicialización es sólo posible cuando se realiza en la misma línea que en la declaración, no es posible inicializar al mismo tiempo varios elementos de un array si lo hacemos en una línea diferente a la de la declaración. También hay que mencionar el hecho de que si damos más elementos inicializadores que los que figuran entre corchetes se genera un error de compilación, si damos menos elementos el compilador inicializa el resto de los elementos con el valor '0'. 3
  • 4. Una cadena en C/C++, es representada internamente como un array de tipo char y utiliza un carácter 'terminador' (0), para indicar el fin de la cadena, ese carácter es el correspodiente al ASCII = 0. Un puntero es un tipo especial de variable, que almacena el valor de una dirección de memoria, esta dirección puede ser la de una variable individual, pero mas frecuentemente será la de un elemento de un array, una estructura u objeto de una clase. Los punteros, al igual que una variable común, pertenecen a un tipo (type), se dice que un puntero 'apunta a' ese tipo al que pertenece. Ejemplos: float * Preal ; //Declara un puntero a un real int * Pentero; //Declara un puntero a entero char * Pcaracter; //Puntero a char fecha * Pfecha; //Puntero a objeto de clase fecha Independientemente del tamaño (sizeof) del objeto apuntado, el valor almacenado por el puntero será el de una única dirección de memoria. En sentido estricto un puntero no puede almacenar la dirección de memoria de 'un array' (completo), sino la de un elemento de un array, y por este motivo no existen diferencias sintácticas entre punteros a elementos individuales y punteros a arrays. La declaración de un puntero a char y otro a array de char es igual. Al definir variables o arrays hemos visto que el tipo (type) modifica la cantidad de bytes que se usarán para almacenar tales elementos, así un elemento de tipo 'char' utiliza 1 byte, y un entero 2 o 4. No ocurre lo mismo con los punteros, el tipo no influye en la cantidad de bytes asociados al puntero, pues todas las direcciones de memoria se pueden expresar con sólo 2 bytes (o 4 si es una dirección de otro segmento) Veamos los efectos de un código como el siguiente, en la zona de almacenamiento de datos: char SALUDO[] = "hola"; char * p; p = SALUDO; //Puntero 'p' apunta a la cdena 'SALUDO' El puntero está en la dirección 0xffee por tanto el valor que hay en esa localidad de memoria es otra dirección, los bytes "F0 FF" indican que el puntero apunta a FF F0 (recordemos que los bytes numéricos se guardan en sentido inverso), donde comienza la cadena de caracteres 'SALUDO' con el contenido “hola”, más el cero de fin de cadena. En las líneas de código no hemos indicado a qué carácter del array apunta el puntero, pero esa notación es equivalente, como ya sabemos a: p = &cad[0]; 4
  • 5. que indica de modo más explícito que se trata de la dirección del primer elemento de ese array de caracteres. El juego con las direcciones puede ilustrarse también del siguiente modo: ffee F0 <----- El puntero ocupa dos bytes para representar la direccion FFF0, dirección a la que 'apunta'. ffef FF fff0 68 <------ cad[0] = ‘h’ (Primer char del array de caracteres, dirección apuntada por el puntero) fff1 6f <------ cad[1] = ‘o’ fff2 6C <------ cad[2] = ‘l’ fff3 61 <------ cad[3] = ‘a’ fff4 00 <------ cad[4] = ‘0’ (Fin del array, caracter ascii = 0 de fin de cadena) Puesto que un puntero tiene como valor una dirección de memoria, es lógico que al llamar a funciones de impresión con un puntero como argumento, la salida en pantalla sea la de una dirección de memoria. En este caso se trata de un puntero que almacena en 2 bytes una dirección de memoria, la cual es FFF0.(16 bits, 4 para cada dígito hexadecimal)) La salida en pantalla de un puntero a char es diferente, pues es tratado como apuntando a una cadena de caracteres, en tal caso no sale en pantalla una dirección de memoria, sino un conjunto de caracteres hasta encontrar el '0'. Un puntero puede almacenar la dirección de ("apuntar a") muy diferentes entidades: una variable, un objeto, una función, un miembro de clase, otro puntero, o un array de cada uno de estos tipos de elementos, también puede contener un valor que indique que no apunta actualmente a ningún objeto (puntero nulo). Tipos como 'int' o 'char', son "tipos predefinidos", pertenecientes al lenguaje. En C/C++ al igual que otros lenguajes, es posible definir tipos nuevos. Las enumeraciones, uniones, estructuras , son tipos nuevos que implementa el programador. La declaración de un tipo no produce ningún efecto en memoria, no hay ningún identificador donde almacenar un dato, por esa razón no tendría sentido, dentro de la definición de una estructura o clase , intentar dar un valor a sus datos, sería lo mismo que intentar dar un valor a un tipo predefinido, por ejemplo: int = 7; long = 83453; float = 2.3 ;char =’s’; //errores Para asignar un valor necesitamos un objeto, pues un objeto implica una región de memoria donde almacenar un valor. El almacenamiento en memoria de una unión, enumeración o estructura (C), no presenta importantes cambios respecto a los tipos predefinidos, sus elementos se ordenaran de modo consecutivo de acuerdo a su tamaño ('sizeof'). (Respecto a C, C++ aporta un nuevo tipo predefinido, las clases, entidad que no sólo es un agregado de datos sino también de funciones, y que por ello presenta novedades de importancia respecto a los tipos anteriores.) Todas las variables, arrays, punteros y objetos en general tienen una duración determinada en el transcurso del programa. Tales objetos son 'creados' y 'destruidos', o en otros términos: se asocian sus nombres (identificadores) a una zona de memoria en la cual no puede asentarse otro objeto, y tales zonas de memoria son liberadas para el uso de otros objetos. 5
  • 6. La existencia de tales objetos está determinada según tres formas básicas de usar la memoria (en C++,C): 1-Memoria estática Los objetos son creados al comenzar el programa y destruidos sólo al finalizar el mismo. Mantienen la misma localización en memoria durante todo el transcurso del programa. Estos objetos son almacenados (en compiladores Borland) al principio del segmento de datos. Los objetos administrados de este modo son: variables globales, variables estáticas de funciones (static), miembros static de clases, y literales de cualquier tipo (arrays, cadenas,...). 2- Memoria automática Los objetos son creados al entrar en el bloque en que están declarados, y se destruyen al salir del bloque. Se trata de un proceso dinámico pero manejado de modo automático por el compilador (no confundir con memoria dinámica). Tales objetos se almacenan en la pila o stack al entrar en la función o bloque. Este procedimiento se aplica a: variables locales y argumentos de función. 3-Memoria dinámica En este caso tanto la creación como destrucción de los objetos están en manos del programador. El sitio donde se almacenan tales objetos se suele denominar en ingles 'heap' o 'free store', traducido como 'montículo' o 'memoria libre'. Pero el sitio preciso donde se encuentre tal 'montículo' depende del compilador y el tipo de puntero utilizado en la reserva de memoria dinámica. Cualquier tipo de objeto puede ser creado y destruido a través de este procedimiento. En C y C++ la administración explícita de memoria por parte del programador juega un rol muy importante, no es así en otros lenguajes (Basic, Smalltalk, Perl) donde la gestión principal es automática. La administración 'manual' permite un mayor grado de flexibilidad pero también multiplica la posibilidad de errores. Un modo de gestionar memoria dinámica en C,C++, aprovechando las ventajas de la memoria automática, es la implementación de destructores que sean llamados de modo automático al salir de un bloque, y que se encarguen de la liberación de memoria dinámica. Según lo visto hasta ahora, la reserva o asignación de memoria para vectores y matrices se hace de forma automática con la declaración de dichas variables, asignando suficiente memoria para resolver el problema de tamaño máximo, dejando el resto sin usar para problemas más pequeños. Así, si en una función encargada de realizar un producto de matrices, éstas se dimensionan para un tamaño máximo (100, 100), con dicha función, se podrá calcular cualquier producto de un tamaño igual o inferior, pero aún en el caso de que el producto sea por ejemplo de tamaño (3, 3), la memoria reservada corresponderá al tamaño máximo (100, 100). Es muy útil el poder reservar más o menos memoria en tiempo de ejecución, según el tamaño del caso concreto que se vaya a resolver. A esto se llama reserva o gestión dinámica de memoria. La memoria es una colección de celdas contiguas con la capacidad de almacenar valores. Cada celda de memoria es localizada por una 'dirección', que consta de un valor de segmento y otro de offset (desplazamiento dentro del segmento). Los detalles de como opera la cpu en relación a la memoria dependen del tipo de procesador, si éste funciona en modo 'real' o 'protegido' (forma en que accede a las posiciones de memoria), del sistema operativo y de otros factores. 6
  • 7. Cada segmento tiene una capacidad de 64 Kb. Una importante directiva en todos los programas es la que determina el MODELO DE MEMORIA que utilizará el programa al ejecutarse. El default suele ser el modelo 'small', pero existen varios modelos más, sus principales diferencias están en el modo en que utilizan los segmentos para almacenar código, datos o ubicar la pila (stack). Al compilar y ejecutar un programa, podemos examinar los registros de la CPU para datos, codigo y stack, estas serían las siglas de tales registros: CS (code segment) Segmento de código DS (data segment) Segmento de datos SS (stack segment) Segmento de pila ES (extra segment) Segmento extra El modelo de memoria utilizado por nuestro programa determinará cuanto espacio (por segmentos) se usará para código, datos y stack (pila). El siguiente cuadro sintetiza las distintas opciones: Modelos de Segmentos Comentarios memoria Código, datos y stack utilizan un único segmento, por lo Tiny cs = ds = ss tanto el ejecutable no podrá ser mayor a 64 Kb. cs Un segmento para código y uno para datos y stack(pila). Small ds = ss Es el modelo default utilizado, si no se especifica otro. El código usa múltiples segmentos, datos y pila cs Medium comparten uno. Es el modelo de elección si hay gran ds = ss cantidad de código y pocos datos. Un segmento para código y múltiples segmentos para cs datos y stack. Modelo apropiado cuando hay poco código Compact ds = ss pero gran cantidad de datos. Los datos son referenciados por punteros 'far'. Múltiples segmentos para código y múltiples segmentos cs Large para código y stack. Se usan punteros 'far' para código y ds = ss para datos. cs Huge Similar a 'large'. ds = ss cs Usa punteros 'near' como el modelo 'small', pero hecho a Flat ds = ss medida para sistemas operativos de 32 bits. Estas categorías no son especificas de un lenguaje de programación, la mayoría de los compiladores de los diferentes lenguajes permiten optar por estos diferentes modelos de memoria. Distinguimos entre código y datos de forma natural, la sintaxis de C obliga a declarar, en una función, primero todos los datos antes de realizar cualquier operación (código). Pero la noción de STACK (PILA) tiene una correspondencia menos obvia con lo que observamos en un lenguaje de alto nivel, se trata de algo manejado de modo automático por el compilador. A lo sumo aparecerá en relación a mensajes de error como 'Stack overflow' o 'Desbordamiento de pila' (también 'volcado de pila', en 7
  • 8. Windows). En programación de bajo nivel (ensamblador) podemos operar directamente sobre la propia pila. La pila es una zona de memoria requerida por todo programa para un uso especial. Su función, es la de servir para el intercambio dinámico de datos durante la ejecución de un programa, principalmente para la reserva y liberación de variables locales y paso de argumentos entre funciones. El espacio utilizado para uso de la pila variará según el modelo de memoria que utilice nuestro programa. Cuando un programa utliliza el modelo de memoria SMALL usa un mismo segmento para datos y stack, 64 Kb entre ambos. Suponiendo que nuestro programa opera con tal modelo de memoria, en la mayoría de los compiladores de BorlandC++, el segmento de datos/stack presentará el siguiente aspecto, después de entrar en la función main() de un programa cualquiera: El inicio del segmento(0x0000) contiene una cadena de Copyright de Borland que no debe ser sobreescrita (pues daría el mensaje "Null pointer assignment"), luego se ubican las variables globales y constantes. Los literales, sean 'de cadena' o 'numericos' son tratados como constantes y almacenados en la parte baja. Al final de la pila (desde 0xFFFF) se guardan datos fundamentales para una buena salida del programa, y debajo se extiende una zona usada para almacenar variables locales y datos pasados como parámetros, por lo tanto es la parte mas dinámica del segmento (en el grafico la parte en blanco). El espacio total del segmento es de 64 Kb, esto significa que el montón de datos que podemos pasar a una función será un poco menor pues hay espacio ocupado por otros elementos. Esta limitación se podría salvar utilizando otro modelo de memoria, pero por ahora nos centraremos en nuestro ejemplo con modelo small. Entonces: 1º) El código del programa se ubica en un lugar de la memoria perfectamente conocido en el momento del enlazado. 2º) Los datos estáticos (variables globales, estáticas (static) y constantes) que están presentes durante todo el tiempo de ejecución, aparecen en la primera parte del segmento, después del copyright de Borland, seguidos de los literales (cadenas, arrays,...). 3º) A continuación aparecen los datos automáticos que no pueden tener una longitud conocida pues se crean al entrar en un módulo o función (parámetros), y se destruyen al salir de él, así como variables locales. Ésta es la zona de pila (stack) y es la parte más dinámica. 4º) Los datos dinámicos tampoco tienen una longitud conocida y su creación y liberación depende de las solicitudes realizadas en tiempo de ejecución. La gestión de estos datos no puede realizarse como una pila y por ello su gestión es independiente de la gestión de los datos de tipo automático. Se emplea para ello un montículo (heap) en el que Turbo C++ intenta asignar o liberar memoria en función de las necesidades. La gestión del montículo precisa conocer las direcciones de los huecos de memoria utilizados. 8
  • 9. Cuando se guardan en la pila más valores de los que caben se produce un 'stack overflow', un desbordamiento de pila. Las funciones recursivas trabajan haciendo una copia de sí mismas y guardándolas en la pila, por esa causa es frecuente provocar desbordamientos de pila de ese modo. Hay muchos motivos para utilizar la pila del modo más económico posible, y los punteros cumplen una gran utilidad en este caso, por ejemplo, al pasar arrays, estructuras u objetos entre funciones a través de una dirección (sólo 2 bytes). Otros detalles en relación a punteros. Todo puntero que esté dentro de este segmento y apunte a otra dirección del mismo segmento será un puntero 'near', para apuntar a un segmento diferente deberemos (en modelo small) explicitar un puntero 'far'. Una cuestión interesante es la de si la memoria dinámica se almacena en este segmento o en algún otro. Los detalles en la implementación de memoria dinámica son en general bastante oscuros y dependen mucho del compilador utilizado, pero si el espacio reservado se asocia a un puntero 'near' es claro que la memoria reservada estará dentro de este mismo segmento. Para estudiar este aspecto es recomendable ejecutar el programa consultando los datos del puntero, el valor de segmento donde se encuentra y el valor de segmento a donde apunta. Funciones para la asignación dinámica de memoria. Uso de punteros. Los punteros también se utilizan en la reserva dinámica de memoria, utilizándose para la creación de estructuras cuyo tamaño se decide en tiempo de ejecución (son creadas y destruidas cuando el programa las necesite), adecuado para programas donde no se pueda hacer una estimación inicial eficiente de necesidades de memoria. Como ya habíamos anunciado el lenguaje C, utiliza para la reserva dinámica de memoria una zona de espacio diferente del segmento de datos y de la pila del programa, llamada montículo (heap), para que ésta no dependa de los movimientos de la pila del programa y se puedan reservar bloques de cualquier tamaño. Existen en C dos funciones que reservan la cantidad de memoria deseada en tiempo de ejecución. Dichas funciones devuelven –es decir, tienen como valor de retorno– un puntero a la primera posición de la zona de memoria reservada. Estas funciones se llaman malloc() y calloc(), y sus declaraciones, que están en la librería stdlib.h, son como sigue: void *malloc(int n_bytes) void *calloc(int n_datos, int tamaño_dato) La función malloc() busca en la memoria el espacio requerido, lo reserva y devuelve un puntero al primer elemento de la zona reservada. La función calloc() necesita dos argumentos: - el nº de celdas de memoria deseadas - el tamaño en bytes de cada celda se devuelve un puntero a la primera celda de memoria. La función calloc() tiene una propiedad adicional: inicializa todos los bloques a cero. Existe también una función llamada free() que deja libre la memoria reservada por malloc() o calloc() y que ya no se va a utilizar. Esta función usa como argumento el puntero devuelto por calloc() o malloc(). La memoria no se libera por defecto, sino que el programador tiene que liberarla explícitamente con la función free(). El prototipo de esta función es el siguiente: 9
  • 10. void free(void *) función malloc( ) void * malloc( size ); La función malloc (memory allocate - asignar memoria) reserva una parte de la memoria y devuelve la dirección del comienzo de esa parte. Esta dirección podemos almacenarla en un puntero y así podemos acceder a la memoria reservada. La función malloc tiene el siguiente formato: puntero = (tipo_de_variable *) malloc( número de bytes a reservar ); • puntero: es una variable tipo puntero que almacena la dirección del bloque de memoria reservado. Puede ser un puntero a char, int, float,... • (tipo_de_variable *): es lo que se llama un molde. La función malloc nos reserva una cierta cantidad de bytes y devuelve un puntero del tipo void (que es uno genérico). Con el molde le indicamos al compilador que lo convierta en un puntero del mismo tipo que la variable puntero. Esto no es necesario en C, ya que lo hace automáticamente, aunque es aconsejable acostumbrarse a usarlo. Una vez reservada la memoria y guardada su dirección en un puntero podemos usar ese puntero como hemos visto y hecho hasta ahora. Si no había suficiente memoria libre, malloc devolverá el valor NULL. El puntero por tanto apuntará a NULL. Es muy importante comprobar siempre si se ha podido reservar memoria o no, comprobando el valor de puntero: if (puntero) se cumple (puntero es una dirección de memoria no nula) si hay memoria suficiente, en caso contrario es falso (puntero señala a la dirección NULL, cero). Ejemplo: float *p; p = (float *) malloc( sizeof(float)); if (p) *p = 5.0; else printf(“No se ha podido reservar memoria”); función calloc( ) void * calloc( size_t n, size_t size ); Reserva un bloque de memoria para ubicar “n” elementos contiguos de tamaño "size” bytes cada uno (ej: un vector). int *p; p = (int *) calloc( 5, sizeof(int)); p[3] = 2; función realloc( ) 10
  • 11. void * realloc( void *pant, size_t size ); Cambia el tamaño de un área de memoria reservada con anterioridad, a un tamaño de "size" bytes contiguos. Si size es mayor que el anterior tamaño, realloc() busca una nueva zona, copia allí los datos y destruye la zona anterior. Si size es menor, truncará el bloque actual al nuevo tamaño. Ejemplo: int *p, *nuevo; p = (int *) calloc( 5, sizeof(int)); nuevo = realloc(p, 10); función free( ) void free( void *punt ); Cuando ya no necesitemos más el espacio reservado debemos liberarlo, es decir, indicar al ordenador que puede destinarlo a otros fines. Si no liberamos el espacio que ya no necesitamos corremos el peligro de agotar la memoria del ordenador. Para ello usamos la función free, que funciona de la siguiente manera: free ( puntero ); Donde puntero, es un puntero que apunta al comienzo del bloque que habíamos reservado. Es muy importante no perder la dirección del comienzo del bloque, pues de otra forma no podremos liberarlo. Tras el uso de free es recomendable anular el puntero poniéndolo a NULL. La función free(), no puede usarse para liberar la memoria de las variables globales o locales (están en el segmento de datos o pila, no en el montículo). Ejemplo: float *p; p = (float *) malloc( sizeof(float)); // liberamos la zona apuntada por p free(p); p=NULL; Ejemplo: #include <stdio.h> #include <stdlib.h> void main() { unsigned long bytes; char *frase; printf("¿Cuantos bytes vamos a reservar?: "); scanf("%lu",&bytes); frase = (char *) malloc(bytes); /* Verificamos el éxito de la operación */ if (frase) { 11
  • 12. printf("La cantidad de memoria reservada es: n"); printf("%lu bytes = %lu kbn", bytes,bytes/1024); printf("El bloque reservado empieza en la direccion: %pn", frase); /* Ahora liberamos la memoria */ free( frase ); /*posicionamos el puntero*/ frase=NULL; } else printf("No se ha podido reservar memorian"); } Este programa pregunta cuánta memoria se quiere reservar. Si se consigue reservar memoria, se indica cuánta memoria se ha reservado y dónde comienza el bloque. Si no se consigue se indica mediante el mensaje: "No se ha podido reservar memoria". Si por ejemplo reservamos 2500 bytes: ¿Cuantos bytes quieres reservar?: 2500 La cantidad de memoria reservada es: 2500 bytes = 2 kbytes = 0 Mbytes El bloque reservado empieza en la dirección: 1234:0004 Si lo volvemos a ejecutar incrementando el tamaño de la memoria que queremos reservar y suponiendo por ejemplo que tenemos 32Mbytes de memoria RAM, al teclear lo siguiente: ¿Cuantos bytes quieres reservar?: 32000000 Nos dará el siguiente mensaje: No se ha podido reservar memoria La función malloc() no ha podido reservar tanta memoria, ya que se trata de casi el 100% de la memoria del ordenador, y devuelve (NULL) por lo que se nos avisa de que la operación no se ha podido realizar. Es muy recomendable mantener siempre una referencia (puntero) al inicio del vector para poder volver al principio en cualquier momento y liberarlo, así como una variable que almacene la cantidad de elementos del vector: Ejemplo: #define n 10 // nº de bytes a reservar int *p, *q; p = (int *) calloc(n, sizeof(int)); q = p; //guardamos posición inicial for(i=0; i<n; i++) //escribimos 1 en todas las posiciones de { //de memoria usando q. *q = 1; q++; } free(p); //liberamos p p=NULL; //posicionamos el puntero Ejemplo: Calculemos el producto Y de una matriz A por un vector X: {y}=[A]{x}. Hay que tener en cuenta que reservando memoria por separado para cada fila de la matriz, 12
  • 13. no se garantiza que las filas estén contiguas en la memoria. Por otra parte, de esta forma se pueden considerar filas de distinto tamaño. El nombre de la matriz se declara como puntero a vector de punteros (indirección doble), y los nombres de los vectores como punteros. Supóngase que N es una constante simbólica predefinida con el número de filas y de columnas, ya que en este caso vamos a suponer que se trata de una matriz cuadrada. Los datos serán enteros: #include <stdio.h> #include <stdlib.h> #include <conio.h> void main() { int **A, *x, *y; int N,i,j; void prod(int dim, int ** A, int * x, int * y); printf ("Dimensión de la matriz cuadrada : "); scanf("%d",&N); // reserva de memoria para la matriz A //reservamos memoria a N punteros a enteros (fila) A = (int **)calloc(N, sizeof(int *)); for (i=0; i<N; i++) //memoria para N enteros (columnas de cada fila) A[i]=(int *) calloc(N, sizeof(int)); // reserva de memoria para los vectores x e y x = (int *) calloc(N, sizeof(int)); y = (int *) calloc(N, sizeof(int)); for(i=0;i<N;i++) for (j=0;j<N;j++) { printf("nElemento de la matriz A [%d][%d]: ",i+1,j+1); //scanf("%d",(*(A+i)+j)); //scanf("%d",A[i]+j); scanf("%d",&A[i][j]); //printf ("%dn",*(&A[i]+j)); //printf("%d",*(*(A+i)+j)); printf("%d",A[i][j]); } for(i=0;i<N;i++) { printf("nElemento del vector x [%d]: ",i+1); scanf("%d",x+i); } prod(N, A, x, y); for(i=0;i<N;i++) printf("y[%d]= %dn",i+1,*(y+i)); getch(); } void prod(int N,int **mat,int *x,int *y) { int i,j,suma; 13
  • 14. for(i=0;i<N;i++) { suma=0; for (j=0;j<N;j++) suma = suma + (*(*(mat+i)+j))*(*(x+j)); *(y+i)=suma; } } Ejemplo: Vamos a sacar por pantalla una matriz unidad cuadrada, de orden N. N es solicitado al usuario del programa con lo que tenemos que asignar memoria dinámica. #include <stdio.h> #include <stdlib.h> void main () { int *p,n,i,j,k; printf( "Orden de la matriz: "); fflush(stdin); scanf ("%d",&n); p=(int *)calloc(n*n,sizeof(int)); for (i=0;i<n;i++) for (j=0;j<n;j++) if (i==j) *(p+(n*i)+j)=1; else *(p+(n*i)+j)=0; for (k=0;k<(n*n);k++) { if (k%n==0) printf ("n"); printf ("%d ",*(p+k)); } } Una estructura se convierte en dinámica cuando sus elementos pueden ser insertados o extraídos sin necesidad de algoritmos complejos, durante la ejecución del programa. Cada elemento de una estructura dinámica se denomina nodo. Son extremadamente flexibles. Podemos clasificarlas en dos grandes grupos: lineales y no lineales. 2. Estructuras dinámicas lineales 14
  • 15. Las estructuras dinámicas lineales de datos son básicamente: las listas enlazadas, las pilas y las colas. Analicemos cada una de ellas. 2.1 LISTAS Una lista es un conjunto de elementos (nodos) de un determinado tipo, compuestos por campos, y que puede variar en número, de tal forma que tendremos: - listas lineales si sus elementos se almacenan en memoria de forma secuencial, es decir contiguos, uno detrás de otro de forma sucesiva. Aquí podemos incluir los arrays, cadenas y los ficheros secuenciales que se gestionan en tiempo real. En este caso operaciones como búsqueda o modificación de elementos no es tan difícil, pero se complica la eliminación o inserción de elementos salvo quizá en la última posición de la lista, ya que habría que realizar desplazamientos y reorganizar la lista para realizar dichas operaciones. - listas enlazadas si cada uno de sus elementos o nodos, posee al menos dos campos: uno para dato o información y otro para enlace (puntero). El último nodo de la lista enlazada, por convenio apunta al valor nulo, nil. En las listas enlazadas no es necesario que los elementos se almacenen consecutivamente en memoria ya que cada nodo indica a través del puntero, donde se encuentra el siguiente elemento de la lista. Por tanto se eliminan los problemas que aparecen en las listas lineales a la hora de insertar, borrar, etc. los elementos. SIG INFO SIG INFO SIG INFO SIG ... ... .. NIL - listas circulares, cuando apuntamos el último nodo de la lista hacia el primer nodo, tenemos una lista circular. Ahora podemos acceder a cualquier nodo desde cualquier posción de la lista, el problema es que se pueden producir fácilmente bucles infinitos. Por ello el primer nodo se identifica con el nombre de cabezera y contiene información adicional para diferenciarlo del resto, a la vez que nos sirve de referencia a la hora de recorrer la lista circular. CABEZERA SIG INFO SIG INFO SIG INFO SIG ...... .. ... . .. - listas doblemente enlazadas, si cada uno de sus nodos poseen tres campos: el de información (INFO), y otros dos que apuntan al nodo anterior (ANT) y posterior (SIG) respectivamente. Ahora la ventaja es que podemos acceder a cualquier elemento desde otra posición cualquiera de la lista, avanzando o retrocediendo nodos según sea necesario. Las operaciones que podemos realizar en una lista son: - Inserción, eliminación y búsqueda de elementos. 15
  • 16. - Determinar el tamaño (nº de elementos) de la lista. - Recorrer la lista con algún propósito. - Ordenar la lista ascendente o descendentemente. - Unir varias listas en una sola. - Copiar o duplicar la lista. - Comparar dos listas. - Borrar la lista. Una lista contigua o densa almacena los elementos, como hemos dicho, de forma contigua en memoria y se complican las eliminaciones e inserciones, ya que hay que desplazar todos sus elementos. En cambio en las listas enlazadas, el almacenamiento es encadenado y su tratamiento es mucho más amplio y flexible. Es por ello, que las listas contiguas se usan muy poco frente a las enlazadas. Veamos por ejemplo, como insertamos o borramos un elemento de una lista enlazada: ....... A B C D Z A B C D Z La eliminación del elemento C, consiste en dirigir el puntero del elemento anterior a C hacia el elemento siguiente a C. La inserción de un elemento AA, consiste en dirigir el puntero del elemento que le va a preceder hacia AA y dirigir el puntero de AA hacia el elemento al que apuntaba el elemento que le va a preceder. A A B C D ...... Z Ejemplo: Crear una lista Entorno Entero L Puntero P (lista) //array de enteros de longitud L indicada por el usuario fin_entorno Inicio Escribir “Dar la longitud del la lista” Leer L Reservar (P) .............. operaciones con la lista ............... Liberar (P) 16
  • 17. fin Ejemplo: Determinar cual es elemento j-ésimo de una lista : Si L=0 entonces Escribir “lista vacía” si_no Si ((0<=j)y(j<=L-1)) entonces Escribir “B=”,P(j) si_no Escribir “Elemento no existente” fin_si fin_si Ejemplo: Borrar un elemento j de la lista: Si L=0 entonces Escribir “lista vacía” si_no Si ((0<=J)y(J<=L-1)) entonces Para I=J hasta L-2 incremento 1 P(I)=P(I+1) fin_para L=L-1 si_no Escribir “Elemento no existente” fin_si fin_si Como vemos el tratamiento es similar al de los arrays, con la única diferencia de que el espacio de memoria se asigna dinámicamente. Es el usuario, el que determina en tiempo de ejecución, cuanto espacio se va a reservar para la lista (array). Por tanto para crear una lista contigua, usamos la función calloc(), vista anteriormente. Esta función nos permite reservar, como vimos en los ejemplos, de forma dinámica L elementos de un tamaño concreto (sizeof), a través de un puntero P : P = (tipo *) calloc (L, sizeof(elemento)). Para la gestión de listas enlazadas, el procedimiento es distinto ya que los elementos no están contiguos. En este caso no reservamos un bloque de memoria, ya que es el propio compilador, el que en tiempo de ejecución va buscando los espacios libres para ir insertando los elementos que deben estar enlazados entre sí a través de un puntero. Llamaremos P al puntero, accederemos al contenido de la dirección apuntada con Val-P, y en el caso de estructuras accedemos a las direcciones de los campos con P(CAMPO) y a sus contenidos con Val-P(CAMPO). Ejemplo: Crear lista vacía Entorno Definir registro nodo 17
  • 18. Cadena INFO Puntero a registro nodo SIG fin_registro Puntero a registro nodo PRIMERO, ULTIMO fin_entorno Inicio PRIMERO(SIG)=nulo ULTIMO(SIG)=nulo .............. operaciones con la lista ............... fin Ejemplo: Insertar elemento al final de lista enlazada Inicio Puntero a registro nodo NUEVO Reservar(NUEVO) //reservamos memoria dinámica para el puntero Escribir “Información del nuevo nodo” Leer Val-NUEVO(INF) NUEVO(SIG)=nulo Si PRIMERO(SIG)=nulo entonces PRIMERO=NUEVO si_no ULTIMO=PRIMERO Mientras (ULTIMO(SIG)!=nulo) hacer ULTIMO=ULTIMO(SIG) ULTIMO=NUEVO fin_si fin Vamos a ver un ejemplo global para observar como se trabajaría con listas enlazadas en C. Este programa crea una lista y permite insertar, borrar y buscar elementos dentro de la misma: //lista enlazada #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <string.h> struct agenda { char nombre[15]; char tel[10]; struct agenda * sig; }; typedef struct agenda nodo; void visualizar(void); 18
  • 19. void buscar(void); void insertar(void); void borrar(void); nodo *primero,*ultimo; void main() { char opc; primero=(nodo *)NULL; ultimo=(nodo *)NULL; if(primero==NULL) printf ("La lista esta vacia."); do { gotoxy(15,5);printf("0. Visualizar elementos de la lista."); gotoxy(15,6);printf("1. Buscar elemento de lista."); gotoxy(15,7);printf("2. Insertar elemento de lista."); gotoxy(15,8);printf("3. Borrar elemento de lista."); gotoxy(15,9);printf("4. Terminar."); gotoxy(10,11);printf("Elegir opcion: "); scanf("%c%",&opc); fflush(stdin); printf("%c%",opc); fflush(stdin); clrscr(); switch (opc) { case '0': visualizar();break; case '1': buscar();break; case '2': insertar();break; case '3': borrar();break; case '4': clrscr();exit(0); default: printf ("ntOpcion no valida.Pulsar tecla"); getchar(); clrscr(); } }while (opc!=4); } void visualizar() { nodo * auxiliar=primero; printf ("Elementos de la lista: n"); if (auxiliar==NULL) printf ("No hay elementos en la lista"); else { while (auxiliar!=NULL) { printf ("t%s t%sn",auxiliar->nombre,auxiliar->tel); 19
  • 20. auxiliar=auxiliar->sig; } } getchar(); clrscr(); } void buscar() { int i=1,sw=0; char nom[15]; nodo * auxiliar; printf ("Nombre a buscar: "); gets(nom); auxiliar=primero; while(auxiliar!=NULL) { if (strcmp(auxiliar->nombre,nom)==0) { printf ("nombre : %s en posicion %dn",auxiliar->nombre,i); sw=1; } auxiliar=auxiliar->sig; i++; } if (sw==0) printf ("Elemento no encontrado.n"); getchar(); clrscr(); } void insertar() { nodo * nuevo; nuevo=(nodo *)malloc(sizeof(nodo)); printf ("Nombre : "); fflush(stdin); gets(nuevo->nombre); printf ("telefono : "); gets(nuevo->tel); nuevo->sig=NULL; if (primero==NULL) { printf ("Primer elemento de la lista.n"); primero=nuevo; } else { ultimo=primero; while (ultimo->sig!=NULL) 20
  • 21. ultimo=ultimo->sig; printf ("Siguiente elemento de la lista.n"); ultimo->sig=nuevo; } getchar(); clrscr(); } //lista enlazada #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <string.h> struct agenda { char nombre[15]; char tel[10]; struct agenda * sig; }; typedef struct agenda nodo; void visualizar(void); void buscar(void); void insertar(void); void borrar(void); nodo *primero,*ultimo; void main() { char opc; primero=(nodo *)NULL; ultimo=(nodo *)NULL; if(primero==NULL) printf ("La lista esta vacia."); do { gotoxy(15,5);printf("0. Visualizar elementos de la lista."); gotoxy(15,6);printf("1. Buscar elemento de lista."); gotoxy(15,7);printf("2. Insertar elemento de lista."); gotoxy(15,8);printf("3. Borrar elemento de lista."); gotoxy(15,9);printf("4. Terminar."); gotoxy(10,11);printf("Elegir opcion: "); fflush(stdin); scanf("%c",&opc); fflush(stdin); printf("%c",opc); fflush(stdin); clrscr(); switch (opc) { 21
  • 22. case '0': visualizar();break; case '1': buscar();break; case '2': insertar();break; case '3': borrar();break; case '4': clrscr();exit(0); default: printf ("ntOpcion no valida.Pulsar tecla"); getchar(); clrscr(); } }while (opc!=4); } void visualizar() { nodo * auxiliar=primero; printf ("Elementos de la lista: n"); if (auxiliar==NULL) printf ("No hay elementos en la lista"); else { while (auxiliar!=NULL) { printf ("t%s t%sn",auxiliar->nombre,auxiliar->tel); auxiliar=auxiliar->sig; } } getchar(); clrscr(); } void buscar() { int i=1,sw=0; char nom[15]; nodo * auxiliar; printf ("Nombre a buscar: "); gets(nom); auxiliar=primero; while(auxiliar!=NULL) { if (strcmp(auxiliar->nombre,nom)==0) { printf ("nombre : %s en posicion %dn",auxiliar->nombre,i); sw=1; } auxiliar=auxiliar->sig; i++; } 22
  • 23. if (sw==0) printf ("Elemento no encontrado.n"); getchar(); clrscr(); } void insertar() { nodo * nuevo; nuevo=(nodo *)malloc(sizeof(nodo)); printf ("Nombre : "); fflush(stdin); gets(nuevo->nombre); printf ("telefono : "); gets(nuevo->tel); nuevo->sig=NULL; if (primero==NULL) { printf ("Primer elemento de la lista.n"); primero=nuevo; } else { ultimo=primero; while (ultimo->sig!=NULL) ultimo=ultimo->sig; ultimo->sig=nuevo; } getchar(); clrscr(); } void borrar() { nodo * penult; char nom[15]; int enc=0; printf ("Nombre a borrar: n"); gets(nom); if (primero==NULL) printf ("No hay elementos para eliminarn"); else { if (strcmp(primero->nombre,nom)==0) { if(primero->sig==NULL) { primero=NULL; printf("No quedan registros en la lista.n"); 23
  • 24. } else { primero=primero->sig; } } else { ultimo=primero; penult=primero; while (strcmp(ultimo->nombre,nom)!=0) { if(ultimo->sig==NULL) { printf("Elemento no halladon"); enc=0; break; } else { penult=ultimo; ultimo=ultimo->sig; enc=1; } } if (enc) penult->sig=ultimo->sig; } } getchar(); clrscr(); } Otra forma muy usual de gestionar listas, es a través de indirección doble, pasando la dirección del nodo que está en la primera posición de la lista (al tratarse de un puntero que apunta a una zona de memoria, para pasar la dirección de la variable (que es un puntero) necesitamos una indirección doble. Ejemplo: #include <stdio.h> #include <stdlib.h> typedef struct n { int inf; struct n *sig; } nodo; void main() { 24
  • 25. void insertar(nodo **pri,nodo *d); void mostrar (nodo *pri); //creamos la lista vacía; nodo *n1=NULL,*dato,*ult; //pedimos datos del nuevo registro do { printf ("Introducir informacion: "); //reservamos memoria para el nodo dato=(nodo *)malloc(sizeof(nodo)); scanf ("%d",&dato->inf); if (dato->inf==0) break; dato->sig=NULL; //insertamos el elemento insertar (&n1,dato); }while (dato->inf!=0); mostrar(n1); printf ("El primer elemento de la lista es : %dn",n1->inf); ult=n1; while (ult->sig!=NULL) ult=ult->sig ; printf ("El ultimo elemento de la lista es : %d",ult->inf); free(dato); } void mostrar (nodo * pri) { int i=1; while (pri!=NULL) { printf ("Elemento %d: %dn",i,pri->inf); pri=pri->sig ; i++; } } void insertar (nodo **pri,nodo *dato) { nodo *aux; aux=*pri; if(aux==NULL) { aux=malloc(sizeof(nodo)); printf ("Primer elemento de la lista.n"); *aux=*dato; *pri=aux; } else { 25
  • 26. while (aux->sig!=NULL) aux=aux->sig ; aux->sig=dato; } } Observamos que en todo momento sabemos cuáles son y dónde están, el primer elemento de la lista, así como el último. De esta forma podríamos hacer inserciones o borrar elementos de posiciones intermedias. En estos ejemplos insertamos y borramos por el final de la lista, pero se podrían modificar las funciones, solicitando una posición a insertar o borrar un elemento de la lista. Como además, sabemos almacenar información en un fichero, podríamos almacenar la lista en un fichero, etc.. 2.2 PILAS Las pilas también denominadas stacks son un tipo de listas lineales en las que la inserción y extracción de elementos, se realiza por uno de los extremos, denominado cima o tope (top). Ejemplos muy usuales de la vida real serían: una pila de platos, una pila de libros, un conjunto de monedas apiladas, una bandeja encima de otra en un comedor, etc. Para poder acceder a cualquier elemento, es necesario ir extrayendo los que se han colocado posteriormente. Las dos operaciones más usuales asociadas a las pilas son: - Push (meter o poner): operación de inserción de un elemento en la pila. - Pop (sacar o quitar): operación de eliminación de un elemento de la pila. Por tanto los elementos se extraen en orden inverso al de inserción y se trata de una estructura LIFO (Last In- First Out), último en entrar- primero en salir. aa ¨¨¨¨¨¨ cima dd cima cc aa bb cc dd cc dd ..... cima aa Las pilas se gestionan a través de un puntero P denominado stack pointer, a través del cual realizamos las inserciones y eliminaciones de elementos. P se dirige al elemento de la pila que se situa en la cima. Al principio, cuando la pila está vacia, el puntero es nulo P=0 (NULL). Es usual y conveniente, para la gestión de las pilas, al igual que de las listas y colas, crear subprogramas para PONER (push) y QUITAR(pop) elementos en la misma. También es usual crear una variable o función booleana VACIA, para determinar si la pila está vacía, ya que no podemos eliminar elementos de una pila vacía. Por otro lado si la pila se crea reservando una cantidad fija de memoria de longitud l, se producirá un desbordamiento cuando se intente sobrepasar dicho límite, intentando insertar un elemento más allá del espacio reservado. 26
  • 27. Las aplicaciones de las pilas son múltiples, son usadas muy frecuentemente por compiladores, sistemas operativos y programas de aplicación. Por ejemplo, cuando en un programa se hace una llamada a un subprograma, es necesario almacenar el lugar dónde se hizo la llamada y los datos que se estaban usando para que cuando el programa principal retorne tras la ejecución del subprograma, sepa encontrar dichos elementos. Imaginemos además, que estas llamadas son sucesivas, de unos módulos a otros, y que no sólo almacenemos los últimos datos a recuperar, sino las sucesivas informaciones relativas a cada una de las llamadas de unos módulos a otros. La pila en este caso almacenaría primero los datos de la función principal F1 para ejecutar la función F2, a continuación cuando F2 llama a otra función F3, se almacenan los datos de F2 para ejecutar la función F3, etc. Cuando se ejecute la última función, que no llama a ningún otro módulo, se va recuperando la información en orden inverso a su almacenamiento hasta llegar a la función principal. Veamos algún ejemplo de cómo podemos gestionar una pila. Ahora no necesitamos guardar la posición del primer elemento y el último, como en las listas, ya que las inserciones o eliminaciones se realizan por el final. Luego sólo necesitamos saber, dónde se ubica el último elemento, e ir añadiendo o eliminando a partir de él, siempre y cuando la lista no esté vacía (comprobación a realizar cada vez que se realiza una operación) en cuyo caso sólo se podrían insertar elementos. Ejemplo: #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <conio.h> typedef struct n { int inf; struct n *ant; }nodo; nodo *ultimo=NULL,*dato; void main() { void insertar(void); void sacar(void); void mostrar(void); char res; do { dato=malloc(sizeof(nodo)); printf ("nIntroducir o eliminar, finalizar con S:(I/E) "); fflush(stdin); scanf("%c",&res); 27
  • 28. if(toupper(res)=='S') break; if (toupper(res)=='I') { printf ("Introducir informacion:n"); scanf("%d",&dato->inf); clrscr(); insertar(); } else if (toupper(res)=='E') sacar(); mostrar(); } while (toupper(res)!='S'); printf ("La pila queda al final:n"); mostrar(); } void sacar(void) { if (ultimo==NULL) printf ("No hay elementos en la pilan"); else ultimo=ultimo->ant; } void mostrar(void) { nodo *aux=ultimo; while (aux!=NULL) { printf ("%dn",aux->inf); aux=aux->ant; } } void insertar(void) { if (ultimo==NULL) { ultimo=malloc(sizeof(nodo)); ultimo=dato; ultimo->ant=NULL; } 28
  • 29. else { dato->ant=ultimo; ultimo=malloc(sizeof(nodo)); *ultimo=*dato; } } Como vemos, para la gestión de una pila sólo es necesario contar con dos operaciones: “sacar” e “introducir” (push, pop) , ya que siempre estamos al final, en el último elemento introducido. Además no es necesario saber donde está ubicado el primer elemento, ya que siempre nos movemos desde el último, por tanto sólo es necesario un puntero, en nuestro caso llamado “ultimo”, que nos da la posición del último elemento. El único cuidado, es saber en todo momento si la pila se ha quedado vacía, ya que entonces no se podrán eliminar elementos. 2.3 COLAS Se trata de otro tipo de estructura lineal similar a las pilas, con la única diferencia de que el modo de inserción y extracción de elementos, se realiza de forma diferente. La inserción de un nuevo elemento en una cola (queue), se realiza como en las pilas, por el final de la estructura, después del último elemento insertado. Mientras que la extracción de un elemento, se realiza por la cabecera o inicio de la estructura. Tenemos también claros ejemplos en la vida real: la cola de espera de un cine, etc. Se trata pues, de unas estructuras de tipo FIFO (First In-First Out), primero en entrar- primero en salir. Por tanto usaremos colas, cuando necesitemos una estructura en la que se han de procesar los datos según su orden de aparición o inserción. Existen casos particulares, por ejemplo la bicola o doble cola, que es una cola en la que las inserciones y extracciones se relizan por ambos extremos. Sabiendo cual es el mecanismo de inserción, borrado o modificación en una lista y una pila, gestionar una cola es muy sencillo, ya que la única modificación hay que realizarla a la hora de eliminar un elemento, que ha de realizarse por el principio de la estructura. Por tanto, habrá que tener constancia en todo momento, de cual es el primer elemento de la cola para eliminar elementos, cual es el último elemento para introducir elementos y si la cola se queda vacía tras realizar una extracción. Ejemplo: //Gestion de una cola #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <ctype.h> typedef struct n { int dato; 29
  • 30. struct n *sig; } nodo; nodo *primero=NULL,*ultimo=NULL; void main() { void sacar(); void introducir(); void mostrar(); char OPC; do { printf ("nElegir opcion: n"); printf ("tA) Introducir n"); printf ("tB) Sacar n"); printf ("tC) Salir n"); fflush(stdin); OPC=getch(); OPC=toupper(OPC); switch (OPC) { case 'A': introducir(); mostrar(); break; case 'B': sacar(); mostrar(); break; default: if(OPC!='C') printf ("Eleccion no validan"); } }while (OPC!='C'); printf ("La cola queda finalmente:n"); mostrar(); } void introducir() { nodo * aux; aux=malloc(sizeof(nodo)); printf ("nIntroducir nuevo dato: "); fflush(stdin); scanf("%d",&aux->dato); aux->sig=NULL; if(primero==NULL) { primero=aux; } else 30
  • 31. { ultimo=malloc(sizeof(nodo)); ultimo=primero; while (ultimo->sig!=NULL) ultimo=ultimo->sig; ultimo->sig=aux; } } void sacar() { if(primero==NULL) printf ("No hay elementos en la colan"); else primero=primero->sig; } void mostrar() { nodo *aux=primero; while (aux!=NULL) { printf ("%dt",aux->dato); aux=aux->sig; } } 3. Estructuras dinámicas no lineales Las estructuras dinámicas no lineales son: los árboles y los grafos. Estas estructuras son mucho más difíciles de manejar, es por ello y sobre todo porque no hay tiempo suficiente, por lo que sólo vamos a comentar como se organizan. Lo que dará una clara idea de la complejidad a la hora de gestionarlas. 3.1 LOS ÁRBOLES Es una de las estructuras más importantes de las ciencias de la computación. Permiten la representación natural de muchas clases de información y datos, además de permitir la resolución algorítmica de muchos problemas. Las estructuras en árbol se usan cuando representamos información, en la que existe una relación jerárquica entre sus elementos. En el mundo real, tenemos claros ejemplos: árbol genealógico, ejemplos de combinatoria matemática, etc. Los elementos de un árbol son: - Elementos constituyentes: Nodos: cada uno de los elementos del árbol (vértices), que constituye una información. Conexiones: cada una de las líneas que expresan una relación entre dos nodos. A 31
  • 32. B nodos C D E F conexiones - Estructura del árbol: Nodo raíz : nodo más alto de la jerarquía a partir del cual, se conectan los demás. Ej: A. Nodo terminal u hoja : el que no contiene ningún subárbol . Ej: C. Nodo interno : aquél que tiene padre e hijos. No es ni raíz, ni terminal. Hijos de un nodo : aquellos nodos conectados hacia abajo, en la jerarquía, de forma directa. Ej: B y C son hijos de A. Descendientes: subárbol o conjunto de nodos de un nodo, que encontramos hacia abajo, en la jerarquía del árbol. Ascendientes : subárbol o conjunto de nodos de un nodo, que encontramos hacia arriba en la jerarquía del árbol. Padre : antecesor directo de un nodo. Todos los nodos tiene padre salvo el ríz. Ej: B es padre D, E y F. Hermanos : nodos hijos del mismo padre. Bosque : una colección de dos o más árboles. - Topología del árbol: Grado de un nodo: número máximo de hijos del nodo. Grado de un árbol: grado máximo de los nodos del árbol. Camino entre dos nodos: es la sucesión de nodos que hay que visitar para ir desde el nodo inicial hasta el final. Ej.: {(A,B), (B,E)} camino de A a E. Rama : camino que finaliza en un nodo terminal u hoja. Nivel de un nodo: Es la longitud del camino que inicia en el nodo raíz (nivel 0) y acaba en el propio nodo. Ej: B y C tienen nivel 1. Altura o profundidad de un árbol : es el nivel máximo del árbol más uno. Ej: el árbol del ejemplo tiene altura 3. Árboles binarios Es un conjunto finito, de cero o más nodos que cumplen: - cada nodo puede tener 0, 1 ó 2 subárboles conocidos como subárbol izquierdo y derecho. - El número de nodos en el nivel i puede ser como máximo 2i . - El número de nodos del árbol de altura k, puede ser como máximo 2k –1. 32
  • 33. En el caso de que todos los nodos de un árbol binario tengan dos subárboles, se dice que es además un árbol binario completo. Entonces, por ejemplo, el nº máximo de nodos n de un árbol binario de altura h, sería nh –1 (nº de nodos si es completo). Representación de árboles binarios completos mediante vectores: Sea v[i] un vector que represente el árbol mediante el siguiente orden: --------------------------------- | | | | | | | | | --------------------------------- v 0 1 2 3 4 5 6 7 Si consideramos desde i=1 como primer elemento, ubicaríamos los elementos del árbol de la siguiente manera: Nodo raíz => v[1] Hijo izquierdo de v[i] => v[2i] si 2i <= n Hijo derecho de v[i] => v[2i+1] si 2i+1 <= n Padre de v[i] => v[i/2] si i > 1 ( v[i] es una hoja si 2i > n ) v[1] nodo raíz v[2] hijo izdo. del raíz v[3] hijo dcho. del raíz v[4] hijo izdo. de v[2] v[5] hijo dcho. de v[2] v[6] hijo izdo. de v[3] v[7] hijo izdo. de v[3], etc Para que coincidan los elementos con los índices del vector bastaría iniciar en i=0, para ello basta restar una unidad a todos los índices obtenidos (2i-1 y 2i). Un árbol es vacío si no posee nodos ni conexiones y es lleno, si todos sus niveles están llenos (2 hijos), salvo el último nivel. Otra forma de almacenar y recorrer un árbol sería usando asignación dinámica mediante dos punteros que almacenan la información de los dos hijos de cada nodo: typedef struct nodoarbolbin { tipo_info clave; struct nodoarbolbin *izq; struct nodoarbolbin *der; } NODOARBOL; NODOARBOL *arbol; Al igual que hacíamos con las listas, basta guardar la posición del nodo raíz a partir del cual podemos acceder mediante aritmética de punteros a los nodos sucesivos. 33
  • 34. Métodos de recorrido del árbol binario: Entendemos por recorrer un árbol visitar (y procesar) cada uno de los nodos que lo componen, una sola vez y en un determinado orden. Existen 3 métodos, los cuales difieren en el orden en que procesamos los nodos, y que comúnmente a la hora de entrar en nuevos nodos comienzan por el subárbol izquierdo: Método preorden: a). Visitar el nodo (y procesarlo). b). Recorrer el subárbol izq en preorden c). Recorrer el subárbol der en preorden. Método inorden: a). Recorrer el subárbol izq en inorden b). Visitar el nodo (y procesarlo). c). Recorrer el subárbol der en inorden. Método postorden: a). Recorrer el subárbol izq en postorden b). Recorrer el subárbol der en postorden. c). Visitar el nodo (y procesarlo). Por ejemplo: void PreOrden( NODOARBOL *nodo) { if( nodo!=NULL) { procesar(nodo); PreOrden(nodo->izq); PreOrden(nodo->der); } } Esta función recursiva trata el método preorden para recorrer y leer el árbol. Ejemplos: * Recorrido pre-orden *+xyz + z Recorrido in-orden x+y*z 34
  • 35. Recorrido post-orden xy+z* x y Recorrido pre-orden MEBADLPNVTZ M Recorrido in-orden ABDELMNPTVZ Recorrido post-orden ADBLENTZVPM E P B L N V A D T Z Ejemplo: Vamos a crear el primer árbol del ejemplo anterior y vamos a recorrerlo usando los tres métodos expuestos anteriormente: pre-orden, in-orden y post-orden. #include <stdio.h> #include <stdlib.h> #include <conio.h> typedef struct nodo { char dato; struct nodo *hijo_izq; struct nodo *hijo_der; } ARBOL; void PreOrden(ARBOL *ptr); void Inorden(ARBOL *ptr); void PostOrden(ARBOL *ptr); void main() { ARBOL *n1, *n2, *n3, *n4, *n5; n1 = malloc(sizeof(ARBOL)); n2 = malloc(sizeof(ARBOL)); n3 = malloc(sizeof(ARBOL)); 35
  • 36. n4 = malloc(sizeof(ARBOL)); n5 = malloc(sizeof(ARBOL)); n1->dato = '*'; n1->hijo_izq = n2; n1->hijo_der = n3; n2->dato = '+'; n2->hijo_izq = n4; n2->hijo_der = n5; n3->dato = 'z'; n3->hijo_izq = NULL; n3->hijo_der = NULL; n4->dato = 'x'; n4->hijo_izq = NULL; n4->hijo_der = NULL; n5->dato = 'y'; n5->hijo_izq = NULL; n5->hijo_der = NULL; printf("Recorido en preorden => "); PreOrden(n1); printf("nRecorrido en orden simetrico => "); Inorden(n1); printf("nRecorrido en postorden => "); PostOrden(n1); getch(); } void PreOrden(ARBOL *ptr) { if(ptr != NULL) { printf("%c",ptr->dato); PreOrden(ptr->hijo_izq); PreOrden(ptr->hijo_der); } } void Inorden(ARBOL *ptr) { if(ptr != NULL) { OrdenSimetrico(ptr->hijo_izq); printf("%c",ptr->dato); OrdenSimetrico(ptr->hijo_der); } } void PostOrden(ARBOL *ptr) { if(ptr != NULL) 36
  • 37. { PostOrden(ptr->hijo_izq); PostOrden(ptr->hijo_der); printf("%c",ptr->dato); } } 3.2 LOS GRAFOS Los árboles anteriormente comentados, son estructuras complejas, pero por lo menos, tienen una jerarquía bien diferenciada, que permite ir avanzando desde el nodo y raíz elegido un método de recorrido, por algunos de los caminos descendentes( según nuestra representación). En el mejor de los casos puede tratarse de un árbol binario, que podría incluso convertirse en un array si se trata de un árbol binario completo. Sin embargo los grafos , que son estructuras no lineales que tienen un gran número de aplicaciones, eliminan las restricciones que tienen los árboles, permitiendo que cualquier nodo, enlace con cualquiera de los demás. De este modo no existen padres e hijos, sino relaciones entre nodos, que complican extremadamente la gestión de dichas estructuras. Elementos constituyentes de los grafos: Zaragoza Madrid Barcelona Sevilla Jaen Soria Avila Cuenca Avila - nodos o vértices, cada uno de los elementos de información. - líneas, aristas o arcos, uniones entre dos nodos. Un grafo se denomina sencillo, si no existe un nodo que enlaza consigo mismo, y sólo existe un arco o línea entre dos nodos. Un camino es una secuencia de dos o más arcos o líneas que conectan dos nodos entre sí. Ej.: C(Madrid,Barcelona)= (Madrid, Zaragoza) (Zaragoza, Barcelona). Dos grafos son adyacentes si existe un arco que los une. Por otro lado los arcos pueden indicar una dirección en cuyo caso tendremos los grafos dirgidos. En otros casos puede que cada línea tenga un peso (valor establecido). En nuestro ejemplo de ciudades, podemos indicar la dirección de las líneas entre 37
  • 38. cada dos nodos y podríamos establecer un peso o valor para cada una de ellas por ejemplo la distancia, tiempo de recorrido, etc. 6 A B 7 3 C Por tanto una vez expuestas las características básicas de los grafos, podemos hacernos una idea de la complejidad que pueden tener los algoritmos que gestionen la creación y el posterior acceso a un grafo para realizar cualquier actualización de l mismo. Suelen construirse las matrices de adyacencia, en las que se representan las conexiones de cada uno de los nodos con todos los demás. A través de las mismas podemos gestionarlos. 6 2 3 1 5 4 (Grafo no dirigido) Matriz de adyacencia IJ 1 2 3 4 5 6 1 0 1 0 0 0 0 2 1 1 1 0 0 0 3 0 1 0 1 1 1 4 0 0 1 0 0 0 5 0 0 1 0 0 0 6 0 0 1 0 0 0 A partir de aquí podemos crear unas listas de adyacencias, que podrían gestionarse mediante punteros: Elementos 1 2 NULO NUNUL 2 1 2 3 NULO 2 5 6 NULO 3 4 NUNUL NUNUL 3 NULO 4 NUNUL 3 NULO 38
  • 39. 5 6 3 39