Introducción, búsquedas y programación dinḿica
Algoritmos | 2ºGRIA
Un algoritmo es un conjunto finito y ordenado de pasos o instrucciones precisas que permiten resolver un problema o realizar una tarea, es decir, llegar a su resultado o solución. Los algoritmos son las bases de los programas, ya que no dejan de ser un algoritmo en leguaje máquina. No obstante, los algoritmos existieron ya desde antes de la informática. Por ejemplo:
- Babilonia: Existian algoritmos de resolución de ecuaciones y problemas matemáticos
- Euclides: Desarrollo un algoritmo mara calcular el MCD. El mas antiguo y documentado
Con respecto a las computadoras, los algoritmos son secuencias formalizadas y precisas que una computadora puede entender y ejecutar. Todos los programas estan construidos sobre ellos y se traducen en lenguajes de programación para su ejecucion. Se caracterizan por ser claros, eficientes (Resolución en tiempo razonable y pocos recursos) y ser generales (Vale para una familia de problemas). Hay varios tipos de algoritmos:
- Búsqueda: Encontrar información en una base de datos o en la web
- Ordenamiento: Organizar datos
- Criptográficos: Proteger información
- Inteligencia artificial: Reconocer imágenes, traducir textos o recomendar canciones
En este caso nos vamos a centrar en los de búsqueda.
La búsqueda es un proceso de encontrar un elemento ovjetivo dentro de un grupo de elementos o determinar su inexistencia. El objetivo para su optimización es minimizar el numero de comparaciones lo máximo posíble. Las busquedas pueden ser internas (Memoria principal) y Externa (Memoria secundaria). Hay varios tipos de búsqueda:
Búsqueda lineal
El tipo mas sencillo, compara uno a uno los elementos hasta encontrar el necesario o revisar sin éxito. Complejidad O(n). Ejemplo: Busco 5 en [1, 4, 5, 6, 7, 9]
- Entro en primer elemento ¿1 = 5? No. Seguimos
- Entro en el segundo ¿4 = 6? No, seguimos.
- Entro en el tercero ¿5 = 5? ¡Si! Encontrado
public static int bus_lineal(int[] valores, int buscar) {
for (int i = 0; i < valores.length; i++) {
if (valores[i] == buscar) {
return i; // ¡Encontrado!
}
}
return -1; // No encontrado
}
// Exemplo de uso:
int[] datos = {5, 12, 7, 9, 3};
int posicion = bus_lineal(datos, 9);
System.out.println("Resultado de la búsqueda: " + posicion);
Búsqueda binaria
Deben de estar ordenados los elementos. Disminuye progresivamente el nº de elementos sobre el que se realiza la busqueda a la mitad. TRas log2n divisiones se locaiza el elemento o se sabe que no está. Complejidad O(log n). Ejemplo: Busco 1 en [1, 4, 5, 7, 9]
- Voy a la mitad. ¿5 > 1? Si, mantengo izquierda.
- Tengo 4 ¿4 > 1? Si, mantengo izquierda
- ¿1 <= 1? ¿1 = 1? Si ¡Encontrado!
public static int bus_binaria(int[] valores, int buscar) {
int izq = 0, dcha = valores.length - 1;
while (izq <= dcha) {
int med = (izq + dcha) / 2;
if (valores[med] == buscar) return med;
if (valores[med] < buscar) izq = med + 1;
else dcha = med - 1;
}
return -1; // No encontrado
}
//Se atopa o valor, imprime a posición:
System.out.println(bus_binaria(new int[]{1, 3, 5, 7, 9, 11}, 7));
Búsqueda mediante hashing
Es un tipo de búsqueda que emplea funciones hash, tranforma un elemento en una dirección o índice situado dentro del array.
Una función hash es una regla matemática que transforma una entrada en un valor, que suelen ser números enteros. Esta entrada puede ser un texto, otro número…
Una clave es el objeto que empleas para acceder a un valor. Por ejemplo en ‘map.put(“58966854I”, “Anxo”)’ la clave sería 58966854I.
El valor obtenido al transformar la clave se denomina HashCode, se calcula usando el método ‘hashCode ()’ y reflejará la posicion del elemento dentro de una tabla hash (La estructura donde se almacenan estos elementos). Si dos elementos tienen el mismo HashCode se produce una colisión.
La posición de la tabla hash donde se guarda el par clave-valor se llama bucket. Para ver en que bucket cae una clave, el HashCode se transforma en un índice y se sigue esta formula: ‘int bucketIndex = hash & (capacity - 1)’. El razonamiento que emplea la formula es el siguiente:
- Obtengo la clave 65954 y la capacidad de mi sistema es 16 (El mínimo en HashMaps)
- Transformo la clave y capacidad a binario, obteniendo 1000 0000 0110 0010 y 00001111 respectivamente
- Como la capacidad tiene 4 bits, me quedo con los últimos 4 bits de la clave (0010)
- Lo transformo a decimal y obtengo que cae en el bucket 2
Las ventajas de este sistema es que los elementos no tienen por que estar ordenados y que es independiente al número de elementos. En la mayor parte de los casos se emplea la clave como el índice de cada registro. La eficiencia del método depende de como de uniforme distrubuye nuestra función hash los elementos.
Ejemplo:
import java.util.HashMap;
public class HashSearchExample2 {
public static void main(String[] args) {
HashMap<Integer, String> estudiantes = new HashMap<>();
estudiantes.put(101, "Ana");
estudiantes.put(202, "Luis");
estudiantes.put(303, "María");
// Buscar
int id = 202;
if (estudiantes.containsKey(id)) {
System.out.println("Alumno encontrado: " + estudiantes.get(id));
} else {
System.out.println("Alumno no registrado");
}
}
}
Para evitar colisiones, se emplean técnicas como truncamiento (Uso los X últimos dígitos de la clave como índice), División (Siendo m el tamaño de la tabla y k la clave, calculo $ℎ(𝑘) = 𝑘 mod 𝑚 $. Por ejemplo: Clave 21 tamaño 10 sería 1) o mediante potencias (Se eleva la clave a un número y se utilizan números del medio. Por ejemplo $589² = 346921 \Longrightarrow 469 $ si cogemos los 3 números del medio).
Programación Dinámica
Técnica de diseño de algoritmos utilizada para resolver problemas que pueden dividirse en subproblemas más pequeños que se repiten. Tiene como objetivo reducir el tiempo de ejecución evitando recalcular soluciones previas. La programación dinámica almacena los resultados mediante un proceso denominado “memorización” o “almacenamiento en tabla”, a diferencia de los algoritmos recursivos clásicos. Este proceso es útil pero se debe equilibrar la optimización de tiempo y espacio según las necesidades del problema, ya que en el caso contrario se puede demasiado almacenamiento. La PD solo funciona cuando el problema completo puede construirse a partir de las soluciones óptimas de sus subproblemas, lo que se denomina subestructura óptima. Hay varios tipos de diseño:
- Top-down que usa recursión para resolver los subproblemas cuando los necesita y guarda sus soluciones en una estructura como diccionarios o arrays
- Bottom-up que empieza por los subproblemas más pequeños y sigue hasta llegar al problema completo (En general más eficiente)
Recursividad
Un método recursivo es aquel que se resuelve llamándose a sí mismo con una versión más simple del problema. Para que funcione:
- Se define un caso base, cuya solución es inmediata y no necesita más llamadas.
- Cada llamada reduce la complejidad del problema, acercándose al caso base.
- Al alcanzarlo, la solución se devuelve hacia atrás, resolviendo las llamadas pendientes hasta llegar a la inicial. La recursión es útil cuando un problema puede expresarse en términos de sí mismo y suele ofrecer soluciones más claras y elegantes.
Ejemplo:
public static int factorial(int n) {
if (n == 1) {
return 1;
}
return n * factorial(n - 1);
}