Búsqueda lineal y búsqueda binaria

9 07 2009

Los algoritmos de búsqueda lineal y binaria son 2 de los algoritmos más usados para encontrar elementos en una estructura de datos.

La búsqueda lineal probablemente es sencilla de implementar e intuitiva. Básicamente consiste en buscar de manera secuencial un elemento, es decir, preguntar si el elemento buscado es igual al primero, segundo, tercero y así sucesivamente hasta encontrar el deseado. Entonces este algoritmo tiene una complejidad de O(n).

La búsqueda binaria al igual que otros algoritmos como el quicksort utiliza la técnica divide y vencerás. Uno de los requisitos antes de ejecutar la búsqueda binaria, es que el conjunto de elementos debe de estar ordenado. Supongamos que tenemos el siguiente array.

57 53 21 37 17 36 22 3 44 97 89 26 31 47 8 17

Debemos ordenarlo
3 8 17 17 21 22 26 31 36 37 44 47 53 57 89 97

¿Como funciona la búsqueda binaria?
Necesitamos una seria de datos para realizar la búsqueda: El elemento en la posición inicio, fin, medio. Y porsupuesto el tamaño del vector y elemento que queremos buscar.

inicio = 0;
fin = tam – 1;
medio = (inicio + fin) / 2;

Preguntamos si el elemento buscado es igual al elemento que se encuentra en la posicion medio del array, si es afirmativo ya encontramos el elemento y no hay nada más que hacer.

if(buscado == array[medio]){
  cout << "Se encuentra en la posicion " << medio + 1 << endl;
  return array[medio];
}

Si no es así. Preguntamos si el elemento que se encuentra en la posición medio es mayor al elemento buscado, si es afirmativo y como el array está ordenado, quiere decir que elemento buscado es menor al del medio. Entonces ahora solo buscaríamos en esa división del array,
por lo tanto, el fin ahora sería el elemento anterior al medio.

if(array[medio] > buscado){
  fin = medio - 1;
}

De lo contrario quiere decir que elemento buscado es mayor al del medio, entonces debemos buscar en la otra división del array, por lo tanto el inicio sería el elemento posterior al medio.

else {
  inicio = medio + 1;
}

Con cualquiera de estas 2 operaciones descartamos la otra mitad del array y por lo tanto reducimos notablemente el número de iteraciones.
Por lo tanto, en cada iteración, la búsqueda se reduce a la mitad del array. Realizamos esto en un bucle mientras el inicio sea menor o igual al fin.

El algoritmo de búsqueda binaria tiene una complejidad de O(log n).

Veamos la implementación de la búsqueda lineal y binaria en C++.

Nota: Para realizar la búsqueda binaria, estoy incluyendo el archivo que contiene el algoritmo de ordenamiento quicksort, para no volver a escribirlo y así reutilizar código.
#include “quicksort.h”

Aquí tienen el código del algoritmo y una breve explicación. Retiren el main para utilizarlo en otros programas.

#include <iostream>
#include "quicksort.h"

using namespace std;

int busqueda_lineal(int *array,int buscado,int tam)
{
    for(int i=0;i<tam;i++){
        if(buscado==array[i]){
          cout << "Se encuentra en la posicion " << i+1 << endl;
        }
   }
}

int busqueda_binaria(int *array, int buscado, int tam)
{
    int medio, inicio, fin;
    inicio = 0;
    fin = tam-1;

    while(inicio <= fin){
      medio = (inicio + fin) / 2;

      if(buscado == array[medio]){
        cout << "Se encuentra en la posicion " << medio + 1 << endl;
        return array[medio];
      } else{
          if(array[medio] > buscado){
            fin = medio - 1;
          } else {
            inicio = medio + 1;
          }
      }
    }
    return -1;
}

void imprimir(int *array,int tam)
{
   for(int i=0;i<tam;i++){
        cout << array[i] << "  ";
   }
   cout << endl << endl;
}

int main()
{
    int tam;
    int buscado;
    cout << "Ingresa el tamanyo del array" << endl;
    cin >> tam;

    int array[tam];

    srand(time(NULL));
    for(int i=0;i<tam;i++){
        array[i] = rand() % 100;
    }

    cout << endl;
    cout << "Array al inicio " << endl;
    imprimir(array,tam);

    cout << "Ingresa el elemento a buscar (Busqueda lineal) ";
    cin >>buscado;
    busqueda_lineal(array,buscado,tam);

    cout << "Array ordenado " << endl;
    quicksort(array,0,tam);
    imprimir(array,tam);

    cout << "Ingresa el elemento a buscar (Busqueda binaria) ";
    cin >>buscado;
    busqueda_binaria(array,buscado,tam);

    return 0;
}

Ejemplo de la salida del programa:

Ingresa el tamanyo del array
16

Array al inicio
57  53  21  37  17  36  22  3  44  97  89  26  31  47  8  17

Ingresa el elemento a buscar (Busqueda lineal) 36
El elemento se encuentra en la posicion 6
Array ordenado
3  8  17  17  21  22  26  31  36  37  44  47  53  57  89  97

Ingresa el elemento a buscar (Busqueda binaria) 89
El elemento se encuentra en la posicion 15




Listas enlazadas – Clase Lista,Nodo en c++

4 07 2009

Una lista es una estructura de datos que nos permite agrupar elementos de una manera organizada. Las listas al igual que los algoritmos son importantísimas en la computación y críticas en muchos programas informáticos.

Las listas están compuestas por nodos, estos nodos tienen un dato o valor y un puntero a otro(s) nodo(s).

Existen varios tipos de listas: Simplemente enlazada, doblemente enlazada, circular simplemente enlazada, circular doblemente enlazada.

Vamos a revisar las listas enlazadas simples, por ser el punto de partida y fundamentales para poder entender las otras.

Una lista enlazada tiene un conjunto de nodos, los cuales almacenan 2 tipos de información: El dato que contienen y un puntero al siguiente nodo en la lista. El último nodo de la lista tiene como siguiente nodo el valor NULL. Entonces las listas enlazadas simples solo pueden ser recorridas en una dirección, apuntando al nodo siguiente, mas no a un nodo anterior.

Aquí una ejemplo de un lista enlazada simple.

En cristiano:
55-> 60-> 31-> 5-> 4-> 51-> 9-> 27-> 68-> 62-> NULL

Internamente:
Nodo-> Dato: 55 Direcion: 0x3d2c00 Siguiente: 0x3d2c80
Nodo-> Dato: 60 Direcion: 0x3d2c80 Siguiente: 0x3d2c90
Nodo-> Dato: 31 Direcion: 0x3d2c90 Siguiente: 0x3d2ca0
Nodo-> Dato: 5  Direcion: 0x3d2ca0 Siguiente: 0x3d2cb0
Nodo-> Dato: 4  Direcion: 0x3d2cb0 Siguiente: 0x3d2cc0
Nodo-> Dato: 51 Direcion: 0x3d2cc0 Siguiente: 0x3d3ab8
Nodo-> Dato: 9  Direcion: 0x3d3ab8 Siguiente: 0x3d3ac8
Nodo-> Dato: 27 Direcion: 0x3d3ac8 Siguiente: 0x3d3ad8
Nodo-> Dato: 68 Direcion: 0x3d3ad8 Siguiente: 0x3d3ae8
Nodo-> Dato: 62 Direcion: 0x3d3ae8 Siguiente: 0

Obviamente, internamente no existen las palabras nodo, dato,dirección y siguiente, es solo una representación.

Como una lista es una estructura de datos dinámica, el tamaño de la misma puede cambiar durante la ejecución del programa.

Como vimos en post anteriores, se puede generar memoria dinámicamente para un array, pero un array es una estructura estática pues su tamaño tiene un limite y así creáramos array dinámicos hay que redimensionar el tamaño si es necesario, lo cual ya implica un costo de volver a generar memoria dinámica.

Entonces podemos ver una ventaja de la listas sobre los arrays: No tener que redimensionar la estructura y poder agregar elemento tras elemento indefinidamente.

Cuando uno ya ha trabajado con arrays (vectores y matrices) y empieza a estudiar las listas, se da cuenta que una restricción de las listas es el acceso a los elementos. En un vector podíamos hacer algo como v[50] y nos estábamos refiriendo al índice 50 del vector v. A esto se le conoce como acceso aleatorio.

En el caso de las listas el acceso es secuencial, es decir, para acceder a un elemento del conjunto debemos de recorrer uno por uno los elementos hasta llegar al solicitado. Rápidamente se puede concluir que el tiempo de acceso a los elementos de un array es muchísimo más rápido que en una lista. Esta es una gran desventaja de las listas, por lo que buscar elementos por índice sería muy costoso. Esto no quiere decir que trabajar con arrays sea mejor que con listas. Las listas son muy flexibles y para muchos casos son imprescindibles.

Bueno, aquí va la primera práctica que hice sobre listas enlazadas. Implementación de una clase Lista, clase Nodo y los siguientes métodos:

  • Añadir un elemento al inicio.
  • Añadir un elemento al final
  • Añadir un elemento de manera ordenada
  • Llenar la lista por teclado
  • Llenar la lista aleatoriamente
  • Imprimir la lista
  • Buscar un elemento
  • Eliminar un elemento por dato
  • Eliminar un elemento por posicion o índice
  • Eliminar toda la lista
  • Invertir una lista
  • Ordernar una lista
  • Cargar una lista desde archivo
  • Guardar la lista en un archivo
  • Concatenar una lista a otra
  • Intersección entre 2 listas

Podrán ver la variable *temp en casi todos los métodos , este es el puntero de tipo Nodo que me va a permitir moverme a través de la lista y que inicialmente es igual a la cabeza (head). Mientras exista algo en la lista, voy avanzado el puntero para que apunte al siguiente. Esto se consigue en casi todos los casos con un while.

While(temp)
temp = temp->gte

Otra operación común en los métodos es preguntar si inicialmente la lista está vacía, es decir, si la cabeza no contiene algo o es igual a Null.

if(!head) o if(head==NULL)

Apliqué mis limitados conocimientos de templates para tener una lista genérica y así pueda funcionar con varios tipos de datos y de verdad funciona.

Ahí la definición e implementación de la clase, lista, clase nodo y el main para ver el funcionamiento. Cualquier crítica, sugerencia o comentarios son bienvenidos siempre.

Nodo.h

#ifndef NODO_H
#define NODO_H
#include <iostream>

using namespace std;
template <class T>
class Nodo
{
  public:
    T dato;
    Nodo *sgte;
    Nodo();
    Nodo(T);
    ~Nodo();

    void print();
    void eliminar();
};

#endif // NODO_H

Nodo.cpp

#include "Nodo.h"

//constructor por defecto
template<typename T>
Nodo<T>::Nodo()
{
  dato = NULL;
  sgte = NULL;
}

//constructor por parametro
template<typename T>
Nodo<T>::Nodo(T dato_)
{
  dato = dato_;
  sgte = NULL;
}

//imprimir un nodo
template<typename T>
void Nodo<T>::print()
{
  //cout << "Nodo-> " << "Dato: " << dato << " Direcion: " << this << " Siguiente: " << sgte << endl;
  cout << dato << "-> ";
}

//eliminar todos los nodos
template<typename T>
void Nodo<T>::eliminar()
{
  if(sgte)
    sgte->eliminar();
  delete this;
}

template<typename T>
Nodo<T>::~Nodo(){}

Lista.h

#ifndef LISTA_H
#define LISTA_H

#include <iostream>
#include "time.h"
#include <fstream>
#include <string>
#include "Nodo.h"
#include "Nodo.cpp"

using namespace std;

template <class T>
class Lista
{
  private:
    int dim,num_nodos;
    T line;
    string archivo;

  public:
    T ele;
    Nodo<T> *head;
    Lista();
    ~Lista();

    void llenar_teclado(int);
    void llenar_aleatorio(int);
    void print();
    void add_end(T);
    void add_head(T);
    void add_sort(T);
    void search(T);
    void del_dato(T);
    void del_pos(int);
    void del_all();

    void invertir();
    void sort();
    void load(string);
    void save(string);

    void concat(Lista);
    void interseccion(Lista);
};

#endif // LISTA_H

Lista.cpp

#include "Lista.h"

using namespace std;

//constructor por defecto
template<typename T>
Lista<T>::Lista()
{
  num_nodos = 0;
  head = NULL;
}

//llenar la lista por teclado
template<typename T>
void Lista<T>::llenar_teclado(int dim)
{
  for(int i=0;i<dim;i++){
    cout << "Ingresa el elemento " << i + 1 << endl;
    cin >> ele;
    add_end(ele);
  }
}

//llenar la lista aleatoriamente para enteros
template<typename T>
void Lista<T>::llenar_aleatorio(int dim)
{
  srand(time(NULL));
  for(int i=0;i<dim;i++){
    add_end(rand() % 100);
  }
}

//imprimir la lista
template<typename T>
void Lista<T>::print()
{
  Nodo<T> *temp = head;
  if(!head){
    cout << "La lista esta vacia " << endl;
  } else{
    while(temp){
      temp->print();
      if(!temp->sgte) cout << "NULL";
      temp = temp->sgte;
    }
  }
  cout << endl << endl;
}

//insertar al final
template<typename T>
void Lista<T>::add_end(T dato_)
{
  Nodo<T> *temp = head;
  Nodo<T> *nuevo = new Nodo<T> (dato_);

  if(!head){
      head = nuevo;
  } else{
    while(temp->sgte !=NULL){
      temp = temp->sgte;
    }
    temp->sgte = nuevo;
  }
  num_nodos++;
}

//insertar al inicio
template<typename T>
void Lista<T>::add_head(T dato_)
{
  Nodo<T> *temp = head;
  Nodo<T> *nuevo = new Nodo<T> (dato_);

  if(!head){
    head = nuevo;
  } else{
    nuevo->sgte = head;
    head = nuevo;
    while(temp){
      temp = temp->sgte;
    }
  }
  num_nodos++;
}

//insertar de manera ordenada
template<typename T>
void Lista<T>::add_sort(T dato_)
{
  Nodo<T> *nuevo = new Nodo<T> (dato_);
  Nodo<T> *temp = head;

  if(!head){
    head = nuevo;
  } else{
    if(head->dato > dato_){
      nuevo->sgte = head;
      head = nuevo;
    } else{
      while((temp->sgte != NULL) && (temp->sgte->dato < dato_)){
        temp = temp->sgte;
      }
      nuevo->sgte = temp->sgte;
      temp->sgte = nuevo;
    }
  }
  num_nodos++;
}

//buscar el dato de un nodo
template<typename T>
void Lista<T>::search(T dato_)
{
  Nodo<T> *temp = head;
  int cont = 1;
  int cont2=0;
  while(temp){
    if(temp->dato == dato_){
	  cout << "El dato se encuentra en la posicion: " << cont << " (Para seres humanos)" << endl;
	  cont2++;
    }
	temp = temp->sgte;
	cont++;
  }
  if(cont2 == 0){
    cout << "No existe el dato " << endl;
  }
  cout << endl << endl;
}

//eliminar por dato del nodo
template<typename T>
void Lista<T>::del_dato(T dato_)
{
  Nodo<T> *temp = head;
  Nodo<T> *temp1 = head->sgte;

  int cont=0;

  if(head->dato == dato_){
    head = temp->sgte;
  } else {
    while(temp1){
      if(temp1->dato == dato_){
	    Nodo<T> *aux = temp1;
        temp->sgte = temp1->sgte;
        delete aux;
        cont++;
        num_nodos--;
      }
	  temp = temp->sgte;
	  temp1 = temp1->sgte;
    }
  }
  if(cont == 0){
    cout << "No existe el dato " << endl;
  }
}

//eliminar por posicion del nodo
template<typename T>
void Lista<T>::del_pos(int pos)
{
  Nodo<T> *temp = head;
  Nodo<T> *temp1 = temp->sgte;

  if(pos < 1 || pos > num_nodos){
    cout << "Fuera de rango " << endl;
  } else if(pos==1){
    head = temp->sgte;
  } else{
    for(int i=2;i<=pos;i++){
      if(i==pos){
        Nodo<T> *aux = temp1;
        temp->sgte = temp1->sgte;
        delete aux;
        num_nodos--;
      }
      temp = temp->sgte;
      temp1 = temp1->sgte;
    }
  }
}

//eliminar todos los nodos
template<typename T>
void Lista<T>::del_all()
{
  head->eliminar();
  head = 0;
}

//invertir la lista
template<typename T>
void Lista<T>::invertir()
{
  Nodo<T> *temp = head;
  Nodo<T> *prev = NULL;
  Nodo<T> *next = NULL;

  while(temp){
    next = temp->sgte;
    temp->sgte = prev;
    prev = temp;
    temp = next;
  }
  head = prev;
}

//ordenar de manera ascendente
template<typename T>
void Lista<T>::sort()
{
  T aux2;
  Nodo<T> *aux = head;
  Nodo<T> *temp = aux;

  while(aux){
    temp=aux;
    while(temp->sgte){
      temp=temp->sgte;

      if(aux->dato>temp->dato){
        aux2=aux->dato;
        aux->dato=temp->dato;
        temp->dato=aux2;
      }
    }
    aux=aux->sgte;
  }
}

//cargar una lista de un archivo
template<typename T>
void Lista<T>::load(string archivo)
{
  ifstream in;
  in.open(archivo.c_str());

  if(!in.is_open()){
    cout << "No se puede abrir el archivo: " << archivo << endl << endl;
  } else{
    while(in >> line){
      add_end(line);
    }
  }
  in.close();
}

//guardar una lista en un archivo
template<typename T>
void Lista<T>::save(string archivo)
{
  Nodo<T> *temp = head;
  ofstream out;
  out.open(archivo.c_str());

  if(!out.is_open()){
    cout << "No se puede guardar el archivo " << endl;
  } else{
    while(temp){
      out << temp->dato;
      out << " ";
      temp = temp->sgte;
    }
  }
  out.close();
}

//concatenar a otra lista
template<typename T>
void Lista<T>::concat(Lista Lista2)
{
  Nodo<T> *temp2 = Lista2.head;

  while(temp2){
    add_end(temp2->dato);
    temp2 = temp2->sgte;
  }
}

//numeros que coinciden en 2 listas
template<typename T>
void Lista<T>::interseccion(Lista Lista2)
{
  Nodo<T> *temp = head;
  Nodo<T> *temp2 = Lista2.head;//cabeza de la segunda lista
  Lista lista_interseccion;//creo otra lista

  int num_nodos2 = Lista2.num_nodos;//nodos de la segunda lista
  int num_inter=0;//numero de coincidencias

  //creo 2 vectores dinamicos
  T *v1 = new T[num_nodos];
  T *v2 = new T[num_nodos2];

  //lleno los vectores v1 y v2 con los datos de la lista original y segunda lista respectivamente
  int i=0;
  while(temp){
    v1[i] = temp->dato;
    temp = temp->sgte;
    i++;
  }

  int j=0;
  while(temp2){
    v2[j] = temp2->dato;
    temp2 = temp2->sgte;
    j++;
  }

  //ordeno los vectores
  insert_sort(v1,num_nodos);
  insert_sort(v2,num_nodos2);

  int v1_i= 0;//indice del 1er vector (v1)
  int v2_i= 0;//indice del 2do vector (v2)

  //mientras no haya terminado de recorrer ambas listas
  while (v1_i < num_nodos && v2_i < num_nodos2) {
    if(v1[v1_i] == v2[v2_i]){//cuando los datos de ambas sean iguales
      lista_interseccion.add_end(v1[v1_i]);//utilizo mi metodo add_end para llenar la lista de intersecciones
      v1_i++;
      v2_i++;
      num_inter++;
    } else if(v1[v1_i] < v2[v2_i]){
      v1_i++;
    } else{
      v2_i++;
    }
  }

  //Solo si hay alguna interseccion imprimo la nueva lista creada
  if(num_inter > 0) {
    cout << "Existen " << num_inter << " intersecciones " << endl;
    lista_interseccion.print();
  } else {
    cout << "No hay interseccion en ambas listas" << endl;
  }
}

//usado por el metodo interseccion
template<typename T>
void insert_sort(T a[],int tam)
{
  T temp;
  for(int i=0;i<tam;i++){
    for(int j=i-1;j >=0 && a[j+1] < a[j];j--){
      temp = a[j+1];
      a[j+1] = a[j];
      a[j] = temp;
    }
  }
}

template<typename T>
Lista<T>::~Lista(){}

main.cpp

#include <iostream>
#include "Lista.h"
#include "Lista.cpp" //por errores de linking de tipo "undefined reference to" (estudiando)

using namespace std;
int main()
{
  Lista<int> l1;
  Lista<int> l2;

  int dim,pos;
  string archivo;

  cout << "Ingresa la dimension de la lista " << endl;
  cin >>dim;

  l1.llenar_aleatorio(dim);//llenar_teclado para otros tipos

  cout << "Lista A al inicio " << endl;
  l1.print();

  cout << "Agrega un elemento por la cabeza" << endl;
  cin >>l1.ele;
  l1.add_head(l1.ele);
  l1.print();

  cout << "Lista invertida " << endl;
  l1.invertir();
  l1.print();

  cout << "Lista ordenada " << endl;
  l1.sort();
  l1.print();

  cout << "Agrega un elemento (Sera ordenado)" << endl;
  cin >>l1.ele;
  l1.add_sort(l1.ele);
  l1.print();

  cout << "Busca un elemento" << endl;
  cin >>l1.ele;
  l1.search(l1.ele);

  cout << "Elimina un elemento por dato" << endl;
  cin >>l1.ele;
  l1.del_dato(l1.ele);
  l1.print();

  cout << "Elimina un elemento por posicion" << endl;
  cin >>pos;
  l1.del_pos(pos);
  l1.print();

  cout << "Cargar una lista desde archivo - Ingresa el nombre" << endl;
  cin >> archivo;//debe estar en el mismo directorio que este programa
  l2.load(archivo);
  cout << "Lista B" << endl;
  l2.print();

  cout << "Guardar la lista en un archivo - Ingresa el nombre" << endl;
  cin >> archivo;//ingresa un nombre cualquiera *.*
  l2.save(archivo);

  cout << "Interseccion entre las listas A y B " << endl;
  l1.interseccion(l2);

  cout << "Listas A y B concatenadas " << endl;
  l1.concat(l2);
  l1.print();

  l1.del_all();
  l1.print();

  return 0;
}

Un ejemplo de la salida del programa:

Ingresa la dimension de la lista
10
Lista A al inicio
55-> 60-> 31-> 5-> 4-> 51-> 9-> 27-> 68-> 62-> NULL

Agrega un elemento por la cabeza
18
18-> 55-> 60-> 31-> 5-> 4-> 51-> 9-> 27-> 68-> 62-> NULL

Lista invertida
62-> 68-> 27-> 9-> 51-> 4-> 5-> 31-> 60-> 55-> 18-> NULL

Lista ordenada
4-> 5-> 9-> 18-> 27-> 31-> 51-> 55-> 60-> 62-> 68-> NULL

Agrega un elemento (Sera ordenado)
45
4-> 5-> 9-> 18-> 27-> 31-> 45-> 51-> 55-> 60-> 62-> 68-> NULL

Busca un elemento
51
El dato se encuentra en la posicion: 8 (Para seres humanos)

Elimina un elemento por dato
60
4-> 5-> 9-> 18-> 27-> 31-> 45-> 51-> 55-> 62-> 68-> NULL

Elimina un elemento por posicion:
11
4-> 5-> 9-> 18-> 27-> 31-> 45-> 51-> 55-> 62-> NULL

Cargar una lista desde archivo - Ingresa el nombre
prueba.txt // mi archivito prueba.txt tenia estos datos
Lista B
8-> 59-> 23-> 15-> 94-> 63-> 9-> 17-> 62-> 33-> NULL

Guardar la lista en un archivo - Ingresa el nombre
otralista.txt // abre tu archivo y mira el contenido
Interseccion entre las listas A y B
Existen 2 intersecciones
9-> 62-> NULL

Listas A y B concatenadas
4-> 5-> 9-> 18-> 27-> 31-> 45-> 51-> 55-> 62-> 8-> 59-> 23-> 15-> 94-> 63-> 9->17-> 62-> 33-> NULL

La lista esta vacia.

Hasta pronto





Operaciones con matrices – Clase Matriz en c++

4 07 2009

El siguiente es el primer ejercicio que hice con matrices. Quizás alguno de estos ejercicios te puedan servir como una base. Puedes leer el post anterior sobre vectores, matrices y punteros si aún no haz trabajo con ellos.

Recomiendo que intentes y practiques mucho antes de copiar y pegar. Si te sientes estancado en algún problema recién trata de buscar una guía para llegar a la solución.

Esta clase matriz tendrá 3 atributos: Matriz,número de filas y número de columnas. Tiene un constructor por defecto,constructor copiador y un constructor por parámetro que recibe el número de filas y columnas.

Las siguientes funciones fueron implementadas:

  • Generar una matriz dinámicamente
  • LLenar la matriz desde teclado:
  • Llenar la matriz aleatoriamente:
  • Imprimir la matriz
  • Hallar el mayor elemento.
  • Hallar el menor elemento.
  • Hallar la moda.
  • Intercambiar filas.
  • Intercambiar columnas.
  • Sumar otra matriz: Sumar 2 objetos de tipo matriz retornar otra matriz. Ejem: c = a + b.
  • Restar otra matriz: Igual que la suma. Ejem: c= a – b
  • Multiplicar por otra matriz: Ejem: c = a * b. El nro de filas de a debe ser igual al nro de columnas de b
  • Multiplicar por un escalar: Ingreso un número y todos los elementos de la matriz se multiplican por ese número.
  • Hallar matriz transpuesta: matr[m][n] su transpuesta es matr[n][m]. Se obtiene cambiando filas por columnas. Donde los elementos de la fila m ahora pertenecen a la columna m de la transpuesta.
  • Verificar si es simétrica: Una matriz es simétrica si es cuadrada(filas=columnas) y cuando su transpuesta es igual a su matriz original.
  • Verificar si es identidad: Es identidad si tiene todos sus elementos nulos excepto los de la diagonal principal que son iguales a 1.

NOTA: He aplicado un poco de templates para manejar la matriz con varios tipos de datos (int, float, char,double) y sobrecarga de operadores para la suma, resta y multiplicación de matrices.

Recien estoy aprendiendo a usar estas 2 características importantes del lenguaje (templates y sobrecarga de operadores) por lo tanto, estos temas serán tratados a profundidad el proximo año.

En breve:

Un template es una manera de que funciones, clases métodos puedan ser usados con varios tipos de datos. Imagínense crear una lista de datos y tener que crear funciones insertar, eliminar, buscar, concatenar, etc para cada tipo de dato. Si los métodos y la clase tienen la misma lógica para que reescribir código si podemos reutilizar.

La sobrecarga de operadores es una manera de realizar las mismas operaciones que solemos hacer con tipos de datos comunes con tipos abstractos de datos. Por ejemplo, la sobrecarga me permitió sumar 2 objetos de tipo Matriz y almacenar el resultado en otro objeto Matriz, del modo c = a + b

Al grano:

Definición de la clase Matriz (La cabecera o header):

matriz.h

#ifndef MATRIZ_H
#define MATRIZ_H

#include <iostream>
#include <time.h>

using namespace std;

template <class T>
class Matriz
{
  private:
    T mayor_,menor_,moda_,ele,**matr1;
    int dim_matriz;
  public:
    int fils,cols;
    T escalar;

    Matriz();
    Matriz(int,int);
    Matriz(const Matriz &m);//constructor copia

    void llenar_teclado();
    void llenar_aleatorio();
    void imprimir();

    T mayor();
    T menor();
    T moda();

    void inter_filas(int,int);
    void inter_cols(int,int);

    bool esSimetrica();
    bool esIdentidad();
    void multi_escalar(T);
    void transpuesta();

    Matriz<T> operator+ (const Matriz &matr2);
    Matriz<T> operator- (const Matriz &matr2);
    Matriz<T> operator* (const Matriz &matr2);

    void eliminar();

    ~Matriz();
    protected:
};

#endif // MATRIZ_H

Implemetanción de la clase:

matriz.cpp

#include "Matriz.h"

//Constructor por defecto
template<typename T>
Matriz<T>::Matriz()
{
  fils=4;
  cols=4;
}

//Constructor copia
template<typename T>
Matriz<T>::Matriz(const Matriz &m)
{
  *this = m;
}

//Constructor por parametro
template<typename T>
Matriz<T>::Matriz(int fils_ , int cols_)
{
  fils = fils_;
  cols = cols_;
  matr1 = new T*[fils];
  for(int i=0;i<fils;i++){
      matr1[i] = new T[cols];
  }
}

//Llenar aleatoriamente una matriz
template<typename T>
void Matriz<T>::llenar_aleatorio()
{
  //srand(time(NULL));
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      matr1[i][j] = rand() % 30;
    }
  }
  mayor_= matr1[0][0];
  menor_= matr1[0][0];
  srand(time(NULL));
}

//Llenar una matriz desde teclado
template<typename T>
void Matriz<T>::llenar_teclado()
{
  for(int i=0;i<fils;i++){
    cout << "Fila " << i+1 << endl;
    for(int j=0;j<cols;j++){
      cout << "Ingresa el elemento " << j+1 << endl;
      cin >>ele;
      matr1[i][j] = ele;
    }
    cout << endl;
  }
  mayor_= matr1[0][0];
  menor_= matr1[0][0];
}

//Imprimir matriz
template<typename T>
void Matriz<T>::imprimir()
{
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      cout << matr1[i][j] << " ";
    }
    cout << endl << endl;
  }
  cout << endl << endl;
}

//Obtener el mayor de la matriz
template<typename T>
T Matriz<T>::mayor()
{
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      if(matr1[i][j] > mayor_){
        mayor_ = matr1[i][j];
      }
    }
  }
  return mayor_;
}

//Obtener el menor de la matriz
template<typename T>
T Matriz<T>::menor()
{
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      if(matr1[i][j] < menor_){
        menor_ = matr1[i][j];
      }
    }
  }
  return menor_;
}

//Obtener la moda de la matriz
template<typename T>
T Matriz<T>::moda()
{
  //creo una matriz auxiliar
  Matriz maux(fils,cols);

  //lleno la matriz con ceros
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      maux.matr1[i][j] = 0;
    }
  }

  dim_matriz = fils * cols;
  int y=0;//para retener una fila n veces
  int z=0;//para retener una columna n veces

  //empiezo a comparar cada elemento n veces
  for(int x=0;x<dim_matriz;x++){
    for (int i = 0; i < fils; i++){
      for (int j = 0; j < cols; j++){
        if(matr1[y][z] == matr1[i][j]){
          maux.matr1[i][j]++;//incremento en 1 el valor de la matriz aux en esa posicion
        }
      }
    }
    //pasar a la siguiente columna despues de n comparaciones
    z++;
    if(z==cols){//empiezo a comparar con la siguiente fila
      z=0;//empiezo nuevamente en la 1era columna
      y++;//paso a la siguiente fila
    }
  }

  //obtengo el valor mayor de la matriz
  mayor_ = maux.mayor();

  if(mayor_==1){//si ningun valor se ha repetido mas de una vez no hay moda
    return -1;
  } else {
    for(int i=0; i<fils;i++){
      for(int j=0;j<cols;j++){
        if(maux.matr1[i][j] == mayor_) {
          moda_ = matr1[i][j];
        }
      }
    }
  }
  return moda_;
}

//Intercambiar dos filas en una matriz
template<typename T>
void Matriz<T>::inter_filas(int fil1, int fil2)
{
  if(fil1 > fils || fil2 > fils){
    cout << "Por favor, hasta cuando " << endl;
  } else {
    T temp;
    fil1--;
    fil2--;

    for(int i=0;i<cols;i++){
      temp = matr1[fil1][i];
      matr1[fil1][i] =  matr1[fil2][i];
      matr1[fil2][i] = temp;
    }
    cout << "Se intercambio la fila " << fil1 + 1 << " por la " << fil2 + 1 << endl;
    imprimir();
  }
}

//Intercambiar dos columnas en una matriz
template<typename T>
void Matriz<T>::inter_cols(int col1, int col2)
{
  if(col1 > cols || col2 > cols){
    cout << "Por favor, hasta cuando " << endl;
  } else{
    T temp;
    col1--;
    col2--;

    for(int i=0;i<fils;i++){
      temp = matr1[i][col1];
      matr1[i][col1] =  matr1[i][col2];
      matr1[i][col2] = temp;
    }
    cout << "Se intercambio la columna " << col1 + 1 << " por la " << col2 + 1 << endl;
    imprimir();
  }
}

//Verificar si una matriz es simetrica
template<typename T>
bool Matriz<T>::esSimetrica()
{
  if(fils!=cols) {
    return false;
  }

  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      if (matr1[i][j]!=matr1[j][i]){
        return false;
       }
    }
  }
  return true;
}

//Verificar si una matriz es identidad
template<typename T>
bool Matriz<T>::esIdentidad()
{
  if(fils!=cols) {
    return false;
  }

  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      if (i == j) {
        if (matr1[i][j] != 1)
          return false;
      } else {
        if (matr1[i][j] != 0)
          return false;
      }
    }
  }
  return true;
}

//Multiplicar a una matriz por un escalar
template<typename T>
void Matriz<T>::multi_escalar(T escalar)
{
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      matr1[i][j] = matr1[i][j] * escalar;
    }
  }
  cout << "Se multiplico a la matriz por el escalar " << escalar << endl;
  imprimir();
}

//Obtener la transpuesta de una matriz
template<typename T>
void Matriz<T>::transpuesta()
{
  Matriz matresult(cols,fils);
  for(int i=0;i<cols;i++){
    for(int j=0;j<fils;j++){
      matresult.matr1[i][j]= matr1[j][i];
    }
  }
  matresult.imprimir();
}

//Suma de matrices con sobrecarga de operadores
template<typename T>
Matriz<T> Matriz<T>::operator+ (const Matriz &matr2)
{
  Matriz matresult(fils,cols);
  for (int i=0;i<fils;i++){
    for (int j=0;j<cols;j++){
	  matresult.matr1[i][j] = matr1[i][j] + matr2.matr1[i][j];
    }
  }
  return matresult;
}

//Resta de matrices con sobrecarga de operadores
template<typename T>
Matriz<T> Matriz<T>::operator- (const Matriz &matr2)
{
  Matriz matresult(fils,cols);
  for (int i=0;i<fils;i++){
    for (int j=0;j<cols;j++){
      matresult.matr1[i][j] = matr1[i][j] - matr2.matr1[i][j];
    }
  }
  return matresult;
}

//Multiplicacion de matrices con sobrecarga de operadores
template<typename T>
Matriz<T> Matriz<T>::operator* (const Matriz &matr2)
{
  Matriz matresult(fils,matr2.cols);
  T total;
  for (int i=0;i<fils;i++){
    for (int j=0;j<matr2.cols;j++){
      for(int k=0;k<cols;k++){
          total += (matr1[i][k] * matr2.matr1[k][j]);
          //para mostrar lo que pasa
          /*cout << "(" << matr1[i][k] << " * " << matr2.matr1[k][j] << ")";
          if(k<cols-1){
              cout << " + ";
          } else {
              cout << " = " << total;//despues del ultimo muestro la suma
          }*/
      }
      //cout << endl;
      matresult.matr1[i][j] = total;
      total = 0;//para limpiar el total sumado arriba
    }
    cout << endl;
  }
  return matresult;
}

template<typename T>
void Matriz<T>::eliminar()
{
  for(int i=0;i<fils;i++){
    delete[] matr1[i];
  }
  delete[] matr1;
}

template<typename T>
Matriz<T>::~Matriz(){}

main.cpp

#include <iostream>
#include "Matriz.h"
#include "Matriz.cpp"

using namespace std;

int main()
{
    srand(time(NULL));//para no generar los mismos numeros aleatorios
    int fils,cols,fil1,col1,fil2,col2;

    cout << "Ingresa nro de filas " << endl;
    cin >> fils;
    cout << "Ingresa nro de columnas " << endl;
    cin >> cols;
    cout << endl;

    Matriz<int> a(fils,cols);
    Matriz<int> b(fils,cols);
    Matriz<int> c(fils,cols);//matriz para almacenar el resultado de las operaciones

    a.llenar_aleatorio();//existe el metodo llenar_teclado() para otros tipos
    b.llenar_aleatorio();

    cout << "Operaciones con matrices " << endl;
    cout << "Matriz A " << endl;
    a.imprimir();

    cout << "Matriz B " << endl;
    b.imprimir();

    cout << "Matriz A + B " << endl;
    c = a + b;
    c.imprimir();

    cout << "Matriz A - B " << endl;
    c = a - b;
    c.imprimir();

    cout << "Matriz A * B " << endl;
    c = a * b;
    c.imprimir();

    cout << "Operaciones basicas con la matriz A" << endl;
    cout << "Matriz A " << endl;
    a.imprimir();

    cout << "El mayor de la matriz es: " << a.mayor() << endl;
    cout << "El menor de la matriz es: " << a.menor() << endl;
    cout << "La moda de la matriz es: " << a.moda() << endl;
    cout << (a.esSimetrica() ? "" : "No") << " Es simetrica." << endl;
    cout << (a.esIdentidad() ? "" : "No") << " Es identidad." << endl;
    cout << endl;

    cout << "Ingresa el escalar " << endl;
    cin >>a.escalar;
    a.multi_escalar(a.escalar);

    cout << "Intercambio: Ingresa dos filas del 1 al " << fils << endl;
    cin >>fil1;
    cin >>fil2;
    a.inter_filas(fil1,fil2);

    cout << "Intercambio: Ingresa dos columnas del 1 al " << cols << endl;
    cin >>col1;
    cin >>col2;
    a.inter_cols(col1,col2);

    cout << "Transpuesta de A " << endl;
    a.transpuesta();

    a.eliminar();
    b.eliminar();
    c.eliminar();

    return 0;
}

Particularmente, todos los ejercicios no son nada difíciles de resolver, pero yo me estanqué en la moda y creo que fue el problema que me hizo pensar un rato.

Lo resolví, pero sinceramente no me siento bien con la solución. No me gustó porque tiene una complejidad de O(n2) y con una matriz muy grande puede tarda mucho debido al número de recorridos y comparaciones.

La solución consistió en:

  • Crear otra matriz auxiliar con el mismo nro de filas columas y llenarla con ceros.
  • Recorrer la matriz original e ir comparando el primer elemento con todos los otros elementos de la matriz y contar sus repeticiones y así sucesivamente con los demás elementos.
  • Si encuentro una repetición, incremento en uno el valor en esa posición de la matriz auxiliar.
  • Luego hallar el mayor elemento de la matriz auxiliar.
  • Y finalmente recorrer la matriz auxiliar, cuando algún elemento de esta matriz sea igual al mayor elemento, quiere decir, que ese fue el elemento que más se repitió en la matriz original. Por lo tanto la moda se encuentra en esa posición.
Matriz original
17 16 10 26
10  3 9 19
29 7  1 6
6  20 0 10

Matriz auxiliar al inicio
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0

Matriz auxiliar despues de las comparaciones
con el nro de repeticiones de los valores
1 1 3 1
3 1 1 1
1 1 1 2
2 1 1 3

La moda de la matriz es: 10,
3 es el valor maximo que se repite en la matriz
Y en las posiciones en que se encuentra 3 se encuentra 10 en la matriz original.

Un tip que me dió el profe es reducir el nro de comparaciones creando una matriz booleana e ir poniendo unos y ceros a los números que ya comparé para luego no volver a comparar con los números que comparé anteriormente. Voy a seguir resolviendo el problema,si alguien tiene otra solución y puede explicarla fantástico.

Si yu





Vectores, Matrices y Punteros en c++

4 07 2009

Estimados lectores y fans, les pido disculpas por este humilde y sencillo post, se que este mini-tutorial super básico es una humillación a su inteligencia y profesionalismo, pero comprendan que esto va dedicado a personas que como yo recién empiezan con sus primeros pininos en C++. Espero de verdad le sea útil a alguien.

Escribir todo esto, me costó una noche entera y mucho café. Parte de los conceptos aquí los aprendí en clases de la U y mucho también lo aprendí por cuenta propia. Así que agradecimientos al teacher Patricio del cual aprendí muchísimo. Una vez más a ustedes y a los tantos miles de seguidores que me leen a diario las disculpas del caso.

Vamos ir avanzando de a pocos así que no se preocupen y desesperen porfavor, paciencia. La idea es que esta sección crezca con su ayuda, feedback y motivación. Luego vamos a ver ejemplos con objetos, clases, sobrecarga de operadores, métodos de búsqueda, màs ordenación, punteros, listas, pilas, colas, templates… por ahora sigo practicando y haciendo simples cosas con este lenguaje. Los códigos los compilé con g++, el compilador de GNU.

VECTORES
Un vector, también llamado array(arreglo) unidimensional, es una estructura de datos que permite agrupar elementos del mismo tipo y almacenarlos en un solo bloque de memoria juntos, uno despues de otro. A este grupo de elementos se les identifica por un mismo nombre y la posición en la que se encuentran. La primera posición del array es la posición 0.

Podríamos agrupar en un array una serie de elementos de tipo enteros, flotantes, caracteres, objetos, etc. Crear un vector en c++ es sencillo, seguimos la siguiente sintaxix: Tipo nombre[tamanyo];

Ejm:

int a[5];//Vector de 5 enteros
float b[5];//vector de 5 flotantes
Producto product[5];//vector de 5 objetos de tipo Producto

Podríamos también inicializar el vector en la declaración:

int a[] = {5,15,20,25,30};
float b[] = {10.5,20.5,30.5,12.5,50.5}
Producto product[] = {celular,calculadora,camara,ipod,usb}

Como hay 5 elementos en cada array, automáticamente se le asignará 5 espacios de memoria a cada vector.Pero si trato de crear el vector de la forma int a[]; el compilador mostrará un error, porque no indiqué el tamaño del vector ni tampoco inicializé sus elementos.

Asigno valores a los elementos de un vector indicando su posición:

int a[4] = 30; // le asigno el valor 30 a la posición 4 del vector, es decir, al 5to elemento.
product[2].setPrecio(300) // le asigno un precio de 300 al producto en la posicion 2, o sea al tercer elemento. 

Obviamente el método setPrecio() debe de estar implementado.

Para llenar, recorrer e imprimir un vector podemos utilizar un bucle for:

#include <iostream>
using namespace std;

int main()
{
  int dim;
  cout << "Ingresa la dimension del vector" << endl;
  cin >> dim; // Supongamos que ingrese 10
  int vector[dim]; // mi vector es de tamanyo 10

  for(int i=0;i < dim;i++){
    vector[i] = i * 10;
    cout << vector[i] << " ";
  }

  return 0;
}

La salida del programa mostrará: 0 10 20 30 40 50 60 70 80 90

Fàcil verdad? Bien ahora creen 2 o más vectores y empiecen a hacer funciones básicas como sumar, restar, buscar, ordenar, moda, etc que ayudan mucho a ir desarrollando la lógica. No vale copiar y pegar, mejor es practicar, practicar y practicar.

Aquí una función simple para sumar 2 vectores a y b y poner el resultado en un tercer vector c

#include <iostream>
using namespace std;

void sumar(int a[], int b[], int c[],int dim){
    for (int i=0; i<dim; i++) {
        c[i]=a[i] + b[i];
    }
}

void imprimir(int v[],int dim)
{
   for(int i=0;i<dim;i++){
        cout << v[i] << " ";
   }
   cout << endl << endl;
}

int main()
{
    int dim;
    cout << "Ingresa la dimension" << endl;
    cin >> dim;

    int a[dim];
    int b[dim];
    int c[dim];

    for(int i=0;i<dim;i++){
        a[i] = i * 10;
        b[i] = i * 5;
    }

    cout << "Vector A " << endl;
    imprimir(a,dim);

    cout << "Vector B " << endl;
    imprimir(b,dim);

    sumar(a,b,c,dim);
    cout << "Vector C " << endl;

    imprimir(c,dim);
    return 0;
}

Este programa me botaría (si ingreso una dimensión de 10):

Vector A
0 10 20 30 40 50 60 70 80 90

VECTOR B
0 5 10 15 20 25 30 35 40 45

VECTOR C
0 15 30 45 60 75 90 105 120 135

Entonces para tomar en cuenta:

Todo vector debe tener definido un tipo de dato.
Todo vector necesita de una dimensión o tamanyo.
El código de arriba se puede mejorar muchísimo con objetos y clases, este es solo un pequeño ejemplo.

MATRICES
Una matriz es un vector de vectores o un también llamado array bidimensional. La manera de declarar una matriz es c++ es similar a un vector:

int matriz[fils][cols];

int es el tipo de dato, matriz es el nombre del todo el conjunto de datos y debo de especificar el numero de filas y columnas. Las matrices también pueden ser de distintos tipos de datos como char, float, double,etc.

Las matrices en c++ se almacenan al igual que los vectores en posiciones consecutivas de memoria. Usualmente uno se hace la idea que una matriz es como un tablero. Pero internamente el manejo es como su definicion lo indica, un vector de vectores, es decir, los vectores estan uno detras del otro juntos.

La forma de acceder a los elementos de la matriz es utilizando su nombre e indicando los 2 subindices que van en los corchetes. Si Coloco int matriz[2][3]=10; //estoy asignando al cuarto elemento de la tercera fila el valor 10. No olvidar que tanto filas como columnas se enumeran a partir de 0.

Bueno y para recorrer una matriz podemos usar igualmente un bucle. En este caso 2 for

for(int i=0;i<fils;i++){
  for(int j=0;j<cols;j++){
    matriz[i][j] = i % j;
  }
}

PUNTEROS
El valor de todas las variales que manejamos en nuestros programas se almacenan en memoria y tienen una dirección. Un puntero es una variable especial que apunta a la dirección de memoria de una variable. El puntero tiene a su vez su propia dirección. Todas estas direcciones tienen un formato hexadecimal.

Los punteros son herramientas muy poderosas con muchas utilidades y enormes ventajas como veremos más adelante. A grandes rasgos, un puntero me permite desplazarme en la memoria, apuntar, redireccionar a ciertas variables, funciones, métodos, objetos sin necesidad de mover grandes bloques de datos, lo cual nos ahorra muchísimo el consumo de memoria en los programas.

Un puntero se debe declarar de acuerdo al tipo de dato al que apunta. Ejem:

int *var; //Un puntero llamado var que podra apuntar a cualquier variable de tipo entero.
char *u;//puntero de tipo char
Persona *per;//puntero de tipo persona

Para determinar,asignar la dirección de una variable en c++, se usa el operador & y para obtener el contenido de un puntero utilizamos el operador * Ejem:

int a;//entero
int *b;//puntero a entero
a = 20;//a tiene 20
b=&a;//asigno la direccion de a al puntero b

cout << b << endl; // imprira la direccion de memoria de a;
cout << *b;// imprimira 20, osea el contenido de a

Ahora analicemos las siguientes instrucciones y veamos como las variables van cambiando de valor en tiempo de ejecución:

#include <iostream>
using namespace std;
int main () {
    int a=10;
    int b=20;
    int *p,*p2;//punteros de tipo entero

    cout << "ANTES" << endl;
    cout << "Variable a = " << a << endl;

    cout << "Direccion de a = " << &a << endl << endl;

    cout << "Variable b = " << b << endl;
    cout << "Direccion de b = " << &b << endl << endl;

    cout << "Contenido de p (BASURA)= " << *p << endl;//tiene basura al principio, podria inicializar con *p=0
    cout << "Direccion de p = " << &p << endl << endl;

    cout << "DESPUES" << endl;
    a++;//incremento a
    p= &a; //p ahora tiene a la direccion de a

    cout << "Contenido de p =  " << *p << endl;

    p = &b;//p ahora tiene la direccion de b
    *p +=20; // le sumo 20 al contenido de p, es decir, estoy incrementando el valor de b

    cout << "Variable a = " << a << endl;
    cout << "Variable b = " << b << endl << endl;

    p=&a;//p ahora tiene la direccion de a
    *p = a * 5;//contenido de p es igual al contenido de a * 5

    cout << "Contenido de p = " << *p << endl;
    cout << "Variable a = " << a << endl << endl;

    cout << "Contenido de p2 (BASURA) = " << *p2 << endl;//tiene basura al principio, podria inicializar con *p2=0
    cout << "Direccion de p2 = " << &p2 << endl << endl;

    p2 = p;//el contenido de p es asignado al contenido de p2
    *p2 +=15;//incremento 15 al contenido de p2

    cout << "Contenido de p2 = " << *p2 << endl;//igual al contenido de p
    p++;//p apunta a otra direccion de memoria,se desplaza 4 bytes en memoria
    cout << "Contenido de p (BASURA) = " << *p << endl;//el contenido de esa nueva direccion

    return 0;
}

La salida del programa:
ANTES

Variable a = 10
Direccion de a = 0×22ff74

Variable b = 2
Direccion de b = 0×22ff70

Contenido de p (BASURA)= -1017291943
Direccion de p = 0×22ff6c

DESPUES
Contenido de p = 11
Variable a = 11

Variable b = 40
Contenido de p = 55
Variable a = 55

Contenido de p2 (BASURA) = 2293680
Direccion de p2 = 0×22ff68
Contenido de p2 = 70

Contenido de p (BASURA) = 2293680

El contenido de p y p2 al principio es basura porque no tienen ningun valor asignado aun. Podriamos asignar el valor NULL a un puntero para luego posteriormente en algun problema que se me presente saber el estado del puntero y saber si contiene algo o no, así:

int *p= NULL;

ARITMÉTICA DE PUNTEROS

En las ultimas sentencias del programa anterior:

p++;
cout << *p;

Pueden visualizar que estoy incrementando el puntero p en 1. Esto quiere decir que el puntero se desplazara 4 bytes en memoria (en este caso por ser entero) y entonces apuntara a otra direccion. Por eso es que el nuevo contenido de p es basura o bueno el contenido de lo que tiene esa nueva direccion a la que apunta.

Supongamos que definir un entero y puntero de tipo char:

char c;
char *d;

d= &c;//asigno la direccion de c a d
c='u';//asigno el valor u a mi variable c
c--;//desplazo una posicion a c
cout << *d;//

No Imprimira ‘u’ porque fijense que desplazé c en sentido negativo 1 byte (los char ocupan a 1 byte). Es decir, que si d estaba apuntado a una direccion como por ejemplo 0×22ff99, despues del c– estara apuntando a algo como 0×22ff98

Para tomar en cuenta cosas que no puedo hacer con punteros:

int a=15;
int *p;

double *q;
void *r;

p = a; //No puedo hacer esto porque estoy asignando una variable a un puntero y un puntero es una direccion.
p = &50; // 50 es un valor constante en este caso y no una variable,por lo tanto no tiene direccion
p = &(a+1); //una expresion no tiene direccion
p = 30;//igual que el primer error, 30 es un entero.
&a = p;//no puedo cambiar la direccion de una variable
p = q;//p es puntero de tipo entero y q de tipo double

Un puntero de tipo void, es un puntero al cual le podemos asignar cualquier tipo de puntero. Por lo tanto si podriamos hacer esto:

r = p;

VECTORES Y PUNTEROS
Cuando declaramos un vector int v[10];El nombre del vector, o sea v, es un puntero al primer elemento del vector, es decir a v[0].Entonces como un vector es un puntero al primer elemento del mismo, también podríamos hacer aritmética de punteros con el vector.

(v+1) ;//apuntara a v[1];
*(v+5);//me refiero al contenido de v[5]

//Y también a los punteros les puedo poner índices:

int *p; //puntero de tipo entero
p = &v[0];//p apunta a la direccion del vector v[0] o tambien a v. p = v
p[8] = 80; //le asigno el valor 80 al puntero en la posicion 8, es decir a v[8]

VECTORES DINÁMICOS

Lo que vimos en el inicio de este post, son vectores estáticos, puesto que tienen una cantidad fija de memoria asignada y tamaño definido que no podemos modificarlo. Sin embargo, un vector podría tener una cantidad variable de datos, a este se le llama un vector dinámico.

Para usar vectores dinámicos necesitamos gestionar memoria dinámica. Si bien es cierto que es trae enormes ventajas, el hacer un mal uso de la memoria dinámica nos podría traer problemas desastrozos. Por eso es importante que que cuando creemos vectores dinámicos también liberemos la memoria utilizada. Obviamente eliminaremos la memoria utilizada cuando ya no necesitamos más usar, en este caso, un determinadao vector.

El operador new sirve para reservar memoria dinámica.
El operador delete se usa para liberar la memoria dinámica reservada con new.
Para liberar memoria de un array dinámico usamos delete[]

El espacio de memoria que hemos reservado con new tendrá vida hasta que finalize la ejecución del programa o cuando liberemos ese espacio con delete. Siempre es recomendable liberar memoria para posteriormente no tener problemas con excesivo consumo de memoria.

Un simple ejem:

#include <iostream>
using namespace std;

int main()
{
    int *pv;
    int dim;

    cout << "Ingresa el tamanyo del vector" << endl;
    cin >>dim;
    pv = new int[dim];

    for(int i=0;i<dim;i++){
        pv[i] = i * i;
        cout << pv[i] << " ";
    }

    delete[] pv;
    return 0;
}

MATRICES Y PUNTEROS

Supongamos que se declaró una matriz int m[5][5]

Como dijimos anteriormente, el nombre o identificador de un vector es un puntero al primer elemento del vector. En el caso de matrices el nombre de la matriz, en este ejemplo v, es un puntero que apunta al primer elemento del primer vector de la matriz. Entonces m es un doble puntero.m es igual a &m[0] que es igual a la direccion de &m[0][0].

Si declaramos un puntero int *pm y luego igualamos pm = m, p ahora puede desplazarse por los valores de m.

*p;//contenido de m[0],el cual apunta al primer elemento de ese vector, es decir, m[0][0]

También puedo referirme a los contenidos con aritmética de punteros

*(p+1);//Desplazo una posicion a p,se refiere al contenido de m[1],el cual apunta al primer elemento de ese vector, es decir, m[1][0]
*(*(p+1)+1);//desplazo una posición en el vector principal y este a su vez se desplaza una posicion en ese vector. es decir,me refiero al contenido de m[1][1];

p[2][4] = 20;//asigno el valor 20 a la posicion 2,4 de la matriz
*(*(p+2)+4) = 20 // es lo mismo que la asignacion anterior
*(pm[2]+4) = 20  // tambien los mismo

//En conclusión:
p[i][j] = *(*(p+i)+j) = *(pm[i]+j)

MATRICES DINÁMICAS

Para crear una matriz dinámica debemos de crear un doble puntero int **pm y samos al igual que los vectores el operador new para reservar memoria y delete para liberar.

Primero tenemos que crear el vector que contendrá a otros vectores especificando el numero de vectores que tendra este vector principal. Ejem:

pm = new int* [fils];//creo el vector de punteros principal
for(int i=0;i<fils;i++){
  pm[i] = new int [cols];//para crear los vectores dentro del vector principal
}

Ahora sí vamos al grano y veamos un simple programa que crea una matriz dinámica, asigna valores, muestra el contenido de cada uno de los elementos los elementos así como sus direcciones de memoria.

También mostramos la matriz usando aritmética de punteros,ahí va:

#include <iostream>
using namespace std;

int main()
{
  int **pm;//puntero a una matriz
  int fils,cols;

  cout << "Ingresa el nro de filas: ";
  cin >>fils;

  cout << endl;
  cout << "Ingresa el nro de columnas: ";
  cin >>cols;

  pm = new int* [fils];
  for(int i=0;i<fils;i++){
    pm[i] = new int [cols];
  }

  cout << "Elementos de la Matriz con sus direcciones" << endl;
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      pm[i][j] = i + j;
      cout << pm[i][j] << "--> ";
      cout << &pm[i][j] << " ";
    }
    cout << endl;
  }
  cout << endl;

  cout << "La matriz con aritmetica de punteros" << endl;
  for(int i=0;i<fils;i++){
    for(int j=0;j<cols;j++){
      *(*(pm+i)+j) = i + j;//aritmetica de punteros
      cout << *(*(pm+i)+j) << "-->";
      cout << &pm[i][j] << " ";
    }
    cout << endl;
  }

  for(int i=0;i<fils;i++){
    delete[] pm[i];//elimino cada vector de la matriz
  }
  delete[] pm;//elimino el vector principal
  return 0;
}

La salida del programa:

Ingresa el nro de filas: 6
Ingresa el nro de columnas: 4

//Elementos de la Matriz con sus direcciones
0–> 0×3d2c90 1–> 0×3d2c94 2–> 0×3d2c98 3–> 0×3d2c9c
1–> 0×3d2ca8 2–> 0×3d2cac 3–> 0×3d2cb0 4–> 0×3d2cb4
2–> 0×3d2cc0 3–> 0×3d2cc4 4–> 0×3d2cc8 5–> 0×3d2ccc
3–> 0×3d3ab8 4–> 0×3d3abc 5–> 0×3d3ac0 6–> 0×3d3ac4
4–> 0×3d3ad0 5–> 0×3d3ad4 6–> 0×3d3ad8 7–> 0×3d3adc
5–> 0×3d3ae8 6–> 0×3d3aec 7–> 0×3d3af0 8–> 0×3d3af4

//La matriz con aritmetica de punteros
0–> 0×3d2c90 1–> 0×3d2c94 2–> 0×3d2c98 3–> 0×3d2c9c
1–> 0×3d2ca8 2–> 0×3d2cac 3–> 0×3d2cb0 4–> 0×3d2cb4
2–> 0×3d2cc0 3–> 0×3d2cc4 4–> 0×3d2cc8 5–> 0×3d2ccc
3–> 0×3d3ab8 4–> 0×3d3abc 5–> 0×3d3ac0 6–> 0×3d3ac4
4–> 0×3d3ad0 5–> 0×3d3ad4 6–> 0×3d3ad8 7–> 0×3d3adc

En mi caso esa son las direcciones de memoria. Bueno ya que hemos revisado conceptos básicos a continuación veamos la Clase Matriz, su definición, algunos métodos y su implementación.





Tail recursion

19 05 2009

La recursividad es una técnica de programación que consiste en que una serie de instrucciones se repiten como una subtarea de la tarea principal, es decir, las funciones, procesos o rutinas se llaman a sí mismos cada vez que lo requieran y se ejecutan repetidas veces hasta que se satisface una condición específica.

Sin embargo, la recursividad en algunos casos, tiene un costo computacional alto, debido a las constantes llamadas a la misma función, rutina y muchas veces estas llamadas consumen demasiada memoria.

En algunos algoritmos recursivos, se puede implementar un caso de recursividad especial llamado Tail Recursion (recursividad por cola), la cual es una técnica para optimizar la recursividad eliminando las constantes llamadas recursivas. Tail recursion es cuando la llamada recursiva es la última instrucción de la función.

Sin embargo, las funciones tail recursive deben cumplir la condición que en la parte que realiza la llamada a la función, no debe existir ninguna otra sentencia.

Una ventaja de la recursividad por cola es que podemos evitar la sobrecarga de cada llamada a la función y nos evitamos el gasto de memoria de pila. Con una función tail recursive se puede evitar lo que se conoce como stack overflow, que ocurre cuando la pila de llamadas (call stack) consume mucha memoria.

Veamos esta técnica con un ejemplo muy conocido que es encontrar el factorial de un número.

Esta simple función obtiene el factorial de un número de la manera recursiva convencional: La explicación es sencilla, llamamos a la misma función fact hasta que n sea igual a 1, en ese momento la recursividad se detiene.

int fact_recursivo(int n)
{
  if(n == 1)
    return n;
  else
    return n * fact_recursivo(n-1);
}

int main() {
  int num=0;
  cout << "Ingresa un nro " << endl;   cin >> num;

  cout << "Su factorial es " << fact_recursivo(num);
  return 0;
}

Digamos que ingresé 15, si compilamos el programa la función me daría 2004310016, lo cual es correcto, pero hay que tener en cuenta todas las llamadas recursivas que hizo la función fact_recursivo, es decir, la función tiene que calcular el factorial de 14 y este el fact de 13, y este el fact de 12… sucesivamente hasta 1.  Tenemos un crecimiento del número de llamadas recursivas.

Ahora vamos a realizar la misma función usando tail recursion. En este caso no se necesita guardar un marco de pila para cada llamada recursiva, y el algoritmo se comporta como si fuera iterativo.

Para conseguir esto usamos un parámetro adicional que actúa como acumulador.

Esta sería la función factorial con tail recursion:

//function factorial con tail recursion
int fact_tail_sum(int n, int sum) {
  if(n == 1) {
    return sum;
  } else {
    return fact_tail_sum(n - 1, sum*n);
  }
}

//para mantener la sintaxis original de como se llama a la función factorial
int fact_tail(int n) {
  return fact_tail_sum(n, 1);
}

int main() {
  int num=0;
  cout << "Ingresa un nro " << endl;   cin >> num;
  cout << "Su factorial es " << fact_tail(num);
  return 0;
}

El resultado igualmente sería 2004310016, pero el costo computacional baja drásticamente debido a que cuando se hace la llamada return fact_tail_sum(n – 1, sum*n) ambos parámetros son inmediatamente resueltos. Con esta llamada podemos calcular sin esperar una llamada a una función recursiva para que nos devuelva un valor.

En este caso el compilador reduce drásticamente el uso de la pila y la cantidad de información que debe ser almacenada, el valor de n y sum es independiente del número de llamadas recursivas.

Una función recursiva normal se puede convertir a tail recursive usando en la función original un parámetro adicional, usado para ir guardando un resultado de tal manera que la llamada recursiva ya no tiene una operación pendiente.

También se usa una función adicional para mantener la sintaxis de como llamamos normalmente a la función. En el caso del factorial, para seguir llamando a la función de la forma fact(n).
En conclusión:

• Una llamada es tail recursive (recursiva por cola) si no tiene que hacer nada más después de la llamada de retorno.
• Tail recursion es cuando la llamada recursiva es la última instrucción en la función.
• Usar tail recursion es muy ventajoso, porque la cantidad de información que debe ser almacenada durante el cálculo es independiente del número de llamadas recursivas.
• El compilador trata una función tail recursive como si fuera una función iterativa.





Quicksort en C++

19 05 2009
Quicksort es un algoritmo de ordenación  considerado entre los más rápidos y eficientes. Fue diseñado en los años 60s por C. A. R. Hoare un científico en computación. El algoritmo usa la técnica divide y vencerás que básicamente se basa en dividir un problema en subproblemas y luego juntar las respuestas de estos subproblemas para obtener la solución al problema central.

Me pareció una buena manera de practicar programación y C++ y hacer una explicación que permite entender este algoritmo , bueno al menos eso intenté.  Hace año y medio también publiqué unos ejemplos de ordenación en Haskell.

Voy a explicar el funcionamiento del quicksort brevemente y después veremos su implementación en C++.

Se tiene una array de n elementos, tomamos un valor del array como pibote(usualmente el primero), separamos los elementos menor a este pibote a laizquierda y los mayores a la derecha, es decir, dividimos el array en 2 subarrays.

Con estos subarrays se repite el mismo proceso de forma recursiva hasta que estos tengan más de 1 elemento. Por lo tanto la función quicksort quedaría de la siguiente manera:

void quicksort(int *array, int inicio, int fin)
{
  int pivote;
  if (inicio < fin) {
    pivote = dividir(array, inicio, fin );
    quicksort( array, inicio, pivote - 1 ); //ordeno la lista de los menores
    quicksort( array, pivote + 1, fin ); //ordeno la lista de los mayores
  }
}

La magia está en la función dividir que es la que voy a explicar a
continuación:

Empezamos creando o generando un array de n elementos, por ejemplo yo use la función rand() de c++ para generar aleatorios del 1 al 15, mi arreglo quedó así:

a[] = {8, 1, 5, 14, 4, 15, 12, 6, 2, 11, 10, 7, 9};

izq                                                                              der

0 1 2 3 4 5 6 7 8 9 10 11 12
8 1 5 14 4 15 12 6 2 11 10 7 9

Tomamos como pibote el 8 y usamos 2 índices que me indiquen la posición del array:

Uno que vaya de izquierda a derecha buscando los elementos mayores al pibote. a[izq]

Y un índice que busque de derecha a izquierda los elementos menores al pibote.  a[der]

El índice izquierdo irá aumentando en 1 mientras el array en la posición
izquierda sea menor o igual al pibote:

while ((izq < der) && (array[izq] <= pibote)){
  izq++;
}

El índice derecho irá reduciéndose en 1 mientras el array en la posición
derecha sea mayor al pibote.

while (array[der] > pibote){
  der--;
}

Si al final de estas 2 operaciones, el índice izquierdo es menor al derecho se intercambian las posiciones a[izq] con a[der], usando una variable temporal:

En este caso, en la primer recorrido el índice izquierdo encuentra al 14 (mayor al pibote) y el índice derecho al 7 (menor al pibote), y
se intercambian los índices:

8 1 5 14 4 15 12 6 2 11 10 7 9
8 1 5 7 4 15 12 6 2 11 10 14 9

Segundo recorrido: El índice izquierdo encuentra al 15 (mayor al
pibote) y el índice derecho al 2 (menor al pibote), se intercambian:

8 1 5 7 4 15 12 6 2 11 10 14 9
8 1 5 7 4 2 12 6 15 11 10 14 9

Tercer recorrido: El índice izquierdo encuentra al 12 (mayor al
pibote) y el índice derecho al 6 (menor al pibote), se intercambian:

8 1 5 7 4 2 12 6 15 11 10 14 9
8 1 5 7 4 2 6 12 15 11 10 14 9

Cuando los índices se juntan o se cruzan ponemos el pibote en el lugar que le corresponde en el array:

temp = array[der];
array[der] = array[inicio]; // el pibote está en el inicio del array
array[inicio] = temp;

Se intercambian el 8 con el 6 y el array quedaría así:

6 1 5 7 4 2 8 12 15 11 10 14 9

Ahora la función quicksort se llamaría 2 veces recursivamente para los 2 subarray que tenemos:

quicksort(a,0,5) // el pibote está en la posición 6
quicksort(a,7,12) // el pibote está en la posición 6

Se repite el mismo proceso con este primer subarray quicksort(a, 0, 5)

El pibote es 6, se encuentra por la izquierda al 7 y por la derecha al 2. Se intercambian y como se cruzaron los índices movemos el pibote a su lugar:

6 1 5 7 4 2
6 1 5 2 4 7
4 1 5 2 6 7

Otra vez tenemos 2 subarrays. Entonces se vuelve a llamar la función

quicksort(a,0,3) // la posición del pibote es 4
quicksort(a,5,5) // No se ejecuta nada, el inicio no es menor al final

El mismo proceso. Se ejecutar quicksort (a, 0, 3). El pibote ahora es 4, se encuentra por el índice izquierdo al 5 y por el derecho a 2.  Se intercambian y como se juntaron los índices se mueve el pivote a su lugar.

4 1 5 2
4 1 2 5
2 1 4 5
quicksort (a,0,1) // la posición del pibote es 2
quicksort (a,3, 3) // No se ejecuta nada, el inicio no es menor al final

Cuando se ejecuta quicksort (a, 0, 1) se intercambia los índices otra vez.

2 1
1 2

Ahora se llamaría a quicksort(a, 0,0), se divide en 2 elementos el subarray
y no hay nada más que hacer por que solo tienen 1 elemento.

Ahora se ejecuta el mismo proceso con el segundo subarray del array original. Quicksort (a, 7,12)

Se encuentra el 15 y el 9, se intercambian y como luego se juntan los índices, se coloca el pibote a su lugar:

12 15 11 10 14 9
12 9 11 10 14 15
10 9 11 12 14 15

Tenemos otros 2 subarrays y se vuelve a llamar la función quicksort

quicksort(a,7,10) // posición del pibote es 10 y la 1era 7
quicksort(a,11,12) //posición del pibote es 10 y la ultima 12

Se intercambian los índices y nos quedan otros 2 subarrays de 1 solo elemento entonces no se ejecuta nada.

10 9 11
9 10 11

Con el anterior subarray (14, 15) se llama a quicksort(a, 11,12).

14 15

Se divide en 2 elementos este subarray y no hay nada más que hacer por que los array que contienen al 14 y al 15 solo tienen 1 elemento.

De manera que el árbol recursivo de ordenación queda más o menos así:

Quick Sort

Quick Sort

Complejidad computacional del Quicksort:

En el mejor de los casos tiene un costo de O(n*log (n)). Que es cuando el pibote siempre queda al medio del arreglo.

quicksort

Quicksort Mejor caso

En el peor de los casos tiene un costo de O(n^2). Cuando el pibote siempre se inclina hacia a un lado, es decir, genera una array de sólo 1 elemento y una segunda con el resto de elementos.

quicksort

Quicksort peor caso

En el caso promedio también tiene un costo de O(n*log (n)). Se produce cuando el pibote se inclina más hacia un lado y los 2 subarrays tienen distinto tamaño de elementos.

quicksort

Quicksort caso promedio

Para calcular el tiempo de ejecución estoy usando la función clock() que determina el tiempo usado por el procesador. En este caso defino 3 variables ini, final y total.

ini=clock(); // Antes del quicksort:
final = clock(); //Después que se ejecuta el quicksort
total =((double)(final – ini)) /
CLOCKS_PER_SEC; // El valor retornado por clock()
debe ser dividido por el valor de la macro CLOCKS_PER_SEC

He leído en varios sitios de c++ que la función clock no tiene mucha precisión. Si alguien sabe un mejor método para calcular el tiempo de ejecución, algún comentario, sugerencia o aporte para optimizar al algoritmo, bienvenido.

Queda pendiente para otro post calcular con exactitud tiempos de ejecución e implementar el código del número de comparaciones e intercambios.

Bueno, aquí está la implementación del algoritmo en C++ disfrutenlo.

#include <iostream>
#include <time.h>
#include <stdio.h>

using namespace std;

//funcion para dividir el array y hacer los intercambios
int dividir (int *array, int inicio, int fin){
  int izq;
  int der;
  int pibote;
  int temp;

  pibote = array[inicio];
  izq = inicio;
  der = fin;

  //Mientras no se cruzen los índices
  while (izq < der){
    while (array[der] > pibote){
	  der--;
    }

	while ((izq < der) && (array[izq] <= pibote)){
      izq++;
    }

    // Si todavia no se cruzan los indices seguimos intercambiando
	if(izq < der){
      temp= array[izq];
      array[izq] = array[der];
      array[der] = temp;
    }
  }

  //Los indices ya se han cruzado, ponemos el pivote en el lugar que le corresponde
  temp = array[der];
  array[der] = array[inicio];
  array[inicio] = temp;

  //La nueva posición del pivote
  return der;
}

//funcion recursiva para hacer el ordenamiento
void quicksort( int *array, int inicio, int fin)
{
  int pivote;
  if(inicio < fin){
    pivote = dividir(array, inicio, fin );
    quicksort( array, inicio, pivote - 1 );//ordeno la lista de los menores
    quicksort( array, pivote + 1, fin );//ordeno la lista de los mayores
  }
}

int main()
{
  int const max = 100;
  int tamanyo;
  clock_t ini, final;
  double total;
  ini=clock();

  cout << "Ingresa tamanyo " << endl;
  cin >> tamanyo;

  int a[tamanyo];
  srand(time(0));//para que el rand no genere siempre los mismos numeros, utilizando la hora del sistema

  for (int i = 0; i < tamanyo; i++){
    a[i] = rand() % max;//rand para generar enteros aleatorios
  }

  cout << "Array antes de ordenarse: " << endl;
  for (int i=0; i < tamanyo; i++){
    cout << a[i] << " ";
  }

  cout << endl << endl;

  quicksort(a, 0, tamanyo - 1 );

  final=clock();
  total=((double)(final - ini)) / CLOCKS_PER_SEC;
  printf("Tiempo de ejecucion : %lf segundos \n",total);

  cout << "Array ordenado " << endl;

  for (int i=0; i < tamanyo; i++ ){
    cout << a[i] << "-";
  }
  cout << endl << endl;

  return 0;
}