Vista previa del material en texto
PROGRAMACIÓN. 2016. MIGUEL CARRASCO 1 RESUMEN PROGRAMACIÓN 18-04-2016 Introducción En esta guía de repaso estudiaremos un resumen de los principales conceptos vistos durante el semestre en Programación. Es importante aclarar al lector que esta guía solo cubre aspectos fundamentales de la materia, pero existen otros detalles que no serán analizados (por temas de espacio). Para estudiar con esta guía te recomendamos revisar el material disponible en Webcursos y en CS50, y luego responder la guía de ejercicios que realizaremos en clases. Buena suerte! NÚMEROS BINARIOS I. El bit es la unidad básica que genera un número binario y sólo posee dos símbolos {0, 1}. El valor 0 representa el bit apagado, y el valor 1 el bit encendido. A medida que empleamos más bits, podemos generar una mayor combinación de números. Una secuencia de 8 bits equivale a 1byte. Como vimos anteriormente, el alfabeto de los números binarios lo componen dos símbolos, es decir, posee base 2. En cambio, los números decimales están compuestos por diez símbolos {0, 1, …9} y su base es diez. Por ejemplo, para generar el número decimal 231, empleamos la siguiente operación (note como empleamos la base y su exponente para el desarrollo): 2 × 102 + 3 × 101 + 1 × 100 = 231 (DECIMAL) Para transformar un número binario a decimal desarrollamos la siguiente formulación, por ejemplo, para el número binario 101100 (observe cómo opera la base 2 y el exponente en la posición del número binario): 1 × 25 + 0 × 24 + 1×23 + 1×22 +0 × 21 + 0×20 = 32+0+8+4+0+0 = 44 (DECIMAL) Si deseamos transformar un número Decimal a Binario, dividimos el número por dos, y almacenamos la parte entera, hasta alcanzar como resultado de la operación el cero. En el siguiente ejemplo, observe que al leer los resultados del resto de la división desde el menor número hasta el mayor, este corresponde exactamente al número binario buscado. 44/2 = 22 ( 22×2+0=44 ) resto 0) 22/2= 11 ( 11×2+0=22 ) resto 0) 11/2= 5 ( 5×2+1=11 ) resto 1) 5/2= 2 ( 2×2+1= 5 ) resto 1) 2/2= 1 ( 1×2+0= 2 ) resto 0) 1/2= 0 ( 0×2 +1= 1 ) resto 1) 0 0 1 1 0 1 Lea los valores desde abajo hacia arriba Codigo binario Encendido Apagado Encendido PROGRAMACIÓN. 2016. MIGUEL CARRASCO 2 RESUMEN PROGRAMACIÓN 18-04-2016 NÚMEROS HEXADECIMALES II. Los números Hexadecimales poseen una base de 16 símbolos, los cuales corresponden a los dígitos {0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F}. Al igual que en el caso anterior, podemos representar un número en una determinada representación y transformarlo a otra. Por ejemplo, el número F corresponde al número decimal 15 y al número binario 1111. Por convención, se utiliza un símbolo para indicar que estamos empleando un número hexadecimal. En este caso el número 15 (en decimal) se escribe como 0xF en hexadecimal. Los computadores utiliza este tipo de base, ya que requiere una menor combinación de símbolos para alcanzar una dirección determinada en memoria. Para transformar un número binario a uno decimal, simplemente debemos agrupar en grupos de 4 bits. Así, para escribir el número 44 (decimal) a Hexadecimal, podemos simplemente emplear la representación binaria y luego transformarlo a Hexadecimal. Veamos un ejemplo: 44 (DEC) >> 101100 (bin) >> 0010 1100 (bin) >> 2C >> 0x2C En este caso para completar los bits en grupos de cuatro, agregamos dos ceros antes, y así completamos 8 bits (1 Byte). Luego, dividimos el grupo en dos partes, donde 0010 corresponde al valor 2 (hex) y 1100 al valor C (hex) (12 en valor decimal). Finalmente escribimos el número 0X2C como el valor hexadecimal. OVERFLOW DE DATOS III. Las variables que creamos en el lenguaje de programación C poseen un tipo de datos definido que el programador define explícitamente. El tipo de datos indica tanto el tipo de variable como la cantidad máxima de valores que podemos almacenar en ella. El overflow ocurre cuando sobrepasamos dicho límite. En este caso, el computador comienza a recorrer los valores desde el rango negativo. En la siguiente tabla podemos ver la cantidad de valores y rangos admitidos según el tipo de datos definido en las primeras versiones del lenguaje C. Por ejemplo Si nos remontamos a la época en que fue diseñado el lenguaje C, los computadores no poseían mucha capacidad en memoria. Por esta razón los primeros programadores se veían enfrentados a diseñar programas con una fuerte restricción en memoria. Esto permitió diseñar programas elegantes y eficientes en términos del uso de recursos. Es importante aclarar que los computadores actuales, el tipo de dato entero (int) se ha extendido a 4 bytes (32 bits). 2 (DEC) 12 (DEC) Representación hexadecimal Tipo de datos Valores Bits Bytes Rango de valores admitidos char carácter 8 1 -128 a 127 int entero 16 2 -32.768 a 32.767 unsigned int entero 16 2 0 a 65.535 long int entero 32 4 -2.147.483.648 a + 2.147.483.647 float flotante 32 4 - 3.40284 e+38 a +3.40284 e+38 double flotante 64 8 -1.79769 e+308 a 1.79769 e+308 long double flotante 80 10 -1.189731 e-4932 a +1.189731 e+4932 PROGRAMACIÓN. 2016. MIGUEL CARRASCO 3 RESUMEN PROGRAMACIÓN 18-04-2016 ESTRUCTURA DEL PROGRAMA IV. Todos los programas escritos en el lenguaje C comienzan su ejecución desde la función principal “main()”. Para que el programa pueda manipular datos, desplegar información por pantalla, leer cadenas de texto, entre otros, el lenguaje dispone de librerías, las cuales permiten la manipulación y manejo de datos. Entre ellas, la más utilizada es la librería “stdio.h”, ya que dispone de funcionalidades para imprimir en pantalla a través de la función “printf”. El nombre de la librería indica el acrónimo Standard Input Output, la cual permite la entrada y salida de datos. Las librerías son un conjunto de archivos que contienen funciones y definiciones las cuales facilitan al programador abstraerse de la implementación de sub problemas, y enfocarse sólo en la solución de su problema. De esta forma, las funcionalidades más conocidas ya se encuentran implementadas por el propio lenguaje. Otros aspectos de la estructura tienen relación con la sintaxis. El lenguaje C, al igual que el lenguaje Español (Inglés, Alemán, Francés, etc) posee reglas precisas y claras sobre la forma como se combinan las palabras y su significado. En el caso de los computadores, las reglas son aún más explicitas, ya que el código está diseñado tanto para la lectura e interpretación del computador, como además la lectura del programador. Por ello si omitimos algunas reglas sintácticas, el compilador indicará que existe un error. Por ejemplo, si analizamos el programa en la versión de la derecha, el compilador (o traductor) fallará su ejecución indicando los siguientes errores: (1) La función printf está siendo empleada sin haber declarado la librería stdio.h que contiene su implementación. (2) falta un punto y coma al final de la instrucción printf Los primeros pasos en la programación en C corresponden a un aprendizaje de la sintaxis y semántica del lenguaje. Afortunadamente las reglas son muy pocas y las enumeramos a continuación (sólo a nivel general) 1) Todo bloque de funciones debe llevar una llave que abre el bloque “{“ y otra que lo cierra “}” 2) Toda instrucción debe finalizar con el símbolo ; 3) Cualquier función que utilicemos debe está previamente declarada, ya sea en una librería, o bien en nuestro propio programa a través de la declaración de prototipos. El programador debe indicar la(s) librería(s) que se debe(n) incluir en el programa. 4) Las estructuras de control nunca llevan el símbolo ; (a menos que sea parte del diseño del programador, poco usual). 5) Todos los tipos de datos deben estar declarados y definidos en su espacio en memoria. int main() { printf("hola mundo") } #include<stdio.h>int main() { printf("hola mundo"); } Versión incorrecta Versión sin errores PROGRAMACIÓN. 2016. MIGUEL CARRASCO 4 RESUMEN PROGRAMACIÓN 18-04-2016 VARIABLES V. La memoria del computador está compuesta por millones de bloques individuales los cuales permiten almacenar diferentes datos, desde símbolos, números o letras. Sin embargo, hasta el momento no sabemos cómo lograr acceder a dichos datos. Para almacenar datos en la memoria, sólo es necesario definir un tipo de datos apropiado y un nombre que identifique dicho bloque. Este concepto se denomina variable. Gracias a las variables el programa puede acceder a la memoria y emplear la información almacenada, pudiendo modificarla, borrarla o crear nuevas variables. Formalmente diremos que toda variable representa una estructura de datos. Todas las variables son almacenadas en un área específica de la memoria, para ello el sistema operativo es el encargado de otorgar el suficiente espacio necesario para crearla. Las variables pueden ser de tamaño fijo o variable durante la ejecución del programa. (1) Si su tamaño es fijo, ésta se mantiene constante y no cambia durante la vida del programa -entenderemos por vida del programa mientras éste se encuentre funcionando-. (2) Si su tamaño es variable, ésta puede ser modificada, es decir, puede emplear más o menos bloques de memoria de lo que originalmente se encontraba definida; concepto conocido como memoria dinámica. En particular, la estructura de datos es la forma en que se organiza la información. En relación a éstas últimas, existen diferentes estructuras de datos sobre las cuales podemos generar una variable, pero mientras tanto comenzaremos con la simple la cual consiste en una variable con un tipo de datos ya definido. El siguiente ejemplo muestra el proceso de asignación de dos variables enteras {a,b} a medida que transcurre el tiempo de ejecución del programa. Note cómo al definir las variables, el sistema operativo reserva espacio de memoria que luego es empleado en el proceso de asignación (=). A través del operador asignación, los valores que están almacenados en las variables pueden ser modificados durante la ejecución del programa. Como veremos más adelante, existen dos tipos de variables, conocidas como variables locales y variables globales. Además, existe un caso especial donde una variable no puede modificar su valor, y éste ocurre cuando la variable ha sido declarada como una constante (ej.: const int a). Variables en la memoria 06ep?g ep angnombre 1 #include <stdio.h> int main() { } #include <stdio.h> int main() { int a,b; } #include <stdio.h> int main() { int a,b; a= 4; } #include <stdio.h> int main() { int a; a= 4; b= a*a; printf("%d",b); } tiempo INICIO Bloques de memoria 0 1 2 3 4 5 2 3 4 Bloques de memoria 0 a:1 2 3 b:4 5 Bloques de memoria 0 a:1 2 3 b:4 5 4 Bloques de memoria 0 a:1 2 3 b:4 5 4 16 espacio reservado espacio reservado D ir e c c io n e s Proceso de asignación de valores en memoria impresión a pantalla FIN PROGRAMACIÓN. 2016. MIGUEL CARRASCO 5 RESUMEN PROGRAMACIÓN 18-04-2016 ESTRUCTURAS DE CONTROL: IF-ELSE VI. Las estructuras de control son el corazón de nuestros programas, con ellas podemos modificar el curso de ejecución y control de nuestro programa. Existen diferentes tipos de estructuras de control, pero todas ellas tienen en común el uso de condiciones lógicas para su uso. Las condiciones permiten evaluar una o más preguntas sobre el estado o valor de las variables. Por ejemplo, tmp>32, donde tmp es una variable, o bien nivel_agua<20. También podemos emplear condiciones anidadas a través de los operadores lógicos1 AND "&&"; o el operador OR "||". Por ejemplo (tmp>32 && nivel_agua<20) En este apartado nos centraremos brevemente en el uso de la estructura de control IF-ELSE. La estructura general se compone de la siguiente forma2: El IF posee un argumento donde se declara una condición lógica. Los valores posibles en su evaluación son TRUE o FALSE (1 o 0). Si la condición es verdadera, entonces el control del programa ingresará al bloque de instrucciones IF. En cambio si la condición es falsa, el control del programa ingresará al bloque de instrucciones ELSE. En ambos casos recuerde que dicho bloque está limitado por las llaves {...}. Observe que no existe una condición en el argumento del ELSE. Solo el IF tiene un único argumento donde se evalúa(n) la (o las) condicion(es) (dicho argumento va entre paréntesis). Cuando el control del programa escoge un camino, no puede regresar al bloque anterior. Por ello el programador debe definir correctamente el curso de acción que seguirá su programa cuando se encuentre en ejecución. Tomando en consideración la estructura anterior, podemos definir la misma estructura al interior de cada bloque (infinitas veces), por ejemplo: 1 Revise el material adicional del curso para ver más detalles sobre los operadores lógicos. 2 Suponga que el código está contenido en un programa principal. if ( <condición_1> ) { if ( <condición_2> ) { // entra solo si condición 1 y 2 es verdadera } else { //entra solo si condición 1 es verdadera y 2 es falsa } } else { // entra si la condición 1 es falsa. Nunca evalúa la condición 2 } if ( <condición(es)> ) { // instrucciones del bloque IF } else { // instrucciones del bloque ELSE } PROGRAMACIÓN. 2016. MIGUEL CARRASCO 6 RESUMEN PROGRAMACIÓN 18-04-2016 ESTRUCTURAS DE CONTROL: FOR VII. La estructura de control FOR nos permite ejecutar ciclos en forma controlada, y un determinado número de veces según el programador defina. El ciclo se realiza mientras la condición es verdadera. Es muy útil cuando se conoce de antemano si el problema emplea índices tanto para incrementar o decrementar (disminuir) valores cada vez que completa un ciclo. Un ciclo se realiza cuando el programa realiza las instrucciones contenidas en un bloque y regresa nuevamente al inicio del FOR. La estructura genérica del ciclo FOR posee tres secciones (argumentos) de la siguiente forma: La inicialización corresponde a la declaración de las variables con valores iniciales, por ejemplo, i=0, count=0; color=100. Puede ser declarada más de una inicialización. La(s) condición(es) lógicas permiten que en algún momento de ejecución el ciclo termine (si el ciclo no terminase, entonces el programa nunca finalizaría). Finalmente el modificador permite que la variable empleada en la inicialización actualice su valor cada vez que completa un ciclo, por ejemplo i++, i--, i+=2, entre otros. Al igual que la estructura de control IF-ELSE, podemos emplear los ciclos FOR en forma más compleja, y con una combinación de estructuras. Analice por un momento el funcionamiento del siguiente programa, y como en cada ciclo se evalúa una condición lógica. A medida que adquiere más experiencia con el lenguaje, el uso de ciclos y condiciones de evaluación dentro del bloque resulta más natural. En la mayoría de los programas que a futuro realice, dispondrá de condiciones y otras estructuras dentro del ciclo. for (<inicialización>; <condición(es)>; <modificador>) { //instrucciones del bloque } i=0 i=1 Una vez que termina la última instrucción, el control del programa incrementa el contador y luego evalúa la condición lógica. Si ésta es verdadera, entonces se ejecuta nuevamente el ciclo. Si la condición es falsa, el ciclo termina El programa inicializa una o más variables y evalúa una o más condiciones lógicas. Si la condición es verdadera, entonces ingresa al bloque del FOR. Si la condición es falsa, sale del ciclo #include<stdio.h> #include<cs50.h> int main(){ int i, a, cont=0;printf("Ingrese un numero entero: "); a= GetInt(); for(i=1; i<=a; i++){ if (a%i==0){ cont++; } } if (cont==2){ printf("El numero es primo\n"); } } PROGRAMACIÓN. 2016. MIGUEL CARRASCO 7 RESUMEN PROGRAMACIÓN 18-04-2016 ESTRUCTURAS DE CONTROL: WHILE VIII. El ciclo WHILE es una estructura de control que permite que se ejecute un ciclo mientras la condición evaluada es verdadera. Cuando la condición es falsa, el ciclo termina. A diferencia del ciclo FOR, el ciclo WHILE sólo posee un argumento donde se evalúa una condición lógica. A medida que usted adquiere más experiencia con el uso de los ciclos, podrá diferenciar el uso entre el ciclo FOR y el ciclo WHILE. Otra forma de ver el ciclo es a través de un diagrama de flujo. En el diagrama, el óvalo representa el inicio o fin del programa. El rectángulo una instrucción, y el rombo, una decisión. Cuando el programa alcanza la decisión, debe evaluar una condición lógica. De lo visto anteriormente, los resultados posibles son verdadero o falso (TRUE, FALSE). Solo cuando la condición es verdadera, el ciclo se ejecuta nuevamente, ingresando al bloque de instrucciones. Si la condición del ciclo nunca se hace falsa, entonces el programa quedará en un loop infinito. Es tarea del programador diseñar una condición que en algún punto del programa se haga falsa. Al igual que las estructuras vistas anteriormente, el ciclo WHILE puede combinarse con otras estructuras de control. Inclusive, puede realizar la misma función que un ciclo FOR. Revise en el siguiente ejemplo un programa que realiza el mismo objetivo, pero uno de ellos se realiza con ciclo WHILE y la otra con ciclo FOR. Analice si alguno tiene una ventaja respecto al otro. ¿En qué casos recomendaría usted emplear un ciclo WHILE y en cuales no? while ( <condicion(es)> ) { //instrucciones del bloque } !"#$%& '()%& #include<stdio.h> #include<cs50.h> int main(){ int i, a, cont=0; printf("Ingrese un numero: "); a= GetInt(); i=1; while(i<=a){ if (a%i==0){ cont++; } i++; } if (cont==2){ printf("El numero es primo\n"); } } #include<stdio.h> #include<cs50.h> int main(){ int i, a, cont=0; printf("Ingrese un numero entero: "); a= GetInt(); for(i=1; i<=a; i++){ if (a%i==0){ cont++; } } if (cont==2){ printf("El numero es primo\n"); } } Versión ciclo FOR Versión ciclo WHILE PROGRAMACIÓN. 2016. MIGUEL CARRASCO 8 RESUMEN PROGRAMACIÓN 18-04-2016 MANEJO DE STRINGS (ASCII) IX. Gracias a las funciones implementadas en la librería cs50.h y string.h, es posible la manipulación de texto en el computador. Entre las principales funcionalidades se encuentran el almacenamiento, la manipulación, la búsqueda. En este resumen revisaremos las tres primeras funcionalidades brevemente. Almacenamiento Para almacenar una cadena de caracteres (STRINGS) empleamos una funcionalidad provista en la librería cs50.h. El siguiente código muestra un ejemplo mínimo que permite ingresar una cadena de texto en una variable; al igual como lo haría al ingresar un número entero. Primero, declare una variable de tipo “string”. Segundo, utilice la función GetString() para asignar el texto leído a una variable. Tercero, utilice la variable declarada con la cadena leída para su manipulación Manipulación La manipulación requiere un conocimiento del número de caracteres que existen en la variable. Para ello empleamos la librería string.h, ya que contiene funciones permiten manipular cadenas de string (entre muchas otras funciones). Una de las que más utilizaremos es el largo de la cadena, a través de la función strlen. Este número es muy relevante ya que si deseamos manipular una cadena de caracteres, es necesario conocer la posición del último caracter (o símbolo). ¿Qué ocurriría si no conocemos el largo de la cadena?, podríamos ingresar a una posición de la cadena que esté en otra parte de la memoria, lo cual podría generar un "segmentation fault". Búsqueda Para realizar operaciones más complejas sobre la cadena, primero revisemos la forma en cómo se almacena la información en la variable. Cuando el computador almacena la cadena de texto en la variable S (del ejemplo anterior), cada caracter (o símbolo) es almacenado en una determinada casilla (caja). De esta forma podemos recorrer, contar, buscar información dentro de la cadena al comparar cada casilla con algún valor. Inclusive podemos cambiar los datos contenidas en ella por otros en nuevas posiciones. #include<stdio.h> #include<cs50.h> int main(){ string S; S= GetString(); printf("texto: %s\n",S); } #include<stdio.h> #include<cs50.h> #include<string.h> int main(){ int p; string S; S= GetString(); //largo de la cadena p=strlen(S); printf("%s tiene %i caracteres\n",S, p); } o l c m h a o o e s t a s ? \0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 16 PROGRAMACIÓN. 2016. MIGUEL CARRASCO 9 RESUMEN PROGRAMACIÓN 18-04-2016 Note en el ejemplo que ingresamos la cadena "hola como estas?". Observe que el espacio en blanco también se almacena en la cadena de texto. Internamente la cadena termina al alcanzar el símbolo '\0', el cual indica al computador que corresponde el final de una cadena. En el siguiente ejemplo utilizaremos esta propiedad al modificar los índices. Observe que podemos determinar una posición del arreglo. Por ejemplo, la posición S[i] determina el símbolo (caracter) en la posición i-ésima (línea 13). En el código se indica la posición S[p-1-i], analice a qué posición corresponde en el arreglo a medida que la variable i se incrementa. Note que fácilmente podemos recorrer el arreglo a través de un ciclo FOR, el cual avanza posición por posición en el arreglo. Dado que conocemos el largo (a través de la función strlen), la condición de término es recorrer el arreglo hasta terminar de revisar el último símbolo almacenado en la variable. En el siguiente programa vamos a modificar cada símbolo por otro. Recuerde que cada símbolo se almacena internamente con un valor numérico (con su correspondiente valor en la tabla ASCII) . Observe con detalle la condición (S[i]!=' ') en la línea 13. Esta condición compara si el símbolo almacenado en la posición i-ésima es distinta al símbolo ' '. Este último símbolo corresponde a un espacio en blanco, y solo podemos realizar esta comparación entre caracteres. Si la condición es verdadera, es decir, no estamos leyendo el espacio en blanco en una determinada posición, entonces, el programa incrementa en una unidad el número contenido en el texto en la i-ésima posición. La explicación de esta operación tiene relación en la forma como se almacenan los caracteres en el computador. Veamos un ejemplo de esta operación. El código ASCII codifica todo el alfabeto que está en su computador, incluidos símbolos y letras. En el caso del símbolo 'a', este posee el valor 97. Internamente de este modo se almacena la información en su computador (en realidad con códigos binarios). Si sumamos al número 97 una unidad (97+1), para efectos del computador, estamos utilizando el símbolo 'b' (línea 14) #include<stdio.h> #include<cs50.h> #include<string.h> int main(){ int p, cont=0; string S= GetString(); p=strlen(S); for(int i=0; i<p; i++){ if (S[p-1-i]==S[i]){ cont++; } } if (cont==p) printf("La palabra es palíndroma"); } #include<stdio.h> #include<cs50.h> #include<string.h> int main(){ int p; string S= GetString(); p=strlen(S); for(int i=0; i<p; i++){ if (S[i]!=' '){ S[i] = S[i]+1; } } printf("cifrado: %s", S); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 1. 2. 3.4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. a 97 ASCII code b 98 Símbolo PROGRAMACIÓN. 2016. MIGUEL CARRASCO 10 RESUMEN PROGRAMACIÓN 18-04-2016 ARREGLOS UNIDIMENSIONALES X. Hasta ahora siempre hemos almacenado un número por cada variable definida. Sin embargo, ¿es posible almacenar más información en una variable? La respuesta es afirmativa, y esto se realiza a través de arreglos unidimensionales (1D). Los arreglos son estructura de datos que permite almacenar muchos valores en una misma variable. Para crear un arreglo, primero debemos definir su tipo, segundo indicar el nombre de la variable, y tercero indicar el tamaño del arreglo. Este tipo de arreglo no puede ser modificado mientras el programa se ejecuta. Una gran ventaja de los arreglos es que en una sola variable podemos almacenar muchos valores, y posteriormente podemos procesarlos. Aplicaciones usuales para este tipo de información son ordenamiento, búsqueda, operaciones matemáticas, estadísticas, análisis de patrones, entre otros. Veamos a continuación dos ejemplos de programas que utilizan operaciones con arreglos. Código Izquierda: A medida que ingresamos los valores en el arreglo, acumulamos el valor almacenado en la variable acum. Note que antes de ingresar los valores, dicha variable posee el valor cero (línea 7). El resultado final del programa arroja la sumatoria de los valores leídos por teclado a través de la función GetInt(). Código derecha: A medida que se ingresan los valores, va comparando si el nuevo valor ingresado es mayor al máximo almacenado en la primera iteración. Esta comparación la realiza en la línea 13. Si dicha condición se cumple, se actualiza el valor almacenado en la variable max. #include<stdio.h> #include<cs50.h> int main(){ int M[10], acum; acum=0; for(int i=0; i<10; i++){ M[i] = GetInt(); acum = acum+M[i]; } printf("Suma: %i", acum); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. #include<stdio.h> #include<cs50.h> int main(){ int M[10], max; for(int i=0; i<10; i++){ M[i]= GetInt(); if(i==0) max= M[i]; else{ if(max < M[i]) max = M[i]; } } printf("Maximo es %i", max); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. #include<stdio.h> int main(){ int a,b,c,d,e; a=10; b=11; c=12; d=13; e=14; } #include<stdio.h> int main(){ int a[5]; a[0]=10; a[1]=11; a[2]=12; a[3]=13; a[4]=14; } Versión sin arreglos Versión con arreglos #include<stdio.h> int main(){ int a[5]={10,11,12,13,14}; } Versión con arreglos PROGRAMACIÓN. 2016. MIGUEL CARRASCO 11 RESUMEN PROGRAMACIÓN 18-04-2016 ARREGLOS MULTIDIMENSIONALES XI. Si ya has descifrado la complejidad de los arreglos unidimensional (una dimensión), este concepto será sumamente simple. Se trata de los arreglos multidimensionales (más de una dimensión). En este punto sólo revisaremos el caso de dos dimensiones, pero es fácilmente replicable a un mayor número. Los arreglos con dos dimensiones corresponden básicamente a una estructura del tipo matriz. En ella podemos almacenar números o letras, y podemos aplicar operaciones sobre ellas, tales como ordenamiento, búsqueda u otras operaciones más complejas. A diferencia del caso en una dimensión, tener más dimensiones implica que debemos considerar el uso de nuevos índices; uno por cada nueva dimensión que utilicemos. En el código del ejemplo empleamos los índices i, j (puede ser cualquier nombre). Note cómo se puede generar en la inicialización el ingreso de datos a la matriz. Usted puede ver en el código que existen dos ciclos FOR. Uno interno, y otro externo. El ciclo interno controla el movimiento entre las columnas, y se completa hasta llegar a la última columna. El ciclo externo cambia las filas, de esta forma cuando se fija el valor i=0 (en el ciclo externo), el valor de j cambia de 0, a 1, hasta llegar a j=3. En dicho punto el ciclo interno finaliza, y el control del programa regresa nuevamente al ciclo externo, actualizando el valor de la variable i, al valor i=1. Observe en detalle la sintaxis para recuperar la información dentro de la matriz (data[i][j]). A diferencia de los arreglos unidimensionales, simplemente agregamos la nueva dimensión al incorporar el término [j]. En el siguiente código (izquierda), observe como agregamos información a la matriz (arreglo multidimensional). Al igual que en el caso de los arreglos, podemos realizar operaciones simplemente usando los índices de las posiciones. Es importante recordar que nunca debemos emplear posiciones que no estén dentro del matriz. De otro modo el programa arrojaría un error en la ejecución (no en la compilación). #include<stdio.h> int main(){ int data[4][4]={ {4,2,3,7}, {1,5,9,2}, {8,3,6,4}, {3,1,5,0}}; int i,j; for (i=0; i<4; i++){ for (j=0; j<4; j++){ printf("%i ",data[i][j]); } printf("\n"); //salto de linea } } 4 ! 2! 3! 7 ! 1 ! 5! 9! 2 ! 8 ! 3! 6! 4 ! 3 ! 1! 5! 0 ! j : columnas! i=0 ! i=1 ! i=2 ! i=3 ! j=0 ! j=1 ! j=2 ! j=3 ! i : f i l a s ! #include<stdio.h> #include<stdlib.h> #include<time.h> int main(){ srand(time(NULL)); int data[4][4]; int i,j; for (i=0; i<4; i++){ for (j=0; j<4; j++){ data[i][j]=rand()%10; printf("%i ", data[i][j]); } printf("\n"); } } PROGRAMACIÓN. 2016. MIGUEL CARRASCO 12 RESUMEN PROGRAMACIÓN 18-04-2016 FUNCIONES XII. Las funciones son bloques de código que nos permite encapsular y dividir un problema en sub partes. Esto permite al programador concentrarse en diseñar una solución al problema general, empleando un conjunto de soluciones/construcciones parciales. Al diseñar una función, usted está implementando un bloque de código que puede ser invocado (llamado) por otras funciones, y al mismo tiempo, ésta puede invocar(llamar) a otras. Los pasos para construir una función son muy simples. Solo debemos tener en cuenta los siguientes requerimientos básicos que se cumplen en toda función. I. Debe tener un único nombre. II. Debe declarar un tipo de datos si retorna (o no) un valor. Si no retorna debe emplear el tipo void III. Debe indicar si recibe valores y de qué tipo IV. Debe especificar si retorna un valor (si no retorna, no empleamos return) [opcional] La estructura que toda función tiene es de la siguiente forma: (1) Posee un tipo de dato. El tipo de dato de la función tiene relación con el valor que se retorna luego de ser invocada. (2) El nombre de la función especifíca cómo será invocada, (3) el argumento tiene dos partes: tipo de datos y nombre de la variable. Cuando se ejecuta una función, los datos se “transfieren” o copian o referencian a dichas variables. Si tenemos más de una variable, esta debe ir separadas por comas simples, (4) Para que una función pueda retornar (devolver) su resultado a quien la invocó (llamó) debemos emplear la instrucción return. Dentro de ella se puede especificar sólo una variable de retorno. Durante la ejecución de una función existen dos pasos fundamentales, los cuales tienen relación a la forma como se “transfiere” la información al código de la función, y posteriormente como esta la “recibe”; proceso conocido como paso de parámetros. Existen dos formas para “transferir” datos a la función. (1) paso por valor, y (2) paso por referencia. En el primer caso, los valores “transferidos” son copiados a las variables que los reciben. La función puede emplear dichos valores como en una copia local a la función. En el segundo caso, sólo se transfiere una dirección de memoria, y las variables que recibenlos datos, solo pueden almacenar una dirección. También podemos emplear una combinación de ambos tipos. En el código de ejemplo, inventamos una función que se llama abc. En la línea 11 del código, la función es invocada, y el valor contenido en la variable “a” es transferido a la variable “o”. De esta forma la función #include<stdio.h> int abc(int); //prototipo int main(){ int a=10; int b; b= abc(a); printf("%i",b); } int abc(int o){ return(o+2); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. int duplicar(int a) { int b; b= a*2; return (b); } tipo de datos de salida nombre de la función parámetro de entrada Bloque de instrucciones variable local instrucción de retorno variable de salida Encabezado PROGRAMACIÓN. 2016. MIGUEL CARRASCO 13 RESUMEN PROGRAMACIÓN 18-04-2016 “recibe” el valor 10 y será almacenado en la variable “o”. Esta función luego realiza la suma “o+2”, y dicho valor es “retornado” al llamado de la función. De otra forma, la expresión “abc(a)” se transforma en el valor 12. Posteriormente el valor 12 es asignado a la variable b. En este punto del programa podemos realizar cualquier operación con dicho resultado. El siguiente diagrama presenta un ejemplo completo de creación de variables y el proceso de paso de datos a una función. Observe que empleamos la función scanf() ya que permite ingresar los valores en una forma mucho más específica que la versión GetInt() de la librería <cs50.h>. Puede encontrar más información sobre su uso en el material del curso. Biblioteca main Inicio variables Ingreso de datos LLamada a función Impresión resultado Paso por valor #include <stdio.h> Inicio variables Operación Retorno función return(b); } INICIO Ejemplo de ejecución secuencial Bloque de memoria 3 3 6 3 3 3 3 3 36 3 36 Reserva de bloques Llamado a función Impresión resultado Copia del valor Reserva del bloque Asignación Asignación Retorno de resultado 6 3 Vista de la memoria desde el programa principal Vista de la memoria desde la función invocada Variable Valores no accesables tiempo FIN 1 3 4 5 6 int main() { int a,b; printf("numero: "); scanf("%d", &a); b = duplicar(a); printf("%d", b); } int duplicar(int a) { int b; b= a*2; 2 PROGRAMACIÓN. 2016. MIGUEL CARRASCO 14 RESUMEN PROGRAMACIÓN 18-04-2016 ÁMBITO DE LAS VARIABLES XIII. Hasta ahora hemos empleado variables en distintas partes del programa, tanto en la función MAIN, como en nuestras funciones. Sin embargo, las variables posee dos formas que pueden ser declaradas: variables globales y variables locales. En forma global, implica que dicha variable puede ser empleada en cualquier función del programa, y cualquier cambio que realicemos a dicha variable afecta a todo el programa. En forma local, significa que la variable que declaremos sólo será conocida por dicha función, y los cambios que realicemos sobre ella durante la ejecución del programa, sólo afectarán a la función y no al resto del programa. Es muy relevante comprender estos dos modos, ya que nos permitirá comunicar en forma eficiente la información en la ejecución del programa. Por regla general, no se recomienda emplear variables globales ya que la lógica del programa se pierde. Solo en casos específicos serán empleadas A continuación detallamos brevemente los principales pasos en la ejecución del programa: ! Línea 3. declaramos una variable global (int C) con valor inicial 3. Esta variable puede ser empleada (y modificada) en cualquier parte de nuestro programa. ! Línea 8. declaramos una variable local (int A) en la función main. Solo dicha función tiene acceso a su valor. ! Línea 11. ejecutamos la función cambiar . Observe que pasamos el contenido de la variable A (paso por valor). ! Línea 17. La función crea una variable A y recibe el valor de quien la invocó, es decir recibe el valor 4. Cuando la función recibe el valor, las variables A, y B son locales, y cualquier cambio en ella solo afecta el ámbito de ejecución de dicha función. ! Línea 24. Modificamos el valor de la variable global C. Observe que C no está declarada en la función ya que es global. ! Línea 27. La función retorna el número entero que está en la variable B. ! Línea 11. La función cambiar() retorna su valor y éste es asignado a la variable local A (local a la función main). variables globales programa! alcance el programa variables locales main()! variables locales mi_funcion() ! alcance local #include<stdio.h> int C=3; //variable global int cambiar (int); //prototipo int main(){ //variable A es local a main int A; A=4; A= cambiar(A); printf("Local en main: %i\n",A); printf("global en main: %i\n",C); } int cambiar(int A){ //variables A y B son locales int B; A = A+1; B = 7; C = A-2; printf("Local en funcion: %i\n", A); printf("global en funcion: %i\n",C); return(B); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. PROGRAMACIÓN. 2016. MIGUEL CARRASCO 15 RESUMEN PROGRAMACIÓN 18-04-2016 ORDENAMIENTO XIV. El ordenamiento de números es una de las operaciones algorítmicas más útiles, ya que muchas operaciones matemáticas, léxicas, y búsqueda parten de la base que existe un conjunto ordenado de números (o palabras). En la literatura se han descrito un extenso número de algoritmos de ordenamiento, tales como bubble sort, selection sort, insertion sort, merge sort, entre otros. Cada uno está descrito según un orden de complejidad (el cual tiene relación con la cantidad de instrucciones que requiere un algoritmo para realizar un conjunto de instrucciones). En el caso de los algoritmos bubble sort, insertion sort y selection sort, todos posee una complejidad de O(n2). Por el contrario, un algoritmo eficiente para el ordenamiento es Merge Sort, cuyo orden es O(nlog2n). A modo de completitud, revisaremos brevemente el algoritmo bubble sort. Suponga que dispone de un arreglo con 5 valores en forma desordenada (aleatoria) ¿Cómo ordenarlos de mayor a menor? En el código de ejemplo tenemos un ciclo externo y otro interno FOR. El ciclo interno regula la variable j y el ciclo externo la variable i. Revise detalladamente todo el proceso completo de ordenamiento hasta alcanzar la quinta iteración. Note el uso del swap como técnica de intercambio de valores. 4! 2! 3! 7! 0 ! 2! 4 ! 3! 7! 0! swap! j=0 ! j=1 ! j=1 ! j=2 ! 2! 3 ! 4! 7! 0! j=3 ! 2! 3 ! 4! 7! 0! j=4 ! 2! 3 ! 4! 0! 7! swap ! Fin primera iteración ! j=3 ! j=2 ! swap! 2 ! 3 ! 4 ! 0! 7 ! 2 ! 3 ! 4 ! 0 ! 7 ! j=0 ! j=1 ! j=1 ! j=2 ! 2 ! 3 ! 4 ! 0 ! 7 ! j=3 ! 2 ! 3 ! 0 ! 4 ! 7 ! j=4 ! 2 ! 3 ! 0 ! 4 ! 7 ! Fin segunda! iteración ! j=3 ! j=2 ! swap ! 2 ! 3 ! 0 ! 4 ! 7 ! 2 ! 3 ! 0 ! 4 ! 7 ! j=0 ! j=1 ! j=1 ! j=2 ! 2 ! 0 ! 3 ! 4 ! 7 ! j=3 ! 2 ! 0 ! 3 ! 4 ! 7 ! j=4 ! 2 ! 0 ! 3 ! 4 ! 7 ! Fin tercera ! iteración ! j=3 ! j=2 ! swap ! 2! 0 ! 3! 4 ! 7! 0! 2 ! 3! 4! 7! j=0 ! j=1 ! j=1 ! j=2 ! 0! 2 ! 3! 4! 7! j=3 ! 0! 2 ! 3! 4! 7! j=4 ! 0! 2 ! 3! 4! 7! Fin cuarta ! iteración ! j=3 ! j=2 ! swap! 0! 2! 3! 4! 7 ! 0! 2! 3! 4! 7! j=0 ! j=1 ! j=1 ! j=2 ! 0! 2! 3! 4! 7! j=3 ! 0! 2! 3! 4! 7! j=4 ! 0! 2! 3! 4! 7! Fin quinta ! iteración! j=3 ! j=2 ! #include<stdio.h> int main(){ int i,j,A[5]={4,2,3,7,0}, tmp; for (i=0; i<5; i++){ for(j=0; j<4; j++){ if (A[j]>A[j+1]){ tmp=A[j]; A[j]=A[j+1]; A[j+1]=tmp; } } } printf("\n arreglo ordenado: "); for( i=0; i<5; i++) printf("%i ",A[i]); } PROGRAMACIÓN. 2016. MIGUEL CARRASCO 16 RESUMEN PROGRAMACIÓN 18-04-2016 BÚSQUEDA XV. La búsqueda de información engrandes cantidades de datos se ha transformado en un problema creciente debido a que cada día más información se genera en Internet. Suponga que Usted desea buscar el número 89 en una lista ordenada, ¿cuál sería la estrategia que ocuparía? ¿tiene alguna utilidad que la lista se encuentre ordenada? Como usted no sabe a priori si el número 89 se encuentra en la lista, en la búsqueda lineal debe comenzar desde el comienzo de la lista y comparar si el número 89 se encuentra en la caja 1 y así sucesivamente hasta encontrarlo. Este caso realizamos ocho comparaciones antes de encontrar el número buscado (ver figura). Sin embargo, si usted sabe que la lista está ordenada, puede emplear esta información para realizar una búsqueda mucho más eficiente. Este procedimiento se conoce como búsqueda binaria y consiste en dividir la lista en dos y revisar en cuál de las dos mitades se encuentra el número. Esto lo hace preguntando ¿mi número es mayor (o menor)al valor que está en la mitad de la lista?. En el ejemplo vemos que este proceso sólo requiere de tres pasos, es decir, si la lista tiene 8 valores, podemos fácilmente verificar que !"#!8=3. ya que 2! = 8. Ahora podemos generalizar el problema. Suponga que debe buscar un número entre mil millones de valores (1.000.000.000). Suponiendo que el número buscado está en la última posición y que cada comparación requiere 1 segundo, le tomaría 31 años y 7 meses en encontrar el valor (en el peor caso). En cambio si aplicara la estrategia de búsqueda binaria a una lista ordenada, sólo le tomaría 30 segundos ¿cuál de las opciones emplearía? ¿Por qué 30 segundos? Utilicemos la misma lógica del problema anterior: !"#!1.000.000.000! ≈ 30 Como puede ver, emplear una estrategia de búsqueda eficiente tiene una gran relevancia al momento de diseñar un algoritmo. La búsqueda binaria debe su nombre a que sólo tiene dos caminos a elegir. Existen otras técnicas de búsqueda eficiente, las cuales escapan de este resumen. !"# $"# %"# 1! 3 ! 34! 36! 45! 56! 67! 89! búsqueda lineal! búsqueda binaria! 1! 3 ! 34! 36! 45! 56! 67! 89! %# $# !# &# '# (# )# *# 45! 56! 67! 89! 67! 89! PROGRAMACIÓN. 2016. MIGUEL CARRASCO 17 RESUMEN PROGRAMACIÓN 18-04-2016 RECURSIVIDAD XVI. La recursividad es un concepto elegante y complejo que requiere un conocimiento profundo sobre cómo operan los datos en el computador. Sin embargo, es sumamente simple de aplicar en matemáticas. Suponga que Usted tiene una lista de números y desea sumarlos. Una forma recursiva de expresar esta formulación puede ser desarrollada con las siguientes dos reglas Como ejemplo, vamos a suponer que nuestra lista posee los siguientes elementos {1, 2, 3, 4} sum({1, 2, 3, 4}) = 1 + sum({2, 3, 4}) //aplicamos segunda regla = 1 + 2 + sum({3, 4}) //aplicamos segunda regla = 1 + 2 + 3 + sum({4}) //aplicamos segunda regla = 1 + 2 + 3 + 4 + sum({}) //aplicamos primera regla = 1 + 2 + 3 + 4 + 0 = 1 + 2 + 3 + 4 = 1 + 2 + 7 = 1 + 9 = 10 Como puede notar, una vez que aplicamos la primera regla, comienzan a ser empleadas las operaciones de suma entre los últimos elementos. Si aplicáramos este método a un lenguaje de programación, fácilmente podríamos implementar la sumatoria de n elementos sin emplear ningún ciclo. Veamos a continuación una implementación de dicho proceso. Como puede observar, el proceso recursivo comienza con el primer llamado a la función "sumatoria(A,4)" (línea 11). Cuando el control del programa ingresa a dicha función, compara el valor de la variable p en la condición del if(p==0). Si p no es cero, entonces realiza el bloque else. En el return del bloque else notamos que se vuelve a invocar la función sumatoria, pero esta vez con el argumento “p-1”. El proceso continúa hasta llegar al último valor de p que ocurre cuando p es cero (línea 18). El proceso comienza a retornar en cascada los resultados de la función que lo invocó, alcanzando finalmente el primer llamado a la función, es decir, "sumatoria(A,4)". primera regla: suma (lista vacía de números) = 0 segunda regla: suma (lista de n números) = primer elemento lista + suma (lista n-1 números restantes) #include<stdio.h> int sumatoria(int A[], int largo); int main(){ int A[5]={4,2,3,7,0}; //llamado a la funcion int suma= sumatoria(A, 4); printf("sumatoria es: %d\n", suma); } //funcion recursiva int sumatoria(int A[], int p){ if (p==0){ return(A[0]); } else{ return(A[p]+sumatoria(A,p-1)); } } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. sumatoria (A, 4)! 4 ! 2! 3! 7! 0! 4!p= ! A= ! !" sumatoria (A, 3) !0! 4! 2! 3! 7! 0! 3 !p= ! A= ! !" sumatoria (A, 2) !7! 4! 2! 3! 7! 0! 2 !p= ! A= ! !" sumatoria (A, 1) !3! 4! 2! 3! 7! 0! 1 !p= ! A= ! !"sumatoria (A, 0) !2! 4! 2! 3! 7! 0! 0 !p= ! A= ! sumatoria (A, 0) ! 4! PROGRAMACIÓN. 2016. MIGUEL CARRASCO 18 RESUMEN PROGRAMACIÓN 18-04-2016 PUNTEROS: MEMORIA DINÁMICA XVII. Los punteros son simplemente variables que almacenan direcciones de memoria. Al igual como declaramos una variable en C (ej. int data), los punteros también son declarados con un tipo de datos. La mayor ventaja al trabajar con punteros es la forma en que podemos manipular los datos. Para ello el lenguaje cuenta con una nomenclatura específica, que le permite al programador diferenciar entre una variable que almacena un valor (número, texto, caracter), o bien una dirección de memoria. En este punto, debe preguntarse, ¿qué es una dirección de memoria? El computador dispone de dos tipos de memoria, (1) la memoria permanente, los cuales son discos duros, dvd, cd, disquetes, etc. y (2) la memoria volátil, siendo la más conocida la memoria RAM. Cuando Usted enciende su computador, grandes cantidades de datos (de sus programas y del sistema operativo) se almacenan en esta memoria, principalmente por que puede ser hasta 10.000 veces más rápida que la memoria permanente. Pero para que el computador administre los datos, el sistema debe asignar una dirección específica, la cual indica al programa donde guardar la información. La cantidad de direcciones disponibles depende tanto de la memoria RAM instalada, como el tipo de computador que utilice. En relación a la nomenclatura, existen cuatro puntos fundamentales para el manejo de punteros. I. Creación. Para indicar que una variable es de tipo puntero, simplemente debemos anteponer el símbolo "*" (asterisco) frente a la variable. En el código de ejemplo (línea 6), la variable y es de tipo puntero a entero. II. Asignación de memoria. Cuando utilice punteros, debe asignar un espacio en memoria (o al menos apuntar a uno), ya que de lo contrario, el programa dejará de funcionar. Para asignar un espacio de memoria, empleamos el símbolo "&". Cuando se utiliza este símbolo frente a una variable, obtenemos su dirección de memoria. En este caso, como la variable "y" es de tipo puntero, y además &x retorna la dirección de memoria donde se encuentra dicha variable, entonces, podemos realizar la asignación y=&x (línea 7). III. Asignación por indirección. Si deseamos modificar el contenido de lo apuntado, podemos emplear la expresión *<variable_puntero>= Con ello estaremos modificando el contenido de lo apuntado (ver ejemplo, línea 8). IV. Acceso a contenido. Finalmente, si deseamos obtener el contenido de lo apuntado, anteponemos el símbolo * frente a la variable puntero al lado derecho de la igualdad (línea 9). Te recomendamos que revises con detalle las cuatro reglas anteriores, las cuales se detallan en la figura. En la siguiente sección se explicará por qué es necesario su uso, y donde se utiliza regularmente. #include<stdio.h>int main(){ int x=1; int *y; y=&x; *y=3; x=*y+1; printf("%i",x); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. x ! 1 ! 0x12F ! línea 5! *y ! 0x6C ! línea 6 ! y=&x ! 0x12F ! 0x6C ! línea 7! *y=3 ! 0x12F ! 0x6C! línea 8! x ! 3! 0x12F ! x= *y+1 ! 0x12F ! 0x6C! línea 9! x ! 4! 0x12F ! PROGRAMACIÓN. 2016. MIGUEL CARRASCO 19 RESUMEN PROGRAMACIÓN 18-04-2016 PASO POR VALOR Y POR REFERENCIA XVIII. Como hemos visto anteriormente los punteros son un tipo especial de variables que permite almacenar una dirección de memoria. Anteriormente, cuando utilizamos las funciones empleamos el concepto de paso por valor. Esto quiere decir que los valores “transferidos” a la función son copiados dentro de ella. En cambio, el paso por referencia, sólo se “transfiere” una referencia a ellos, o bien una dirección de memoria (no se copian los valores). En el primer ejemplo, analicemos qué ocurre a medida que se ejecuta un programa con el paso por valor de una variable a una función. Es importante que el lector observe con calma cómo se almacenan los valores en la memoria del computador. ¿Qué ocurre cuando un valor se copia dentro de la función, y ¿cómo se devuelve (retorna) el resultado a quien lo invocó? Esto se encuentra detallado en el siguiente diagrama: 1 tiempo INICIO #include <stdio.h> int main() { int a,b; } Bloques de memoria 0 a:1 2 3 b:4 5 2 3 5 FIN 6 7 8 #include <stdio.h> int main() { int a,b; a= 4; } Bloques de memoria 0 a:1 2 3 b:4 5 4 int duplicar(int a) { } Bloques de memoria 0 a:1 a:2 3 b:4 5 4 4 int duplicar(int a) { int b; } Bloques de memoria 0 a:1 a:2 3 b:4 5 4 4 #include <stdio.h> int main() { } Bloques de memoria 0 1 2 3 4 5 D ir e c c io n e s int duplicar(int a) { int b; b = a*a; } Bloques de memoria 0 a:1 a:2 b:3 b:4 5 4 4 16 9 bloque disponible bloque reservado bloque no accesible 0 a:1 a:2 b:3 b:4 5 int duplicar(int a) { int b; b = a*a return(b); } Bloques de memoria 4 4 16 Retorno de resultado Bloques de memoria 0 a:1 2 3 b:4 5 4 4 16 #include <stdio.h> int main() { int a,b; a= 4; b= duplicar(a); } 16 Asignación del valor bloques con valores no accesables #include <stdio.h> int main() { int a,b; a= 4; b= duplicar(a); } Bloques de memoria 0 a:1 2 3 b:4 5 4 paso por valor llamado a la función 4 nomenclatura PROGRAMACIÓN. 2016. MIGUEL CARRASCO 20 RESUMEN PROGRAMACIÓN 18-04-2016 En el paso por referencia, las variables son referenciadas dentro de la función, es decir, sólo “reciben” direcciones de memoria. A través de los conceptos de asignación por indirección y acceso a contenido, los valores de los punteros pueden ser modificados. El objetivo de este ejemplo es intercambiar los valores dos valores en las variables enteras a y b. En clases hemos analizado que para hacer este proceso dentro de una función, requerimos una tercera variable (variable auxiliar). Sin embargo, la única forma para hacerlo dentro de una función es a través de punteros. Como vemos en la ilustración, empleamos el acceso al contenido de lo apuntado, como al contenido por indirección. El concepto es sumamente simple, pero requiere un gran conocimiento sobre el funcionamiento de la memoria del computador. Probablemente Usted debe preguntarse ¿para qué hacer esto?, si recuerda los algoritmos de ordenamiento, estos utilizan arreglos en memoria para almacenar la información. Por ello la única forma para modificar dichos valores es a través del uso de las direcciones de memoria. Esto otorga una mayor libertad al programador, pero al mismo tiempo, una mayor complejidad. 1 tiempo INICIO #include <stdio.h> int main() { int a,b; } Bloques de memoria 0 a:1 2 3 b:4 5 2 3 5 FIN 6 7 #include <stdio.h> int main() { int a,b; a= 5; b= 8; } Bloques de memoria 0 a:1 2 3 b:4 5 5 8 void swap(int *y, int *p) { } Bloques de memoria 0 a:1 y:2 p:3 b:4 5 5 0x1 0x4 8 void swap(int *y, int *p) { } void swap(int *y, int *p) { } void swap(int *y, int *p) { } int tmp=*y; int tmp=*y; *y = *p; int tmp=*y; *y = *p; *p = tmp; #include <stdio.h> int main() { } Bloques de memoria 0 1 2 3 4 5 D ir e c c io n e s Bloques de memoria 0 a:1 y:2 p:3 b:4 tmp:5 5 5 8 0X1 0x4 Bloques de memoria 0 a:1 y:2 p:3 b:4 tmp:5 5 8 8 0X1 0x4 Bloques de memoria 0 a:1 y:2 p:3 b:4 tmp:5 5 8 5 0X1 0x4 8 bloque disponible bloque reservado bloque no accesible bloque referenciado Indirección de variable Indirección de variable Acceso a contenido acceso a bloque por referencia #include <stdio.h> int main() { int a,b; a= 5; b= 8; swap(&a,&b); } Bloques de memoria 0 a:1 2 3 b:4 5 5 8 #include <stdio.h> int main() { int a,b; a= 5; b= 8; swap(&a,&b); } Bloques de memoria 0 a:1 2 3 b:4 5 8 0x1 0x4 5 paso por referencia llamado a la función 4 nomenclatura 9 PROGRAMACIÓN. 2016. MIGUEL CARRASCO 21 RESUMEN PROGRAMACIÓN 18-04-2016 PARTE II Una vez estudiado los principales conceptos relacionados con el uso y manejo de datos, arreglos, matrices, funciones, ordenamiento, búsqueda en el lenguaje C, comenzaremos el segundo módulo estudiando conceptos más avanzados del lenguaje C, tales como manejo de memoria dinámica para arreglos, estructuras, estructuras dinámicas, pilas, colas, árboles, funciones de inserción, eliminación, búsqueda, etc. Todo el contenido de este capítulo requiere un conocimiento profundo y completo de los contenidos vistos anteriormente. Por ello te recomendamos que si existe algún punto de la materia que está poco claro, primero resuelve dicha dificultad antes de proseguir con el estudio de este material. DISCLAIMER Toda la materia vista hasta este punto corresponde al contenido mínimo que será evaluado en la primera prueba. Recuerda que este material es un resumen, y no constituye en si todo el contenido de la materia vista en clases, así como en las tareas. Te recomendamos trabajar en grupos y resolver las guías de ejercicios desarrolladas en clases. pointer! data ! pointer ! data ! pointer ! data ! NULL! COPYRIGHT SCOOT ADAMS. 1995 © United Feature Syndicate, Inc. (NYC) PROGRAMACIÓN. 2016. MIGUEL CARRASCO 22 RESUMEN PROGRAMACIÓN 18-04-2016 ARITMÉTICA DE PUNTEROS XIX. Como estudiamos en la sección de arreglos unidimensionales, si deseamos recorrer un arreglo, simplemente indicamos su posición y modificamos el valor del índice (por ejemplo: arr[i], donde i es una variable que comienza en la posición 0 y termina la posición 3). Note en la figura que cada vez que avanzamos en el arreglo, recorremos cuatro direcciones de memoria3. Esto se debe a que en realidad requerimos 4 bytes por cada valor que almacenamos en una determinada posición; ya que nuestro arreglo es de tipo entero (int)4. Internamente, el computador ejecuta la siguiente operación aritmética de puntero: La expresión anterior implica que la variable que declaramos como arreglo, en realidad es un puntero a su primera posición, y para recorrerlo, solo es necesario adicionar una nueva posición. Probablemente cause confusión la expresión arr+1 lo cual puede representar 100+1 que es 101, y no 104 (con i==1). Internamente, el computador reconoce el tipo de datos que utilizamos, y efectúa dicha aritmética, a pesar de la inconsistencia anterior. Esto ocurre cuando indicamos el arreglo al momento de crear la variable. A continuación explicamos brevemente el código del ejemplo: Línea 6. Creamos un puntero de tipo entero. Línea 7. Empleamos la instrucción malloc() para reservar un espacioen memoria. El argumento debe indicar el número de bytes que deseamos reservar en memoria. Para indicar con precisión el tamaño en bytes que ocupa un entero, empleamos la instrucción sizeof() la cual determina el número de bytes de cualquier tipo de datos (inclusive uno definido por nosotros). Como en este caso deseamos reservar espacio para 10 valores (un arreglo de largo 10), entonces simplemente multiplicamos 10 por el tamaño en bytes del tipo entero. Línea 10. Tal como indicamos anteriormente, cuando empleamos arreglos, internamente empleamos una aritmética de punteros. En este caso, asignamos un número entero a cada posición del arreglo. Compile y luego ejecute el código anterior para verificar los valores de salida. 3 el número de bytes empleados en cada posición del arreglo depende según el tipo de datos, ver pág.1. 4 más información en https://goo.gl/7vEpfy arr[i] ! *(arr+i) " arr[0] ! 100! arr[1] ! arr[2] ! arr[3] ! arr! 104! 108! 112! arr+1! arr+2! arr+3! dirección!arreglo ! #include<stdio.h> #include<stdlib.h> int main(){ int *arr; arr =(int*)malloc(10*sizeof(int)); for (int i=0; i<10; i++){ *(arr+i)=i; //forma como arreglo for (int i=0; i<10; i++){ printf("%i\n", arr[i]); } } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. PROGRAMACIÓN. 2016. MIGUEL CARRASCO 23 RESUMEN PROGRAMACIÓN 18-04-2016 ESTRUCTURAS DE DATOS XX. En los ejemplos anteriores hemos creado y manipulado arreglos y matrices los cuales almacenan información en cada posición de dicha estructura. Por lo general, esta información ha estado compuesta por números; pudiendo ser cualquier tipo de datos. Ahora bien, suponga que usted desea almacenar información compuesta por valores combinados, tales como número de teléfono, nombres, apellidos, etc. de tal forma de crear una agenda. El lenguaje C permite la creación de este tipo de variables a través de estructuras struct. La estructura struct permite agrupar variables de distinto tipo en una única variable, la cual puede almacenar toda la información. Como puede observar, es extremadamente útil cuando en nuestro problema deseamos almacenar información. Esto lo podemos hacer empleando esta agrupación. Una vez que la estructura ha sido declarada (como en el ejemplo) es necesario indicar una variable para su utilización. Esto se realiza empleando la siguiente nomenclatura: Usted puede definir todas las variables que desee de la misma estructura (en este caso solo empleamos una variable: mi_agenda). Para acceder a los campos de cada variable, solo empleamos el nombre de la variable, el operador punto, y el nombre de la variable declarado dentro de la estructura del siguiente modo. En el ejemplo del programa, definimos una estructura antes de la función MAIN, y luego definimos una variable (mi_agenda) del tipo estructura (línea 13) en la función MAIN. Usted puede declarar la variable dentro o fuera de una función según dependa el ámbito (scope) de uso. En la línea 16 hasta la 18 llenamos cada uno de los campos definidos en la estructura a través de una asignación, del mismo modo como empleamos la asignación de una variable. En realidad la estructura es una variable la cual tiene “campos” los cuales son accesados por el operador punto aplicado sobre la variable que almacena la estructura (líneas 16-18, y 20-22). Otra propiedad de las estructuras es que la información de una estructura puede ser “transferida” a otra con todos sus campos empleando simplemente una asignación (ver código a la izquierda). struct ! nombre_estructra ! }! ;! {! tipo_1 !variable_1;! tipo_2 !variable_2;! ! tipo_n !variable_n;! nombre de la estructura Variables de la estructura #include<stdio.h> #include<cs50.h> struct agenda{ string nombre; string correo; int id; }; //<<< note el punto y coma!! int main(){ struct agenda mi_agenda; //agregamos información a la estructura mi_agenda.nombre = GetString(); mi_agenda.correo = GetString(); mi_agenda.id = 1; printf("Id: \t%i\n", mi_agenda.id); printf("Nombre: \t%s\n", mi_agenda.nombre); printf("Correo: \t%s\n", mi_agenda.correo); } struct! nombre_estructra ! nombre_variable! nombre_variable ! variable!!" 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. #include<stdio.h> #include<cs50.h> struct agenda{ string nombre; }; int main(){ struct agenda A, B; A.nombre= GetString(); //copiamos A en B B = A; } PROGRAMACIÓN. 2016. MIGUEL CARRASCO 24 RESUMEN PROGRAMACIÓN 18-04-2016 ARREGLO DE ESTRUCTURAS: LISTA DENSA XXI. Antes de comenzar a revisar las estructuras dinámicas, introduciremos brevemente el concepto de redefinición de tipos con el comando typedef. Typedef permite definir distintos tipos de datos complejos a otros más simples empleando un alias. Typedef no crea tipos de datos, sólo define un nuevo identificador. En el ejemplo, si deseamos declarar una variable del tipo estructura, debemos escribir struct nombre_estructura variable. Lo anterior puede ser reducido al emplear typedef, ya que al declarar un alias, creamos un nuevo identificador el cual agrupa la declaración anterior; en este caso como tagenda. Una de las ventajas de las estructuras, es que podemos crear un arreglos de estructuras, o bien emplear punteros a ellas. Revisaremos a continuación la creación de arreglos de estructuras, al igual que cuando creamos arreglos unidimensionales. Líneas 5-8. Creamos un estructura agenda, sin embargo empleamos una redifinición de tipos. Lo anterior se realiza anteponiendo el comando typedef antes de la estructura, y luego indicando un nuevo alias para dicha redefinición. Observe que al término de la estructura hay un punto- y-coma. Línea 11. Una vez que hemos creado el identificador tagenda (línea 8) creamos una variable arreglo fichas con la misma nomenclatura de un arreglo ficha[3]. Lo anterior reserva espacio en memoria para tres estructuras del tipo tagenda. El espacio definido para esto considera el espacio del número entero y el tipo string. Línea 15-16. A diferencia del formato de ingreso visto anteriormente, ahora vamos a ingresar datos en un arreglo de estructuras. Por ello debemos indicar la posición donde ingresar los datos. Lo anterior se realiza del mismo modo que operábamos los índices de arreglos, pero ahora indicando el campo que debemos completar. Por ejemplo ficha[i].id = i; significa que ingresamos un número entero en la ficha i-ésima en el campo id. Operamos del mismo modo para emplear el campo nombre. Línea 21. Del mismo modo como ingresamos los datos, podemos emplear la misma estructura para acceder al contenido en la estructura. Esto lo hacemos mostrando el contenido de la i-ésima ficha como ficha[i].id. #include<stdio.h> #include<cs50.h> //estructura con redifinición de tipos typedef struct agenda{ int id; string nombre; }tagenda; //no olvide cerrar con punto-y-coma int main(){ tagenda ficha[5]; //empleamos el alias tagenda //ingreso de datos for (int i=0; i<5; i++){ ficha[i].id = i; ficha[i].nombre = GetString(); } //despliegue de datos for (int i=0; i<5; i++){ printf("%i\t%s\n", ficha[i].id, ficha[i].nombre); } } typedef ! tipo_de_datos_actual! nuevo tipo de datos! struct ! nombre_estructra! typedef ! tagenda !struct ! agenda ! ; ! ; ! Ejemplo: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. PROGRAMACIÓN. 2016. MIGUEL CARRASCO 25 RESUMEN PROGRAMACIÓN 18-04-2016 RETORNO DE FUNCIONESCON TIPOS DE DATOS XXII. Anteriormente analizamos el ingreso de datos directamente desde el programa principal a través de una estructura. Como hemos estudiado en este resumen, siempre debemos diseñar programas que utilicen funciones, de esta forma será más simple el proceso de revisión, simplificación y resolución de cualquier problema bajo el paradigma “divide para conquistar”. Sin embargo, ¿cómo podemos retornar un tipo de dato más complejo de los ya conocidos int, float, char, void? Para realizar un retorno de una estructura de datos, nuestra función simplemente debe emplear el mismo tipo de datos definido para la estructura. En el programa sin función, los datos son ingresados a la variable ficha en forma directa dentro de la función main(). ¿Cómo podemos realizar este proceso ingresando los datos a una función, y que ella retorne una estructura del mismo tipo que la ficha? En este caso vamos a crear una función, cuyos parámetros de entrada sean un entero y un string. Una vez que los datos son “transferidos” a la función, vamos a retornar una estructura de datos. Para ello el tipo de datos de la función debe corresponde con el tipo de datos que retorna (con la instrucción return). En el ejemplo, la función es de tipo tagenda, y lo retornado, debe ser de tipo tagenda. De esta forma, cuando la función retorna el resultado, la variable que recibe dicho valor debe poseer el mismo tipo de datos. En la versión con función, la variable que recibe el retorno de la función es la variable m (línea 22, versión con función). tagenda ingreso(int i, string data){ tagenda ficha; //creamos una ficha ficha.id = i; ficha.nombre = data; return ficha; } #include<stdio.h> #include<cs50.h> //estructura typedef struct agenda{ int id; string nombre; }tagenda; //programa principal int main(){ tagenda ficha; //creamos una ficha ficha.id = 3; ficha.nombre = GetString(); printf("%s\n", ficha.nombre); } #include<stdio.h> #include<cs50.h> //estructura typedef struct agenda{ int id; string nombre; }tagenda; //funcion tagenda ingreso(int i, string data){ tagenda ficha; //creamos una ficha ficha.id = i; ficha.nombre = data; return ficha; } //programa principal int main(){ tagenda m; m = ingreso(3, GetString() ); printf("%s\n", m.nombre); } Versión sin función Versión con función 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. PROGRAMACIÓN. 2016. MIGUEL CARRASCO 26 RESUMEN PROGRAMACIÓN 18-04-2016 ESTRUCTURAS DINÁMICAS XXIII. A medida que nuestros programas se hacen más complejos y flexibles, será necesario la utilización de la memoria dinámica en extenso. En este punto Usted debe haber comprendido en profundidad todos los conceptos sobre punteros, funciones, paso por referencia, estructuras y manejo de memoria. A diferencia de la forma anterior, al emplear memoria dinámica podemos agregar, eliminar, buscar, recorrer tantas fichas como queramos. Esto permite que el sistema pueda incorporar nueva información. En una primera versión, crearemos una sola ficha sobre la cual reservaremos memoria, y haremos ingreso de los datos solicitados dentro de la función main. Para ello declaramos una ficha del tipo puntero (línea 11), y luego reservamos memoria según su tipo de datos (que en este caso es tagenda). En las líneas 14 en adelante observamos un nuevo modo para direccionar el campo de la estructura. Esto se debe a que el operador “.” punto tiene prioridad sobre el puntero, y por ello la expresión *ficha.id es ilegal. Por ello, empleamos el operador “->”, el cual se utiliza únicamente cuando las estructuras utilizan memoria dinámica. En un segundo ejemplo, creamos la función add_ficha que recibe un número y un nombre como parámetros de entrada, y luego ingresa los valores dentro de la lista. Dentro de la función (líneas 13–20) se realiza la operatoria de crear una ficha reservando un espacio de memoria y la asignación de datos. En la línea 21 asignamos el puntero de una_ficha a la lista vacía “lista”. En la función principal, los datos se ingresan a través de la lectura de datos por teclado (línea 29). Una vez que se ejecuta la función, y se asigna la ficha a la lista (línea 21), ésta se asigna al puntero a la primera ficha ingresada. Por esto podemos desplegar la información de la ficha con los datos ingresados a través de la función “add_ficha”. Sin embargo, este código sólo permite agregar una ficha al listado. Para agregar más fichas al conjunto debemos modificar la estructura para que cada ficha almacene un puntero (dirección) a la siguiente ficha. #include<stdio.h> #include<cs50.h> typedef struct agenda{ int id; string nombre; }tagenda; int main(){ tagenda *ficha; ficha = (tagenda*)malloc(sizeof(tagenda)); //ingreso de datos ficha->id = 1; ficha->nombre =GetString(); //despliegue de datos printf("%i, \t%s\n", ficha->id, ficha->nombre); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. #include<stdio.h> #include<cs50.h> // ESTRUCTURA // typedef struct agenda{ int id; string nombre; }tagenda; tagenda *lista=NULL; //variable global // FUNCION // void add_ficha(int i, string nombre){ tagenda *una_ficha; una_ficha = (tagenda*)malloc(sizeof(tagenda)); //asignación de datos una_ficha->id = i; una_ficha->nombre = nombre; lista = una_ficha; } // MAIN // int main(){ printf("Ingresa un número y tu nombre: "); //llamado a la funcion add_ficha(GetInt(), GetString()); printf("%i, \t%s\n", lista->id, lista->nombre); } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. PROGRAMACIÓN. 2016. MIGUEL CARRASCO 27 RESUMEN PROGRAMACIÓN 18-04-2016 LISTAS ENLAZADAS XXIV. Empleando los conceptos vistos anteriormente, ahora vamos a construir una lista generada en forma dinámica (lista, declarada como variable global), lo cual nos permite realizar operaciones más complejas sobre ella. Las operaciones más comunes son {Buscar, Insertar, Eliminar, Buscar_k_esimo, Visualizar}. A diferencia de la estructura anterior, ahora creamos un campo en la estructura que permite almacenar una dirección de memoria (línea 8, struct agenda *next) del mismo tipo que la estructura. Este punto es clave ya que permite almacenar una dirección de memoria del siguiente nodo en la lista dentro de la misma estructura. En el siguiente ejemplo, estudiaremos brevemente el caso de insersión de un nodo. Cuando comienza la ejecución del programa en la función main, se realiza un llamado a la función add_nodo(). Dentro de ella se solicita al computador reservar un espacio de memoria para almacenar los campos de la estructura (id y nombre). Si la lista está vacía, -lo cual es verdadero siempre en el primer caso-, entonces la primera ficha se asigna a la lista (lista = ficha, línea 25). En caso contrario, vamos a recorrer la lista hasta que el último nodo sea vacío. En ese punto vamos a insertar la nueva ficha (líneas 29-40). Este proceso se realiza avanzando en la lista hasta que el siguiente nodo sea nulo (el cual es el último nodo, línea 32,) Estando en dicho punto insertamos el nuevo nodo (línea 36). Lo anterior ocurre cada vez que llamamos a la función add_nodo() en la línea 51, dentro de la funciónmain(). El resultado del proceso anterior se representa gráficamente como el ingreso sucesivo de fichas a la lista global. En nuestra aplicación, cada vez que se inserta un nuevo nodo, este se inserta al final de la lista. Note que el último nodo siempre apunta a NULL de tal forma que siempre podamos determinar cuando terminar de recorrer la lista. A continuación revisaremos en detalle el proceso de inserción. Suponga que ya posee una lista con datos previamente ingresados (con el mismo programa), y desea ingresar la ficha con los siguientes datos {id=3, nombre=”marta”} a la lista. #include<stdio.h> #include<cs50.h> // ESTRUCTURA // typedef struct agenda{ int id; string nombre; struct agenda *next; }tagenda; tagenda *lista=NULL; //variable global // FUNCION ADD_NODO// void add_nodo(int i, string nombre){ //creamos un nodo de la lista tagenda *ficha= malloc(sizeof(tagenda)); ficha->id = i; ficha->nombre = nombre; ficha->next=NULL; //si es el primero de la lista if(lista==NULL){ printf("insertando %i\n", i); lista = ficha; } else{ //vamos a insertar un nodo al final de la lista tagenda *nodo_virtual= lista; while(true){ if (nodo_virtual->next!=NULL){ nodo_virtual= nodo_virtual->next; } else{ nodo_virtual->next = ficha; printf("insertando %i\n", i); break; } } } } // MAIN // int main(){ for(int i=0; i<3; i++){ //llamado a la funcion printf("Ingresa tu nombre: "); add_nodo(i, GetString()); } //vamos a recorrer la lista FIFO printf("\nListado: \n"); tagenda* virtual_ptr = lista; while (virtual_ptr != NULL){ printf("%i\t%s\n",virtual_ptr->id, virtual_ptr->nombre); //avazamos al siguiente nodo virtual_ptr = virtual_ptr->next; } } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. PROGRAMACIÓN. 2016. MIGUEL CARRASCO 28 RESUMEN PROGRAMACIÓN 18-04-2016 En primera instancia, los datos son ingresados dentro de la función main (línea 50), y luego son transferidos a la función add_nodo a través de un paso por valor. Cuando el control del programa ingresa a la función, primero reserva espacio para un nodo del tamaño de la estructura (línea 17), y luego ingresa los datos a dicho nodo. Note que este nodo apunta a NULL o vacío. El segundo paso consiste en buscar la dirección de memoria del nodo que está al final de la lista, de tal forma que podamos incorporar un nuevo nodo en dicha posición. En este ejemplo, este proceso se realiza a través de un ciclo while evaluando la condición (nodo_virtual->next!=NULL). Como no sabemos a priori cuantos nodos tenemos en la lista., debemos avanzar entre los nodos, simplemente asignando a la dirección actual, el nodo con la siguiente dirección: nodo_virtual= nodo_virtual->next; El tercer paso permite que el nodo sea insertado en la posición final de la lista. Este proceso se realiza una vez que en el paso anterior el puntero se encuentre justo en la última posición de la lista. Estando en dicho punto, entonces asignamos como última posición el nuevo nodo creado en el primer paso: nodo_virtual->next = ficha; Una vez asignado el nuevo nodo, terminamos la ejecución del ciclo con la instrucción break. NULL! NULL! NULL ! 2! Juan ! !"#$% 0x4C ! 1 ! Pedro! !"#&% NULL ! 3 ! Marta ! !"'(% *nodo_virtual! Nuevo nodo a insertar 2 ! 1 ! 3 ! 0x3F! 2! Juan ! !"#$% 0x4C ! 1 ! Pedro! !"#&% NULL ! NULL ! 3 ! Marta! !"'(% Nodo buscado una vez encontrado este nodo, asignamos la dirección de memoria del nuevo nodo tagenda *nodo_virtual= lista; ! ! while(true){! if (nodo_virtual->next!=NULL){! " nodo_virtual= nodo_virtual->next;! } ! ! ! ! } ! ! void add_nodo(int i, string nombre){ ! //creamos un nodo de la lista! tagenda *ficha= malloc(sizeof(tagenda));! ! ficha->id = i; ! ficha->nombre = nombre; ! ficha->next=NULL;! while(true){! if (nodo_virtual->next!=NULL){! " nodo_virtual= nodo_virtual->next;! } ! else{ ! nodo_virtual->next = ficha; ! "printf("insertando %i\n", i);! "break;! } ! } ! *ficha! PROGRAMACIÓN. 2016. MIGUEL CARRASCO 29 RESUMEN PROGRAMACIÓN 18-04-2016 PILAS Y COLAS XXV. Cómo vimos anteriormente, el manejo de memoria dinámica permite crear y manipular estructuras de datos más complejas y ricas en información. Otras estructuras muy relevantes en ciencias de la computación son las pilas y colas. Estas estructuras se diferencian de las listas generales en el modo de operación y manejo de los datos. Pilas (Stacks): son listas cuyo orden se define como el primero en entrar es el último en salir. Imagine una pila de libros donde el primer libro es aquél que está en la primera posición. Para obtener el primer libro, es necesario remover todos los anteriores uno a uno. Este modelo se conoce como LIFO (Last-In-Last-Out). Las operaciones básicas de las pilas son: {apilar, desapilar, crear_pila} Colas (Queues): son listas cuyo orden se define como el primero en entrar es el primero en salir. Por ejemplo, una cola de supermercado, donde el primer cliente en llegar a la caja es el primero en salir. Este modelo de funcionamiento se conoce como FIFO (First-In-Fist-Out). Las operaciones básicas de las colas son: {insertar, quitarPrimero, primero} item 2! item 1! item 3! item 4 ! queue ! dequeue! item 3 ! Modelo de Pila! item 2! item 3! item 4! queue! item 1! dequeue! Modelo de Cola! item 1! PROGRAMACIÓN. 2016. MIGUEL CARRASCO 30 RESUMEN PROGRAMACIÓN 18-04-2016 ÁRBOLES BINARIOS XXVI. Una vez que hayas dominado los misterios de las listas y sus aplicaciones (pilas y colas), revisaremos una de las estructuras más relevantes en ciencias de la computación: los árboles binarios. Un árbol binario es una estructura de datos donde cada nodo posee dos hijos (izquierdo y derecho) que son punteros y un número que permite indicar su valor. A medida que el árbol se va poblando de información, los datos se ingresan en forma ordenada de tal forma que la lectura y búsqueda de información se realiza en forma eficiente. Esta estructura puede ser mejorada aún más de tal forma que los nodos se ingresen en forma balanceada; técnica conocida como binary heap. Gracias a este tipo de estructuras, es posible comprimir la información, siendo el método más conocido el árbol de Huffman (Huffman Tree). Las aplicaciones pueden ir desde la compresión de la música en formato MP3, compresión de archivos (ZIP, RAR), telecomunicaciones, imágenes satelitales, por nombrar algunas. Primero comenzaremos redefiniendo la estructura de datos de tal forma que en cada nodo podamos almacenar información de dos punteros; nodo izquierdo y nodo derecho, además del número (u letra) que queramos almacenar dentro de la estructura. Este valor es muy importante ya que nos servirá para ordenar la información en forma estructurada. Antes de explicar el código, primero analicemos el proceso de creación de un árbol empleando la siguiente secuencia de números {-5, 2, -6, 0, 7}, y cuyos datos se ingresan el orden como se lee el arreglo. Explicación Nodo Resultado de árbol Primero creamos un nodo, e insertamos el primer valor de nuestra secuencia. Resultado, creamos el primer nodo del árbol. Nos movemos al nodo del árbol {-5}. Luego preguntamos {2} es mayor al nodo {-5}. La respuesta es verdadera, entonces creamos un nodo y lo asignamos la dirección de memoria