Your SlideShare is downloading. ×
TEMA 7

ESTRUCTURAS DE DATOS: ESTRUCTURAS DINÁMICAS

       1. Asignación dinámica de memoria. Punteros.
       2. Estruct...
un programa facilita la comprensión sobre el modo en que cada tipo (type) se asocia a
direcciones de memoria.
       Supon...
modo en que se almacena en memoria. En la siguiente tabla se encuentran algunos
ejmplos más :




                        ...
Una cadena en C/C++, es representada internamente como un array de tipo char
y utiliza un carácter 'terminador' (0), para ...
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...
La existencia de tales objetos está determinada según tres formas básicas de usar
la memoria (en C++,C):

1-Memoria estáti...
Cada segmento tiene una capacidad de 64 Kb. Una importante directiva en todos
los programas es la que determina el MODELO ...
Windows). En programación de bajo nivel (ensamblador) podemos operar directamente
sobre la propia pila.
        La pila es...
Cuando se guardan en la pila más valores de los que caben se produce un 'stack
overflow', un desbordamiento de pila. Las f...
void free(void *)

función malloc( )

void * malloc( size );

       La función malloc (memory allocate - asignar memoria)...
void * realloc( void *pant, size_t size );

       Cambia el tamaño de un área de memoria reservada con anterioridad, a un...
printf("La cantidad de memoria reservada es: n");
        printf("%lu bytes = %lu kbn", bytes,bytes/1024);
        printf(...
no se garantiza que las filas estén contiguas en la memoria. Por otra parte, de esta
forma se pueden considerar filas de d...
for(i=0;i<N;i++)
  {
       suma=0;
        for (j=0;j<N;j++)
               suma = suma + (*(*(mat+i)+j))*(*(x+j));
     ...
Las estructuras dinámicas lineales de datos son básicamente: las listas enlazadas,
las pilas y las colas. Analicemos cada ...
-   Determinar el tamaño (nº de elementos) de la lista.
       -   Recorrer la lista con algún propósito.
       -   Orden...
fin

Ejemplo: Determinar cual es elemento j-ésimo de una lista :
     Si L=0 entonces
             Escribir “lista vacía”
...
Cadena INFO
                   Puntero a registro nodo SIG
       fin_registro
       Puntero a registro nodo PRIMERO, ULT...
void buscar(void);
void insertar(void);
void borrar(void);
nodo *primero,*ultimo;

void main()
{
char opc;
primero=(nodo *...
auxiliar=auxiliar->sig;
        }
    }
getchar();
clrscr();
}

void buscar()
{
int i=1,sw=0;
char nom[15];
nodo * auxilia...
ultimo=ultimo->sig;
  printf ("Siguiente elemento de la lista.n");
  ultimo->sig=nuevo;
  }
getchar();
clrscr();
}

//list...
case '0': visualizar();break;
        case '1': buscar();break;
      case '2': insertar();break;
      case '3': borrar()...
if (sw==0)
        printf ("Elemento no encontrado.n");
getchar();
clrscr();
}

void insertar()
{
nodo * nuevo;
nuevo=(nod...
}
     else
       {
       primero=primero->sig;
       }
    }
 else
  {
  ultimo=primero;
  penult=primero;
  while (st...
void insertar(nodo **pri,nodo *d);
  void mostrar (nodo *pri);
                 //creamos la lista vacía;
   nodo *n1=NULL...
while (aux->sig!=NULL)
       aux=aux->sig ;
    aux->sig=dato;
    }
}

Observamos que en todo momento sabemos cuáles son...
Las aplicaciones de las pilas son múltiples, son usadas muy frecuentemente por
compiladores, sistemas operativos y program...
if(toupper(res)=='S')
                 break;
        if (toupper(res)=='I')
                 {
                 printf ("...
else
         {
    dato->ant=ultimo;
    ultimo=malloc(sizeof(nodo));
    *ultimo=*dato;
    }
}

        Como vemos, par...
struct n *sig;
       } nodo;

nodo *primero=NULL,*ultimo=NULL;

void main()
{
void sacar();
void introducir();
void mostr...
{
        ultimo=malloc(sizeof(nodo));
        ultimo=primero;
        while (ultimo->sig!=NULL)
               ultimo=ult...
B                                        nodos
                                               C

             D           ...
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 comple...
Métodos de recorrido del árbol binario:

        Entendemos por recorrer un árbol visitar (y procesar) cada uno de los nod...
Recorrido post-orden    xy+z*
                                                  x           y




Recorrido pre-orden MEBA...
n4 = malloc(sizeof(ARBOL));
         n5 = malloc(sizeof(ARBOL));
         n1->dato = '*';
         n1->hijo_izq = n2;
    ...
{
              PostOrden(ptr->hijo_izq);
              PostOrden(ptr->hijo_der);
              printf("%c",ptr->dato);
  ...
cada dos nodos y podríamos establecer un peso o valor para cada una de ellas
por ejemplo la distancia, tiempo de recorrido...
5

6
    3




        39
Upcoming SlideShare
Loading in...5
×

Tema7 dinamicas

1,758

Published on

0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total Views
1,758
On Slideshare
0
From Embeds
0
Number of Embeds
6
Actions
Shares
0
Downloads
50
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Transcript of "Tema7 dinamicas"

  1. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 39. 5 6 3 39

×