Вы находитесь на странице: 1из 241

Curso de C alculo Numerico en C++

Juan Jose G omez Cadenas


Juan Z u niga Rom an
Departamento de Fsica At omica Molecular y Nuclear
Curso 2001-2002

Indice general
1. Un recorrido guiado por C++ 7
1.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2. El programa Hola mundo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.1. Comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.2. Directivas al preprocesador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.3. Ficheros de cabecera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.4. La biblioteca est andar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2.5. Ficheros de cabecera en C y en C++ . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.6. La funci on main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.7. Sentencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.8. Salida de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.9. Tipo devuelto por una funci on . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2.10. El programa hola Mundo visto por el compilador . . . . . . . . . . . . . . . . 10
1.3. Tipos y declaraciones. Variables y expresiones aritmeticas. Bucle while . . . . . . . . . 10
1.3.1. El espacio de nombres std . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.2. Variables y constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.3. Operaciones aritmeticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.4. Bucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.4. Funciones matem aticas. Bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.4.1. Declaraci on de las funciones matem aticas de C y C++ . . . . . . . . . . . . . . 16
1.4.2. Bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.5. Salida con formato. Bloque if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.5.1. Salida de datos con formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.5.2. Bloque if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.6. Escritura a un chero de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.6.1. Entrada y salida de datos revisitada . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7. Entrada de datos. Bloques l ogicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.1. Bucle while innito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7.2. Entrada de datos. La instrucci on cin . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7.3. Sentencia break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7.4. Control de ujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.8. Operadores l ogicos. Strings. Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8.1. Datos tipo string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.8.2. Operadores l ogicos OR y AND . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.8.3. Manejo de excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.9. Bucle do while. Reales en doble Precisi on. Un primer algoritmo numerico . . . . . . . 28
1.9.1. N umeros reales en doble precisi on . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.9.2. Bucle do while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.10. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3
4

INDICE GENERAL
2. Tipos 31
2.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.2. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.2.1. Tipos booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.2.2. Tipo car acter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.2.3. Tipos enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.2.4. Tipos en coma otante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2.5. Tama nos de los tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2.6. Tipo void . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2.7. Enumeraciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.3. Declaraciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.4. Alcance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.5. Typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3. Punteros, arreglos y estructuras 37
3.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.2. Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.3. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.4. Punteros y Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.5. Determinaci on din amica de la dimensi on de un arreglo . . . . . . . . . . . . . . . . . . 41
3.6. Cadenas literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.7. Referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.8. Estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4. Funciones 47
4.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.2. Declaraci on y Denici on de una Funci on . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.3. Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.4. Punteros a Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5. Soluci on de ecuaciones no lineales 57
5.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.2. Metodo de Bisecci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.3. Regula falsi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.4. Newton-Raphson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.5. Programaci on del metodo de bisecci on . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.5.1. Manejo de errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.5.2. Control de informaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5.5.3. Dise no . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
5.5.4. El programa xbiseccion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.5.5. El algoritmo de bisecci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.5.6. Recapitulaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.6. Programaci on del algoritmo de regula falsi . . . . . . . . . . . . . . . . . . . . . . . . . 79
5.7. Programaci on del algoritmo de Newton Raphson . . . . . . . . . . . . . . . . . . . . . 83
5.8. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6. Clases de vectores y matrices 89
6.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
6.2. Estructuras y clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.2.1. Estructuras revisitadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.2.2. Funciones de acceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
6.2.3. Encapsulamiento, apantallamiento de datos y clases . . . . . . . . . . . . . . . 94

INDICE GENERAL 5
6.2.4. Constructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.2.5. Funciones de lectura y escritura . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
6.3. La clase Complejo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
6.3.1. Un tipo complejo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
6.3.2. Funciones externas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
6.4. La clase de vectores reales RVector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.4.1. Representaci on de un espacio vectorial . . . . . . . . . . . . . . . . . . . . . . . 112
6.4.2. Representaci on interna . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.4.3. Constructores, destructores y constructor de copia . . . . . . . . . . . . . . . . 113
6.4.4. Funciones y operadores de acceso . . . . . . . . . . . . . . . . . . . . . . . . . . 116
6.5. Operadores y funciones que manejan vectores . . . . . . . . . . . . . . . . . . . . . . . 120
6.5.1. Tama no de un objeto y argumentos a funciones . . . . . . . . . . . . . . . . . . 122
6.5.2. El programa xvector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
6.6. Recapitulaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.7. La clase de matrices reales RMatriz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.7.1. El espacio vectorial de las matrices . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.7.2. Representaci on interna . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.7.3. Constructores, destructores y constructor de copia . . . . . . . . . . . . . . . . 128
6.7.4. Funciones y operadores de acceso . . . . . . . . . . . . . . . . . . . . . . . . . . 129
6.8. Operadores y funciones que manejan matrices . . . . . . . . . . . . . . . . . . . . . . . 131
6.8.1. El programa xmatriz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
6.9. Recapitulaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
7. Sistemas lineales de ecuaciones 139
7.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
7.2. Sistemas lineales f aciles de resolver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
7.3. Factorizaci on LU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
7.4. Factorizaci on de Doolittle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
7.5. Permutaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
7.6. C alculo de la matriz inversa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
7.7. Determinante de una matriz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
7.8. Programaci on del algoritmo de descomposici on LU . . . . . . . . . . . . . . . . . . . . 145
7.8.1. Clases de matrices y vectores revisadas . . . . . . . . . . . . . . . . . . . . . . . 145
7.8.2. Clases de permutaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
7.8.3. Clases de algoritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
7.8.4. El algoritmo ALineal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
7.8.5. Aplicaci on del algoritmo ALineal . . . . . . . . . . . . . . . . . . . . . . . . . . 158
7.8.6. Recapitulaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
8. Diagonalizaci on de matrices 161
8.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
8.2. Matrices diagonalizables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
8.2.1. Matrices diagonalizables por semejanza . . . . . . . . . . . . . . . . . . . . . . 162
8.2.2. Matrices reales y simetricas. Metodo de Jacobi . . . . . . . . . . . . . . . . . . 163
8.3. Implementaci on numerica del algoritmo de Jacobi . . . . . . . . . . . . . . . . . . . . . 165
8.4. Programaci on de un algoritmo de diagonalizaci on de matrices reales y simetricas . . . 166
8.4.1. Clase de matrices simetricas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
8.4.2. El algoritmo RDiagonal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
8.4.3. Ejemplo de aplicaci on. El programa xdiag . . . . . . . . . . . . . . . . . . . . . 170
8.5. Recapitulaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
9. Aproximaci on de funciones 173
6

INDICE GENERAL
9.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
9.2. Sistema lineal asociado a un polinomio. Matriz de Vandermonde . . . . . . . . . . . . 174
9.3. Interpolaci on de Lagrange . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
9.4. Interpolaci on de Newton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
9.4.1. Diferencias divididas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
9.4.2. Errores en la interpolaci on polin omica . . . . . . . . . . . . . . . . . . . . . . . 180
9.5. Interpolaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
9.6. Algoritmo de Neville . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
10.Diferenciaci on e integraci on numerica 191
10.1. Diferenciaci on numerica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
10.1.1. Extrapolaci on de Richardson . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
10.2. Integraci on Numerica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
10.2.1. Regla Trapezoidal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
10.2.2. Metodo de los coecientes indeterminados . . . . . . . . . . . . . . . . . . . . . 199
10.2.3. Regla de Simpson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
10.2.4. Integral de Romberg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
10.3. Programaci on de algoritmos de integraci on y derivaci on . . . . . . . . . . . . . . . . . 203
10.3.1. La clase Medida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
10.3.2. El algoritmo Derint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
10.3.3. Recapitulaci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
11.Tratamiento de datos experimentales 211
11.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
11.2. Propagaci on de errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
11.2.1. Funci on de densidad de probabilidad y funci on de distribuci on acumulativa . . 211
11.2.2. Valor esperado de una funci on . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
11.2.3. Propagaci on de errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
11.3. Principio de m axima verosimilitud . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
11.4. Mnimos cuadrados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
11.5. Mnimos cuadrados en el caso lineal . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
11.5.1. Ajuste de una recta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
11.5.2. Las ecuaciones normales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
11.5.3. Notaci on matricial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
11.5.4. Ajuste de una par abola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
11.6. Mnimos cuadrados en el caso no lineal . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
11.7. Verosimilitud del ajuste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
11.8. Programaci on de algoritmos de ajustes por mnimos cuadrados . . . . . . . . . . . . . 229
Pr ologo
7
8
Captulo 1
Un recorrido guiado por C++
1.1. Introducci on
En este tema presentamos una introducci on elemental al lenguaje de programaci on C++, a traves
de una serie de programas muy sencillos que comentaremos con gran detalle. Nuestro prop osito es
presentar los elementos m as b asicos del lenguaje, necesarios para empezar a programar algoritmos
numericos. Para ello necesitamos introducir las bases del lenguaje, esto es: variables y constantes,
tipos, aritmetica, control del ujo de ejecuci on, y los rudimentos de entrada y salida de datos.
1.2. El programa Hola mundo
Nuestro primer programa se limita a escribir la sentencia Hola Mundopor la pantalla y naliza.

Este es el c odigo:
/* holaMundo.cpp
* Descripci on:
* Nuestro primer programa en C++. Consta de una sola funci on
* main, que no espera argumentos (lo cual se indica por los par entesis vac os)
* y devuelve un entero, esto es un c odigo que indica si el programa termin o con
* exito (normalmente "0") o con alg un problema (normalmente un entero
* diferente de cero).
* El programa imprime un mensaje por la salida
* est andar (normalmente la consola)
* y termina
*/
#include <iostream> // biblioteca de entrada y salida de datos
int main()

std::cout << "Hola mundo!n";


return 0; // devuelve 0 al sistema (todo ha ido bien!)

9
1.2.1. Comentarios
En C++, se considera un comentario cualquier texto encerrado entre /* ...*/, as como cualquier
texto en una lnea que aparezca tras doble barra //. Notad que el programa holaMundo comienza con
un comentario del primer tipo y luego a nade varios comentarios del segundo tipo.
1.2.2. Directivas al preprocesador
La primera instrucci on que encontramos tras los comentarios es:
#include <iostream>
Este es un tipo especial de instrucci on, llamado una directiva al preprocesador. El preprocesador es un
programa que se ejecuta antes de compilar, y que entiende instrucciones que empiezan por #, tales
como:
#include, #define
y otras, que iremos comentando en su momento. En concreto, la directiva:
#include <iostream>
indica al preprocesador que debe incluir en el chero que est a leyendo (holaMundo.cpp en este caso),
antes de compilar (de ah el nombre, preprocesador), el contenido del chero iostream. Los corche-
tes <>, le indican al preprocesador que debe buscar dicho chero en un lugar est andar (del cuya
localizaci on no necesita preocuparse el programador).
1.2.3. Ficheros de cabecera
El chero iostream es un ejemplo de chero de cabecera o header le est andar, de los muchos
disponibles en C++. En concreto, iostream contiene la declaraci on de las herramientas necesarias
para la entrada y salida de datos, tales como el operador << y el ujo de salida std::cout.
1.2.4. La biblioteca estandar
Por que es necesario declarar las funciones de entrada y salida? En otros lenguajes de programa-
ci on, tales como FORTRAN, las funciones que se ocupan de negociar la entrada y salida de datos son
parte intrnseca del lenguaje. No as en C ni en C++ (que es una extensi on de C, por tanto compatible
con este). Ambos lenguajes tienen un n ucleo muy reducido, delegando toda una serie de utilidades a la
llamada Biblioteca Est andar, que abreviaremos en los sucesivo como Std (del ingles Standard Library).
La Std contiene, entre otras herramientas, las siguientes funciones:
1. entrada y salida de datos (con y sin formato)
2. manejo de errores de ejecuci on
3. manejo de cadenas de caracteres
4. funciones matem aticas
y un nutrido etcetera.
Ahora bien, para utilizar cualquiera de estas funciones, es necesario declararlas, es decir informarle
al compilador de su sintaxis. De ah que necesitemos incluir diferentes cabeceras (seg un las funciones
de la Std que queramos utilizar) en nuestro programa.
10
1.2.5. Ficheros de cabecera en C y en C++
En C un chero de cabecera se escribe como nombre.h. En C++, por convenci on se omite la
extensi on .hpara las cabeceras de la biblioteca est andar que no existan en C (como por ejemplo
iostream donde se declara, como hemos visto las funciones de entrada y salida de C++, diferentes
de las de C).
Por otra parte, en C++ se usan, a menudo, cabeceras que ya existan en C (recordad que C++
es una extensi on de C y por lo tanto puede utilizar todas las funciones denidas para este). Un
ejemplo son las funciones matem aticas, comunes a los dos lenguajes. Un programa en C incluira la
correspondiente cabecera mediante la directiva:
#include <math.h>
Lo que tambien es correcto en C++, aunque resulta preferible utilizar la convenci on:
#include <cmath>
ambas instrucciones suponen, en primera aproximaci on, la inclusi on del mismo chero. La cindica
que math es una cabecera que tambien est a disponible en C.
1.2.6. La funci on main
Nuestro programa consta de una unica funci on, main. Una funci on es un fragmento de c odigo
encerrado entre parentesis de llave (), que puede tomar uno o m as argumentos (los cuales aparecen
en los parentesis que siguen al nombre) y devuelve un cierto tipo (por ejemplo un n umero entero). En
concreto, main no toma argumentos (los parentesis tras el nombre est an vacos) y devuelve un n umero
entero (antes del nombre aparece la instrucci on int que indica que el tipo que main devuelve es un
entero). La funci on main es especial en el sentido de que el compilador siempre la ejecuta sin que sea
necesario que la invoquemos explcitamente.
1.2.7. Sentencias
En nuestro programa
int main()

std::cout << "Hola mundo!n";


return 0;

aparecen dos lneas terminadas en punto y coma. Un programa de C++, no es, de hecho, otra cosa
que un conjunto de instrucciones ( ordenes al compilador), terminadas en punto y coma y agrupadas
en funciones. El programa holaMundo consta de una sola funci on (main) con s olo dos sentencias.

Estas
pueden empezar en cualquier parte (podramos haber encadenado las dos en nuestro programa en una
sola lnea) pero el punto y coma es obligatorio, para indicar su nal. C++ diferencia entre may usculas
y min usculas. Por lo tanto, el nombre Kavafis es diferente del nombre kavafis. Los nombres pueden
ser de longitud arbitraria y mezclar caracteres alfabeticos y n umeros, con algunas restricciones que
comentaremos en su momento.
1.2.8. Salida de datos
La primera instrucci on de la funci on main es
std::cout << "Hola mundo!n" ;
11
El operador << escribe su segundo argumento (el objeto que le sigue) en el primero (el objeto que le
precede), es decir, escribe la cadena literal de caracteres (string literal en ingles) "Hola mundo!n" en
el objeto std::cout que representa la salida de datos est andar, normalmente asociada a la pantalla.
Una cadena literal es una secuencia de caracteres encerrados por dobles comillas. En una cadena
literal, el car acter seguido de otro car acter, especica un s olo car acter especial (car acter de escape).
En concreto n especica el car acter nueva lnea. En consecuencia, el resultado de la instrucci on es
escribir por la pantalla la sentencia Hola mundo! seguida por un salto de carro.
1.2.9. Tipo devuelto por una funci on
La segunda y ultima instrucci on del programa es return 0;. Como convenci on, la funci on principal,
main, debe devolver al sistema un n umero entero, en este caso un 0, que se interpreta como el
programa termin o normalmente.
1.2.10. El programa hola Mundo visto por el compilador
El compilador es el encargado de traducir las instrucciones escritas en C++ al lenguaje m aquina,
es decir, a una serie de instrucciones que puedan ejecutarse en la CPU del ordenador.
Cuando invocamos al compilador, este a su vez llama al preprocesador, que incluye todos los cheros
de cabecera que se le soliciten, por ejemplo iostream. A continuaci on el compilador empieza a leer
nuestro c odigo. Ignora todos los comentarios, encuentra en el chero iostream la declaraci on de las
herramientas de entrada y salida (tales como el operador << y el ujo de salida est andar std::cout)
y llama autom aticamente a la funci on main. Comprueba que la sintaxis de las instrucciones es correcta
y las traduce a lenguaje m aquina. El resultado de la compilaci on es un chero habitualmente
llamado objeto (el chero con las instrucciones en C++ se suele llamar fuente). Tpicamente, el
compilador toma el chero holaMundo.cpp (c odigo fuente) y produce un chero holaMundo.o (c odigo
objeto) que no es legible para los humanos pero s para el ordenador. El siguiente paso es enlazar
el programa, es decir, resolver las llamadas a objetos que no se denen en nuestro c odigo, como por
ejemplo el operador << y el ujo std::cout cuya denici on se encuentra en la biblioteca est andar.
En el programa holaMundo todos los objetos que se utilizan est an denidos en la Std con la que
el compilador sabe enlazar autom aticamente. El resultado es lo que se denomina c odigo ejecutable,
tpicamente holaMundo.exe.
El resultado es ciertamente predecible:
$ ./holaMundo.exe
Hola mundo!
1.3. Tipos y declaraciones. Variables y expresiones aritmeti-
cas. Bucle while
Nuestro siguiente programa utiliza la f ormula 1 c = 166, 386 pts para imprimir una tabla de
conversi on pesetas-euros y viceversa.
/** euros.cpp
*
* Imprime una tabla de conversi on pesetas-euros.
* Los valores var an entre 1 y 100 euros.
* Se calculan 20 valores intermedios igualmente espaciados.
*/
#include <iostream>
using namespace std; // uso directo de los nombres de la Std
12
int main()

const oat PTAS POR EURO = 166.386; // constante (n umero en coma otante)
oat minimo; // minimo es una variable en coma otante
oat maximo; // maximo es una variable en coma otante
int npasos = 20; // variable entera con valor inicial 20
minimo = 1.; // asignamos el valor 1 a la variable minimo
maximo = 100.; // asignamos el valor 100 a la variable maximo
/* A continuaci on denimos la variable en coma otante paso
* asign andole el resultado de una operaci on aritmetica
*/
oat paso = (maximo-minimo)/npasos;
cout << euros t pesetas n; //imprime cabecera de la tabla
oat euros = minimo;
/* el bucle while a continuaci on se ejecuta mientras
* el valor de la variable euros es menor o igual que el
* valor de la variable maximo
*/
while (euros <= maximo)
oat pts = euros*PTAS POR EURO;
cout << euros << t << pts << n;
euros = euros + paso;

return 0;

1.3.1. El espacio de nombres std


Tras los comentarios y la directiva al preprocesador que ya conocemos, aparece una sentencia
nueva,
using namespace std;
cuyo efecto es avisar al compilador que vamos a utilizar por defecto los nombres declarados en la
biblioteca est andar std. Esto nos permite escribir, por ejemplo, cout en lugar de std::cout.
1.3.2. Variables y constantes
A pesar de lo intuitivo que resulta el concepto de variable, vale la pena precisar su signicado cuando
se trata de un lenguaje de programaci on. Un programa no es sino un procedimiento que permite alterar
el valor de ciertos objetos, utilizando referencias simb olicas a estos (es decir asoci andoles nombres). Un
objeto es un area de de la memoria del ordenador en la cual hemos almacenado una cierta secuencia
de bits, que constituyen su valor. El tipo del objeto, indica la forma precisa en que dichos bits pueden
ser manipulados. Una variable es una asociaci on entre un nombre y un objeto. El tipo de la variable
especica el tipo de objetos con los que puede asociarse esta.
Para ilustrar los conceptos anteriores, consideremos la cuarta sentencia en la funci on main:
int npasos = 20; // variable entera con valor inicial 20
13
int entero
npasos 20
oat real simple precisi on
minimo 1.
oat real simple precisi on
maximo 100.
Figura 1.1: Asociaci on entre objetos y variables. En la primera la asociamos un objeto de tipo entero
con valor 20 a la variable de tipo int npasos. En la segunda y tercera la asociamos objetos de tipo
real en simple precisi on a las variables de tipo float minimo y maximo.
La instrucci on consta de dos partes. Por un lado declara que npasos es una variable de tipo int, es
decir, que el nombre npasos puede asociarse a un objeto de tipo entero. Por otro, asigna la variable
npasos a un objeto cuyo valor es 20. La gura 1.1 representa la situaci on en la memoria del ordenador.
Las asociaciones variable-objeto se han representado mediante cajas contiguas. La variable se sit ua a
la izquierda y el objeto a la derecha. La caja que representa una variable est a dividida en dos partes,
que especican su nombre y su tipo (usando la sintaxis de C++, int, float, etc). Igualmente, la
caja que representa el objeto est a dividida en dos partes, especicando su tipo (en notaci on intuitiva,
entero, real, etc) y su valor.
La declaraci on de una variable notica al compilador el tipo de objeto al que puede asociarse,
incluso si todava no hemos asignado ninguno. As, las sentencias segunda y tercera de main:
float minimo; // minimo es una variable en coma flotante
float maximo; // maximo es una variable en coma flotante
avisan al compilador que minimo y maximo pueden ser asignados a un objeto de tipo real (o en
coma otante, esto es, con una parte fraccionaria) y precisi on simple. M as adelante encontramos las
sentencias:
minimo = 1.; // asignamos el valor 1 a la variable minimo
maximo = 100.; // asignamos el valor 100 a la variable maximo
que asignan minimo y maximo a dos objetos concretos, de tipo real y valor 1 y 100 respectivamente.
Una constante en C++ no es otra cosa que una variable cuyo valor no puede modicarse. La
instrucci on:
const float PTAS POR EURO = 166.386; // constante (n umero en coma flotante)
declara que la variable de tipo oat PTAS POR EURO es, en realidad, una constante (notad el calicador
const), y le asigna el valor 166,386. Esta asociaci on no puede modicarse en el resto de la funci on.
Los nombres de las constantes suelen escribirse en may usculas por convenci on, lo que facilita su
identicaci on al leer el c odigo.
En C++ es obligatorio declarar todas las variables y constantes que se utilizan en una funci on. En
general, una declaraci on puede incluir una lista de variables, tal como:
float a, b, c;
No es necesario declarar las variables al principio de la funci on y de hecho tpicamente se declaran
justo antes de asignarlas, como en nuestro ejemplo:
float paso = (maximo-minimo)/npasos;
14
El tipo representa un car acter, tal como por ejemplo las letras del alfabeto o determinados carac-
teres de control, tales como los que encontramos en la instrucci on:
cout << "euros t pesetas n";
Ya vimos el efecto del car acter n (salto de carro). El car acter t representa un tabulador, otro
ejemplo corriente de secuencia de escape.
C++ ofrece adem as un tipo l ogico o booleano, bool, que toma los valores true y false.
Adem as de estos tipos b asicos existen muchos otros, tales como arreglos, referencias, punteros,
estructuras, etc, que iremos comentando en su momento. M as a un, C++ ofrece la posibilidad de
programar nuevos tipos, mediante el uso de clases, una de las herramientas m as potentes del lenguaje.
1.3.3. Operaciones aritmeticas
Como hemos visto la instrucci on:
float paso = (maximo-minimo)/npasos;
asigna a la variable de tipo float el resultado de la operaci on aritmetica entre las variables de tipo
float, maximo y minimo y la variable de tipo int, npasos. Obviamente, la instrucci on resta la
variable minimo de la variable maximo y divide el resultado por npasos. Es decir, la resta de dos
n umeros se representa por el smbolo y su divisi on por el smbolo /. An alogamente la suma se
representa por + y la multiplicaci on por .
Pero atenci on, maximo, minimo y npasos son de tipos diferentes. C++ especica una serie de
reglas para determinar el tipo resultante cuando se mezclan en una operaci on aritmetica variables de
diferentes tipos. Concretamente, si los operandos de los operadores aritmeticos tienen tipos diferentes,
el compilador convierte uno de los operandos al tipo del otro y el resultado de la operaci on tiene
este ultimo tipo. La regla de conversi on es la siguiente: si los dos operandos tienen el mismo tipo el
resultado es de ese tipo. En otro caso ambos operandos son convertidos a un tipo com un. Ese tipo
com un es, por orden, double, float, int. Es decir, la suma de un doble y un entero, por ejemplo, es
un doble. Hay que tener siempre cierta precauci on con los tipos y las operaciones, para evitar sorpresas
desagradables. Por ejemplo:
int i = 3, j = 2; cout << i/j << ","; // Imprime i/j
Cu al sera el resultado? Por que?
Otro detalle importante. Notad los parentesis alrededor de la resta (maximo - minimo). Estos
parentesis son necesarios. ya que C++ especica reglas de precedencia para cada uno de los operadores
aritmeticos. As, por ejemplo, la multiplicaci on y la divisi on tienen precedencia m as alta que la suma
y la resta y por lo tanto el c odigo:
float paso = maximo-minimo/npasos;
resultara en dividir primero minimo por npasos y restar a continuaci on el resultado de esta divisi on
del valor de maximo.
La mejor forma de evitar sorpresas desagradables es utilizar los parentesis tal como lo haramos
en matem aticas, para evitar ambig uedades.
1.3.4. Bucle while
Consideremos por ultimo el fragmento:
while (euros <= maximo)
float pts = euros*PTAS POR EURO;
cout << euros << "t" << pts << "n";
euros = euros + paso;

15
Un bucle while tiene la forma general:
while (expresi on)
...

Las sentencias contenidas entre llaves (el cuerpo del bucle) se ejecutan repetidamente mientras
la expresi on entre parentesis es verdadera. La comprobaci on de la veracidad de la expresi on entre
parentesis se realiza antes de ejecutar el cuerpo del bucle, lo que implica que este se ejecuta cero o
m as veces (es decir, si la expresi on es inicialmente falsa, el bucle no se ejecuta nunca).
En nuestro ejemplo, el bucle while se ejecuta mientras la variable euros (cuyo valor inicial es igual
a minimo es decir a 1) sea menor o igual (<=) que la variable maximo (que vale 100). Puesto que la
condici on inicial es cierta el cuerpo del bucle se ejecuta por primera vez. Adem as del operador l ogico
menor o igual, C++ dene los operadores menor (<), mayor (>), mayor o igual (>=) igual (==) y
no igual (! =).
Notad que la variable pts se declara dentro del bucle, puesto que no nos hace falta antes. El valor
que le asignamos es el resultado de multiplicar la variable euros por la constante PTAS POR EURO, es
decir, 166,386 la primera vez que se ejecuta el cuerpo del bucle. A continuaci on escribimos el valor de
ambas variables por la pantalla. Finalmente, asignamos a euros un nuevo valor, igual al valor anterior
que tena (1) m as el valor de la variable paso (= (maximo minimo)/npasos = 99/20 = 4,95.)
As pues, euros vale ahora 5,95. Al encontrar la llave que cierra el bucle while volvemos al principio
y comparamos de nuevo euros con maximo y as sucesivamente hasta que el valor de este exceda el
de aquel en cuyo momento el bucle termina y naliza el programa (normalmente o con exito, lo que
indicamos, por convenci on devolviendo un cero).
El resultado de ejecutar el programa es:
$ ./euros.exe
euros pesetas
1 166.386
5.95 989.997
10.9 1813.61
15.85 2637.22
20.8 3460.83
25.75 4284.44
30.7 5108.05
35.65 5931.66
40.6 6755.27
45.55 7578.88
50.5 8402.49
55.45 9226.1
60.4 10049.7
65.35 10873.3
70.3 11696.9
75.25 12520.5
80.2 13344.2
85.15 14167.8
90.1 14991.4
95.05 15815
100 16638.6
16
1.4. Funciones matematicas. Bucle for
El programa anterior es todava un poco rudimentario. Para empezar, la lista de valores en euros que
hemos producido contiene valores fraccionarios que no son demasiado intuitivos (5,95, 10,9, 15,85...).
Adem as, al estar los valores de la tabla igualmente espaciados, perdemos la conversi on para peque nas
fracciones de euro.
Una versi on mejorada del programa producira, por ejemplo, una tabla de conversi on para frac-
ciones peque nas de euro (por ejemplo, diez valores entre 0,1 y 1 euro), seguida de una tabla de
conversi on para cantidades medianas (por ejemplo diez valores entre uno y diez euros) y una tabla
nal que proporcionara conversiones para valores grandes, digamos entre diez y cien euros.
/** euro2.cpp
*
* Imprime una tabla de conversi on pesetas-euros.
* Los valores var an entre 1 y 100 euros de la siguiente manera:
* 10 valores entre 0.1 y 1 euros
* 10 valores entre 1 y 10 euros
* 10 valores entre 10 y 100 euros
*/
#include <iostream>
#include <cmath>
using namespace std;
int main()
const float PTAS POR EURO = 166.386; // constante (n umero en coma flotante)
cout << "euros t pesetas n"; // cabecera de la tabla
/* El bucle for se ejecuta con valor inicial i=0, mientras
* i es menor que 3, incrementando i en una unidad a cada paso */
for (int i = 0; i< 3; i++)
float escala = pow(10,i); // escala = 10
i
float minimo = 0.1*escala; // para i=0,1,2, escala=1,10
1
,10
2
float maximo = 1*escala;
int npasos = 9;
float paso = (maximo-minimo)/npasos;
float euros = minimo;
while (euros <= maximo)
float pts = euros*PTAS POR EURO;
cout << euros << "t" << pts << "n";
euros = euros + paso;
// cierra el bucle while
// cierra el bucle for
return 0;

17
1.4.1. Declaraci on de las funciones matematicas de C y C++
Notad la nueva directiva al preprocesador:
#include <cmath>
la cabecera cmath contiene la declaraci on de las funciones matem aticas de C (de ah la convenci on,
cmath, que ya explicamos) a las que tambien tiene acceso C++. Como ya hemos comentado, las
funciones matem aticas (la tabla 1.4.2 muestra las m as comunes) no son intrnsecas a C/C++ sino que
est an denidas en la Std . En concreto, en nuestro programa necesitamos usar una de tales funciones,
pow(x,y) cuyo efecto es elevar x a la potencia y, pow(x,y)=x
y
.
1.4.2. Bucle for
Tras denir la constante de conversi on entre euros y pesetas e imprimir la cabecera de la tabla
encontramos la instrucci on:
for (int i = 0; i< 3; i++)
Se trata de un nuevo tipo de bucle, cuya sintaxis es la siguiente:
for(sentencia-inicial; expresi on-a-evaluar; incremento)
...

En nuestro ejemplo, la sentencia inicial es :


int i = 0;
que declara la variable i como de tipo int y la dene (le asigna un valor) como igual a 0. La expresi on
a evaluar es:
i< 3
es decir, comprobamos si la variable i es menor que tres. La ultima parte del bucle for incrementa la
variable i, mediante la sentencia:
i++
que se traduce como i = i + 1. En resumen, la traducci on del bucle for al castellano es: mientras i,
cuyo valor inicial es cero sea menor que tres, incrementa i y ejecuta todas las sentencias entre llaves.
La primera vez que se ejecuta el bucle for, i vale 0. Por lo tanto, en la instrucci on:
float escala = pow(10,i); // escala = 10
i
los argumentos de pow son 10 y 0. Esto es, escala vale 10
0
= 1. Por tanto minimo vale 0.1 y maximo
vale 1. El resto del c odigo es identico al del programa anterior, ejecut andose el bucle while mientras
la variable euros (cuyo valor inicial es de 0.1) es menor que la variable maximo (que vale 1 en esta
iteraci on del bucle for). En consecuencia, el programa imprime una tabla de nueve valores entre 0.1
y 0.9 (el bucle while) antes de volver de nuevo al principio del bucle for e incrementar el valor de i.
La condici on l ogica (i < 3) todava se cumple, por lo que el bucle for se ejecuta de nuevo. Esta vez
escala vale 10
1
= 10 y por lo tanto minimo vale 1 y maximo vale 10. El resultado es una tabla entre 1
y 9. La ultima iteraci on del bucle for resulta en escala = 10
2
= 100, minimo = 10, maximo =100.
El resultado de ejecutar el programa anterior es:
$ ./euro2.exe
euros pesetas
0.1 16.6386
18
Funciones Signicado
sin(x) seno de x (x en radianes)
cos(x) coseno de x (x en radianes)
tan(x) tangente de x (x en radianes)
asin(x) sin
1
(x) en el rango [/2, /2], x[1, 1]
acos(x) cos
1
(x) en el rango [0, ], x[1, 1]
atan(x) tan
1
(x) en el rango [/2, /2]
atan2(x) tan
1
(y/x) en el rango [, ]
sinh(x) seno hiperb olico de x
cosh(x) coseno hiperb olico de x
tanh(x) tangente hiperb olica de x
exp(x) funci on exponencial e
x
log(x) logaritmo natural ln(x), x > 0
log10(x) logaritmo base 10 log
10
(x), x > 0
pow(x, y) x
y
sqrt(x)
_
(x), x 0
fabs(x) [x[
abs(x) [x[ (identico a fabs(x) s olo en C++ no en C
Cuadro 1.1: Algunas de las funciones matem aticas disponibles en la biblioteca est andar.
0.2 33.2772
0.3 49.9158
0.4 66.5544
0.5 83.193
0.6 99.8316
0.7 116.47
0.8 133.109
0.9 149.747
1 166.386
2 332.772
3 499.158
4 665.544
5 831.93
6 998.316
7 1164.7
8 1331.09
9 1497.47
10 1663.86
20 3327.72
30 4991.58
40 6655.44
50 8319.3
60 9983.16
70 11647
80 13310.9
90 14974.7
19
1.5. Salida con formato. Bloque if
El lector atento habr a detectado un problema. El ultimo n umero que imprimimos es 90, no 100
como dese abamos. La raz on es evidente. El bucle while se ejecuta mientras el valor de euros es
menor que el valor de maximo. En las dos primeras iteraciones del bucle for esto es exactamente
lo que queremos hacer, pero en la ultima queremos que el bucle while se ejecute mientras euros
sea menor o igual que maximo, para poder imprimir la conversi on del n umero 100. Alternativamente,
deseamos cambiar el valor de maximo en la ultima iteraci on del bucle para que sea ligeramente mayor
que 100.
Adem as, sera conveniente obtener nuestra tabla de conversi on con un n umero de decimales con-
creto (dos por ejemplo).
La tercera versi on del programa eurointroduce estas mejoras.
/** euro3.cpp
*
* Imprime una tabla de conversi on pesetas-euros.
* Id entico a euro2.cpp excepto por un bloque if
* y la salida con formato.
*/
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
int main()
const float PTAS POR EURO = 166.386; //constante en coma flotante
cout << "euros t pesetas n"; // cabecera de la tabla
// Uso de salida formateada
cout << setiosflags(ios::fixed);
/* El bucle for se ejecuta con valor inicial i=0, mientras
* i es menor que 3, incrementando i en una unidad a cada paso */
for (int i = 0; i< 3; i++)
float escala = pow(10,i); // escala = 10
i
float minimo = 0.1*escala; // para i = 0, 1, 2, escala = 1, 10
1
, 10
2
float maximo = 1*escala;
int npasos = 9;
float paso = (maximo-minimo)/npasos;
float euros = minimo;
/* Incrementa maximo en una unidad en la ultima
* iteraci on del bucle, para imprimir el ultimo
* valor de la lista (100) */
if (i == 2)
maximo += paso;
while (euros <= maximo)
20
float pts = euros*PTAS POR EURO;
cout << setw(8) << setprecision(2)
<< euros<< "t" << setw(8) << setprecision(2)
<< pts << "n";
euros = euros + paso;
//cierra el bucle while
// cierra el bucle for
return 0;

1.5.1. Salida de datos con formato


Encontramos tambien aqu una nueva directiva al preprocesador, la instrucci on:
#include <iomanip>
donde se declaran las funciones que se ocupan de la salida de datos con formato. En particular,
queremos escribir los datos en formato de coma otante, para lo cual necesitamos la instrucci on:
cout << setiosflags(ios::fixed);
cuyo efecto es informar al compilador que toda salida de datos posterior a esta instrucci on debe tener
el formato dddd.dd (si, por ejemplo hubieramos escrito ios::scientific, el formato requerido sera
d.dddEdd).
Consideremos ahora las instrucciones:
cout << setw(8) << setprecision(2)
<< euros<< "t" << setw(8) << setprecision(2)
<< pts << "n";
que se ocupan, como en el ejemplo anterior de escribir por la pantalla las variables euros y pts, pero
en este caso con el formato en coma otante especicado anteriormente. Dicho formato requiere dos
cantidades para especicarlo, la anchura de campo (la extensi on total del n umero, contando espacios
en blanco, decimales y el punto decimal) y la precisi on (el n umero de cifras decimales signicativas).
La anchura de campo se especica con el calicador setw(anchura-de-campo) y la precisi on con el
calicador setprecision(precisi on). En nuestro ejemplo pedimos una anchura de campo total de
8 para todos los n umeros de la tabla y una precisi on de dos cifras decimales. Notad que el formato de
un n umero se especica como una modicaci on a la salida normal sin formato. En el ejemplo anterior
la instrucci on:
cout << euros;
instrua al compilador de lo siguiente: saca el valor de la variable euros por la pantalla. La presencia
de los calicadores:
cout << setw(8) << setprecision(2)
<< euros
indica al compilador que la cantidad que se escriba tras ellos debe tener un formato acorde a la anchura
de campo y precisi on especicadas, por lo tanto: escribe con anchura de campo 8 y precisi on 2 el
valor de la variable euros.
21
1.5.2. Bloque if
Para poder escribir el ultimo valor de la tabla (100) utilizamos la sentencia:
if (i == 2)
maximo += paso;
que se traduce as: si i es igual a 2, incrementa maximo en un paso. La variable i vale 2 en la tercera
iteraci on del bucle for, cuando maximo vale 100, por tanto la instrucci on tiene el efecto de que maximo
valga 101 antes de ejecutar el bucle while, exactamente como necesitamos para imprimir el valor 100.
El bloque if tiene la forma general:
if (expresi on)
...

y su efecto es: si la expresi on entre parentesis es correcta, ejecuta las sentencias entre comillas.
El resultado es:
$ ./euro3.exe
euros pesetas
0.10 16.64
0.20 33.28
0.30 49.92
0.40 66.55
0.50 83.19
0.60 99.83
0.70 116.47
0.80 133.11
0.90 149.75
1.00 166.39
2.00 332.77
3.00 499.16
4.00 665.54
5.00 831.93
6.00 998.32
7.00 1164.70
8.00 1331.09
9.00 1497.47
10.00 1663.86
20.00 3327.72
30.00 4991.58
40.00 6655.44
50.00 8319.30
60.00 9983.16
70.00 11647.02
80.00 13310.88
90.00 14974.74
100.00 16638.60
1.6. Escritura a un chero de datos
Hasta ahora hemos escrito por la pantalla. Sin embargo, muy a menudo, necesitaremos escribir el
resultado de nuestros c alculos en un chero en disco.
22
La cuarta y ultima versi on del programa euronos permite escribir la tabla de conversi on a un
chero en el disco.
/** euro4.cpp
*
* Escribe en el fichero cambio.dat una tabla de conversi on pesetas-euros.
* Id entico a euro3.cpp excepto por la salida a fichero.
*/
#include <iostream>
#include <fstream>
#include <cmath>
#include <iomanip>
using namespace std;
int main()

const float PTAS POR EURO = 166.386; //constante en coma flotante


cout << "euros t pesetas n"; // cabecera de la tabla
ofstream fout("cambio.dat"); // conecta "fout" con el fichero cambio.dat
// Uso de salida formateada
cout << setiosflags(ios::fixed); // formato en punto flotante para salida por pantalla
fout << setiosflags(ios::fixed); // ditto para salida en el fichero cambio.dat"
/* El bucle for se ejecuta con valor inicial i=0, mientras
* i es menor que 3, incrementando i en una unidad a cada paso */
for (int i = 0; i< 3; i++)
float escala = pow(10,i); // escala = 10
i
float minimo = 0.1*escala; // para i = 0, 1, 2, escala = 1, 10
1
, 10
2
float maximo = 1*escala;
int npasos = 9;
float paso = (maximo-minimo)/npasos;
float euros = minimo;
/* Incrementa maximo en una unidad en la ultima
* iteraci on del bucle, para imprimir el ultimo
* valor de la lista (100) */
if (i == 2) maximo++;
while (euros < 3 maximo)
float pts = euros*PTAS POR EURO;
// salida por la pantalla
cout << setw(8) << setprecision(2)
<< euros<< "t" << setw(8) << setprecision(2)
<< pts << "n";
23
// salida al fichero cambio.dat
fout << setw(8) << setprecision(2)
<< euros<< "t" << setw(8) << setprecision(2)
<< pts << "n";
euros = euros + paso;
//cierra el bucle while
// cierra el bucle for
cout << "La tabla ha sido guardada en el fichero "cambio.dat"" << endl;
return 0;

1.6.1. Entrada y salida de datos revisitada


En los ejemplos anteriores hemos ilustrado como escribir datos por la salida est andar utilizando
el operador << que enva su segundo argumento al ujo est andar cout.
Precisemos ahora la noci on de ujo de datos (data stream en ingles). Se trata de una secuencia de
bytes producidos (salida) o consumidos (entrada) por un programa. Dicho ujo puede asociarse a un
chero en disco o a un dispositivo, tal como un terminal. En concreto, los dos ujos m as corrientes,
cin (cuyo uso ilustramos en el siguiente ejemplo) y cout, conectan el objeto enviado por el operador
<< (en el caso de cout) o el operador >> (en el caso de cin) con la salida est andar (pantalla) o la
entrada est andar (teclado). Mencionaremos aqu un tercer ujo sumamente util, cerr, que conecta el
objeto enviado por << con la salida est andar de error (que a menudo es tambien la pantalla, pero
puede desviarse a otro dispositivo).
Para escribir o leer de un chero, en lugar de utilizar la entrada y salida est andar, conectamos
los operadores << y >> a un ujo de salida o entrada a un chero, utilizando un objeto de tipo
ofstream (del ingles output le stream) para la salida o de tipo ifstream (input le stream) para la
salida. En el programa anterior, la instrucci on:
ofstream fout("cambio.dat"); // conecta "fout" con el fichero cambio.dat
asocia el objeto de tipo ofstream cuyo nombre es fout con el chero en disco cambio.dat. Una
vez que hemos especicado a que chero asociamos fout (el nombre es arbitrario, podramos haber
escogido fsalida, por ejemplo) este se comporta exactamente igual que cout, excepto que los datos
que le enviemos usando el operador << terminar an en el chero cambio.dat en lugar de en la pantalla.
De ah que sea necesario especicar un formato para fout, adem as de para cout (recordad, son dos
objetos diferentes, aunque muy parecidos, uno es un ujo de salida por la pantalla, el otro a un chero),
como se hace en el programa:
cout << setiosflags(ios::fixed); // formato en punto flotante para salida por pantalla
fout << setiosflags(ios::fixed); // ditto para salida en el fichero cambio.dat"
y, naturalmente, es necesario escribir una instrucci on de escritura para fout identica a la que escribimos
para cout.
// salida por la pantalla
cout << setw(8) << setprecision(2)
<< euros<< "t" << setw(8) << setprecision(2)
<< pts << "n";
// salida al fichero cambio.dat
fout << setw(8) << setprecision(2)
<< euros<< "t" << setw(8) << setprecision(2)
<< pts << "n";
24
Por ultimo, notad que para utilizar ujos de entrada y salida a cheros en disco ofstream como en
este ejemplo y ifstream que veremos m as adelante, es necesario incluir la cabecera # <fstream>.
Al ejecutarse, el programa escribir a una tabla de cambio euros a pesetas por la pantalla, identica
a la del ejemplo anterior y adem as crear a un chero en disco con una copia de esa tabla.
1.7. Entrada de datos. Bloques l ogicos
Un programa m as util que los de los ejemplos anteriores sera uno que nos permitiera simular una
euro calculadora. Es decir, el usuario elige si la cantidad que va a introducir son euros o pesetas, a
continuaci on teclea la cantidad a convertir y el programa devuelve el resultado y ofrece convertir otro
n umero hasta que el usuario decide terminar. El siguiente programa implementa una versi on sencilla:
/** euroCalculadora.cpp
*
* Calculadora para cambiar euros a pesetas y viceversa.
*/
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
int main()

const float PTAS POR EURO = 166.386;


cout << setiosflags(ios::fixed);
while (true) // bucle infinito la condici on l ogica siempre es cierta
int unidad; // euros o pesetas
float cantidad; // cantidad a cambiar
float resultado; // resultado del cambio
cout << " para cambiar pts a euros teclee 1 n"
<< " para cambiar euros a pts teclee 2 n"
<< " para finalizar teclee 0 n"
<< " teclee 1,2, o 0 ->";
cin >> unidad;
// salimos del bucle si el usuario teclea 0
if (unidad == 0) break;
if (unidad == 1) // pesetas a euros
cout << " teclee la cantidad en pts->";
cin >> cantidad;
resultado = cantidad/PTAS POR EURO;
cout << setw(10) << setprecision(1) << cantidad
<< " pesetas equivalen a"
<< setw(10) << setprecision(2) << resultado
<< " euros" << endl;

else if(unidad == 2) //euros a pesetas


cout << " teclee la cantidad en euros->";
25
cin << cantidad;
resultado = cantidad*PTAS POR EURO;
cout << setw(10) << setprecision(2) << cantidad
<< " euros equivalen a"
<< setw(10) << setprecision(1) << resultado
<< " pesetas" << endl;

return 0;

1.7.1. Bucle while innito


Reparad en el argumento del bucle while :
while (true)
recordemos que un bucle while se ejecuta mientras la condici on entre parentesis sea cierta. Por lo
tanto, la condici on en el bucle es siempre cierta. En otras palabras, hemos construido un bucle innito.
1.7.2. Entrada de datos. La instrucci on cin
A estas alturas ya estamos familiarizados con las instrucciones de escritura de datos. An alogamente
a estas, el operador >> toma datos de su primer argumento (que debe ser por tanto un ujo de entrada)
y los coloca en su segundo argumento (por tanto una variable, de cualquiera de los tipos v alidos en
C++). El ujo cin est a conectado con la entrada de datos est andar, normalmente el teclado. En
nuestro programa la instrucci on:
cin >> unidad;
resulta en tomar un dato del teclado (va cin) y colocarlo en la variable entera unidad.
1.7.3. Sentencia break
La sentencia break nos permite escaparde un bucle (sea este while, for o de otro tipo). Su
efecto es saltar a la sentencia inmediatamente posterior al nal del bucle donde aparece.
1.7.4. Control de ujo
Esperamos que la variable entera unidad valga 1 (pesetas a euros), 2 (euros a pesetas) o 0 para
terminar. De ah la sentencia:
if (unidad == 0) break;
cuyo efecto es sacarnos del bucle cuando el usuario teclea cero. El bloque if()else a continuaci on:
if (unidad ==1) // pesetas a euros
cout << " teclee la cantidad en pts->";
cin >> cantidad;
resultado = cantidad/PTAS POR EURO;
cout << setw(10) << setprecision(1) << cantidad
<< " pesetas equivalen a"
<< setw(10) << setprecision(2) << resultado
<< " euros" << endl;
26

else if(unidad ==2) //euros a pesetas


cout << " teclee la cantidad en euros->";
cin << cantidad;
resultado = cantidad*PTAS POR EURO;
cout << setw(10) << setprecision(2) << cantidad
<< " euros equivalen a"
<< setw(10) << setprecision(1) << resultado
<< " pesetas" << endl;

hace lo siguiente: si unidad vale uno se ejecutan las sentencias en el bloque if que convierten pesetas
a euros e imprimen el resultado por la pantalla. Si unidad vale 2 se ejecuta el bloque else if que
convierte euros a pesetas. En otro caso habremos llegado al nal del bucle y volveremos al principio
sin realizar ninguna acci on.
He aqu la euro calculadora en acci on:
$ ./euroCalculadora.exe
para cambiar pts a euros teclee 1
para cambiar euros a pts teclee 2
para finalizar teclee 0
teclee 1,2, o 0 ->1
teclee la cantidad en pts->235
235.0 pesetas equivalen a 1.41 euros
para cambiar pts a euros teclee 1
para cambiar euros a pts teclee 2
para finalizar teclee 0
teclee 1,2, o 0 ->2
teclee la cantidad en euros->35
35.00 euros equivalen a 5823.5 pesetas
para cambiar pts a euros teclee 1
para cambiar euros a pts teclee 2
para finalizar teclee 0
teclee 1,2, o 0 ->0
$
1.8. Operadores l ogicos. Strings. Excepciones
Una versi on mejorada del programa anterior, permitira al usuario introducir su elecci on de euros o
pesetas tecleando un car acter (por ejemplo e para euros, p para pesetas, f para n) en lugar
de un n umero. Adem as, el programa debera ofrecer valores por defecto (por ejemplo, a la ultima
cantidad a la que se cambi o).
Veamos como introducir estas mejoras.
/** euroCalculadora2.cpp
*
* Calculadora para cambiar euros a pesetas y viceversa.
* Versi on modificada de euroCalculadora, introduce el uso del tipo string.
*/
#include <iostream>
#include <cmath>
#include <iomanip>
27
#include <string>
using namespace std;
int main()

const float PTAS_POR_EURO = 166.386;


cout << setiosflags(ios::fixed);
while (true) // bucle infinito, la condici on l ogica siempre es cierta
string eleccion; // tipo de cambio
float cantidad; // cantidad a cambiar
float resultado; // resultado del cambio
cout << endl
<< "Elige:" << endl
<< " PtasAEuros (P) - para cambiar ptas a euros" << endl
<< " EurosAPtas (E) - para cambiar euros a ptas" << endl
<< " Fin (F) - para finalizar" << endl
<< endl;
cout << "> ";
cin >> eleccion;
// Si el usuario desea terminar salimos inmediatamente del bucle
if (eleccion == "Fin" || eleccion == "F" || eleccion == "f")
break;
/*
* Efectuamos el cambio.
*/
// Pesetas a euros
if (eleccion == "PtasAEuros" || eleccion == "P" || eleccion == "p")
cout << "Cantidad en Ptas: ";
cin >> cantidad;
resultado = cantidad/PTAS_POR_EURO;
cout << setw(10) << setprecision(1) << cantidad
<< " pesetas equivalen a"
<< setw(10) << setprecision(2) << resultado
<< " euros" << endl;

// Euros a pesetas
else if (eleccion == "EurosAPtas" || eleccion == "E" || eleccion == "e")
cout << "Cantidad en Euros: ";
cin >> cantidad;
resultado = cantidad*PTAS_POR_EURO;
cout << setw(10) << setprecision(2) << cantidad
<< " euros equivalen a "
28
<< setw(10) << setprecision(1) << resultado
<< " pesetas" << endl;

else
cerr << "No entiendo lo que pides, prueba de nuevo." << endl;

return 0;

1.8.1. Datos tipo string


Una ventaja de usar string en lugar de int para controlar el tipo de cambio que deseamos es que
resulta m as mnemotecnico teclear e para euro que, digamos, 1. En este ejemplo no nos aporta
mucho m as, pero en general las cadenas de caracteres aparecen con frecuencia en programaci on.
En C las cadenas de caracteres se suelen representar mediante un arreglo de chars terminados en el
car acter nulo (NULL), y la manipulaci on de estas cadenas se hace mediante las funciones de la biblioteca
string.h (m as sobre arreglos y cadenas en el tema tres). En C++ existe una herramienta m as potente,
el tipo string, que representa una cadena de caracteres y que dispone de mayor funcionalidad siendo
al mismo tiempo m as sencillo de manejar y menos propenso a errores que sus equivalentes en C.
Para utilizar strings en nuestro programa debemos incluir la cabecera
#include <string>
1.8.2. Operadores l ogicos OR y AND
La instrucci on
if (eleccion == "Fin" || eleccion == "F" || eleccion == "f")
break;
se traduce de la siguiente manera: si la variable eleccion es igual a "Fin" o igual a "F" entonces
sal del bucle. El operador [[es el OR l ogico en C++. Una sentencia del tipo
(condici on a || condici on b)
es cierta si la condici on a o la condici on b son ciertas.
El operador && es el AND l ogico en C++. Una sentencia del tipo
(condici on a && condici on b)
es cierta si la condici on a y la condici on b son ciertas.
1.8.3. Manejo de excepciones
Los programas, muy a menudo, no hace lo que el programador espera de ellos. Una parte im-
prescindible de la tarea de programar, es introducir lo que se suele llamar control de excepciones.
Ejemplos tpicos. El resultado de un c alculo arroja un n umero irrazonablemente grande (Quiz as es-
tamos dividiendo por un n umero m as peque no de la cuenta?) o el argumento de una raz cuadrada
se hace negativo (en realidad deba ser positivo pero hemos cometido un error de redondeo?). Otras
posibilidades m as insidiosas es que el resultado de un bloque l ogico sea distinto del que nos esperamos.
En el ejemplo que estamos comentando, si se escribe correctamente, no debe llegar nunca al bloque:
29
else
cerr << "No entiendo lo que pides, prueba de nuevo." << endl;

ya que eleccion debera valer "E" o "P" a esas alturas del programa.
La manera m as sencilla y expeditiva de manejar una excepci on es precisamente escribir un mensaje
de aviso por cerr, la salida est andar de error (que suele ser la pantalla, pero podemos desviar, por
ejemplo a un chero) y abortar la ejecuci on mediante la instrucci on exit(1) (que provoca que el
programa termine con el c odigo de error 1, recordad que 0 signica terminaci on sin error y un
entero distinto de cero signica error de ejecuci on, el valor del entero puede asociarse a un c odigo
de error).
En nuestro caso, como el unico peligro que corremos es el de no saber que est a pidiendo el usuario,
le avisamos de que no le entendemos y volvemos a empezar.
1.9. Bucle do while. Reales en doble Precisi on. Un primer
algoritmo numerico
Ya estamos en condiciones de escribir nuestro primer algoritmo numerico, que es tambien el ultimo
ejemplo de este captulo de introducci on. El siguiente programa implementa el algoritmo de Newton
para calcular la raz cuadrada de un n umero, partiendo del valor del n umero como primera aproxi-
maci on. Como veremos en el tema correspondiente, el algoritmo de Newton es un caso particular del
metodo de Newton-Rapson, aplicado a la funci on

x. Seg un dicho algoritmo, dada una aproximaci on
x a la raz de un n umero dado, y, pueden obtenerse sucesivas aproximaciones m as exactas a partir de
la ecuaci on:
x = 0,5 (x + y/x)
/* Programa newton.cpp
*
* Implementa el algoritmo de Newton para calcular el valor de
* la ra z cuadrada de un n umero.
*
* El algoritmo consiste en hallar aproximaciones sucesivamente mejores
* de la ra z cuadrada x=sqrt(y), basadas en la siguiente f ormula iterativa:
*
* x(n+1) = 1/2 * ( x(n) + y / x(n) )
*/
#include <iostream>
#include <cmath>
using namespace std;
const double TOLERANCIA = 1.0e-6; // tolerancia del c alculo
int main()

// Pedimos al usuario el valor del n umero cuya ra z quiere calcular


double y;
cout << "Introduce el numero cuya raiz quieres calcular: ";
cin >> y; // capturamos el n umero como una variable de tipo doble
30
// Tomamos como (burda) aproximaci on inicial de sqrt(y) el
// propio valor "y".
double x = y;
/* El bucle do-while eval ua las sucesivas aproximaciones a la ra z,
* deteni endose cuando la aproximaci on "x" es lo suficientemente buena,
* es decir, cuando x
2
y < TOLERANCIA. El bucle se eval ua
* al menos una vez.
*/
do
x = (x + y/x) / 2;
while (abs(x*x - y) > TOLERANCIA);
// Imprimimos el valor de la ra z.
cout << "La raiz del numero " << y << " es " << x << endl;
return 0;

1.9.1. N umeros reales en doble precisi on


Ya hemos comentado que el tipo double se utiliza para representar n umeros reales en doble preci-
si on. En este primer ejemplo de aplicaci on numerica y, en general, en la casi totalidad de los algoritmos
que desarrollaremos a lo largo del curso, utilizaremos el tipo double con preferencia al tipo float.
1.9.2. Bucle do while
El c alculo se realiza en el bucle:
do
x = (x + y/x) / 2;
while(abs(x*x - y) > TOLERANCIA);
El bucle do while es muy similar al bucle while. Las sentencias contenidas entre llaves (el cuerpo
del bucle) se ejecutan repetidamente mientras la expresi on entre parentesis es verdadera (no eval ua
a cero). La unica diferencia, de hecho, con el bucle while es que en este caso la comprobaci on de la
veracidad de la expresi on entre parentesis se realiza despues de ejecutar el cuerpo del bucle, lo que
implica que este se ejecuta una o m as veces (es decir, incluso si la expresi on es inicialmente falsa, el
bucle se ejecuta al menos una vez).
Como podeis comprobar la implementaci on numerica del metodo de Newton es muy sencilla. To-
mamos como primera aproximaci on de la raz x del n umero y el valor de y. A continuaci on mejoramos
la aproximaci on mediante la f ormula x = 0,5 (x + y/x) y comprobamos si [x
2
y[ > tol, donde
tol es la tolerancia del c alculo (especicada por la constante TOLERANCIA como 10
6
). Calculamos
aproximaciones sucesivas, mientras la diferencia en valor absoluto (funci on fabs()) entre el cuadrado
de x e y no sea menor que la tolerancia del c alculo.
El resultado de ejecutar el programa anterior es:
$ ./newton.exe
Teclee el numero cuya raiz quiere calcular 566
La raiz del numero 566 es 23.7908
31
1.10. Resumen
En este primer captulo hemos presentado los elementos m as b asicos del lenguaje de programaci on
C++ a traves de una serie de programas sencillos. Concretamente, hemos visto que:
1. Un programa de C++ consiste en una serie de sentencias, agrupadas en funciones. Las sentencias
de C++ se escriben en formato libre y terminan en un punto y coma. El lenguaje distingue entre
may usculas y min usculas.
2. Existe una amplia variedad de tipos en C++. Enteros de diferentes tama nos (int, long int,
short int, unsigned int), reales en simple y doble precisi on (float, double), tipos l ogicos o
booleanos (bool), tipos asociados a un car acter (char), etc. En el siguiente captulo deniremos
algunos tipos m as, tales como punteros, arreglos, referencias y estructuras de datos.
3. Una variable es una referencia simb olica que nos permite manipular objetos de tipos determina-
dos. Todas las variables deben declararse antes de asignarse a un valor. En el siguiente captulo
hablaremos del rango de validez de la declaraci on de una variable.
4. As mismo, toda funci on que vaya a utilizarse en un programa debe declararse. En concreto,
las funciones de la biblioteca est andar, Std deben declararse incluyendo en nuestro c odigo los
correspondientes cheros de cabecera, va directivas #include al preprocesador.
5. Hemos presentado los rudimentos de entrada y salida de datos, comentando someramente los
operadores << y >>, as como los ujos de entrada y salida est andar de datos cin y cout(cuya
declaraci on se encuentra en <iostream>), as como los ujos de tipo ifstream y ofstream,
que se utilizan para conectar la entrada y salida de datos con cheros en disco (cuya declaraci on
se encuentra en <fstream>).
6. As mismo, hemos comentado las funciones matem aticas m as importantes de la Std (cuya de-
claraci on se encuentra en <cmath>).
7. Tambien hemos comentado las estructuras de control m as importantes (bucles while, do while
y for, bloques if else), ilustrando su uso con varios ejemplos.
32
Captulo 2
Tipos
2.1. Introducci on
Tras el paseo guiado por el lenguaje C++, dedicamos este breve captulo a repasar con mayor rigor
los tipos fundamentales. Hablamos tambien de la declaraci on y denici on de variables as como de su
alcance (scope).
2.2. Tipos
Los tipos fundamentales en C++ son:
1. El tipo booleano o l ogico (bool)
2. El tipo car acter (char)
3. Tipos asociados a n umeros enteros (por ejemplo int)
4. Tipos asociados a n umeros en coma otante (por ejemplo double)
Adem as el programador puede denir tipos de enumeraci on (enum) para representar conjuntos
especcos de valores.
Comentaremos adem as los tipos:
1. Un tipo void, que se utiliza para indicar la ausencia de informaci on
2. Punteros (tales como int*)
3. Arreglos (tales como char[])
4. Referencias (tales como double&)
5. Estructuras
De los cuales trataremos en el siguiente captulo.
2.2.1. Tipos booleanos
Un tipo booleano, bool, puede tomar dos valores, true o false. El tipo booleano se usa para
expresar el resultado de una operaci on l ogica. Por ejemplo, el resultado de una funci on que comprueba
si se cumple una cierta condici on, tal como:
bool mayorQue (int a, int b) return a > b ;
33
La funci on mayorQue(int a, int b) devuelve el resultado de la operaci on l ogica a >b que es
cierto si a es mayor que b y falso en otro caso.
Por denici on, true toma el valor 1 cuando se convierte a entero y false toma el valor 0. An alo-
gamente, es posible convertir el tipo entero al booleano. Un int distinto de cero se convierte en true
mientras que cero se convierte en false.
2.2.2. Tipo caracter
El tipo car acter, char se usa para representar caracteres individuales, tales como b, 1, n, etc.
Tpicamente un char tiene 8 bits (1 byte). Puede, por tanto, almacenar 2
8
= 256 valores diferentes.
Toda constante tipo char tiene asociado un valor entero. El siguiente programa permite imprimir
el valor int asociado a un char.
/** charToint.cpp
* Descripci on:
* Imprime el valor int de una variable tipo char.
* Ilustra el uso del tipo bool
* Ilustra el uso de la entrada y salida de datos
* bucle while y bloque if() b asico
*/
#include <iostream> // entrada y salida de datos
using namespace std; // usa por defecto el
// espacio de nombres de la librer a est andar
int main()
bool iterando = true; // iterando est a definido en todo main
while (iterando == true)
char c; // alcance de c limitado al bucle while
cout << "Introducir un car acter (t para terminar) ";
cin >> c;
if (c == t) iterando = false;
cout << " El valor de " << c << " es " << int(c) << n";

return 0;

2.2.3. Tipos enteros


El tipo int representa n umeros enteros. El alcance de un entero depende del ordenador. Un int
de 16 bits usa el primero para almacenar el signo y los restantes 15 para construir n umeros enteros
entre 2
0
= 1 y 2
15
1 = 32767, adem as del cero. Un int de 32 bits puede representar hasta 2
31
1 =
2147483647, adem as del cero.
La pr actica totalidad de los ordenadores hoy en da permiten representar los enteros al menos con
32 bits. De ah que, normalmente, el tipo int se reera a un entero de 32 bits. Existen otros dos tipos
relacionados, short int, que normalmente se reere a un entero de 16 bits y long int que suele
referirse a un entero de 64 bits. El tipo unsigned int (tambien llamado size t) representa un entero
positivo (es decir, no necesitamos invertir un bit en almacenar el signo).
34
2.2.4. Tipos en coma otante
Los tipos en coma otante representan n umeros reales. El tipo float (precisi on simple) normalmen-
te utiliza 4 bytes para representar un n umero real. Tpicamente, float ofrece siete dgitos signicativos
y un alcance de 10
38
.
A lo largo del curso utilizaremos m as a menudo el tipo double (real en doble precisi on) que
normalmente usa 8 bytes, ofreciendo hasta 15 dgitos signicativos y un alcance de 10
308
.
2.2.5. Tama nos de los tipos
Como hemos comentado, el tama no de algunos de los tipos (por ejemplo el tipo int) depende de la
implementaci on (el tipo de ordenador, compilador, etc). Por esta raz on, C++ proporciona la funci on
sizeof() que devuelve el tama no de un determinado tipo. El tama no de los objetos en C++ viene
expresado en m ultiplos del tama no de un char, por lo tanto, por denici on, el tama no de un char
(casi universalmente un byte de memoria) es uno.
El siguiente programa imprime los tama nos de algunos tipos fundamentales:
/** sizeOf.cpp
* Descripci on:
* Utiliza la funci on sizeof para imprimir el tama~ no de los diferentes tipos
* Asume que char ocupa 8 bits
*/
#include <iostream> // entrada y salida de datos
using namespace std; // usa por defecto el
// espacio de nombres de la librer a est andar
int main()

char c;
int i;
unsigned int s;
float f;
double d;
cout << "El tama~ no de un char es " << sizeof(c)*8 << " bits n";
cout << "El tama~ no de un int es " << sizeof(i)*8 << " bits n";
cout << "El tama~ no de un size_t es " << sizeof(s)*8 << " bits n";
cout << "El tama~ no de un float es " << sizeof(f)*8 << " bits n";
cout << "El tama~ no de un double es " << sizeof(d)*8 << " bits n";
return 0;

2.2.6. Tipo void


El tipo vaco o void es especial en el sentido de que s olo puede usarse como parte de un tipo m as
complejo. Es decir, podemos denir objetos de tipo puntero a void pero no objetos de tipo void.
En particular, tambien podemos especicar el tipo de una funci on como void para indicar que no
devuelve un tipo.
35
2.2.7. Enumeraciones
Una enumeraci on, tipo enum, contiene un conjunto de valores especicados por el programador.
Una vez denida, una enumeraci on se utiliza como un tipo entero. Por ejemplo:
enum viernes, sabado, domingo;
dene tres constantes enteras, llamadas enumeradores y les asigna valores. Por defecto, los valores se
asignan en orden creciente a partir de 0, esto es viernes == 0, sabado == 1, domingo == 2.
Podemos ponerle nombre a una enumeraci on, por ejemplo:
enum findDeSemana viernes, sabado, domingo;
Notad que cada enumeraci on es un tipo diferente. El tipo de un enumerador es su enumeraci on. Por
lo tanto viernes es de tipo finDeSemana.
Un enumerador puede tomar un valor inicial. El alcance de una enumeraci on abarca todos los
enumeradores, redondeado a la potencia binaria m as cercana menos uno. Por ejemplo:
enum e1 dia, noche; // alcance 0:1
enum e2 claro = 3, oscuro = 9; // alcance 0:15 (2
4
1)
2.3. Declaraciones
Antes de utilizar un nombre (es decir, un identicador) en C++, debemos declararlo, es decir
debemos especicar su tipo, para informar al compilador con que clase de objeto est a asociado. Por
ejemplo:
char c;
string poeta = "Kavafis";
float f = 10.5;
const double pi = 3.1415926535897932385;
double sqrt(double);
A menudo una declaraci on tambien es una denici on, es decir, se asocia un objeto concreto con el
nombre al que se reere. Por ejemplo, la instrucci on char c dene, adem as de declarar, la variable c
(especica que una cierta cantidad de memoria, tpicamente un byte debe ser asignada a c). Igualmente,
la constante pi, el real en simple precisi on f y el string (hablaremos de este tipo m as adelante) poeta
se denen (asign andoles un tipo y un valor inicial) en las sentencias anteriores. La unica sentencia que
es una declaraci on pero no una denici on es double sqrt(double). Es decir, el c odigo que especica
que hace exactamente la funci on sqrt() debe escribirse en alg un otro sitio. Recordad que un tipo
puede declararse tantas veces como se quiera pero s olo puede denirse una vez, de ah que el c odigo:
int i;
int i; // error, i se redefine en esta sentencia
resulte en un error de compilaci on, ya que redene la variable i.
Notad que el valor asignado a una variable no es parte de su denici on. El valor inicial de una
variable puede cambiarse a voluntad:
string poeta = "Kavafis";
...
poeta = "Rilke";
No as una constante. El c odigo:
const double pi = 3.1415926535897932385;
...
pi = 3.141516; // error, el valor de pi es constante
36
resulta en un error de compilaci on.
Es posible declarar varios nombres en una sola declaraci on, por ejemplo:
double x,y;
aunque conviene prestar atenci on cuando se declaran punteros o referencias, por ejemplo:
int* i,j; // int* i, int j, NO int* j;
signica que i es un puntero a int pero j es un int no un puntero a int. Conviene evitar este tipo
de declaraciones, sumamente proclives a error.
2.4. Alcance
Una declaraci on introduce un nombre en un cierto alcance. Es decir, un nombre puede utilizarse
s olo en una parte especca de un programa. Por ejemplo, para un nombre declarado en una funci on
el alcance (scope en ingles) se extiende desde el punto donde se declara hasta el nal del bloque en
que se hace la declaraci on. Un bloque es una secci on del c odigo delimitada por parentesis de llave ().
Decimos que un nombre es global si est a denida fuera de cualquier funci on, clase o espacio de
nombres. El alcance de un nombre global se extiende desde el punto en que se declara hasta el nal
del chero en el que la declaraci on aparece.
Un nombre local (un nombre que se declara en un bloque) puede enmascarar una declaraci on
en un bloque que lo incluya o un nombre global. En otras palabras, un nombre puede redenirse
para referirse a un objeto diferente dentro de un bloque. Al salir del bloque, el nombre recupera su
signicado previo. Por ejemplo:
// Programa alcance.cpp
// Descripcion:
/* Ilustra el uso de variables locales.
*/
#include <iostream> // entrada y salida de datos
using namespace std; // usa por defecto el
// espacio de nombres de la librer a est andar
int main()
float temp ; // Variable cuyo nombre vamos a enmascarar
int a;
int b;
cout << "Este programa demuestra el uso de variables locales n";
cout<<"Introduzca un numero real "; cin >> temp;
cout<<"n La variable que acaba de introducir se llama temp ="
<<temp <<endl;
cout<<"Introduzca dos numeros a y b ";
cin >> a >> b;
if (a < b)
int temp = a; // Este "temp" oculta al "temp" con alcance m as amplio.
cout << "n Dentro del bloque if, temp = "<<temp << endl;
// Cuando el bloque if termina el "temp" local se destruye
else
37
int temp = b; // Otro "temp" oculta al "temp" con alcance m as amplio.
cout << "n Dentro del bloque else, temp = "<<temp << endl;

cout << "n Fuera del bloque ifelse, de nuevo temp = "<<temp << endl;
return 0;

Un nombre global enmascarado en un bloque puede ser referido usando el operador de resoluci on ::.
Por ejemplo:
int x; // nombre global
// inicio del bloque
int x = 1; // enmascara el x global
::x = 2; // asigna 2 al x global
x = 3; // asigna 3 al x local
// fin del bloque, x local fuera de alcance
2.5. Typedef
Una declaraci on precedida por la palabra reservada typedef declara un nuevo nombre para un
tipo dado, en lugar de una nueva variable de ese tipo. Por ejemplo:
typedef unsigned char size_t;
typedef double real;
Es decir, un typedef no es otra cosa que sin onimo para un tipo dado. No conviene abusar de estos
motes, siendo preferible usar los tipos fundamentales siempre que sea posible.
38
Captulo 3
Punteros, arreglos y estructuras
3.1. Introducci on
En este tercer tema continuamos nuestra introducci on elemental al C++, introduciendo nuevos
tipos fundamentales, como punteros, referencias y arreglos. Hablaremos tambien de estructuras de
datos, las cuales pueden representarse en C++ por el tipo struct.
3.2. Punteros
Un puntero es un objeto que reere o apunta a otro objeto. Es decir, contiene la direcci on en la
memoria del ordenador, donde se encuentra el objeto al que apunta. El tipo puntero se forma a partir
de cualquier otro tipo a nadiendo un asterisco al tipo. Por ejemplo:
int* i ; // i es un puntero a entero
double* d; // d es un puntero a doble
Dado un objeto cualquiera, el operador de direcci on, & permite obtener la direcci on en memoria
de este objeto. As, por ejemplo, la denici on:
int i = 3;
Asocia un objeto de tipo int, con valor 3 a la variable i. La operaci on &i, proporciona la direcci on
en la memoria de ese objeto. Podemos ahora denir la variable p como un puntero a int, mediante
la instrucci on int* p; si queremos inicializar esta variable para que apunte precisamente al objeto
asociado con i, basta con iniciarla a la direcci on en la memoria de i, especicada por &i. Es decir:
int* p = &i;
La gura 3.1 ilustra gr acamente el concepto de puntero.
Dado un puntero a un objeto, el operador de indirecci on *, accede el objeto. En la gura 3.1, p
apunta al objeto de tipo int asociado a la variable i, cuyo valor es 3. Por lo tanto *p vale 3.
Que ocurre si ahora escribimos *p = 5;? Puesto que p apunta a i y *p accede al valor de i
cambiar *p es equivalente a cambiar i. En otras palabras, despues de ejecutar la instrucci on *p = 5;
nos encontramos con que i tiene el valor 5.
El siguiente ejemplo ilustra el uso de punteros:
/* Programa puntero.cpp
* Descripcion:
* Ilustra el uso de punteros.
*/
39

int* p int i
int
3
Figura 3.1: El concepto de puntero. i es una variable de tipo int asociada a un objeto entero (tpica-
mente 2 bytes de memoria) cuyo valor es 3. Dicho objeto se encuentra almacenado en una direcci on
de memoria concreta del ordenador. La variable p es de tipo int* esto es, apunta a un objeto de tipo
entero, concretamente a i puesto que se ha inicializado con la direcci on en memoria de i, int* p =
&i.
#include <iostream>
using namespace std;
int main()

double x = 10; // x es una variable de tipo double


cout<<" x = " << x << " direccion de x en memoria = " << &x <<endl;
double* p = &x; // p apunta a la direccion de x, *p accede al valor de x
cout<<" p apunta a x: p = " << p << " *p = " << *p <<endl;
double y = 101; // nueva variable de tipo double
cout<<" y = " << y << " direccion de y en memoria = " << &y <<endl;
p = &y; // p apunta ahora a y
cout<<" p apunta a y: p = " << p << " *p = " << *p <<endl;
*p = 77; // cambiamos el contenido del objeto al que apunta p
cout<<" *p = " << *p << ", y = " << y <<endl;
return 0;

Notad que en el programa anterior usamos:


<<endl;
es decir, escribimos la instrucci on endl (que signica nal de lnea, end-of-line en ingles) por la salida
est andar. La instrucci on es equivalente al car acter de salto de carro. El resultado es:
$ ./puntero.exe
x = 10 direcci on de x en memoria = 0x22feac
p apunta a x: p = 0x22feac *p = 10
y = 101 direcci on de y en memoria = 0x22fe9c
p apunta a y: p = 0x22fe9c *p = 101
*p = 77 y = 77
3.3. Arreglos
Para un tipo dado T, T[d] es del tipo arreglo (array) de elementos de tipo T con dimensi on d.
Por ejemplo:
40
float v[3]; // arreglo de 3 n umeros reales en simple precisi on
double dd[3]; // arreglo de 3 dobles. dd[0], dd[1], dd[2]
char* a[30]; // arreglo de 30 punteros a char, a[0]...a[31]
El n umero de elementos del arreglo debe ser una expresi on constante. Es decir, el c odigo:
int v1[i]; // error la dimensi on del arreglo no es una constante
resulta en un error de compilaci on.
Un arreglo multidimensional se representa como un arreglo de arreglos. Por ejemplo:
int d2[10][20]; // d2 es un arreglo de 10 arreglos de 20 enteros
Un arreglo puede inicializarse mediante una lista de valores, por ejemplo:
int v1[] = 1,2,3,4;
char v2[] = b,c,d;
Cuando se declara un arreglo sin especicar una dimensi on, como en el ejemplo anterior, esta se
calcula contando los elementos de la lista de inicializaci on. Por lo tanto v1 es de tipo int[4] y v2
de tipo char[3]. Cuando se especica la dimensi on del arreglo el n umero de elementos de la lista
de inicializaci on debe coincidir con la dimensi on para que no ocurra un error de compilaci on. Por el
contrario, si la lista contiene menos elementos que la dimensi on del arreglo, los elementos restantes se
asumen cero.
3.4. Punteros y Arreglos
En C++ existe una relaci on muy estrecha entre punteros y arreglos, que ilustraremos a continua-
ci on. Consideremos, por ejemplo, la declaraci on
float x[5];
que asocia x con un arreglo de cinco n umeros. La variable x es de tipo arreglo de float. Por otra
parte, el nombre de un arreglo puede utilizarse en C++ como un puntero a su primer elemento. Por
lo tanto, las siguientes deniciones:
float* p1 = x; // correcto, p1 apunta a x[0] (conversi on impl cita)
float* p2 = &x[0] // correcto p2 apunta a x[0] (primer elemento x)
float* p3 = &x[4] // correcto p3 apunta a x[4] ( ultimo elemento x)
son todas correctas. La relaci on entre puntero y arreglo se ilustra gr acamente en la gura 3.2:
C++ almacena en memoria los elementos de un arreglo de forma consecutiva (es decir si i<j,
&x[i] <&x[j]), de tal manera que podemos apuntar a cada uno de los elementos del arreglo simple-
mente desplazando el puntero x por enteros sucesivos. Puesto que x apunta a x[0], (x+1) apunta
a x[1] y as sucesivamente. Podemos acceder al valor almacenado en x[0],x[1], etc, utilizando el
operador de indirecci on, (*x = x[0]), (*(x+1) = x[1]), etc. En consecuencia, podemos acceder y
manipular los elementos de un arreglo utilizando ndices y utilizando aritmetica de punteros. El si-
guiente ejemplo ilustra estos conceptos:
/* punteroYArreglo.cpp
* Descripcion:
* Ilustra el uso de punteros y arreglos.
*/
#include <iostream>
using namespace std;
41

float* p1
float* p2
float* p3
float x[0] x[1] x[2] x[3] x[4]
Figura 3.2: Relaci on entre puntero y arreglo. x es de tipo arreglo de float. El nombre x puede ser
utilizado como un puntero a x[0]. En la gura, el puntero p1 (float* p1 = x; ) apunta a x[0]
igual que el puntero p2 (float* p2 = &x[0];). El puntero p3 apunta al ultimo elemento del arreglo,
(float* p3 = &x[4];).
int main()

double x[5]=10,11,12,13,14 ; // x es de tipo arreglo de double


for(int i = 0; i<5; i++)
cout<<"x["<<i<<"] = " << x[i] // contenido de x
<<" direccion de x["<<i<<"] en memoria = " << &x[i] <<endl;

cout<<"El nombre x puede usarse como un puntero a &x[0] n";


for(int i = 0; i<5; i++)
cout<<"x +"<<i<<" = " << x+i
<<" *(x +"<<i<<") = " << *(x+i) <<endl;

return 0;

Al ejecutar este programa obtenemos:


$ ./punteroYArreglo.exe
x[0] = 10 direcci on de x[0] en memoria = 0x22fe84
x[1] = 11 direcci on de x[1] en memoria = 0x22fe8c
x[2] = 12 direcci on de x[2] en memoria = 0x22fe94
x[3] = 13 direcci on de x[3] en memoria = 0x22fe9c
x[4] = 14 direcci on de x[4] en memoria = 0x22fea4
El nombre x puede usarse como un puntero a &x[0]
x +0 = 0x22fe84 *(x +0) = 10
x +1 = 0x22fe8c *(x +1) = 11
x +2 = 0x22fe94 *(x +2) = 12
x +3 = 0x22fe9c *(x +3) = 13
x +4 = 0x22fea4 *(x +4) = 14
42
3.5. Determinaci on dinamica de la dimensi on de un arreglo
Los arreglos que hemos comentado en los ejemplos anteriores son de dimensi on ja. En C++ existe
un mecanismo para asignar una cierta cantidad de memoria, din amicamente, a un arreglo, pudiendo
recuperarse cuando el arreglo ya no es necesario. Para ello se utilizan los operadores new y delete,
cuyo uso ilustramos con el siguiente ejemplo:
// arregloDinamico.cpp
/* Descripcion:
* Ilustra la creaci on de un arreglo determinando su
* dimensi on din amicamente.
*/
#include <iostream>
using namespace std;
int main()
cout<<"El siguiente codigo crea dinamicamente un arreglo"
<<"de numeros enteros intDinn";
cout<<"Introduzca la dimension del arreglo intDin ";
int n; cin >> n; // leemos la dimensi on del arreglo
// Creamos din amicamente el arreglo intDin[n]. Para ello utilizamos
// la relaci on entre punteros y arreglos que nos permite definir
// intDin como un puntero a int
int* intDin = new int[n];
// El operador new crea un arreglo de n objetos de tipo int y devuelve
// un puntero al primer elemento del arreglo, con el que inicializamos
// intDin.
cout<<"n intDin ha sido creado dinamicamente n";
for(int i = 0; i < n; i++)
cout<<"Introduzca el valor de intDin["<<i<<"] "; cin>> intDin[i];

cout<<" Escribimos ahora intDin usando indices y punteros n";


for(int i = 0; i < n; i++)
cout<<"intDin["<<i<<"] =" <<intDin[i];
cout<<" *(intDin +"<<i<<") =" <<*(intDin + i) << endl;

delete [] intDin; // Libera la memoria


return 0;

43
El operador new crea un arreglo (de n objetos de tipo int en el ejemplo anterior) y devuelve un
puntero al primer elemento del arreglo. Para denir din amicamente un objeto del mismo tipo devuelto
por new basta con asignar una variable tipo puntero al tipo creado por new (intDin es de tipo int* en
el ejemplo). Cuando creamos un arreglo din amicamente, estamos utilizando un porci on de la memoria
del ordenador en el que nuestro programa se est a ejecutando. La memoria se obtiene de la reservade
memoria activa (heap) de la que dispone el ordenador al invocar el operador new y no se libera hasta
que invocamos el operador delete (en el ejemplo delete[] para indicarle al compilador que se trata
de un arreglo).
El resultado de ejecutar el programa anterior es:
%
$ ./arregloDinamico.exe El siguiente c odigo crea din amicamente un
arreglo de numeros enteros intDin Introduzca la dimensi on del
arreglo intDin 4
intDin ha sido creado din amicamente
Introduzca el valor de intDin[0] 10
Introduzca el valor de intDin[1] 33
Introduzca el valor de intDin[2] 55
Introduzca el valor de intDin[3] 77
Escribimos ahora intDin usando indices y punteros
intDin[0] =10 *(intDin +0) =10
intDin[1] =33 *(intDin +1) =33
intDin[2] =55 *(intDin +2) =55
intDin[3] =77 *(intDin +3) =77
3.6. Cadenas literales
Una cadena (string) literal es una secuencia de caracteres encerrada entre dobles comillas:
"Pues la belleza no es sino el principio del terror"
Una cadena literal termina con el car acter nulo 0. De ah que, el tama no de, por ejemplo, la
cadena Rilke sea:
sizeof("Rilke") == 6;
es decir, el n umero de caracteres visibles m as uno, el nulo.
El tipo de una cadena literal es arreglo de caracteres constante. Es decir, Rilke es de tipo
const char[6] (la dimensi on incluye el car acter nulo).
Una cadena literal puede asignarse a un puntero a car acter, char*, lo que permite escribir cadenas
arbitrariamente largas por ejemplo:
char* escritorRuso = "Leo Tolstoi famoso autor de Ana Karenina";
Si la cadena es demasiado larga puede dividirse dejando un blanco en el medio, tal como en el
siguiente ejemplo:
char* escritorRuso = "Leo Tolstoi famoso autor de Ana Karenina,"
" Guerra y Paz, y otras muchas obras de renombre";
utilizando el hecho de que el compilador concatena cadenas adyacentes.
En C++, la Std dene un tipo string, m as avanzado que el tipo puntero a char. A lo largo del
curso iremos viendo ejemplos de uso de este tipo.
44
3.7. Referencias
Una referencia es un nombre alternativo o alias para un objeto dado. Por ejemplo, las declaraciones:
int i = 3; // Variable de tipo int
int& ir = i; // Referencia a la variable i
Declaran una variable, i, y una referencia a ella, ir. int& se lee referencia a int. Ambas variables
se reeren al mismo objeto.
Para garantizar que una referencia es un nombre asociado a un objeto especco, es obligatorio
inicializar la referencia. Por ejemplo:
int i = 1;
int& r1 = i; //Correcto, r1 se inicializa correctamente
int& r2; //Error, r2 no se ha inicializado
Tanto un puntero como una referencia nos permiten acceder a un objeto. Sin el objeto al que apunta
el puntero puede cambiarse si as lo deseamos y para acceder al valor del objeto necesitamos usar
el operador de indirecci on. En cambio una referencia apunta a un objeto jo (para lo cual debemos
inicializarla obligatoriamente a dicho objeto) y no requiere el uso del operador de indirecci on para
proporcionar el valor del objeto. El siguiente ejemplo ilustra estos conceptos:
/* Programa referencia.cpp
* Descripcion:
* Ilustra el uso de referencias y punteros
*/
#include <iostream>
using namespace std;
int main()
double x = 10; // x es una variable de tipo double
double& xref = x; // xref es una referencia a double inicializada con x
double* xp = &x; // xp es un puntero a doble inicializado con &x
cout<<"Variable x = " << x << endl
<<"Referencia a x (xref) = " << xref << endl
<<"Puntero a x (xp) = " << xp <<endl;
cout<<"Direcci on de la variable x = " << &x << endl
<<"Direcci on v a la referencia a x = " << &xref << endl
<<"Valor de x v a *(xp) = " << *xp <<endl;
// Cambiamos el valor de x v a el puntero
*xp = 100;
cout<<"xp = " << xp << " *xp = " << *xp << " x = " << x <<endl ;
// Cambiamos el valor de x v a la referencia
xref = 500;
cout<<"xref = " << xref << " x = " << x <<endl ;
return 0;

Cuya ejecuci on, arroja:


45
$ ./referencia.exe
Variable x = 10
Referencia a x (xref) = 10
Puntero a x (xp) = 0x22feac
Direcci on de la variable x = 0x22feac
Direcci on v a la referencia a x = 0x22feac
Valor de x v a *(xp) = 10
xp = 0x22feac *xp = 100 x = 100
xref = 500 x = 500
3.8. Estructuras
Podemos denir un arreglo como un agregado de elementos del mismo tipo. An alogamente, una
estructura, (tipo struct) es un agregado de elementos de tipo arbitrario. Por ejemplo:
struct agenda
char* nombre; // "Konstantino Kavafis"
char* calle; // "camino de Itaca"
int numero ; // 2
char* ciudad; // "Alejandr a"
int telefono; // 29051933
char* e-mail; // "Constantino@parnaso.heaven"
;
El c odigo anterior dene un nuevo tipo, en el que se almacena la informaci on tpica de una entrada
de agenda. Notad que los elementos que componen el agregado est an encerrados entre parentesis de
llaves (terminados en ;).
Una variable tipo agenda puede declararse exactamente igual que otras variables. Se puede acceder
a los miembros de agregado mediante el operador . (punto). Por ejemplo, el c odigo:
agenda constantino; // declara que la variable constantino es de tipo agenda
constantino.nombre = "Konstantino Kavafis";
constantino.calle = "camino de Itaca";
...
constantino.telefono = 29051933;
declara la variable constantino como de tipo agenda y asigna valores a cada uno de los campos del
agregado.
Una estructura puede inicializarse de manera an aloga a un arreglo, por ejemplo:
agenda constantino = "Konstantino Kavafis",
"Calle de Itaca",
2,
"Alejandr a",
29051933,
"Constantino@parnaso.heaven";
Si en lugar de una estructura estamos manejando un puntero a una estructura, accedemos a sus
miembros mediante el operador ->. Por ejemplo:
agenda* cavafis = constantino; // cavafis es un puntero a agenda
cout << "El nombre del gran poeta griego es" << cavafis->nombre << endl;
es decir, cavafis->nombre es equivalente a (*cavafis).nombre).
46
Las estructuras pueden manejarse, esencialmente, como cualquier otro tipo en C++. Es decir
pueden asignarse, pasarse como argumentos a funciones y ser devueltas por estas. Sin embargo algunas
operaciones (tales como == y <=) no est an denidas por defecto para estructuras, aunque pueden
denirse por el programador.
El nombre de un tipo en C++ puede utilizarse inmediatamente despues de que este aparezca por
primera vez, no siendo necesario que se haya completado la declaraci on del objeto. Por ejemplo:
struct agenda; // declara la estructura agenda
struct listaDeAgendas
agenda* entrada;
; // utiliza la estructura agenda
...
struct agenda
char* nombre;
...
; //completa la declaraci on de agenda
Por ultimo conviene recordad que dos estructuras diferentes son siempre dos tipos diferentes, incluso
si tienen los mismos miembros, es decir:
struct S1int a;; // define la estructura S1
struct S2int a;; // define S2
Si ahora hacemos:
S1 x; // x es de tipo S1
S2 y = x; // error, y es de tipo S2
El siguiente programa muestra el uso elemental de una estructura:
/*
* iarreglo.cpp
* Usa el tipo struct para definir un arreglo entero
* (tipo iarray) "mejorado" con respecto al tipo b asico en C++
* /
#include <iostream>
#include <cstddef> // declaraci on de size_t
using namespace std;
/* Comentarios:
* size_t es equivalente a unsigned int
* iarray se define como un nombre global, fuera
* de todos los bloques en el fichero iarreglo.cpp. Por lo
* tanto su rango es todo el fichero */
struct iarray // definimos un arreglo tipo int mejorado
int* array; // puntero a int que contiene los elementos del arreglo
size_t dim; // n umero de elementos o dimensi on del arreglo
;
int main()
iarray p; // declara p como de tipo iarray
// Lee la dimensi on del arreglo
cout << "Introduzca la dimensi on del arreglo "; cin >>p.dim;
47
// reserva el espacio correspondiente
p.array = new int[p.dim];
// Lee los elementos, usando algebra de punteros:
cout << "Introduzca los elementos del arreglo" << endl;
for (int i = 0; i < p.dim; i++)
cout << "? "; cin >> *(p.array + i);

//Escribe los elementos usando ndices


cout << "Los elementos del arreglo son:" << endl;
for (int i = 0; i < p.dim; i++)
cout << p.array[i] << endl;

La ejecuci on del programa, arroja:


$ ./iarreglo.exe
Introduzca la dimensi on del arreglo 5
Introduzca los elementos del arreglo
? 1
? 33
? 22
? 899
? 6
Los elementos del arreglo son:
1
33
22
899
6
48
Captulo 4
Funciones
4.1. Introducci on
El uso de funciones es universal en todos los lenguajes de alto nivel, como una herramienta que
permite agrupar tareas bien denidas (tpicamente una funci on codica un algoritmo o utilidad que
deseamos usar repetidamente) en un bloque de c odigo al que asociamos un nombre. En C++, con-
cretamente, una funci on se caracteriza, adem as de por su nombre, por los argumentos que toma (los
cuales especican los objetos que va a manipular) y el tipo que devuelve.
4.2. Declaraci on y Denici on de una Funci on
C++ exige que se declaren todas las funciones que un determinado programa vaya a utilizar. La
declaraci on de una funci on especica el nombre de la funci on, el n umero y tipo de argumentos que
toma y el tipo de argumento que devuelve. Es decir:
tipo-de-vuelta nombre-de-la-funci on(lista-de-argumentos);
La denici on de una funci on a nade el cuerpo de la funci on:
tipo-de-vuelta nombre-de-la-funci on(lista-de-argumentos)
cuerpo de la funci on
(lista de sentencias separadas por ;)

Veamos un ejemplo:
/* coulomb.cpp
* Ilustra la declaraci on y definici on de una
* funci on, fuerzaDeCoulomb, que calcular la fuerza que act ua entre
* dos cargas puntuales situadas a una distancia r.
*/
#include <iostream>
using namespace std;
// Declaraci on de la funci on:
// tipo devuelto nombre argumentos
double fuerzaDeCoulomb (double q1, double q2, double r);
49
// Definci on
//---------------------------
// Calcula la fuerza de Coulomb actuando entre dos cargas puntuales
// q1 y q2 est an a una distancia r. Las unidades son MKS.
double fuerzaDeCoulomb(double q1, double q2, double r)
const double k = 8.9875e9; // nt-m**2/coul**2
return k * q1 * q2 / (r * r);

int main()

// El unico prop osito de este main es llamar a la funci on


// CoulombsLaw
cout << " Este programa calcula la fuerza entre dos cargas puntuales ";
cout << " situadas a una distancia r. Las unidades son MKS ";
cout << " Introduzca el valor de las cargas q1 y q2 " ;
double q1,q2;
cin >> q1 >> q2;
cout << " Introduzca el valor de la distancia r " ;
double r;
cin >> r ;
double force = fuerzaDeCoulomb(q1,q2,r);
cout << " La fuerza de Coulomb es " << force << endl;
return 0;

En general, un programa de C++ se suele descomponer en numerosas funciones que realizan la


mayor parte de las tareas. La funci on main, se limita a llamar a algunas de estas funciones (que a su
vez pueden llamar a otras). La organizaci on de los cheros tambien suele ser modular, agrup andose
la denici on de una o varias funciones en cheros de c odigo (extensi on .cpp) y sus declaraciones en
cheros de cabecera (extensi on .h). Cada uno de los cheros .cpp incluye las cabeceras necesarias y
se compila por separado, enlaz andose nalmente los distintos objetos en un s olo ejecutable.
4.3. Argumentos
Los argumentos que aparecen en la declaraci on de una funci on se llaman argumentos formales,
mientras que los argumentos que pasamos a una funci on al llamarla se llaman argumentos especcos.
Cada argumento formal es una variable local de la funci on que se inicializa con el argumento especco
correspondiente. Por ejemplo, el c odigo:
void f(int i, float x)
50
i = 100;
x = 101.0;

Declara dos argumentos formales: i y x que son variables locales a la funci on f. Si ahora invocamos
a esta de la siguiente manera:
int j = 1;
float y = 2;
// ...
f(j, y);
El resultado es equivalente a:
int i = j; // inicializa i con valor 1
float x = y; // inicializa x con valor 2
//
i = 100; // asigna 100 a i
x = 101.0; // asigna 101 a x
Es decir: al llamar a la funci on f los argumentos formales (i,x) se inicializan con los argumentos
especcos correspondientes (j,y). M as adelante, dentro del cuerpo de la funci on, es posible cambiar
los valores de i y x como se hace en el ejemplo, asign andoles otros valores.
Sin embargo, las asignaciones a i y x alteran las variables dentro de la funci on f, pero no tienen
efecto alguno en los valores especcos de los argumentos j e y en la funci on que llama a f. Esto
es as porque los argumentos formales son variables locales de la funci on y por lo tanto su alcance
est a limitado a esta. A este mecanismo de paso de argumentos se le denomina por valor.
En consecuencia, si una funci on F, llama a otra funci on f con ciertos argumentos, el mecanismo de
paso por valor permite inicializar los argumentos formales (por tanto ciertas variables locales) de f a
partir del valor de ciertas variables en F, pero no es posible cambiar el valor de dichas variables en F.
Claramente, esto supone una limitaci on, puesto que muy a menudo el programador desea utilizar f,
precisamente para cambiar el valor de ciertas variables en F.
Una soluci on consiste en pasar un puntero a la variable que queremos cambiar. Considerad la
siguiente funci on:
void f(float* x)
*x = 999; // asigna 999 a la variable a la que apunta x
Si ahora llamamos a f desde otra funci on F, as:
float y = 10; // asigna el valor 10 a la variable y
...
f(&y); // inicializa el argumento formal de f con
// la direcci on de y en memoria
el efecto, es inicializar el argumento formal de f, (float* x) con el objeto que hemos pasado en la
llamada, esto es &y. Por tanto, en f:
float* x = &y; // x apunta a y
...
*x = 999; // cambia el valor de la variable a la que apunta x
...

Cuando volvemos a F, el valor de y ha cambiado, puesto que la llamada a f ha asociado y con un


puntero. En f hemos cambiado el valor de la variable a la que apunta el puntero (cambiar *x es lo
51
mismo que cambiar y). El alcance de x est a limitado a f, pero cuando volvemos a F el valor de y ha
sido modicado a traves de esta.
Que ocurre cuando pasamos un arreglo? Ya se coment o que el compilador de C++ interpreta el
nombre de un arreglo (por ejemplo v[]) como un puntero al primer elemento de ese arreglo (por tanto
v apunta a v[0]). Consideremos de nuevo la funci on f, ligeramente modicada:
void f(float* x)
...
*x = 999;
*(x+1) = 1000;
*(x+2) = 1001;
...

Y la funci on F, que la llama:


float y[] = 1.0,2.0,3.0;
...
f(y);
...
Notad, en primer lugar que la llamada es f(y), no f(&y). Puesto que y es un arreglo, esto es correcto,
ya que el nombre de un arreglo es equivalente a la direcci on en memoria del primer elemento del
arreglo. En f, el puntero a float x, se inicializa con el valor de dicha direcci on:
float* x = &y[0]; // x apunta a y[0]
Y por lo tanto, las asignaciones:
*x = 999; // cambia el valor de y[0]
*(x+1) = 1000; // cambia el valor de y[1]
*(x+2) = 1001; // cambia el valor de y[2]
cambian el valor del arreglo y. Finalmente, notad que podramos haber utilizado la notaci on alterna-
tiva:
void f(float x[]) // equivalente a float* x
...
x[0] = 999; // equivalente a *x
x[1] = 1000; // equivalente a *(x+1)
x[1] = 1001; // equivalente a *(x+2)
...

Es decir, cuando pasamos un arreglo a f, cualquier asignaci on que hagamos en f altera el valor del
arreglo en F.
En C++, existe un mecanismo alternativo al uso de punteros para alterar el valor de una varia-
ble mediante la llamada a una funci on. Dicho mecanismo involucra el uso de un tipo que ya se ha
comentado brevemente, la referencia a un objeto.
Consideremos el c odigo:
double a = 100; // a es un doble con valor inicial 100
double& b = a; // b es una referencia al doble a
El nombre b, como vimos, es un sin onimo para referenciar el objeto asociado a la variable a. La
asignaci on:
52
b = 50; // cambia el valor del objeto al que b refiere, esto es a
cambia el valor del objeto al que b se reere, esto es a. Si escribimos la funci on f como:
void f(double& x)
...
x = 101; // x es una referencia a cierto objeto
// al que asignamos el valor 101
...

y, de nuevo, llamamos a f desde F:


double y = 10; // y es un doble con valor inicial 10
...
f(y); // pasamos a f el nombre y
el efecto es:
double& x = y; // x es una referencia asociada a y
...
x = 101; // x cambia el valor de y a 101
...
Es decir, asignamos la referencia x a la variable y. Cambiar x no es m as que un sin onimo para cambiar
y, que queda, consecuentemente, modicada de vuelta a la funci on F.
El siguiente ejemplo ilustra todos estos conceptos:
/* argumentos.cpp
* Ilustra el mecanismo del paso de argumentos
* a funciones en un programa de C++!
*/
#include <iostream>
using namespace std;
// A continuaci on declaramos y definimos la
// funci on f
void f(int i, float* x, float* a, double b[], double& c)

i = 100; // i es local a f
*x = 101.0; // cambia el valor de la variable a la que apunta
*a = 0.0; *(a + 1) = 10.0 ; *(a + 2) = -22.0; //ditto
b[0] = 10.0; b[1] = 100.0 ; b[2] = -220.0; // ditto
c = 999; // cambia el valor de la variable a que refiere

int main()

int j = 1;
float s = 2;
float y[] = 3.0, 4.0, 5.0;
double z[] = 30.0, 40.0, 50.0;
53
double r = 99;
// Antes de llamar a la funci on f()!
cout << " Antes de llamar a la funcion f()"<<endl;
cout << " j = " << j << endl;
cout << " s = " << s << endl;
cout << " y[] = " <<y[0] << " " << y[1] << " " << y[2] << endl;
cout << " z[] = " <<z[0] << " " << z[1] << " " << z[2] << endl;
cout << " r = " << r << endl;
// Llamamos a f
f(j, &s, y, z, r);
// Despu es de llamar a la funci on f()
cout << " Despu es de llamar a la funcion f()"<<endl;
cout << " j = " << j << endl;
cout << " s = " << s << endl;
cout << " y[] = " <<y[0] << " " << y[1] << " " << y[2] << endl;
cout << " z[] = " <<z[0] << " " << z[1] << " " << z[2] << endl;
cout << " r = " << r << endl;
return 0;

El resultado de ejecutar el programa es:


$ ./argumentos.exe
Antes de llamar a la funci on f()
j = 1
s = 2
y[] = 3 4 5
z[] = 30 40 50
r = 99
Despu es de llamar a la funci on f()
j = 1
s = 101
y[] = 0 10 -22
z[] = 10 100 -220
r = 999
4.4. Punteros a Funciones
An alogamente a como hemos denido un puntero a un tipo cualquiera (tal como un double o
un char) podemos denir un puntero a una funci on, como un objeto que contiene la direcci on en la
memoria donde reside esta. Consideremos, por ejemplo, la funci on:
double senoPorCoseno (double x) // devuelve el producto sin(x)*cos(x)
return sin(x)*cos(x);

que toma como argumento un double y devuelve as mismo un double. Podemos denir un puntero
a una funci on de este tipo mediante la instrucci on:
double (*spc) (double);
54
que nos indica que spc es un puntero a una funci on que toma un double en el argumento y
devuelve un double. El prototipo (esto es, el tipo y n umero de argumentos que toma y el argumento
que devuelve) del puntero a la funci on es identico al de la funci on a la que apunta. Escribimos double
(*spc) (double) y no double * spc (double) para evitar la confusi on entre declarar un puntero
a una funci on (que devuelve un argumento de tipo double) y declarar una funci on que devuelve un
puntero a double. Escribiendo entre parentesis el nombre de la funci on, precedido del operador *,
estamos especicando que nos referimos al puntero a una funci on.
A continuaci on podemos inicializar el puntero con la direcci on de la funci on:
spc = &senoPorCoseno;
Ocurre, sin embargo, que al igual que en el caso de los arreglos, el nombre de una funci on es equivalente
a la direcci on de la funci on en memoria. Por tanto, para inicializar el puntero basta con escribir:
spc = senoPorCoseno;
Una vez inicializado podemos utilizar el puntero equivalentemente al nombre de la funci on. Es decir,
en lugar de:
double theta = PI/2; // angulo en radianes
double x = senoPorCoseno(theta); // x = seno(pi/2)*cos(pi/2)
Podramos escribir:
double theta = PI/2; // angulo en radianes
double x = (*spc)(theta); // spc apunta a la funci on (*spc) es su valor
La notaci on (*spc)(theta) es, sin duda, algo engorrosa, pero, afortunadamente, C++ permite
una notaci on compacta en la que se suprime el operador de indirecci on, que el compilador asume
implcitamente, es decir, en la pr actica, escribimos:
double theta = PI/2; // angulo en radianes
double x = spc(theta); // notaci on compacta, spc = (*spc) en este caso
Veamos el ejemplo completo:
// punteroAFuncion.cpp
// Ilustra el mecanismo de punteros a funciones
#include <iostream>
#include <cmath>
using namespace std;
// Esta funci on devuelve seno(x)*cos(x)
double senoPorCoseno(double x)
return sin(x)*cos(x); // necesitan <cmath>

int main()

const double PI = 3.14159265358979323846264338328 ;


// Llamamos primero a la funci on
cout << " x = " << PI/4 << " sin(x)*cos(x) = "
<< senoPorCoseno(PI/4) << endl;
55
// Declaramos un puntero a la funci on
double (*spc) (double);
// Lo inicializamos con la direcci on en memoria de la funci on
spc = senoPorCoseno;
//Llamamos ahora al puntero en lugar de a la funci on
cout << " x = " << PI/6 << " sin(x)*cos(x) = "
<< (*spc)(PI/6) << endl;
//Llamamos ahora al puntero en notaci on compacta
cout << " x = " << PI/7 << " sin(x)*cos(x) = "
<< spc(PI/7) << endl;
return 0;

Al ejecutar el programa, obtenemos:


$ ./punteroAFuncion.exe
x = 0.785398 sin(x)*cos(x) = 0.5
x = 0.523599 sin(x)*cos(x) = 0.433013
x = 0.448799 sin(x)*cos(x) = 0.390916
Podemos utilizar el puntero a una funci on para denir sin onimos a ciertos tipos de funcines,
utilizando la instrucci on typedef. Por ejemplo, podemos denir un tipo de funci on de una dimensi on
que toma un argumento double y devuelve uno double de la siguiente manera:
typedef double (*func) (double) ;
De tal manera que la instrucci on func spc = senoPorCoseno; dene spc como de tipo func (esto
es una funci on que toma un double en el argumento y devuelve un double) y le asigna el valor de
la funci on senoPorCoseno. Esto resulta util para pasar a una funci on el puntero a otra funci on. Por
ejemplo:
// funcToFunc.cpp
// Ilustra el mecanismo pasar un puntero a una funci on a otra funci on
//-----------------------------------------------
#include <iostream>
#include <cmath>
using namespace std;
// definimos el tipo func como un puntero a una funci on
// que toma un argumento double y devuelve un double
typedef double (*func) (double);
// definci on de la funci on senoPorCoseno
double senoPorCoseno(double x)
return sin(x)*cos(x); // necesitan <cmath>
56

// Esta funci on toma un puntero a una funci on


// y devuelve el valor de la funci on multiplicado por
// la escala y
double escala(func f, double x, double y)
return y*f(x); // Escala en y el valor de f(x)

int main()

const double PI = 3.14159265358979323846264338328 ;


double y = 10.0 ; // escala
// Llamamos primero a la funci on senoPorCoseno
cout << " x = " << PI/4 << " sin(x)*cos(x) = "
<< senoPorCoseno(PI/4) << endl;
// Y ahora a la funci on escala
cout << " x = " << PI/4 << " escala y = " << y
<< "y*sin(x)*cos(x)= " << escala(senoPorCoseno,PI/4, y) << endl;
return 0;

La funci on escala(func f, double x, double y) toma como argumento el tipo func, que es
un sin onimo de puntero a una funci on que toma un double en el argumento y devuelve un double.
Puesto que la funci on double senoPorCoseno (double x) es precisamente de ese tipo, podemos
pasarla a escala, como en el ejemplo:
cout << " x = " << PI/4 << " escala y = " << y
<< "y*sin(x)*cos(x)= " << escala(senoPorCoseno,PI/4, y) << endl;
El resultado de ejecutar el programa es:
$ ./funcToFunc.exe
x = 0.785398 sin(x)*cos(x) = 0.5
x = 0.785398 escala y = 10y*sin(x)*cos(x)= 5
57
58
Captulo 5
Soluci on de ecuaciones no lineales
5.1. Introducci on
En este captulo abordamos la soluci on numerica de ecuaciones no lineales. El problema es equiva-
lente al de encontrar las races de una funci on en un determinado intervalo, puesto que toda ecuaci on
puede representarse como:
f(x) = 0
Donde la funci on f(x), no es, en general, lineal. Por ejemplo, el problema de encontrar las soluciones
de la ecuaci on:
x = cos x (5.1)
es equivalente al de encontrar las races o ceros de la funci on f(x) = x cos x, tal como se ilustra en
la gura 5.1.
-8
-6
-4
-2
0
2
4
6
-6 -4 -2 0 2 4 6
f(x) = x-cos x
Figura 5.1: La funci on x cos x en el intervalo [2, 2].
Dada una funci on unidimensional arbitraria no es posible, en general, dise nar un algoritmo que
determine todas sus races. Por el contrario, si sabemos que una raz existe en un determinado intervalo
[a, b], podemos siempre encontrar dicha raz. En consecuencia, el primer paso en la determinaci on
numerica de una raz consiste en acotarla. Dado un intervalo [a, b], el teorema del valor medio asegura
que si f(a) f(b) < 0 entonces existe al menos una raz c, tal que f(c) = 0. As, en la gura 5.1,
f(2) f(2) < 0, lo que garantiza que existe una raz en el intervalo. El teorema contrario no es
cierto. Si f(a) f(b) > 0, puede darse el caso de que:
59
1. No exista raz en el intervalo.
2. Exista una raz.
3. Exista m as de una raz.
La gura 5.2 ilustra cuatro casos diferentes. La par abola y = x
2
(gura 5.2:a) corta exactamente el eje
de abcisas (sin embargo f(a) f(b) > 0, para cualquier valor de a, b). La par abola y = x
2
+ 5 (gura
5.2:b) no corta el eje de abcisas,mientras que la par abola y = x
2
10x 10 (gura 5.2:c) corta al eje
dos veces (en ambos casos f(10) f(10) > 0). Finalmente, la par abola y = x
2
20x 10 (gura
5.2:d) corta el cero una vez (y f(10) f(10) < 0).
0
10
20
30
40
50
60
70
80
90
100
-10 -5 0 5 10
a) f(x) = x**2
0
10
20
30
40
50
60
70
80
90
100
110
-10 -5 0 5 10
b) f(x) = x**2+5
-50
0
50
100
150
200
250
-10 -5 0 5 10
c) f(x) = x**2-10x+10
-100
-50
0
50
100
150
200
250
300
350
-10 -5 0 5 10
d) f(x) = x**2-20x+10
Figura 5.2: Cuatro par abolas ilustrando diferentes posibilidades: a) corta el eje de abcisas exactamente;
b) no lo corta nunca, c) lo corta dos veces y d) lo corta una vez. En los tres primeros casos f(10)
f(10) > 0, en el el ultimo f(10)f(10) < 0 garantizando, por el teorema del valor medio, la existencia
de al menos una raz.
Finalmente, conviene precisar que el hecho de que f(a) f(b) < 0 para un cierto intervalo [a, b] no
garantiza la existencia de una sola raz, como se ilustra en la gura 5.3 que representa la funci on seno
entre 3 y 3, f(3) f(3) < 0 donde encontramos cinco races.
En este captulo describiremos tres metodos numericos para encontrar una raz que sabemos acota-
da en un cierto intervalo. El primero de estos, bisecci on, se limita a dividir el intervalo repetidamente,
60
-1
-0.8
-0.6
-0.4
-0.2
0
0.2
0.4
0.6
0.8
1
-8 -6 -4 -2 0 2 4 6 8
f(x) = sin(x)
Figura 5.3: La funci on seno(x) en el intervalo [3, 3].
hasta que la anchura de este sea menor que la precisi on requerida. El algoritmo converge siempre,
pero es relativamente lento. Regula falsi consigue una mayor velocidad de convergencia para funciones
suaves, tomando como raz aproximada la intersecci on de la recta que pasa por los extremos del in-
tervalo y sus im agenes con el eje de abcisas. A un m as r apido es el algoritmo de Newton-Raphson, que
requiere el conocimiento de la derivada de la funci on para calcular una nueva aproximaci on a partir
de la intersecci on de la tangente a la curva en la raz aproximada, con el eje de abcisas.
5.2. Metodo de Bisecci on
Consideremos una funci on tal como la representada en la gura 5.1 que tiene una raz acotada en
un determinado intervalo [a, b]. El metodo de bisecci on comienza por tomar, como raz aproximada,
el punto medio:
c =
a + b
2
A continuaci on:
1. Si f(a) f(c) < 0 escoge como nuevo intervalo [a, c]
2. Si f(c)f(b) < 0 escoge como nuevo intervalo [c, b]
Una vez establecido el nuevo intervalo se repite el procedimiento, hasta alcanzar la precisi on re-
querida por el usuario. Dicha precisi on puede ser absoluta o relativa. La condici on de convergencia en
el primer caso es:
[b a[ <
abs
(5.2)
Es decir, requerimos que la anchura del intervalo sea menor que la precisi on especicada. El problema
de esta condici on es que no toma en cuenta el valor concreto de la raz. Claramente no es lo mismo
alcanzar una precisi on absoluta de 10
6
cuando la raz est a cercana al cero, que cuando vale, digamos,
10
6
. Resulta por tanto, preferible, especicar una condici on de convergencia relativa:
[b a[ <
rel
[[ (5.3)
Donde es la raz. De ah que:

abs
=
rel
[[
61
Es decir, utilizamos el valor absoluto de la raz como escala de la precisi on que queremos alcanzar.
Por otra parte, la ecuaci on 5.3 presenta dos problemas. El primero es que no conocemos el valor
exacto de la raz y por lo tanto tenemos que sustituir por una aproximaci on a esta. En el caso que
nos ocupa, para cada iteraci on, podemos aproximar la raz por el centro del intervalo:
[[
[b + a[
2
Sustituyendo en 5.2:
[b a[ <
rel
[b + a[
2
(5.4)
El segundo problema es que hacer si el intervalo inicial es simetrico alrededor de cero, o, m as gene-
ralmente, c omo especicar la precisi on relativa cuando la raz es igual o est a muy cercana al cero. En
este caso, podemos modicar la condici on de convergencia de tal manera que la precisi on relativa mul-
tiplique la anchura del intervalo (es decir, escogemos [b a[ como escala de la precisi on). La ecuaci on
5.4 se transforma entonces en:
[b a[ <
rel
MAXIMO(
[b + a[
2
, [b a[) (5.5)
Si la raz est a acotada en el intervalo inicial, el metodo de bisecci on, converge siempre. Sin embargo
converge lentamente. Tras cada iteraci on, los lmites entre los cuales est a contenida la raz disminuyen
en un factor dos. Es decir, si tras la enesima iteraci on la raz est a en el intervalo de tama no
n
, entonces
tras la siguiente iteraci on estar a contenida en el intervalo:

n+1
=

n
2
(5.6)
Supongamos que nuestro intervalo de partida es
0
y queremos encontrar la raz con tolerancia
absoluta . Los intervalos sucesivos ser an:

1
=
0
/2

2
=
1
/2 =
0
/2
2
...

n
=
0
/2
n
Igualando el tama no del intervalo con la tolerancia deseada:

n
=

0
2
n
=
obtenemos:
n = log
2
_

_
= 3, 32 log
10
_

_
(5.7)
Donde n es el n umero de iteraciones necesarias para alcanzar la raz con la precisi on absoluta
especicada. La relaci on entre un intervalo n + 1 y el anterior, n, es:

n+1
= C (
n
)
m
(5.8)
donde C = 1/2 y m = 1. Cuando un metodo converge como en la ecuaci on 5.8, con m = 1, se dice
que es lineal. Este es el caso del metodo de bisecci on. Cuando m > 1 se dice que el metodo converge
supralinealmente. Un metodo donde m = 2, se dice que converge cuadr aticamente.
62
-4
-3
-2
-1
0
1
2
3
4
5
-3 -2 -1 0 1 2 3
f(x)
r(x)
Figura 5.4: La funci on f(x) = x cos(x) en el intervalo [, ], superpuesta a la recta que pasa por
los puntos (1, f(1)), (1, f(1)). El corte de la recta con el eje de abcisas proporciona la aproximaci on
a la raz.
5.3. Regula falsi
Si la funci on es suave (es decir se comporta de manera aproximadamente lineal) cerca de la raz,
el metodo de falsa posici on o regula falsi converge supralinealmente.
La idea es muy sencilla. Dado un intervalo [a, b], el metodo de regula falsi determina la raz
aproximada calculando la recta que pasa por los puntos (a, f(a)) y (b, f(b)):
y = x +
donde:
=
f(a) f(b)
a b
y
=
bf(a) af(b)
b a
La intersecci on de dicha recta con el eje de abcisas:
y = x + = 0
se toma como la raz aproximada:
c =

=
af(b) bf(a)
f(b) f(a)
(5.9)
La gure 5.4 muestra la funci on f(x) = x cos(x) en el intervalo [, ], superpuesta a la recta
que pasa por los puntos (1, f(1)), (1, f(1)). El corte de la recta con el eje de abcisas proporciona
la aproximaci on a la raz. Como puede verse, la aproximaci on es buena y la convergencia en este caso
es supralineal.
En la pr actica, el metodo de regula falsi se combina con el de bisecci on, ensay andose primero la
raz proporcionada por el corte de la recta con el eje de abcisas. A continuaci on se decide que mitad
del intervalo descartar (all a donde la funci on no cambia de signo) y se calcula la anchura del intervalo
que retenemos. Si es mayor que la mitad del intervalo original, la aproximaci on por regula falsi es
peor que por bisecci on y, consecuentemente, se descarta, realiz andose una bisecci on en su lugar. El
proceso se repite hasta alcanzar la tolerancia deseada. La combinaci on de regula falsi y bisecci on
converge siempre y a menudo converge supralinealmente, pero en funciones patol ogicas puede llegar
a converger m as lentamente que bisecci on, debido a las evaluaciones extras de la funci on.
63
5.4. Newton-Raphson
Si conocemos la derivada de la funci on y si la funci on que queremos evaluar es aproximadamen-
te lineal en el intervalo de interes, el metodo de Newton Raphson ofrece convergencia cuadr atica.
Geometricamente el metodo consiste en extender la tangente a la curva en la imagen de uno de
los extremos del intervalo hasta que corte el eje de abcisas, escogiendo ese punto como nueva raz.
Algebraicamente, expandimos en serie de Taylor la funci on f(x) en torno a un punto:
f(x + ) = f(x) + f

(x) +
f

(x)
2

2
+ ...
En la vecindad de la raz podemos quedarnos a primer orden:
f(x + ) = f(x) + f

(x)
Si c = x + es una raz aproximada, entonces f(x + ) 0 y por tanto = f(x)/f

(x). Partiendo
de un extremo del intervalo [a, b], digamos a, la raz aproximada es:
c = a
f(a)
f

(a)
(5.10)
Para estudiar la convergencia del metodo supongamos que r es una raz simple de f(x) (por tanto
f(r) = 0 ,= f

(r)) y que adem as la segunda derivada, f

es continua. Si x
n
es la raz aproximada en
la iteraci on n, entonces la anchura del intervalo es
n
= r x
n
. La anchura del intervalo
n+1
ser a:

n+1
= r x
n+1
= r x
n
+
f(x
n
)
f

(x
n
)
=
n
+
f(x
n
)
f

(x
n
)
=

n
f

(x
n
) + f(x
n
)
f

(x
n
)
(5.11)
Por otra parte:
f(r) = f(x
n
+
n
) = 0
expandiendo en serie de Taylor entorno a x
n
:
f(x
n
+
n
) = f(x
n
) +
n
f

(x
n
) +
1
2

2
n
f

(x
n
) + ... = 0 (5.12)
Qued andonos ahora en segundo orden obtenemos:

n
f

(x
n
) + f(x
n
) =
1
2

2
n
f

(x
n
)
Sustituyendo en 5.11:

n+1

1
2
f

(x
n
)
f

(x
n
)

2
n
= C
2
n
(5.13)
Comparando con la ecuaci on 5.8, obtenemos que m = 2. Es decir, el metodo converge cuadr aticamente.
Notad, sin embargo que para obtener 5.13 hemos hecho la hip otesis de que podemos quedarnos a
segundo orden en la expansi on de Taylor de la ecuaci on 5.12, lo cual implica que estamos lo bastante
cerca de la raz. Lejos de esta, los terminos de orden superior pueden ser importantes, restando validez
a la deducci on anterior. Adem as si la derivada se anula en cualquiera de las aproximaciones a la raz,
el algoritmo diverge.
Como ilustraci on del metodo, deduciremos la f ormula de Newton para el c alculo aproximado de
una raz cuadrada. Consideremos la funci on:
f(x) = N x
2
(5.14)
donde N es un n umero real positivo. Obviamente, hallar la raz de la funci on en la ecuaci on 5.14,
f(x) = 0, corresponde a encontrar la raz cuadrada de N.
64
Sea x
0
una raz aproximada de la funci on. El metodo de Newton Raphson estipula que
x
1
= x
0
f(x)/f

(x)[
x=x
0
es una aproximaci on mejor a la raz. Sustituyendo en la ecuaci on 5.14 obtenemos que:
x
1
=
1
2
(x
0
+
N
x
0
)
que no es sino la f ormula de Newton para determinar la raz de un n umero N, dada una raz aproximada
x
0
.
5.5. Programaci on del metodo de bisecci on
5.5.1. Manejo de errores
Antes de comenzar a programar algoritmos numericos conviene recordar la primera ley fundamen-
tal de la inform atica. Todo programa es sospechoso de contener errores hasta que no demuestre lo
contrario. No nos referimos tanto a los errores de compilaci on (relativamente f aciles de corregir) como
a los m as insidiosos errores de ejecuci on. Muchos de estos se deben a la segunda ley fundamental de
la inform atica. Un programa no hace lo que creemos que hace, sino lo que le hemos programado que
haga. El programador se equivoca. Escribe algoritmos incorrectos, comete fallos l ogicos al dise nar sus
algoritmos y un largo y desafortunado etcetera. En consecuencia, el programa se comporta de manera
inesperada, a menudo ex otica y en m as de una ocasi on, francamente maligna. La unica defensa contra
el duende que habita en todo compilador, es escribir programas dise nados para manejar errores.
La manera m as simple de reaccionar ante un error es escribir un mensaje por la pantalla y abortar
la ejecuci on del programa, enviando un c odigo de error. Esta va a ser nuestra losofa en casi todos
los casos, a lo largo del curso.
Un c odigo de error suele asociarse a un error tpico, por ejemplo:
int newtonRaphson (double x0,double x1)
...
// La f ormula de Newton Raphson es: x1 = x0 - f(x0)/f(x0)
// f(x) es una cierta funci on
// f(x) es la funci on derivada de f(x)
// si f(x0) es cero el resultado de f(x0)/f(x0) es indefinido
double dfx0 = fprime(x0); // f(x0)
double fx0 = f(x0);
...
if (dfx0 == 0) // error!
return 12;
else // No hay problema
x1 = x0 - fx0/dfx0;

...
return 0; // todo fue bien

El c odigo del ejemplo anterior preve la posibilidad de una divisi on por cero, enviando un c odigo
(un n umero entero diferente de cero) a la funci on externa (la funci on que llam o a newtonRaphson).
Desde esa funci on (supongamos que fue main) podemos tomar la siguiente acci on:
int main ()
65

...
int status = newtonRaphson (raizAproximada, mejorRaiz);
// si status = 0 no hubo errores
if (status == 0)
cout << " la ra z en esta iteraci on es = " << mejorRaiz << endl;
// etc
else // hubo un c odigo de error
cerr << " c odigo de error " << status << endl;
// aborta el programa devolviendo el c odigo de error correspondiente
exit (status);
...

En caso de error recibiremos un mensaje por el dispositivo asociado al ujo cerr (por ejemplo la
pantalla) y el programa se interrumpir a.
Es obligatorio interrumpir el programa ante un error? Depende, naturalmente, del tipo de error y
de la aplicaci on que estemos programando. El arte del manejo de errores es sutil y requiere experiencia.
En el ejemplo que estamos comentando podramos decidir llamar a una rutina que aplicara el metodo
de bisecci on, por ejemplo, cuando la derivada de la funci on se hace nula o muy peque na, en lugar de
abortar el programa. La expeditiva receta de interrumpir la ejecuci on frente a un error es un caso
extremo, aunque bastante robusto de tratar errores.
Por otra parte, incluso un programador avezado puede sufrir m as de un dolor de cabeza si sus pro-
gramas le envan mensajes tan crpticos como error de ejecuci on n umero 12. Claramente, los c odigos
de error deben llevar asociado un nombre mnemotecnico adem as de un n umero entero. Indiscutible-
mente la jaqueca no es tan seria si el programa enva el mensaje error de ejecuci on EZERODIV.
Un mecanismo muy simple para devolver un c odigo de error entero que adem as lleve asociado un
nombre mnemotecnico es utilizar una enumeraci on:
/
/* enumeraci on an onima cuyo cometido es asociar
* nombres mnemot ecnicos a una serie
* de n umeros enteros que representan posibles errores de ejecuci on
*/
enum
EXITO = 0,
FALLO = -1,
ITERANDO = -2, /* todav a iterando */
EDOM = 1, /* error de dominio, e.g sqrt(-1) */
ERANGO = 2, /* rango incorrecto */
EPTR = 3, /* puntero no v alido */
EINVAL = 4, /* argumento no v alido */
EFALLO = 5, /* fallo gen erico (no sabemos bien qu e ha pasado) */
EFACTOR = 6, /* error de factorizaci on */
ESANITY = 7, /* error de cordura (sanity en ingl es). Caso desesperado*/
ENOMEM = 8, /* "No hay Memoria", error al pedir recursos en new */
EFUNC = 9, /* problemas con la funci on escrita por el usuario */
ENOCTRL = 10, /* el proceso iterativo esta fuera de control*/
EMAXITER = 11, /* excedido el n umero m aximo de iteraciones */
EZERODIV = 12, /* intento de dividir por cero */
66
EMALTOL = 13, /* tolerancia proporcionada por el usuario inv alida */
ETOL = 14, /* el proceso no alcanza la tolerancia especificada */
EUNDRFLW = 15, /* desbordamiento negativo de un real o entero*/
EOVRFLW = 16, /* desbordamiento positivo de un real o entero */
EPRECI = 17, /* p erdida de precisi on */
EREDON = 18, /* fallo debido a error de redondeo*/
EMALDIM = 19, /* problema con la dimensi on de una matriz o vector */
ENOCUAD = 20, /* matriz no cuadrada */
ESING = 21, /* singularidad*/
EDIVERGE = 22, /* integral o serie divergente */
ENOPROG = 23, /* iteraci on estancada*/
;
Notad que la enumeraci on es an onima, esto es, no hemos declarado un tipo. Los nombres a la
izquierda de la enumeraci on no son sino sin onimos para los n umeros a la derecha. Desde el punto de
vista del compilador, EZERODIV y 12 son identicos.
La siguiente funci on, error, es un ejemplo de tratamiento elemental (y contundente) de errores.
Escribe un mensaje por cerr y aborta la ejecuci on saliendo con el c odigo de error correspondiente.
inline void error (string mensaje, int codigo)
cerr << mensaje << endl;
switch(codigo)
case FALLO:
cout << "Fallo al ejecutar" << endl;
exit(FALLO);
break;
case EDOM:
....
default:
cout << "Error no catalogado" << endl;
exit(FALLO);

5.5.2. Control de informaci on


A menudo, es conveniente que el nivel de informaci on que proporciona una determinada rutina
o funci on, pueda controlarse por parte del usuario. Por ejemplo, podemos desear, en ciertos casos,
imprimir cada uno de los pasos que el algoritmo de bisecci on ejecuta hasta converger, mientras que
en otros nos conformaremos con que imprima el resultado nal. En consecuencia, necesitamos dise nar
un mecanismo para controlar el nivel de informaci on que una funci on proporciona.
Una vez m as podemos utilizar una enumeraci on para ello. Por ejemplo:
// nivel de informaci on proporcionado por la rutina
enum info NOINFO,CONCISO,DETALLADO;
Una variable de tipo info puede tomar los valores NOINFO,CONCISO y DETALLADO (si
necesitamos m as basta a nadir, por ejemplo, un valor MUYDETALLADO). Recordemos que se trata
de una enumeraci on, es decir, los nombres NOINFOCONCISO y DETALLADO son sin onimos
de los valores enteros 0, 1 y 2. En consecuencia, en el c odigo:
67
info nivel = CONCISO; // define nivel como una variable de tipo info
//con valor inicial CONCISO
...
if (nivel > NOINFO)
cout << "iteraci on" << " t" << " xinf " << " t"
<<" xsup " << " t" << " ra z" << endl;
la instrucci on cout se ejecuta si nivel es mayor que NOINFO (es decir, que 0). Puesto que CONCISO
es mayor que NOINFO (CONCISO = 1), la instrucci on cout se ejecuta (y se ejecuta tambien si nivel =
DETALLADO).
5.5.3. Dise no
Antes de escribir un programa vale la pena dise narlo, es decir, decidir los diferentes elementos
que lo componen y bosquejar su relaci on l ogica. A menudo, los procesos de dise no y programaci on
son iterativos, es decir, comenzamos dise nando un primer borrador, entendemos como mejorarlo tras
implementarlo y probarlo lo que nos permite dise nar un borrador mejorado y as sucesivamente.
Veamos un ejemplo. Queremos escribir un programa que encuentre las races de la funci on xcos(x)
por el metodo de bisecci on.
La aproximaci on de fuerza bruta al problema anterior consistira en escribir una funci on main
que lo hiciera todo. Pidiera al usuario los datos necesarios, implementara el algoritmo y escribiera los
resultados. Tal soluci on puede ser razonable en algunos casos, cuando necesitamos obtener un resultado
r apidamente y no es necesario programar demasiado para obtenerlo. En general, sin embargo, tales
soluciones quick and dirty
1
tienden, como el monstruo de Frankenstein a volverse en contra de su
creador.
Una aproximaci on m as sensata involucra dividir el problema en varias partes independientes: fun-
ciones que codiquen el algoritmo propiamente dicho, otras que proporcionen una interfaz para el
usuario (es decir se ocupen de adquirir las variables que necesitamos para efectuar el c alculo y de
escribir los resultados) y nalmente una funci on principal que haga las veces de director de orques-
ta, haciendo intervenir los diferentes interpretes del programa en el momento apropiado. Veamos un
borrador:
---Programaci on del m etodo de bisecci on
---Programa principal ---
---Inicializaci on----
--Imprime la funci on del programa---
--Pide los datos necesarios para inicializar el algoritmo
---Inicializa el algoritmo
---Itera hasta alcanzar la precisi on o exceder el n umero
m aximo de iteraciones permitidas
----Ejecuta iteraci on
--- Imprime, si se desea, resultados intermedios
--- Prueba condici on de convergencia
--- Sale del bucle si se alcanza convergencia
1
Una expresi on inglesa algo m as delicada que el equivalente en buen castellano, que sera el de chapuceras
68
--- Incrementa contador de iteraciones en otro caso
---analiza el resultado, imprime resultado final si
se desea, manejo de posibles errores
--termina
----------------------------
---Funci on Obtener Datos del Usuario
--Adquiere los datos necesarios para el algoritmo
---Funci on analizar resultados
--Analiza resultados finales, imprime, maneja errores
---Funci on Inicializa Algoritmo ---
--calcula valores iniciales
--guarda valores iniciales
--Funci on Itera Algoritmo ---
--ejecuta un paso del algoritmo (una bisecci on)
--guarda resultados intermedios
--Funci on Prueba Convergencia ---
--Comprueba si se alcanza la precisi on requerida
El dise no anterior no es unico, ni, necesariamente optimo. Podramos, quiz as, a nadir o quitar
funciones e incluso cambiar por completo la losofa del programa conductor. Por otra parte, se trata
de una soluci on razonable, sencilla y bastante general. Si cambiamos Bisecci on por regula falsi o
por Newton Raphson el dise no no cambia, tan s olo tenemos que a nadir funciones que inicialicen,
ejecuten iteraciones, etc., para los nuevos metodos.
5.5.4. El programa xbiseccion
Veamos una implementaci on del programa principal para el algoritmo de bisecci on:
// xbiseccion.cpp
// Aplicaci on del m etodo de bisecci on
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <iomanip>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Declaraciones funci on a explorar
#include <xcosx.h>
// algoritmos de ra ces
#include <ra ces.h>
#include <biseccion.h>
69
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

//Inicializaci on
double xinf, xsup, raiz, epsrel;
int iter, maxiter;
info nivel;
inicializa (xinf,xsup,epsrel,maxiter,nivel);
cout << " Este programa calcula el cero de la funci on "
<< "x-cos(x)" << endl;
cout << " Por el m etodo de Bisecci on" << endl;
if (nivel > NOINFO)
cout << "iteraci on" << " t " << " xinf " << " t"
<<" xsup " << " t" << " ra z" << endl;
int status;
double fxinf,fxsup; // im agenes de los extremos del intervalo
status = bisecIni (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en bisecIni",status);
iter = 0;
do
iter++;
status = bisecIter (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en bisecIter",status);
status = testIntervalo (xinf,xsup,epsrel);
if (status == EXITO)
cout << " El m etodo ha convergido" << endl;
if(nivel > NOINFO)
cout << iter << " t "
<< setw(12) << setprecision(7) <<
xinf << " t "
<< setw(12) << setprecision(7) <<
xsup << " t "
<< setw(12) << setprecision(7) <<
raiz << endl;
if (iter >= maxiter ) status = EMAXITER;

while (status == ITERANDO);


analiza (status, raiz, iter );
70
return EXITO;

Utilidades matematicas
Antes de escribir la funci on main, incluimos, como de costumbre, las cabeceras necesarias y espe-
cicamos los espacios de nombres que vamos a usar. Recordemos que:
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <iomanip>
#include <string>
son cabeceras de la biblioteca est andar que se ocupan, respectivamente, de declarar los ujos de
entrada y salida de datos, los manipuladores para salida formateada y los strings de C++. Adem as,
encontramos nuevas cabeceras:
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Declaraciones funci on a explorar
#include <xcosx.h>
// algoritmos de ra ces
#include <ra ces.h>
#include <biseccion.h>
Hemos incluido en <error.h> la enumeraci on que codica los varios c odigos de error que manejaremos
y la funci on error, que tambien hemos comentado. Recordemos que los parentesis <> indican al
compilador que la cabecera error.h se encuentra en un lugar est andar, o, en otras palabras, que el
directorio donde se encuentra error.h est a incluido en la ruta del compilador, esto es, en una serie
de directorios (parte de los cuales est an predeterminados, mientras que otros pueden ser especicados
por el usuario) en los que el compilador busca cabeceras. Eso quiere decir, que, a la hora de compilar,
debemos recordar incluir en la ruta del compilador, el directorio donde hayamos guardado error.h.
La siguiente cabecera, <matutil.h>, contiene, como su nombre indica, una serie de utilidades
matem aticas, en particular, la denici on de un cierto n umero de constantes de interes:
static const double EXP = 2.71828182845904523536028747135 ; // e
static const double LOG2E = 1.44269504088896340735992468100; //log_2 e
static const double LOG10E = 0.43429448190325182765112891892; // log_10 (e)
static const double SQRT2 = 1.41421356237309504880168872421; // sqrt(2)
static const double SQRT1_2 =0.70710678118654752440084436210; // sqrt(1/2)
static const double SQRT3 = 1.73205080756887729352744634151 ; // sqrt(3)
static const double PI = 3.14159265358979323846264338328 ; // pi
static const double PI_2 = 1.57079632679489661923132169164 ; // pi/2
static const double PI_4 = 0.78539816339744830966156608458 ; // pi/4
static const double SQRTPI = 1.77245385090551602729816748334 ; // sqrt(pi)
static const double LN10 = 2.30258509299404568401799145468 ; // ln(10)
static const double LN2 = 0.69314718055994530941723212146 ; // ln(2)
static const double LNPI = 1.14472988584940017414342735135 ; // ln(pi)
static const double EULER = 0.57721566490153286060651209008 ; // cte Euler
static const double AUREA = 0.618033988749895; // Media A urea
71
Instrucciones typedef utiles tales como:
typedef double (*func) (double) ;
que nos permite usar el nombre func como sin onimo de puntero a una funci on que tomar un doble
en el argumento y devuelve un doble. Tambien encontramos la enumeraci on
enum info NOINFO,CONCISO,DETALLADO;
de la que ya hemos hablado y unas cuantas funciones cuya denici on comentaremos m as adelante. Se
trata de utilidades para calcular cantidades que aparecen a menudo, tales como el m aximo y mnimo
de dos n umeros, decidir si un n umero es par o impar, decidir el signo de un n umero, etc. Tambien se
incluyen las primeras potencias de un n umero. En C++, desafortunadamente, no existe un operador de
exponenciaci on
2
utiliz andose en su lugar la funci on pow(), que resulta algo engorrosa cuando aparece
repetidamente en una f ormula. Por esa raz on, hemos denido funciones para las potencias peque nas
que aparecen m as frecuentemente, tales como cuadrado() y cubo(). Para las potencias siguientes
usamos la notaci on pow4 (cuarta potencia), pow5, etc.
Programaci on modular
La siguiente cabecera que nos encontramos es:
#include <xcosx.h>
donde declaramos la funci on cuyas races queremos encontrar. Por ejemplo:
// xcosx.h
#ifndef __XCOSX__
#define __XCOSX__
// Declaraci on de la funci on
double xcosx (double x);
// Y su derivada
double Dxcosx (double x);
#endif
declara dos funciones que toman un doble y devuelven un doble, xcosx (la funci on que vamos a
estudiar x cos(x)) y Dxcosx (su derivada, necesaria para el metodo de Newton-Raphson).
La denici on de estas funciones la escribimos en un chero diferente al chero xbiseccion.cpp
donde se encuentra el programa conductor. As, si deseamos estudiar una funci on diferente de la que
nos ocupa, nos basta con cambiar la cabecera, declarando una funci on diferente y con compilar el
c odigo conductor junto al chero donde se dena dicha funci on. La estrategia de dividir un programa
en unidades o m odulos que puedan ensamblarse selectivamente se denomina programaci on modular
o estructurada y suele dar buenos resultados, siempre que los m odulos se elijan juiciosamente, lo
cual no siempre es f acil. Siguiendo con nuestro ejemplo, el chero xcosx.cpp contiene la denici on de
las funciones a estudiar:
// implementa la funci on x -cos (x) y su derivada
#include <xcosx.h>
#include <matutil.h>
double xcosx (double x)
return x -cos (x);

double Dxcosx (double x)


return 1 + sin (x);

2
Tal, como, por ejemplo en FORTRAN, donde x y se traduce por x
y
72
Funciones comunes a los metodos de b usqueda de races
Finalmente, nos encontramos con las cabeceras:
// algoritmos de ra ces
#include <ra ces.h>
#include <biseccion.h>
La primera de estas, <ra ces.h>, contiene las declaraciones comunes a todos los algoritmos de
b usqueda de races:
// ra ces.h
// declaraciones comunes para los
// distintos m etodos que calculan ra ces de funciones
#ifndef __ra cesXX__
#define __ra cesXX__
#include <matutil.h>
// Funci on para inicializar par ametros leyendo por terminal
void inicializa (double& a, double& b, double& epsrel,
int& maxiter, info& nivel);
// Funci on para analizar el resultado final de una b usqueda
void analiza (int resul, double raiz, int iter);
// Comprueba si
// [ b a [ < rel MAXIMO(
| b + a |
2
, [b a[)
int testIntervalo (double a, double b, double epsrel);
// Comprueba si
// [ x0 x1[ < epsrel [x1[
int testDelta (double x0, double x1, double epsrel);
#endif
La primera es la funci on inicializa, cuyo cometido es obtener del usuario los datos necesarios
para los distintos algoritmos de races (los extremos del intervalo, la precisi on absoluta y relativa
del c alculo, el n umero m aximo de iteraciones que se permite y el nivel de informaci on que se desea).
An alogamente, la funci on analiza, se ocupa de analizar el resultado del c alculo. Toma como argumento
un c odigo entero (que, de acuerdo con nuestra enumeraci on de errores vale EXITO si todo fue bien o
proporciona una pista del problema si lo hubo), el valor nal de la raz estimado por el algoritmo y
la iteraci on en que se alcanz o convergencia. La versi on m as simple de analiza se limitar a a escribir
la raz estimada y la iteraci on en que se alcanz o en caso de EXITO y a escribir un mensaje (y quiz as
abortar el programa) en caso de error.
La funci on testIntervalo comprueba si:
[b a[ <
rel
MAXIMO(
[b + a[
2
, [b a[)
donde a y b son los extremos inferior y superior del intervalo y
rel
la precisi on relativa requerida por
el usuario.
An alogamente, la funci on testDelta comprueba si
[x
1
x
0
[ <
rel
x
1
73
donde x
1
es la aproximaci on a la raz en la iteraci on considerada y x
0
la aproximaci on en la iteraci on
anterior.
La denici on de todas estas funciones se encuentra en el chero ra ces.cpp:
// ra ces.cpp
// funciones comunes a los algoritmos de ra ces
#include <iostream>
#include <iomanip>
#include <error.h>
#include <ra ces.h>
#include <matutil.h>
using namespace std;
void inicializa (double& a, double& b, double& epsrel,
int& maxiter, info& nivel)

int invel;
cout << " El cero de la funci on se busca en el intervalo";
cout << "[a,b]" << endl;
cout << " Introduzca el valor de a " ; cin >> a;
cout << " Introduzca el valor de b " ; cin >> b;
cout << " Que precisi on relativa desea? " ; cin >> epsrel;
cout << " N umero m aximo de iteraciones? " ; cin >> maxiter;
cout << " Introduzca el nivel de informaci on que desea" << endl;
cout << " (0 = NOINFO, 1 = CONCISO, 2 = DETALLADO) " ;
cin >> invel;
cout << " Buscamos la ra z en el intervalo ["
<< a << "," <<b << "]" << endl;
nivel = info(invel); // nivel de informaci on

// analiza el resultado en funci on del c odigo resul y la ra z raiz


void analiza (int resul, double raiz, int iter)

switch(resul)
case EXITO:
cout << " La ra z es "
<< setw(12) << setprecision(8)
<< raiz << endl;
cout << " N umero de iteraciones empleado "
<< iter << endl;
break;
case EMAXITER:
cout << " Excedido el n umero m aximo de iteraciones" << endl;
break;
default:
error ("Error ineseperado!!", FALLO);

74
// a y b son los extremos del intervalo
// epsrel es la precisi on relativa
int testIntervalo (double a, double b, double epsrel)

if (epsrel < 0.0)


error ("La precisi on requerida es negativa", EMALTOL);
if (a > b)
error ("L mite inferior mayor que L mite superior intervalo", EINVAL);
double tol = epsrel* maximo ( fabs(b-a), fabs(a+b)/2);
if (fabs(b - a) < tol)
return EXITO;
return ITERANDO ;

// x0 es la ra z en la iteraci on anterior
// x1 es la ra z en la iteraci on actual
int testDelta (double x0, double x1, double epsrel)

if (epsrel < 0.0)


error ("La precisi on requerida es negativa", EMALTOL);
double tol = epsrel* fabs(x1);
if (fabs(x1-x0) < tol)
return EXITO;
return ITERANDO ;

Declaraci on de las funciones de bisecci on


La ultima cabecera del programa xbiseccion es:
#include <biseccion.h>
donde se encuentran las declaraciones especcas del algoritmo de bisecci on:
// biseccion.h
// declara las funciones espec ficas del m etodo de bisecci on
#ifndef __BISECCION__
#define __BISECCION__
#include <matutil.h> // typedef de func
// Inicializa el algoritmo
int bisecIni (func f, double a, double b,
double& fa, double& fb, double& c);
75
// Ejecuta una iteraci on
int bisecIter (func f, double& a, double& b,
double& fa, double& fb, double& c);
#endif
Declaramos una funci on de inicializaci on, bisecIni y una funci on que itera el algoritmo bisecIter.
Comentaremos ambas un poco m as adelante.
Programa principal
El resto de main es muy sencillo. Nos encontramos primero con la declaraci on de las variables
que necesitamos inicializar (los extremos del intervalo xinf y xsup, la precisi on relativa epsrel y
el n umero de iteraciones m aximo permitido maxiter). Declaramos adem as un contador, iter, del
n umero de iteraciones, la variable donde almacenamos el resultado nal raiz, y una variable tipo
info, nivel, para controlar el nivel de informaci on:
//---main---
int main ()
//Inicializaci on
double xinf, xsup, raiz, epsrel;
int iter, maxiter;
info nivel;
Llamamos a continuaci on a la funci on inicializa que pide al usuario valores iniciales para todas las
variables relevantes y escribimos por la pantalla, en funci on del nivel de informaci on que el usuario
haya seleccionado:
inicializa (xinf,xsup,epsrel,maxiter,nivel);
cout << " Este programa calcula el cero de la funci on "
<< "x-cos(x)" << endl;
cout << " Por el m etodo de Bisecci on" << endl;
if (nivel > NOINFO)
cout << "iteraci on" << " t " << " xinf " << " t"
<<" xsup " << " t" << " ra z" << endl;
Inicializamos el algoritmo de bisecci on. La funci on bisecIni devuelve un entero que es siempre EXITO
si todo fue bien o un c odigo de error que podemos tratar con la funci on error en otro caso.
int status;
double fxinf,fxsup; // im agenes de los extremos del intervalo
status = bisecIni (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en bisecIni",status);
A continuaci on iniciamos el bucle principal (del tipo do while). La funci on bisecIter realiza una
iteraci on del metodo de bisecci on y devuelve el control. La funci on testIntervalo comprueba si se
76
ha alcanzado convergencia, en cuyo caso devuelve EXITO. En caso contrario devuelve ITERANDO.
Tambien puede devolver un c odigo de error (por ejemplo, si el usuario requiere un intervalo en el que
la funci on no cambie de signo o un valor negativo para la precisi on). El programa ejecuta el bucle
mientras la variable status vale ITERANDO (recordemos que status es un entero e ITERANDO es uno
de los valores mnemotecnicos de la enumeraci on a la que ya nos hemos referido). Cuando status vale
EXITO o un c odigo de error, el bucle termina:
iter = 0;
do
iter++;
status = bisecIter (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en bisecIter",status);
status = testIntervalo (xinf,xsup,epsrel);
if (status == EXITO)
cout << " El m etodo ha convergido" << endl;
if(nivel > NOINFO)
cout << iter << "t"
<< setw(12) << setprecision(7) <<
xinf << "t"
<< setw(12) << setprecision(7) <<
xsup << "t"
<< setw(12) << setprecision(7) <<
raiz << endl;
if (iter >= maxiter ) status = EMAXITER;

while (status == ITERANDO);


Finalmente, el programa llama a la funci on analiza que se ocupa de imprimir el resultado nal o
reaccionar ante un posible error y termina.
analiza (status, raiz, iter );
return EXITO;

5.5.5. El algoritmo de bisecci on


Podemos, por n, discutir la codicaci on del algoritmo de bisecci on propiamente dicha, que se
encuentra en el chero biseccion.cpp, donde denimos las funciones bisecIni y bisecIter. La
primera de estas es:
int bisecIni (func F, double xa, double xb,
double& fa, double& fb, double& raiz)

raiz = 0.5 * (xa + xb) ; //estimaci on inicial ra z


fa = F(xa); // im agenes de los extremos
fb = F(xb);
// error si la funci on no cambia de signo
77
if( fa*fb >0)
return EINVAL;

return EXITO;

El primero de los argumentos es de tipo func que no es sino un typedef para una funci on que toma
un puntero a double y devuelve un double. Este argumento nos permite pasar el puntero a la funci on
que queremos analizar.
El resto de los argumentos es: los extremos inferior y superior del intervalo xa y xb; las im agenes
de dichos extremos, fa = F(xa) y fb = F(xb) y el valor inicial de la raz. Las variables xa y xb, que
no se modican en bisecIni se pasan como reales en doble precisi on mientras que las im agenes de los
extremos y el valor inicial de la raz (los cuales se calculan en bisecIni) se pasan como referencias a
estos.
El cuerpo de la funci on es muy sencillo. Se limita a calcular la raz como el punto medio de los
extremos, as como las im agenes fa = F(xa), fb = F(xb). Si F no cambia de signo en el intervalo
fa fb > 0, bisecIni devuelve un c odigo de error. En otro caso devuelve EXITO y naliza.
La funci on bisecIter tambien es muy sencilla:
int rfalsiIter (func F, double& xa, double& xb,
double& fa, double& fb, double& raiz)
double xinf = xa; // extremo inferio original del intervalo
double xsup = xb; // extremo superior original del intervalo
// Comprobamos si hemos alcanzado la ra z
// en uno de los dos extremos del intervalo
if (fa == 0.0)
raiz = xinf ;
xb = xinf;
return EXITO;
else if (fb == 0.0)
raiz = xsup ;
xa = xsup;
return EXITO;
// Calculamos la recta que pasa por (xa, f(xa)),
// (xb,f(xb)). Dividimos el intervalo en el punto en que la
// recta corta el exe de las X
if (fa == fb)
return EZERODIV;
raiz = (fa * xsup - fb* xinf) / (fa - fb);
double fc = F(raiz);
if (fc == 0.0)
78
xa = raiz;
xb = raiz;
return EXITO;
// Elimina la parte del intervalo que no contiene la ra z
double w;
if ( fa * fc < 0.0 )
xb = raiz;
fb = fc;
w = raiz -xinf; // distancia entre la ra z y el intervalo inferior
else
xa = raiz;
fa = fc;
w = xsup - raiz ; // distancia entre la ra z y el intervalo superior
// Si la distancia entre la ra z y el extremo del intervalo
// es menor que la mitad del intervalo original, hemos
// avanzado m as r apido que haciendo una simple bisecci on
// en ese caso hemos terminado
if (w < 0.5 * fabs(xsup - xinf))
return EXITO ;
// En otro caso, hacemos una bisecci on
double xbis = (xinf + xsup) / 2.0;
double fxbis = F(xbis);
if ( fa * fxbis < 0.0 )
xb = xbis;
fb = fxbis;
if ( raiz > xbis)
raiz = 0.5 * (xinf + xbis) ;
else
xa = xbis;
fa = fxbis;
if ( raiz < xbis)
raiz = 0.5 * (xsup + xbis) ;
return EXITO;
Los argumentos son los mismos que en bisecIni excepto por el hecho de que los extremos del intervalo
son ahora referencias. Esto es as ya que, en cada llamada de bisecIter actualizamos los extremos
del intervalo que van reduciendose progresivamente. La iteraci on es muy sencilla. Comprobamos en
79
primer lugar si hemos alcanzado la raz exacta (es decir si la imagen de la funci on se anula en los
extremos del intervalo o en la raz estimada). En otro caso, realizamos una bisecci on y nos quedamos
con la mitad del intervalo que contenga todava la raz. Finalmente guardamos las im agenes de los
nuevos extremos antes de devolver el control.
5.5.6. Recapitulaci on
La codicaci on del algoritmo de bisecci on nos ha permitido introducir un cierto n umero de concep-
tos importantes. Hemos visto que un programa robusto requiere un mnimo control sobre errores de
ejecuci on, as como la capacidad de imprimir informaci on selectivamente. Tambien hemos comentado
la necesidad de dise nar antes de programar, as como la conveniencia de introducir modularidad en
nuestro c odigo, que hemos dividido en varias componentes bien diferenciadas:
1. Un m odulo que declara (<xcosx.h>) y dene (xcosx.cpp) la funci on a estudiar (y, eventual-
mente, su derivada).
2. Un m odulo que agrupa las funciones comunes a varios algoritmos de races (<ra ces.h>,ra ces.cpp).
Dichas funciones se ocupan de tareas tales como requerir del usuario los valores iniciales del pro-
blema, imprimir los resultados y comprobar si se ha alcanzado convergencia.
3. Un m odulo que agrupa las funciones que inician e iteran el metodo de bisecci on (<biseccion.h>,
biseccion.cpp).
4. Por ultimo usamos un simple programa conductor (xbiseccion.cpp) para llamar a los diferentes
m odulos que componen nuestro programa.

Este es el resultado:
$ ./xbiseccion.exe
El cero de la funci on se busca en el intervalo[a,b]
Introduzca el valor de a -3
Introduzca el valor de b 3
Que precisi on relativa desea? 1e-6
N umero m aximo de iteraciones? 100
Introduzca el nivel de informaci on que desea
(0 = NOINFO, 1 = CONCISO, 2 = DETALLADO) 1
Buscamos la ra z en el intervalo [-3,3]
Este programa calcula el cero de la funci on x-cos(x)
Por el m etodo de Bisecci on
iteraci on xinf xsup ra z
1 0 3 1.5
2 0 1.5 0.75
3 0 0.75 0.375
4 0.375 0.75 0.5625
5 0.5625 0.75 0.65625
6 0.65625 0.75 0.703125
7 0.703125 0.75 0.7265625
8 0.7265625 0.75 0.7382812
9 0.7382812 0.75 0.7441406
10 0.7382812 0.7441406 0.7412109
11 0.7382812 0.7412109 0.7397461
12 0.7382812 0.7397461 0.7390137
13 0.7390137 0.7397461 0.7393799
14 0.7390137 0.7393799 0.7391968
80
15 0.7390137 0.7391968 0.7391052
16 0.7390137 0.7391052 0.7390594
17 0.7390594 0.7391052 0.7390823
18 0.7390823 0.7391052 0.7390938
19 0.7390823 0.7390938 0.7390881
20 0.7390823 0.7390881 0.7390852
21 0.7390823 0.7390852 0.7390838
22 0.7390838 0.7390852 0.7390845
El m etodo ha convergido
23 0.7390845 0.7390852 0.7390848
La ra z es 0.73908484
N umero de iteraciones empleado 23
5.6. Programaci on del algoritmo de regula falsi
Si nuestro dise no del metodo de bisecci on es correcto, debera ser sencillo codicar otros metodos
similares, utilizando parte del c odigo que ya hemos escrito (por ejemplo la denici on de la funci on
x cos(x) o las funciones comunes a los diferentes algoritmos.
Veamos, que, en efecto, la programaci on del metodo de regula falsi es muy similar a la del metodo
de bisecci on.
La implementaci on del programa principal para el algoritmo de regula falsi (xrfalsi.cpp) es la
siguiente:
/* xrfalsi.cpp
* Aplicaci on del m etodo de de regula falsi
*/
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <iomanip>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Declaraciones funci on a explorar
#include <xcosx.h>
// algoritmos de ra ces
#include <ra ces.h>
#include <rfalsi.h>
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

//Inicializaci on
double xinf, xsup, raiz, epsrel;
int iter, maxiter;
info nivel;
81
inicializa (xinf,xsup,epsrel,maxiter,nivel);
cout << " Este programa calcula el cero de la funci on "
<< "x-cos(x)" << endl;
cout << " Por el metodo de regula falsi" << endl;
if (nivel > NOINFO)
cout << "iteraci on" << "t" << " xinf " << "t"
<<" xsup " << "t" << " ra z" << endl;
int status;
double fxinf,fxsup; // im agenes de los extremos del intervalo
status = rfalsiIni (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en rfalsiIni",status);
iter = 0;
do
iter++;
status = rfalsiIter (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en rfalsiIter", status);
status = testIntervalo (xinf,xsup,epsrel);
if (status == EXITO)
cout << " El m etodo ha convergido" << endl;
if(nivel > NOINFO)
cout << iter << "t"
<< setw(12) << setprecision(7) <<
xinf << "t "
<< setw(12) << setprecision(7) <<
xsup << "t "
<< setw(12) << setprecision(7) <<
raiz << endl;
if (iter >= maxiter ) status = EMAXITER;

while (status == ITERANDO);


analiza (status, raiz, iter );
return EXITO;

Como era de esperar el c odigo es casi identico al de xbiseccion.cpp. Las unicas diferencias son que
aqu incluimos la cabecera rfalsi.h en lugar de biseccion.h y las llamadas a rfalsiIni (en lugar de
a bisecIni) y a rfalsiItera (en lugar de a bisecItera). Las funciones inicializa, testIntervalo
y analiza son las mismas para ambos algoritmos.
En cuanto a las funciones especcas del algoritmo (<rfalsi.h>, rfalsi.cpp), rfalsiIni es
identica a bisecIni:
82
int rfalsiIter (func F, double& xa, double& xb,
double& fa, double& fb, double& raiz)

double xinf = xa; // extremo inferio original del intervalo


double xsup = xb; // extremo superior original del intervalo
// Comprobamos si hemos alcanzado la ra z
// en uno de los dos extremos del intervalo
if (fa == 0.0)
raiz = xinf ;
xb = xinf;
return EXITO;

else if (fb == 0.0)


raiz = xsup ;
xa = xsup;
return EXITO;

// Calculamos la recta que pasa por (xa, f(xa)),


// (xb,f(xb)). Dividimos el intervalo en el punto en que la
// recta corta el exe de las X
if (fa == fb)
return EZERODIV;

raiz = (fa * xsup - fb* xinf) / (fa - fb);


double fc = F(raiz);
if (fc == 0.0)
xa = raiz;
xb = raiz;
return EXITO;

// Elimina la parte del intervalo que no contiene la ra z


double w;
if ( fa * fc < 0.0 )
xb = raiz;
fb = fc;
w = raiz -xinf; // distancia entre la ra z y el intervalo inferior

else
xa = raiz;
fa = fc;
w = xsup - raiz ; // distancia entre la ra z y el intervalo superior

// Si la distancia entre la ra z y el extremo del intervalo


// es menor que la mitad del intervalo original, hemos
83
// avanzado m as r apido que haciendo una simple bisecci on
// en ese caso hemos terminado
if (w < 0.5 * fabs(xsup - xinf))
return EXITO ;

// En otro caso, hacemos una bisecci on


double xbis = (xinf + xsup) / 2.0;
double fxbis = F(xbis);
if ( fa * fxbis < 0.0 )
xb = xbis;
fb = fxbis;
if ( raiz > xbis)
raiz = 0.5 * (xinf + xbis) ;

else
xa = xbis;
fa = fxbis;
if ( raiz < xbis)
raiz = 0.5 * (xsup + xbis) ;

return EXITO;

El algoritmo arroja el siguiente resultado:


$ ./xrfalsi.exe
El cero de la funci on se busca en el intervalo[a,b]
Introduzca el valor de a -3
Introduzca el valor de b 3
Que precisi on relativa desea? 1e-7
N umero m aximo de iteraciones? 100
Introduzca el nivel de informaci on que desea
(0 = NOINFO, 1 = CONCISO, 2 = DETALLADO) 1
Buscamos la ra z en el intervalo [-3,3]
Este programa calcula el cero de la funci on x-cos(x)
Por el metodo de regula falsi
iteraci on xinf xsup ra z
1 -0.9899925 1.005004 -0.9899925
2 0.5390306 0.7720172 0.5390306
3 0.7374989 0.754758 0.7374989
4 0.7390797 0.7469189 0.7390797
5 0.7390851 0.743002 0.7390851
6 0.7390851 0.7410436 0.7390851
7 0.7390851 0.7400643 0.7390851
El m etodo ha convergido
8 0.7390851 0.7390851 0.7390851
La ra z es 0.73908513
84
N umero de iteraciones empleado 8
Observad que para esta funci on, regula falsi necesita tan s olo la tercera parte de las iteraciones que
bisecci on.
5.7. Programaci on del algoritmo de Newton Raphson
El metodo de Newton Raphson debe aplicarse con mayores precauciones que los metodos anteriores,
puesto que si nos encontramos lejos de la raz, la aproximaci on en la que se basa deja de ser v alida.
Si, por ejemplo, iniciamos la b usqueda de la raz de xcos(x) tomando como aproximaci on inicial
a la raz uno de los extremos del intervalo nos puede ocurrir lo siguiente:
$ ./xnewton
El cero de la funci on se busca en el intervalo[a,b]
Introduzca el valor de a -3
Introduzca el valor de b 3
Que precisi on relativa desea? 1e-6
N umero m aximo de iteraciones? 100
Introduzca el nivel de informaci on que desea
(0 = NOINFO, 1 = CONCISO, 2 = DETALLADO) 1
Buscamos la ra z en el intervalo [-3,3]
Este programa calcula el cero de la funci on x-cos(x)
Por el m etodo de Newton-Raphson
Tomamos -3 como valor inicial para Newton
1 -0.6597342
2 3.085848
3 -0.782897
4 4.279717
5 -46.71257
6 29.59062
7 -896.8049
8 -446.8498
9 938.7159
10 344.5637
11 -1912.889
12 958.5224
13 -474.1495
14 139.5268
15 68.57416
16 -71.01534
17 1243
18 -8901.154
19 -4098.416
20 185889.6
21 92915.6
22 -15911.44
23 31613.19
24 12029.13
25 276.8954
26 82.75456
27 38.95998
85
28 19.16084
29 5.221337
30 -32.13064
31 63.30177
32 20.34362
33 10.19521
34 -25.75474
35 37.90408
36 7.223731
37 3.554027
38 -3.906719
39 -2.024779
40 13.63512
41 6.625687
42 2.370874
43 0.5506507
44 0.7486058
45 0.7391049
46 0.7390851
El m etodo ha convergido
47 0.7390851
La ra z es 0.73908513
N umero de iteraciones empleado 47
Que ha ocurrido? Simplemente, que, lejos de la raz, las derivadas de orden superior no pueden
ignorarse en la expansi on de Taylor. Como consecuencia, el metodo oscila, convergiendo nalmente
tras 47 iteraciones, el doble de las que necesita bisecci on.
La moraleja es que Newton-Raphson no debe utilizarse s olo, sino en combinaci on con otro metodo
cuya convergencia sea estable y que nos permita acercarnos a la raz.
En el siguiente ejemplo, hemos combinado Newton-Raphson con bisecci on, de la siguiente manera.
Bisecci on se utiliza para aproximarnos a la raz con una precisi on relativa del 100 %, lo cual consigue
muy r apidamente. El valor nal obtenido por el primer metodo se toma como valor inicial para el
segundo. Este es el c odigo:
/* xnewton.cpp
* Aplicaci on del m etodo de Newton Raphson
* combinado con bisecci on
*/
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <iomanip>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Declaraciones funci on a explorar
#include <xcosx.h>
// algoritmos de ra ces
#include <raices.h>
#include <newton.h>
#include <biseccion.h>
86
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

//Inicializaci on
double xinf, xsup, raiz, epsrel;
int iter, maxiter;
info nivel;
inicializa (xinf,xsup,epsrel,maxiter,nivel);
cout << " Este programa calcula el cero de la funci on "
<< "x-cos(x)" << endl;
cout << " Por el m etodo de Newton-Raphson" << endl;
if (nivel > NOINFO)
cout << "iteraci on" << "t" << " ra z" << endl;
int status;
double f,df; // f(ra z) y f(ra z)
double fxinf, fxsup;
// Llamamos primero al m etodo de bisecci on
// Para aproximarnos a la ra z con 100 % de precisi on
cout << "Llamada preliminar a Bisecci on" <<endl;
status = bisecIni (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO) error("error en bisecIni",status);
iter = 0;
do
iter++;
status = bisecIter (xcosx, xinf, xsup, fxinf, fxsup, raiz);
if (status != EXITO)
error("error en bisecIter",status);
status = testIntervalo (xinf,xsup,1.0);
if (status == EXITO)
cout << " El m etodo ha convergido"
<< "Iteraci on = "<<iter << endl;
if(nivel > NOINFO)
cout << iter << "t"
<< setw(12) << setprecision(7) <<
xinf << "t "
<< setw(12) << setprecision(7) <<
xsup << "t "
<< setw(12) << setprecision(7) <<
87
raiz << endl;
if (iter >= maxiter ) status = EMAXITER;

while (status == ITERANDO);


cout <<"Bisecci on encuentra " <<raiz << endl;
cout << "Tomamos "<<raiz<< " como valor inicial para Newton"
<<endl;
// Incializamos newton con el valor obtenido por Bisecci on
newtonIni (xcosx, Dxcosx, f, df, raiz);
iter = 0;
do
iter++;
double x0 = raiz ; // raiz en la iteraci on anterior
status = newtonIter (xcosx, Dxcosx, f, df, raiz);
status = testDelta (x0,raiz,epsrel);
if (status == EXITO)
cout << " El m etodo ha convergido" << endl;
if(nivel > NOINFO)
cout << iter << "t t"
<< raiz << endl;
if (iter >= maxiter ) status = EMAXITER;

while (status == ITERANDO);


analiza (status, raiz, iter );
return EXITO;

El resultado es ahora mucho m as agradable:


$ ./xnewton
El cero de la funci on se busca en el intervalo[a,b]
Introduzca el valor de a -3
Introduzca el valor de b 3
Que precisi on relativa desea? 1e-6
N umero m aximo de iteraciones? 100
Introduzca el nivel de informaci on que desea
(0 = NOINFO, 1 = CONCISO, 2 = DETALLADO) 1
Buscamos la ra z en el intervalo [-3,3]
Este programa calcula el cero de la funci on x-cos(x)
Por el m etodo de Newton-Raphson
iteraci on ra z
Llamada preliminar a Bisecci on
88
1 0 3 1.5
2 0 1.5 0.75
3 0 0.75 0.375
El m etodo ha convergido en la iteraci on = 4
4 0.375 0.75 0.5625
Bisecci on encuentra 0.5625
Tomamos 0.5625 como valor inicial para Newton
1 0.7473458
2 0.7391001
3 0.7390851
El m etodo ha convergido
4 0.7390851
La ra z es 0.73908513
N umero de iteraciones empleado 4
Cuando la aproximaci on en la que se basa es aceptable, el metodo de Newton-Raphson converge
muy r apidamente. En el ejemplo, necesita s olo cuatro iteraciones para alcanzar una precisi on relativa
de 10
6
. Si probamos bisecci on en el mismo intervalo ([0,375, 0,75]) y con la misma precisi on, requiere
diecinueve iteraciones para converger. Conviene resaltar, sin embargo, que la estrategia que hemos
adoptado no garantiza en absoluto la convergencia del metodo de Newton-Raphson. En un problema
real, tras la aproximaci on inicial a la raz, evaluaramos el comportamiento de las sucesivas aproxima-
ciones, para decidir si el metodo estaba convergiendo o si se necesitaba una intervenci on de urgencia
(por ejemplo una bisecci on). De hecho, la forma en que hemos programado los diferentes algoritmos
permiten, como acabamos de ver combinarlos para construir diferentes estrategias, seg un requiera el
problema que estemos estudiando.
Las funciones que codican el algoritmo (newtonIni y newtonIter) se declaran en newton.h y se
denen en newton.cpp. En este caso no utilizamos la funci on testIntervalo para comprobar si el
algoritmo ha convergido, sino otra muy similar, testDelta, que comprueba si:
[x
1
x
0
[ <
rel
[x
1
[
Donde x
1
es la aproximaci on a la raz en la iteraci on considerada y x
0
la aproximaci on anterior.
La funci on newtonIni se limita a calcular la funci on y su derivada en la raz aproximada:
int newtonIni (func F, func dF,
double& f, double& df, double& c)

f = F(c);
df = dF(c);
return EXITO;

Mientras que el algoritmo se codica en newtonIter:


int newtonIter (func F, func dF,
double& f, double& df, double& c)

if (df == 0.0)
return EZERODIV;
c = c - (f / df);
89
f = F(c);
df = dF(c);
if (!finite(f))
return EFUNC;
if (!finite (df))
return EFUNC;
return EXITO;

La funci on finite (declarada en <cfloat>) devuelve un entero diferente de cero (es decir el equivalente
a true) si su argumento es un n umero nito y cero (false) en otro caso. Su uso nos permite garantizar
que la funci on es continua en la aproximaci on a la raz y que la derivada es nita en dicho punto.
5.8. Resumen
En este captulo hemos estudiado en detalle tres metodos para buscar las races de una funci on. El
primero, bisecci on, converge siempre, aunque lo hace linealmente. Regula falsi, converge supralineal-
mente para funciones sucientemente suaves, pero puede converger m as lentamente que bisecci on en
algunos caso patol ogicos. El algoritmo m as r apido, Newton Raphson, requiere el conocimiento de la
derivada de la funci on y converge cuadr aticamente, siempre que estemos lo sucientemente cerca de
la raz y que la derivada no se anule en alguna de las iteraciones.
A la hora de programar estos algoritmos, hemos identicado diversas tareas. Algunas de ellas
comunes, tales como la de adquirir los datos iniciales, analizar los resultados o comprobar si se ha
alcanzado convergencia (ra ces.h, ra ces.cpp) y otras especcas(biseccion.h, biseccion.cpp,
rfalsi.h, rfalsi.cpp, newton.h, newton.cpp). Cada algoritmo se codica con dos funciones, una
para dar valores iniciales a los par ametros relevantes (primera aproximaci on a la raz, extremos del
intervalo, im agenes de los extremos, valor de la derivada en la raz aproximada, etc.) y otra que ejecuta
una iteraci on del algoritmo (una bisecci on, por ejemplo) cada vez que se la invoca. Finalmente hemos
escrito programas conductores, casi identicos para cada metodo, que se ocupan de llamar a las funciones
de inicio y an alisis, arrancar los algoritmos de races e iterar hasta que se alcanza convergencia o se
excede el n umero m aximo de iteraciones.
90
Captulo 6
Clases de vectores y matrices
6.1. Introducci on
Hemos visto que el tipo double representa en C++ un n umero real en doble precisi on. Repre-
senta quiere decir que el lenguaje proporciona una serie de reglas (casi siempre muy intuitivas) para
manipular objetos de tipo double que permiten transcribir operaciones matem aticas a lenguaje de
programaci on. De esta manera, es posible traducirf ormulas tales como:
a = sin(x) +
_
(x) + x
3
a c odigo C++:
double x;
double a = sin(x) + sqrt(x) + pow(x,3);
En lenguaje m aquina la sentencia anterior se traduce en docenas de instrucciones, que el progra-
mador de un lenguaje de alto nivel puede ignorar, con la misma tranquilidad con que el espectador
de una obra de teatro ignora la frenetica actividad, entre candilejas, de tramoyistas y gurantes. Por
ejemplo, el siguiente programa:
int main()
double a=10,b = 0.4;
double c = a*b;
return 0;

se traduce, en el lenguaje ensamblador generado por el compilador en:


_main:
pushl %ebp
movl %esp, %ebp
subl $40, %esp
andl $-16, %esp
movl $0, -32( %ebp)
movl -32( %ebp), %eax
call __alloca
call ___main
movl $0, -8( %ebp)
91
movl $1076101120, -4( %ebp)
movl $-1717986918, -16( %ebp)
movl $1071225241, -12( %ebp)
fldl -8( %ebp)
fmull -16( %ebp)
fstpl -24( %ebp)
movl $0, %eax
movl %ebp, %esp
popl %ebp
ret
No es necesario entender que signican las instrucciones anteriores para darse cuenta de la complejidad
que es posible ocultar gracias al uso de una abstracci on apropiada (asociar el tipo double a un n umero
real y proporcionar la maquinaria para manipularlo como tal).
Consideremos ahora los n umeros complejos. Algebraicamente se trata de un cuerpo que, de hecho,
contiene al de los n umeros reales. Siendo z
1
, z
2
un par de complejos, la f ormula:
z
1
= sin(z
2
) +
_
(z
2
) + z
3
2
tiene perfecto sentido matem atico. Sin embargo, en C el tipo complejo no existe y por lo tanto no
es posible traducir la f ormula anterior de manera directa a lenguaje de alto nivel, escribiendo algo
as como:
complejo z1;
complejo z2 = sin(z1) + sqrt(z1) + pow(z1,3);
En general, decimos que un lenguaje es de alto nivel cuando permite una gran abstracci on. Len-
guajes como FORTRAN, C, C++ o Java, calican, en mayor o menor medida como tales. Existe, sin
embargo, una diferencia fundamental entre ellos. Mientras que FORTRAN apenas proporciona meca-
nismos que permitan al programador abstraer sus propios tipos (C es ligeramente superior, aunque
tambien bastante limitado a este respecto) C++ y Java disponen de una considerable artillera al
efecto. De esta manera, cuando identicamos en C++ la necesidad de un nuevo tipo (por ejemplo uno
que describa los n umeros complejos
1
) el lenguaje proporciona los mecanismos para crearlo.
En este captulo presentamos una breve introducci on a dichos mecanismos.
6.2. Estructuras y clases
6.2.1. Estructuras revisitadas
En el captulo 3 introdujimos el concepto de estructura de datos para representar un agregado de
elementos de diferentes tipos. Comentamos, como ejemplo, una estructura util para representar una
agenda de direcciones:
struct agenda
string nombre;
string calle;
int numero ;
string ciudad;
int telefono;
1
El tipo complejo no es intrnseco a C++ estrictamente hablando, pero est a denido en el Biblioteca Est andar y por
lo tanto, en la pr actica, podemos usar complejos en C++ como un tipo fundamental.
92
string email;
;
Una estructura (tipo struct) permite denir un nuevo tipo, en este caso el tipo agenda. Una
variable tipo agenda puede declararse exactamente igual que otras variables. As mismo, pueden
denirse punteros y/o referencias a esta, utilizarse como argumento o valor devuelto por una funci on,
etc., etc.
El operador .(punto) permite acceder a los miembros del agregado. Por ejemplo, el c odigo:
...
agenda constantino; // declara que la variable es de tipo agenda
constantino.nombre = "Konstantino Kavafis";
constantino.calle = "camino de Itaca";
...
constantino.telefono = 29051933;
declara la variable constantino como de tipo agenda y asigna valores a cada uno de sus miembros.
Si en lugar de una estructura estamos manejando un puntero a una estructura, usamos el operador
-> en lugar del punto. Por ejemplo:
...
agenda* cavafis = &constantino; // cavafis es un puntero a agenda
cout << "El nombre del gran poeta griego es" << cavafis->nombre << endl;
Recordemos que si p es un puntero a cierta variable, entonces *p accede al contenido de esta. La
variable cavafis que acabamos de denir es un puntero al tipo agenda, por tanto *cavafis accede
al contenido de la variable y en consecuencia (*cavafis).nombre devuelve el miembro nombre del
agregado. La notaci on cavafis->nombre no es sino un sin onimo para (*cavafis).nombre.
6.2.2. Funciones de acceso
Siguiendo con el ejemplo anterior. Supongamos que queremos escribir un programa que administre
una base de datos, usando variables de tipo agenda. Para ello, resulta conveniente preparar una serie
de funciones que nos permitan manipular sus miembros, en lugar de acceder directamente a estos. Por
ejemplo:
...
// funci on para inicializar la estructura
void iniAgenda(agenda&, string nombre, string calle, int numero,
string ciudad, int telef, string mail);
// funci on para cambiar el miembro nombre
void cambiaNombre(agenda&, string nombre);
// funci on para devolver el miembro nombre
string devuelveNombre(const agenda&);
// funci on para cambiar el miembro calle
void cambiaCalle(agenda&, string calle);
// funci on para devolver el miembro calle
string devuelveCalle(const agenda&);
...
Cada una de estas funciones toma una referencia al tipo agenda. En el caso de las funciones cuyo
unico cometido es devolver un miembro de la estructura, la referencia es constante, para evitar que
93
dicho valor pueda cambiarse accidentalmente. El uso de estas funciones, nos permite encapsular el
manejo de los miembros internos de agenda. Por ejemplo, la funci on de inicializaci on sera:
...
void iniAgenda(agenda& a, string nombre, string calle, int numero,
string ciudad, int telef, string mail)

a.nombre = nombre;
a.calle = calle;
...
a.mail = mail;

mientras que la funci on devuelveNombre, sera:


...
string devuelveNombre(const agenda&)

return agenda.nombre;

La raz on por la que estamos obligados a pasar una referencia a las funciones que manejan los datos
de la estructura es que no existe conexi on entre estas y aquellos. Por otra parte, C++ nos permite
establecer esa conexi on, sin m as que declarar las funciones que manipulan los datos de agenda como
miembros de la estructura. Tendramos entonces:
struct agenda
// Datos
string nombre;
...
string email;
//Funciones
// funci on para inicializar la estructura
void iniAgenda(string nombre, string calle, int numero,
string ciudad, int telef, string mail);
// funci on para cambiar el miembro nombre
void cambiaNombre(string nombre);
// funci on para devolver el miembro nombre
string devuelveNombre() const;
// funci on para cambiar el miembro calle
void cambiaCalle(string calle);
// funci on para devolver el miembro calle
string devuelveCalle() const;
...
;
Con esta nueva versi on ya no es necesario pasar a las funciones la referencia a agenda, ya que cada
copia de un objeto de tipo agenda posee dichas funciones adem as de sus datos. Es decir:
...
agenda antonio; // antonio es una variable de tipo agenda
94
antonio.iniAgenda("Antonio Machado", "Juan de Mairena",7,
"Soria",96775566,"antonio@parnaso.heaven");
...
cout << "El gran poeta se llamaba" << antonio.devuelveNombre() << endl;
Reparemos en el calicativo const que aparece tras el nombre de las funciones que devuelven los
miembros de la estructura:
string devuelveNombre() const;
string devuelveCalle() const;
Recordemos que estas funciones, por denici on, no deben alterar los miembros de la estructura cuyo
contenido devuelven. Cuando se trataba de funciones externas a la estructura garantiz abamos este
requerimiento pasando una referencia constante a la estructura. En el caso de funciones internas (que
tienen acceso directo a su copia de la estructura) el calicativo const impone que las funciones no
puedan modicar el valor de los miembros de la estructura.
Veamos un ejemplo. Supongamos que no hubieramos declarado la funci on devuelveNombre con el
calicativo const y que su denici on fuera:
string devuelveNombre()

calle = "Un patio de Sevilla";


return nombre;

El compilador no detecta error alguno en este c odigo, a pesar de que la funci on hace algo que no
debera hacer (cambiar el miembro calle antes de devolver el miembro nombre). Cuando ejecut aramos
el siguiente c odigo:
...
agenda antonio; // antonio es una variable de tipo agenda
antonio.iniAgenda("Antonio Machado", "Juan de Mairena",7,
"Soria",96775566,"antonio@parnaso.heaven");
// El valor de calle es "Juan de Mairena"
...
cout << "El gran poeta se llamaba" << antonio.devuelveNombre() << endl;
// El valor de calle ahora es "Un patio de Sevilla"
Precisamente, para evitar errores de este tipo
2
declaramos la funci on devuelveNombre como const.
En este caso el c odigo:
string devuelveNombre() const

calle = "Un patio de Sevilla"; // error de compilaci on, modifica miembro calle
return nombre;

resulta en un error de compilaci on, puesto que la funci on no puede modicar ninguno de los miembros
de la estructura.
2
Pero, podramos argumentar, el despierto programador nunca comete errores tan est upidos. Ay, la tercera ley de
la inform atica, que coincide con la primera ley de la sociologa estipula que la estupidez de humanos y programadores
(ambos tipos no son siempre identicos) no est a acotada superiormente.
95
6.2.3. Encapsulamiento, apantallamiento de datos y clases
Por otra parte, a pesar de las molestias que nos hemos tomado en denir un conjunto de funciones
que encapsulen las manipulaciones de los datos de nuestra estructura, podemos seguir accediendo
directamente a estos. Nada nos impide escribir:
...
agenda miguel;
miguel.iniAgenda("Miguel de Cervantes", "Un lugar de la Mancha",12,
"No quiero acordarme",964575566,"miguel@parnaso.heaven");
cout << "El genial autor del Quijote se llamaba " << miguel.nombre;
Lo cual no es necesariamente un problema, pero resulta, como mnimo, poco elegante. Puesto que
hemos denido funciones que manipulan los miembros internos de la estructura, sera preferible que
s olo estas tuvieran acceso a dichos miembros. Haciendolo as, las funciones internas a la estructura
son las unicas que dependen de la representaci on interna de esta, mientras que el resto del programa
s olo ve la interfaz que dichas funciones proporcionan.
La decisi on de ocultar o apantallar la representaci on interna de una estructura de datos al resto
del programa, proporcionando en su lugar una interfaz va funciones p ublicas marca la transici on del
tipo struct al tipo class. Una clase no es otra cosa que una estructura que distingue entre miembros
p ublicos (accesibles desde cualquier parte del programa) y privados (accesibles s olo por las funciones
de la clase).
clase agenda
// Datos
private: // indica que los miembros a continuaci on son privados
string nombre;
...
string email;
public: // indica que los miembros a continuaci on son p ublicos
//Funciones
// funci on para inicializar la estructura
void iniAgenda(string nombre, string calle, int numero,
string ciudad, int telef, string mail);
// funci on para cambiar el miembro nombre
void cambiaNombre(string nombre);
// funci on para devolver el miembro nombre
...
;
Las etiquetas public y private se utilizan para distinguir entre los miembros p ublicos de la clase (la
interfaz al resto del mundo) y los miembros privados (accesibles s olo por las funciones de la clase). La
transici on de struct a class no nos obliga a reescribir las funciones de la clase ya que estas siguen
pudiendo acceder directamente a los miembros privados. Pero el c odigo:
...
agenda miguel;
miguel.iniAgenda("Miguel de Cervantes", "Un lugar de la Mancha",12,
"No quiero acordarme",964575566,"miguel@parnaso.heaven");
// error, nombre es privado
96
cout << "El genial autor del Quijote se llamaba " << miguel.nombre;
...
resulta ahora en un error de compilaci on, ya que el miembro nombre, es ahora privado, o interno a la
clase.
6.2.4. Constructores
Aunque podemos usar funciones de inicializaci on (tales como iniAgenda) para inicializar los da-
tos de una clase resulta preferible utilizar un mecanismo estandarizado para este prop osito
3
. C++
proporciona unas funciones especiales, llamadas constructores, cuyo nombre es identico al de la clase.
Para una clase dada pueden denirse tantos constructores como se desee. Por ejemplo:
clase agenda
// Datos
private: // indica que los miembros a continuaci on son privados
...
public: // indica que los miembros a continuaci on son p ublicos
// Declaraci on de los constructores
// constructor por defecto. No toma argumentos
agenda();
// constructor que toma nombre como unico argumento
agenda(string nombre);
// constructor m as general (reemplaza iniAgenda)
agenda(string nombre, string calle, int numero,
string ciudad, int telef, string mail);
//Funciones
// funci on para cambiar el miembro nombre
void cambiaNombre(string nombre);
// funci on para devolver el miembro nombre
string devuelveNombre() const;
...
;
...
...
// Definici on de los constructores (en otra parte del programa)
// constructor por defecto
agenda::agenda()
// valores por defecto
nombre = ""; //
calle = "";
numero = 0;
ciudad = " ";
telefono = 0;
email = " ";

// constructor que toma el nombre


agenda::agenda(string nombre)
3
No es un capricho. Si nadie nos obliga a invocar a la funci on iniAgenda, podemos olvidarnos tranquilamente de
inicializar un objeto de tipo agenda o inicializarlo m as de una vez, a menudo con identicos desastrosos resultados.
97
// valores por defecto
nombre = nombre; // toma el nombre que le pasamos
calle = ""; // las dem as variables a cero
numero = 0;
ciudad = " ";
telefono = 0;
email = " ";

// constructor m as general
agenda::agenda(string nombre, string calle, int numero,
string ciudad, int telef, string mail)
nombre = nombre;
calle = calle ;
...
email = email;

Notad que utilizamos el mismo nombre para las variables internas a la clase (nombre, calle, etc.) y a
las variables que pasamos como argumentos. Para el compilador esto no es un problema ya que sabe
distinguir por el contexto, pero los humanos, a menudo menos h abiles, pueden confundirse f acilmente
con la notaci on. Una buena costumbre es etiquetar los miembros internos (privados) de una clase
mediante alg un tipo de convenci on. Podemos, por ejemplo, escribirlos todos con un p min uscula
como primera letra (p de privado) seguido del nombre de la variable empezando por may uscula. Por
ejemplo pNombre en lugar de nombre, pCalleen lugar de calle, etc.
Otro detalle importante. La denici on de la funci on constructor (al igual que del resto de las funcio-
nes de la clase) se suele escribir en un chero aparte. Tpicamente, escribiremos una cabecera, (ejemplo,
agenda.h) con la denici on de la clase (donde declaramos los constructores y dem as funciones) y otro
chero (agenda.cpp) donde denimos las funciones de la clase. Puesto que las funciones de clases
diferentes pueden llamarse igual, utilizamos el calicador nombre-de-la-clase::nombre-funci on,
en nuestro ejemplo, agenda::agenda() para el constructor por defecto y as sucesivamente
4
.
6.2.5. Funciones de lectura y escritura
Examinemos ahora las funciones:
...
// funci on para cambiar el miembro nombre
void cambiaNombre(string nombre);
// funci on para devolver el miembro nombre
string devuelveNombre() const;
La primera, cambiaNombre, nos permite cambiar el valor del atributo interno de la clase (pNombre).
La segunda devuelveNombre nos devuelve el valor del atributo interno.
Veamos una alternativa para la funci on cambiaNombre:
string& agenda::cambiaNombre()

return pNombre;
4
Un error arquetpico, cometido a menudo por el autor de estas notas es olvidar el prejo con el nombre de la clase
en las deniciones de las funciones.
98

Si ahora escribimos:
...
// Creamos el objeto antonio
agenda antonio("Machado"); // ahora pNombre = "Machado"
antonio.cambiaNombre() = "Antonio"; // ahora pNombre = "Antonio"
En efecto, la funci on cambiaNombre devuelve una referencia a string, que asignamos en el c odigo a
Antonio. La variable con la que hemos inicializado la referencia es la devuelta por la funci on, es
decir, pNombre. Desde el punto de vista del compilador, hemos hecho lo siguiente:
string pNombre; // la variable que mandamos en el return
string& r = pNombre; // tipo devuelto por la funci on
r = "Antonio"; // por lo tanto pNombre = "Antonio"
Puesto que una referencia es un sin onimo al objeto al que reere, cambiar esta resulta en cambiar
aquel.
Por que esta versi on de cambiaNombre resulta preferible a la antigua? Una raz on inmediata es que
nos permite simplicar mucho la notaci on. Veamos c omo. Para ello, escribamos primero otra versi on
de la funci on de lectura, devuelveNombre:
const string& agenda::devuelveNombre() const

return pNombre;

La nueva versi on de nuestra funci on de lectura devuelve una referencia constante al tipo string. Si
intent aramos escribir ahora:
...
// Creamos el objeto antonio
agenda antonio("Machado"); // ahora pNombre = "Machado"
antonio.devuelveNombre() = "Antonio"; // error, referencia constante
El astuto compilador detecta que intentamos hacer algo equivalente a lo siguiente:
string pNombre; // la variable que mandamos en el return
const string& r = pNombre; // tipo devuelto por la funci on
r = "Antonio"; // imposible, r es una referencia constante
// no puede cambiarse
Es decir, cambiar el valor del objeto al que reere una referencia constante, lo cual es ilegal (una
referencia constante, como su nombre indica no puede cambiar el valor del objeto al que reere). El
resultado es un error de compilaci on, como es l ogico, ya que, si queremos cambiar la variable debemos
usar cambiaNombre en lugar de devuelveNombre.
Por otra parte, si escribimos:
...
cout << "el nombre de pila de Machado era " << antonio.devuelveNombre();
99
El compilador asiente, satisfecho, ya que cout no intenta modicar el valor de pNombre, limit andose
a exhibirlo por la pantalla.
Podemos, por n, sacar un peque no conejo de la atiborrada chistera del lenguaje C++. Si usamos
el mismo nombre para referirnos a las funciones cambiaNombre y devuelveNombre:
string& nombre(); // permite modificar el valor de pNombre
// a trav es de una referencia
const string& nombre () const; // devuelve el valor de pNombre
// no puede modificarse
el inteligentsimo compilador sabe distinguir entre una funci on y la otra por el contexto (se da cuenta
de que son diferentes, ya que devuelven objetos diferentes). Vimos, cuando hablamos de constructores,
la misma habilidad de distinguir entre funciones diferentes (diferentes constructores) a partir del hecho
de que tomaban diferentes argumentos. En C++, en resumen, es posible sobrecargar
5
un nombre para
referirse a dos funciones distintas.
Podemos ahora escribir:
...
agenda antonio("Machado");
// usa la versi on de nombre que devuelve una referencia
// string& nombre();
antonio.nombre() = "Antonio";
// usa la versi on constante de nombre que devuelve una referencia constante
// const string& nombre() const;
cout << "el nombre de pila de Machado era "
<< antonio.nombre();
El compilador sabe que funci on elegir en cada caso.
Una ultima convenci on. Es una buena costumbre utilizar nombres que empiezan por may uscula
para referirnos a una clase, a n de distinguir a simple vista entre el tipo y la variable. Por ejemplo,
una sentencia como:
agenda agenda; // crea un objeto, de nombre agenda y tipo agenda
es un poco confusa. En cambio:
Agenda agenda; // el nombre del objeto es agenda, el tipo Agenda
es m as clara.
He aqu una versi on completa de la clase Agenda (que escribimos en el chero agenda.h).
// agenda.h
// Definici on de una clase para representar una agenda
#ifndef _AGENDA_
#define _AGENDA_
#include <iostream> // sobreescritura del operador <<
#include <string>
using namespace std;
class Agenda
5
Quiz as sera m as correcto decir que abusamos de un nombre pero la lengua inglesa, siempre tan prudente, utiliza
el termino overload (sobrecarga) para referirse a este mecanismo.
100
// miembros internos de la clase
private:
string pNombre;
string pCalle;
int pNumero ;
string pCiudad;
int pTelefono;
string pEmail;
public:
// constructores
// defecto
Agenda();
// general
Agenda(string nombre, string calle, int numero,
string ciudad, int telefono, string email);
// funciones de lectura y escritura
const string& nombre() const;
string& nombre();
const string& calle() const;
string& calle();
const int& numero() const;
int& numero();
const string& ciudad() const;
string& ciudad();
const int& telefono() const;
int& telefono();
const string& email() const;
string& email();
;
#endif /* _AGENDA__ */
La denici on de las funciones (en agenda.cpp) es la siguiente:
//agenda.cpp
// definci on de las funciones de la clase Agenda
#include <agenda.h>
// constructores
Agenda::Agenda()
pNombre = "";
pCalle = "";
pNumero = 0;
pCiudad = "" ;
pTelefono = 0;
pEmail = "";

Agenda::Agenda(string nombre, string calle, int numero,


string ciudad, int telefono, string email)
pNombre = nombre;
pCalle = calle;
101
pNumero = numero;
pCiudad = ciudad ;
pTelefono = telefono;
pEmail = email;

// funciones de lectura y escritura


const string& Agenda::nombre() const
return pNombre;

string& Agenda::nombre()
return pNombre;

const string& Agenda::calle() const


return pCalle;

string& Agenda::calle()
return pCalle;

const int& Agenda::numero() const


return pNumero;

int& Agenda::numero()
return pNumero;

const string& Agenda::ciudad() const


return pCiudad;

string& Agenda::ciudad()
return pCiudad;

const int& Agenda::telefono() const


return pTelefono;

int& Agenda::telefono()
return pTelefono;

const string& Agenda::email() const


return pEmail;

string& Agenda::email()
return pEmail;

Por ultimo, he aqu un peque no programa para demostrar su uso:


// xagenda.cpp
// Ejemplo de uso de la clase Agenda
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
// control de errores
102
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Clase Agenda
#include <agenda.h>
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

// Constructor est andar;


Agenda p; // p es de tipo Agenda inicializado a cero
cout << "introduzca el nombre del poeta "; cin>>p.nombre();
cout << "introduzca la calle del poeta "; cin>>p.calle();
cout << "introduzca el n umero del portal "; cin>>p.numero();
cout << "introduzca la ciudad "; cin>>p.ciudad();
cout << "introduzca el tel efono "; cin>>p.telefono();
cout << "introduzca el e-mail "; cin>>p.email();
cout << "-------------------------" <<endl<<endl;
cout << "El nombre del poeta es " << p.nombre()<< endl;
cout << "La calle del poeta es " << p.calle()<< endl;
cout << "El n umero del portal es " << p.numero()<< endl;
cout << "La ciudad es " << p.ciudad()<< endl;
cout << "El tel efono " << p.telefono()<< endl;
cout << "El e-mail " << p.email()<< endl;
return EXITO;

Que, al ejecutarlo, ofrece la siguiente conversaci on:


$ ./xagenda
introduzca el nombre del poeta Aleixandre
introduzca la calle del poeta Espadas
introduzca el n umero del portal 23
introduzca la ciudad ComoLabios
introduzca el tel efono 2233344
introduzca el e-mail Vicente@parnaso.heaven
-------------------------
El nombre del poeta es Aleixandre
La calle del poeta es Espadas
El n umero del portal es 23
La ciudad es ComoLabios
El tel efono 2233344
El e-mail Vicente@parnaso.heaven
103
6.3. La clase Complejo
6.3.1. Un tipo complejo
Veamos ahora un ejemplo m as realista del uso de clases para denir y manejar un nuevo tipo
que nos permita representar las propiedades matem aticas de los complejos
6
. En particular debemos
ser capaces de especicar las operaciones algebraicas elementales, es decir, debemos poder traducir
f ormulas del tipo:
z
1
= a + ib
z
2
= c + id
z
3
= z
1
+ z
2
a c odigo parecido a este:
double a,b;
complejo z1(a,b); // a y b son las partes real e imaginaria de z1
complejo z2(c,d); // c y d son las partes real e imaginaria de z2
complejo z3 = z1 + z2;
Veamos como. Escribimos la denici on de la clase en un chero de cabecera, complejo.h, cuyo
contenido es:
// complejo.h
// Definici on de una clase de n umeros complejos
// Implementaci on en complejo.cpp
#ifndef _COMPLEJO__
#define _COMPLEJO__
#include <iostream> // sobrecarga del operador <<
using namespace std;
class Complejo
// miembros internos
private:
double pReal; // parte real
double pImag; // parte imaginaria
//Funciones de acceso
public:
// Constructores
// Constructor por defecto: z = 0 + i0
Complejo();
// Constructor est andar z = x + iy
Complejo(double x, double y);
// Funciones de lectura (devuelven un valor constante)
const double& real() const; // devuelve la parte real, Re(z)
6
Se trata de un ejemplo academico, como ya hemos comentado, el tipo complex est a denido en la Biblioteca Est andar
de C++.
104
const double& imag() const; // devuelve la parte imaginaria, Im(z)
// Funciones de escritura (permiten modificar atributos internos)
double& real(); // asigna la parte real, Re(z)
double& imag(); // asigna la parte imaginaria, Im(z)
;
// Operadores externos a la clase
Complejo operator - (Complejo z) ; // -z ;
Complejo operator + (Complejo z1, Complejo z2); // z3 = z1+z2
Complejo operator - (Complejo z1, Complejo z2); // z3 = z1-z2
Complejo operator * (Complejo z1, Complejo z2); // z3 = z1*z2
Complejo operator / (Complejo z1, Complejo z2); // z3 = z1/z2
// Funciones externas
double modulo(Complejo z); // modulo
double modulo2(Complejo z); // modulo al cuadrado
Complejo conjugado(Complejo z); // Complejo conjugado
Complejo inverso(Complejo z); // z z
1
= 1
// operador de escritura
ostream& operator << (ostream& s, Complejo z);
#endif /* _COMPLEJO___ */
Seguimos la convenci on, anotada en la secci on anterior de utilizar un nombre en may usculas (Com-
plejo, no complejo) para el nombre de la clase (y por lo tanto para el tipo).
class Complejo
...
;
Los miembros privados son:
private:
double pReal; // parte real
double pImag; // parte imaginaria
donde seguimos la convenci on, anotada anteriormente de escribir nombres que comienzan por p
seguidos por una may uscula. Los n umeros reales (tipo double) pReal y pImag representan las partes
reales e imaginarias del complejo.
Hemos declarado dos constructores:
// Constructores
// Constructor por defecto: z = 0 + i0
Complejo();
// Constructor est andar z = x + iy
Complejo(double x, double y);
El constructor por defecto inicializa el complejo a 0, mientras que el constructor est andar, lo inicializa
con los dos n umeros reales que toma en el argumento:
105
// Constructores
// Crea un n umero complejo z = 0 + i0
Complejo::Complejo()

pReal = 0;
pImag = 0;

// Crea un n umero Complejo z = x + iy


Complejo::Complejo(double real, double imag)

pReal = real;
pImag = imag;

Recordamos que la notaci on Complejo:: es necesaria para indicar al compilador que las funciones a
las que nos referimos (los constructores, en este caso) pertenecen a la clase Complejo.
Recordad tambien que podemos escribir tantos constructores como estimemos necesario. Podramos,
por ejemplo, haber a nadido a nuestra clase un constructor que tomara los argumentos del complejo
en polares (, ) en lugar de en coordenadas rectangulares.
Usamos los constructores para crear nuevos objetos de tipo Complejo. Por ejemplo (en xcomplejo.cpp):
...
// Constructor est andar;
double real, imag;
cout << "introduzca la parte real de z1 "; cin>>real;
cout << "introduzca la parte imaginaria de z1 "; cin>>imag;
Complejo z1(real,imag);
cout << "creado n umero complejo z1 = " << z1 << endl;
...
// Constructor por defecto
Complejo z2;
cout << "creado n umero complejo z2 = " << z2 << endl;
Conviene, en este punto, claricar la diferencia entre clase y objeto. Podemos visualizar una
clase como una especie de molde, o patr on, para un tipo dado. As, en el ejemplo que nos ocupa, la
denici on de la clase especica que el tipo Complejo tiene dos atributos internos (pReal y pImag). Un
objeto es una instancia de la clase, fabricado a partir del molde que la clase especica. Cada objeto
contiene su propia copia tanto de los atributos internos (pReal = 0, pImag = 0 para el objeto z1,
pReal = 10, pImag = 3 para el objeto z2), como de las funciones de la clase.
Tras los constructores, encontramos las funciones internas a la clase:
// Funciones de lectura (devuelven un valor constante)
const double& real() const; // devuelve la parte real, Re(z)
const double& imag() const; // devuelve la parte imaginaria, Im(z)
// Funciones de escritura (permiten modificar atributos internos)
double& real(); // asigna la parte real, Re(z)
double& imag(); // asigna la parte imaginaria, Im(z)
106
Que, al igual que los constructores, estas sobrecargadas, existiendo dos declaraciones diferentes para
cada una de ellas. Como vimos en la secci on anterior, la funci on:
const double& real() const;
y la funci on:
double& real();
son diferentes. Ambas devuelven una referencia a un objeto de tipo double, pero en un caso la
referencia es constante y en el otro no. Sin embargo, como ya hemos anotado, el compilador es capaz
de distinguir por el contexto (los argumentos con que llamamos a la funci on) a que versi on nos estamos
reriendo.
Una de las versiones devuelve las partes reales e imaginarias del n umero a traves de una referen-
cia constante (que por lo tanto no puede asignarse). La funci on se declara const, evitando as una
modicaci on accidental de los atributos de la clase:
// Devuelve la parte real de z
const double& Complejo::real() const

return pReal;

// Devuelve la parte imaginaria de z


const double& Complejo::imag() const

return pImag;

Reparad, de nuevo, en la notaci on Complejo:: que indica que las funciones pertenecen a esta clase.
En xcomplejo.cpp vemos un ejemplo de su uso:
...
// Devuelve parte real e imaginaria de z2
cout << " Re(z2) = " << z2.real()
<< " Im(z2) = " << z2.imag()
<< endl;
La segunda versi on de estas funciones:
// Asigna la parte real de z
double& Complejo::real()

return pReal;

// Asigna la parte imaginaria de z


double& Complejo::imag()

return pImag;

107
permiten asignar los miembros pReal y pImag a traves de las referencias a double que devuelven.
Cuando escribimos, por ejemplo:
...
// Asigna parte real e imaginaria
cout << "introduzca la parte real de z2 ";
cin>> z2.real();
cout << "introduzca la parte imaginaria de z2 ";
cin >> z2.imag();
El resultado es equivalente a:
...
double pReal; // la copia de pReal que tiene z2
double& r = pReal; // el valor devuelto por la funci on real()
cin >> r; // equivale a cin >> z2.pReal
...
double pImag; // la copia de pImag que tiene z2
double& r = pImag; // el valor devuelto por la funci on imag()
cin >> r; // equivale a cin >> z2.pImag
6.3.2. Funciones externas
Tras la declaraci on de la clase encontramos una serie de operadores y funciones externas a la clase.
...
// Operadores externos a la clase
Complejo operator - (Complejo z) ; // -z ;
Complejo operator + (Complejo z1, Complejo z2); // z3 = z1+z2
Complejo operator - (Complejo z1, Complejo z2); // z3 = z1-z2
Complejo operator * (Complejo z1, Complejo z2); // z3 = z1*z2
Complejo operator / (Complejo z1, Complejo z2); // z3 = z1/z2
// Funciones externas
double modulo(Complejo z); // modulo
double modulo2(Complejo z); // modulo al cuadrado
Complejo conjugado(Complejo z); // Complejo conjugado
Complejo inverso(Complejo z); // z z
1
= 1
// operador de escritura
ostream& operator << (ostream& s, Complejo z);
Una funci on (u operador) se declara como externo (fuera de las llaves que denen la clase), cuando
no necesita acceso a la representaci on interna de la clase. Las funciones real() e imag() que ya hemos
comentado necesitan acceder a pReal y pImag, raz on por la que las hemos declarado como internas a
la clase. Sin embargo una funci on como conjugado, que calcula el complejo conjugado de un n umero
complejo, puede utilizar a su vez las funciones p ublicas real() e imag():
...
Complejo conjugado(Complejo z)

return Complejo(z.real(),-z.imag());
108

El conjugado de un n umero complejo es a su vez otro n umero complejo, raz on por la que la funci on
conjugado crea un nuevo objeto de tipo Complejo, y lo inicializa con Re(z) y -Imag(z) (ya que el
conjugado de a + ib es a ib). El tipo que devuelve la funci on es, l ogicamente, Complejo. Notad que
en este caso no utilizamos referencias, puesto que creamos en la funci on un objeto que no exista
anteriormente que es el que devolvemos. Reparad tambien en que la denici on de la funci on no lleva
el prejo Complejo::, como es l ogico, ya que no se trata de una funci on de la clase, aunque manipule
objetos de esta.
An alogamente, denimos las dem as funciones externas:
double modulo(Complejo z)

return sqrt(modulo2(z));

double modulo2(Complejo z)

return cuadrado(z.real()) + cuadrado(z.imag()) ;

Complejo inverso(Complejo z)

double escala = 1/modulo2(z);


return Complejo(z.real()*escala,-z.imag()*escala);

Notad que la funci on modulo e inverso utilizan la funci on modulo2, lo cual es perfectamente legal
puesto que todas las funciones se han declarado previamente (en complejo.h).
Nos queda, por ultimo, comentar los operadores:
...
// Operadores externos a la clase
Complejo operator - (Complejo z) ; // -z ;
Complejo operator + (Complejo z1, Complejo z2); // z3 = z1+z2
Complejo operator - (Complejo z1, Complejo z2); // z3 = z1-z2
Complejo operator * (Complejo z1, Complejo z2); // z3 = z1*z2
Complejo operator / (Complejo z1, Complejo z2); // z3 = z1/z2
...
// operador de escritura
ostream& operator << (ostream& s, Complejo z);
La palabra operator es una de las palabras reservadas de C++ y se utiliza para sobrecargar
operadores (en este caso los operadores =, +, , , / y el operador <<).
Para entender este punto, consideremos, por ejemplo, el operador suma. Cuando los objetos que se
suman son dos n umeros reales (tipo double o float) el operador suma (+) crea un tercer n umero real
que contiene la adici on de ambos. En el caso del tipo Complejo deseamos que el operador suma nos
permita representar la suma de dos complejos, es decir la adici on de las partes reales e imaginarias
de estos. Si se trata de un tipo, que, por ejemplo, represente vectores, querremos que el operador
suma cree un nuevo vector cuyos elementos sean la suma de los elementos que componen los vectores
sumados y as sucesivamente. Es decir, la acci on del operador (+) depende de los tipos que maneja.
109
Como de costumbre, el compilador es capaz de distinguir por el contexto y, en consecuencia es posible
sobrecargar el operador (+). Los mismo podemos decir de los dem as operadores, en particular del
operador de escritura (<<) que nos permite encadenar distintos objetos al ujo de salida de datos
est andar cout.
Para entender la sintaxis de la declaraci on de operadores, consideremos de nuevo el operador suma.
La declaraci on:
Complejo operator + (Complejo z1, Complejo z2); // z3 = z1+z2
Es equivalente a declarar una funci on:
Complejo suma(Complejo z1, Complejo z2); // z3 = z1+z2
As mismo, la denici on:
Complejo operator + (Complejo z1, Complejo z2)

double real = z1.real() + z2.real();


double imag = z1.imag() + z2.imag();
return Complejo(real,imag);

es equivalente a:
Complejo suma (Complejo z1, Complejo z2)

double real = z1.real() + z2.real();


double imag = z1.imag() + z2.imag();
return Complejo(real,imag);

Y el uso del operador suma:


...
cout << "compruebo suma, z1 + z2 = "
<< z1 + z2 << endl;
equivalente a usar una funci on:
...
cout << "compruebo suma, z1 + z2 = "
<< suma(z1,z2) << endl;
Pero, innegablemente, la notaci on z1+z2 es m as concisa e intuitiva que suma(z1,z2).
An alogamente a la suma, denimos los operadores resta, multiplicaci on y divisi on:
Complejo operator - (Complejo z1, Complejo z2)

double real = z1.real() - z2.real();


double imag = z1.imag() - z2.imag();
return Complejo(real,imag);
110

Complejo operator * (Complejo z1, Complejo z2)

double real = z1.real() * z2.real() - z1.imag() * z2.imag();


double imag = z1.real() * z2.imag() + z1.imag() * z2.real();
return Complejo(real,imag);

Complejo operator / (Complejo z1, Complejo z2)

return z1*inverso(z2);

Resaltemos que todos estos operadores son binarios, es decir toman dos argumentos. Hemos denido
adem as un operador unario (un s olo argumento), el de cambio de signo (-z):
Complejo operator - (Complejo z)

double real = -z.real() ;


double imag = -z.imag();
return Complejo(real,imag);

Finalmente, la denici on del operador de escritura (<<) es:


ostream& operator << (ostream& s, Complejo z)

s << "( " << z.real() << ", " << z.imag() << " )";
return s;

Una funci on equivalente a este operador sera:


ostream& escribe (ostream& s, Complejo z)

s << "( " << z.real() << ", " << z.imag() << " )";
return s;

que toma como argumentos una referencia a un objeto de tipo ostream (output stream es decir ujo
de salida) y un objeto de tipo Complejo, devolviendo a su vez una referencia a ostream.
Cuando encadenamos un objeto de tipo Complejo a cout va el operador <<:
...
Complejo z;
cout << z1 << endl;
El efecto es equivalente a llamar a la funci on escribe con argumentos:
...
111
Complejo z;
cout << escribe(cout,z) << endl;
Recordemos que cout es un ujo de salida de datos y que su declaraci on (ostream cout;) se encuentra
en <iostream>.
Dentro de la funci on escribe (o el operador <<) escribimos las partes real e imaginaria del
complejo (que hemos tomado en el argumento) al ujo de salida (que tambien hemos tomado en el
argumento). Finalmente devolvemos una referencia al ujo que asignamos a cout, resultando en la
salida por pantalla.
El programa xcomplejo.cpp es un ejemplo de c omo usar el tipo Complejo. Notad el uso de los dos
constructores, las funciones sobrecargadas real() e imag()y la utilizaci on de operadores +, , , /, <<
aplicados a n umeros complejos.
// xcomplejo.cpp
// Banco de pruebas de la clase complejo
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <iomanip>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Clase complejos
#include <complejo.h>
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

// Constructor est andar;


double real, imag;
cout << "introduzca la parte real de z1 "; cin>>real;
cout << "introduzca la parte imaginaria de z1 "; cin>>imag;
Complejo z1(real,imag);
cout << "creado n umero complejo z1 = " << z1 << endl;
// Devuelve parte real e imaginaria de z1
cout << " Re(z1) = " << z1.real()
<< " Im(z1) = " << z1.imag()
<< endl;
// Constructor por defecto
Complejo z2;
cout << "creado n umero complejo z2 = " << z2 << endl;
// Asigna parte real e imaginaria
112
cout << "introduzca la parte real de z2 ";
cin>> z2.real();
cout << "introduzca la parte imaginaria de z2 ";
cin >> z2.imag();
// Devuelve parte real e imaginaria de z2
cout << " Re(z2) = " << z2.real()
<< " Im(z2) = " << z2.imag()
<< endl;
// operador -
cout << "compruebo operador cambio de signo -z1 = "
<< -z1 << endl;
// operador =
Complejo z3 = z1; // asigna el valor de z1 a z3
cout << "compruebo operador de asignaci on z3 = z"
<< z3 << endl;
// operaciones
cout << "compruebo suma, z1 + z2 = "
<< z1 + z2 << endl;
cout << "compruebo resta, z1 - z2 = "
<< z1 - z2 << endl;
cout << "compruebo multiplicaci on, z1 * z2 = "
<< z1 * z2 << endl;
cout << "compruebo divisi on, z1/z2 = "
<< z1/z2 << endl;
// funciones
cout << "compruebo modulo(z1) = "
<< modulo(z1) << endl;
cout << "compruebo modulo2(z1) = "
<< modulo2(z1) << endl;
cout << "compruebo conjugado conjugado(z1) = "
<< conjugado(z1) << endl;
cout << "compruebo inverso inverso(z1) = "
<< inverso(z1) << endl;
// asociatividad
cout << "encadeno operadores, modulo(conjugado(inverso(z1+z2*z3)="
<< modulo(conjugado(inverso(z1+z2*z3)))
<< endl;
return EXITO;

La ejecuci on del programa arroja:


$ ./xcomplejo.exe
introduzca la parte real de z1 10.
introduzca la parte imaginaria de z1 2.
113
creado n umero complejo z1 = ( 10, 2 )
Re(z1) = 10 Im(z1) = 2
creado n umero complejo z2 = ( 0, 0 )
introduzca la parte real de z2 5.
introduzca la parte imaginaria de z2 7.
Re(z2) = 5 Im(z2) = 7
compruebo operador cambio de signo -z1 = ( -10, -2 )
compruebo operador de asignaci on z3 = z( 10, 2 )
compruebo suma, z1 + z2 = ( 15, 9 )
compruebo resta, z1 - z2 = ( 5, -5 )
compruebo multiplicaci on, z1 * z2 = ( 36, 80 )
compruebo divisi on, z1/z2 = ( 0.864865, -0.810811 )
compruebo modulo(z1) = 10.198
compruebo modulo2(z1) = 104
compruebo conjugado conjugado(z1) = ( 10, -2 )
compruebo inverso inverso(z1) = ( 0.0961538, -0.0192308 )
compruebo asociatividad, modulo(conjugado(inverso(z1+z2*z3)=0.0106359
6.4. La clase de vectores reales RVector
6.4.1. Representaci on de un espacio vectorial
Nuestro siguiente ejemplo es una clase que nos permita representar vectores de n umeros reales.
Un vector es un objeto de dimensi on n, con propiedades algebraicas bien denidas. Recordemos
que el conjunto de los vectores de dimensi on n, V
n
tiene estructura de espacio vectorial sobre el cuerpo
de los reales R, esto es:
1. En V
n
hay denida una operaci on interna (suma) que le conere estructura de grupo abeliano.
2. En V
n
hay denida una operaci on externa (producto) cuyo dominio de operadores es el cuerpo
R (escalares) tal que se cumplen las siguientes operaciones ( y son escalares arbitrarios, u y
v vectores arbitrarios):
(u +v) = u + v
( + )u = u + u
(u) = ()u
1u = u
donde 1 es la unidad de R.
Debemos, por tanto, ser capaces de transcribir a C++ un amplio conjunto de operaciones, tales
como:
1. Suma (y resta) de vectores (V
n
, + es un grupo).
2. Producto por un escalar (V
n
, R, +, es un espacio vectorial).
3. Producto escalar, norma, m odulo, etc.
Comentamos a continuaci on la clase RVector, que dene un tipo de vectores reales. La denici on
de la clase se encuentra en vector.h, mientras que la implementaci on se encuentra en vector.cpp y
el programa ejemplo en xvector.cpp
114
6.4.2. Representaci on interna
Nuestro nuevo tipo debe ser capaz de reejar el hecho de que un vector puede tener dimensi on
arbitraria n. Necesitamos por tanto que la representaci on interna de la clase sea un arreglo din amico
de tipo double. Debemos tambien guardar la dimensi on del arreglo. Esto es:
class RVector
private:
double* pElem; // arreglo din amico de elementos
int pDim; // dimensi on del arreglo
...
;
6.4.3. Constructores, destructores y constructor de copia
Para reforzar la noci on de que un vector debe crearse con una dimensi on igual o mayor que uno,
escribiremos un constructor que tome como argumentos la dimensi on del vector (un entero) y produzca
un error si la dimensi on es cero o negativa. No escribiremos un constructor por defecto, prohibiendo
as explcitamente la creaci on de vectores con dimensi on nula. Por lo tanto:
public:
// Constructor est andar (no permite constructor por defecto)
RVector(int dimension);
La implementaci on de este constructor es:
// Constructor est andar. Crea un vector de dimensi on n
RVector::RVector(int n)

if (n < 1)
error("Error, la dimensi on m nima del vector es 1",ERANGO);
pDim = n;
pElem = new double[pDim];
if (!pElem)
error("Imposible crear vector",ENOMEM);

A diferencia de los ejemplos de las anteriores secciones (la clase Agenda y la clase Complejo), en
este constructor se requieren recursos del sistema. La instrucci on new pide que se asigne una cierta
cantidad de memoria (especicada por la dimensi on del vector, pDim) al arreglo din amico pElem. Dicha
memoria se busca en el area de memoria libre del procesador
7
y no es, naturalmente, ilimitada. Si un
programa empieza a crear vectores de dimensiones astron omicas, antes o despues el sistema agota la
memoria libre y la instrucci on new devuelve un valor nulo, en lugar de una direcci on v alida. De ah la
instrucci on:
if (!pElem)
error("Imposible crear vector",ENOMEM);
la cual invoca a la funci on de error con el c odigo correspondiente (no hay memoria) si se da esta
circunstancia.
7
La expresi on inglesa, muy gr aca, especica que la memoria se obtiene from the heap, o sea del mont on de
memoria disponible en ese instante.
115
Por otra parte, aunque nos abstengamos de crear vectores monstruosos, si creamos miles de pe-
que nas bestias sin liberar la memoria requerida cuando ya no son necesarias, no tardaremos en agotar
el mont on disponible. En el constructor s olo podemos asignar recursos, ya que estamos creando
el objeto. Necesitamos pues una funci on que se invoque autom aticamente cuando el objeto ya no es
necesario, esto es,cuando se destruye (lo cual ocurre normalmente, recordemos, cuando el objeto sale
de alcance, es decir, se ejecuta el cuerpo de instrucciones entre llaves en el cual se ha denido). Esta
funci on se llama destructor. En las clases de los ejemplos anteriores, que no solicitaban memoria era
innecesario escribir un destructor (si el programador no lo hace, el compilador, siempre atento, genera
uno, que por defecto no hace nada). En este caso, sin embargo, necesitamos denir un destructor que
libere la memoria requerida cuando el objeto se destruye. Su declaraci on es:
// Destructor
~RVector();
Y su denici on:
// Destructor
RVector::~RVector()

delete[] pElem;

De la misma manera que el compilador genera un constructor y un destructor por defecto si el


programador no lo hace, genera otra funci on especial llamada constructor de copia que se ocupa de
copiar dos objetos del tipo denido por la clase. La forma en que dos objetos se copian es miembro a
miembro. As, si escribimos:
...
Complejo z1(19.,3.); // z1 = 19 + i3
Complejo z2 = z1; // copia z1 en z2. Equivale a Complejo z2(z1);
el constructor de copia generado por el compilador, copia los dos reales (double) que representan las
partes real e imaginaria del complejo de un objeto a otro, lo cual es perfectamente correcto. Por lo
tanto, es innecesario escribir un constructor de copia para la clase Complejo.
Consideremos ahora el caso de un vector:
...
RVector v(3); // crea v(3)
v(1) = 1; v(2) = 5; v(3) = 7; // v = (1,5,7)
RVector v2(v); // Copia v en v2, equivale a v2 = v;
Cuando creamos v, creamos un arreglo din amico de dimensi on tres, o en otras palabras, un puntero
a double para el que reservamos tres direcciones consecutivas de memoria. M as tarde, al asignar los
valores de v(1),v(2) y v(3), el miembro interno v.pElem (que es un puntero a double) apunta a las
direcci on donde se almacenan el objeto asociado a v(1)(y por tanto v.*pElem accede al contenido de
v(1)), v.pElem+1 apunta a la direcci on donde se almacena v(2) (v.*(pElem+1) accede a su contenido)
y v.pElem+2 apunta a la direcci on donde se almacena v(3) (v1.*(pElem+2) accede a su contenido).
Que ocurre cuando el compilador encuentra la instrucci on RVector v2(v);? Si no hemos escrito
un constructor de copia, el defecto es copiar los miembros internos de un objeto a otro. Pero v.pElem,
v.pElem+1 y v.pElem+2, son direcciones en memoria, no datos. El resultado es err oneo. Lo que desea-
mos hacer es crear un nuevo puntero, v2.pElem (que apuntar a a otra zona diferente de la memoria) y
116
hacer que el contenido de los objetos a los que apunta sea identico al contenido de los objetos a los que
apuntaba el puntero v.pElem (es decir v2.*pElem = v.*pElem, etc., pero no v2.pElem = v.pElem,
etc.).
Es necesario, por lo tanto, que en este caso (y en general siempre que los miembros internos de la
clase sean punteros) escribamos nuestro propio constructor de copia, que declaramos como:
// Constructor de copia
RVector(const RVector& m);
y denimos como:
// Constructor de copia
RVector::RVector(const RVector& v)

// Crea un nuevo vector con la dimensi on de v


pDim = v.pDim;
pElem = new double[pDim];
if (!pElem)
error("Imposible crear vector",ENOMEM);
// Y copia cada elemento
for (int i = 1; i <= pDim; i++)
(*this)(i) = v(i);

Nos queda por aclarar dos detalles para entender la funci on anterior. El primero es la notaci on v(i)
para referirnos a los elementos del vector. Recordemos que en C++, accedemos a los elementos de un
arreglo mediante el operador corchete [ ] no el operador parentesis ( ). Pero v no es un arreglo (no
es de tipo double*) sino un vector (de tipo, RVector). La representaci on interna de v, pElem, es el
objeto de tipo double*.
En una clase, al igual que hemos denido operadores +, , , etc., podemos denir el operador ()
que nos permite acceder a los elementos del vector (que no tienen por que coincidir con los del arreglo
que lo representa internamente). Volveremos sobre este punto un poco m as adelante. Por ahora, nos
basta con recordar que los elementos v(i), i=1,2,...pDim, se corresponden a las coordenadas del
vector (que varan entre 1 y n para un vector n-dimensional) no a las del arreglo que lo representa
internamente (de hecho, v(1) = pDim[0], v(2) = pDim[1], etc.).
El siguiente detalle a claricar es el sentido de la palabra m agica this:
...
(*this)(i) = v(i);
aunque, en realidad, podramos casi adivinar lo que signica por el contexto. Hemos visto que al
invocar el constructor de copia, un objeto (v2 en el ejemplo anterior) toma como argumento otro
objeto del mismo tipo (v en el ejemplo). Es decir, pasamos el vector v a la funci on constructor de
copia en el argumento.
C omo pasamos el objeto v2? Precisamente, gracias a this, que no es sino un puntero, denido
autom aticamente para cada funci on de la clase
8
que apunta al objeto que invoca la funci on. As,
cuando escribimos:
...
RVector v2(v); // Copia v en v2, equivale a v2 = v;
8
Estrictamente hablando para cada funci on no est atica de la clase (nota para puristas).
117
El objeto que invoca al constructor de copia es v2 y por tanto this apunta a este y *this accede a
su contenido. Es decir, (*this)(i) = v(i); equivale a v2(i) = v(i);
En resumen, para la clase RVector:
1. Hemos denido un constructor que toma como argumentos la dimensi on del vector. No hemos
denido un constructor por defecto (sin argumentos). El compilador dene un constructor por
defecto, siempre que no haya otro constructor denido por el programador. En este caso hemos
denido uno, por lo tanto el compilador no dene un constructor por defecto y es imposible
crear vectores de dimensi on nula.
2. Hemos denido un destructor, que sustituye al destructor por defecto. El destructor es necesario
para liberar la memoria requerida con la instrucci on new en el constructor. En general, una regla
infalible para evitar fugas de memoria (el caso en que requerimos memoria sin devolverla)
es recordar que siempre que se escriba new en un constructor, debe escribirse delete en el
destructor.
3. Tambien hemos denido un constructor de copia, necesario ya que la representaci on interna de la
clase es un puntero. En estos casos el constructor de copia denido por defecto por el compilador
(el cual copia cada atributo interno de un objeto a otro) no es apropiado.
6.4.4. Funciones y operadores de acceso
Tras los constructores declaramos la funci on:
const int& dimension() const; // devuelve la dimensi on del vector
cuya denici on es:
// devuelve la dimensi on del vector
const int& RVector::dimension() const

return pDim;

y cuyo cometido, obvio, es devolver la dimensi on del vector. Notad que la versi on que nos permitira
asignar la dimensi on del vector no existe. Podemos ver aqu la conveniencia de disponer de un me-
canismo para ocultar los atributos internos de la clase. Si bien es perfectamente legtimo interesarse
por la dimensi on de un vector que hemos creado en alg un momento del programa (por lo que propor-
cionamos la funci on que devuelve dicho atributo) no hay raz on alguna para permitir al usuario que
modique la dimensi on de un vector ya creado y por lo tanto no se escribe la funci on correspondiente,
previniendo la posibilidad de cambiar la variable interna.
Encontramos a continuaci on los operadores de la clase. El primero es el operador () del que hemos
hablado anteriormente:
const double& operator () (int indice) const;
double& operator () (int indice);
La declaraci on ya indica que escribimos versiones constantes y no constantes de (). La denici on del
operador es:
// Devuelve el elemento v(i), i=1,2...n
const double& RVector::operator () (int i) const

118
// Comprueba el rango
if (i < 1 || i > pDim)
error("Error, indice fuera de rango",ERANGO);
return pElem[i - 1];

// Asigna el elemento v(i), i=1,2...n


double& RVector::operator () (int i)

// Comprueba el rango
if (i < 1 || i > pDim)
error("Error, indice fuera de rango",ERANGO);
return pElem[i - 1];

Su cometido es muy sencillo. Cuando pasamos un ndice i a la funci on operator () esta nos devuelve
el elemento pElem[i - 1], bien como referencia (lo que nos permite asignarlo) o como referencia cons-
tante (para lectura). Es decir, como ya hemos avanzado, v(1) accede a pElem[0], v(2) a pElem[1],
etc.
Podemos, en este punto, resaltar dos ventajas importantes del uso del tipo RVector en lugar de
un simple arreglo de C++ para representar un vector. La primera (menor) es que podemos acceder
a los elementos del vector en notaci on natural-(un vector de dimensi on n se suele representar en
notaci on algebraica como v(i), i = 1, n no como v(i), i = 0, n1). La segunda (mayor) es la habilidad
de controlar si el ndice solicitado est a fuera del rango del vector lo cual causara un comportamiento
imprevisible y debe por tanto resultar en un error.
El siguiente operador es el operador de asignaci on, que no es sino un sin onimo para el constructor
de copia:
RVector& operator = (const RVector& v);
y cuya denici on es identica a la de este:
// Operaci on = ( v = v1)
RVector& RVector::operator = (const RVector& v1)

// Crea un nuevo vector con la dimensi on de v


pDim = v1.pDim;
pElem = new double[pDim];
if (!pElem)
error("Imposible crear vector",ENOMEM);
// Y copia cada elemento
for (int i = 1; i <= pDim; i++)
(*this)(i) = v1(i);
return *this;

El operador = nos permite escribir instrucciones del tipo:


...
119
RVector v1(2); // v1 es un vector de dimensi on 2
v1(1) = 7; v1(2) = 4.5; // v1 -->(7,4.5)
RVector v2 = v1; // v2 --> (7,4.5). Equivale a RVector v2(v1);
Vale la pena reexionar una vez m as sobre la complejidad que se oculta tras el uso de estos operadores.
La instrucci on:
...
RVector v2 = v1;
Resulta, como puede verse en la denici on del operador, en las siguientes acciones:
1. Copiar la dimensi on de v1 en el atributo interno de v2.
2. Crear un arreglo din amico de esa dimensi on.
3. Iterar sobre cada elemento, copiando el contenido del arreglo que representa internamente a v2
al arreglo que representa internamente a v1.
4. Comprobar que el proceso se produce sin errores.
Todas estas acciones, una vez denida e implementada la clase quedan tan ocultas al usuario, como el
ensamblador generado por el compilador queda oculto al programador de C++. El resultado es que
podemos escribir c odigo m as sencillo, intuitivo, limpio y libre de errores.
Finalmente, nos encontramos con los operadores:
RVector& operator += (const RVector& v);
RVector& operator -= (const RVector& v);
RVector& operator *= (double a);
Recordemos lo que signican estos operadores para los tipos fundamentales. Por ejemplo:
...
double a = 4; double b = 3;
a+=b; // a = a + b;
Es decir, el operador += suma el objeto que lo invoca (el objeto a su izquierda, a) con el objeto a su
derecha, almacenando el resultado en el primero. En el caso de vectores, los operadores +=, -= y *=
hacen exactamente lo mismo (notad que *= toma un doble como argumento, corresponde a multiplicar
el vector que lo invoca por un escalar). He aqu su denici on:
// Operaci on += ( v+=v1 ==> v = v +v1)
RVector& RVector::operator += (const RVector& v1)

// v y v1 deben tener la misma dimensi on


if(pDim != v1.pDim)
error("Los vectores no tienen la misma dimensi on",EMALDIM);
// Suma los elementos
for (int i = 1; i <= pDim; i++)
(*this)(i) += v1(i);
return *this;

120
// Operaci on -= ( v -=v1 ==> v = v - v1)
RVector& RVector::operator -= (const RVector& v1)

// v y v1 deben tener la misma dimensi on


if(pDim != v1.pDim)
error("Los vectores no tienen la misma dimensi on",EMALDIM);
// Resta los elementos
for (int i = 1; i <= pDim; i++)
(*this)(i) -= v1(i);
return *this;

// Operaci on*= ( v*= a --> v = v*a, a es un real)


RVector& RVector::operator *= (double a)

for (int i = 1; i <= pDim; i++)


(*this)(i) *= a;
return *this;

En todas las funciones anteriores, el puntero this apunta al objeto que invoca la funci on. As, si por
ejemplo escribimos:
...
RVector va(2);
va(1) = 0.1; va(2) = 0.3;
RVector vb(2);
vb(1) = 0.3; vb(2) = 0.5;
va+=vb;
this apunta a va y la funci on operator += toma como argumento una referencia constante (que no
podemos modicar) a vb. Las sentencias:
...
// Suma los elementos
for (int i = 1; i <= pDim; i++)
(*this)(i) += v1(i);
return *this;
se ocupan de iterar sobre los elementos de va y vb (this es un puntero a va y v1 una referencia a
vb). (*this)(i) accede al elemento i-esimo del vector va. v1(i) accede al elemento i-esimo del vector
vb. Ambas cantidades son del tipo double y por lo tanto la operaci on + = est a bien denida, siendo
equivalente a:
...
va(i) = va(i) + vb(i);
Para cada elemento del vector. Puesto que el argumento del operador es una referencia al objeto que lo
invoc o, devolvemos *this (que accede al contenido del objeto). Lo mismo para las otras operaciones.
121
6.5. Operadores y funciones que manejan vectores
Aunque hemos denido los operadores +=, -= y *= (internos a la clase, ya que necesitan su repre-
sentaci on interna) tenemos todava que denir los operadores +, - y *:
RVector operator + (const RVector& v1, const RVector& v2);
RVector operator - (const RVector& v1, const RVector& v2);
RVector operator * (const RVector& v1, double d);
RVector operator * (double d, const RVector& v1);
se trata, como puede verse, de funciones diferentes a los operadores +=, -= y *=. Mientras estos suman
(restan, etc.) el contenido de dos vectores en el vector a la izquierda del operador, los segundos crean
un nuevo vector en el que se almacena la suma (resta, etc.) de los otros dos. Por ejemplo, el operador
+ se usa de la siguiente manera:
...
RVector va(2);
va(1) = 0.1; va(2) = 0.3;
RVector vb(2);
vb(1) = 0.3; vb(2) = 0.5;
RVector vc = va + vb;
Ya vimos, cuando estudiamos la clase de complejos, un ejemplo de denici on de estos operadores, que
entonces, como ahora, eran externos a la clase ya que no necesitan la representaci on interna de esta.
De hecho, en el caso que nos ocupa, los operadores +, - y * se denen trivialmente utilizando +=, -=
y *=:
// v= v1 + v2
RVector operator + (const RVector& v1, const RVector& v2)

// Crea un nuevo vector como una copia de v1


RVector v = v1; // Uso del operador de asignaci on (=)
v+=v2; // v = v + v2 --> v1 + v2
return v;

// v= v1 - v2
RVector operator - (const RVector& v1, const RVector& v2)

// Crea un nuevo vector como una copia de v1


RVector v = v1; // Uso del operador de asignaci on (=)
v-=v2; // v = v - v2 --> v1 - v2
return v;

// v = v1*a
RVector operator * (const RVector& v1, double a)

// Crea un nuevo vector como una copia de v1


RVector v = v1; // Uso del operador de asignaci on (=)
v*=a; // v *=a --> v = v*a
122
return v;

RVector operator * (double a, const RVector& v)

return v*a; // a* v = v*a

Notad que es necesario denir los operadores:


RVector operator * (const RVector& v1, double a);
RVector operator * (double a, const RVector& v);
para representar la propiedad conmutativa del producto de un vector por un escalar (el compilador
no sabe algebra y no puede adivinar que la multiplicaci on por la izquierda de un escalar y un vector
es identica a la multiplicaci on por la derecha).
Declaramos tambien, de manera an aloga a como lo hicimos en la clase de complejos, el operador
unario menos:
RVector operator - (const RVector& v);
cuya denici on es:
// v= -v1
RVector operator - (const RVector& v1)

// Crea un nuevo vector como una copia de v1


RVector v = v1; // Uso del operador de asignaci on (=)
for (int i = 1; i <= v.dimension(); i++)
v(i) = -v1(i);
return v;

Nos queda todava por representar una operaci on importante, el producto escalar de dos vectores.
Para ello podemos escribir una simple funci on:
double productoEscalar(const RVector& v1, const RVector& v2);
que toma referencias constantes a dos vectores y devuelve un n umero real (un double). La denici on
es:
double productoEscalar(const RVector& v1, const RVector& v2)

// v1 y v2 deben tener la misma dimensi on


if(v1.dimension() != v2.dimension())
error("Los vectores no tienen la misma dimensi on",EMALDIM);
double escalar = 0;
for (int i = 1; i <= v1.dimension(); i++)
escalar += v1(i)*v2(i);
return escalar;

123
Por otra parte, en algebra, la notaci on v
1
v
2
indica producto escalar. Esto nos sugiera la posibilidad
de sobrecargar el operador cuando toma como argumentos dos vectores, para que represente el
producto escalar de una manera concisa (y, esperamos, intuitiva). La declaraci on:
double operator * (const RVector& v1, const RVector& v2);
nos indica que el resultado de multiplicar dos vectores es un n umero real, como esperamos del
producto escalar. La denici on no puede ser m as inmediata:
// Producto escalar (sobrecarga operador *)
double operator * (const RVector& v1, const RVector& v2)

return productoEscalar(v1,v2);

El ultimo operador que nos queda por comentar es <<:


ostream& operator << (ostream& s, const RVector& v);
que sobrecarga, como ya vimos en el ejemplo de la clase de complejos la escritura por la pantalla:
ostream& operator << (ostream& s, const RVector& v)

s << endl;
s << "(t";
for (int i = 1; i <= v.dimension(); i++)
s << v(i) << "t";
s << ")" << endl;
return s;

6.5.1. Tama no de un objeto y argumentos a funciones


Una vez concluida la descripci on de la clase de vectores y antes de mostrar un ejemplo de su uso y
recapitular lo que hemos aprendido, vamos a tomar un corto vericueto lateral para hablar de tama nos
de objetos.
Consideremos un Complejo. Se trata de un objeto peque no, ya que su representaci on interna se
reduce a dos n umeros reales. Por tanto, en la pr actica, lo podemos manipular como si se tratara de
un (peque no) tipo fundamental, int, float o double. De ah que lo pasemos por valor en funciones
como:
...
Complejo operator - (Complejo z) ; // -z ;
Complejo operator + (Complejo z1, Complejo z2); // z3 = z1+z2
Complejo operator - (Complejo z1, Complejo z2); // z3 = z1-z2
Complejo operator * (Complejo z1, Complejo z2); // z3 = z1*z2
Complejo operator / (Complejo z1, Complejo z2); // z3 = z1/z2
// Funciones externas
double modulo(Complejo z); // valor absoluto
124
double modulo2(Complejo z); // valor absoluto al cuadrado
Complejo conjugado(Complejo z); // Complejo conjugado
Complejo inverso(Complejo z); // z z
1
= 1
// operador de escritura
ostream& operator << (ostream& s, Complejo z);
Es decir, pasamos directamente una copia del objeto a las funciones que lo manipulan.
En cambio, un objeto de tipo RVector puede ser muy grande
9
. En este caso, pasar una copia
del objeto es totalmente ineciente. Esa es la raz on por la que, para un objeto grande (un vector,
una matriz) pasamos referencias constantes en aquellos lugares donde, para un objeto peque no (un
complejo) nos limitamos a pasar el objeto por valor. Por ejemplo:
...
RVector operator - (const RVector& v);
RVector operator + (const RVector& v1, const RVector& v2);
RVector operator - (const RVector& v1, const RVector& v2);
double operator * (const RVector& v1, const RVector& v2);
RVector operator * (const RVector& v1, double d);
RVector operator * (double d, const RVector& v1);
double productoEscalar(const RVector& v1, const RVector& v2);
ostream& operator << (ostream& s, const RVector& v);
Una referencia no es m as que una direcci on de memoria que apunta al objeto y por lo tanto es tan
eciente, en la pr actica, como pasar el objeto por valor.
6.5.2. El programa xvector
El siguiente programa ilustra el uso de la clase de vectores que hemos construido:
// xvector.cpp
// Banco de pruebas de la clase RVector
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Clase Rvector
#include <vector.h>
// Espacio de nombres est andar
using namespace std;
//---main---
9
Nadie nos impide manejar vectores de miles de coordenadas y los problemas en los que tales monstruos aparecen
son menos infrecuentes de lo que os imagin ais.
125
int main ()

// Constructor est andar;


int ndim;
cout << "introduzca la dimensi on del vector v1 "; cin>>ndim;
RVector v1(ndim);
cout << "He creado un vector v1 de dimensi on "
<< v1.dimension() << endl;
// Asigna y lee elementos
for (int i = 1; i <= ndim; i++)
cout << "v1("<<i<<")?" ; cin>>v1(i);
cout << "v1("<<i<<") = " << v1(i) << endl;
// Operador <<
cout << " Vector v1 = " << v1;
// Copia (operador = y constructor de copia)
RVector v2 = v1;
cout << "He creado un vector v2 de dimensi on "
<< v2.dimension() << endl;
// Operador <<
cout << " Ahora v2 debe ser una copia de v1 = " << v2;
// Asigna y lee elementos (reescribe elementos copiados)
for (int i = 1; i <= ndim; i++)
cout << "v2("<<i<<")?" ; cin>>v2(i);
cout << "v2("<<i<<") = " << v2(i) << endl;
// Operador <<
cout << " Vector v2 = " << v2;
RVector v0 = v1;
// operador -
cout << "compruebo operador cambio de signo -v1 = "
<< -v1 << endl;
v1 = v0;
// operaciones
v1+=v2;
cout << "compruebo v1+=v2 "
<< v1 << endl;
v1 = v0;
v1-=v2;
126
cout << "compruebo v1-=v2 "
<< v1 << endl;
v1 = v0;
double real;
cout << "Introduzca un n umero real "; cin >> real;
v1*=real;
cout << "compruebo v1*=real "
<< v1 << endl;
v1 = v0;
cout << "compruebo suma, v1 + v2 = "
<< v1 + v2 << endl;
cout << "compruebo resta, v1 - v2 = "
<< v1 - v2 << endl;
cout << "compruebo v1*a = "
<< v1*real << endl;
cout << "compruebo a*v1 = "
<< real*v1 << endl;
cout << "compruebo producto escalar v1 y v2 = "
<< v1*v2 << endl;
return EXITO;

Obteniendose, al ejecutarlo, el siguiente resultado:


$ ./xvector.exe
introduzca la dimensi on del vector v1 5
He creado un vector v1 de dimensi on 5
v1(1)?2
v1(1) = 2
v1(2)?3.4
v1(2) = 3.4
v1(3)?5.4
v1(3) = 5.4
v1(4)?7.8
v1(4) = 7.8
v1(5)?3
v1(5) = 3
Vector v1 =
( 2 3.4 5.4 7.8 3 )
He creado un vector v2 de dimensi on 5
Ahora v2 debe ser una copia de v1 =
( 2 3.4 5.4 7.8 3 )
v2(1)?1
v2(1) = 1
v2(2)?33.
v2(2) = 33
v2(3)?556.
v2(3) = 556
v2(4)?7894.
v2(4) = 7894
127
v2(5)?2.
v2(5) = 2
Vector v2 =
( 1 33 556 7894 2 )
compruebo operador cambio de signo -v1 =
( -2 -3.4 -5.4 -7.8 -3 )
compruebo v1+=v2
( 3 36.4 561.4 7901.8 5 )
compruebo v1-=v2
( 1 -29.6 -550.6 -7886.2 1 )
Introduzca un n umero real 4
compruebo v1*=real
( 8 13.6 21.6 31.2 12 )
compruebo suma, v1 + v2 =
( 3 36.4 561.4 7901.8 5 )
compruebo resta, v1 - v2 =
( 1 -29.6 -550.6 -7886.2 1 )
compruebo v1*a =
( 8 13.6 21.6 31.2 12 )
compruebo a*v1 =
( 8 13.6 21.6 31.2 12 )
compruebo producto escalar v1 y v2 = 64695.8
6.6. Recapitulaci on
La escritura de la clase RVector nos ha permitido abstraer el concepto de vector a un tipo m as
conciso, elegante y f acil de usar que la alternativa de bajo nivel que nos proporcionara el lenguaje,
es decir, el uso directo de un arreglo din amico.
El uso de constructores y destructores nos permite encapsular las operaciones de adquirir y liberar
memoria para nuestros vectores de manera transparente al usuario. La sobrecarga de operadores per-
mite expresar de manera concisa, casi una transcripci on matem atica de las f ormulas correspondientes,
las operaciones con vectores. Hemos abstrado un concepto (el de vector) y denido un tipo que lo
representa ecientemente, ocultando todos los engorrosos detalles asociados al manejo de memoria,
manipulaci on de punteros, etc., entre bastidores. En el siguiente ejemplo, vamos a abstraer un nuevo
tipo, el de matrices de n umeros reales.
6.7. La clase de matrices reales RMatriz
6.7.1. El espacio vectorial de las matrices
El ultimo ejemplo que vamos a estudiar es una clase que nos permita representar matrices de
n umeros reales.
128
Una matriz es un objeto de dimensi on mn, donde m es es su n umero de las y n su n umero de
columnas. El conjunto de las matrices M
mn
tiene estructura de espacio vectorial sobre el cuerpo de
los reales R. Por tanto, como en el caso de los vectores, debemos, por tanto, ser capaces de transcribir
a C++ operaciones, tales como:
1. Suma (y resta) de Matrices (M
mn
, + es un grupo).
2. Producto por un escalar (M
mn
, R, +, es un espacio vectorial).
3. Producto de matrices, norma, transpuesta, etc.
Comentamos a continuaci on la clase RMatriz, que dene un tipo de matrices reales. La denici on
de la clase se encuentra en matriz.h, mientras que la implementaci on se encuentra en matriz.cpp y
el programa ejemplo en xmatriz.cpp
6.7.2. Representaci on interna
Al igual que en el caso de los vectores la representaci on interna de la clase es un arreglo din amico
de tipo double. Debemos guardar, tambien, el n umero de las y de columnas. Esto es:
class RMatriz
private:
double* pElem; // arreglo din amico de elementos
int pFilas; // n umero de filas
int pCols; // n umero de columnas
...
;
Tambien en este caso, deseamos que los ndices de la matriz varen entre uno y el n umero de las
(uno y el n umero de columnas). Recordemos que en el ejemplo anterior:
...
RVector v;
v(i) = pElem[i-1] ;
donde v era un vector y pElem su representaci on interna. La relaci on entre los ndices del vector y los
del arreglo que lo representa internamente es inmediata, diferenci andose tan s olo en el desplazamiento
de un ndice.
La relaci on entre el elemento m(i,j) de una matriz y el correspondiente elemento en el arreglo
din amico (tipo double*) pElem, es algo menos trivial.
Consideremos, por ejemplo, una matriz 3 3. Internamente, crearemos un arreglo de dimensi on 9,
tal que:
a(1,1), a(1,2), a(1,3) ---> pElem[0],pElem[1],pElem[2]
a(2,1), a(2,2), a(2,3) ---> pElem[3],pElem[4],pElem[5]
a(3,1), a(3,2), a(3,3) ---> pElem[6],pElem[7],pElem[8]
Para acceder al elemento de la matriz a(fila,columna), necesitamos ir al elemento del arreglo:
pElem[(columna - 1) + pCols * (fila - 1)];
En nuestro ejemplo:
a(2,3) = pElem[(3 - 1) + 3 * (2 - 1)] = pElem[2 + 3]= pElem[5];
a(3,1) = pElem[(1 - 1) + 3 * (3 - 1)] = pElem[0 + 3*2]= pElem[6];
y as sucesivamente.
129
6.7.3. Constructores, destructores y constructor de copia
Al igual que en el caso de los vectores, no escribimos un constructor por defecto, imponiendo que
toda matriz deba crearse especicando sus las y columnas (o como copia de otra). Por lo tanto:
...
public:
// Constructor est andar (no hay constructor por defecto)
RMatriz(int nfilas, int ncols);
La implementaci on de este constructor es:
// Crea una matriz con i filas y j columnas m(i,j)
RMatriz::RMatriz(int i, int j)

pFilas = i;
pCols = j;
if (i*j < 1)
error("Error, la dimensi on m nima de la matrix es 1",
ERANGO);
// la dimensi on de la matriz es ixj
pElem = new double[pFilas * pCols];
if (!pElem)
error("Imposible crear matriz",ENOMEM);

Al igual que hicimos en el caso de los vectores, comprobamos si existe suciente memoria para crear
la matriz, devolviendo un error en otro caso. Tambien aqu es necesario escribir un destructor para
liberar la memoria requerida cuando el objeto se destruye. Su declaraci on es:
// Destructor
~RMatriz();
Y su denici on:
// Destructor
RMatriz::~RMatriz()

delete[] pElem;

Tambien es necesario escribir un constructor de copia, puesto que la representaci on interna de la


matriz es un puntero:
// Constructor de copia
RMatriz(const RMatriz& m);
cuya denici on es:
// Constructor de copia
RMatriz::RMatriz(const RMatriz& m)
130

pFilas = m.pFilas;
pCols = m.pCols;
pElem = new double[pFilas * pCols];
if (!pElem)
error("Imposible crear matriz",ENOMEM);
// Copia cada pElem
for (int i = 1; i <= pFilas; i++)
for (int j = 1; j <= pCols; j++)
(*this)(i, j) = m(i, j);

6.7.4. Funciones y operadores de acceso


Tras los constructores declaramos funciones para devolver el n umero de las y de columnas de la
matriz (que no permitimos asignar, prohibiendo la modicaci on de la dimensi on de la matriz una vez
creada):
const int& filas() const; // devuelve el n umero de filas
const int& columnas() const; // devuelve el n umero de columnas
la denici on de estas funciones es:
// Devuelve n umero de filas
const int& RMatriz::filas() const

return pFilas;

// Devuelve n umero de columnas


const int& RMatriz::columnas() const

return pCols;

Para acceder a los elementos de la matriz a(i,j) usamos el operador ():


// lectura (ejemplo, cout << a(i,j))
const double& operator () (int fila, int col) const;
// asignaci on (ejemplo, a(i,j) = 7.)
double& operator () (int fila, int col);
En cuya denici on podemos ver la representaci on interna de la matriz que ya hemos comentado:
// Devuelve un elemento de la matriz
const double& RMatriz::operator () (int fila, int col) const

// Comprueba rango
if (fila < 1 || fila > pFilas)
error("Error, fila fuera de rango",ERANGO);
if (col < 1 || col > pCols)
131
error("Error, columna fuera de rango",ERANGO);
return pElem[(col - 1) + pCols * (fila - 1)];

// Asigna un valor
double& RMatriz::operator () (int fila, int col)

// Comprueba rango
if (fila < 1 || fila > pFilas)
error("Error, fila fuera de rango",ERANGO);
if (col < 1 || col > pCols)
error("Error, columna fuera de rango",ERANGO);
return pElem[(col - 1) + pCols * (fila - 1)];

Sigue el operador de asignaci on:


RMatriz& operator = (const RMatriz& m);
cuya denici on es identica a la del constructor de copia:
// Operaci on = ( m = m1)
// C odigo id entico al operador de copia
RMatriz& RMatriz::operator = (const RMatriz& m)

pFilas = m.pFilas;
pCols = m.pCols;
pElem = new double[pFilas * pCols];
if (!pElem)
error("Imposible crear matriz",ENOMEM);
// Copia cada pElem
for (int i = 1; i <= pFilas; i++)
for (int j = 1; j <= pCols; j++)
(*this)(i, j) = m(i, j);

Finalmente, nos encontramos con los operadores:


RMatriz& operator += (const RMatriz& m);
RMatriz& operator -= (const RMatriz& m);
RMatriz& operator *= (double a);
Cuya denici on, an aloga al caso de los vectores es:
// Operaci on += (m+=m1 -->m = m + m1)
RMatriz& RMatriz::operator += (const RMatriz& m)

// Comprueba dimensi on
132
if (m.pFilas != pFilas || m.pCols != pCols)
error("Las matrices no tienen la misma dimensi on",
EMALDIM);
// Suma cada elemento
for (int i = 1; i <= pFilas; i++)
for (int j = 1; j <= pCols; j++)
(*this)(i, j) += m(i, j);
return *this;

// Operaci on -= (m-=m1 -->m = m - m1)


RMatriz& RMatriz::operator -= (const RMatriz& m)

// Comprueba dimensi on
if (m.pFilas != pFilas || m.pCols != pCols)
error("Las matrices no tienen la misma dimensi on",
EMALDIM);
// Resta cada elemento
for (int i = 1; i <= pFilas; i++)
for (int j = 1; j <= pCols; j++)
(*this)(i, j) -= m(i, j);
return *this;

// Operaci on *= (m*=a -->m = m * a)


RMatriz& RMatriz::operator *= (double a)

// Multiplica cada elemento de la matriz por el factor a */


for (int i = 1; i <= pFilas; i++)
for (int j = 1; j <= pCols; j++)
(*this)(i, j) *= a;
return *this;

6.8. Operadores y funciones que manejan matrices


An alogamente a la clase de vectores, declaramos:
RMatriz operator - (const RMatriz& m);
RMatriz operator + (const RMatriz& m1, const RMatriz& m2);
RMatriz operator - (const RMatriz& m1, const RMatriz& m2);
RMatriz operator * (const RMatriz& m1, const RMatriz& m2);
RMatriz operator * (const RMatriz& v1, double d);
RMatriz operator * (double d, const RMatriz& v1);
RMatriz transpuesta(const RMatriz& m);
ostream& operator << (ostream& s, const RMatriz& m);
133
Es decir, la suma, resta y multiplicaci on de matrices, as como la multiplicaci on (conmutativa) por un
escalar, la transpuesta de la matriz y el operador de escritura. Escribimos, sin m as, la denici on de
todas estas funciones, muy parecidas a las que ya hemos visto para el caso de vectores:
// Cambio de signo
RMatriz operator - (const RMatriz& m1)

// Crea una nueva matriz como una copia de m1


RMatriz m = m1; // Uso del operador de asignaci on (=)
for (int i = 1; i <= m.filas(); i++)
for (int j = 1; j <= m.columnas(); j++)
m(i,j) = -m1(i,j);
return m;

// Suma
RMatriz operator + (const RMatriz& m1, const RMatriz& m2)

// Comprueba dimensi on
if (m1.filas() != m2.filas() ||
m1.columnas() != m2.columnas())
error("Las matrices no tienen la misma dimensi on",
EMALDIM);
// Crea una nueva matriz copia de m1
RMatriz m(m1); // m = m1
// Y suma
m+=m2; // m = m+m2 =m1+m2
return m;

// Resta
RMatriz operator - (const RMatriz& m1, const RMatriz& m2)

// Comprueba dimensi on
if (m1.filas() != m2.filas() ||
m1.columnas() != m2.columnas())
error("Las matrices no tienen la misma dimensi on",
EMALDIM);
// Crea una nueva matriz copia de m1
RMatriz m(m1); // m = m1
// Y resta
m-=m2; // m = m+m2 =m1+m2
return m;

// Producto por un escalar


RMatriz operator * (const RMatriz& m1, double a)

// Crea una copia de m1


RMatriz m(m1); // Uso del operador de asignaci on (=)
134
m*=a; // m *=a --> m = m*a
return m;

RMatriz operator * (double a, const RMatriz& m)

return m*a; // a* v = v*a

// Producto de matrices
RMatriz operator * (const RMatriz& m1, const RMatriz& m2)

// Comprueba que tienen la dimensi on adecuada


if (m1.columnas() != m2.filas())
error("Intento de multiplicar matrices con dimensiones incompatibles",
EMALDIM);
// Crea una nueva matriz con la dimensi on adecuada
RMatriz m(m1.filas(), m2.columnas());
// Multiplica elemento a elemento
for (int i = 1; i <= m1.filas(); i++)
for (int j = 1; j <= m2.columnas(); j++)
double subtotal = 0;
for (int k = 1; k <= m1.columnas(); k++)
subtotal += m1(i, k) * m2(k, j);
m(i, j) = subtotal;

return m;

// Matriz traspuesta
RMatriz traspuesta(const RMatriz& m)

// Crea una matriz de columnas x filas


RMatriz mt(m.columnas(), m.filas());
// Copia filas en columnas
for (int i = 1; i <= mt.filas(); i++)
for (int j = 1; j <= mt.columnas(); j++)
mt(i, j) = m(j, i);
return mt;

// Escribe una matriz


ostream& operator << (ostream& s, const RMatriz& m)

s << endl;
135
for (int i = 1; i <= m.filas(); i++)
s << "(t";
for (int j = 1; j <= m.columnas(); j++)
s << m(i, j) << "t";
s << ")" << endl;

return s;

6.8.1. El programa xmatriz


El siguiente programa ilustra el uso de la clase de matrices que hemos construido:
// xmatriz.cpp
// Banco de pruebas de la clase RMatriz
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Clase RMatriz
#include <matriz.h>
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

// Constructor est andar;


int nfilas, ncols;
cout << "introduzca las filas de la matriz m1 "; cin>>nfilas;
cout << "introduzca las columnas de la matriz m1 "; cin>>ncols;
RMatriz m1(nfilas,ncols);
cout << "He creado una matriz m1 de "
<< m1.filas() << " filas x " << m1.columnas()
<< " columnas" << endl;
// Asigna y lee elementos
for (int i = 1; i <= nfilas; i++)
for (int j = 1; j <= ncols; j++)

cout << "m1("<<i<<","<<j<<")?" ; cin>>m1(i,j);


cout << "m1("<<i<<","<<j<<") = " << m1(i,j)
136
<< endl;

// Operador <<
cout << " Matriz m1 = " << m1;
// Copia (operador = y constructor de copia)
RMatriz m2 = m1;
cout << "He creado una matriz m2 de dimensi on "
<< m2.filas() << " filas x " << m2.columnas()
<< " columnas" << endl;
// Operador <<
cout << " Ahora m2 debe ser una copia de m1 = " << m2;
// Asigna y lee elementos (reescribe elementos copiados)
for (int i = 1; i <= nfilas; i++)
for (int j = 1; j <= ncols; j++)

cout << "m2("<<i<<","<<j<<")?" ; cin>>m2(i,j);


cout << "m2("<<i<<","<<j<<") = " << m2(i,j)
<< endl;

// Operador <<
cout << " Matriz m2 = " << m2;
RMatriz m0 = m1;
// operador -
cout << "compruebo operador cambio de signo -m1 = "
<< -m1 << endl;
m1 = m0;
// operaciones
m1+=m2;
cout << "compruebo m1+=m2 "
<< m1 << endl;
m1 = m0;
m1-=m2;
cout << "compruebo m1-=m2 "
<< m1 << endl;
m1 = m0;
double real;
cout << "Introduzca un n umero real "; cin >> real;
m1*=real;
cout << "compruebo m1*=real "
<< m1 << endl;
m1 = m0;
137
cout << "compruebo suma, m1 + m2 = "
<< m1 + m2 << endl;
cout << "compruebo resta, m1 - m2 = "
<< m1 - m2 << endl;
cout << "compruebo m1*a = "
<< m1*real << endl;
cout << "compruebo a*m1 = "
<< real*m1 << endl;
cout << "compruebo transpuesta, mt1 = "
<< transpuesta(m1) << endl;
cout << "compruebo producto m1 * trans(m2) = "
<< m1*transpuesta(m2) << endl;
return EXITO;

Obteniendose, al ejecutarlo, el siguiente resultado:


$ ./xmatriz.exe
introduzca las filas de la matriz m1 2
introduzca las columnas de la matriz m1 3
He creado una matriz m1 de 2 filas x 3 columnas
m1(1,1)?1.1
m1(1,1) = 1.1
m1(1,2)?1.2
m1(1,2) = 1.2
m1(1,3)?1.3
m1(1,3) = 1.3
m1(2,1)?2.1
m1(2,1) = 2.1
m1(2,2)?2.2
m1(2,2) = 2.2
m1(2,3)?2.3
m1(2,3) = 2.3
Matriz m1 =
( 1.1 1.2 1.3 )
( 2.1 2.2 2.3 )
He creado una matriz m2 de dimensi on 2 filas x 3 columnas
Ahora m2 debe ser una copia de m1 =
( 1.1 1.2 1.3 )
( 2.1 2.2 2.3 )
m2(1,1)?11.1
m2(1,1) = 11.1
m2(1,2)?11.2
m2(1,2) = 11.2
m2(1,3)?11.2
m2(1,3) = 11.2
m2(2,1)?22.1
m2(2,1) = 22.1
m2(2,2)?22.2
m2(2,2) = 22.2
138
m2(2,3)?22.3
m2(2,3) = 22.3
Matriz m2 =
( 11.1 11.2 11.2 )
( 22.1 22.2 22.3 )
compruebo operador cambio de signo -m1 =
( -1.1 -1.2 -1.3 )
( -2.1 -2.2 -2.3 )
compruebo m1+=m2
( 12.2 12.4 12.5 )
( 24.2 24.4 24.6 )
compruebo m1-=m2
( -10 -10 -9.9 )
( -20 -20 -20 )
Introduzca un n umero real 10
compruebo m1*=real
( 11 12 13 )
( 21 22 23 )
compruebo suma, m1 + m2 =
( 12.2 12.4 12.5 )
( 24.2 24.4 24.6 )
compruebo resta, m1 - m2 =
( -10 -10 -9.9 )
( -20 -20 -20 )
compruebo m1*a =
( 11 12 13 )
( 21 22 23 )
compruebo a*m1 =
( 11 12 13 )
( 21 22 23 )
compruebo transpuesta, mt1 =
( 1.1 2.1 )
( 1.2 2.2 )
( 1.3 2.3 )
compruebo producto m1 * trans(m2) =
( 40.21 79.94 )
( 73.71 146.54 )
6.9. Recapitulaci on
Como en el caso de vectores, la escritura de la clase RMatriz nos ha permitido abstraer el concepto
de matriz a un tipo m as conciso, elegante y f acil de usar que la alternativa de bajo nivel que nos
139
proporcionara el lenguaje, es decir, el uso directo de un arreglo din amico.
Hemos ocultado las manipulaciones que efectuamos en la representaci on interna de la matriz cuando
la creamos, accedemos a sus ndices o efectuamos operaciones. Gracias a la abstracci on que hemos
introducido, crear matrices y manejarlas, resulta una transcripci on casi literal de las operaciones
matem aticas relevantes, al igual que sucede con los tipos intrnsecos del lenguaje.
140
Captulo 7
Sistemas lineales de ecuaciones
7.1. Introducci on
En este captulo abordamos la resoluci on de sistemas lineales de ecuaciones, del tipo:
a
11
x
1
+ a
12
x
2
+ + a
1n
x
n
= w
1
a
21
x
1
+ a
22
x
2
+ + a
2n
x
n
= w
2
(7.1)
a
m1
x
1
+ a
m2
x
2
+ + a
mn
x
n
= w
m
Donde las n inc ognitas est an relacionadas entre s por m ecuaciones. Los coecientes a
ij
son n umeros
conocidos, al igual que los coecientes w
i
.
La ecuaci on 7.1 puede escribirse usando notaci on matricial como:
A x = w (7.2)
Donde A es la matriz de coecientes, w el vector de terminos independientes y x el vector de inc ognitas.
Para el caso n = m, que es el unico que estudiaremos en este captulo, podemos escribir, explcitamente:
_

_
a
11
a
12
... a
1n
a
21
a
22
... a
2n
...
a
n1
a
n2
a
nn
_

_
_

_
x
1
x
2
...
x
n
_

_
=
_

_
w
1
w
2
...
w
n
_

_
(7.3)
Resolver el sistema de ecuaciones 7.3 es equivalente, desde el punto de vista algebraico, a encontrar
la inversa de la matriz A, aunque, como veremos en breve, el problema numerico de resolver un
sistema lineal de ecuaciones admite soluciones m as sencillas que la de encontrar la inversa de la matriz
de coecientes.
En este captulo estudiaremos un metodo particularmente elegante y eciente para resolver siste-
mas lineales de ecuaciones, que implica factorizar la matriz de coecientes, descomponiendola en el
producto de una matriz triangular inferior y una matriz triangular superior. Este metodo, llamado
genericamente metodo de descomposici on LU admite diversas variantes, dependiendo de si se escoge
la matriz triangular inferior (que denotaremos como L del ingles, lower) tal que los elementos de la
diagonal valen uno, l
ii
= 1, i = 1, 2, ..., n), o bien es la matriz triangular superior (U del ingles upper)
la que se escoge tal que u
ii
= 1, i = 1, 2, ..., n. El primer algoritmo se llama factorizaci on de Doolittle
y es el que desarrollaremos explcitamente. El segundo, llamado de Crout, es totalmente equivalente.
Por ultimo, en el caso de matrices simetricas (a
ij
= a
ji
) y denidas positivas, existe una poderosa
variante del algoritmo LU, en el cual U = L
T
, llamada factorizaci on de Choleski.
141
7.2. Sistemas lineales faciles de resolver
Para comprender por que resulta util factorizar una matriz arbitraria en el producto de una trian-
gular inferior y otra superior, consideremos primero la soluci on de un sistema lineal de ecuaciones
cuando la matriz de coecientes es de tipo L o U. En el primer caso:
_

_
l
11
0 0 ... 0
l
21
l
22
0 ... 0
l
31
l
32
l
33
... 0
...
l
n1
l
n2
l
n3
... l
nn
_

_
_

_
x
1
x
2
x
3
...
x
n
_

_
=
_

_
w
1
w
2
w
3
...
w
n
_

_
(7.4)
El sistema 7.4 admite una soluci on muy sencilla, en la que cada termino se calcula partiendo del
anterior:
l
11
x
1
= w
1
x
1
= w
1
/l
11
l
21
x
1
+ l
22
x
2
= w
2
x
2
= (w
2
l
21
x
1
)/l
22
l
31
x
1
+ l
32
x
2
+ l
33
x
3
= w
3
x
3
= (w
3
l
31
x
1
l
32
x
2
)/l
33
.... .... ....
obteniendose la f ormula general para la llamada sustituci on progresiva:
x
i
=
w
i

i1
j=1
l
ij
x
j
l
ii
(7.5)
An alogamente, en el caso de una matriz triangular superior:
_

_
u
11
u
12
u
13
... u
1n
0 u
22
u
23
... u
2n
0 0 u
33
... u
3n
...
0 0 0 ... u
nn
_

_
_

_
x
1
x
2
x
3
...
x
n
_

_
=
_

_
w
1
w
2
w
3
...
w
n
_

_
(7.6)
La soluci on es igualmente inmediata, resolviendo de abajo arriba:
u
nn
x
n
= w
n
x
n
= w
n
/u
nn
u
n1n1
x
n1
+ u
n1n
x
n
= w
n1
x
n1
= (w
n1
u
n1n
x
n
)/u
n1n1
u
n2n2
x
n2
+ u
n2n1
x
n1
+ u
n2n
x
n
= w
n2
x
n2
= (w
n2
u
n2n1
x
n1
u
n2n
x
n
)/u
n2n2
... ...
Resultando en la f ormula general de la sustituci on regresiva:
x
i
=
w
i

n
j=i+1
u
ij
x
j
u
ii
(7.7)
Las ecuaciones 7.5 y 7.7 plantean, en apariencia, un problema en el caso de que cualquiera de
los terminos diagonales l
ii
, u
ii
, i = 1, 2, ..., n sea nulo. En realidad no es el caso, puesto que, siempre
que al menos uno de los elementos del conjunto (l
i+1i
, l
i+2i
, l
ni
) sea distinto de cero en el caso de las
ecuaciones 7.5, o bien al menos uno de los elementos del conjunto (u
1i
, u
2i
, u
i1i
) sea distinto de cero
en el caso de las ecuaciones 7.7, podemos recurrir al simple mecanismo de permutar las las de la
matriz A permutando a su vez el vector de coecientes, de tal manera que los algoritmos de sustituci on
(progresiva o regresiva) no cambian, pero el termino divisor en las ecuaciones 7.5,7.7 (al que se suele
denominar con el nombre de pivote) es (siempre que el sistema no sea singular) distinto de cero.
142
7.3. Factorizaci on LU
Si es posible descomponer la matriz del sistema en el producto de una matriz triangular inferior y
una matriz triangular superior:
A = L U (7.8)
Entonces la ecuaci on 7.2 se transforma en:
(L U)x = w L(Ux) = w (7.9)
que puede resolverse, resolviendo los sistemas:
Ly = w
Ux = y (7.10)
Donde el primero de estos es del tipo 7.4, mientras que el segundo es del tipo 7.6. Como acabamos
de ver, la soluci on de estos sistemas por sustituci on progresiva (regresiva), ecuaciones 7.5 y 7.7 es
inmediata.
Veamos como proceder a la factorizaci on. Resaltemos, en primer lugar, que las ecuaciones 7.8
no determinan unvocamente los valores de L y U, puesto que al resolver el sistema van a aparecer
productos de la forma l
kk
u
kk
que permiten un grado de libertad en la elecci on de los coecientes de la
diagonal, de tal manera que uno de ellos puede elegirse distinto de cero (pero no ambos). Esto es, una de
las matrices puede escogerse estrictamente triangular (los terminos distintos de cero est an por encima
o por debajo de la diagonal) mientras que los terminos de la diagonal de la otra se escogen distintos de
cero. Como hemos mencionado, los casos particulares en los que U es estrictamente triangular superior
y l
kk
= 1, k = 1, 2, ..., n (factorizaci on de Doolittle) o bien L es estrictamente triangular inferior y
u
kk
= 1, k = 1, 2, ..., n (factorizaci on de Crout), son particularmente interesantes.
Para deducir un algoritmo para la factorizaci on LU de la matriz A, comenzamos con la f ormula
para la multiplicaci on de matrices:
a
ij
=
n

s=1
l
is
u
sj
(7.11)
Puesto que l
is
= 0 para s > i y u
sj
= 0 para s > j, la ecuaci on anterior se transforma en:
a
ij
=
min(i,j)

s=1
l
is
u
sj
(7.12)
Explcitamente:
a
11
= l
11
u
11
a
12
= l
11
u
12
a
13
= l
11
u
13
a
1n
= l
11
u
1n
a
21
= l
21
u
11
a
22
= l
21
u
12
+ l
22
u
22
a
23
= l
21
u
13
+ l
22
u
23
a
2n
= l
21
u
1n
+ l
22
u
2n
a
31
= l
31
u
11
a
32
= l
31
u
12
+ l
32
u
22
a
33
= l
31
u
13
+ l
32
u
23
+ l
33
u
33
a
3n
= l
31
u
1n
+ l
32
u
2n
+ l
33
u
3n
....
a
n1
= l
n1
u
11
a
n2
= l
n1
u
12
+ l
n2
u
22
a
n3
= l
n1
u
13
+ l
n2
u
23
+ l
n3
u
33
a
nn
= l
n1
u
1n
+ ... + l
nn
u
nn
(7.13)
En el proceso especicado en las ecuaciones anteriores, cada paso determina una nueva la de U
y una nueva columna de L. Para cada k podemos suponer calculadas las las 1, 2, ..., k 1 de U y las
columnas 1, 2, ..., k 1 de L. Por ejemplo, si nos jamos en la primera columna, el primer termino:
a
11
= l
11
u
11
nos permite determinar un valor para u
11
a condici on de jar uno para l
11
. Si usamos la factorizaci on
de Doolittle, (l
kk
= 1, k = 1, 2, ...n) obtenemos u
11
= a
11
. El segundo termino de la columna:
a
21
= l
21
u
11
143
nos permite obtener el valor de l
21
a partir del coeciente conocido de la matriz original, a
21
y el
termino que acabamos de calcular, u
11
. El resto de la columna arroja sucesivamente los valores de
l
31
, l
41
...l
n1
. En la segunda columna el primer termino permite calcular u
12
directamente a partir
del termino correspondiente de la matriz original, mientras que el segundo termino arroja u
22
=
a
22
l
21
u
12
, donde u
12
se calcul o en el paso anterior y l
21
en la columna anterior. Descendiendo a lo
largo de la columna se calculan los terminos l
32
, l
42
...l
n2
. El proceso se repite en la tercera columna, en
la que calculamos primero u
13
, u
23
y u
33
seguidos de l
43
, l
53
...l
n3
. En resumen: avanzando por columnas
calculamos los terminos de L, avanzando por las los de U y en cada paso necesitamos tan s olo de
terminos previamente calculados.
Haciendo i = j = k en las ecuaciones 7.12 (lo que corresponde a tomar los terminos diagonales en
7.13) obtenemos:
a
kk
=
k1

s=1
l
ks
u
sk
+ l
kk
u
kk
(7.14)
Como ya hemos indicado, si l
ii
se ha especicado (por ejemplo l
ii
= 1, i = 1, 2, ...n para la factorizaci on
de Doolittle) la ecuaci on 7.14 nos permite calcular u
ii
. An alogamente, si u
ii
se ha especicado (por
ejemplo u
ii
= 1, i = 1, 2, ...n para la factorizaci on de Crout) la ecuaci on 7.14 nos permite calcular l
ii
.
Conocidas l
ii
y u
ii
, podemos escribir la k-esima la:
a
kj
=
k1

s=1
l
ks
u
sj
+ l
kk
u
kj
(k < j n) (7.15)
Por ejemplo, poniendo k = 3, j = n, obtenemos:
a
3n
= l
31
u
1n
+ l
32
u
2n
+ l
33
u
3n
que corresponde a la tercera la en las ecuaciones 7.13. An alogamente, podemos escribir la k-esima
columna:
a
ik
=
k1

s=1
l
is
u
sk
+ l
ik
u
kk
(k < i n) (7.16)
Por ejemplo, poniendo k = 3, i = n, obtenemos:
a
n3
= l
n1
u
13
+ l
n2
u
23
+ l
n3
u
33
que corresponde a la tercera columna en las ecuaciones 7.13. Si l
kk
,= 0 la ecuaci on 7.15 puede
utilizarse para calcular los elementos u
kj
. An alogamente, si u
kk
,= 0 la ecuaci on 7.16 puede utilizarse
para calcular los elementos l
ik
.
7.4. Factorizaci on de Doolittle
Escogiendo ahora L como una matriz triangular inferior cuyos elementos diagonales son la unidad,
podemos escribir f ormulas explcitas para los elementos de las matrices U y L. A partir de la ecuaci on
7.14 obtenemos los terminos diagonales de U:
u
kk
= a
kk

k1

s=1
l
ks
u
sk
(7.17)
Los terminos de U por encima de la diagonal se obtienen a partir de la ecuaci on 7.15:
u
kj
= a
kj

k1

s=1
l
ks
u
sj
(k < j n) (7.18)
144
Mientras que los terminos de L por debajo de la diagonal se obtienen a partir de la ecuaci on 7.16:
l
ik
=
a
ik

k1
s=1
l
is
u
sk
u
kk
(k < i n) (7.19)
Una vez calculados los coecientes de las matrices L y U, resolvemos el sistema:
Ly = w (7.20)
Usando sustituci on progresiva:
y
i
= w
i

i1

j=1
l
ij
y
j
(7.21)
Y a continuaci on resolvemos:
Ux = y (7.22)
usando sustituci on regresiva:
x
i
=
y
i

n
j=i+1
u
ij
x
j
u
ii
(7.23)
Un problema que ya hemos mencionado antes es la posibilidad de que alguno de los elementos u
kk
en la ecuaci on 7.19 sea nulo. La forma de resolverlo es efectuar una permutaci on de las en la matriz L
que garantice que el privote (el termino divisor en la ecuaci on 7.19) no sea nulo (siempre, naturalmente,
que sea posible descomponer la matriz original mediante factorizaci on LU, lo cual ocurre si y s olo si
la matriz no es singular).
Un algoritmo de pivoteo sencillo y a la vez robusto consiste en escoger como pivote el elemento
de mayor valor absoluto en el conjunto (u
kk
, l
k+1k
, l
k+2k
...l
nk
) para cada una de las n columnas de la
matriz L. Cada una de estas permutaciones se guarda en una matriz de permutaciones P que se obtiene
a partir de las n permutaciones aplicadas entre las las de la matriz L (efectuamos una permutaci on
entre las al resolver cada columna mediante la ecuaci on 7.19, en particular, en el caso de que u
kk
resulte ser el pivote, la permutaci on correspondiente es la unidad). As pues, la descomposici on LU
se modica simplemente, escribiendo:
PAx = P(LU)x = (PL)Ux = Pw (7.24)
donde aplicamos al vector de terminos independientes w la misma matriz de permutaciones que hemos
aplicado a L. La ecuaci on 7.20 se modica de manera muy sencilla, a n de tomar en cuenta las
permutaciones de L en el proceso de factorizaci on, que deben aplicarse al vector w antes de resolver:
PLy = Pw (7.25)
Mientras que la ecuaci on 7.22 no se modica, puesto que el sistema 7.25 es equivalente al sistema 7.20
y por lo tanto el vector y que se obtiene en ambos casos es identico.
7.5. Permutaciones
Examinemos a continuaci on como representar las sucesivas permutaciones asociadas al proceso de
pivotaje. Dado un vector de dimensi on n (o una matriz cuadrada de dimensiones n n), podemos
representar la operaci on que permuta los elementos i y j del vector (las las i y j de la matriz), i < j
mediante la permutaci on (1, 2, 3...j...i...n), donde (1, 2, 3...i...j...n) es la permutaci on unidad.
Por ejemplo. Supongamos que en la factorizaci on LU de una cierta matriz de dimensi on 4 4 nos
encontramos con la siguiente situaci on al resolver columna a columna:
1. El m aximo elemento en valor absoluto del conjunto u
11
, l
21
, l
31
, l
41
es l
21
. Por tanto hay que
permutar la la 1 y la la 2.
145
2. El m aximo elemento en valor absoluto del conjunto u
22
, l
32
, l
42
es l
42
. Por tanto hay que permutar
la la 2 y la la 4.
3. El m aximo elemento en valor absoluto del conjunto u
33
, l
43
es u
33
. Por lo tanto no hay que
permutar.
4. No es necesario permutar en la ultima columna ya que u
44
no se usa como pivote.
La permutaci on unidad para n = 4 es (1, 2, 3, 4). La primera operaci on resulta en la permutaci on
p1 = (2, 1, 3, 4) que especica que el segundo elemento del conjunto ordenado sobre el que la permu-
taci on act ua (las las de la matriz problema o los elementos del vector de terminos independientes)
ha intercambiado su posici on con el primer elemento.
La segunda operaci on es permutar la la 2 y la la 4. Si partieramos de la permutaci on unidad,
obtendramos p2 = (1, 4, 3, 2). Puesto que partimos de un conjunto al que se ha aplicado previamente la
permutaci on (2, 1, 3, 4), debemos componer ambas, p = p2 p1 = (2, 4, 3, 1). Las ultimas dos columnas
no nos obligan a permutar. El resultado nal se lee directamente en la permutaci on. La primera la en
la matriz sin permutar ha acabado en cuarto lugar en la matriz permutada, la segunda en primero, la
cuarta en segundo y la tercera no se ha movido. Para deshacer la permutaci on no hay m as que leerla
al reves, de derecha a izquierda.
Formalmente, podemos construir una matriz de permutaciones P a partir de la permutaci on p.
Para ello, partimos de la matriz unidad de la dimensi on correspondiente (4 4 en nuestro ejemplo) y
permutamos, para cada la, el uno de la diagonal, con la cifra correspondiente a la columna especicada
por la permutaci on, es decir, el uno en la diagonal de la primera la lo permutamos con el 0 en la
segunda columna, el de la segunda la con el cero en la cuarta columna, el de la tercera con el uno en
la tercera columna (o sea, no permutamos) y el uno en la diagonal de la cuarta la con el cero en la
primera columna. Ergo, los terminos p
12
, p
24
, p
33
y p
41
de la matriz de permutaci on P valen uno y el
resto valen cero. Esto es:
_
_
_
_
0 1 0 0
0 0 0 1
0 0 1 0
1 0 0 0
_
_
_
_
_
_
_
_
a
11
a
12
a
13
a
14
a
21
a
22
a
23
a
24
a
31
a
32
a
33
a
34
a
41
a
42
a
43
a
44
_
_
_
_
=
_
_
_
_
a
21
a
22
a
23
a
24
a
41
a
42
a
43
a
44
a
31
a
32
a
33
a
34
a
11
a
12
a
13
a
14
_
_
_
_
Naturalmente, la misma permutaci on, aplicada al vector de terminos independientes, (w
1
, w
2
, w
3
, w
4
)
asociado con el sistema, resulta en un vector permutado (w
2
, w
4
, w
3
, w
1
).
7.6. Calculo de la matriz inversa
El c alculo de la inversa de una matriz cuadrada es inmediato una vez que se conoce su descompo-
sici on LU. Puesto que, por denici on:
M M
1
= I (7.26)
Para una matriz n n tenemos, si M = LU:
LU
_

_
a
11
a
12
... a
1n
a
21
a
22
... a
2n
...
a
n1
a
n2
a
nn
_

_
=
_

_
1 0 ... 0
0 1 ... 0
...
0 0 1
_

_
(7.27)
donde los a
ij
son los coecientes, a determinar de la matriz M
1
.
Dada la estructura diagonal de la matriz unidad, podemos descomponer la ecuaci on matricial 7.27
en n sistemas de ecuaciones lineales independientes:
LU
_

_
a
11
a
21
...
a
n1
_

_
=
_

_
1
0
...
0
_

_
146
LU
_

_
a
12
a
22
...
a
n2
_

_
=
_

_
0
1
...
0
_

_
.....
LU
_

_
a
1n
a
2n
...
a
nn
_

_
=
_

_
0
0
...
1
_

_
Como hemos visto, cada uno de estos n sistemas de ecuaciones lineales puede resolverse f acilmente
una vez que la factorizaci on LU de M ha sido calculada. La soluci on de cada sistema proporciona una
columna de la matriz inversa.
Vale la pena, sin embargo, anotar que el c alculo de M
1
implica la resoluci on de n sistemas de
ecuaciones. Consideremos ahora, de nuevo, el problema de resolver el sistema:
M x = w
Numericamente, la soluci on directa del sistema implica la factorizaci on LU de la matriz, seguida de
la soluci on de los sistemas intermedios por sustituci on progresiva y regresiva. En cambio, la soluci on
calculada a partir de la inversa:
x = M
1
w
implica, adem as de la factorizaci on LU, la soluci on de n sistemas de ecuaciones. El c alculo de la
inversa, por lo tanto, es mucho m as ineciente que la resoluci on directa del sistema.
7.7. Determinante de una matriz
El c alculo del determinante de una matriz cuya descomposici on LU se conoce es inmediato. En
efecto:
det(M) = det(LU) = det(L) det(U)
Por otra parte el determinante de una matriz triangular es igual al producto de sus elementos diago-
nales. Por construcci on los elementos diagonales de la matriz L son unos. Por lo tanto:
det(M) =
n

i
u
ii
(1)
j
(7.28)
donde j es el n umero de permutaciones que hemos efectuado en la descomposici on LU (cada permu-
taci on de las cambia el signo del determinante).
7.8. Programaci on del algoritmo de descomposici on LU
7.8.1. Clases de matrices y vectores revisadas
En el captulo 6, introdujimos una clase, RMatriz, para representar matrices de n umeros reales (en
doble precisi on), as como una clase, RVector, para representar vectores de n umeros reales. Describimos
con detalle tanto la estructura interna de cada clase (miembros privados) como su interfaz (el conjunto
de funciones p ublicas que la clase proporciona).
En este captulo presentamos una versi on revisada de dichas clases con mayor funcionalidad (es
decir, mayor n umero de funciones p ublicas capaces de ejecutar acciones utiles). A diferencia de lo que
hemos venido haciendo hasta ahora, no vamos a describir la implementaci on de estas nuevas funcio-
nes (los espritus inquietos pueden, no obstante, estudiar dichas implementaciones en vector.cpp y
147
matriz.cpp) limit andonos a describir la interfaz mejorada de las clases (cuya denici on se encuentra
en vector.h y matriz.h).

Esta es una de las grandes ventajas de la programaci on orientada a objeto. El lenguaje nos permite
encapsular las propiedades de entidades tales como matrices y vectores, ocultando los detalles de
la programaci on de tales propiedades (la implementaci on de las funciones de la clase) al resto del
programa, que s olo precisa conocer la lista de funciones y sus respectivas rmas.
Recordemos que las propiedades b asicas de un objeto tipo RMatriz son:
1. Representa una matriz de n umeros reales en doble precisi on.
2. Los ndices empiezan en uno (no en cero como en el caso de un arreglo) y se accede a ellos
mediante el uso del operador (). As, por ejemplo, si A es un objeto de tipo RMatriz, A(i, j)
accede al elemento a
ij
de la matriz.
3. Un objeto de tipo RMatriz A puede asignarse a otro de su misma dimensi on B, mediante el
operador de asignaci on, A = B.
4. Si A y B son dos objetos de tipo RMatriz los operadores binarios += y -= representan las
siguientes operaciones: A+ = B A = A + B y A = B A = AB.
5. Si A es un objeto de tipo RMatriz y d un n umero real (en doble precisi on) el operador binario
*= representa la operaci on: A = d A = A d.
6. Si A es un objeto de tipo RMatriz con elementos a
ij
= A(i, j), entonces A tiene elementos
A(i, j).
7. Dos objetos de tipo RMatriz pueden sumarse, restarse y multiplicarse resultando en otro objeto
del mismo tipo.
8. Un objeto RMatriz puede multiplicarse por la izquierda y por la derecha por un escalar (un
n umero real en doble precisi on) resultando en un objeto de tipo RMatriz.
La tabla 7.1 muestra las funciones p ublicas de la clase RMatriz, mientras que la tabla 7.2 muestra
las funciones p ublicas que manipulan objetos de la clase RMatriz.
Funci on Acci on
RMatriz() Constructor por defecto (matriz 1 1)
RMatriz(int nlas, int ncols) Constructor est andar (matriz n n)
int las() Devuelve el n umero de las de la matriz
int columnas() Devuelve el n umero de columnas de la matriz
RMatriz& intercambiaFilas(int i, int j) Intercambia las las i y j de la matriz
RMatriz& intercambiaColumnas(int i, int j) Intercambia las columnas i y j de la matriz
RMatriz& copiaFila(int i, int j) Copia la la j de la matriz en la la i
RMatriz& copiaColumna(int i, int j) Copia la columna j de la matriz en la la i
RMatriz& anulaElementos() Anula los elementos de la matriz
RMatriz& asignaIdentidad() Asigna la matriz identidad
int lee(string nombre-chero) Lee la matriz A de un chero en disco
int escribe(string chero) Escribe la matriz A en un chero en disco
Cuadro 7.1: Funciones p ublicas de la clase RMatriz.
Un objeto de tipo RVector se caracteriza por:
1. Representa un vector de n umeros reales en doble precisi on.
148
Funci on Acci on
RMatriz traspuesta(const RMatriz& m) Devuelve la matriz traspuesta de m
double maximo(const RMatriz& m, int& i, int& j) Devuelve el elemento m aximo y sus ndices
double minimo(const RMatriz& m, int& i, int& j) Devuelve el elemento mnimo y sus ndices
double maxNoDiag(const RMatriz& m, int& i, int& j) Devuelve el elemento m aximo no diagonal
double minNoDiag(const RMatriz& m, int& i, int& j) Devuelve el elemento mnimo no diagonal
Cuadro 7.2: Funciones p ublicas que manipulan objetos de la clase RMatriz.
2. Los ndices empiezan en uno (no en cero como en el caso de un arreglo) y se accede a ellos
mediante el uso del operador (). As, por ejemplo, si v es un objeto de tipo RVector, v(i) accede
al elemento v
i
del vector.
3. Un objeto de tipo RVector v puede asignarse a otro de su misma dimensi on w, mediante el
operador de asignaci on, v = w.
4. Si v y w son dos objetos de tipo RVector los operadores binarios += y -= representan las siguientes
operaciones: v+=wv = v + w y v-=wv = v w.
5. Si v es un objeto de tipo RVector y d un n umero real (en doble precisi on) el operador binario
*= representa la operaci on: v*=dv = v d.
6. Si v es un objeto de tipo RVector con elementos v
i
= v(i), entonces -v tiene elementos v(i).
7. Dos objetos de tipo RVector pueden sumarse y restarse resultando en otro objeto del mismo
tipo.
8. El operador * (producto) aplicado a dos vectores representa el producto escalar de ambos. Por
tanto el producto de dos objetos de tipo RVector es un escalar (un n umero en doble precisi on).
9. Un objeto RVector puede multiplicarse por la izquierda y por la derecha por un escalar (un
n umero real en doble precisi on) resultando en un objeto de tipo RVector.
La tabla 7.3 muestra las funciones p ublicas de la clase RVector, mientras que la tabla 7.4 muestra
las funciones p ublicas que manipulan objetos de la clase RVector.
Funci on Acci on
RVector() Constructor por defecto (vector de dimensi on 1)
RVector(int dimension) Constructor est andar (vector de dimensi on n)
int dimension() Devuelve la dimensi on del vector
RVector& anulaElementos() Anula los elementos del vector
int lee(string nombre-chero) Lee el vector v de un chero en disco
int escribe(string chero) Escribe el vector v en un chero en disco
Cuadro 7.3: Funciones p ublicas de la clase RVector.
Adem as de las funciones que manipulan matrices y vectores, hemos denido (linalg.h) e imple-
mentado (linalg.cpp) la operaci on producto (sobrecargando el operador *) as como una serie de
funciones que mezclan objetos de ambos tipos las cuales se detallan en la tabla 7.5.
149
Funci on Acci on
double productoEscalar
(const RVector& v1, const RVector& v2) producto escalar de v1 y v2
int maximo(const RVector& v) Devuelve el ndice del
elemento m aximo del vector
int minimo(const RVector& v) Devuelve el ndice del
elemento mnimo del vector
Cuadro 7.4: Funciones p ublicas que manipulan objetos de la clase RVector.
Funci on Acci on
RVector operator *
(const RMatriz& m, const RVector& v) m(i, j) v(j) = w(i)
RVector operator *
(const RVector& v, const RMatriz& m) v
T
(i) m(i, j) = w(j)
RVector proyectaFila
(const RMatriz& a, int ) proyecta la la fi de la matriz a en un vector
RVector proyectaColumna
(const RMatriz& a, int col) proyecta la columna col de la matriz a en un vector
void copiaFila
(RMatriz& a, const RVector& v, int ) copia el vector v en la la fi de la matriz a
void copiaColumna
(RMatriz& a, const RVector& v, int col) copia el vector v en la columna col de la matriz a
Cuadro 7.5: Funciones p ublicas que manipulan vectores y matrices.
7.8.2. Clases de permutaciones
Como hemos visto, la necesidad de pivotar en el algoritmo LU requiere construir una matriz de
permutaciones P durante la factorizaci on, que debe aplicarse al vector de terminos independientes
durante la sustituci on progresiva que constituye la primera parte de la soluci on del sistema.
Por otra parte, almacenar una permutaci on de orden n en una matriz nn, la mayora de cuyos ele-
mentos son ceros, es muy ineciente. Resulta mucho m as econ omico e igualmente intuitivo utilizar un
objeto de tipo permutaci on (es decir un objeto con las propiedades algebraicas de las permutaciones).
Dado un conjunto ordenado de n elementos, el conjunto de todas las permutaciones que pueden
efectuarse sobre dicho conjunto es un grupo nito (con n! elementos) con respecto a la operaci on que
reordena los elementos del conjunto. Por ejemplo, el conjunto de permutaciones de un conjunto de
tres elementos consta de 3! = 6 elementos:
p1 = (123)
p2 = (213)
p3 = (231)
p4 = (321) (7.29)
p5 = (132)
p6 = (312)
Una permutaci on puede, por lo tanto, asociarse a un nuevo tipo, Permutacion (que representa las
permutaciones an alogamente a como el tipo RMatriz representa las matrices de n umeros reales).
150
La denici on e implementaci on de la clase Permutacion se encuentra en los cheros permutacion.h
y permutacion.cpp. El programa xpermutacion.cpp ilustra como utilizar este nuevo tipo. Aqu nos
limitamos a resaltar las propiedades de las permutaciones necesarias para el algoritmo LU.
1. Un objeto de tipo Permutacion se crea mediante la instrucci on:
Permutacion p(n);
donde n es la dimensi on del conjunto ordenado sobre el que act ua la permutaci on, accediendose
a los elementos de la permutaci on mediante el operador (). As, por ejemplo, la permutaci on
unidad para n = 3 puede crearse mediante las instrucciones:
Permutacion p(3);
p(1)=1; p(2)=2; p(3)=3;
2. La permutaci on p1 puede asignarse a la permutaci on p2, mediante la instrucci on p1 = p2.
3. Las siguientes funciones est an denidas para objetos de tipo Permutacion:
const int& dimension() const; Dimensi on de la permutaci on
Permutacion& ini(); Inicializa la permutaci on a la unidad.
Permutacion& intercambia(int i, int j); Intercambia el elemento i de la permutaci on
con el elemento j.
Permutacion& siguiente(); Avanza a la siguiente permutaci on.
Permutacion& previa(); Permutaci on previa.
const int& signo() const; Signo de la permutaci on.
4. Dada una permutaci on p1 la funci on Permutacion pinv(const Permutacion& p); devuelve la
inversa de la permutaci on.
5. La composici on de permutaciones, as como la acci on de una permutaci on sobre un vector y una
matriz son operaciones bien denidas, que se expresan sobrecargando el operador producto en
los casos correspondientes:
a) Permutacion operator * (const Permutacion& p1, const Permutacion& p2);
b) RVector operator * (const Permutacion& p, const RVector& v1);
c) RMatriz operator * (const Permutacion& p, const RMatriz& m1);
7.8.3. Clases de algoritmos
Las clases que hemos estudiado hasta el momento (Complejo RMatriz, RVector, Permutacion) son
ejemplos tpicos de clases de datos que representan entidades tales como n umeros complejos, matrices,
vectores y permutaciones. Este tipo de objetos puede asignarse a otros del mismo tipo y se pueden
denir operaciones entre ellos de acuerdo a reglas bien denidas.
Vamos a introducir ahora el concepto de clase de algoritmo, para representar m aquinas que
manipulan internamente datos, ejecutando una serie de acciones sobre estos.
Consideremos, en concreto, un problema de algebra lineal. Los datos del problema son la matriz
de coecientes A y el vector de terminos independientes v. Sobre dichos datos, queremos aplicar un
algoritmo cuyas posibles acciones sean:
1. Resolver el sistema lineal, proporcionando el vector de inc ognitas x.
151
2. Calcular la inversa de la matriz A
1
.
3. Calcular el determinante de la matriz [A[.
Adem as, el algoritmo debe ser capaz de proporcionar resultados intermedios, tales como:
1. La matriz compacta LU.
2. Las matrices L y U.
3. La permutaci on asociada p.
Para realizar todas estas acciones el algoritmo tiene que ejecutar una serie de operaciones inter-
namente, tales como calcular la descomposici on LU de la matriz A y resolver los sistemas de las
ecuaciones 7.10. Tales operaciones, sin embargo, no necesitan exponerse al resto del programa, pu-
diendose encapsular en el interior del algoritmo.
Examinemos los requerimientos para un algoritmo que resuelva problemas de algebra lineal. Debe
poder aceptar como datos:
1. S olo una matriz (es posible que no haya sistema lineal asociado y que s olo se desee calcular la
inversa y/o el determinante de la matriz).
2. S olo un vector (la matriz puede haberse introducido como dato anteriormente).
3. Una matriz y un vector (sistema lineal completo).
Adem as dichos datos deben poder renovarse tantas veces como se desee. Cada vez que cambiemos
la matriz o el vector del sistema (o ambos) el algoritmo debe saber que tiene que actualizarse
internamente.
Precisamente, una caracterstica fundamental de los objetos que llamamos algoritmos,que no po-
seen los objetos que hemos llamado datos, es un estado interno bien denido, que puede reejarse
mediante la simple met afora de un cuadro de control con una serie de interruptores que s olo admiten
dos posiciones, abierto (apagado) o cerrado (encendido).
7.8.4. El algoritmo ALineal
Consideremos de nuevo nuestro algoritmo de algebra lineal. Su estado interno puede representarse
por un cuadro de control con seis interruptores, a saber:
1. Un interruptor con la etiqueta matriz que est a cerrado si hemos establecido una matriz al
algoritmo y abierto en otro caso.
2. Un interruptor con la etiqueta vector que est a cerrado si hemos establecido un vector al
algoritmo y abierto en otro caso.
3. Un interruptor con la etiqueta lu que est a cerrado si el algoritmo ha calculado ya internamente
la descomposici on LU de la matriz inicial y abierto en otro caso.
4. Un interruptor con la etiqueta lineal que est a cerrado si el algoritmo ha calculado ya interna-
mente la soluci on del sistema lineal asociado y abierto en otro caso.
5. Un interruptor con la etiqueta inversa que est a cerrado si el algoritmo ha calculado ya inter-
namente la inversa de la matriz y abierto en otro caso.
6. Un interruptor con la etiqueta determinante que est a cerrado si el algoritmo ha calculado ya
internamente el determinante de la matriz y abierto en otro caso.
152
Cuando creamos un algoritmo de algebra lineal, su estado interno inicial tiene todos los interrup-
tores abiertos. Una vez que inicializamos el algoritmo con una matriz, un vector, o ambos, los corres-
pondientes interruptores (vector y matriz) se cierran. Supongamos que, a continuaci on, pedimos
al algoritmo que calcule la matriz inversa A
1
. Puesto que el correspondiente interruptor (inversa)
est a abierto, el algoritmo ejecuta internamente las acciones necesarias para calcular A
1
, comenzando
por descomponer LU la matriz del problema (ya que a un no la ha descompuesto, lo cual se reeja en
el interruptor lu, que est a abierto). Al nal del proceso, el algoritmo ha almacenado internamente
el resultado de descomponer la matriz LU (y ha cerrado el correspondiente interruptor) as como la
matriz inversa (cerrando a su vez el interruptor asociado a esta acci on).
Si ahora pedimos al algoritmo la matriz compacta LU (o las matrices L, U), el interruptor lu
est a cerrado, lo que indica que basta con devolver la matriz LU calculada previamente no siendo
necesario calcularla de nuevo. An alogamente, si pedimos al algoritmo el determinante de la matriz, el
interruptor determinante est a todava abierto, por lo que el algoritmo debe calcular esta cantidad,
pero en el proceso usa la matriz LU calculada previamente ya que el interruptor lu sigue cerrado.
Una vez que hemos pedido al algoritmo que ejecute sus acciones b asicas una vez (inversa, deter-
minante, sistema lineal, etc.) todos los interruptores est an cerrados con lo cual cada vez que pidamos
estas cantidades el algoritmo sabe que no necesita calcularlas de nuevo sino que basta con devolver
las que ya ha almacenado. Naturalmente, si introducimos una nueva matriz, vector o ambos, el algo-
ritmo abre los correspondientes interruptores, puesto que es necesario rehacer c alculos con los nuevos
datos.
Concretamente:
1. El interruptor matriz se cierra cuando se introduce la primera matriz al algoritmo y ya no
se vuelve a abrir (este interruptor s olo sirve para impedir que intentemos ejecutar acciones con
el algoritmo sin haberlo inicializado con una matriz). No hay ninguna acci on del algoritmo que
pueda ejecutarse sin la matriz del sistema, por lo tanto, si el interruptor matriz est a abierto,
las acciones del algoritmo devuelven siempre un error.
2. El interruptor vector se cierra cuando se introduce el primer vector al algoritmo y ya no
se vuelve a abrir. La acci on del interruptor es an aloga al del anterior, con la diferencia de
que s hay una serie de acciones que pueden ejecutarse aunque el interruptor este abierto (por
ejemplo calcular la descompuesta LU, la inversa o el determinante de la matriz de coecientes).
Sin embargo, si el interruptor vector est a abierto y se intenta resolver el sistema lineal, la
acci on resulta en un error.
3. El interruptor lu se cierra cuando ejecutamos por primera vez una acci on que exija la descom-
posici on LU de la matriz (pedimos explcitamente al algoritmo la matriz compacta LU, o bien
L, U, A
1
o [A[) y se abre cuando introducimos una nueva matriz en el algoritmo.
4. El interruptor inversa se cierra cuando pedimos por primera vez A
1
al algoritmo y se abre
cuando introducimos una nueva matriz en el algoritmo.
5. El interruptor determinante se cierra cuando pedimos por primera vez [A[ al algoritmo y se
abre cuando introducimos una nueva matriz en el algoritmo.
6. interruptor lineal se cierra cuando pedimos por primera vez la soluci on del sistema lineal al
algoritmo y se abre cuando introducimos un nuevo vector (o una nueva matriz) en el algoritmo.
La acci on de un algoritmo, en resumen, est a siempre perfectamente denida por el estado del
cuadro de control, cuyos interruptores especican que variables internas deben actualizarse cuando se
le exige una acci on al algoritmo.
Para representar un cuadro de control hemos denido la clase CControl que encontrareis en el
directorio Util (ccontrol.h). No vamos a describir la clase con todo detalle (aunque es muy sencilla)
ya que no es necesario que conozcamos su estructura interna, bastando con su interfaz p ublica, que
describimos en la tabla 7.6.
153
Funci on Acci on
CControl cuadro dene cuadro como de tipo CControl
void nuevoInterruptor(string nombre)
a nade el interruptor nombre al cuadro de control
por ejemplo cuadro.nuevoInterruptor("lu")
a nade el interruptor lu al cuadro de control cuadro
void cierraInterruptor(string nombre)
cierra el interruptor nombre
por ejemplo cuadro.cierraInterruptor("lu")
cierra el interruptor lu
void abreInterruptor(string nombre)
abre el interruptor nombre
por ejemplo cuadro.abreInterruptor("lu")
cierra el interruptor lu
bool interruptorCerrado(string nombre)
Devuelve true si el interruptor est a cerrado
bool interruptorAbierto(string nombre)
Devuelve true si el interruptor est a abierto
Cuadro 7.6: Funciones p ublicas m as importantes de la clase CControl.
Con todos estos elementos, podemos denir la clase ALineal como:
class ALineal
private:
RMatriz* pLU ; // (puntero a la) Matriz LU
RMatriz* pInv ; // (puntero a la) Matriz inversa de A
RVector* pV ; // (puntero al) Vector de soluciones/inc ognitas
Permutacion* pP ; // (puntero a la) Matriz de permutaciones
double pDet; // determinante de A
int pN ; // dimensi on de la matriz (= filas = cols)
CControl pCC; // Cuadro de Control
// Constructor de copia privado para evitar que la
// clase pueda copiarse
ALineal(const ALineal& m);
// Operador de asignaci on privado (la misma raz on)
ALineal& operator = (const ALineal& lu);
// Efect ua la descomposici on LU de la matriz
int descomponeLU(RMatriz& lu,Permutacion& p);
// resuelve el sistema lineal PLU*X = V
int resuelveLU(const RMatriz& lu, const Permutacion& p,
RVector& x);
public:
// Constructor
ALineal();
154
// Destructor
~ALineal();
// Establece una matriz para el sistema lineal
// (eMatriz = establece Matriz)
void eMatriz(const RMatriz& a);
// Establece un vector para el sistema lineal
void eVector(const RVector& v);
// Establece el sistema lineal
void eSistema(const RMatriz& a, const RVector& v);
// Acciones del algoritmo
RVector x() ; // calcula/devuelve el vector de soluciones
RMatriz inversa();// calcula/devuelve la inversa de A
double det(); // calcula/devuelve el determinante de A
RMatriz lu(); // calcula/devuelve la matriz LU compacta
RMatriz l() ; // calcula/devuelve la matriz L
RMatriz u() ; // calcula/devuelve la matriz U
Permutacion p() const; // devuelve la matriz de permutaciones
;
Los datos internos del algoritmo son:
1. La matriz pLU donde almacenamos la matriz descompuesta LU.
2. La matriz pInv donde almacenamos la matriz inversa.
3. El vector pV donde almacenamos las soluciones del sistema lineal.
4. La permutaci on asociada a la descomposici on LU, pP.
5. El determinante de la matriz problema, pDet.
6. La dimensi on de la matriz, pN.
7. Un cuadro de control, pCC donde se a naden los interruptores que ya hemos descrito.
A diferencia de un dato, no tiene sentido copiar ni asignar un algoritmo, raz on por la cual hemos
denido el constructor de copia y el operador de asignaci on como privados. Tambien hemos denido
como privadas las dos funciones que constituyen la espina dorsal del algoritmo. La primera es:
int descomponeLU(RMatriz& lu,Permutacion& p);
La funci on toma una referencia a una matriz (la matriz problema a la entrada de la funci on, la cual se
ha sobreescrito con la matriz LU tras nalizar la funci on) y una referencia a una permutaci on (que a
la salida de la funci on contiene la permutaci on asociada a la descomposici on). El entero que la funci on
devuelve es cero si todo fue bien y un c odigo de error en otro caso.
La segunda funci on es:
int resuelveLU(const RMatriz& lu, const Permutacion& p,
RVector& x);
Los argumentos son referencias constantes a la matriz descompuesta LU y la permutaci on asociada
(que no se cambian) y una referencia a un vector, que a la entrada de la funci on contiene los terminos
independientes y a la salida las soluciones del sistema.
La programaci on de ambas funciones es esencialmente una transcripci on de las f ormulas matem ati-
cas que vimos en la secci on anterior.
155
/* Factorizaci on de una matriz general A(N x N) en la forma:
*
* P A = L U
*
* donde P es una matriz de permutaciones, L es triangular inferior y U
* es triangular superior.
*
* P se guarda en la permutaci on p. Si el elemento i de la permutaci on
* toma el valor k entonces p
ik
= 1, el resto de los elementos
* de la matriz son nulos. El signo de la permutaci on es (1)n,
* donde n es el n umero de intercambios en la permutaci on.
*/
int ALineal::descomponeLU(RMatriz& lu,Permutacion& p)

// uij matriz triangular superior


// lij matriz triangular inferior
//
// | u11 u12 u13...u1n |
// | 0 u22 u23...u2n |
//uij= | 0 0 u33...u3n |
// | 0 0 0 ...0 |
//
// | 1 0 0... 0 |
// | l21 1 0... 0 |
//lij= | l31 l32 0... 0 |
// | ...
// | ln1 ln2 ln3...1 |
// Itera sobre cada columna de la matriz
int pN = lu.filas();
for (int j = 1; j <= pN; j++)
// C alculo de factores
// Los uij pueden calcularse sin conocer el pivote
for (int i = 1; i <= j; i++)
lu(i,j) = lu(i,j) - sum(i,j,1,i-1,lu);

// Los lij pueden se pre-calculan


// dividimos por el pivote m as tarde
for (int i = j+1; i <= pN; i++)
lu(i,j) = lu(i,j) - sum(i,j,1,j-1,lu);

// Buscamos el pivote como el elemento mayor del


// conjunto (ujj,lj+1,lj+2,...lnj)
156
// Crea un vector de dimensi on n que contiene una copia
// de la columna j de la matriz
RVector vc = proyectaColumna(lu, j);
// Encuentra el m aximo del vector a partir de la diagonal:
int iPivote = maximo(vc,j);
// Si el pivote es cero el m etodo fracasa
if (vc(iPivote) ==0)
error("Imposible factorizar LU",
EFACTOR);
return EFACTOR;

// Si el ndice del pivote es mayor que la diagonal


// hay que permutar
if (iPivote > j)
lu.intercambiaFilas(j,iPivote);
p.intercambia(j,iPivote);

// Divide por el pivote los lij pre-calculados


for (int i = j+1; i <= pN; i++)
lu(i,j) = lu(i,j)/lu(j,j);

return EXITO;

// resuelve el sistema lineal PLU*X = V


// Donde P es la matriz de permutaciones obtenida
// cuando se descompone la matriz original para obtener LU
// Devuelve el vector de inc ognitas sustituyendo al de coeficientes
int ALineal::resuelveLU(const RMatriz& lu, const Permutacion& p,
RVector& x)

int pN = lu.filas();
// La ecuaci on a resolver es la siguiente:
// AX = PW --> (LU)X = PW --> L(UX) = PW
// UX = Y
// LY = PW
157
// Primero resolvemos PLY = W donde
// P es la matriz de permutaciones
// L la matriz inferior
// W es el vector de soluciones
// Y reemplaza a W
// y
i
= w
i


i1
j=1
l
ij
y
j
x = p*x; // LY = PW
for (int i= 1; i<=pN; i++)
for (int j = 1; j <= i-1; j++)
x(i) = x(i) - lu(i,j)*x(j);

// A continuaci on resolvemos
// UX = Y
// x
i
= y
i


n
j=1+1
u
ij
x
j
// x
i
= x
i
/u
ii
for (int i= pN; i>=1; i--)
for (int j = i+1; j <= pN; j++)
x(i) = x(i) - lu(i,j)*x(j);

x(i) = x(i)/lu(i,i);

return EXITO;

Donde hemos utilizado la funci on auxiliar:


// Funci on auxiliar de descomponeLU
static double sum(int i, int j, int ki, int kf,
const RMatriz& lu)
double sum = 0;
for (int k = ki; k <= kf; k++)
double bik = lu(i,k);
double ckj = lu(k,j);
sum += bik*ckj;

return sum;

La implementaci on de la inversa y el determinante de la matriz nos muestra como funciona en la


pr actica el mecanismo de interruptores:
// Calcula la inversa de A resolviendo el sistema lineal
// PLU = W para los vectores de la base
158
RMatriz ALineal::inversa()

// Hay una matriz en la m aquina?


if (pCC.interruptorAbierto("matriz"))
error("La matriz del sistema no ha sido establecidad",
ENOINI);
// Si ya hemos calculado la inversa el
// correspondiente interruptor est a cerrado
if (pCC.interruptorCerrado("inversa"))
return *pInv;

else
// Si el interruptor est a abierto
// descompone la matriz LU
if (pCC.interruptorAbierto("lu"))
// Descompone la matriz LU
// Toma en el argumento la matriz LU y la permutaci on
int status = descomponeLU(*pLU,*pP);
if (status) error("Error en LU",status);
pCC.cierraInterruptor("lu");

// ai es una referencia a la matriz inversa


RMatriz& ai = *pInv;
// Inicializamos a la matriz unidad
ai.asignaIdentidad();
// Iteramos sobre los vectores de la base
for (int i = 1; i<= pN; i++)
// Proyectamos las columnas de la matriz
// (vectores de la base)
RVector x = proyectaColumna(ai, i);
// x es el vector de inc ognitas a la salida
int status = resuelveLU(*pLU,*pP,x);
if (status) error("Error en LU::inversa",status);
// copia x an ainv
copiaColumna(ai, x, i);

// Hemos calculado la inversa, cierra el interruptor


pCC.cierraInterruptor("inversa");
return ai;

// calcula el determinante de A
double ALineal::det()

159
// Hay una matriz en la m aquina?
if (pCC.interruptorAbierto("matriz"))
error("La matriz del sistema no ha sido establecidad",
ENOINI);
// Si ya hemos calculado el determinante el
// correspondiente interruptor est a cerrado
if (pCC.interruptorCerrado("determinante"))
return pDet;

else
// Si el interruptor est a abierto
// descompone la matriz LU
if (pCC.interruptorAbierto("lu"))
int status = descomponeLU(*pLU,*pP);
if (status) error("Error en LU",status);
pCC.cierraInterruptor("lu");

// Accede a la matriz u (superior) mediante


// una referencia constante (para simplificar
// notaci on)
const RMatriz& u = this->u();
double det = 1;
for (int k = 1; k <= pN; k++)
det *=u(k,k);

// El signo del determinante es la paridad de la permutaci on


pDet = det*pP->signo();
// cierra el interruptor
pCC.cierraInterruptor("determinante");
return pDet;

El resto de la implementaci on de la clase, cuya descripci on omitimos por brevedad, se encuentra


en alineal.cpp.
7.8.5. Aplicaci on del algoritmo ALineal
Dise nar e implementar el algoritmo que acabamos de comentar requiere un nivel razonable de
dominio de C++, adem as de conllevar un moderado esfuerzo de programaci on. A cambio, la utilizaci on
del algoritmo para resolver un problema de algebra lineal es muy sencilla.
El programa xalin.cpp ilustra este punto.
// xalin
// Ejemplo de resoluci on de problemas
// de algebra lineal
160
#include <iostream>
#include <linalg.h>
#include <alineal.h>
#include <error.h>
#include <matutil.h>
int main ()
string fmatriz="m1d5.dat";
string fvector="b1d5.dat";
//Crea un algoritmo de algebra lineal
ALineal xal;
// Crea una matriz de dimensi on 5
int n = 5;
RMatriz m(n,n);
// Y leela del disco
m.lee(fmatriz);
cout << " matriz m = " << m << endl;
// Establece la matriz en el algoritmo
xal.eMatriz(m);
// Calcula la inversa y el determinante
RMatriz minv = xal.inversa();
cout << " minv = " << minv << endl;
cout << " m*minv = " << m*minv << endl;
cout << " minv*m = " << minv*m << endl;
cout << "determinante = "<<xal.det()<<endl;
// Crea un nuevo vector
RVector w(n);
// Y leelo
w.lee(fvector);
cout << " vector = " << w << endl;
// Establece el vector en el algoritmo
xal.eVector(w);
// Resolvemos el sistema
RVector x = xal.x();
cout << " Vector de inc ognitas"
<< x << endl;
// Y Comprobamos:
cout << " m * x - w=" << m*x -w << endl;
return 0;
Como puede observarse la notaci on es muy concisa e intuitiva. Conviene insistir de nuevo en que
no es necesario entender cada uno de los detalles de la implementaci on del algoritmo para utilizarlo,
basta con conocer las acciones que proporciona.
161
7.8.6. Recapitulaci on
En este captulo hemos estudiado la resoluci on de sistemas lineales de ecuaciones, utilizando en
primer lugar la factorizaci on LU de la matriz del sistema, seguida de la resoluci on (por sustituci on
progresiva y regresiva) del sistema LU asociado. Hemos visto tambien que el c alculo del determinante
de una matriz es inmediato una vez que esta se ha descompuesto LU. Para calcular la inversa es
necesario resolver el sistema de ecuaciones para los vectores de la base. No obstante, la resoluci on de
un sistema lineal mediante el c alculo de la inversa es mucho m as lento que su resoluci on directa, raz on
por la cual este segundo metodo debe ser el preferido.
Hemos introducido un nuevo tipo de clases, las clases de algoritmos, que a diferencia de las clases
de datos que hemos estudiado en captulos anteriores representan m aquinas capaces de ejecutar
una serie de acciones sobre un conjunto de datos. Tales m aquinas est an caracterizadas por un estado
interno que hemos representado por un cuadro de control en el que una serie de interruptores (abiertos
o cerrados) controlan cuando el algoritmo debe actualizarse. El algoritmo ALineal es un ejemplo de
clase de algoritmo capaz de resolver problemas de algebra lineal. El n ucleo del algoritmo son las
funciones descomponeLU y resuelveLU que se ocupan, respectivamente, de descomponer LU una
matriz y de resolver el sistema lineal asociado. Declarando dichas funciones como internas a la clase
encapsulamos todo el manejo de datos, de tal manera que el uso del algoritmo ALineal resulta muy
sencillo y econ omico en terminos de lneas de programaci on, adem as de minimizarse la posibilidad de
errores.
162
Captulo 8
Diagonalizaci on de matrices
8.1. Introducci on
Consideremos una matriz cuadrada A, de dimensi on N. Decimos que un cierto vector X es un
vector propio, con el valor propio , si satisface el conjunto de ecuaciones:
A X = X (8.1)
Que equivale a:
a
11
x
1
+ a
12
x
2
+ + a
1N
x
N
= x
1
a
21
x
1
+ a
22
x
2
+ + a
2N
x
N
= x
2

a
N1
x
1
+ a
N2
x
2
+ + a
NN
x
N
= x
N
(8.2)
Las ecuaciones 8.2 pueden rescribirse como un sistema homogeneo de ecuaciones sin m as que pasar
los terminos en x a la izquierda. Recordemos, por otra parte, que un sistema homogeneo de ecuaciones
solo tiene soluci on distinta de la trivial si el rango de la matriz es menor que el n umero de inc ognitas,
lo que implica:
det(AI) =
_

_
a
11
a
12
... a
1N
a
21
a
22
... a
2N
...
a
N1
a
N2
a
NN

_

_
= 0 (8.3)
La ecuaci on 8.3 se conoce como ecuaci on caracterstica. El conjunto =
1
,
2
, ...,
N
de
soluciones se llama valores propios (o autovalores) de la matriz A. Pueden ser reales o complejos (son
siempre reales en el caso de matrices simetricas o hermticas) y no son necesariamente todos distintos.
Denominamos degeneraci on de un valor propio a su grado de multiplicidad. Si
0
es un autovalor de
A su multiplicidad algebraica, m, es la multiplicidad de
0
como raz de la ecuaci on caracterstica, 8.3,
mientras que su multiplicidad geometrica, d, es la dimensi on del subespacio propio de A asociado a

0
, es decir, d = N rango(AI).
Para determinar los vectores propios (o autovectores) asociados a un valor propio hay que susti-
tuir en el sistema de ecuaciones lineales por el valor propio concreto
i
. Puesto que el sistema es
compatible indeterminado (lo cual est a garantizado por el hecho de que det(A I) = 0) las ecua-
ciones no son linealmente independientes y la forma de resolver el sistema, en la pr actica, consiste en
dar valores arbitrarios para tantas componentes de los vectores propios como indique el valor de la
multiplicidad. Por ultimo, una vez determinados los vectores propios estos pueden normalizarse, por
ejemplo, tomando la norma eucldea:
N

i=1
x
2
i
= 1 (8.4)
163
En este captulo vamos a estudiar un metodo numerico sencillo, el algoritmo de Jacobi, para deter-
minar los autovalores y autovectores de matrices reales y simetricas, las cuales, adem as de tener todos
sus autovalores reales pueden diagonalizarse mediante transformaciones ortogonales, como veremos en
la siguiente secci on.
Por otra parte existe un amplio n umero de problemas fsicos interesantes que pueden representarse
mediante matrices simetricas (y sus equivalentes en el caso de los complejos, las matrices hermticas),
de ah la gran importancia de poder tratar numericamente estos casos.
8.2. Matrices diagonalizables
8.2.1. Matrices diagonalizables por semejanza
Sea A una matriz cuadrada, de dimensi on n n. Se dice que A es diagonalizable por semejanza
si existe una matriz diagonal semejante a esta, esto es, si existe una matriz regular P y una matriz
diagonal D tales que:
D = P
1
AP (8.5)
Diagonalizar A no es otra cosa que hallar las matrices D y P.
Si A tiene
1
,
2
, ...,
p
autovalores distintos con multiplicidades algebraicas m
1
, m
2
, ..., m
p
y mul-
tiplicidades geometricas d
1
, d
2
, ..., d
p
entonces A es diagonalizable si y s olo si se verican las siguientes
condiciones:
1. m
1
+ m
2
+ ...m
p
= n
2. d
i
= m
i
i (i = 1, 2, ..., p)
En particular si A tiene n autovalores distintos entonces es diagonalizable. Suponiendo que A sea
diagonalizable su forma diagonal, D, ser a:
D =
_
_
_
_

1
0 ... 0
0
2
... 0
... ... ... ...
0 0 0
n
_
_
_
_
(8.6)
Donde
1
,
2
, ...,
n
son los autovalores de A, cada uno de ellos repetido tantas veces como indique
su multiplicidad algebraica. La matriz P se obtiene escribiendo como columnas los vectores de las
bases de los subespacios vectoriales generados por los autovalores de A. Es decir, P es la matriz de
autovectores.
Consideremos, como ejemplo, la matriz:
A =
_
_
1 2 10
2 1 10
1 1 6
_
_
Se trata de demostrar, en primer lugar que la matriz puede diagonalizarse, encontrando a continuaci on
sus autovalores y autovectores.
Para ello, comenzamos por resolver la ecuaci on caracterstica:
= det(AI) = 0
=
3
+ 4
2
+ 5 + 2 = 0
( + 1)
2
( + 2) = 0 = 2(m = 1), = 1(m = 2)
164
A continuaci on calculamos los vectores propios:
AI =
_
_
1 2 10
2 1 10
1 1 6
_
_
= 1
_
_
2 2 10
2 2 10
1 1 5
_
_

_
_
2 2 10
0 0 0
0 0 0
_
_
d
1
= 3 1 = 2 = m
1
= 2
_
_
3 2 10
2 3 10
1 1 4
_
_

_
_
3 2 0
2 3 0
1 1 0
_
_
d
1
= 3 2 = 1 = m
2
Puesto que las multiplicidades algebraicas y geometricas son iguales la matriz puede diagonalizarse.
Encontramos ahora la base del subespacio propio V
=1
.
_
_
2 2 10
2 2 10
1 1 5
_
_
_
_
x
y
z
_
_
=
_
_
0
0
0
_
_
x + y + 5z = 0
Tenemos tres inc ognitas y una sola ligadura. Dando valores arbitrarios a x e y obtenemos una base:
x = 1, y = 1 z = 0 (1, 1, 0)
x = 5, y = 0 z = 1 (5, 0, 1)
Encontramos ahora la base del subespacio propio V
=2
:
_
_
3 2 10
2 3 10
1 1 4
_
_
_
_
x
y
z
_
_
=
_
_
0
0
0
_
_

3x + 2y + 10z = 0
2x + 3y + 10z = 0
En este caso tenemos tres inc ognitas y dos ligaduras, resultando en:
x y = 0 x = y
5x + 10z = 0 z =
1
2
x
x = 2 y = 2, z = 1 (2, 2, 1)
Por lo tanto las matrices P y D ser an:
P =
_
_
1 5 2
1 0 2
0 1 1
_
_
D =
_
_
1 0 0
0 1 0
0 0 2
_
_
8.2.2. Matrices reales y simetricas. Metodo de Jacobi
Toda matriz real simetrica, A, de dimensi on nn, es ortogonalmente diagonalizable, es decir existe
alguna matriz ortogonal P (P
T
= P
1
) tal que D = P
T
AP. Las columnas de P son vectores propios
de A que, en este caso, forman un sistema ortonormal.
El metodo de Jacobi se base en esta propiedad de las matrices simetricas, para ejecutar una
secuencia de transformaciones de la forma:
A P
T
1
A P
1
P
T
2
P
T
1
A P
1
P
2
P
T
3
P
T
2
P
T
1
A P
1
P
2
P
3
etc
Cada transformaci on, llamada rotaci on de Jacobi, es simplemente una rotaci on en un plano, di-
se nada para cancelar uno de los elementos no diagonales. Naturalmente, el efecto de las transforma-
ciones sucesivas resulta en que terminos previamente cancelados tomen de nuevo valores no nulos, a
165
pesar de lo cual los elementos de fuera de la diagonal van disminuyendo, hasta que la matriz queda
diagonalizada, dentro de la precisi on de nuestro ordenador. El metodo converge siempre para matrices
reales y simetricas y resulta muy adecuado para matrices de dimensi on moderada (hasta orden 1010,
m as o menos). Inicialmente, vamos a suponer que la estrategia que adoptamos para cancelar elementos
no diagonales consiste en considerar primero el elemento de mayor m odulo, al que llamaremos a
pq
,
efectuar una rotaci on P
pq
que lo anule, buscar a continuaci on el siguiente elemento de mayor m odulo
y continuar as el proceso hasta que se cumpla cierta condici on de convergencia satisfactoria, por
ejemplo, que el m odulo del elemento mayor sea menor que un cierto n umero peque no, que especica
la precisi on de nuestro c alculo. As pues, una vez identicado el elemento de mayor m odulo, a
pq
,
efectuamos la transformaci on ortogonal:
A

= P
T
pq
A P
pq
(8.7)
Donde la rotaci on P
pq
en el plano pq viene dada por:
P
pq
=
_

_
1 0 0 ... ... 0
0 1 0 ... ... 0
... 1 ... ... 0
0 0 ... cos ... sin ... 0
... ... 1 ... 0
0 0 ... sin ... cos ... 0
0 0 ... ... 0
0 0 ... ... 1
_

_
(8.8)
El producto P
T
pq
A cambia s olo las las p y q de A, mientras que el producto A P
pq
cambia s olo las
columnas p y q. Por lo tanto, los unicos elementos que cambian en la matriz A tras la transformaci on
8.7, est an en las las y columnas p y q, es decir:
P
pq
=
_

_
... a

1p
... a

1q
...
a

2p
... a

2q
...
... ... ... ...
a

p1
... a

pp
... a

pq
... a

pn
... ... ...
a

q1
... a

qp
... a

qq
... a

qn
... ... ...
... a

np
a

nq
...
_

_
(8.9)
Es decir, los elementos que se ven afectados por la rotaci on son los a
pp
, a
qq
, a
pq
, a
qp
y los a
ip
, a
iq
(i ,=
p, i ,= q). Efectuando las multiplicaciones en 8.7 y utilizando la simetra de la matriz A, obtenemos
las f ormulas explcitas:
a

ip
= a

pi
= ca
ip
sa
iq
i ,= p, i ,= q
a

iq
= a

qi
= ca
iq
+ sa
ip
i ,= p, i ,= q
a

pp
= c
2
a
pp
+ s
2
a
qq
2sca
pq
s = sin , c = cos
a

qq
= s
2
a
pp
+ c
2
a
qq
+ 2sca
pq
a

pq
= (c
2
s
2
)a
pq
+ sc(a
pp
a
qq
)
(8.10)
La idea del metodo es cancelar los elementos no diagonales por medio de una serie de rotaciones
planares. De acuerdo con esto, para conseguir que el termino sea nulo, en las ecuaciones 8.10, debemos
hacer:
a

pq
= 0 (a
qq
a
pp
) sin cos = a
pq
(cos
2
sin
2
) (8.11)
Utilizando la denici on del angulo doble:
sin2 = 2 sin cos
cos 2 = cos
2
sin
2

166
Obtenemos:
tan 2 =
2a
pq
a
pp
a
qq
(8.12)
Que corresponde a una rotaci on de /4 para a
pp
= a
qq
.
8.3. Implementaci on numerica del algoritmo de Jacobi
Las ecuaciones 8.10 pueden calcularse sin necesidad de utilizar directamente funciones trigonometri-
cas, sin m as que aplicar las relaciones elementales entre estas. La condici on 8.11 para cancelar el
termino a
pq
, puede reescribirse como:
= cot 2 =
c
2
s
2
2sc
=
a
qq
a
pp
2a
pq
(8.13)
Donde s = seno() y c = cos(). Deniendo la tangente t = s/c, obtenemos:
t
2
+ 2t 1 = 0 (8.14)
Recordemos que las races de una ecuaci on de segundo grado pueden escribirse como:
ax
2
+ bx + c = 0
x
1
=
b+(b
2
4ac)
1/2
2a
=
2c
b+(b
2
4ac)
1/2
x
2
=
b(b
2
4ac)
1/2
2a
=
2c
b+(b
2
4ac)
1/2
Estamos interesados en el menor valor de la tangente que verica la ecuaci on 8.14, es decir en la
soluci on m as peque na:
t =
2
2 +

4
2
+ 4
=
sgn()
[[ +

2
+ 1
(8.15)
En el lmite de muy grande (lo suciente como para que no pueda calcularse en nuestro ordenador)
podemos aproximar

2
+ 1

= y por lo tanto:
t = 1/(2) (8.16)
Una vez calculada la tangente, escribimos el seno y el coseno en funci on de esta:
c =
1

t
2
+ 1
, , s = tc (8.17)
Podemos ahora reescribir las ecuaciones 8.10 adecuadamente. Una vez escogido el angulo de rota-
ci on que cancela a
pq
este termino se cancela, a

pq
= 0. En el resto de las ecuaciones, podemos escribir
los nuevos terminos en funci on de los terminos anteriores m as una correcci on peque na. Por ejemplo,
el termino a

pq
vale:
a

pp
= c
2
a
pp
+ s
2
a
qq
2sca
pq
Por otra parte, utilizando que:
a

pq
= (c
2
s
2
)a
pq
+ sc(a
pp
a
qq
) = 0
a
qq
=
c
2
s
2
sc
a
pq
+ a
pp
Nos queda:
a

pp
= c
2
a
pp
+ s
2
c
2
s
2
sc
a
pq
+ s
2
a
pp
2sca
pq
= a
pp
+
s
c
a
pq
(c
2
s
2
2c
2
) = a
pp
ta
pq
167
Es decir, expresamos el nuevo termino a

pq
en funci on del termino de la iteraci on anterior, a
pq
m as
una correcci on peque na (puesto que, presumiblemente, la rotaci on que estamos efectuando, sobre todo
al cabo de unas pocas iteraciones, es peque na). Manipulando de forma an aloga los dem as terminos
llegamos a:
a

pp
= a
pp
ta
pq
a

qq
= a
qq
+ ta
pq
a

ip
= a
ip
s(a
iq
+ a
ip
)
a

iq
= a
iq
+ s(a
ip
a
iq
)
(8.18)
donde = tan

2
=
s
1+c
.
Las ecuaciones 8.18 pueden escribirse directamente en un algoritmo numerico que implemente el
metodo de Jacobi.
Puede demostrarse que el metodo de Jacobi converge siempre, ya que las sucesivas rotaciones van
cancelando los terminos fuera de la diagonal. El proceso naliza cuando obtenemos una matriz diagonal
hasta una cierta precisi on especicada de antemano. Los elementos diagonales son los autovalores de
la matriz original A, ya que:
D = V
T
A V
Donde:
V = P
1
P
2
P
3
..
Son las matrices de rotaci on de Jacobi que aplicamos sucesivamente. Las columnas de la matriz V son
los autovectores. En cada paso del c alculo pueden obtenerse aplicando la f ormula:
V

= V P
i
Finalmente comentar que el metodo no arroja autovalores y autovectores ordenados.
8.4. Programaci on de un algoritmo de diagonalizaci on de ma-
trices reales y simetricas
Podemos programar el algoritmo de diagonalizaci on de matrices reales y simetricas sin m as que
aplicar directamente las ideas sobre clases de algoritmos estudiadas en el captulo 7. Los datos que
queremos manejar son matrices reales y simetricas. El metodo numerico subyacente al algoritmo es el
de Jacobi. Las unicas acciones que el algoritmo ejecuta son:
1. Devuelve la matriz simetrica original, A.
2. Calcula/Devuelve la matriz diagonalizada, D.
3. Calcula/Devuelve la matriz P, formada por los autovectores en columna.
Programar este algoritmo es, de hecho, m as sencillo que la programaci on del algoritmo de algebra
lineal estudiado en el captulo 7, ya que s olo necesitamos dos interruptores, uno para indicar que la
matriz del problema ha sido establecida al menos una vez (si no es el caso el algoritmo aborta tras
dar un mensaje de error cuando se le pide una acci on) y otro para indicar si ya hemos diagonalizado
la matriz o no (de hecho el algoritmo podra programarse con un s olo interruptor, pero es m as simple
utilizar dos, como hemos hecho). Las funciones en las que se programa el metodo de Jacobi son internas
al algoritmo y se limitan a transcribir las f ormulas 8.18.
8.4.1. Clase de matrices simetricas
La unica novedad, desde el punto de vista de programaci on que introducimos en este captulo es
la clase de matrices simetricas.
168
Las matrices simetricas, como vimos, tienen una serie de propiedades muy interesantes, entre las
que se cuentan que sus autovalores son siempre reales. De ah que el algoritmo de Jacobi no sea sino
el m as sencillo de una familia de algoritmos que se ocupan de diagonalizar tales matrices, las cuales,
conviene insistir, aparecen con frecuencia en problemas de fsica e ingeniera.
Por otra parte, tanto Jacobi como cualquier otro algoritmo de su tipo, requiere una matriz simetrica.
Eso quiere decir que debemos garantizar que el objeto que introducimos al algoritmo sea de ese tipo
y no de otro. No podemos conformarnos con nuestra clase habitual de matrices RMatriz, ya que no
tenemos forma de garantizar que la matriz sea simetrica, dado que los operadores de asignaci on nos
permiten sobreescribir cualquier elemento de la matriz. De hecho, para garantizar que una matriz sea
simetrica, es necesario impedir acceso de escritura a sus elementos, una vez que se ha creado.
Una posibilidad muy sencilla, la que hemos adoptado aqu, es formar una matriz simetrica a partir
de una matriz real (una RMatriz) simplemente qued andonos con la parte de la matriz por encima de
la diagonal y forzando a
ij
= a
ji
internamente.
La clase SMatriz (que podeis encontrar en smatriz.h, smatriz.cpp) representa una matriz
simetrica. Sus propiedades son an alogas a las de una matriz real, con la excepci on de que no puede
accederse directamente a sus elementos y por lo tanto s olo puede crearse a partir de una matriz real.
Por ejemplo:
RMatriz m(3,3);
m(1,1) = 3; m(1,2) = 1 ; m(1,3) = 1;
m(2,1)= 1; m(2,2) = 3; m(2,3) =1;
m(3,1)=1; m(3,2) = 1; m(3,3) =3;
SMatriz s(m);
El elemento interno de una matriz simetrica no es otra cosa que una matriz real que podemos
devolver para lectura. Sin embargo el operador () que usamos para acceder a los elementos se declara
en la denici on de la clase como privado, de tal manera que sea imposible reescribir los elementos de
la matriz simetrica.
La implementaci on de las funciones de la clase no a nade nada nuevo a la discusi on de captulos
anteriores y la omitimos ( los espritus inquietos, pueden no obstante, consultar smatriz.cpp).
8.4.2. El algoritmo RDiagonal
El algoritmo RDiagonal, para diagonalizar matrices reales y simetricas viene implementado por la
clase de algoritmo RDiagonal:
class RDiagonal
private:
SMatriz* pM; // matriz sim etrica
RMatriz* pD; // matriz diagonalizada
RMatriz* pV; // matriz de autovectores
double pEps; // precisi on en diagonalizaci on
int pMaxiter; // n umero m aximo de iteraciones
CControl pCC; // Cuadro de Control
// Constructor de copia y operador = privados
// Las clases tipo "m aquina" no pueden copiarse
RDiagonal(const RDiagonal& rd)
RDiagonal& operator = (const RDiagonal& rd)
// Las funciones de diagonalizaci on son privadas
// a la clase.
169
void jacobi(RMatriz& a, RMatriz& r, double eps, double maxiter);
void rotJacobi(RMatriz& a, RMatriz& r, int p, int q);
public:
// constructor
RDiagonal();
// Destructor
~RDiagonal();
// Establece una matriz para diagonalizar
void eMatriz(const SMatriz& m,
double precision=1e-5, int maxiter = 100);
// Acciones
SMatriz& matriz() ; // devuelve la matriz sim etrica original
RMatriz& diagonal(); // devuelve matriz diagonalizada
RMatriz& autovectores() ; // devuelve matriz autovectores
;
El n ucleo del algoritmo es la funci on jacobi:
jacobi(RMatriz& a, RMatriz& r, double eps, double maxiter);
Notemos que la funci on toma como argumentos dos matrices reales (de tipo RMatriz). La primera es,
a la entrada de la funci on la matriz que queremos diagonalizar y a la salida la matriz diagonalizada.
La segunda es la matriz de autovectores, que se calculan durante la ejecuci on de la funci on.
La forma en que el algoritmo de Jacobi funciona, nos obliga a acceder a los elementos de la matriz
que queremos diagonalizar (ya que tenemos que efectuar sobre estos una serie sucesiva de rotaciones).
Por otra parte, por dise no, no puede accederse a los elementos de una matriz simetrica. Lo cual, nos
obliga a que la funci on jacobi no tome una matriz simetrica como argumento. Pero esta circunstancia
no supone ning un problema, ya que jacobi es una funci on interna (privada) al algoritmo RDiagonal
que no puede accederse desde fuera y s olo se llama desde las funciones diagonal y autovectores.
Estas funciones pasan en el argumento la matriz real interna a la matriz simetrica que el usuario ha
establecido en el algoritmo. En consecuencia, no hay posibilidad de error. Este es un claro ejemplo
de como la tecnica de ocultar o encapsular los datos (en este caso la estructura interna de la matriz
simetrica) nos ayuda a programar de manera m as correcta, sencilla y eciente.
La funci on jacobi se limita a encontrar el elemento mayor fuera de la diagonal y efectuar sobre
este una rotaci on planar de acuerdo a las f ormulas 7:
void RDiagonal::jacobi(RMatriz& a, RMatriz& r, double eps, double maxiter)

// Inicializamos r a la matriz unidad


r.asignaIdentidad();
// Iteramos hasta alcanzar el n umero m aximo
// de iteraciones o la precisi on requerida
// en el elemento mayor fuera de la diagonal mayor
int iter =0; // iteraciones
double max = BIG; // elemento m aximo
while (iter <= maxiter && abs(max) > eps)
170
int p,q;
// Encontramos el elemento m aximo fuera de la diagnonal
max = maxNoDiag(a,p, q);
if (max == 0) break; // matriz ya diagonal
// Efectuamos una rotaci on de jacobi en dicho elemento
rotJacobi(a,r,p,q);
iter++;

void RDiagonal::rotJacobi(RMatriz& a, RMatriz& r, int p, int q)

// Efect ua una rotaci on teta de Jacobi en el plano pq, tal que


// el t ermino a(p,q)=0.
// La rotaci on se almacena en r, cuyas columnas ser an los autovectores
// de a al finalizar el proceso
// n es la dimensi on de la matriz a (y r)
int n = a.filas(); // matriz sim etrica, filas = columnas
// d = cot(2teta) f ormula 1.12
double d = (a(q,q)-a(p,p))/(2.0*a(p,q));
double t;
if (abs(d) < BIG)
//t = tan(teta) f ormula 1.14
t = signo(d)/(fabs(d) + sqrt(d*d + 1));

else
t = 1/(2*d);

// c = cos(teta)
double c = 1.0/sqrt(t*t + 1.0);
// s = sin(teta)
double s = t*c;
double tau = s/(1+c);
// Ecuaciones (1.17)
a(p,p) -= t*a(p,q);
a(q,q) += t*a(p,q);
a(p,q) = a(q,p) = 0.0;
for (int i = 1; i <= n; i++)
if (i != p && i!= q)
double aip = a(i,p) - s*(a(i,q) + tau*a(i,p));
double aiq = a(i,q) + s*(a(i,p) - tau*a(i,q));
a(i,p) = a(p,i) = aip;
a(i,q) = a(q,i) = aiq;

171
// Guardamos la rotaci on en la matriz r
for (int i = 1; i <= n; i++)
double Rip = c*r(i,p) - s*r(i,q);
double Riq = s*r(i,p) + c*r(i,q);
r(i,p) = Rip;
r(i,q) = Riq;

8.4.3. Ejemplo de aplicaci on. El programa xdiag


Una vez m as, el trabajo que nos tomamos a la hora de construir el algoritmo viene recompensado
por lo f acil que resulta utilizarlo, como ilustra el programa xdiag.cpp:
// xdiag.cpp
// Banco de pruebas algoritmo de Jacobi
//----Cabeceras ---
// biblioteca est andar
#include <iostream>
#include <string>
// control de errores
#include <error.h>
// utilidades matem aticas
#include <matutil.h>
// Clase RDiag
#include <rdiagonal.h>
// Espacio de nombres est andar
using namespace std;
//---main---
int main ()

// Lee matriz real de disco


int n;
cout << " dimensi on de la matriz? "; cin >> n;
RMatriz m(n,n);
string fichero;
cout << " Nombre fichero matriz? "; cin>> fichero;
m.lee(fichero);
cout << "matriz real m = " << m << endl;
// Construye una matriz sim etrica a partir de m;
SMatriz sm(m);
// Operador <<
cout << " Matriz sim etrica sm = " << sm << endl;
172
// Inicializa una m aquina de diagonalizar
RDiagonal xd;
// Establece matriz
// Todos los par ametros por defecto
xd.eMatriz(m);
// Imprime resultados
cout << "matriz diagonalizada" << xd.diagonal() << endl;
cout << "autovectores" << xd.autovectores() << endl;
return EXITO;

El resultado de ejecutar el programa es el siguiente:


$ ./xdiag.exe
dimensi on de la matriz? 3
Nombre fichero matriz? m6d3.dat
matriz real m =
| 3 1 1 |
| 1 3 1 |
| 1 1 3 |
Matriz sim etrica sm =
| 3 1 1 |
| 1 3 1 |
| 1 1 3 |
matriz diagonalizada
| 2 0 0 |
| 0 5 0 |
| 0 0 2 |
autovectores
| 0.79 0.58 0.21 |
| -0.21 0.58 -0.79 |
| -0.58 0.58 0.58 |
8.5. Recapitulaci on
En este captulo hemos estudiado una tecnica sencilla para diagonalizar matrices reales y simetricas,
el algoritmo de Jacobi. Hemos programado dicho algoritmo utilizando las clases de datos (vectores,
matrices, etc.) desarrolladas en captulos anteriores (as como una nueva clase, que hemos estrenado
en el captulo, de matrices simetricas). El algoritmo en s mismo se programa de manera an aloga al
algoritmo de algebra lineal del captulo 7 (es de hecho un ejemplo m as sencillo de clase de algoritmo).
173
174
Captulo 9
Aproximaci on de funciones
9.1. Introducci on
El problema b asico que atacamos en este captulo es el siguiente. Disponemos de una tabla de n+1
puntos (llamados nodos) en los que conocemos el valor de cierta funci on f(x), tal como se ilustra en
la tabla 9.1, pero no conocemos la forma explcita de dicha funci on y deseamos encontrar una funci on
F(x) que aproxime la funci on f(x), para lo cual disponemos de las ligaduras F(x
i
) = f(x
i
).
Cuando requerimos que F(x) aproxime a f(x) en el intervalo que abarca el conjunto de nodos, es
decir para valores de x tales que x
0
< x < x
n
, estamos tratando con un problema de interpolaci on.
Recprocamente, si utilizamos F para aproximar f en puntos x / [a, b], estamos tratando un problema
de extrapolaci on. En este captulo vamos a estudiar tan s olo problemas de interpolaci on, que admi-
ten un tratamiento matem atico robusto, pudiendose en general encontrar siempre una interpolaci on
razonable a los valores de una funci on. Por el contrario, la extrapolaci on de una funci on aproximada
a puntos fuera del conjunto de nodos es un proceso inestable, que s olo es aceptable, por lo general,
cuando los puntos a los que se extrapola est an muy cerca del conjunto de nodos.
x x
0
x
1
x
2
... x
n
y y
0
y
1
y
2
... y
n
Cuadro 9.1: Tabla de n + 1 nodos en los que se conocen las im agenes de cierta funci on f: y
i
= f(x
i
),
i = 0, 1, 2, ...n.
El problema de interpolar un conjunto de nodos a una funci on est a relacionado, pero no es identico
al problema de ajustar un cierto conjunto de medidas a un modelo representado por una funci on,
que trataremos en el captulo 11. En ambos casos queremos encontrar una funci on F(x) que tenga la
propiedad de ajustar la tabla de datos, es decir que verique:
F(x
i
) = f(x
i
) (9.1)
Sin embargo, en este captulo asumimos que los datos se conocen sin error o con un error des-
preciable, mientras que en el captulo 11, consideraremos el caso en que los datos se conocen con un
cierto error de medida. La diferencia entre ambas situaciones es substancial. Si cada uno de los datos
de la tabla 9.1 se conoce sin error, entonces la condici on 9.1 se verica estrictamente, mientras que si
las medidas est an afectadas de errores, entonces, en general, es posible encontrar m as de una funci on
que verique la condici on 9.1 dentro de la tolerancia permitida por los errores experimentales.
Existen diversos metodos para escoger la funci on F. El m as sencillo es utilizar un polinomio de
grado menor o igual que n. Existe un unico polinomio de grado n que pasa por los n + 1 nodos que
constituyen la tabla inicial de datos (es decir, una unica recta que pasa por dos puntos, una unica
175
par abola que pasa por tres, etc., etc.) si bien dicho polinomio puede expresarse de distintas maneras,
como comentaremos a lo largo del captulo.
Por otra parte, interpolar n + 1 puntos a un polinomio de grado n resulta sumamente impr actico
si la tabla es lo sucientemente densa, resultando preferible interpolar un subconjunto de los nodos,
cercano al punto x cuyo valor p(x) f(x) queremos calcular, a un polinomio de grado n. El
polinomio optimo puede obtenerse partiendo del grado m as bajo e increment andolo progresivamente
hasta alcanzar una cierta tolerancia, lo cual requiere que seamos tambien capaces de estimar, la
ganancia en precisi on que conlleva la adici on de sucesivos terminos al polinomio original.
Es decir, si se desea calcular el valor aproximado de la funci on en un punto x
p
comprendido entre
los nodos x
i
y x
i+1
, tomamos como primera aproximaci on la recta que pasa por los nodos x
i
y x
i+1
,
a continuaci on la par abola que pasa por (digamos) x
i
, x
i+1
y x
i+2
y as sucesivamente, hasta que la
aproximaci on no mejore al incrementar el grado del polinomio. Como veremos en este captulo, el
metodo de Newton de las diferencias divididas nos permite calcular cada uno de estos polinomios a
partir de los anteriores. Estudiaremos tambien una potente variante de dicho metodo, el algoritmo de
Neville, que permite incluir en la interpolaci on, de manera recursiva, los nodos a izquierda y derecha
del punto que se quiere aproximar.
9.2. Sistema lineal asociado a un polinomio. Matriz de Van-
dermonde
Consideremos en primer lugar una tabla de dos puntos a la que deseamos interpolar el unico
polinomio de grado uno que verica que p(x
i
) = y
i
, i = 1, 2. Se trata, naturalmente, del problema
trivial de determinar la recta que pasa por dos puntos, la cual podemos escribir como:
p
1
(x) = a
0
+ a
1
x (9.2)
Imponiendo la condici on 9.1, obtenemos el sistema lineal de ecuaciones:
y
0
= p(x
0
) = a
0
+ a
1
x
0
y
1
= p(x
1
) = a
0
+ a
1
x
1
que podemos expresar de forma matricial como:
_
1 x
0
1 x
1
__
a
0
a
1
_
=
_
y
0
y
1
_
para resolver el sistema lo reducimos en primer lugar a una matriz triangular superior:
_
1 x
0
0 x
1
x
0
__
a
0
a
1
_
=
_
y
0
y
1
y
0
_
cuya soluci on es inmediata:
a
1
=
y
1
y
0
x
1
x
0
a
0
= y
0

y
1
y
0
x
1
x
0
x
0
=
y
0
x
1
y
1
x
0
x
1
x
0
Consideremos ahora el caso de una par abola que pasa por tres puntos:
p
2
(x) = a
0
+ a
1
x + a
2
x
2
176
an alogamente al caso anterior, la matriz del sistema sera:
_
_
1 x
0
x
2
0
1 x
1
x
2
1
1 x
2
x
2
2
_
_
para obtener los coecientes a
0
, a
1
y a
2
resolveramos el sistema lineal de ecuaciones asociado. En el
caso m as general de un conjunto de n + 1 puntos por los que pasa un unico polinomio de grado n la
matriz del sistema se generaliza de manera inmediata a:
_
_
_
_
_
_
_
_
1 x
0
x
2
0
x
3
0
... x
n
0
1 x
1
x
2
1
x
3
1
... x
n
1
1 x
2
x
2
2
x
3
2
... x
n
2
... ... ... ... ... ...
... ... ... ... ... ...
1 x
n
x
2
n
x
3
n
... x
n
n
_
_
_
_
_
_
_
_
(9.3)
En principio, el problema de encontrar el polinomio interpolador se reduce al de resolver el sistema
lineal asociado a la matriz de la ecuaci on 9.3, llamada matriz de Vandermonde. Puesto que el sistema
tiene n+1 ecuaciones y n+1 inc ognitas, x
0
, x
1
, x
2
, ..., x
n
, la condici on necesaria y suciente para que
sea compatible determinado (es decir para que exista soluci on y sea unica) es que el determinante de
la matriz sea distinto de cero.
El determinante de la matriz de Vandermonde puede calcularse, resultando en:
det
_
_
_
_
_
_
_
_
1 x
0
x
2
0
x
3
0
... x
n
0
1 x
1
x
2
1
x
3
1
... x
n
1
1 x
2
x
2
2
x
3
2
... x
n
2
... ... ... ... ... ...
... ... ... ... ... ...
1 x
n
x
2
n
x
3
n
... x
n
n
_
_
_
_
_
_
_
_
=

0j<kn
(x
k
x
j
) ,= 0
y por lo tanto el sistema es compatible determinado, o en otras palabras puede garantizarse la exis-
tencia de un unico polinomio de grado n que pasa por los n + 1 nodos considerados.
Sin embargo, la matriz de Vandermonde est a a menudo mal condicionada (aunque la matriz no
es singular, los coecientes del polinomio interpolador son a menudo tales que la matriz se aproxima
a singular dentro de los errores de redondeo). En la pr actica, esta tecnica se utiliza rara vez para
calcular los coecientes del polinomio interpolador ya que existen algoritmos m as robustos y ecientes
numericamente.
9.3. Interpolaci on de Lagrange
Si bien es cierto que, dado un conjunto de n +1 puntos, existe un unico polinomio de grado n que
los interpola, dicho polinomio puede expresarse de varias maneras distintas (todas ellas, por supuesto,
equivalentes). La forma de Lagrange se obtiene a partir del siguiente razonamiento. Supongamos que
queremos aproximar cierta funci on f(x) que pasa por dos puntos x
0
y x
1
. Cerca de x
0
podemos
expandir la funci on en serie de Taylor:
f(x
0
) = f(x) + (x
0
x)f

(x) + ...
An alogamente, cerca de x
1
:
f(x
1
) = f(x) + (x
1
x)f

(x) + ...
Podemos introducir ahora una funci on p(x) que aproxima la funci on f(x), tal que:
f(x
0
) = p(x) + (x
0
x)p

(x)
f(x
1
) = p(x) + (x
1
x)p

(x)
177
donde la funci on p(x) (cuya derivada denotamos por p

(x)) es igual a f(x) en los puntos x


0
y x
1
y
quiz as una aproximaci on razonable en la vecindad de dichos puntos. Resolviendo el sistema para p(x)
obtenemos:
p(x) = y
0
(x x
1
)
x
0
x
1
+ y
1
(x x
0
)
x
1
x
0
donde y
0
= f(x
0
) y y
1
= f(x
1
). Se trata, de nuevo, de la recta que pasa por dos puntos, expresada en
una forma particularmente interesante, puesto que:
p(x) = y
0
l
0
(x) + y
1
l
1
(x)
donde las funciones l
i
, i = 0, 1 dependen de las abcisas pero no de las ordenadas:
l
0
(x) =
(x x
1
)
x
0
x
1
l
1
(x) =
(x x
0
)
x
1
x
0
veric andose adem as que:
l
0
(x
0
) = 1 l
0
(x
1
) = 0
l
1
(x
0
) = 0 l
1
(x
1
) = 1
Es decir l
ij
=
ij
, donde
ij
es la delta de Kronecker.
Podemos repetir el mismo ejercicio para una funci on a la que queremos aproximar en tres puntos.
Para ello expandiremos hasta segundo orden, imponiendo que el polinomio interpolador coincida con
la funci on en cada uno de los puntos y adem as su primera y segunda derivada coincidan con las de la
funci on de tal manera que podamos truncar la serie en orden dos. Un procedimiento an alogo al que
hemos seguido nos lleva a una f ormula del tipo:
p(x) = y
0
l
0
(x) + y
1
l
1
(x) + y
2
l
2
(x)
donde, las funciones l
i
, i = 0, 1, 2 obtenidas al resolver el sistema son:
l
0
(x) =
(x x
1
)
(x
0
x
1
)
(x x
2
)
(x
0
x
2
)
l
1
(x) =
(x x
0
)
(x
1
x
0
)
(x x
2
)
(x
1
x
2
)
l
2
(x) =
(x x
0
)
(x
2
x
0
)
(x x
1
)
(x
2
x
1
)
Como en el caso anterior, l
ij
= l
i
(x
j
) =
ij
.
La forma general de la llamada f ormula de Lagrange para un polinomio de orden n que interpola
n + 1 datos es:
p(x) = y
0
l
0
(x) + y
1
l
1
(x) + y
2
l
2
(x) + ... + y
n
l
n
(x) =
n

i=0
y
i
l
i
(x) (9.4)
donde, las funciones l
i
(x) (llamadas funciones cardinales) se escriben como:
l
i
(x) =
n

j=0
j=i
x x
j
x
i
x
j
(9.5)
178
El inconveniente de la f ormula de Lagrange es que las funciones cardinales deben calcularse cada
vez cuando interpolamos un polinomio de orden diferente. A menudo, en problemas pr acticos de
an alisis numerico, de desea interpolar los datos a polinomios de ordenes crecientes, deteniendo el
proceso cuando se alcanza una determinada precisi on establecida de antemano (es decir, cuando,
dado un punto x, [p
n
(x) p
n+1
(x)[ < , donde p
n
(x) es la aproximaci on a la funci on que queremos
interpolar por un polinomio de grado n,p
n+1
(x) por un polinomio de grado n + 1 y es la precisi on
pre establecida) . Para que el proceso sea efectivo, conviene poder calcular los polinomios de ordenes
sucesivos de manera iterativa (es decir deseamos poder calcular la forma de p
n+1
(x) a partir de la
forma de p
n
(x)), lo cual no es posible utilizando la f ormular de Lagrange.
9.4. Interpolaci on de Newton
Veamos como construir un algoritmo para calcular recursivamente un polinomio de grado n que
interpola los puntos x
0
, x
1
, ..., x
n1
, a partir del conocimiento del polinomio de grado n 1 que
interpola los puntos x
0
, x
1
, ..., x
n2
.
Comenzamos con el orden m as bajo, n = 1, que se corresponde a interpolar un s olo punto experi-
mental, x
0
a un polinomio de grado cero, es decir una constante:
p
0
(x
0
) = c
0
= y
0
El caso n = 2, se corresponde a interpolar dos puntos experimentales, x
0
, x
1
a un polinomio de
grado uno (una recta), partiendo del polinomio de grado cero que interpola x
0
. Para ello escribimos:
p
1
(x) = c
0
+ c
1
(x x
0
)
de tal manera que, para x = x
0
, p
1
(x
0
) = c
0
, como deseamos. La constante c
1
se obtiene utilizando el
hecho de que p
1
(x
1
) = y
1
y p
0
(x
1
) = p
0
(x) = c
0
:
c
1
=
y
1
c
0
x
1
x
0
=
y
1
y
0
x
1
x
0
El caso n = 3 se corresponde a interpolar los puntos x
0
, x
1
, x
2
a un polinomio de grado dos,
partiendo del polinomio de grado uno que interpola x
0
, x
1
:
p
2
(x) = p
1
(x) + c
2
(x x
0
)(x x
1
)
= p
0
(x) + c
1
(x x
0
) + c
2
(x x
0
)(x x
1
)
= c
0
+ c
1
(x x
0
) + c
2
(x x
0
)(x x
1
)
de tal manera que p
2
(x
i
) y p
1
(x
i
), i = 0, 1. La constante c
2
se obtiene imponiendo que p
2
(x
2
) = y
2
:
c
2
=
y
2
p
1
(x
2
)
(x
2
x
0
)(x
2
x
1
)
=
y
2
c
0
c
1
(x
2
x
0
)
(x
2
x
0
)(x
2
x
1
)
La expresi on general para un polinomio de orden n que interpola n + 1 puntos es:
p
n
(x) = c
0
+
n

i=0
c
i
i1

j=0
(x x
j
) (9.6)
y las constantes c
k
se calculan, recursivamente, como:
c
k
=
y
k
p
k1
(x
k
)
(x
k
x
0
)(x
k
x
1
)...(x
k
x
k1
)
(9.7)
para k = 1, 2..., siendo c
0
= f(x
0
).
179
9.4.1. Diferencias divididas
La ecuaci on 9.6 puede reescribirse como:
p
n
(x) =
n

i=0
c
i
q
i
(x) (9.8)
donde:
q
0
(x) = 1
q
1
(x) = (x x
0
)
q
2
(x) = (x x
0
)(x x
1
)
... ...
q
n
(x) = (x x
0
)(x x
1
)(x x
2
)...(x x
n1
)
Las condiciones de interpolaci on resultan en un sistema lineal de ecuaciones para determinar los
coecientes c
j
.
n

j=0
c
j
q
j
(x
i
) = f(x
i
) = y
i
(9.9)
En el cual, la matriz de coecientes A es una matriz (n + 1) (n + 1) cuyos elementos son:
a
i+1,j+1
= q
j
(x
i
) i, j = 0, 1...n (9.10)
Pero, a diferencia de la matriz de Vandermonde, A es triangular inferior, ya que:
q
j
(x) =
j1

k=0
(x x
k
)
q
j
(x
i
) =
j1

k=0
(x
i
x
k
) = 0 si i j 1 (9.11)
Por ejemplo, para el caso de tres puntos, podemos escribir:
p
2
(x) = c
0
q
0
(x) + c
1
q
1
(x) + c
2
q
2
(x)
= c
0
+ c
1
(x x
0
) + c
2
(x x
0
)(x x
1
)
El sistema lineal de ecuaciones asociado es:
_
_
1 0 0
1 (x
1
x
0
) 0
1 (x
2
x
0
) (x
2
x
0
)(x
2
x
1
)
_
_
_
_
c
0
c
1
c
2
_
_
=
_
_
f(x
0
)
f(x
1
)
f(x
2
)
_
_
Como ya sabemos, la soluci on del sistema cuando la matriz es triangular inferior es muy sencilla:
c
0
= f(x
0
) = f[x
0
]
c
1
=
f(x
1
) f(x
0
)
x
1
x
0
= f[x
0
, x
1
]
c
2
=
f(x
2
) (x
2
x
0
)c
1
c
0
(x
2
x
0
)(x
2
x
1
)
= f[x
0
, x
1
, x
2
]
180
Resolvemos el sistema empezando por c
0
y calculando cada coeciente a partir del anterior. La
notaci on que hemos introducido:
c
n
= f[x
0
, x
1
, ..., x
n
] (9.12)
indica, precisamente esa dependencia de los coecientes sucesivos con los anteriormente calculados.
Las expresiones f[x
0
, x
1
, ..., x
n
] se llaman diferencias divididas (notad como la forma, de por ejem-
plo, el termino f[x
0
, x
1
] justica el nombre).
El uso de las diferencias divididas es ventajoso para un algoritmo numerico debido a la relaci on
recursiva que verican:
f[x
0
, x
1
, ..., x
n
] =
f[x
1
, ..., x
n
] f[x
0
, x
1
, ..., x
n1
]
x
n
x
0
(9.13)
Para demostrar la f ormula 9.13, demostremos en primer lugar, que, dado un polinomio de grado
n, p
n
, que interpola n + 1 puntos, x
0
, x
1
, x
2
, ..., x
n1
, x
n
, se verica que:
p
n
(x) = q
+
n1
(x) +
x x
n
x
n
x
0
(q
+
n1
(x) q

n1
(x)) (9.14)
donde q
+
n1
es el polinomio de grado n 1 que interpola los puntos x
1
, x
2
, ..., x
n1
, x
n
y q

n1
es el
polinomio de grado n 1 que interpola los puntos x
0
, x
2
, ..., x
n1
. Para demostrar la f ormula 9.14,
basta con demostrar que el polinomio a la izquierda de la igualdad, p
n
(x), interpola los mismos puntos
que la combinaci on de polinomios a la derecha de la igualdad. Veamos que, en efecto, esto se verica
para x = x
0
:
p
n
(x
0
) = f(x
0
),
q
+
n1
(x
0
) +
x
0
x
n
x
n
x
0
(q
+
n1
(x
0
) q

n1
(x
0
)) = q
+
n1
(x
0
) q
+
n1
(x
0
) + q

n1
(x
0
)) =
q

n1
(x
0
) = f(x
0
)
Para x = x
n
:
p
n
(x
0
) = f(x
n
),
q
+
n1
(x
n
) +
x
n
x
n
x
n
x
0
(q
+
n1
(x
n
) q

n1
(x
n
)) = q
+
n1
(x
n
) = f(x
n
)
Finalmente, para x = x
k
, k = 1, 2, ..., n 1:
p
n
(x
k
) = f(x
k
),
q
+
n1
(x
k
) +
x
k
x
0
x
n
x
0
(q
+
n1
(x
k
) q

n1
(x
k
)) =
q
+
n1
(x
k
) = f(x
k
)
ya que q
+
n1
(x
k
) = q

n1
(x
k
), k = 1, 2, ...n 1. La f ormula 9.14 establece una relaci on recursiva que
nos permite obtener p
n
(x) a partir de los polinomios de grado inmediatamente inferior que interpolan
los mismos puntos, q
+
n1
y q

n1
.
Una vez demostrada la ecuaci on 9.14, podemos demostrar a su vez la ecuaci on 9.13 sin m as que
escribir los polinomios a izquierda y derecha de la igualdad en funci on de las diferencias divididas. El
caso n = 1 es trivial. Para el caso n = 2:
p
2
(x) = c
0
+ c
1
(x x
0
) + c
2
(x x
0
)(x x
1
)
= f[x
0
] + f[x
0
, x
1
](x x
0
) + f[x
0
, x
1
, x
2
](x x
0
)(x x
1
)
Mientras que:
q
+
1
(x) +
x x
2
x
2
x
0
(q
+
1
(x) q

1
(x)) = f[x
1
] + f[x
1
, x
2
](x x
1
) + (9.15)
x x
2
x
2
x
0
(f[x
1
] + f[x
1
, x
2
](x x
1
) f[x
0
] f[x
0
, x
1
](x x
0
))
181
Contando potencias a ambos lados de la ecuaci on 9.15 obtenemos, aplicando que los coecientes que
multiplican la potencia en x
2
a ambos lados de la ecuaci on deben ser iguales:
f[x
0
, x
1
, x
2
] =
f[x
1
, x
2
] f[x
0
, x
1
]
x
2
x
0
(9.16)
Podemos repetir el razonamiento anterior para polinomios de cualquier orden, llegando nalmente a
la ecuaci on 9.13. La misma relaci on se verica para cualquier polinomio intermedio. Por ejemplo, en
el caso de la c ubica que pasa por x
0
, x
1
, x
2
, x
3
, las dos par abolas en funci on de las cuales podemos
expresarla pasan por x
1
, x
2
, x
3
y x
0
, x
1
, x
2
. Dichas par abolas pueden expresarse a su vez en funci on
de dos rectas (las que pasan por x
0
, x
1
y por x
1
, x
2
para la par abola q

2
(x) y las que pasan por x
1
, x
2
y por x
2
, x
3
para la par abola q
+
2
(x)). Llegamos pues a la ecuaci on:
f[x
i
, x
i+1
, x
i+2
, ..., x
i+j
] =
f[x
i+1
, x
i+2
, ..., x
i+j
] f[x
i
, x
i+1
, ..., x
i+j1
]
x
i+j
x
i
(9.17)
que nos proporciona la relaci on recursiva que aplicaremos para la implementaci on numerica del algo-
ritmo de diferencias divididas. La idea es ir obteniendo las sucesivas diferencias divididas (y por lo
tanto los sucesivos polinomios de orden creciente) a partir de las diferencias divididas inmediatamente
anteriores, tal como se ilustra en la gura:
x
0
f(x
0
) [
f[x
0
, x
1
]
x
1
f(x
1
) [ f[x
0
, x
1
, x
2
]
f[x
1
, x
2
] f[x
0
, x
1
, x
2
, x
3
]
x
2
f(x
2
) [ f[x
1
, x
2
, x
3
]
f[x
2
, x
3
]
x
3
f(x
3
) [
9.4.2. Errores en la interpolaci on polin omica
Puesto que, cuando aproximamos una funci on f en un intervalo [a, b] utilizando un polinomio inter-
polador p, se verica que p(x
i
) = f(x
i
) i = 0, 1, 2, ...n, resulta natural especular que la aproximaci on
a la funci on f en los puntos intermedios a los nodos ser a as mismo razonable y mejorar a, adem as, a
medida que el n umero de nodos aumenta.
Esta hip otesis intuitiva, no es correcta. Un ejemplo cl asico fue propuesto por Runge en 1901. Se
trata de la funci on:
f(x) =
1
1 + x
2
(9.18)
en el intervalo [5, 5]. Si construimos polinomios interpoladores p
n
para esta funci on utilizando nodos
igualmente espaciados en el intervalo [5, 5], encontramos que:
lm
n
m ax
5x5
[f(x) p
n
(x)[ =
Y por lo tanto, el efecto de exigir que la funci on f y el polinomio interpolador p
n
coincidan en
m as y m as puntos es el de aumentar el error fuera de los nodos. El corolario de este sorprendente
resultado, sobre el que volveremos m as adelante es que interpolar un polinomio de grado alto a un
conjunto elevado de nodos puede resultar en una representaci on insatisfactoria de la funci on que se
quiere aproximar, a no ser que el conjunto de nodos se escoja cuidadosamente.
De hecho y de nuevo en contra de la intuici on, la elecci on de nodos igualmente espaciados suele
ser una elecci on pobre para interpolar. Una elecci on mucho mejor viene dada por los llamados nodos
de Chebyshev, los cuales se denen en el intervalo [1, 1] como:
x
i
= cos[(
i
n
)] (0 i n)
182
El correspondiente conjunto de (n+1) nodos en un intervalo arbitrario [a, b] puede derivarse mediante
una simple transformaci on lineal:
x
i
=
1
2
(a + b) +
1
2
(b a)cos[(
i
n
)] (0 i n)
Consideremos de nuevo la funci on de Runge, ecuaci on 9.18, en el intervalo [5, 5]. La gura
9.1(izquierda) muestra el valor autentico de la funci on, la lnea azul corresponde al polinomio de
grado diez que la interpola en once nodos (diamantes en verde) igualmente espaciados. Observese que
la aproximaci on es muy deciente en los extremos del intervalo, en particular el polinomio se hace
negativo alrededor de los nodos 4. Por otra parte, la gura 9.1(derecha) muestra la interpolaci on
cuando se utilizan nodos de Chebyshev. Observese que la aproximaci on a la funci on es mucho mejor
en este caso.
-0.5
0
0.5
1
1.5
2
-4 -2 0 2 4
f(x)
"prunge.dat"
"runge.dat"
0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1
-4 -2 0 2 4
f(x)
"runge.che"
"prunge.dat"
Figura 9.1: Izquierda:La funci on (1 + x
2
)
1
en el intervalo [5, 5]. La linea roja corresponde al valor
autentico de la funci on, la lnea azul al del polinomio de grado diez que la interpola en once nodos
(diamantes en verde) igualmente espaciados. Observese que la aproximaci on es muy deciente en los
extremos del intervalo, donde el polinomio interpolador toma valores negativos seguidos de una fuerte
oscilaci on hacia valores positivos. Derecha: La interpolaci on cuando se usan nodos de Chebishev,
arrojando resultados mucho mejores.
Estudiamos a continuaci on un teorema que cuantica la discrepancia entre una cierta funci on de
la que conocemos una tabla de n + 1 nodos y un polinomio de grado n que los interpola.
Sea f una funci on de clase C
n+1
[a, b] (es decir, f pertenece al conjunto de funciones n + 1 veces
diferenciable y continuas en [a, b]) y sea p un polinomio de grado n que interpola f en los nodos
x
0
, x
1
, ..., x
n
, los cuales pertenecen al intervalo [a, b]. Entonces, para cualquier x [a, b] existe un
punto
x
en (a, b) tal que:
f(x) p(x) =
1
(n + 1)!
f
n+1
(
x
)
n

i=0
(x x
i
) (9.19)
Veamos la demostraci on. Si x es uno de los nodos de interpolaci on la ecuaci on 9.19 es obviamente
cierta puesto que se reduce a cero en ambos lados de la igualdad. Consideremos por tanto un punto
x que no coincida con un nodo. Denimos la funci on:
(t) = f(t) p(t) w(t)
183
donde:
w(t) =
n

i=0
(t x
i
)
y es el n umero real tal que se anula para t = x. Es decir:
(x) = f(x) p(x) w(x) = 0
y por lo tanto:
=
f(x) p(x)
w(x)
La funci on (t) es de clase C
n+1
[a, b], puesto que f es de clase C
n+1
[a, b] y f, w, p son continuas
en [a, b]. Adem as se anula en n+2 puntos, a saber, los n+1 nodos (ya que w(x
i
) = 0 y p(x
i
) = f(x
i
)
para i = 0, 1, 2, ..., n) y el punto x (por construcci on). Ahora bien, el teorema de Rolle asegura que
si f es una funci on continua en [a, b] tal que f(a) = f(b) = 0 y su derivada f

existe en el intervalo
(a, b), entonces dicha derivada tiene una raz en el intervalo, es decir, existe un cierto (a, b) tal que
f

() = 0. Como:
(x
0
) = (x
1
) = ... = (x
n
) = (x) = 0
entonces por el teorema de Rolle

(x) tendr a n+1 races en los intervalos respectivos (x


0
, x
1
), (x
1
, x
2
)
etc. Aplicando el teorema de Rolle de nuevo a

concluimos que

(x) tiene al menos n races distintas


en (a, b). Repitiendo el argumento, concluimos nalmente que
n+1
tiene al menos un cero, al que
llamaremos
x
en el intervalo (a, b).
Ahora bien, el polinomio p(x) es de grado n y por lo tanto su derivada de orden n + 1 se cancela,
p
n+1
= 0. Por su parte, la funci on w(x) contiene la potencia x
n+1
, puesto que:
w(x) =
n

i=0
(x x
i
)
y en consecuencia w
n+1
= (n + 1)n(n 1).... = (n + 1)!. Entonces:

n+1
= f
n+1
p
n+1
w
n+1
= f
n+1
(n + 1)!
De donde:

n+1
(
x
) = 0 = f
n+1
(
x
) (n + 1)! = f
n+1
(
x
) (n + 1)!
f(x) p(x)
w(x)
Despejando f(x) p(x) se obtiene la ecuaci on 9.19, como queramos demostrar.
Consideremos, por ejemplo, la funci on sin(x). Si la aproximamos por un polinomio de grado n que
interpola n + 1 nodos en el intervalo [0, 1], el error en dicho intervalo es:
[ sin(x) p(x)[
1
(n + 1)!
ya que [ sin
n
(
x
)[ 1 y

n
i=0
[xx
i
[ 1 para cualquier punto del intervalo. As pues, si interpolamos
una recta a dos nodos cometemos un error de 1/2, si interpolamos una par abola a tres puntos el error
se reduce a 1/6 y para una c ubica obtenemos un error de 1/24. Con tan s olo diez puntos el error de
interpolaci on es diminuto, del orden de 10
7
.
Esto es, en el caso de la funci on f(x) = sin(x), podemos construir polinomios interpoladores que
converjan uniformemente a f, es decir, que veriquen que la cantidad:
[[f p
n
[[

= max
axb
[f(x) p
n
(x)[ (9.20)
converja a cero cuando n .
184
Sin embargo, como hemos visto, existen funciones como la funci on de Runge que hemos estudiando
anteriormente en la que los polinomios interpoladores no convergen uniformemente a f, es decir, la
cantidad [[f p
n
[[

no converge a cero.
En general, dado un conjunto de nodos en un determinado intervalo:
a x
0
< x
1
< ....x
n
b n 0 (9.21)
puede demostrarse (consultar por ejemplo [3]) que existe siempre una cierta funci on f, continua en
[a, b] tal que los polinomios interpoladores para f utilizando el conjunto de nodos denidos en 9.21 no
convergen uniformemente a f. Este es el caso de la funci on de Runge con espaciado uniforme.
Sin embargo, tambien puede demostrarse ([3]) que para toda funci on continua f en el intervalo [a, b]
existe siempre un sistema de nodos tal que los polinomios interpoladores que pasan por esos nodos
convergen uniformemente a f. Hemos visto un ejemplo para los nodos de Chebyshev y la funci on de
Runge.
La consecuencia pr actica de los teoremas anteriores es que, dada un cierta funci on continua en un
cierto intervalo y un conjunto de n +1 nodos por los que pasa un polinomio interpolador de grado n,
no es posible garantizar que el error de interpolaci on disminuya a medida que n crece, a no ser que
podamos demostrar explcitamente que [f
n+1
(
x
)[ y

n
i=0
[x x
i
[ est an acotados superiormente para
cualquier [a, b].
9.5. Interpolaci on
Consideremos ahora el problema pr actico de obtener el valor aproximado de una funci on en un
punto x
p
tal que x
0
< x
p
< x
n
, donde x
0
y x
n
son el primer y ultimo punto del conjunto de n+1 nodos
donde conocemos los valores de la funci on. Como ya hemos comentado, un polinomio de grado n que
pasa por cada uno de los n + 1 nodos puede no ser una buena aproximaci on a la funci on, resultando
preferible interpolar un polinomio de grado n que pase por los nodos cercanos al punto x
p
del que
se desea aproximar el valor f(x
p
). Para ilustrar este punto, supongamos que se conocen las im agenes
de la funci on sin
3
(x) en un conjunto de 21 nodos, en el intervalo [, ], tal como se muestra en
la gura 9.2. Supongamos a continuaci on que deseamos conocer el valor aproximado de un punto en
la vecindad de /2, por ejemplo, x
p
= 1,73. Basta examinar la gura 9.2 para convencernos de
que una simple par abola proporcionara una excelente aproximaci on a la funci on en la vecindad de
/2. Por otra parte, los teoremas que hemos comentado sobre el error de interpolaci on advierten
que para el conjunto de nodos escogido, la convergencia uniforme de los polinomios interpolantes
no est a garantizada. Es decir, no sabemos a priori si los sucesivos polinomios de orden creciente
proporcionar an una aproximaci on mejor que la simple par abola. En la pr actica, la cuesti on que nos
planteamos es la de encontrar el polinomio interpolador de grado mnimo que permite aproximar un
punto dado x
p
con una cierta precisi on establecida de antemano. Tal estrategia, no obstante, requiere
un metodo recursivo para calcular los sucesivos polinomios. Un metodo como el de Lagrange resulta
inapropiado aqu, puesto que exige recalcular las funciones cardinales cada vez que modicamos el
orden del polinomio. En cambio, el metodo de Newton, en particular utilizando las diferencias divididas
que acabamos de estudiar, nos permite calcular recursivamente los polinomios de grado creciente.
La idea es comenzar por el polinomio de orden m as bajo (p
1
, una recta) que interpola los dos
nodos x
i
, x
i+1
que acotan a x
p
. A continuaci on calculamos el polinomio de orden siguiente (p
2
, una
par abola) que pasa por los nodos x
i
, x
i+1
, x
i+2
, calculando de paso una aproximaci on a la correcci on
que hemos realizado como la diferencia = [p
1
(x
p
) p
2
(x
p
)[ (puesto que no podemos calcular la
diferencia real,
0
= [p
2
(x
p
) f(x
p
)[ entre el polinomio y el valor autentico de la funci on). En el
siguiente paso, interpolamos una c ubica, p
3
a los nodos x
i
, x
i+1
, x
i+2
, x
i+3
y calculamos la correcci on
= [p
3
(x
p
) p
2
(x
p
)[. Detenemos el proceso cuando se hace m as peque no que la tolerancia que
hemos especicado de antemano.
La gura 9.3 ilustra la tecnica. La primera aproximaci on (una recta) es obviamente muy burda,
pero como puede verse, el siguiente polinomio (una par abola) proporciona una excelente aproximaci on
185
-1
-0.5
0
0.5
1
-3 -2 -1 0 1 2 3
f(x)
"sin3.dat"
Figura 9.2: La funci on sin
3
(x) en el intervalo [, ]. La linea continua corresponde al valor autentico
de la funci on, los diamantes al conjunto de nodos a interpolar.
a la funci on en la vecindad de /2. La c ubica que supone el orden siguiente a nade una correcci on
muy peque na (de hecho, se aproxima menos al valor autentico de la funci on que la par abola). La tabla
9.5 muestra los resultados obtenidos cuando se interpolan polinomios de orden creciente. Resaltemos
que las correcciones estimadas no decrecen mon otonamente, como tampoco lo hacen las diferencias
entre la aproximaci on polin omica y el valor autentico de la funci on, a pesar de lo cual tanto como

0
se van haciendo m as peque nas a medida que n se hace mayor. Vale la pena tambien notar que para
mejorar la aproximaci on proporcionada por la par abola (precisa a nivel del uno por mil) necesitamos
interpolar un polinomio de orden cinco.
Utilizando el metodo de las diferencias divididas, los sucesivos polinomios se calculan utilizando
los resultados anteriores. En efecto, si x
i
< x
p
< x
j
:
p
1
(x) = f[x
i
] + f[x
i
, x
j
](x x
i
)
p
2
(x) = f[x
i
] + f[x
i
, x
j
](x x
i
) + f[x
i
, x
j
, x
j+1
](x x
i
)(x x
j
)
p
3
(x) = f[x
i
] + f[x
i
, x
j
](x x
i
) + f[x
i
, x
j
, x
j+1
](x x
i
)(x x
j
)
+ f[x
i
, x
j
, x
j+1
, x
j+2
](x x
i
)(x x
j
)(x x
j+1
)
... ....
Donde cada una de las diferencias divididas que aparecen en la f ormula anterior, puede calcularse a
partir de la ecuaci on 9.17.
9.6. Algoritmo de Neville
En la discusi on precedente hemos utilizado el algoritmo de Newton de las diferencias divididas:
P(x) =
n

k=0
f[x
0
, x
1
, ..., x
k
]
k1

j=0
(x x
j
) (9.22)
186
-1.1
-1
-0.9
-0.8
-0.7
-0.6
-0.5
-0.4
-0.3
-2.2 -2 -1.8 -1.6 -1.4 -1.2 -1 -0.8
f(x)
'sin3.dat'
"p1sin3.dat"
"p2sin3.dat"
"p3sin3.dat"
Figura 9.3: La funci on sin
3
(x) en la vecindad de /2. La lnea continua en rojo representa el valor
autentico de la funci on, los diamantes (verde) representan los datos a interpolar, la lnea recta (azul)
es el polinomio de grado uno que pasa por los dos nodos que acotan el punto que se desea aproximar,
x
p
= 1,73. La lnea rosa es la par abola que pasa por tres nodos, mientras que las aspas (en morado)
representan la c ubica que pasa por cuatro nodos.
para construir recursivamente polinomios de orden creciente, a nadiendo puntos sucesivos a la derecha
del punto cuyo valor queremos aproximar.
Por otra parte, puesto que los sucesivos nodos entran en el polinomio con peso (x
i
x
p
), es de
esperar que los nodos inmediatamente a la izquierda del punto x
p
tengan m as peso en la interpolaci on
que nodos lejanos por la derecha. En consecuencia, un algoritmo de interpolaci on recursivo debera
ser capaz de incluir tanto los nodos a la izquierda como a la derecha del punto que se desea aproximar
(lo cual no puede hacerse aplicando sin m as la ecuaci on 9.22), proporcionando adem as una estrategia
para ir a nadiendo los sucesivos nodos (la m as obvia sera ir a nadiendo sucesivos nodos a izquierda y
derecha del punto que se quiere aproximar, pero existen otras estrategias posibles, como veremos).
El algoritmo de Neville no es sino una variante del algoritmo de Newton de las diferencias divididas
que nos permite a nadir nodos recursivamente tanto a izquierda como a derecha del punto que se desea
aproximar.
Para construirlo comenzamos reescribiendo la ecuaci on 9.14 como:
P
i,i+1,i+2,...,i+j
=
(x x
i+j
)P
i,i+1,i+2,...i+j1
+ (x
i
x)P
i+1,i+2,...i+j
x
i
x
i+j
(9.23)
Para comprobar que, en efecto, la ecuaci on 9.14 y la ecuaci on 9.23 son equivalentes, basta con tomar
i = 0, j = n en 9.23, notando que, por construcci on q

= P
0,1,...,n1
y q
+
= P
1,...,n
. Obtenemos:
p
n
= P
0,1,...,n
=
(x
n
x)q

+ (x x
0
)q
+
x
n
x
0
que es, en efecto, la misma f ormula que 9.14.
Consideremos ahora un conjunto de nodos x
0
, x
1
, x
2
, ...x
n
a interpolar y llamemos P
ii
a los polino-
mios de orden cero que coinciden con el valor de las im agenes de la funci on (conocidas) en cada uno
187
x
p
f(x
p
) n p
n
(x) [p
n
(x) p
n1
(x) [p(x) f(x)[
-1.73 -0.9635160366 1 -0.9301195 - 0.03339653664
2 -0.96505975 0.03494025 0.001543713357
3 -0.9705944078 0.005534657839 0.007078371197
4 -0.9664261426 0.004168265214 0.002910105983
5 -0.9622316971 0.004194445528 0.001284339546
6 -0.9613559482 0.0008757489255 0.002160088471
7 -0.9628081442 0.001452195993 0.0007078924782
8 -0.9642461077 0.001437963549 0.0007300710706
9 -0.9644253494 0.0001792417314 0.000909312802
10 -0.9637115037 0.0007138457111 0.0001954670909
11 -0.9630950006 0.0006165031141 0.0004210360233
12 -0.9630950006 0 0.0004210360233
13 -0.9634872752 0.000392274585 0.0000287614
14 -0.9637724145 0.0002851392627 0.0002563778245
15 -0.9637175416 0.00005487287525 0.0002015049492
16 -0.9634813468 0.0002361947668 0.00003468981758
Cuadro 9.2: Aproximaciones al valor de la funci on sin
3
(x) en el punto x
p
= 1,73 obtenida por
sucesivos polinomios interpoladores.
de los nodos, esto es:
P
00
= f(x
0
) P
11
= f(x
1
) P
22
= f(x
2
) ... P
nn
= f(x
n
)
Llamamos P
ij
a los polinomios de orden uno que interpolan dos nodos sucesivos, de tal manera que
P
01
es la recta que pasa por x
0
, x
1
, P
12
la que pasa por x
1
, x
2
, P
23
interpola x
2
, x
3
y as sucesivamente.
An alogamente, P
012
es la par abola que pasa por x
0
, x
1
, x
2
, P
123
la que pasa por x
1
, x
2
, x
3
, P
234
inter-
pola x
2
, x
3
, x
4
, etc. La misma notaci on aplica a los sucesivos polinomios que podemos ir construyendo
hasta llegar al de grado m aximo P
0,1,2,...,n
. Los sucesivos polinomios forman un tablero en cuyo apex
est a el polinomio de mayor grado posible. Por ejemplo para seis nodos:
x
0
: f(x
0
) = P
00
P
01
x
1
: f(x
1
) = P
11
P
012
P
12
P
0123
x
2
: f(x
2
) = P
22
P
123
P
01234
P
23
P
1234
P
012345
x
3
: f(x
3
) = P
33
P
234
P
12345
P
34
P
2345
x
4
: f(x
4
) = P
44
P
345
P
45
x
5
: f(x
5
) = P
55
Consideremos la diagonal superior del tablero:
P
00
P
01
P
012
...P
012345
La secuencia de polinomios que abarca empieza en el de orden cero que corresponde a la imagen del
nodo x
0
, sigue con la recta que interpola x
0
, x
1
, la par abola que interpola x
0
, x
1
, x
2
y as sucesivamente
hasta llegar al polinomio de m aximo grado posible. An alogamente, la diagonal inferior del tablero
contiene la secuencia de polinomios que empieza con la imagen del ultimo nodo, x
5
, continua con la
188
recta que interpola x
5
, x
4
, la par abola que interpola x
5
, x
4
, x
3
etc., hasta llegar al polinomio de grado
m aximo posible. Claramente, la secuencia de polinomios se corresponde a una interpolaci on hacia
adelante, la unica posible cuando el punto a aproximar, x
p
est a en el intervalo [x
0
, x
1
], mientras que
la secuencia de la diagonal inferior:
P
55
P
45
P
345
...P
012345
se corresponde a una interpolaci on hacia atr as, la unica posible cuando x
p
[x
4
, x
5
]. Ambas son
equivalentes y pueden calcularse a partir de la ecuaci on 9.22 (ya que las diferencias divididas son
funciones pares, esto es f[x
0
, x
1
] = f[x
1
, x
0
] y as sucesivamente).
Consideremos ahora el caso en que x
1
< x
p
< x
2
. Una secuencia posible corresponde a bajar a lo
largo de la segunda diagonal del tablero ascendiendo al nal:
P
11
P
12
P
123
...P
12345
P
012345
lo que corresponde a interpolar una recta a x
1
, x
2
, una par abola a x
1
, x
2
, x
3
, etc., hasta llegar al
polinomio de orden cuatro que interpola x
1
, x
2
, x
3
, x
4
, x
5
, a nadiendo en ultimo lugar x
0
para calcular
el polinomio de mayor grado posible. Existe,por otra parte, una secuencia alternativa, que corresponde
a ascender en primer lugar a la diagonal superior, descendiendo despues a lo largo de esta:
P
11
P
12
P
012
...P
01234
P
012345
Lo que corresponde a interpolar una recta a x
1
, x
2
, una par abola a x
0
, x
1
, x
2
, etc., hasta alcanzar el
apex.
Si x
2
< x
p
< x
3
el n umero de secuencias aumenta. Entre estas, destacaremos:
1. Ascender hasta la diagonal superior y descender por esta:
P
22
P
23
P
123
...P
01234
P
012345
2. Descender hasta la diagonal inferior y ascender por esta:
P
22
P
23
P
234
P
2345
...P
012345
3. Avanzar en zigzag, dando a nadiendo un nodo por la izquierda y otro por la derecha hasta llegar
a la diagonal superior o inferior:
P
22
P
23
P
123
P
1234
...P
012345
La primera secuencia corresponde a a nadir nodos por la izquierda hasta llegar a x
0
y a continuaci on
a nadir nodos por la derecha. La segunda secuencia hace lo contrario, a nadiendo nodos por la derecha
hasta llegar a x
5
antes de a nadir nodos por la izquierda. La tercera va alternando mientras le quedan
nodos a la izquierda. Estas son las tres estrategias b asicas que podramos adoptar para interpolar
polinomios de grado creciente.
Debemos demostrar, no obstante, que es posible recorrer cada una de las secuencias que hemos
descrito recursivamente, esto es, obteniendo cada polinomio a partir de los anteriores, tanto si a nadimos
puntos por la izquierda como por la derecha.
Para ello, denimos las cantidades:
c
ij
= P
i,i+1,i+2,...i+j
P
i,i+1,i+2,...i+j1
d
ij
= P
i,i+1,i+2,...i+j
P
i+1,i+2,i+3,...i+j
(9.24)
Las c
ij
son las diferencias entre polinomios de orden sucesivo a lo largo de las diagonales del tablero.
As por ejemplo:
c
01
= P
01
P
00
c
02
= P
012
P
01
c
03
= P
0123
P
012
c
04
= P
01234
P
0123
c
05
= P
012345
P
01234
c
11
= P
12
P
11
c
12
= P
123
P
12
c
13
= P
1234
P
123
c
14
= P
12345
P
1234
c
21
= P
23
P
22
c
22
= P
234
P
23
c
23
= P
2345
P
234
c
31
= P
34
P
33
c
32
= P
345
P
34
c
41
= P
45
P
44
189
Mientras que las d
ij
son las diferencias entre polinomios sucesivos moviendonos transversalmente
a las diagonales:
d
01
= P
01
P
11
d
02
= P
012
P
12
d
03
= P
0123
P
123
d
04
= P
01234
P
1234
d
05
= P
012345
P
12345
d
11
= P
12
P
22
d
12
= P
123
P
23
d
13
= P
1234
P
234
d
14
= P
12345
P
2345
d
21
= P
23
P
33
d
22
= P
234
P
34
d
23
= P
2345
P
345
d
31
= P
34
P
44
d
32
= P
345
P
45
d
41
= P
45
P
55
Gr acamente:
f(x
0
) = P
00
c
01
P
01
d
01
c
02
f(x
1
) = P
11
P
012
c
11
d
02
c
03
P
12
P
0123
d
11
c
12
d
03
f(x
2
) = P
22
P
123
c
21
d
12
P
23
d
21
f(x
3
) = P
33
Cualquier polinomio del tablero puede expresarse en funci on de polinomios previamente calculados
a partir de las c
ij
, d
ij
. As, podemos recorrer la diagonal superior (inferior) del tablero sin m as que ir
a nadiendo coecientes c
ij
(d
ij
):
P
01
= P
00
+ c
01
= f(x
0
) + c
01
P
45
= P
55
+ d
41
= f(x
5
) + d
41
P
012
= P
01
+ c
02
= P
00
+ c
01
+ c
02
P
345
= P
45
+ d
32
= P
55
+ d
41
+ d
32
P
0123
= P
00
+ c
01
+ c
02
+ c
03
P
2345
= P
55
+ d
41
+ d
32
+ d
23
P
01234
= P
00
+ c
01
+ c
02
+ c
03
+ c
04
P
12345
= P
55
+ d
41
+ d
32
+ d
23
+ d
14
P
012345
= P
00
+ c
01
+ c
02
+ c
03
+ c
04
+ c
05
P
012345
= P
55
+ d
41
+ d
32
+ d
23
+ d
14
+ d
05
Si x
p
no est a en el primer o ultimo intervalo podemos recorrer el tablero, como hemos visto antes,
utilizando diferentes recorridos. Por ejemplo:
P
012345
= P
33
+ d
21
+ d
12
+ d
03
+ c
04
+ c
05
P
012345
= P
33
+ c
31
+ c
32
+ d
23
+ d
14
+ d
05
P
012345
= P
33
+ d
21
+ c
22
+ d
13
+ c
14
+ d
05
Nos ocuparemos m as adelante de establecer un criterio para recorrer el tablero. Antes, sin embar-
go, vamos a expresar las c
ij
, d
ij
en funci on de las diferencias divididas. Comenzamos por las c
ij
y
procedemos por inducci on. Tenemos que:
c
01
= P
01
P
00
= f[x
0
] + f[x
0
, x
1
](x x
0
) f[x
0
] = f[x
0
, x
1
](x x
0
)
c
02
= P
012
P
01
= f[x
0
] + f[x
0
, x
1
](x x
0
) + f[x
0
, x
1
, x
2
](x x
0
)(x x
1
)
f[x
0
] f[x
0
, x
1
](x x
0
) = f[x
0
, x
1
, x
2
](x x
0
)(x x
1
)
c
03
= P
0123
P
012
= f[x
0
, x
1
, x
2
, x
3
](x x
0
)(x x
1
)(x x
2
)
y an alogamente para los ndices restantes, lo que nos lleva a la f ormula:
c
ij
= f[x
i
, x
i+1
, x
i+2
, ...x
i+j
]
j1

k=i
(x x
k
) (9.25)
190
El c alculo de las d
ij
es similar:
d
01
= P
01
P
11
= f[x
0
] f[x
1
] + f[x
0
, x
1
](x x
0
) =
f[x
0
, x
1
](x
0
x
1
) + f[x
0
, x
1
](x x
0
) =
f[x
0
, x
1
](x
0
x
1
+ x x
0
) = f[x
0
, x
1
](x x
1
)
d
02
= P
012
P
12
= f[x
0
] f[x
1
] + f[x
0
, x
1
](x x
0
) f[x
1
, x
2
](x x
1
)+
f[x
0
, x
1
, x
2
](x x
0
)(x x
1
) =
f[x
0
, x
1
](x x
1
) f[x
1
, x
2
](x x
1
) + f[x
0
, x
1
, x
2
](x
0
x
1
)(x x
1
) =
(x x
1
)(f[x
0
, x
1
] f[x
1
, x
2
]) + f[x
0
, x
1
, x
2
](x x
0
)(x x
1
) =
(x x
1
)(x
0
x
2
)f[x
0
, x
1
, x
2
] + f[x
0
, x
1
, x
2
](x x
0
)(x x
1
) =
(x x
1
)f[x
0
, x
1
, x
2
](x
0
x
2
+ x x
0
) =
f[x
0
, x
1
, x
2
](x x
1
)(x x
2
)
resultando en la f ormula general:
d
ij
= f[x
i
, x
i+1
, x
i+2
, ...x
i+j
]
j

k=i+1
(x x
k
) (9.26)
La relaci on entre las c
ij
y las d
ij
es por lo tanto:
d
ij
= c
ij
(x x
i
)
(x x
j
)
(9.27)
En resumen. Podemos expresar las c
ij
y las d
ij
en funci on de las diferencias divididas, para las cuales
disponemos de una f ormula recursiva. Por lo tanto, el c alculo de las c
ij
y las d
ij
(y en consecuencia
de cualquier polinomio del tablero) puede realizarse recursivamente.
Finalmente debemos establecer un criterio para avanzar en el tablero. Entre las muchas posibi-
lidades, adoptaremos el siguiente: Avanzar tan recto como se pueda entre el punto inicial y el apex
del tablero. Obviamente, este es el caso cuando x
p
se encuentra en el intervalo x
0
, x
1
(avanzamos a
lo largo de la diagonal superior, sumando c
0i
, i = 1, 2, ...) o cuando x
p
se encuentra en el intervalo
x
n1
, x
n
(avanzamos a lo largo de la diagonal inferior, sumando d
ij
, i + j = n). Si x
p
se encuentra
en el intervalo [x
1
, x
2
] entonces nuestro criterio establece que los sucesivos polinomios se obtengan a
partir de la secuencia:
P
11
+ d
01
+ c
02
+ c
03
+ ... + c
0n
es decir, interpolamos hacia atr as el primer nodo y luego interpolamos el resto hacia adelante. An alo-
gamente si Si x
p
se encuentra en el intervalo [x
2
, x
3
] entonces:
P
22
+ d
11
+ d
02
+ c
03
+ ... + c
0n
interpolamos primero los dos nodos hacia detr as y luego el resto hacia adelante. Si x
p
se encuentra en
un intervalo m as all a de la mitad del conjunto de nodos, el criterio de avanzar tan recto como se pueda
nos lleva a interpolar primero hacia delante y luego hacia atr as. En el ejemplo que hemos comentado,
con seis nodos, si x
p
se encuentra en el intervalo [x
3
, x
4
]:
P
33
+ c
31
+ c
32
+ d
23
+ d
14
+ d
05
En resumen, hemos construido un algoritmo recursivo, basado en las diferencias divididas que
permite interpolar polinomios de orden creciente incluyendo tanto los nodos a la derecha como los
nodos a la izquierda del punto que se desea aproximar x
p
.
191
192
Captulo 10
Diferenciaci on e integraci on
numerica
10.1. Diferenciaci on numerica
Sea f(x) una funci on continua en un cierto intervalo (a, b) y consideremos un punto x (a, b).
Denimos la derivada f

(x) como la tangente a la curva en ese punto:


f

(x) = lm
h0
f(x + h) f(x)
h
(10.1)
Una primera aproximaci on numerica a f

(x), puede obtenerse sin m as que aproximar:


f

(x)
1
h
[f(x + h) f(x)] (10.2)
La ecuaci on 10.2 es exacta para una funci on lineal f(x) = ax + b. Para cualquier otro tipo de
funciones, podemos calcular el error que cometemos al utilizar esta aproximaci on, recurriendo al
desarrollo en serie de Taylor de la funci on f(x) entorno a x:
f(x + h) = f(x) + hf

(x) +
h
2
f

() (10.3)
donde (x, x + h). Despejando f

(x):
f

(x) =
1
h
[f(x + h) f(x)]
h
2
f

() (10.4)
donde el error en 10.4 es el producto de la cantidad h y la derivada segunda de la funci on aplicada
en cierto punto del intervalo. Si hacemos tender h a cero el error se anula, recuper andose la f ormula
exacta.
Calculemos como ejemplo la derivada de la funci on coseno en el punto /4 utilizando la f ormula
10.4 para estimar su valor aproximado as como su error. Tomando un valor h = 0,01:
f

(x)
1
h
[f(x + h) f(x)] =
1
0,01
[cos(/4 + 0,01) cos(/4)]
=
1
0,01
[0,700000476 0,707106781]
= 0,71063051
193
Podemos estimar el error escribiendo:
[
h
2
f

()[ = 0,005[ cos()[


y, puesto que /4 < < /4 + h, [ cos()[ < 0,707107. Por tanto:
0,00353
que coincide, hasta la cuarta cifra decimal con el error real cometido,
R
= sin(/4)(0,71063051) =
0,00352.
El error de truncaci on (llamamos as al termino [
h
2
f

()[, dado que lo obtenemos como resul-


tado de truncar la expansi on de Taylor de la funci on, en este caso a primer orden) en la f ormula
10.4 disminuye linealmente con h. Una aproximaci on mejor sera aquella en la que el termino de
error disminuyera con el cuadrado, cubo, o potencias superiores de h. Veamos como obtener dichas
aproximaciones mejoradas.
Para ello partimos de la expansi on de Taylor en torno a valores positivos y negativos de x:
f(x + h) = f(x) + hf

(x) +
h
2
2
f

(x) +
h
3
3!
f

(
1
) (10.5)
f(x h) = f(x) hf

(x) +
h
2
2
f

(x)
h
3
3!
f

(
2
) (10.6)
restando una ecuaci on de otra obtenemos:
f

(x) =
1
2h
[f(x + h) f(x h)]
h
2
6
[f

(
1
) + f

(
2
)] (10.7)
si f

es continua en el intervalo [x h, x + h] entonces existir a un [x h, x + h] tal que:


f

() =
1
2
[f

(
1
) + f

(
2
)]
con lo que:
f

(x) =
1
2h
[f(x + h) f(x h)]
2h
2
3
f

() (10.8)
Extendiendo las ecuaciones 10.5 en un termino adicional y sumando, se obtiene una aproximaci on
a la derivada segunda:
f

(x) =
1
h
2
[f(x + h) 2f(x) + f(x h)]
h
2
6
f
(4)
() (10.9)
10.1.1. Extrapolaci on de Richardson
Veamos a continuaci on una tecnica que permite aumentar la precisi on de las aproximaciones obte-
nidas utilizando cierto tipo de funciones numericas. Comenzamos por escribir la expansi on de Taylor
completa de una funci on entorno a un punto:
f(x + h) =

k=0
1
k!
h
k
f
(k)
(x) (10.10)
f(x h) =

k=0
1
k!
(h)
k
f
(k)
(x) (10.11)
restando la segunda ecuaci on de la primera cancelamos todos los terminos pares, obteniendo:
f(x + h) f(x h) = 2hf

(x) +
2
3!
h
3
f

(x) +
2
5!
h
5
f
(5)
(x) +
2
7!
h
7
f
(7)
(x) + ....
194
de donde:
f

(x) =
1
2h
[f(x + h) f(x h)] + [(
1
3!
h
2
)f

(x) + (
1
5!
h
4
)f
(5)
(x) + (
1
7!
h
6
)f
(7)
(x) + ....]
La ecuaci on anterior es de la forma:
D = d
0
(h) + a
2
h
2
+ a
4
h
4
+ a
6
h
6
+ .... (10.12)
donde D representa el valor exacto de la derivada, D = f

(x), d
0
(h) =
1
2h
[f(x + h) f(x h)] y
los coecientes a
i
, i = 2, 4, 6... son n umeros que dependen de la funci on que estemos aproximando
pero no del valor h que dene la anchura del intervalo entorno al punto x donde queremos evaluar la
funci on.
Veamos como eliminar el termino de error dominante (a
2
h
2
) de la ecuaci on 10.12. Para ello rees-
cribimos dicha ecuaci on sustituyendo h por h/2.
D = d
0
(h/2) + a
2
h
2
/4 + a
4
h
4
/16 + a
6
h
6
/64 + .... (10.13)
A continuaci on multiplicamos por cuatro:
4D = 4d
0
(h/2) + a
2
h
2
+ a
4
h
4
/4 + a
6
h
6
/16 + ....
y restamos la ecuaci on 10.12:
4D D = 3D = 4d
0
(h/2) d
0
(h)
3
4
a
4
h
4

15
16
a
6
h
6
...
obteniendo, nalmente:
D =
4
3
d
0
(h/2)
1
3
d
0
(h)
1
4
a
4
h
4

5
16
a
6
h
6
... (10.14)
La ecuaci on 10.14 representa el primer paso en la extrapolaci on de Richardson. Se trata de una
simple combinaci on de las funciones d
0
(h) y d
0
(h/2) que proporciona una aproximaci on cuyo error
dominante es O(h
4
).
Si deseamos aumentar la precisi on de la aproximaci on, podemos repetir el proceso anterior. De-
niendo:
d
1
(h) =
4
3
d
0
(h/2)
1
3
d
0
(h)
obtenemos:
D = d
1
(h) + b
4
h
4
+ b
6
h
6
+ ...
donde b
4
=
1
4
a
4
, etc. Reescribiendo la ecuaci on con h = h/2:
D = d
1
(h/2) + b
4
h
4
/16 + b
6
h
6
/64 + ...
multiplicando esta ultima ecuaci on por 16 y restando de la anterior obtenemos una aproximaci on cuyo
error dominante es O(h
6
).
D =
16
15
d
1
(h/2)
1
15
d
1
(h) b
6
h
6
/20 ... (10.15)
Podramos repetir el proceso escribiendo:
d
2
=
16
15
d
1
(h/2)
1
15
d
1
(h)
de tal manera que obtendramos:
D = d
2
(h) + c
6
h
6
+ c
8
h
8
+ ....
y as sucesivamente.
Hemos dado con un algoritmo recursivo que nos permite realizar un n umero arbitrario, llamemosle
M de extrapolaciones de Richardson sucesivas. Formalmente el procedimiento es como sigue:
195
1. Partimos de un cierto valor de h y calculamos los M + 1 n umeros:
D(n, 0) = d
0
(h/2
n
) (0 n M) (10.16)
2. Calculamos el tablero determinado por la f ormula recursiva:
D(n, k) =
4
k
4
k
1
D(n, k 1)
1
4
k
1
D(n 1, k 1) (10.17)
donde k = 1, 2, ..., M y n = k, k + 1, ..., M.
Consideremos como ejemplo que deseamos efectuar dos extrapolaciones de Richardson, M = 2.
Calculamos en primer lugar los n umeros:
D(0, 0) = d
0
(h)
D(1, 0) = d
0
(h/2)
D(2, 0) = d
0
(h/4)
y a continuaci on las cantidades D(1, 1), D(2, 1) y D(2, 2):
D(1, 1) =
4
3
D(1, 0)
1
3
D(0, 0)
=
4
3
d
0
(h/2)
1
3
d
0
(h) = d
1
(h)
D(2, 1) =
4
3
D(2, 0)
1
3
D(1, 0)
=
4
3
d
0
(h/4)
1
3
d
0
(h/2) = d
1
(h/2)
D(2, 2) =
16
15
D(2, 1)
1
15
D(1, 1)
=
16
15
d
1
(h/2)
1
15
d
1
(h) = d
2
(h)
Resulta por lo tanto evidente que:
D(0, 0) = D + O(h
2
)
D(1, 1) = D + O(h
4
)
D(2, 2) = D + O(h
6
)
El resultado general es:
D(n, k 1) = D + O(h
2k
) (10.18)
La expresi on 10.18 es una consecuencia de la forma general de las cantidades D(n, k) que obedecen
a una ecuaci on de la forma:
D(n, k 1) = D +

j=k
A
jk
(h/2
n
)
2j
(10.19)
Para demostrar que la ecuaci on 10.19 es cierta, procedemos por inducci on. Para k = 1, obtenemos:
D(n, 0) = D +

j=1
A
j1
(h/2
n
)
2j
196
Por otra parte, a partir de las ecuaciones 10.16 y 10.13:
D(n, 0) = d
0
(h/2
n
) = D a
2
(h/2
n
)
2
a
4
(h/2
n
)
4
...
= D

j=1
a
2j
(h/2
n
)
2j
con lo cual el caso k = 1 se demuestra sin m as que tomar A
j1
= a
2
j. A continuaci on suponemos
que la f ormula es v alida para el caso k 1 y la demostramos para k. A partir de la ecuaci on 10.17
obtenemos:
D(n, k) =
4
k
4
k
1
D(n, k 1)
1
4
k
1
D(n 1, k 1)
=
4
k
4
k
1
[D +

j=k
A
jk
(
h
2
n
)
2j
]
1
4
k
1
[D +

j=k
A
jk
(
h
2
n1
)
2j
]
que se simplica a:
D(n, k) = D +

j=k
A
jk
[
4
k
4
j
4
k
1
](
h
2
n
)
2j
] (10.20)
deniendo:
A
j,k+1
= A
j,k
[
4
k
4
j
4
k
1
] (10.21)
y teniendo en cuenta que A
k,k+1
= 0 por la ecuaci on anterior, llegamos a:
D(n, k) = D +

j=k+1
A
j,k+1
(
h
2
n
)
2j
] (10.22)
como queramos demostrar.
Las f ormulas para D(n, 0), D(n, k) nos permiten construir un tablero similar al que apareca en el
c alculo de las diferencias divididas:
D(0, 0)
D(1, 0) D(1, 1)
D(2, 0) D(2, 1) D(2, 2)
D(3, 0) D(3, 1) D(3, 2) D(3, 3)
... ... ... ...
en el cual las sucesivas extrapolaciones de Richardson vienen dadas por los terminos D(0, 0), D(1, 1), D(2, 2)
etc.
Conviene resaltar, antes de concluir esta secci on, que, aunque la tecnica que acabamos de estudiar
permite reducir el error de truncaci on arbitrariamente, los resultados numericos que se obtienen vienen
a menudo dominados por el error de redondeo, que no disminuye con las potencias sucesivas de h. Esta
es la raz on por la que, a menudo, basta con unas pocas extrapolaciones de Richardson para saturar
la precisi on que se puede alcanzar en el c alculo.
10.2. Integraci on Numerica
El problema que nos planteamos es el de calcular numericamente la integral:
_
b
a
f(x)dx (10.23)
197
Una estrategia particularmente sencilla para ello, es la de reemplazar la funci on f(x) por otra
funci on g(x) que sea, por una parte una buena aproximaci on a f y por otra, f acil de integrar. Por
ejemplo, podemos utilizar un polinomio, p(x), que interpola un conjunto de nodos, (x
0
, x
1
, ..., x
n
)
[a, b]) a la funci on f:
p(x) =
n

i=0
f(x
i
)l
i
(x) (10.24)
donde hemos expresado p(x) utilizando la f ormula de Lagrange en terminos de las funciones cardinales:
l
i
(x) =
n

j=0
j=i
x x
j
x
i
x
j
(0 i n)
Para calcular la integral de la ecuaci on 10.23 sustituimos la funci on f por el polinomio que la interpola:
_
b
a
f(x)dx
_
b
a
p(x)dx =
n

i=0
f(x
i
)
_
b
a
l
i
(x)dx =
n

i=0
A
i
f(x
i
) (10.25)
donde:
A
i
=
_
b
a
l
i
(x)dx
10.2.1. Regla Trapezoidal
El caso m as simple de interpolaci on corresponde a n = 1, en cuyo caso tenemos los nodos x
0
= a
y x
1
= b. Las funciones cardinales son:
l
0
(x) =
b x
b a
l
1
(x) =
x a
b a
y por tanto:
A
0
=
b
_
a
l
0
(x)dx =
b
_
a
b x
b a
dx =
1
b a
[b(b a)
1
2
(b
2
a
2
)]
=
1
2(b a)
[2b(b a) (b + a)(b a)] =
2b b a
2
=
1
2
(b a)
A
1
=
b
_
a
l
1
(x)dx =
b
_
a
x a
b a
dx =
1
b a
[
1
2
(b
2
a
2
) a(b a)]
=
1
2
[b + a 2a] =
1
2
(b a)
de ah que:
_
b
a
f(x)d(x)
b a
2
[f(a) + f(b)] (10.26)
La ecuaci on 10.26 se denomina regla trapezoidal. Es exacta para todos los polinomios de grado uno
como m aximo.
198
Precisi on de la regla trapezoidal
El error asociado a la aproximaci on puede calcularse sin m as que integrar el termino de error en
la aproximaci on polinomial:
= f(x) p(x) =
1
(n + 1)!
f
n+1
(
x
)
n

i=0
(x x
i
)
que, para n = 1 es simplemente:
= f(x) p(x) =
1
2
f

(
x
)(x a)(x b)
Para integrar la ecuaci on anterior debemos recurrir al teorema del valor medio para integrales, el cual
establece que si f es continua y g integrable Riemann en un intervalo [a, b], siendo g(x) 0 (o bien
g(x) 0), entonces existe un punto tal que a b para el que se verica que:
_
b
a
f(x)g(x)dx = f()
_
b
a
g(x)dx
Utilizando este resultado:
=
_
b
a
=
1
2
f

()
b
_
a
(x a)(x b) =
1
2
f

()
b
_
a
(x
2
x(a + b) + ab)dx
=
1
2
f

()
1
6
[b
3
+ a
3
+ 3ab
2
3a
2
b] =
1
12
f

()(b a)
3
done (a, b).
Regla trapezoidal compuesta
Si subdividimos el intervalo [a, b] en n subintervarlos, mediante el conjunto de nodos:
a = x
0
< x
1
< x
2
< ... < x
n
= b
con espaciado h = (b a)/n y aplicamos la regla trapezoidal al subintervalo [x
i
, x
i+1
], obtenemos:
_
x
i+1
x
i
f(x)dx =
h
2
[f(x
i
) + f(x
i+1
)]
1
12
h
3
f

()
donde x
i
< < x
i+1
. Sumando a continuaci on sobre cada uno de los subintervalos obtenemos la regla
trapezoidal compuesta:
_
b
a
f(x)dx =
n1

i=0
_
x
i+1
x
i
f(x)dx
=
h
2
n1

i=0
[f(x
i
) f(x
i+1
)]
h
3
12
n1

i=0
f

(
i
) (10.27)
El termino de error en la ecuaci on 10.27 puede simplicarse sin m as que utilizar que h = (b a)/n:
h
3
12
n1

i=0
f

() =
(b a)
12
h
2
[
1
n
n1

i=0
f

()] =
(b a)
12
h
2
f

()
donde hemos aplicado el teorema del valor intermedio de las funciones continuas
1
para concluir que,
puesto que el valor medio
1
n

n1
i=0
f

(
i
) se encuentra entre los valores mayor y menor de f

en (a, b),
entonces existe alg un punto (a, b) tal que f

() =
1
n

n1
i=0
f

().
1
que garantiza que si g es continua en [a, b], entonces para todo c, tal que g(a) < c < g(b), existe un punto [a, b]
que verica que c = g().
199
Regla trapezoidal recursiva para 2
n
subintervalos iguales
Escribiendo x
i
= a + ih la ecuaci on 10.27 toma la forma:
_
b
a
f(x)dx
h
2
n1

i=1
[f(a + ih) f(a + (i + 1)h)] (10.28)
si reemplazamos n por 2
n
y tomamos en cuenta que h = (b a)/2
n
entonces la f ormula 10.28 se
transforma en:
R(n, 0) = h
2
n
1

i=1
f(a + ih) +
h
2
[f(a) + f(b)] (10.29)
donde hemos introducido la notaci on R(n, 0) para designar la regla trapezoidal aplicada a 2
n
subin-
tervalos iguales, tal como se representa en la gura 10.1.

R(0,0)
R(1,0)
R(2,0)
R(3,0)
Figura 10.1: La regla trapezoidal aplicada a 2
n
subintervalos.
Veamos una f ormula recursiva para expresar R(n, 0) en funci on de R(n1, 0). Para ello, escribimos:
C =
h
2
[f(a) + f(b)]
con lo que la ecuaci on 10.29 queda como:
R(n, 0) = h
2
n
1

i=1
f(a + ih) + C (10.30)
R(n1, 0) no es sino la regla trapezoidal aplicada a 2
n1
intervalos, como se ilustra en la gura 10.1,
donde R(3, 0) aplica la regla trapezoidal a 2
3
= 8, R(2, 0) a 2
2
= 4, R(1, 0) a 2
1
= 2 y R(0, 0) a 2
0
= 1
intervalos. Si jamos h = (b a)/2
n
para R(n, 0), entonces para R(n 1, 0), h

= (b a)/2
n1
=
2(b a)/2
n
= 2h, con lo que:
R(n 1, 0) = 2h
2
n1
1

j=1
f(a + 2jh) + 2C
y por lo tanto:
1
2
R(n 1, 0) = h
2
n1
1

j=1
f(a + 2jh) + C (10.31)
200
substrayendo la ecuaci on 10.31 de la ecuaci on 10.30 obtenemos:
R(n, 0)
1
2
R(n 1, 0) = h
2
n
1

i=1
f(a + ih) h
2
n1
1

j=1
f(a + 2jh) = h
2
n1

k=1
f[a + (2k 1)h]
puesto que los terminos pares se cancelan al substraer ambos terminos. Obtenemos por lo tanto la
f ormula recursiva:
R(0, 0) =
1
2
(b a)[f(a) + f(b)]
R(n, 0) =
1
2
R(n 1, 0) + h
2
n1

k=1
f[a + (2k 1)h] (10.32)
con h = (b a)/2
n
.
La ecuaci on 10.32 permite calcular una serie de aproximaciones a una integral denida usando
la regla trapezoidal sin necesidad de evaluar de nuevo el integrando en los puntos donde ya ha sido
evaluado.
10.2.2. Metodo de los coecientes indeterminados
Sabemos que la f ormula 10.25 es exacta para todos los polinomios de grado n. La forma de los
coecientes A
i
la obtuvimos a partir de la f ormula de Lagrange para un polinomio.
Existe otra manera de obtener dichos coecientes, partiendo, precisamente, del hecho de que la
f ormula 10.25 es exacta para todos los polinomios de grado n. Veamos, como derivar la regla
trapezoidal utilizando dicho metodo. Buscamos una f ormula:
_
b
a
f(x)dx A
0
f(a) + A
1
f(b) (10.33)
que sea exacta para todos los polinomios de grado 1. Ser a por lo tanto exacta para el polinomio de
grado cero p
0
(x) = 1 y para el polinomio de grado 1 p
1
(x) = x. Por lo tanto:
_
b
a
1 dx = b a = A
0
1 + A
1
1
_
b
a
xdx =
1
2
(b
2
a
2
) = A
0
a + A
1
b
Multiplicando la primera ecuaci on por (b + a) y rest andole la segunda ecuaci on multiplicada por 2
obtenemos que A
0
= A
1
y de ah que:
A
0
= A
1
=
1
2
(b a)
sustituyendo en 10.33 obtenemos de nuevo la regla trapezoidal.
10.2.3. Regla de Simpson
Apliquemos ahora el metodo de los coecientes indeterminados para calcular una integral numerica
que sea exacta para todos los polinomios de grado 2. Para ello, consideremos el conjunto de nodos
a = x
0
< x
1
< x
2
= b y escribamos:
_
x
2
x
0
f(x)dx A
0
f(x
0
) + A
1
f(x
1
) + A
2
f(x
2
) (10.34)
201
Puesto que la ecuaci on ser a exacta para los polinomios 1, x, x
2
, obtenemos el sistema de ecuaciones:
_
x
2
x
0
dx = (b a) = x
2
x
0
= A
0
+ A
1
+ A
2
_
x
2
x
0
xdx =
1
2
(x
2
2
x
2
0
) = A
0
x
0
+ A
1
x
1
+ A
2
x
2
_
x
2
x
0
x
2
dx =
1
3
(x
3
2
x
3
0
) = A
0
x
2
0
+ A
1
x
2
1
+ A
2
x
2
2
Ahora bien, la resoluci on directa de este sistema es sumamente tediosa. Si, en cambio, aplicamos el
metodo de los coecientes indeterminados al intervalo [0, 1], obtenemos un sistema mucho m as sencillo:
_
1
0
f(x)dx A
0
f(0) + A
1
f(1/2) + A
2
f(1) (10.35)
y:
_
1
0
dx = 1 = A
0
+ A
1
+ A
2
_
1
0
xdx =
1
2
=
1
2
A
1
+ A
2
_
1
0
x
2
dx =
1
3
=
1
4
A
1
+ A
2
cuya soluci on es inmediata, obteniendose A
0
= 1/6, A
1
= 2/3, y A
2
= 1/6. Por lo tanto:
_
1
0
f(x)dx
1
6
[f(0) + 4f(1/2) + f(1)] (10.36)
Consideremos ahora el cambio de variable:
x = a + (b a)t dx = (b a)dt (10.37)
entonces:
_
b
a
f(x)dx =
_
1
0
f(a + (b a)t)(b a)dt = h
_
1
0
g(t)dt
donde la funci on g es tal que g(t) = f(a + ht). Utilizando la ecuaci on 10.36 obtenemos:
_
b
a
f(x)dx (b a)
_
1
0
g(t)dt =
1
6
[g(0) + 4g(1/2) + g(1)]
=
1
6
[f(a) + 4f(a +
b a
2
) + g(a + (b a))]
=
1
6
[f(a) + 4f(
a + b
2
) + g(b)]
Encontramos as la regla de Simpson:
_
b
a
f(x)dx
b a
6
[f(a) + 4f(
a + b
2
) + f(b)] (10.38)
en el caso de que los dos subintervalos esten igualmente espaciados podemos reescribir la ecuaci on
10.38 como:
_
a+2h
a
f(x)dx
h
3
[f(a) + 4f(a + h) + f(a + 2h)] (10.39)
202
Precisi on de la regla de Simpson
El termino de error para esta regla puede deducirse f acilmente a partir de la ecuaci on 10.39. Para
ello desarrollamos en serie de Taylor los terminos a la derecha de la ecuaci on:
f(a) = f(a)
4f(a + h) = 4f(a) + 4hf

(a) +
4
2!
h
2
f

(a) +
4
3!
h
3
f
(3)
(a) +
4
4!
h
4
f
(4)
(a) + ...
f(a + 2h) = f(a) + 2hf

(a) +
4
2!
h
2
f

(a) +
8
3!
h
3
f
(3)
(a) +
16
4!
h
4
f
(4)
(a) + ...
f(a) + 4f(a + h) + f(a + 2h) = 6f(a) + 6hf

(a) + 4h
2
f

(a) + 2h
3
f
(3)
(a) +
20
4!
h
4
f
(4)
(a) + ...
y por lo tanto:
h
3
[f(a) + 4f(a + h) + f(a + 2h)] = 2hf(a) + 2h
2
f

(a) +
4
3
h
3
f

(a) +
2
3
h
4
f
(3)
(a) +
20
3 4!
h
5
f
(4)
(a) + ...
(10.40)
A continuaci on desarrollamos en serie de Taylor la parte izquierda de la ecuaci on 10.39. Sea F la
funci on primitiva de f:
F(t) =
_
t
a
f(x)dx
la cual, por el teorema fundamental del c alculo verica que F

= f. La serie de Taylor para F es:


F(a + 2h) = F(a) + 2hF

(a) + 4
h
2
2
F

(a) +
4
3
h
3
F
(3)
(a)
+
2
3
h
4
F
(4)
(a) +
2
5
5!
h
5
F
(5)
(a) + ...
veric andose adem as que:
F(a) =
_
a
a
f(t)dt = 0
F(a + 2h) =
_
a+2h
a
f(t)dt
que es precisamente la integral a la izquierda de la ecuaci on 10.39. Puesto que F

= f, F

= f

,
F

= f

, etc., obtenemos:
_
a+2h
a
f(x)dx = 2hf(a) + 2h
2
f

(a) +
4
3
h
3
f

(a) +
2
3
h
4
f
(3)
(a) +
2
5
5 4!
h
5
f
(4)
(a) + ... (10.41)
Restando 10.40 de 10.41, obtenemos:
_
a+2h
a
f(x)dx =
h
3
[f(a) + 4f(a + h) + f(a + 2h)]
h
5
90
f
(4)
(a)
Un an alisis m as detallado (consultar, por ejemplo, [3]) mostrara que el termino de error para la
regla de Simpson fundamental, ecuaci on 10.38 es:

1
90
[(b a)/2]
5
f
(4)
()
donde (a, b).
203
Regla de Simpson compuesta
An alogamente al caso de la trapezoidal, podemos obtener una regla de Simpson compuesta divi-
diendo el intervalo [a, b] en n subintervalos, donde n es un n umero par. Deniendo:
h = (b a)/n x
i
= a + ih (0 i n)
Obtenemos:
_
b
a
f(x)dx =
_
x
2
x
0
f(x)dx +
_
x
4
x
2
f(x)dx + ... +
_
x
n
x
n2
f(x)dx+
aplicando la regla de Simpson a cada uno de los subintervalos, obtenemos la regla de Simpson repetida:
_
b
a
f(x)dx =
h
3
n/2

i=1
[f(x
2i2
) + 4f(x
2i1
) + f(x
2i
)] (10.42)
donde la parte derecha de la ecuaci on 10.42 debe calcularse, para evitar repeticiones de terminos como:
h
3
[f(x
0
) + 2
n/2

i=2
f(x
2i2
) + 4
n/2

i=1
f(x
2i1
) + f(x
n
)]
nalmente, damos sin demostraci on el termino de error de esta f ormula:

1
180
(b a)h
4
f
(4)
()
para (a, b).
10.2.4. Integral de Romberg
Partimos de la f ormula de Euler-Maclaurin, que permite expresar la integral denida de una funci on
f C
2m
[0, 1]
2
como:
_
1
0
f(t)dt =
1
2
[f(0) + f(1)]
+
m1

k=1
A
2k
[f
(2k1)
(0) f
(2k1)
(1)]
A
2m
f
(2m)
(
0
) (10.43)
donde
0
(0, 1). Las constantes A
k
se conocen como n umeros de Bernoulli. Una prueba de esta
importante f ormula se deriva, por ejemplo en [3].
A continuaci on aplicamos la f ormula 10.43 a una funci on g denida como g(t) = f(x
i
+ht), donde
h = x
i+1
x
i
:
_
1
0
g(t)dt =
1
2
[g(0) + g(1)]
+
m1

k=1
A
2k
[g
(2k1)
(0) g
(2k1)
(1)] + ...error
2
Es decir, f es una funci on continua y 2m veces derivable en el intervalo [0, 1].
204
teniendo en cuenta que g

(x) = hf(x
i
+ht), g

(x) = h
2
f(x
i
+ht),...,g
k
(x) = h
k
f(x
i
+ht) y omitiendo,
por simplicidad, el termino de error, obtenemos:
_
1
0
f(x
i
+ ht)dt =
1
2
[f(x
i
) + f(x
i+1
)]
+
m1

k=1
A
2k
h
(2k1)
[f
(2k1)
(x
i
) f
(2k1)
(x
i+1
)] + ...
hacemos ahora el cambio de variable t = (x x
i
)/h obteniendo:
_
x
i+1
x
i
f(x)dx =
h
2
[f(x
i
) + f(x
i+1
)]
+
m1

k=1
A
2k
h
(2k)
[f
(2k1)
(x
i
) f
(2k1)
(x
i+1
)] + ... (10.44)
Aplicamos a continuaci on la operaci on

2
n1
i=0
a ambos miembros de la ecuaci on 10.44. Si x
i
= a+ih
para 0 i 2
n
y h = (b a)/2
n
, entonces:
_
b
a
f(x)dx =
h
2
2
n1

i=0
[f(x
i
) + f(x
i+1
)]
+
m1

k=1
A
2k
h
(2k)
[f
(2k1)
(a) f
(2k1)
(b)] + ... (10.45)
y por lo tanto, a partir de la ecuaci on 10.45 obtenemos que:
I = R(n, 0) + c
2
h
2
+ c
4
h
4
+ c
6
h
6
+ ... (10.46)
donde I es el valor exacto de la integral, R(n, 0) la f ormula trapezoidal utilizando subintervalos igual-
mente espaciados de tama no h = (b a)/2
n
y las constantes c
2k
no dependen de h. Estamos ante
una f ormula identica a la ecuaci on 10.12, con lo que el an alisis que hicimos sobre las extrapolaciones
de Richardson es v alido sin m as que sustituir los terminos D(n, m) que denamos all por terminos
R(n, m) que representan las correspondientes extrapolaciones de Richardson utilizando la f ormula
trapezoidal R(n, 0) cuya forma explcita ya hemos visto. Obtendremos por tanto un tablero del tipo:
R(0, 0)
R(1, 0) R(1, 1)
R(2, 0) R(1, 1) R(2, 2)
R(3, 0) R(3, 1) R(3, 2) R(3, 3)
... ... ... ...
donde los R(n, m) se construyen a partir de la f ormula:
R(n, m) = R(n, m1) +
1
4
m
1
[R(n, m1) R(n 1, m1)] (10.47)
10.3. Programaci on de algoritmos de integraci on y derivaci on
10.3.1. La clase Medida
Se trata de una simple clase de datos que nos permite expresar de forma compacta el resultado de
una medida experimental, que siempre consta de dos n umeros, la medida en s misma y su error. La
denici on de la clase (medida.h) se encuentra en el directorio Util:
205
class Medida
private:
double px; // valor de la medida
double pd; // error;
public:
// Constructores
Medida()px = 0; pd = 0;
Medida(double x,double d)px = x; pd = d;
// Destructor
~Medida()
// Funciones de acceso
double x() const return px; ;
double dx() const return pd;;
double& x()return px; ;
double& dx()return pd; ;
;
inline ostream& operator << (ostream& s, const Medida& m)
s << "( " << m.x() << "," << m.dx() <<" )" ;
return s;

10.3.2. El algoritmo Derint


Nuestro objetivo es programar un algoritmo que nos permita integrar y/o derivar una funci on
utilizando tantas extrapolaciones de Richardson como deseemos, a n de explorar la precisi on que
puede alcanzarse en una derivada (integral) numerica.
La clase de algoritmo Derint
Como el resto de los algoritmos que hemos ido introduciendo, la clase Derint acepta una serie
de datos (en este caso un puntero a la funci on que queremos derivar o integrar y el orden de la
extrapolaci on de Richardson) y ejecuta una serie de acciones (derivar la funci on establecida en un
cierto punto o integrarla en un determinado intervalo). La denici on de la clase (derint.h) es la
siguiente:
class Derint: public Algoritmo
private:
int pM; // orden de la extrapolaci on
DVector pR; // vector din amico para extrap de Richardson
func pF; // funci on a derivar o integrar
//Calcula la tabla de Richardson. La entrada es un vector
// din amico relleno con la primera columna que contiene
// las estimaciones D(0,0), D(1,0), D(2,0).... donde los
// t erminos D(0,0), D(1,0), D(2,0).... se refieren a las sucesivas
// aproximaciones a la derivada o la trapezoidal. La funci on calcula
// el tablero D(1,1)
206
// D(2,1) D(2,2)
// D(3,1) D(3,2) D(3,3).....
// hasta el orden m,
// columna a columna, reescribiendo en cada paso los t erminos de la
// columna que no est an en la diagonal inferior.
// A la salida, el vector R contiene
// las extrapolaciones de Richardson m as precisas de cada columna,
// D(m,0), D(m,1)...D(m,m).
void eRichardson(DVector& R);
public:
// Constructor
Derint();
// Destructor
~Derint();
//Establece la funci on
void eFuncion(func f);
//Establece el orden de la extrapolacion
void eExtrap(int M = 3);
//Acciones
// Integra la funci on establecida entre a y b
Medida integra(double a, double b);
//Deriva la funci on establecida en x
Medida deriva(double x);
//Devuelve la diagonal de la tabla de Richardson
DVector& richardson();
;
El algoritmo es muy sencillo, ejecutando tan s olo tres acciones, integra, deriva y richardson.
El n ucleo del algoritmo es la funci on privada eRichardson, donde se calculan las extrapolaciones de
Richardson deseadas por el usuario. Dichas extrapolaciones s olo requieren el c alculo de la primera co-
lumna del tablero, que se realiza en las funciones integra o deriva. Cada columna puede calcularse
a partir de la anterior, siendo posible reutilizar el espacio que hemos asignado a la primera columna
para almacenar, al nal del proceso, la diagonal inferior del tablero, que contiene las extrapolacio-
nes de Richardson de ordenes sucesivos, (R(n, 0), R(n, 1), R(n, 2)...R(n, n).). Esta tecnica de c alculo
por columnas ya la empleamos para almacenar las sucesivas diferencias divididas en la interpolaci on
utilizando el algoritmo de Newton. Recordemos que la primera columna del tablero de Richardson
contiene expresiones del tipo R(n, 0) o D(n, 0), que se calculan a partir de las f ormula 10.32, en el caso
de la integraci on (utilizando la regla trapezoidal repetida) y la f ormula 10.16 en el de la derivaci on.
Una vez calculado el tablero de Richardson el valor de la integral o derivada se toma como el
apex del tablero (R(n, n) o D(n, n) y el error como la diferencia entre el apex y el elemento anterior
(R(n, n 1)), en valor absoluto. El resultado de la derivada o integral se devuelve en una variable de
tipo Medida.:
//Integra la funci on utilizando la regla trapezoidal
// repetida y las extrapolaciones de Richardson requeridas
Medida Derint::integra(double a, double b)
207
if(pCC.interruptorAbierto("func"))
error("Funci on no establecida en Derint::integra",
EPROCESO);
pCC.cierraInterruptor("integra");
// Calculamos R(0,0), que no es otra cosa que la
// regla trapezoidal.
pR[0] = (1./2.)*(b - a)*( pF(a) + pF(b) );
// Y ahora calculamos las extrapolaciones requeridas
// Comenzamos por llenar el tablero con
// R(1,0), R(2,0), etc., donde R(n,0) es la regla
// trapezoidal para 2^n divisiones.
for (int i = 1; i < pM; i++)
double h = (b - a)/pow(2.,(double)i);
pR[i] = 0.5 * pR[i-1] + h * suma(pF,i,a,h);

// y a continuaci on realizamos las extrapolaciones de Richardson


eRichardson(pR);
// El resultado es una Medida, cuyo valor es el apex del
// tablero de Richardson y su error es la diferencia en valor
// absoluto entre el apex y el elemento anterior de la diagonal
Medida r;
r.x() = pR[0];
r.dx() = fabs(pR[0] - pR[1]);
return r;

//Deriva la funci on
//Deriva la funci on
Medida Derint::deriva(double x)
if(pCC.interruptorAbierto("func"))
error("Funci on no establecida en Derint::deriva",
EPROCESO);
pCC.cierraInterruptor("deriva");
double h = H;
// Calculamos en primer lugar los D(0,0), D(1,0)...etc
for (int i = 0; i < pM; i++)
pR[i] = d(pF,h,i,x);

208
// Y a continuaci on las extrap de Richardson
eRichardson( pR);
Medida r;
r.x() = pR[0];
r.dx() = fabs(pR[0] - pR[1]);
return r;

//Calcula la tabla de extrapolaci on de Richardson. Guarda los elementos


//de la diagonal inferior, de tal manera que al final del proceso el
//vector din amico contiene los elementos:
//R(n,n), R(n,n-1), R(n,n-2)....R(n,0)
void Derint::eRichardson(DVector& R)

int M = R.dimension();
for (int m = 1; m < M ; m ++)
for ( int n = 1; n <= M-m; n ++)
double temp = ((pow(4.,(double) m)/(pow(4.,(double) m)-1.)))*
R[n] - (1./(pow(4.,(double) m) -1.)*R[n-1]);
R[n-1] = temp;

Por ultimo, la funci on richardson tiene como unica tarea devolver la diagonal de la tabla de
Richardson, es decir, los elementos R(1,1), R(2,2),...R(m,m).
DVector& Derint::richardson()
if(pCC.interruptorAbierto("integra")
&& pCC.interruptorAbierto("deriva"))
error("Derint::Richardson --Extrapolacion no realizada",
EPROCESO);

return pR;

El programa xderint.cpp, ilustra el funcionamiento del algoritmo.


#include <iostream>
#include <error.h>
#include <matutil.h>
#include <derint.h>
inline double trig(double x)

return sin(x)*sin(x) - cos(x);


209

inline double xexp(double x)

return x*exp(x - 10.);

inline double ff (double x)


return exp(x);

// Imprime el tablero de Richardson con


// las cifras decimales deseadas
void imprimeR(DVector& m, int campo, int eps)
if (m.vacio())
cout << "vector vac o" << endl;

cout << " " ;


int i;
for (i = 1; i <= m.dimension(); i++)
cout << setw(campo)<<setprecision(eps)<< m(i) ;
if (i < m.dimension()) cout << " , ";

cout << " " << endl;

// Imprime la medida con las cifras decimales deseadas


void imprimeM(Medida& m, int campo, int eps)
cout << "( " << setw(campo)<<setprecision(eps) << m.x()
<< "," << setw(campo)<<setprecision(eps) <<m.dx()
<<" )" << endl;

int main ()

// Creamos una m aquina de deriva/integra


Derint dd;
func F;
//Lee la fuci on a integrar/derivar
cout << " Qu e funci on desea? 1 = sin(x)*sin(x)-cos(x),"<< endl
<< " 2 = x * exp(x-10)," << endl
<< " 3 = F " << endl;
int ifun; cin >> ifun;
if (ifun == 1)
F = trig ;
else if (ifun == 2)
F = xexp ;
else if (ifun == 3)
F = ff ;
// Establece la funci on
210
dd.eFuncion(F);
// Establecemos el orden de las extrap de Richardson
cout << "Orden de la extrapolaci on de Richardson: " << endl;
int m;
cin >> m;
dd.eExtrap(m);
//Si quiere integrar, pide los l mites de integraci on
//Si quiere derivar, pide el punto en el que quiere el
//c alculo de la derivada
string respuesta;
cout << "?Quieres integrar o derivar la funci on escogida?" << endl;
cin >> respuesta;
if ( respuesta == "integrar" || respuesta== "i" )
double a, b;
cout << " L mite inferior:" << endl;
cin >> a;
cout << " L mite superior:" << endl;
cin >> b;
cout << "Integral (con error) de "
<< ifun <<" entre "<<a<<" y " << b << endl;
Medida r = dd.integra(a,b);
imprimeM(r,14,10);
cout << "Tablero de Richardson correspondiente: " << endl;
imprimeR(dd.richardson(),14,10);
// cout << dd.richardson() << endl;

if ( respuesta == "derivar" || respuesta == "d" )


double xp;
cout << " Punto en el que quieres el c alulo de la derivada"<<endl;
cin >> xp;
cout << "Derivada (con error) de "<< ifun << " en " << xp << endl;
Medida r = dd.deriva(xp);
imprimeM(r,14,10);
cout << "Tablero de Richardson correspondiente: " << endl;
imprimeR(dd.richardson(),14,10);

return 0;

La ejecuci on del programa ofrece al usuario la posibilidad de elegir la funci on con la que quiere
trabajar, as como de elegir los lmites de integraci on (o el punto en el que quiere calcular la derivada).
Tambien pregunta el orden de la extrapolaci on de Richardson al que se quiere llegar en las distintas
acciones del programa. Los resultados que se ofrecen son los valores de la integral o de la derivada seg un
el caso, con sus respectivos errores, y los elementos de la diagonal inferior del tablero de Richardson,
que constituyen las sucesivas aproximaciones a los resultados requeridos.
211
10.3.3. Recapitulaci on
En este captulo hemos estudiado varias tecnicas elementales de integraci on y derivaci on numerica
para funciones continuas y derivables en un cierto intervalo. Como aplicaci on numerica, se ha pro-
gramado el algoritmo Derint, basado en el algoritmo de extrapolaci on de Richardson, que se utiliza
indistintamente para el c alculo de la derivada o la integral denida de la funci on problema una vez
que se ha rellenado la primera columna del tablero de Richardson mediante las f ormulas de derivaci on
o integraci on numerica pertinentes.
212
Captulo 11
Tratamiento de datos
experimentales
11.1. Introducci on
Dado un conjunto de medidas experimentales, se desea a menudo ajustarlas a un determinado mo-
delo, a menudo una funci on cuya forma viene dictada por una teora fsica. Dicho modelo depender a de
una serie de par ametros que son, precisamente, los que queremos determinar.
Para ajustar una serie de medidas a un modelo, debemos dise nar una funci on de merito que mide
el acuerdo entre los datos experimentales y el modelo propuesto y a continuaci on variar los par ametros
del modelo hasta que la funci on de merito alcance un mnimo. Los par ametros que mejor ajustan los
datos al modelo son, precisamente, los que corresponden a dicho mnimo.
As pues, el problema de ajustar datos experimentales, se reduce al problema, arduo en general,
de buscar el mnimo de una funci on, en principio arbitraria. No obstante, en algunos casos particu-
lares, muy notablemente cuando la dependencia en los par ametros de la funci on modelo sea lineal, el
tratamiento matem atico del problema es sencillo.
Puesto que los datos experimentales no son exactos, sino que vienen afectados por errores de
medida, los par ametros determinados por el ajuste se obtendr an tambien con errores. En consecuencia,
el ajuste debe proporcionar, no s olo los valores de los par ametros del modelo en el mnimo, sino tambien
los errores de estos y sus correlaciones. Finalmente, puesto que un conjunto de datos nunca ajusta
exactamente a un modelo dado, incluso si este es correcto (debido precisamente a la existencia de
errores de medida) es necesario poder determinar, a partir del ajuste un procedimiento para determinar
la bondad o verosimilitud de este.
11.2. Propagaci on de errores
11.2.1. Funci on de densidad de probabilidad y funci on de distribuci on acu-
mulativa
Sea una variable aleatoria y continua X, que puede tomar cualquier valor en un cierto dominio .
Escribimos la probabilidad de que la variable aleatoria X tome valores en el intervalo [x, x+dx] como:
P(x < X < x + dx) = f(x)dx (11.1)
la funci on f(x) (que se mide en unidades de probabilidad por unidad de longitud) se llama funci on de
densidad de probabilidad, o p.d.f. (de las siglas inglesas, probability density function) para la variable
213
x. Por denici on:
_

f(x)dx = 1. (11.2)
La funci on de distribuci on acumulativa se dene como:
F(x) =
_
x
x
min
f(x

)dx

. (11.3)
donde x
min
es el lmite inferior de x. Como por construcci on f(x) es denida positiva, entonces F(x)
es mon otona creciente en el intervalo x
min
< x < x
max
. Es inmediato comprobar que:
F(x
min
) = 0, F(x
max
) = 1. (11.4)
11.2.2. Valor esperado de una funci on
Sea g(x) una cierta funci on de x. Denimos el valor esperado de la funci on g(x) para la p.d.f. f(x)
como:
< g(x) >=
_

g(x)f(x)dx, (11.5)
donde el intervalo de integraci on se extiende sobre todo el dominio de la variable. Es decir, f(x) act ua
como peso de g(x) en la cantidad resultante. < g(x) > no es otra cosa que una medida del valor
central o medio de la funci on g(x) con respecto al p.d.f f(x).
Denimos la varianza de g(x) para la p.d.f. f(x) como:
V (g(x)) =< g(x)
2
< g(x) >
2
> . (11.6)
La aplicaci on m as sencilla de las deniciones anteriores se obtiene particularizando al caso g(x) = x.
Es decir, calculamos el valor esperado de la propia variable aleatoria x, que no es otra cosa que el
valor medio de x para la p.d.f. f(x):
=< x >=
_

xf(x)dx. (11.7)
Para calcular la dispersi on de x alrededor de su valor medio denimos la varianza V(x) de x para
la p.d.f. f(x):

2
= V (x) =< (x )
2
>=
_

(x )
2
f(x)dx. (11.8)
La cantidad se denomina desviaci on est andar de x para la p.d.f. f(x).
11.2.3. Propagaci on de errores
Funci on escalar
Sea p(x) = p(x
1
, x
2
, ..., x
n
) una funci on de las variables aleatorias x
1
, x
2
, ..., x
n
y asumamos que
la matriz de covarianza de dichas variables, V (x) (es decir, la matriz que especica los errores de las
variables y sus correlaciones) es conocida. Para calcular la varianza de p, expandimos alrededor de los
valores medios
1
,
2
, ...,
n
de las variables x
1
, x
2
, ..., x
n
. Qued andonos a primer orden, obtenemos:
p(x) p( ) +
n

i=1
(x
i

i
)
p
x
i
[
x=
. (11.9)
Si tomamos el valor esperado en la f ormula anterior los terminos de primer orden se cancelan ya que
< x
i
>=
i
y por tanto:
< p(x) >= p( ) (11.10)
214
Introduciendo esta f ormula en la expresi on para la varianza (ecuaci on 11.6) nos queda:
V (p(x)) = < p(x) < p(x) >>
2
< p( ) +
n

i=1
(x
i

i
)
p
x
i
[
x=
p( ) >
2
= <
n

i=1
(x
i

i
)
p
x
i
[
x=
n

j=1
(x
j

j
)
p
x
j
[
x=
>
=
n

i=1
n

j=1
p
x
i
[
x=

p
x
j
[
x=
< (x
i

i
)(x
j

j
) >
Pero los valores esperados < (x
i

i
)(x
j

j
) > no son otra cosa que los elementos de la matriz
de covarianza de los x. Por tanto:
V (p(x))
n

i=1
n

j=1
p
x
i
p
x
j
V
ij
(x), (11.11)
donde se considera implcitamente que las derivadas se eval uan en x = .
La f ormula 11.11 se conoce como ley de propagaci on de errores. Hay que recordar que en el caso
general se trata s olo de una ley aproximada, ya que para derivarla nos hemos quedado en primer orden
en la expansi on de Taylor de p(x) alrededor de los valores medios , o, en otras palabras, hemos asumido
que las variables aleatorias est an pr oximas a sus valores medios y por tanto las cantidades (x
i

i
)
son lo sucientemente peque nas como para despreciar terminos de orden superior. Sin embargo, en el
caso particular en el que p es una funci on lineal de x, la expresi on anterior es exacta.
En el caso particular de que las variables x
i
sean todas independientes entre s obtenemos la familiar
expresi on:
V (p(x)) =
n

i=1
(
p
x
i
)
2
V
ii
(x) =
n

i=1
(
p
x
i
)
2

2
i
. (11.12)
Funci on vectorial
Vamos ahora a generalizar la ley de propagaci on de errores al caso en que tenemos un conjunto de
funciones p
1
, p
2
, ...p
m
que dependen de las variables aleatorias x
1
, x
2
, ..., x
n
de tal manera que:
p
k
(x) = p
k
(x
1
, x
2
, ..., x
n
) (11.13)
Como en el caso escalar expandimos entorno a los valores medios x = :
p
k
(x) = p
k
( ) +
n

i=1
(x
i

i
)
p
k
x
i
[
x=
. (11.14)
Tomando el valor esperado los terminos de primer orden se cancelan y por tanto:
< p
k
(x) >= p
k
( ). (11.15)
Generalizando la ecuaci on 11.6 e introduciendo la f ormula anterior:
V
kl
( p(x)) =< (p
k
(x) < p
k
(x) >) (p
l
(x) < p
l
(x) >) > (11.16)
De donde:
V
kl
( p(x))
n

i=1
n

j=1
p
k
x
i
[
x=

p
l
x
j
[
x=
< (x
i

i
)(x
j

j
) > (11.17)
215
Por tanto:
V
kl
( p(x))
n

i=1
n

j=1
p
k
x
i
p
l
x
j
V
ij
(x) (11.18)
donde, una vez m as se considera implcitamente que las derivadas se eval uan en x = . Los terminos
V
kl
( p) denen la matriz de covarianza para las variables dependientes p.
Para nalizar, resulta conveniente reescribir las f ormulas anteriores en notaci on matricial. Consi-
derando x y p como vectores columna con n y m elementos respectivamente, podemos escribir:
p = c + Sx + O(superior) (11.19)
En la ecuaci on anterior c es un vector de constantes, de m componentes y S una matriz m n que
describe la parte lineal de la transformaci on. Como hemos visto, se verica, a primer orden:
p
k
x
i
= S
ki
,
p
l
x
j
= S
lj
(11.20)
y por tanto:
V
kl
( p)
n

i=1
n

j=1
S
ki
V
ij
(x)S
lj
. (11.21)
Con lo que la ley de propagaci on de errores se puede escribir sencillamente como:
V ( p) = SV (x)S
T
. (11.22)
Donde S
T
es la matriz transpuesta de S y la f ormula anterior es exacta si la transformaci on es lineal.
11.3. Principio de maxima verosimilitud
Supongamos que tenemos un conjunto de observables o
i
, (i = 1, ..., n) que siguen una determinada
distribuci on de probabilidad f(o
i
; ), dependiente de un cierto par ametro desconocido. Por ejem-
plo consideremos una serie de n medidas independientes, y
1
, y
2
, ..., y
n
de la misma cantidad (que
queremos determinar) y asumamos, que el error de medida es el mismo para todas las medidas
y de naturaleza puramente aleatoria (es decir, ignoramos errores sistem aticos, o uctuaciones en la
cantidad debidas a fen omenos fsicos no aleatorios). En ese caso podemos asumir que cada una de
dichas medidas obedece a una distribuci on de probabilidad normal o gaussiana, N(, ), en la que
es desconocido (mientras que se conoce):
N(y
i
, ; ) =
1

2
e

(y
i
)
2
2
2
(11.23)
Denimos la verosimilitud de las observaciones o
1
, o
2
...o
n
, como:
L(o
1
, o
2
, ..., o
n
[) =
n

i=1
f(o
i
; ) (11.24)
Para el caso de n medidas independientes:
L(y
1
, y
2
, ..., y
n
[) =
n

i=1
N(y
i
, ; ) (11.25)
Nuestro problema es determinar el par ametro a partir del conjunto de observables o
i
, i = 1, n
(en el ejemplo se trata de determinar el valor de la media, , a partir de las medidas y
i
, i = 1, n).
216
El principio de m axima verosimilitud, establece que si variamos el par ametro , manteniendo
constantes las observaciones o
i
, entonces el mejor estimador del par ametro que buscamos, es aquel
que maximiza la funci on de verosimilitud L.
Veamos como estimar el par ametro (la media de las observaciones) a partir de las y
1
, y
2
, ..., y
n
medidas (con error com un ) aplicando el principio que acabamos de enunciar. La funci on de verosi-
militud ser a:
L(y
i
; [) =
n

i=1
1

2
e
(
1
2
(
y
i

)
2
)
(11.26)
Donde consideramos L una funci on de . Para que L sea m axima con respecto a la primera
derivada tiene que cancelarse. Puesto que tenemos un productorio es m as c omodo trabajar con la
funci on ln L que se comporta igual que L, pero es m as sencilla de derivar:
lnL

i=1
(
1
2
ln(2
2
)
1
2
(
y
i

)
2
) = 0 (11.27)
La soluci on de la ecuaci on anterior es simplemente el valor esperado para la media aritmetica:
=
1
n
n

i=1
y
i
= y (11.28)
Es decir: de acuerdo con el principio de m axima verosimilitud, el mejor estimador de la cantidad
(al que llamamos ) es precisamente, la familiar media aritmetica y. An alogamente, es inmediato
demostrar que en el caso de que cada medida tenga un error
i
diferente, el mejor estimador de es
la media pesada:
=

n
i=1
y
i
/
2
i

n
i=1
1/
2
i
(11.29)
Finalmente, en el caso de medidas distribuidas gaussianamente alrededor de un valor medio con
un error com un sigma, ambos desconocidos, obtenemos las dos ecuaciones:
ln L

=
n

i=1
(
y
i

2
) = 0 (11.30)
y
ln L
()
2
=
n

i=1
(
1
2
1

2
+
1
2
4
(y
i
)
2
) = 0 (11.31)
De las que obtenemos los estimadores:
=
1
n
n

i=1
y
i
= y (11.32)
y

2
=
1
n
n

i=1
(y
i
y)
2
(11.33)
11.4. Mnimos cuadrados
Supongamos que tenemos un conjunto de puntos x
1
, x
2
, ..., x
n
en los que realizamos las medidas
y
1
, y
2
, ..., y
n
, cada una de las cuales es independiente, con errores
1
,
2
...
n
. Los verdaderos valores
de los observables son desconocidos, pero asumimos que existe un modelo te orico que predice cada
uno de dichos valores en funci on de los puntos x
i
y de una serie de par ametros
1
,
2
, ...,
L
. Es decir,
para cada x
i
, el valor autentico del observable viene dado por una funci on f(
1
,
2
, ...,
L
; x
i
).
217
Por ejemplo, si el volumen de una muestra de un gas ideal se mantiene constante, entonces su
temperatura T es una funci on lineal de su presi on P
T = A + BP (11.34)
Donde la constante A es la temperatura en la que la presi on P sera cero (si el gas no se condensara
primero en lquido); es decir A corresponde al cero absoluto, A = 273,15
0
y B depende de la
naturaleza del gas, su masa y su volumen.
Si realizamos una serie de medidas de la temperatura T a diferentes presiones (asumiremos, por
comodidad, que el dispositivo experimental nos permite medir la presi on con un error despreciable,
mientras que en la temperatura tendremos un cierto error, distinto en principio para cada punto),
nuestra hip otesis es que el valor autentico para cada valor P
i
de la temperatura vendr a dado por la
ley fsica que estamos estudiando:
T
i
= A + BP
i
(11.35)
Que depende a su vez de los par ametros A y B, que queremos determinar. Asumimos que los
valores experimentales de temperatura t
i
medidos en cada punto, estar an distribuidos alrededor de los
valores autenticos T
i
, con errores
i
.
Si asumimos que cada una de las medidas experimentales y
i
(t
i
en nuestro ejemplo) est an dis-
tribuidas normalmente alrededor de sus autenticos valores (desconocidos, pero especicados por la
funci on f(
1
,
2
, ...,
L
; x
i
)) con varianza
2
i
(correspondiente al error en cada medida), entonces, la
verosimilitud de observar las medidas y
1
, y
2
, ..., y
n
es:
L =
n

i=1
1

2
e

1
2
(
y
i
f(x
1
,
1
,...,
l
)

i
)
2
e

1
2

n
i=1
(
y
i
f(x
1
,
1
,...,
l
)

i
)
2
(11.36)
De acuerdo con el principio de m axima verosimilitud, los valores m as probables de las cantidades
autenticas (las T
i
, en nuestro ejemplo) alrededor de las cuales se distribuyen las medidas experimen-
tales, son aquellos que hacen m axima la funci on de verosimilitud y por tanto mnima la cantidad:

2
=
n

i=1
(
y
i
f(x
1
,
1
, ...,
l
)

i
)
2
(11.37)
En nuestro ejemplo:

2
=
n

i=1
(
t
i
(A + BP
i
)

i
)
2
(11.38)
En resumen: dado un conjunto de medidas x
i
, y
i
,
i
, (i = 1, n) y un modelo que depende de una serie
de par ametros desconocidos
1
,
2
, ...
L
, de tal manera que asumimos, a) que el modelo es correcto,
es decir, que para cada punto x
i
existe un valor autentico especicado por la funci on que describe el
modelo,
i
= f(x
i
,
1
,
2
, ...
L
) y b) que las medidas experimentales y
i
est an distribuidas normalmente
alrededor de esos valores verdaderos,
i
, entonces el principio de mnimos cuadrados, prescribe que la
mejor estimaci on de los par ametros se obtiene minimizando
2
. Bajo la hip otesis de errores gaussianos,
este principio es exactamente equivalente al de m axima verosimilitud.
11.5. Mnimos cuadrados en el caso lineal
El metodo de mnimos cuadrados resulta particularmente sencillo cuando la dependencia en los
par ametros de la funci on modelo es lineal, en cuyo caso puede obtenerse una soluci on exacta para los
par ametros.
218
11.5.1. Ajuste de una recta
El caso m as sencillo de dependencia lineal en los par ametros es el de una recta. Sea un conjunto
de puntos x
i
(sin error) en los que hemos realizado las medidas y
i
con errores
i
. Si el modelo que
describe esas medidas es una recta, y = a + bx, entonces el
2
se escribe como:

2
=
n

i=1
(
y
i
a bx
i

i
)
2
(11.39)
Para minimizar el
2
cancelamos las derivadas parciales con respecto a los par ametros a y b:

2
a
= 2
n

i=1
y
i
a bx
i

2
i
= 0 (11.40)

2
b
= 2
n

i=1
x
i
(y
i
a bx
i
)

2
i
= 0 (11.41)
Lo que nos proporciona un sistema de dos ecuaciones con dos inc ognitas cuya soluci on es inmediata.
Deniendo las siguientes cantidades:
< 1 >=
n

i=1
1

2
i
< x >=

n
i=1
x
i

2
i
< y >=
n

i=1
y
i

2
i
< x
2
>=
n

i=1
x
2
i

2
i
< xy >=

n
i=1
x
i
y
i

2
i
(11.42)
Obtenemos las ecuaciones:
a < 1 > +b < x > = < y >
a < x > +b < x
2
> = < xy > (11.43)
La soluci on de este sistema es inmediata. Reescribiendolo en forma matricial, tenemos:
_
< 1 > < x >
< x > < x
2
>
__
< a >
< b >
_
=
_
< y >
< xy >
_
(11.44)
Deniendo:
V =
_
< y >
< xy >
_
P =
_
< a >
< b >
_
F =
_
< 1 > < x >
< x > < x
2
>
_
(11.45)
Obtenemos que el vector de par ametros viene dado por:
P = F
1
V (11.46)
La matriz C = F
1
se calcula trivialmente:
C =
1

_
< x
2
> < x >
< x > < 1 >
_
(11.47)
Siendo el determinante:
=< x
2
>< 1 > < x >< x > (11.48)
219
medida P (mm Hg) T (en C
0
) Error en T (en C
0
)
1 45 -100 10
2 55 -60 10
3 65 -20 7
4 75 17 5
5 85 42 5
6 95 94 10
7 105 127 10
Cuadro 11.1: Resultados de un experimento con un gas ideal.
-300
-250
-200
-150
-100
-50
0
50
100
150
0 20 40 60 80 100 120 140
T
e
m
p
e
r
a
t
u
r
a

T


[
d
e
g

C
e
l
s
.
]
Presion P (mm Hg)
Figura 11.1: Datos y ajuste para un experimento con un gas ideal
La matriz simetrica C se llama matriz de covarianza. Los terminos diagonales corresponden a
las varianzas de los par ametros, mientras que los terminos fuera de la diagonal corresponden a la
covarianza de los par ametros. Es decir:
C =
_

2
a

2
ab

2
ab

2
b
_
(11.49)
Demostraremos de manera general que, en efecto C = F
1
, en la siguiente secci on.
Volviendo al ejemplo anterior, supongamos que el resultado de un experimento de medida de la
temperatura de un gas ideal en funci on de su presi on a volumen constante han sido los valores que se
muestran en la tabla 11.1:
El ajuste por mnimos cuadrados nos permite determinar los par ametros y sus errores, obteniendose
A = 261 14, B = 3,7 0,2. Los datos y la recta ajustada se dibujan en la gura 11.1.
220
11.5.2. Las ecuaciones normales
Consideramos ahora el caso m as general en el que pretendemos ajustar un conjunto de n puntos
x
i
en los que hemos realizado las medidas y
i
con errores
i
a un modelo lineal con L par ametros:
f
i
= f
i
(
1
,
2
, ...
L
; x
i
) =
L

l=1
a
il

l
(11.50)
donde L < n y los a
il
son los coecientes que acompa nan a los par ametros. N otese que en el caso m as
general los a
il
son funciones arbitrarias de x
i
. As por ejemplo la funci on y = a sin x +b ln x +ce
x
2
, es
lineal en los par ametros a, b y c, con coecientes sinx, ln x y e
x
2
.
A continuaci on buscamos los par ametros que nos minimizan el
2
:

2
=
n

i=1
(
y
i
f
i

i
)
2
=
n

i=1
1

2
i
(y
i

l=1
a
il

l
)
2
(11.51)
Cancelando las derivadas parciales con respecto a cada par ametro,
2
/
k
obtenemos las L ecua-
ciones:

k
= 2
n

i=1
a
ik
1

2
i
(y
i

l=1
a
il

l
) = 0, k = 1, 2, ..., L (11.52)
que podemos escribir, explcitamente como:
n

i=1
a
i1
a
i1

2
i

1
+
n

i=1
a
i1
a
i2

2
i

2
+ ... +
n

i=1
a
i1
a
iL

2
i

L
=
n

i=1
a
i1
y
i

2
i
n

i=1
a
i2
a
i1

2
i

1
+
n

i=1
a
i2
a
i2

2
i

2
+ ... +
n

i=1
a
i2
a
iL

2
i

L
=
n

i=1
a
i2
y
i

2
i
.
.
.
n

i=1
a
iL
a
i1

2
i

1
+
n

i=1
a
iL
a
i2

2
i

2
+ ... +
n

i=1
a
iL
a
iL

2
i

L
=
n

i=1
a
iL
y
i

2
i
(11.53)
Que son las llamadas ecuaciones normales para los L par ametros desconocidos. Se trata de un
sistema de L ecuaciones con L inc ognitas, cuya resoluci on estudiamos a continuaci on.
11.5.3. Notaci on matricial
Denimos ahora:
y =
_
_
_
_
_
_
_
y
1
y
2
.
.
.
y
n
_
_
_
_
_
_
_

f =
_
_
_
_
_
_
_
f
1
f
2
.
.
.
f
n
_
_
_
_
_
_
_

=
_
_
_
_
_
_
_

2
.
.
.

L
_
_
_
_
_
_
_
(11.54)
Puesto que las observaciones son independientes, los errores en las cantidades observadas no est an
correlacionados y por tanto podemos escribirlos en terminos de una matriz diagonal:
221
V = V (y) =
_
_
_
_
_
_
_

2
1
0 . . . 0
0
2
2
. . . 0
. .
. .
. .
0 0 . . .
2
n
_
_
_
_
_
_
_
(11.55)
Mientras que los coecientes forman una matriz de N las por L columnas:
A =
_
_
_
_
_
_
_
a
11
a
12
. . . a
1L
a
21
a
22
. . . a
2L
. .
. .
. .
a
n1
a
n2
. . . a
nL
_
_
_
_
_
_
_
(11.56)
Con esta notaci on escribimos ahora:

f = A

(11.57)
Y para el
2
:

2
= (y A

)
T
V
1
(y A

) (11.58)
Cancelando las derivadas de
2
con respecto al vector de par ametros obtenemos:

2
= 2(A
T
V
1
y A
T
V
1
A

) = 0 (11.59)
De donde obtenemos la forma matricial de las ecuaciones normales:
(A
T
V
1
A)

= A
T
V
1
y (11.60)
Si la matriz (A
T
V
1
A) es no-singular entonces puede ser invertida y las soluciones para los par ame-
tros son:

= (A
T
V
1
A)
1
A
T
V
1
y (11.61)
Estas ecuaciones siguen siendo v alidas cuando la matriz de covarianza V es no diagonal, lo cual
ocurre cuando los errores de los puntos y
i
no son independientes.
La matriz de covarianza de los par ametros de obtiene aplicando, la ley de propagaci on de errores.
Reescribiendo la ecuaci on 11.61 como:

= S y (11.62)
Donde S = (A
T
V
1
A)
1
A
T
V
1
. Como puede verse los par ametros dependen linealmente de las
variables independientes y, veric andose entonces que:
V (

) = SV (y)S
T
(11.63)
como corresponde al caso lineal en las ecuaciones 11.19 y 11.21.
Desarrollando ahora explcitamente:
V = SV S
T
=
_
(A
T
V
1
A)
1
A
T
V
1
_
V
_
(A
T
V
1
A)
1
A
T
V
1
_
T
=
(A
T
V
1
A)
1
(A
T
V
1
V )(A
T
V
1
)
T
((A
T
V
1
A)
1
)
T
=
(A
T
V
1
A)
1
(A
T
(V
1
)
T
(A
T
)
T
)((A
T
V
1
A)
T
)
1
222
medida x y y
1 -0.6 5 2
2 -0.2 3 1
3 0.2 5 1
4 0.6 8 2
Cuadro 11.2: Datos experimentales para un ajuste parab olico.
= (A
T
V
1
A)
1
(A
T
(V
1
)
T
A)(A
T
(V
1
)
T
A)
1
= (A
T
V
1
A)
1
(11.64)
As pues, nos queda, simplemente:
C(

) = (A
T
V
1
A)
1
(11.65)
Lo que nos permite reescribir las ecuaciones 11.60 como:
C
1

= A
T
V
1
y (11.66)
y por tanto: Lo que nos permite reescribir las ecuaciones 11.60 como:

= C A
T
V
1
y (11.67)
Comparando con la ecuaci on 11.46, vemos que, en efecto, C = F
1
.
11.5.4. Ajuste de una parabola
Como ejemplo de lo anterior vamos a discutir el ajuste de una par abola
f(
1
,
2
,
3
; x) =
1
+ x
2
+ x
2

3
(11.68)
al siguiente conjunto de observaciones:
Luego tenemos L = 3 par ametros y N = 4 datos. La matriz A es de dimensi on 4 3:
A =
_
_
_
1 x
1
x
2
1
1 x
2
x
2
2
1 x
3
x
2
3
1 x
4
x
2
4
_
_
_ =
_
_
_
1 0,6 (0,6)
2
1 0,2 (0,2)
2
1 0,2 (0,2)
2
1 0,6 (0,6)
2
_
_
_ (11.69)
La matriz de covarianza de las medidas es diagonal:
V =
_
_
_

2
1
0 0 0
0
2
2
0 0
0 0
2
3
0
0 0 0
2
4
_
_
_ =
_
_
_
2
2
0 0 0
0 1
2
0 0
0 0 1
2
0
0 0 0 2
2
_
_
_ (11.70)
De ah el producto A
T
V
1
A:
A
T
V
1
A =
_
_
1 1 1 1
0,6 0,2 0,2 0,6
(0,6)
2
(0,2)
2
(0,2)
2
(0,6)
2
_
_
_
_
_
0,25 0 0 0
0 1 0 0
0 0 1 0
0 0 0 0,25
_
_
_
_
_
_
1 0,6 (0,6)
2
1 0,2 (0,2)
2
1 0,2 (0,2)
2
1 0,6 (0,6)
2
_
_
_
=
_
_
2,5 0 0,26
0. 0,26 0.
0,26 0. 0,068
_
_
223
0
2
4
6
8
10
-1 -0.5 0 0.5 1

y

x
Figura 11.2: Ajuste parab olico
La matriz A
T
V
1
A es no singular y por lo tanto puede invertirse: obteniendose la matriz de
covarianza de los par ametros:
C() = (A
T
V
1
A)
1
=
_
_
0,664 0 2,54
0. 3,85 0.
2,54 0. 24,42
_
_
(11.71)
Finalmente, los par ametros se obtienen multiplicando las matrices:

= (A
T
V
1
A)
1
A
T
V
1
y =
_
_
3,68
3,27
7,81
_
_
(11.72)
Llegamos pues a la soluci on:

f = 3,68 + 3,27x + 7,81x


2
(11.73)
Mientras que los errores en los par ametros vienen dados por la raz cuadrada de los elementos
diagonales de la matriz de covarianza C.
Los datos y la par abola ajustada se dibujan en la gura 11.2
11.6. Mnimos cuadrados en el caso no lineal
Si la funci on que queremos ajustar no depende linealmente de los par ametros, no es posible,
en general, encontrar una soluci on exacta, como lo era en el caso lineal. El procedimiento, en este
caso deber a ser iterativo. Es decir, partiremos de una soluci on aproximada que iremos mejorando
productivamente, obteniendo valores cada vez m as peque nos del
2
. El procedimiento continuar a hasta
que el
2
no vare o vare muy poco, en cuyo caso asumiremos haber encontrado el mnimo.
Una buena aproximaci on para abordar el problema consiste en asumir que, cerca del mnimo, la
funci on
2
puede aproximarse por una forma cuadr atica:

2
(

) =

+
1
2

(11.74)
224
La ecuaci on anterior surge simplemente de expandir el
2
en serie de Taylor, hasta segundo orden.
Recordemos, que en el caso escalar, la expansi on de Taylor de una funci on alrededor de un punto x
0
es::
f(x) = f(x
0
) + (x x
0
)f

(x)[
x=x
0
+
(x x
0
)
2
2
f

(x)[
x=x
0
+ ... (11.75)
Mientras que si la f = f(x), tenemos:
f(x) = f( x
0
) +

i
( x
i
x
0
)
f
x
i
[
x= x
0
+
1
2

ij
( x
i
x
0
)

2
f
x
i
x
j
[
x= x
0
( x
j
x
0
) + ... (11.76)
c = f( x
0
)

b =

i
( x
i
x
0
)
f
x
i
[
x= x
0
= f[
x
0
A = (A
ij
) =

ij

2
f
x
i
x
j
[
x
0
(11.77)
Obtenemos:
f(x) = c

b (x x
0
) +
1
2
(x x
0
)A(x x
0
) + ... (11.78)
En donde podemos ver que, al expandir hasta segundo orden alrededor al punto x
0
, obtenemos una
forma parab olica para la funci on f, donde el termino lineal es el gradiente de la funci on en el punto
alrededor del cual expandimos (cambiado de signo) mientras que el termino cuadr atico es una matriz
de derivadas parciales llamada matriz Hessiana, o simplemente Hessiano. Tomando el gradiente en la
f ormula anterior, obtenemos:
f = A (x x
0
)

b (11.79)
Si la funci on est a cerca del mnimo, entonces f = 0, lo que implica:
A (x x
0
) =

b (11.80)
Si x
0
es el mnimo aproximado, la ecuaci on anterior nos proporciona la correcci on que necesitamos
para obtener una aproximaci on mejor:
x = x
0
+ A
1

b (11.81)
Reriendonos ahora a la ecuaci on 11.74, siendo el
2
la funci on que queremos minimizar y habiendo
encontrado una soluci on aproximada
0
, obtenemos que, una mejor soluci on viene dada por la ecuaci on
11.81. Es decir:

0
+ A
1
(
2
(

0
)) (11.82)
La ecuaci on 11.82 representa la soluci on de nuestro problema, puesto que sabemos calcular tanto
el gradiente como el Hessiano del
2
. Escribiendo este explcitamente:

2
(

) =
n

i=1
_
y
i
f(x
i
;

i
_
2
(11.83)
El gradiente con respecto a los par ametros es:

k
= 2
n

i=1
(y
i
f(x
i
;

))

2
i
f(x
i
;

k
k = 1, 2, ..., L (11.84)
Tomando derivadas parciales de nuevo:
225

l
= 2
n

i=1
1

2
i
_
f(x
i
;

k
f(x
i
;

l
(y
i
f(x
i
;

2
f(x
i
;

l
_
(11.85)
Deniendo ahora:

k
=
1
2

kl
=
1
2

l
(11.86)
Podemos escribir la ecuaci on 11.82 como un sistema lineal de ecuaciones:
L

l=i

kl

l
=
k
(11.87)
donde

0
son los incrementos que tenemos que a nadir a la aproximaci on actual para obtener
la siguiente aproximaci on.
Estrictamente hablando, el problema est a resuelto. Sin embargo, podemos simplicar la forma
del Hessiano, que como podemos ver en la ecuaci on 11.85 contiene primeras y segundas derivadas
con respecto a los par ametros. Sin embargo, a menudo se suelen omitir los terminos en las derivadas
segundas. Las razones para ello son varias. En primer lugar, cuando el problema es lineal, o casi
lineal, la derivada segunda es o nula, o muy peque na. Adem as, ocurre que el termino en derivadas
segundas va multiplicado por la cantidad (y
i
f(x
i
;

)), la cual, si el modelo que queremos ajustar es


correcto, estar a distribuida, cerca del mnimo del
2
alrededor de cero (puesto que los valores experi-
mentales y
i
estar an, dentro del error, alrededor del valor verdadero f(x
i
;

)). As pues la cantidad


(y
i
f(x
i
;

)), para un modelo correcto, es, primero, peque na, y segundo, puesto que est a distribuida
como el error de los datos y esos errores tienen valores positivos y negativos, tender a a cancelarse en
la suma, lo que implica que la suma de los terminos en la segunda derivada parcial debe tender a
cancelarse. Consecuentemente, se suele escribir el Hessiano como:

kl
=
n

i=1
1

2
i
_
f(x
i
;

k
f(x
i
;

l
_
(11.88)
En resumen, el algoritmo para resolver un problema no lineal por mnimos cuadrados es el siguiente:
en primer lugar debemos obtener una estimaci on inicial de los par ametros, tan buena como sea posible
(es decir que nos lleve el
2
tan cerca como sea posible del mnimo). Esta estimaci on puede obtenerse
de diversas formas, por ejemplo, minimizando numericamente el
2
o aproximando el problema no
lineal por uno lineal (por ejemplo si queremos ajustar un crculo podemos quedarnos como primera
aproximaci on con una par abola). Una vez que tenemos la estimaci on inicial, calculamos el gradiente
y el hessiano (las matrices
k
y
kl
y resolvemos el sistema de ecuaciones 11.88 para obtener el
incremento en los par ametros que nos lleva a una mejor soluci on. Iteramos el procedimiento hasta que
el
2
no cambie, es decir, hasta llegar al mnimo.
Como ejemplo vamos a considerar el ajuste de los datos representados en la gura 11.3. Dichos
datos fueron generados a partir de la suma de dos gaussianas, a las que se a nadi o ruido:
f(x) = 5e
((x2)/3)
2
+ 2e
((x5)/3)
2
+ ruido (11.89)
Para ajustar estos datos se emple o una funci on del mismo tipo (obviamente no lineal en los par ame-
tros) que se minimiz o siguiendo el algoritmo que acabamos de describir. Explcitamente:
f(x) = c
1
e
((x
1
)/
1
)
2
+ c
2
e
((x
2
)/
2
)
2
(11.90)
El resultado del ajuste se muestra en la tabla 11.3 y se ilustra en la gura 11.4. La matriz de
covarianza de los par ametros es:
226
0
1
2
3
4
5
6
0 1 2 3 4 5 6 7 8 9 10


Figura 11.3: Datos experimentales para un ajuste de dos gaussianas
c
1

1

1
c
2

2

2
generado 5 2 3 2 5 3
ajustado 5.3 2.1 3.1 1.5 5.3 2.9
errores 0.20 0.11 0.08 0.30 0.25 0.07
Cuadro 11.3: Resultado del ajuste a dos gaussianas
C =
_
_
_
_
_
_
_
_
_
c
1

1

1
c
2

2

2
c
1
0,0401 0,0220 0,0162 0,0615 0,0502 0,0140

1
0,0220 0,0121 0,0090 0,0340 0,0276 0,0076

1
0,0162 0,0090 0,0070 0,0255 0,0204 0,0055
c
2
0,0615 0,0340 0,0255 0,0955 0,0773 0,0213

2
0,0502 0,0276 0,0204 0,0773 0,0631 0,0176

2
0,0140 0,0076 0,0055 0,0213 0,0176 0,0049
_
_
_
_
_
_
_
_
_
(11.91)
11.7. Verosimilitud del ajuste
Para comprender como puede evaluarse la verosimilitud o bondad de un ajuste por mnimos cua-
drados, es preciso reparar en que la cantidad
2
sigue una distribuci on estadstica conocida, llamada
distribuci on
2
.
En otras palabras: supongamos que realizamos repetidamente ajustes de datos a sucesivos conjun-
tos de medidas del mismo experimento, de tal manera que cada uno de estos conjuntos tiene el mismo
n umero de puntos y los errores de dichos puntos est an distribuidas normalmente. A continuaci on, di-
bujamos los valores que hemos obtenido del
2
. Dichos valores siguen una distribuci on de probabilidad
cuya forma explcita es:
f(
2
; ) =
1
2

2
(

2
)
(
2
)

2
1
e

2
2
(11.92)
227
0
1
2
3
4
5
6
0 1 2 3 4 5 6 7 8 9 10


Figura 11.4: Ajuste de dos gaussianas
Dicha distribuci on depende del par ametro , el n umero de grados de libertad, = N L, donde
N es el n umero de puntos experimentales y L el n umero de par ametros en el ajuste. La funci on ()
que aparece en la f ormula 11.92 es la funci on Gamma, cuya denici on es:
() =
_

0
dt t
1
e
t
(11.93)
La forma de la distribuci on
2
para diferentes grados de libertad se muestra en la gura 11.5.
N otese que la distribuci on es mon otona decreciente para < 3 y a presenta un m aximo para 3.
Para grados de libertad peque nos, la distribuci on es muy asimetrica, tendiendo a una gaussiana para
grados de libertad grandes (comp arense la gura 11.5 a y b). El valor medio de cada una de las curvas
que se muestra, corresponde precisamente al n umero de grados de libertad. Concretamente:
x

2 (11.94)
donde x

) es el valor medio (error) de la distribuci on


2
con grados de libertad.
El hecho de que el valor medio m as probable de la distribuci on
2
coincida con el n umero de grados
de libertad, nos permite establecer un primer criterio para estimar la bondad del ajuste, a saber: un
ajuste es aceptable cuando la cantidad
2
/ 1. En el lmite de grande, la condici on anterior tiene
una interpretaci on muy intuitiva. Los valores del
2
se distribuyen normalmente, con media y error

2 y por lo tanto, al 68 % de nivel de conanza esperamos que el


2
resultante en un experimento
concreto,
2
o
, este comprendido en el intervalo

2 <
2
o
< +

2.
Cuando decidimos rechazar un determinado ajuste? En el caso de que sea grande, podemos
simplemente aplicar los mismos criterios probabilsticos que si la distribuci on
2
fuera una gaussiana.
Por ejemplo, si

2 <
2
o
< +

2 el ajuste es aceptable con un 68 % de probabilidad, si


2

2 <
2
o
< + 2

2 el ajuste es aceptable con un 95 % de probabilidad, mientras que si


3

2 <
2
o
< + 3

2 el ajuste es aceptable con un 99 % de probabilidad (probabilidades


gaussianas para 1, 2 y 3, respectivamente. En otras palabras, si
2
o
est a dentro del intervalo

2 <
2
o
< +

2, el ajuste es aceptable siempre, si est a fuera del intervalo de 1 pero dentro del
de 2, 2

2 <
2
o
< + 2

2 el ajuste es todava aceptable (con una probabilidad entre el 5 %


y el 30 %) si est a fuera del intervalo de 2 y dentro del de 3. 3

2 <
2
o
< + 3

2, el ajuste
es marginalmente aceptable, con una probabilidad entre el 1 y el 5 % y nalmente, si est a fuera del
228
0
0.2
0.4
0.6
0.8
1
0 5 10 15 20

p
d
f
Chi2
0
0.02
0.04
0.06
0.08
0.1
0 5 10 15 20 25 30 35 40 45 50

p
d
f
Chi2
Figura 11.5: f(
2
; ) para a) = 1, 2, 3, 4, 5, 7,b) = 10, 20, 30.
intervalo de 3 el ajuste suele rechazarse (aunque en algunos casos una probabilidad del 1 % puede
considerarse aceptable).
De una manera m as general, e independientemente de cual sea el n umero de grados de libertad
(es decir, sin estar restringidos al caso de grande, en el que el
2
es gaussiano), podemos denir
una probabilidad del
2
, P(
2
>
2
0
), de la siguiente manera: P(
2
>
2
0
) es la probabilidad de que,
habiendo observado un chi-cuadrado
2
0
para un cierto experimento, observemos, repitiendo dicho
experimento en similares condiciones, un valor
2
>
2
0
.
De esta denici on se deduce que si P(
2
>
2
0
) es grande, el ajuste es aceptable (n otese que en el
caso imposible de obtener
2
0
= 0, que correspondera a un modelo exacto sobre unos datos sin error,
P(
2
>
2
0
) sera siempre igual a su valor m aximo, es decir, uno), mientras que deberemos rechazar
ajustes correspondientes a un valor bajo de P(
2
>
2
0
), (as, por ejemplo, rechazaremos, al 1 % de
nivel de conanza, ajustes con un valor P(
2
>
2
0
) < 0,01)
P(
2
>
2
0
) se calcula f acilmente, como el complementario de la probabilidad acumulativa del
2
P(
2
>
2
0
) = 1 F(
2
; ) (11.95)
donde,
F(
2
; ) =
_

2
0
f(
2
; ) (11.96)
La funci on F (y por la tanto P) puede escribirse explcitamente en terminos de la llamada funci on
Gamma incompleta, que se dene como:

P
(a, x) =
1
(a)
_
x
0
e
t
t
a1
dt a > 0 (11.97)
Haciendo el cambio de variable en a = /2, t =
2
/2, dt = d
2
/2:
F(
2
; ) = 2
_
t
0
f(2t; 2a)dt
= 2
_
t
0
1
2
a
(a)
(2t)
a1
e
t
dt
=
2
2(a)
_
t
0
t
a1
e
t
dt
229
0.001
0.01
0.1
1
1 10

C
D
F
Chi2
0.001
0.01
0.1
1
1 10

C
D
F
Chi2
Figura 11.6: F(
2
0
; ) para a) = 1, 2, 3, 4, 5, 7,b) = 10, 20, 30.
0.001
0.01
0.1
1
1 10

P
r
o
b

(
C
h
i
2
)
Chi2
0.001
0.01
0.1
1
1 10

P
r
o
b

(
C
h
i
2
)
Chi2
Figura 11.7: P(
2
>
2
0
) para a) = 1, 2, 3, 4, 5, 7,b) = 10, 20, 30.
=
P
(

2
,

2
2
)
y por lo tanto:
P(
2
>
2
0
) = 1
P
(

2
,

2
2
) (11.98)
Los valores de la funci on F(
2
0
; ) y la funci on P(
2
>
2
0
), para distintos grados de libertad, se
muestran en las guras 11.6 y 11.7 (n otese la escala logartmica).
La tabla 11.4 muestra los valores de P(
2
>
2
0
) (en %) para distintos valores del chi-cuadrado
reducido y el n umero de grados de libertad. Observese que la probabilidad de encontrar un valor
del chi-cuadrado reducido mucho m as bajo que uno es muy alta, lo que parece inconsistente con el
hecho intuitivo de que valores del
2
mucho m as peque nos que la media deben ser (al menos en el
lmite de grande) tan improbables como los valores muy grandes. En realidad no hay contradicci on,
en el sentido de que, como la notaci on indica, P(
2
>
2
0
) nos proporciona tan s olo la probabilidad
de que un modelo correcto resulte en un valor chi-cuadrado m as alto que un determinado n umero
(
2
0
). La probabilidad de que el chi-cuadrado de un modelo correcto sea m as alto o m as bajo que un
230
/2
0
/ 0.1 .25 .5 .75 1. 1.25 1.5 1.75 2.0
1 75.2 61.7 48.0 38.6 31.7 26.4 22.1 18.6 15.7
2 90.5 77.9 60.7 47.2 36.8 28.7 22.3 17.4 13.5
3 96.0 86.1 68.2 52.2 39.2 29.0 21.2 15.4 11.2
4 98.2 91.0 73.6 55.8 40.6 28.7 19.9 13.6 9.2
5 99.2 94.0 77.6 58.6 41.6 28.3 18.6 11.9 7.5
10 100.0 99.1 89.1 67.8 44.0 25.3 13.2 6.4 2.9
15 100.0 99.8 94.2 73.5 45.1 22.5 9.5 3.5 1.2
25 100.0 100.0 98.2 80.9 46.2 18.1 5.2 1.2 0.2
30 100.0 100.0 99.0 83.5 46.6 16.3 3.9 0.7 0.1
50 100.0 100.0 99.9 90.4 47.3 11.0 1.3 0.1 0.0
Cuadro 11.4: P(
2
>
2
0
) para varios valores del
2
reducido (
2
/ y del n umero de grados de libertad
.
determinado n umero s olo se puede calcular correctamente en el caso de grande, donde se puede
integrar a izquierda y derecha de la distribuci on (que para grande es simetrica).
Dicho de otra manera, cuando tenemos muy pocos grados de libertad, s olo es factible decidir
si nuestro ajuste es inverosmil, con cierto nivel de conanza. Por ejemplo, reriendonos a la tabla
11.4, con un grado de libertad, un ajuste que arrojara un chi-cuadrado reducido de 2 sera todava
perfectamente aceptable, ya que el 16 % de las veces un modelo correcto resultar a en este valor.
Hara falta valores del chi-cuadrado reducido mucho mayores (de 3 o m as) para que el ajuste fuera
razonablemente inverosmil (P(
2
>
2
0
) del orden de 1 % o menor). Adem as, si obtenemos un chi-
cuadrado reducido muy bajo, no podemos hacer otra cosa que armar que el ajuste no es inverosmil.
Todo lo cual es l ogico, porque con un grado de libertad, estamos imponiendo muy pocas restricciones
sobre el ajuste (el ejemplo sera el de una recta que pasa por 3 puntos o una par abola que pasa por
cuatro). En cambio, si consideramos 50 grados de libertad, veremos en la tabla que la probabilidad de
que el chi-cuadrado reducido sea 2.0 es menor que 0.1. Por otra parte, recordando que para 50 grados
de libertad el chi-cuadrado es una gaussiana de media 50 y desviaci on est andar 10, podemos estimar
la probabilidad de obtener un chi-cuadrado reducido menor que un determinado valor. Por ejemplo,
la probabilidad de obtener un valor
2
/ < 0,2 sera en este caso menor del 1 %.
Para terminar comentaremos el sentido intuitivo de la regla aproximada que requiere que el chi-
cuadrado reducido sea del orden de uno. En el lmite de muchos puntos experimentales (en el lmite
para ser exactos de muchos grados de libertad), esperamos que, si el modelo que queremos ajustar
es el correcto, los puntos experimentales esten distribuidos en cada x
i
, de acuerdo a una gaussiana
centrada en f(x
i
, ), con error . Es decir, para cada punto, la cantidad y
i
f(xi, ) y por tanto
la cantidad (y
i
f(xi, ))/ 1 de ah que el chi-cuadrado (que es la suma de esas cantidades sobre
los puntos) deba ser del orden del n umero de puntos (que coincide con el n umero de grados de libertad
en el lmite de muchos puntos).
De acuerdo con esta interpretaci on Si
2
/ >> 1 debemos esperar que nuestro modelo no sea
correcto. En cambio, si
2
/ << 1 lo que est a ocurriendo probablemente es que estamos sobreesti-
mando los errores experimentales.
11.8. Programaci on de algoritmos de ajustes por mnimos cua-
drados
Nuestro objetivo es programar un algoritmo que nos permita ajustar una determinada funci on
modelo a un conjunto de datos, x, y,
y
, donde
y
es el error en la variable dependiente y. Consideramos
que el error en la variable independiente x es despreciable. Ya comentamos en su momento la clase
231
CDatos, que utilizaremos para representar este conjunto.
En nuestro tratamiento numerico del problema de ajustes por mnimos cuadrados consideraremos
tan s olo, por simplicidad, el caso de dependencia lineal en los par ametros. Asumiremos as mismo,
que la matriz de covarianza de los datos es diagonal, es decir que los errores de estos no est an corre-
lacionados.
La clase de datos DMatriz
La representaci on de una matriz diagonal requiere mucho menos espacio que la representaci on de
una matriz arbitraria (basta con un vector de dimensi on n, para almacenar la diagonal, en lugar de
un arreglo de dimensi on n
2
para una matriz cuadrada arbitraria de la misma dimensi on). Adem as
las operaciones que involucran matrices diagonales (tales como suma, resta, multiplicaci on, etc.) son
mucho m as sencillas (en particular la inversi on de una matriz diagonal es trivial). Resulta, por tanto,
conveniente, crear un nuevo tipo para representarlas. La clase DMatriz (dmatriz.h, dmatriz.cpp) se
ocupa de este menester.
class DMatriz
private:
RVector* pV; // Vector con los t erminos diagonales
int pDim; // dimensi on de la matriz
public:
// Constructor por defecto (matriz diagonal 1x1)
DMatriz();
// Constructor est andar (matriz identidad de nxn)
DMatriz(int n);
// Constructor a partir de un vector (que tomamos en la diagonal)
DMatriz(const RVector& v);
// Constructor a partir de un vector din amico (que tomamos en la diagonal)
DMatriz(const DVector& v);
// Constructor a partir de una matriz (tomamos la diagonal de la matriz)
DMatriz(const RMatriz& m);
// Constructor de copia
DMatriz(const DMatriz& m);
// Destructor
~DMatriz();
// Dimensi on de la matriz
int dimension() const;
// Devuelve la diagonal en un vector
const RVector& diagonal() const;
// Operadores
// lectura (ejemplo, cout << a(i,i))
const double& operator () (int i) const;
// asignaci on (ejemplo, a(i,i) = 7.)
double& operator () (int i);
// Operador = entre matrices diagonales
DMatriz& operator = (const DMatriz& m);
DMatriz& operator += (const DMatriz& m);
232
DMatriz& operator -= (const DMatriz& m);
DMatriz& operator *= (double a);
;
Como vemos, una matriz diagonal puede construirse especicando la dimensi on de la diagonal, o
bien a partir de vectores (tanto los tipos RVector como DVector) u otras matrices, diagonales o no.
La denici on de la clase incluye todas las operaciones habituales internas. Tambien se han denido
los operadores y funciones externas:
/* Operaciones con matrices diagonales */
DMatriz operator - (const DMatriz& m);
DMatriz operator + (const DMatriz& m1, const DMatriz& m2);
DMatriz operator - (const DMatriz& m1, const DMatriz& m2);
DMatriz operator * (const DMatriz& v1, double d);
DMatriz operator * (double d, const DMatriz& v1);
DMatriz operator * (const DMatriz& m1, const DMatriz& m2);
// Operaciones entre matrices diagonales y reales
RMatriz operator * (const DMatriz& m1, const RMatriz& m2);
RMatriz operator * (const RMatriz& m1, const DMatriz& m2);
RMatriz operator + (const DMatriz& m1, const RMatriz& m2);
RMatriz operator + (const RMatriz& m1, const DMatriz& m2);
RMatriz operator - (const DMatriz& m1, const RMatriz& m2);
RMatriz operator - (const RMatriz& m1, const DMatriz& m2);
DMatriz inversa(const DMatriz& m);
int maximo(const DMatriz& m);
int minimo(const DMatriz& m);
ostream& operator << (ostream& s, const DMatriz& m);
que se ocupan de denir las operaciones entre matrices diagonales (y no diagonales). Finalmente, se
han a nadido operaciones entre vectores y matrices no diagonales (en linalg.cpp, linalg.h):
// Para matrices diagonales
// m(i,j) * vt(j) = v(i)
RVector operator * (const DMatriz& m, const RVector& v);
// v(i)*m(i,j) = v(j)
RVector operator * (const RVector& v, const DMatriz& m);
Biblioteca de

Algebra Lineal
A lo largo del curso hemos desarrollado una considerable artillera relacionada con el manejo de
vectores y matrices, as como las diversas operaciones de algebra lineal entre estos, que vamos a utilizar
en el algoritmo de ajuste de datos por mnimos cuadrados, implementado en formalismo matricial.
En consecuencia, hemos creado un nuevo directorio, AlgebraLineal, donde se ha colocado la versi on
m as completa y actualizada de las clases de algebra lineal (matrices, vectores y los algoritmos que
los manipulan). El directorio incluye una Makefile que permite compilar todo el c odigo, creando una
biblioteca de objetos (libAL.a) con la que puede enlazarse cualquier c odigo que necesite utilizar estos
tipos.
233
El algoritmo MCdos
El ajuste de un conjunto de datos por mnimos cuadrados resulta muy sencillo en el caso de
dependencia lineal en los par ametros. Hemos dise nado el algoritmo Mcdos (mcdos.h, mcdos.cpp) para
resolver este tipo de problemas.
typedef RVector (*fv) (double) ;
class MCdos: public Algoritmo
private:
int pN; //N umero de puntos experimentales
int pM; // N umero de par ametros del ajuste
double pChi2; // chi2 del ajuste
RMatriz* pC; // Matriz de covarianza de los par ametros (mxm);
RVector* pX; // Vector de abcisas (n)
RVector* pY; // Vector de ordenadas (n)
RVector* pP; // Vector de par ametros (m)
DMatriz* pV; // Matriz de covarianza de los datos (nxn);
// pV se toma como matriz diagonal
fv pF; // Funci on modelo
// Realiza un ajuste por m nimos cuadrados en el caso lineal,
// devuelve el vector de par ametros y la matriz de covarianza
// como referencias y el chi2 en el tipo devuelto por la funci on
double mclineal(fv f, const RVector& x, const RVector& y,
DMatriz& V, RVector& p, RMatriz& C);
// Ejecuta una acci on de la m aquina
// (ajusta por m nimos cuadrados si el interruptor est a abierto)
void accion();
public:
// Constructor (m es el n umero de par ametros del ajuste)
MCdos(int m);
// Destructor
~MCdos();
//Establece el modelo
void eModelo(fv f);
//Establece los datos
void eDatos(const CDatos& datos);
//Acciones
// Devuelve/Calcula el vector de par ametros
RVector& param();
// Devuelve/Calcula la matriz de covarianza de los par ametros
RMatriz& covar();
//Devuelve/Calcula el chi2
double chi2();
//Devuelve/Calcula el n umero de grados de libertad
int nu();
;
234
El algoritmo acepta un conjunto de datos (especicado por una variable de tipo CDatos) y un
modelo, especicado por un puntero a una funci on que toma un doble como argumento y devuelve un
vector de dobles:
typedef RVector (*fv) (double) ;
Esta funci on es necesaria para evaluar los coecientes de los par ametros. Recordemos, que en el
caso de dependencia lineal en estos, la funci on modelo se puede escribir como:
f(x) = f
1
(x)
1
+ f
2
(x)
2
+, ..., +f
m
(x)
n
(x)
donde las f
1
, f
2
, ..., f
m
son funciones que multiplican los m par ametros de los que depende la funci on.
Por ejemplo, para una recta:
f(x) =
1
x + b
y por tanto f
1
(x) = x, f
2
(x) = 1. Para una par abola:
f(x) =
1
x
2
+
2
x +
3
y por tanto f
1
(x) = x
2
, f
2
(x) = x, f
3
(x) = 1. Conviene insistir que la forma de las f
i
es arbitraria,
ya que la dependencia lineal del modelo es en los par ametros, no en la variable independiente x. Por
ejemplo, el modelo:
f(x) =
1
sin
3
(x
2
) +
2
e

x
+
3
log(1/x)
depende linealmente de los par ametros
1
,
2
,
3
, a pesar de que f
1
(x) = sin
3
(x
2
), f
2
(x) = e

x
y
f
3
(x) = log(1/x) sean funciones fuertemente no lineales.
El c alculo de los par ametros del modelo requiere evaluar cada una de las m funciones en cada uno
de los n puntos experimentales. De ah que el modelo venga especicado por una funci on que toma
un doble como argumento (el punto donde queremos evaluar las funciones) y devuelve un vector (de
dimensi on m) correspondiente a la evaluaci on de las funciones en dicho punto.
El algoritmo ejecuta cuatro posibles acciones. Devuelve el vector de par ametros, la matriz de cova-
rianza, el
2
y el n umero de grados de libertad del ajuste = nm. Cada una de estas acciones implica
el c alculo del ajuste si se han establecido nuevos datos, devolviendose la informaci on almacenada en
las variables internas en otro caso. El n ucleo del algoritmo es la funci on:
double mclineal(fv f, const RVector& x, const RVector& y,
DMatriz& V, RVector& p, RMatriz& C);
que toma como argumentos el modelo que desea ajustarse (fv), as como referencias constantes al
vector de variables independientes x, ((ls-prog.tex el vector de variables dependientes y, as como la
matriz de covarianza de los datos V. La funci on permite calcular el vector de par ametros p, =======
el vector de variables dependientes y y la matriz de covarianza de los datos V. La funci on calcula el
vector de par ametros p, ))1.2 y la matriz de covarianza de los par ametros C (ambas cantidades se
pasan como referencias en los argumentos) y devuelve el chi2 del ajuste.
La implementaci on de esta funci on, utilizando formalismo matricial es extremadamente simple.
Recordemos que el vector de par ametros viene dado por:

= C(A
T
V
1
)y
donde C es la matriz de covarianza de los par ametros:
C(

) = (A
T
V
1
A)
1
mientras que V es la matriz de covarianza de los datos y A la matriz de coecientes obtenida al evaluar
cada uno de los n puntos con cada una de las m funciones que multiplican los par ametros. El chi2 es,
simplemente:

2
= (y A

)
T
V
1
(y A

)
235
La implementaci on de la funci on mclineal es una transcripci on directa de las f ormulas anteriores,
lo cual resulta posible, insistimos, debido al alto grado de abstracci on que el lenguaje nos permite
introducir:
double MCdos::mclineal(fv f, const RVector& x, const RVector& y,
DMatriz& V, RVector& p, RMatriz& C)

// Creamos la matriz de coeficientes A


RMatriz A(pN,pM);
for (int i = 1; i<= pN; i++)
// Guardamos las im agenes de las funciones que multiplican
// a los par ametros del ajuste en el vector p;
// p(1)=f1(x(i), p(2)=f2(x(i)....p(m)=fm(xi)
// donde el modelo es
//f = f1(x)*theta1 + f2(x)*theta2 +...+fm(x)*thetam
p = f(x(i));
// Y copiamos en la matriz de coeficientes
copiaFila(A, p, i);

// Usamos el espacio de V para almacenar su inversa


// a fin de ahorrar operaciones
V = inversa(V);
// Calculamos la inversa de la matriz de covarianza
C = traspuesta(A)*V*A;
// Y la invertimos
C = inversa(C);
// Calculamos el vector de par ametros
p = C*traspuesta(A)*V*y;
// Y por ultimo el chi2
double chi2 = (y - A*p)*V*(y - A*p);
cout << "chi2 = " << chi2 << endl;
return chi2;

La distribuci on de probabilidad Chi2


Como hemos estudiado, la verosimilitud de un ajuste puede estimarse a partir de la forma de la
distribuci on
2
. Concretamente, la funci on P(
2
>
2
0
) es la probabilidad de que, habiendo observado
un chi-cuadrado
2
0
para un cierto experimento, observemos, repitiendo dicho experimento en similares
condiciones un valor
2
>
2
0
.
236
Vimos que si P(
2
>
2
0
) es grande, el ajuste es aceptable (n otese que en el caso imposible
de obtener
2
0
= 0, que correspondera a un modelo exacto sobre unos datos sin error, P(
2
>

2
0
) sera siempre igual a su valor m aximo, es decir, uno), mientras que deberemos rechazar ajustes
correspondientes a un valor bajo de P(
2
>
2
0
), (as, por ejemplo, rechazaremos, al 1 % de nivel de
conanza, ajustes con un valor P(
2
>
2
0
) < 0,01).
La funci on P(
2
>
2
0
) se obtiene a partir de la funci on Gamma, como:
P(
2
>
2
0
) = 1
P
(

2
,

2
2
)
A n de proporcionar las herramientas necesarias para calcular la verosimilitud de un ajuste hemos
creado el directorio de funciones especiales, FuncionesEspeciales, donde se han incluido la funci on
Gamma, la p.d.f.
2
as como la funci on P(
2
>
2
0
). La declaraci on de la funci on gamma se encuentra
en la cabecera gammap.h en dicho directorio:
// Funci on gamma incompleta complementaria
double GammaP(double a, double x);
// Funci on gamma logar tmica
double gammln(double x);
// Funciones auxiliares
void gcf(double& gammcf, double a, double x, double& gln);
void gser(double& gamser, double a, double x, double& gln);
mientras que las declaraciones de las funciones Chi2 y Chi2P se encuentran en chi2.h:
// Distribuci on Chi2
// f(chi2,nu) = (1/(2^nu/2Gamma(nu/2))*(chi2)^(nu/2)-1exp(-chi2)
double Chi2(double chi2, double nu);
// Chi2 acumulativa
double Chi2F(double chi02, double nu);
// Probabilidad del Chi2
// P(chi02, nu) = 1. -F(chi02, nu)
double Chi2P(double x, double nu);
Se trata de funciones con alcance global (no pertenecen a una clase) y pueden por lo tanto utilizarse
sin m as que incluir las correspondientes cabeceras y enlazar con la biblioteca de funciones especiales
(que podemos crear sin m as que ejecutar la Makefile en el correspondiente directorio).
El programa de ejemplo xajuste
El programa xajuste.cpp ilustra como ajustar un conjunto de datos utilizando el algoritmo Mcdos:
RVector recta(double x)
// y = f1(x)*a + f2(x)*b = a*x + b
// f1(x) = x; f2(x) = 1;
RVector ff(2);
237
ff(1) =x;
ff(2) = 1.;
return ff;

RVector parabola(double x)
// y = f1(x)*a + f2(x)*b + f3(x)*c = a*x^2 + b*x + c
// f1(x) = x*x; f2(x) = x; f3(x)=1
RVector ff(3);
ff(1) =x*x;
ff(2) =x;
ff(3) = 1.;
return ff;

RVector FF(double x)
// y = f1(x)*a + f2(x)*b + f3(x)*c + f4(x)*d
// = a*exp(x) + b*x^3 + c*sin(x)+d*cos(x)
// f1(x) = exp(x); f2(x) = x^3; f3(x)=sin(x) f4(x)=cos(x)
RVector ff(4);
ff(1) =exp(x);
ff(2) =cubo(x);
ff(3) = sin(x);
ff(4) = cos(x);
return ff;

int main ()

// Lee datos a interpolar


string fdatos;
cout << "fichero de datos a ajustar? "; cin >> fdatos;
int m;
cout << "N umero de par ametros del ajuste? "; cin >> m;
CDatos cd;
cd.lee(fdatos);
cout << "xi = " << cd.x() << endl;
cout << "yi = " << cd.y() << endl;
cout << "eyi = " << cd.ey() << endl;
// Crea una m aquina de ajustar por m nimos cuadrados
MCdos xmc(m);
// Establece datos
xmc.eDatos(cd);
cout << " Este programa ajusta una funci on modelo"
<<" a un conjunto de "<<cd.dimension()<<" puntos"
<< endl;
// establece el modelo
238
cout << "modelo a ajustar (1) recta (2) parabola (3) F?";
int imodel;
cin >> imodel;
if (imodel == 1)
xmc.eModelo(recta);
else if (imodel == 2)
xmc.eModelo(parabola);
else if (imodel == 3)
xmc.eModelo(FF);
else
error("teclee un n umero de 1 a 3", EPROCESO);
// Par ametros:
RVector par = xmc.param();
cout << "Par ametros obtenidos por el ajuste" << par << endl;
//Matriz de Covarianza
RMatriz C = xmc.covar();
cout << "Matriz de covarianza" << C << endl;
// Chi2 por grados de libertad;
double chi2 = xmc.chi2()/xmc.nu();
// Probabilidad del Chi2
double chi2P = Chi2P(xmc.chi2(),xmc.nu());
cout << "Chi2 = " << chi2 << "Chi2 prob = " << chi2P << endl;
return 0;

El programa permite introducir tres modelos, una recta, una par abola o bien una funci on de usua-
rio. A continuaci on lee un conjunto de datos (que puede generarse previamente con el programa
xgendat.cpp) y lo ajusta, escribiendo los resultados del ajuste por pantalla.
Por ejemplo:
$ ./xgendat.exe
?Cuantos puntos quiere generar? 20
Introduzca rango [xmin,xmax]
xmin? 0
xmax? 4
Introduzca error medio de los puntos
sigma? 1.
Introduzca dispersi on de los errores
dispersi on? 0.1
introduzca un entero para inicializar el generador
de n umeros aleatorios112345
Qu e funci on desea?
Opciones: (1 = a*x+b, 2 = a*x**2+b*x+c, 3 = F)2
Introduzca a,b,c
a? 4.
b? 2.
c? 1.
El programa genera 20 puntos (que almacena en el chero parabola.d) con error promedio de una
unidad (el error en s mismo vara de unos puntos a otros de acuerdo con la dispersi on del error, de
0.1) a partir de una par abola y = ax
2
+ bx + c con par ametros a = 4,b = 2 y c = 1.
239
A continuaci on ejecutamos el programa xajuste.exe:
$ ./xajuste.exe
fichero de datos a ajustar? parabola.d
N umero de par ametros del ajuste? 3
Este programa ajusta una funci on modelo a un conjunto de 20 puntos
modelo a ajustar (1) recta (2) parabola (3) F?2
Par ametros obtenidos por el ajuste( 3.9 2 1.5 )
Matriz de covarianza
| 0.029 -0.11 0.071 |
| -0.11 0.49 -0.35 |
| 0.071 -0.35 0.36 |
Chi2 = 0.75Chi2 prob = 0.75
El programa lee un chero con los datos (el que hemos generado previamente, parabola.d). Debe-
mos especicar adem as el modelo que queremos ajustar (en este caso una par abola) y el n umero de
par ametros del modelo. El resultado del ajuste es el vector de par ametros (en el ejemplo obtenemos
a = 3,9, b = 2, c = 1, 5 y la matriz de covarianza de estos, (los terminos diagonales son los cuadrados
de los errores de los par ametros). Tambien obtenemos el chi2 por grados de libertad y su probabilidad,
que indican que el ajuste es verosmil.
Los datos y el ajuste se muestran en la gura 11.8.
-10
0
10
20
30
40
50
60
70
80
0 0.5 1 1.5 2 2.5 3 3.5 4
"parabola.d"
p(x)
Figura 11.8: Ajuste una par abola
240
Bibliografa
[1] F.B. Hildebrand Introduction to Numerical Analysis, McGraw-Hill (New York, 1964).
[2] J. Stoer and R. Burlisch, Introduction to Numerical Analysis, Second Edition, Springer, 1991.
[3] D. Kincaid and W.Cheney, Numerical Analysis, Second Edition, Brooks & Cole, 1996.
[4] D. Kincaid and W.Cheney, Numerical Mathematics and Computing, Fourth Edition, Brooks &
Cole, 1999.
[5] W.H. Press et al, Numerical Recipes. Existen variantes en C, FORTRAN y PASCAL. Cambridge
University Press.
[6] GNU Scientic Library http://www.gnu.org
[7] R. Guardiola, E. Hig on y J. ROs, M`etodes Num`erics per a la fsica, Universidad de Valencia.
[8] B. Stroustrup, The C++ Programming Language, Addison-Wesley, third or special edition.
Stroustrup es el autor del lenguaje C++ y su libro presenta una introducci on completsima,
muy rigurosa y ay!, bastante difcil, al C++.
241

Вам также может понравиться