Está en la página 1de 28

Programación Avanzada:

7. Laboratorios

Nicolas Thériault

Departamento de Matemática y Ciencia de la Computación,


Universidad de Santiago de Chile.

N. Thériault (USACH) Presentación 1 / 28


Laboratorios (fechas tentativas)

1 Resolver sistemas lineales grandes, 23/10

2 Multiplicar matrices, 06/11

3 Calcular matrices inversas y resolver sistemas grandes, 20/11

4 Ordenar listados de puntos según sus coordenadas, 18/12

5 Resolver un problema de geometrı́a computacional, 15/01

6 Resolver un problema de programación dinamica, 22/01

N. Thériault (USACH) Presentación 2 / 28


Para los laboratorios 1, 2 y 3
Para evitar problemas de redondeo en la aritmética de matrices:
se trabaja módulo p = 3781996138433268199 (número primo de 62 bits)
aritmética de enteros (exacta, no hay problemas de redondeo)
permite dividir entre cualquier número 6= 0

suma/restas con resultados en {0, 1, . . . , p − 1}


producto con resta modular

programados para long de 32 bits, long long de 64 bits


deberı́a ser compatible con compiladores C99, C11 y C17

inverso (b −1 ): utiliza el algoritmo Euclidiano extendido


división a/b: calcular como a · (b −1 )

operaciones especiales:
long long SumaP( long long a , long long b ) ;
long long RestaP( long long a , long long b ) ;
long long MultP( long long a , long long b) ;
long long InvP( long long a ) ;

N. Thériault (USACH) Presentación 3 / 28


Para los laboratorios 4 y 5

Para evitar problemas de redondeo en la evaluación de distancias:


se trabaja con enteros long
aritmética de enteros para d 2 (exacta, no hay problemas de redondeo)
I d1 > d2 si y solo si d1 2 > d2 2
I Distancias al cuadrado en formato long long
I Calcular la raı́z cuadrada introduce un error, complica para comparar
I Riesgo: d + 1 y d − 2 parecen distintos aunque deberı́an ser iguales
I Riesgo: d1 + 1 > d2 − 2 aunque d1 < d2

programados para long de 32 bits, long long de 64 bits


deberı́a ser compatible con compiladores C99, C11 y C17
I para C11 y C17, se puede trabajar con listados más grandes
I para C99, empieza a producir repeticiones de puntos más temprano

N. Thériault (USACH) Presentación 4 / 28


Objetivos

Trabajar en equipos

Seleccionar técnicas de programación adecuadas según el problema a resolver

Aplicar las técnicas vistas en clases en situaciones prácticas

Controlar la utilización de memoria dentro de los programas

Producir código limpio y eficiente

Hacer experimentos para estimar el comportamiento del algoritmo a grande escala

N. Thériault (USACH) Presentación 5 / 28


Aspectos generales

Utilizar un sistema de menu en la ejecución del algoritmo.

Si debe leer un archivo (para trabajar los datos que contiene), debe pedir a los
usuarios de escribir su nombre dentro de la ejecución del programa

Testear todas sus funciones.

Si suponen algunos limitantes en las entradas, documentarlo

Siempre deben liberar la memoria utilizada antes de cerrar el programa (antes de


hacer el “salir” final)
I Si necesario, hacer una pausa del tipo:
printf(“Listo para cerrar, entrar cualquier entero: ”);
scanf(“%d”,&selecion);

N. Thériault (USACH) Presentación 6 / 28


C vs C++

Ventajas del C++:

Programación orientada a objetos

I Manejo más flexible de los tipos de variables

I Modelos (templates)

I Métodos/funciones virtuales

Manejo de excepciones (exception handling)

Librerı́as estandares más amplias

N. Thériault (USACH) Presentación 7 / 28


desventajas del C++

Algunas de las funcionalidades del C++ pueden afectar la eficiencia de los programas:

Modelos (templates)
I Requiere cuidado para asegurar que no tiene mucho costo
I Hacer el trabajo especifico al caso permite tomar ventaja de algunas propiedades

Métodos/funciones virtuales
I Afecta la eficiencia

Manejo de excepciones (exception handling)


I Afecta la eficiencia
I Da seguridad para evitar errores, pero las verificaciones se pagan
I Mejor verificar solamente cuando es necesario

Librerı́as estandares más amplias


I Requiere cuidado para asegurar que no tiene mucho costo
I Si se utiliza algo sin entenderlo, puede no ser lo más optimal para el caso

N. Thériault (USACH) Presentación 8 / 28


Ventajas de C

Lenguaje más pequeño, pero completo:


Se puede conocer todo el lenguaje estandar (sin tener que utilizar referencias).

Sencillo de leer.

Hay pocas manejas de hacer las cosas, por lo que es más sencillo entender el código
escrito por otros/otras.

Facilita incorporar nuevas personas en un grupo de trabajo.

Obliga a ser más cuidadoso


Los programas en C++ pueden ser igual de eficientes de los en C, pero es más fácil
de “descuidarse” en C++, y los descuidos se pueden acumular unos sobre otros.

Para trabajos conjuntos: TODO el equipo debe mantener el cuidado

N. Thériault (USACH) Presentación 9 / 28


Entradas y salidas en C
formato básico:
printf (equivalente de cout)
printf(“Listado de %d entradas.\n”,valor);

scanf (equivalente de cin)


scanf(“%d”,&valor);

Debe incluir los tipos de variables en la parte de “texto”:


c (char), s (string)
d (int), u (unsigned), ld (long), lld (long long)
f (float - decimal), e (float - exponencial), lf (double), llf (long double)

En printf, se puede inticar el número de posiciones (no utilizar en scanf):


%5d → entero de ≥ 5 espacios decimales
%6.3f → número decimal de 6 posiciones (con el “.”), hasta los milésimos.

Con archivos:
fprintf, fscanf (formato parecido, pero incluye el nombre del archivo)
Empezar con fopen, terminar con fclose
Tipo de variable del archivo: FILE (en mayusculas)
N. Thériault (USACH) Presentación 10 / 28
Punteros

Los punteros corresponden a la posición (bloque de memoria) donde se encuentran


unos datos
Se definen como las estructuras a las cuales apuntan, con una ∗ antes del nombre

tipo *nombre de la variable;


float *puntoflotante;
long *enterogrande;

Una vez definido, no se escribe el ∗


puntoflotante = NULL;

Para conocer el puntero asociado a una variable, se pone & frente al nombre
puntoflotante = &abc;

Necesarios para trabajar las entradas y salidas de muchas funciones


I Por ejemplo si una función requiere cambiar el valor de una de sus entradas

N. Thériault (USACH) Presentación 11 / 28


Arreglos

Bloques continuos de memoria de un mismo tipo de estructura.

Se debe conocer el tamaño (cantidad de entradas) al momento de definir

Se definen como sus estructuras, con un [tamaño] después del nombre


I El tamaño puede ser un valor fijo o una variable (int/long) con un valor dado

tipo nombre[tamaño];
float puntosflotantes[7];
long enterosgrandes[n];

La j-ésima entrada (empezando a contar posiciones en 0) se obtiene como:


nombre[j];
puntosflotantes[4];

En efecto, un arreglo es un puntero a la primera entrada


I Para pasar un arreglo como entrada de una función, se utiliza solo el puntero
I El procesador calcula la posición de cada entrada sumando su indice al puntero inicial
I Las funciones que reciben arreglos, no conocen su tamaño

N. Thériault (USACH) Presentación 12 / 28


Memoria dinámica en C
Permiten reservar espacios de memoria que seguirán disponibles fuera de la función
donde fueron definidos, en general como arreglos, sin restricción de tamaño total (dentro
de lo que permite el procesador).

malloc: reserva un bloque de espacios de memoria, devolviendo la dirrección inicial


int *arreglo = (int *) malloc( 20 * sizeof(int) ) ;

calloc: reserva un bloque de espacios de memoria, inicializando en 0


int *arreglo = (int *) calloc( 20 , sizeof(int) ) ;

realloc: ajusta el espacio de memoria asociado a una variable (puede cambiar la


dirección inicial). Libera el espacio ya no utilizado y copia los valores si la dirección
cambia.
arreglo = (int *) realloc( arreglo , 20 * sizeof(int) ) ;

free: libera (todo) el espacio asociado a la variable


free( arreglo ) ;
I No libera los espacios apuntados dentro la variable
I Necesario para evitar que la memoria queda reservada mientrás funciona el programa

N. Thériault (USACH) Presentación 13 / 28


Estructuras en C
Definición (ejemplo: nodo de árbol binario):

typedef struct nodoarbol {


long valor ;
nodoarbol *ramaizquierda ;
nodoarbol *ramaderecha ;
} nodo ;
I podemos poner cuantos elementos como queremos, de cualquier tipos
I el nombre inicial (temporario) sirve para poner punteros a la estructura dentro de ella
I el nombre final será el oficial de la estructura, puede ser distinto o repetir el inicial

Definir una variable/puntero:


nodo bloque = { 3 , izq , der } ;
nodo *dondebloque ;
Llamado a un elemento:
bloque.valor ;
dondebloque->ramaizquierda ; (*dondebloque).ramaizquierda ;
En malloc, calloc y realloc:
sizeof( nodo ) ;
N. Thériault (USACH) Presentación 14 / 28
Matrices y arreglos dobles
El lenguaje C permite definir arreglos dobles (matrices), pero su uso es limitado:
Se define como
tipo nombre[tamaño1][tamaño2];
En realidad es un arreglo simple de largo tamaño1 × tamaño2, donde los dados
están organizados en “filas” de largo tamaño2.
La posición de del bloque nombre[i][j] es i × tamaño2 + j
Para pasar a una función se hace como para un arreglo simple, con el puntero del
bloque inicial
Se pierde el formato “arreglo doble” para la función, se ve como un arreglo simple
La función no puede utilizar directamente la organización interna

Como consecuencia, los arreglos dobles son poco prácticos para ser utilizados en
sub-funciones:
Riesgos de trabajo fuera del arreglo (simple o doble), que puede producir un
segmentation fault o corromper los valores de otras variables
Se deben enviar todos los tamaños de los arreglos como entradas de la función
Se debe calcular la posición como i × tamaño2 + j para todas las utilizaciones

N. Thériault (USACH) Presentación 15 / 28


Matrices, alternativa 1
una forma manera de reducir los cálculos consiste en utilizar punteros dobles:
Se define como
tipo **nombre;
Es un arreglo de punteros, cada entrada corresponde a la posición inicial de una fila
(arreglo de variables tipo)
En vez de hacer el cálculo “i × tamaño2 + j”, se sigue el i-ésimo puntero y se toma
la j-ésima entrada de este
Si se utilizan varias entradas de una misma fila, se puede copiar el puntero del
arreglo para reducir el trabajo
Trabajar con memoria dinámica:
I Se crea el espacio para el arreglo de punteros primero, luego se crea el espacio para
cada fila y se guarda su ubicación en el arreglo de punteros.
I Para liberar el espacio de memoria, primero liberar el espacio de cada filas, y luego el
espacio del arreglo de punteros
Más flexible para la memoria: no requiere un espacio continuo para toda la matriz,
solamente un espacio continuo para cada filas (que pueden ser separadas una de la
otra) y un espacio continuo para el listado de filas.
Todavı́a hay que entregar las dimensiones como entradas de funciones que utilizan la
matriz.

N. Thériault (USACH) Presentación 16 / 28


Matrices, alternativa 2
Definir estructuras especiales para contener la matriz:
Una estructura que contiene las dimensiones (tamaños) de la matriz y un puntero
para el arreglo (o puntero doble)
Ejemplo:
typedef struct matriz {
long filas ;
long columnas ;
double **listado ;
} matriz ;

Para enviar a funciones, se necesita solamente la estructura (o su puntero) como


entrada en vez de tener las dimensiones como entradas a parte
I Código más compacto
I Más facil de seguir y verificar
I Ofrece más seguridad (menos riesgo de inconsistencias de tamaños por errores de
redacción o confusiones)

Se puede considerar estructuras especiales para las filas también, que contienen el
tamaño y el puntero para el arreglo
I Puede facilitar operaciones de filas

Desventaja: hay que manejar la estructuras para seguir el código

N. Thériault (USACH) Presentación 17 / 28


Funciones en C
Incluyen los tipos (estructura) de cada variable
void Invertir Matriz( matriz A , matriz *B ); // B = A∧(-1)
I void: sin entrada o salida (según donde se escribe)

Dos funciones no pueden tener el mismo nombre, aun si el tipo de variables cambia
int maximo int( int a , int b );
float maximo float( float a , float b );
El compilador puede permitir algunos ajustes de tipos, pero no es recomendable
I Ejemplo: si se manda un int de entrada cuando se pide un long

Se debe incluir las declaraciones de funciones antes de utilizarlas


I Esencial para funciones recursivas

Funciones con resultado único


I Utilizar return(variable), lo que termina la ejecución de la función.
I Utilizar puntero como entrada (escribir el resultado en la dirección asociada).
Funciones con varios resultados
I Utilizar punteros como entradas (escribir los resultados en las direcciones asociadas).
I Utilizar un arreglo (si todos los resultados son del mismo tipo).
I Utilizar una estructura especial.

N. Thériault (USACH) Presentación 18 / 28


Algunas funciones y constantes comunes en C

clock(): tiempo del procesador asociado al programa, en tipo de variable “clock t”


CLOCKS PER SEC: constante (de tipo clock t) para convertir a segundos

rand(): generador aleatorio, devuelve enteros entre 0 y RAND MAX


srand(semilla): inicializa la “semilla” del generador aleatorio
arc4random(): generador aleatorio con mejores propiedades, devuelve enteros entre
0 y UINT32 MAX. No disponible en versiones más antiguas de C

abs(int): valor absoluto; labs(long), fabs(float), fabsf(double), fabsl(long double)


log2(float): logaritmo en base 2; log2f(double), log2l(long double)
ceil(valor): parte entera (redondeada hacı́a arriba)
floor(valor): parte entera (redondeada hacı́a abajo)

NULL: puntero “vacio”

N. Thériault (USACH) Presentación 19 / 28


Algunos aspectos a considerar al programar

Orden de las operaciones

Operadores lógicos y acciones condicionales

Bucles while y for

Matrices y arreglos dobles

Sub-funciones e inlining

Funciones recursivas

Testeo de programas

N. Thériault (USACH) Presentación 20 / 28


Orden de las operaciones
Siempre verificar si el orden de las operaciones importan:
I Para sumas y productos de enteros: conmutativos y asociativos (el orden no importa)
I Para sumas de matrices: conmutativas y asociativas
I Para productos de matrices: asociativos pero no conmutativos
A · (B · C ) = (A · B) · C A · B 6= B · A
I Para productos de floats: conmutativos y asociativos
I Para sumas de floats: conmutativas pero no asociativos
F Problemas de redondeos (estudio de análisis númerico, etc)
−70 −70
20 + 3 · 10 + . . . + 3 · 10 = 20
−70 −70
3 · 10 + . . . + 3 · 10 + 20 = 20.0000000006

Algunas funciones conmutativas pueden no considerar ambas entradas como


equivalentes:
I “acumular” (suma que pone el resultado en un espacio de entrada)
podrı́a ser permitido para a ← a + b pero no para b ← a + b
I Se recomienda utilizar const en la definición de la función si se considera que el bloque
asociado al puntero de entrada no puede ser afectado (seguridad)

N. Thériault (USACH) Presentación 21 / 28


Operadores lógicos y acciones condicionales
Comparaciones:
I a == b (¿a es igual a b?), a! = b (¿a es distinto de b?),
I a < b (¿a es menor que b?), a <= b (¿a es menor o igual que b?),
I a > b (¿a es mayor que b?), a >= b (¿a es mayor o igual que b?),

Operadores lógicos:
I !P (negación lógica)
I P&&Q (P y Q)
I PkQ (P o Q)

Acciones condicionales: if e if-else


I Formato básico:
if (condicones)
{
acciones A;
}
else
{
acciones B;
}

I Cuidado: un “;” después de la condición (antes de los “{” y“}”) es equivalente a


poner una secuencia de acciones vacı́a.

N. Thériault (USACH) Presentación 22 / 28


Bucles while y for
Bucles while:
I Formato básico:
inicialización;
while (condicones)
{
acciones;
}

I Más flexibles, no es necesario saber cuantas iteraciones se harán


I Si hay incrementos, se pueden hacer antes o después de las acciones

Bucles for:
I Formato básico:
for ( inicialización ; condiciones ; incremento )
{
acciones;
}

I Más facı́l de seguir para entender el programa y verificar su validez


I Se sabe cuantas iteraciones hay
I Siempre hay un incremento, después de las acciones

N. Thériault (USACH) Presentación 23 / 28


Sub-funciones e inlining
En programas más complejos (de escribir), se puede utilizar sub-funciones o inlining:

Crear sub-funciones:
I Más sencillo de seguir el código
I Más sencillo para la verificación y el testeo
I El llamado a funciones aumenta un poco el tiempo de ejecución

Inlining manual:
I Las operaciones están detalladas a cada utilización
I Evita llamados a funciones (más eficiente) y “saltos” dentro del programa
I Aumenta en tamaño del código redactado
I Requiere cuidado con los nombres de variables, pero puede reutilizar las mismas
variables temporarias

Inlining automatico:
I Recomienda al compilador de hacer el reemplazo automáticamente
I Se agrega “inline” o “static inline” antes dedefinir/declarar la función
I Más seguro (evita errores de nombres de variables)
I Poco control sobre el aumento de tamaño del programa

Lo ideal es llegar a un equilibrio entre sub-funciones y inlining: crear funciones


cuando se necesitan, sin sub-dividir demasiado.
I Se recomienda empezar con sub-funciones para el testeo inicial

N. Thériault (USACH) Presentación 24 / 28


Funciones recursivas

Una función recursiva es una función que se llama a si misma (una o más veces).

Se debe declarar la función primero (sin excepción)


No se puede utilizar “inline”

Puede ser una herramienta esencial para obtener mejor eficiencia


I Formato natural para algoritmos Reducir-y-Conquistar y Dividir-y-Conquistar
I Común en algoritmos de Progración dinámica

Se deberı́a pensar como si se trataba de una demostración por inducción


Debe incluir unas condiciones para evitar recursiones infinitas
I Una función recursive siempre tendrá a lo menos un “if” para elegir entre el caso base
(sin recursión) y el caso recursivo
I Los emphcasos recursivos corresponden al paso inductivo en la demostración por
inducción
I Los emphcasos bases corresponden a la eituación bases en la demostración por
inducción (donde se inicia la inducción)
I Se llama a la función misma solamente si se cumple la hipótesis de inducción

N. Thériault (USACH) Presentación 25 / 28


Testeo de programas

Aunque un algoritmo puede ser bien desarrollado (preparado), con un pseudo-código


claro y verificado, varias cosas pueden salir mal al programarlo:

Errores de escritura
Errores de signos
Problemas con el manejo de memorı́a dinámica
Conflictos con sub-funciones y/o funciones provenientes de otras librerı́as
Interpretación equivocada de de los indices, etc
Detalles no considerados en el pseudo-código

En algunos casos, la complejidad conceptual (dificultad de entender todos los detalles del
programa y tenerlos todos en mente al mismo tiempo), especialmente con programas
desarrollados en equipos de trabajo grandes, puede ser muy dificil de asegurar que todo
está bien con el programa.

Por eso, es importante testear un programa antes de hacerlo oficial.

N. Thériault (USACH) Presentación 26 / 28


Consejos para el testeo

Empezar con ejemplos pequeños primero:


I Más rápido
I Idealmente comparar con valores calculados a mano o obtenidos por otros algoritmos
(previamente testeados)

Testear las (sub-)funciones una por una antes de testearlas juntas


I Idealmente testear cada función justo después de programarla y antes de empezar a
trabajar la próxima
I Juntar las funciones de a poco
I Si se detectan errores pero no se sabe donde, mostrar resultados parciales

Antes de testear una función recursiva de forma general, se recomiendo testear una
versión que hace a lo más un paso de recursión.
I Utilizar una versión de la función que parra después de aplicar el proceso de inducci’on
una sola vez
I Comparar con un algoritmo más básico (e.g. fuerza bruta)
I Puede ayudar a optimizar la función también (determinar cual es el caso base práctico)
I Evita entrar en bucles infinitos por errores en en proceso de los pasos
I Si hay un(os) error(es), es más sencillo identificar donde antes de que las recursiones
hagan una difusión del error

N. Thériault (USACH) Presentación 27 / 28


Mediciones de tiempo

Utilizar clock para medir tiempo (tiempo cpu), no time (tiempo reloj).
Según la instalación de C, podrı́an haber otras funciones de medición del tiempo cpu
(más precisas), pero no siempre están disponibles. Solo utilizar clock.
Hay un nivel mı́nimo de precisión de la medición, no considerar debajo de µs.

Para cálculos debajo del minuto, en general el proceso puede compartir el


procesador, lo que produce ruido en la medición del tiempo cpu.
Para mediciones de mayor precisión, se recomienda repetir el proceso hasta que
requiere a lo mı́nimo un minuto, medir el tiempo total y dividir por la cantidad de
repeticiones.
I Ejemplo: 10, 100, 1000, 104 , ... repeticiones.

Idealmente repetir con entradas (aleatorias) distintas, pero generar las entradas
aleatorias ANTES de empezar a medir el tiempo.
I Si el algoritmo requiere dos (o más) entradas, preparar un conjunto de valores para
cada una y ejecutar para las diferentes combinaciones (requiere menos tiempo y
memoria de preparación).
I Asegurar que las entradas preparadas no agotan el RAM.

N. Thériault (USACH) Presentación 28 / 28

También podría gustarte