Un algoritmo es una secuencia finita y ordenada de instrucciones precisas diseñadas para resolver un problema específico o realizar una tarea concreta. En esencia, funciona como una receta de cocina o un manual de instrucciones: si se siguen los pasos en el orden correcto, se llega a un resultado predecible. Este concepto es la base fundamental de la computación, pero también se extiende a las matemáticas, la biología y la organización social.
La importancia de los algoritmos radica en su capacidad para automatizar procesos que, de otra manera, requerirían un esfuerzo mental o físico considerable. Desde el cálculo de una ruta en un mapa digital hasta la clasificación de datos en una hoja de cálculo, los algoritmos transforman datos de entrada en información útil de salida. Comprender cómo funcionan permite no solo usar la tecnología con mayor eficiencia, sino también cuestionar las decisiones que toman los sistemas automáticos que nos rodean.
Definición y concepto
Un algoritmo es una secuencia finita, ordenada y no ambigua de instrucciones diseñadas para resolver un problema específico o realizar una tarea concreta. En esencia, es un plan de acción paso a paso que, al ejecutarse, transforma una entrada inicial en una salida deseada. Esta definición abarca desde las recetas de cocina hasta los complejos cálculos de la computación moderna, aunque el rigor técnico varía según el contexto.
Diferencias conceptuales
A menudo se confunden términos como receta, procedimiento y algoritmo, pero existen matices técnicos cruciales. Una receta es una instrucción práctica, a veces subjetiva (como "cocinar hasta dorar"), mientras que un algoritmo exige precisión absoluta. Un procedimiento puede ser más amplio y a veces abierto, pero un algoritmo debe garantizar que, si se siguen los pasos correctamente, se llega al resultado esperado en un tiempo finito. La distinción radica en la previsibilidad y la estructura lógica estricta.
Propiedades esenciales
Para que una secuencia de instrucciones sea considerada un algoritmo válido, debe cumplir con cinco propiedades fundamentales establecidas en la teoría de la computación:
- Finitud: El proceso debe terminar después de un número finito de pasos. Un algoritmo que gira en bucle infinito sin criterio de parada falla en esta propiedad.
- Definición (No ambigüedad):strong> Cada paso debe estar claramente definido, dejando poco espacio a la interpretación. La instrucción "mezclar bien" puede ser ambigua; "mezclar durante 30 segundos a 500 rpm" es definida.
- Entrada: Un algoritmo puede tener cero o más entradas, que son cantidades dadas inicialmente antes de que comience la ejecución.
- Salida: Debe producir al menos una salida, que está relacionada de manera específica con las entradas.
- Efectividad: Todas las operaciones deben ser básicas lo suficiente para que puedan realizarse, en principio, por una persona usando solo lápiz y papel en un tiempo razonable.
Dato curioso: Aunque asociamos los algoritmos a la era digital, la palabra proviene de Al-Juarismi, un matemático persa del siglo IX cuyo nombre latinizado dio origen a la palabra "algorismo", que luego evolucionó hacia "algoritmo".
Estas propiedades aseguran que el algoritmo sea una herramienta confiable para la resolución de problemas. Sin finitud, el proceso podría durar para siempre; sin definición, dos personas podrían obtener resultados distintos siguiendo las mismas instrucciones. La efectividad garantiza que las operaciones no sean tan complejas que requieran de un genio matemático para resolver cada paso individual.
En el contexto de la computación, estos conceptos son la base sobre la cual se construyen los lenguajes de programación. Un programa de computadora es, en su núcleo, una implementación de uno o más algoritmos. Comprender estas propiedades permite a los estudiantes de secundaria y universidad analizar la eficiencia y la corrección de los métodos que utilizan para resolver problemas, ya sea en matemáticas, ingeniería o ciencias de la datos. La precisión en la definición de cada paso es lo que separa un proceso exitoso de uno lleno de errores.
Historia y evolución del concepto
La palabra "algoritmo" tiene raíces en el nombre del matemático persa Muhammad ibn Musa al-Khwarizmi. Su obra del siglo IX sobre el cálculo algebraico llegó a Europa a través de traducciones latinas. Los eruditos tradujeron su nombre como "Algoritmi". Con el paso de los siglos, el término evolucionó para describir el procedimiento paso a paso que él detallaba para resolver ecuaciones. Esta etimología conecta directamente la historia antigua con la lógica matemática moderna.
El concepto permaneció relativamente intuitivo durante siglos. Los matemáticos lo usaban para describir procesos finitos y definidos. Sin embargo, la necesidad de una definición precisa surgió con la crisis de los fundamentos de las matemáticas. La pregunta era: ¿qué hace que un procedimiento sea considerado "efectivo"?
Formalización matemática
En la década de 1930, tres matemáticos llegaron a definiciones casi simultáneas. Alonzo Church propuso el cálculo lambda. Stephen Cole Kleene desarrolló las funciones recursivas. Alan Turing introdujo la famosa máquina de estado finito. El modelo de Turing es quizás el más intuitivo. Imagina una cinta infinita dividida en casillas. Una cabeza de lectura lee y escribe símbolos. Según el símbolo y el estado interno, la máquina se mueve a la izquierda o a la derecha. Este mecanismo simple captura la esencia del cálculo.
Dato curioso: La máquina de Turing es un modelo abstracto. Ninguna computadora física ha usado exactamente una cinta infinita y una sola cabeza de lectura. Sin embargo, casi toda la arquitectura de hardware actual se basa en sus principios lógicos fundamentales.
La hipótesis de Church-Turing unificó estas ideas. Afirma que cualquier función que pueda ser calculada por un procedimiento efectivo puede ser calculada por una máquina de Turing. Esto estableció el límite teórico de la computación. No se trata de velocidad, sino de capacidad lógica. El modelo demuestra que la complejidad del cálculo puede reducirse a operaciones básicas.
De la mente a la máquina
Antes de la formalización, el cálculo dependía de la memoria humana. Los astrónomos usaban tablas de logaritmos para simplificar las multiplicaciones. Los ingenieros usaban la regla de cálculo. Estos eran algoritmos ejecutados por manos y ojos. La transición hacia la automatización fue gradual. La máquina analítica de Charles Babbage fue un diseño pionero. Aunque nunca se terminó completamente, estableció la estructura de entrada, proceso y salida.
Ada Lovelace vio más allá del mero cálculo numérico. En sus notas sobre la máquina de Babbage, describió una secuencia de operaciones para calcular los números de Bernoulli. Muchos historiadores la consideran la primera programadora. Ella entendió que, si los datos eran correctos, la máquina podría manipular cualquier cosa representable por números. Esta visión anticipó la computación moderna por casi un siglo.
El desarrollo de la electrónica aceleró la evolución. Las primeras computadoras usaban válvulas de vacío. Más tarde, los transistores redujeron el tamaño y aumentaron la velocidad. Hoy, los algoritmos gobiernan desde la navegación satelital hasta el procesamiento del lenguaje natural. La lógica de Al-Khwarizmi sigue vigente. Solo ha cambiado la velocidad de ejecución.
¿Cómo se analizan los algoritmos?
Analizar un algoritmo no se trata solo de saber si funciona, sino de entender cuánto "cuesta" ejecutarlo a medida que crece la cantidad de datos. Esta evaluación se conoce como complejidad computacional. No basta con medir el tiempo en segundos en una computadora moderna, ya que mañana podría ser el doble de rápida. Lo fundamental es observar cómo escala el rendimiento cuando el conjunto de datos se duplica o se multiplica por diez.
Notación Big O y la eficiencia
Para estandarizar estas mediciones, los informáticos utilizan la Notación Big O. Esta herramienta matemática describe el límite superior del crecimiento de un algoritmo. En lugar de decir "toma 5 segundos", decimos "su tiempo crece linealmente con los datos". Esto permite comparar dos métodos independientemente del hardware o del lenguaje de programación utilizado.
Dato curioso: La notación Big O proviene del análisis matemático clásico, donde la letra "O" representa a "Orden". Fue adoptada por la informática teórica a finales del siglo XX para simplificar discusiones complejas sobre escalabilidad.
Es crucial distinguir entre dos recursos limitados: el tiempo de ejecución y el espacio de memoria. Un algoritmo puede ser rapidísimo pero consumir toda la memoria RAM disponible, o viceversa. A menudo, se intercambia memoria por velocidad: guardar resultados intermedios (más espacio) evita volver a calcularlos (menos tiempo). La elección depende de si el cuello de botella es el procesador o la memoria.
Tipos de complejidad comunes
Existen varias formas en que los algoritmos escalan. Comprender estas diferencias es vital para elegir la mejor herramienta.
La complejidad lineal ocurre cuando el esfuerzo aumenta proporcionalmente al tamaño de la entrada. Si tienes el doble de elementos, tardas el doble de tiempo. Un ejemplo clásico es buscar un nombre en una lista desordenada: en el peor caso, debes revisar cada uno de los n elementos. Su fórmula se expresa así:
O(n)La complejidad cuadrática es más exigente. Aquí, si duplicas los datos, el tiempo se cuadruplica. Esto sucede a menudo cuando se comparan todos los elementos entre sí, como en un bucle anidado dentro de otro bucle. Es común en ordenamientos simples como el de burbuja:
O(n2)Por el contrario, la complejidad logarítmica es muy eficiente. El tiempo crece lentamente incluso con enormes cantidades de datos. Esto ocurre cuando el algoritmo divide el problema a la mitad en cada paso, como al buscar una palabra en un diccionario ordenado (búsqueda binaria):
O(logn)La eficiencia no es un lujo, es una necesidad. En sistemas con millones de usuarios, la diferencia entre un algoritmo cuadrático y uno lineal puede significar la diferencia entre cargar la página en un segundo o en un minuto. Elegir bien el algoritmo ahorra dinero, energía y tiempo de espera para el usuario final.
Clasificación y tipos de algoritmos
Los algoritmos se clasifican según la estrategia que emplean para resolver un problema específico. Esta categorización permite a los programadores seleccionar la herramienta más eficiente según los recursos disponibles, como tiempo de ejecución o memoria RAM. No existe una única solución óptima para todos los casos; la elección depende de la naturaleza de los datos de entrada.
Estrategias fundamentales
Los algoritmos de búsqueda localizan elementos dentro de una estructura de datos. La búsqueda lineal revisa cada elemento secuencialmente, mientras que la búsqueda binaria divide el espacio de búsqueda a la mitad en cada paso, requiriendo que los datos estén previamente ordenados. Esta última es significativamente más rápida para conjuntos grandes.
El ordenamiento organiza datos en un orden específico, como numérico o alfabético. Algoritmos como el Quick Sort o el Merge Sort utilizan la técnica de "divide y vencerás", descomponiendo el problema en subproblemas más pequeños. Por otro lado, los algoritmos recursivos llaman a sí mismos para resolver instancias menores del mismo problema, hasta alcanzar un caso base que detiene la recursión.
Dato curioso: La recursión puede volverse costosa en memoria si no se gestiona bien, ya que cada llamada se "apila" en la memoria del procesador. Un error común es olvidar el caso base, provocando un bucle infinito conocido como desbordamiento de pila.
La programación dinámica resuelve problemas complejos dividiéndolos en subproblemas más simples y almacenando sus resultados para evitar cálculos repetidos. Es ideal cuando los subproblemas se superponen. En contraste, los algoritmos voraces (greedy) toman la mejor decisión local en cada paso, esperando que esto lleve a la mejor solución global. No siempre garantizan el óptimo absoluto, pero suelen ser rápidos y sencillos de implementar.
Determinismo vs. No determinismo
Un algoritmo determinista produce siempre la misma salida para una misma entrada, siguiendo una secuencia predecible de pasos. La mayoría de los algoritmos básicos son deterministas. Los algoritmos no deterministas, en cambio, pueden tomar diferentes caminos en cada ejecución, a menudo utilizando el azar o múltiples hipótesis simultáneas. Esto es común en la inteligencia artificial y en problemas de optimización complejos, donde encontrar la solución exacta es costoso.
| Tipo de Algoritmo | Estrategia Principal | Ejemplo Común | Uso Típico |
|---|---|---|---|
| Búsqueda Binaria | Dividir y conquistar | Encontrar una palabra en un diccionario | Listas ordenadas |
| Quick Sort | Ordenamiento por partición | Ordenar una lista de notas | Ordenamiento general |
| Recursivo | Llamada a sí mismo | Factorial de un número | Problemas con estructura repetitiva |
| Programación Dinámica | Memoización de subproblemas | Secuencia de Fibonacci | Optimización con subproblemas superpuestos |
| Voraz (Greedy) | Mejor decisión local inmediata | Cambio de monedas | Optimización rápida |
| No Determinista | Elección múltiple o aleatoria | Algoritmos genéticos | Optimización compleja y IA |
La complejidad computacional varía entre estos tipos. Un algoritmo voraz puede ser más rápido que uno de programación dinámica, pero sacrifica precisión por velocidad. Entender estas diferencias es crucial para optimizar el rendimiento del software. La elección incorrecta puede resultar en tiempos de carga excesivos o consumo innecesario de memoria.
Ejercicios resueltos
Ordenamiento por burbuja (Bubble Sort)
Este algoritmo recorre repetidamente la lista, compara elementos adyacentes y los intercambia si están en el orden incorrecto. El proceso se repite hasta que no haya más intercambios. Tomemos la lista [5, 1, 4, 2, 8]. En la primera pasada, se comparan 5 y 1; al ser 5 > 1, se intercambian, resultando en [1, 5, 4, 2, 8]. Luego se comparan 5 y 4, intercambiándolos para obtener [1, 4, 5, 2, 8]. El 5 sigue avanzando hasta encontrar el 8. Tras la primera pasada completa, el mayor elemento (8) queda fijo al final. La segunda pasada ordena el resto, dejando el 5 en su lugar. Este método es intuitivo pero ineficiente para listas grandes, ya que realiza comparaciones redundantes.
Búsqueda binaria
La búsqueda binaria requiere una lista ordenada. Se compara el elemento buscado con el valor central. Si coinciden, se encuentra el dato. Si el buscado es menor, se busca en la mitad izquierda; si es mayor, en la derecha. Consideremos la lista ordenada [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] y busquemos el número 23. El índice central inicial es 4 (valor 16). Como 23 > 16, descartamos la mitad izquierda y nos centramos en [23, 38, 56, 72, 91]. El nuevo centro es 38. Como 23 < 38, miramos la izquierda: [23]. El centro es 23. Coincide. Se encontró en tres pasos. La eficiencia radica en reducir el espacio de búsqueda a la mitad en cada iteración.
Complejidad algorítmica
La complejidad mide cómo crece el tiempo de ejecución respecto al tamaño de la entrada n. Analicemos el algoritmo de búsqueda lineal en una lista de n elementos. En el peor caso, el elemento está al final o falta, requiriendo n comparaciones. El tiempo de ejecución es proporcional a n, lo que se denota como O(n). Para la búsqueda binaria, cada paso divide n por 2. El número de pasos necesarios es el logaritmo en base 2 de n. La fórmula es:
log2(n) Esto significa que para 1.024 elementos, solo se necesitan aproximadamente 10 comparaciones en el peor caso, frente a 1.024 en la búsqueda lineal. Esta diferencia es crítica en grandes volúmenes de datos.Dato curioso: La búsqueda binaria fue formalizada por John Mauchly en 1946, pero no fue hasta 1962 cuando Knuth demostró que, a pesar de su simplicidad, muchos programadores escribían la implementación con errores sutiles de desbordamiento de enteros.
Pseudocódigo de ejemplo
El pseudocódigo ayuda a visualizar la lógica sin depender de un lenguaje específico. Para la búsqueda binaria:
- Inicializar bajo = 0, alto = longitud - 1.
- Mientras bajo <= alto:
- Calcular medio = (bajo + alto) / 2.
- Si lista[medio] == objetivo, devolver medio.
- Si lista[medio] < objetivo, bajo = medio + 1.
- Si lista[medio] > objetivo, alto = medio - 1.
- Devolver -1 si no se encuentra.
Este enfoque estructurado permite traducir la lógica a cualquier lenguaje de programación con mínima fricción. La claridad en los pasos evita errores lógicos comunes.
Aplicaciones prácticas y ejemplos
Los algoritmos no residen exclusivamente en el código fuente de un ordenador; operan en capas casi invisibles de la vida moderna. Su función principal es transformar datos de entrada en decisiones o resultados de salida mediante una secuencia lógica. Comprender su funcionamiento revela cómo se estructura la información que consumimos a diario.
Algoritmos en la vida cotidiana
El sistema de posicionamiento global (GPS) es un ejemplo de algoritmos de búsqueda de caminos. No se limita a medir distancias lineales, sino que pondera variables como el tráfico en tiempo real, los límites de velocidad y el tipo de vía. El algoritmo de Dijkstra, por citar un caso clásico, evalúa nodos en una red para encontrar la ruta más eficiente. La consecuencia es directa: el tiempo de viaje se optimiza dinámicamente.
En las redes sociales, los algoritmos de recomendación determinan qué contenido aparece en la pantalla del usuario. Estos sistemas analizan el historial de interacciones —me gusta, comentarios, tiempo de visualización— para predecir la relevancia futura. No muestran necesariamente lo que más te gusta, sino lo que más probablemente te haga hacer clic. Esto genera el efecto de "burbuja de filtros", donde la diversidad de opiniones puede reducirse.
La criptografía depende de algoritmos para garantizar la seguridad de las transacciones digitales. El cifrado simétrico y asimétrico utiliza operaciones matemáticas complejas para convertir texto legible en datos aparentemente aleatorios. Sin estos procesos, cada correo electrónico o compra en línea estaría expuesta a miradas externas. La confianza en la economía digital se basa en esta capa de abstracción lógica.
Dato curioso: El algoritmo de ordenamiento "Quicksort", creado en 1959, sigue siendo uno de los más utilizados en ordenadores modernos debido a su eficiencia media, demostrando que la simplicidad lógica a menudo vence a la complejidad computacional.
El algoritmo de Euclides en matemáticas
Uno de los algoritmos más antiguos y fundamentales es el de Euclides, utilizado para hallar el máximo común divisor (MCD) de dos números. Este método evita la descomposición completa en factores primos, ahorrando tiempo de cálculo. El proceso se basa en la división sucesiva: se divide el número mayor entre el menor, y luego se repite la operación con el divisor anterior y el resto obtenido.
La fórmula recursiva que define este proceso es:
MCD(a,b)=MCD(b,amodb)El proceso continúa hasta que el resto es cero. El último divisor no nulo es el MCD. Este algoritmo es la base de la fracción irreducible y tiene aplicaciones en teoría de números y criptografía moderna. Su elegancia radica en reducir un problema complejo a una operación repetitiva simple.
Importancia en la inteligencia artificial
La inteligencia artificial (IA) y el aprendizaje automático (machine learning) son esencialmente colecciones de algoritmos que mejoran con la experiencia. Los algoritmos de aprendizaje supervisado, como la regresión lineal o los árboles de decisión, buscan patrones en conjuntos de datos etiquetados. El objetivo es minimizar el error entre la predicción del modelo y el resultado real.
En el aprendizaje profundo, las redes neuronales utilizan algoritmos de retropropagación para ajustar millones de parámetros. Cada capa de la red transforma los datos de entrada mediante funciones de activación. Esto permite a la máquina "aprender" características abstractas, como bordes en una imagen o sílabas en un sonido. La potencia de la IA no está en la inteligencia intrínseca, sino en la capacidad de los algoritmos para procesar grandes volúmenes de datos con precisión.
La transparencia de estos algoritmos sigue siendo un desafío. A menudo funcionan como "cajas negras", donde la entrada y la salida son claras, pero el camino intermedio es difícil de interpretar para el ser humano. Entender estos mecanismos es crucial para evaluar sesgos y tomar decisiones informadas en la era digital.
Limitaciones y paradojas algorítmicas
Los algoritmos no son soluciones mágicas a toda problemática computacional. Tienen límites intrínsecos que definen lo que una máquina puede y no puede resolver. Comprender estas fronteras es fundamental para evitar la sobreconfianza tecnológica.
El límite teórico: El problema de la parada
Existe un límite fundamental en la capacidad de predicción de las máquinas. El problema de la parada, formulado por Alan Turing en 1936, demuestra que no existe un algoritmo universal capaz de determinar si cualquier otro algoritmo terminará su ejecución o se quedará atrapado en un bucle infinito.
Este resultado no es una falta de potencia de procesamiento, sino una propiedad lógica de los sistemas formales. La consecuencia es directa: hay problemas que son computables, pero no todos son decidibles por un único procedimiento general. Esto obliga a los ingenieros a diseñar estrategias específicas para cada caso, en lugar de buscar una solución única para todos.
Exactitud frente a eficiencia práctica
En la teoría de la complejidad, se clasifica a los algoritmos según cómo escalan con el tamaño de los datos. Un algoritmo exacto garantiza la mejor solución posible, pero puede requerir un tiempo exponencial. Por otro lado, los algoritmos heurísticos sacrifican la certeza absoluta por la velocidad de cálculo.
Dato curioso: En el famoso problema del viajante, encontrar la ruta más corta entre 20 ciudades es rápido, pero para 20 ciudades, el número de rutas posibles supera el número de átomos en el universo observable si se calcula por fuerza bruta pura.
Los heurísticos, como los algoritmos genéticos o el descenso de gradiente, son esenciales en la inteligencia artificial moderna. No prometen el óptimo global, sino una solución "suficientemente buena" en un tiempo razonable. Esta distinción es crucial: la eficiencia teórica (notación Big O) no siempre se traduce en eficiencia práctica debido a factores como la memoria caché o la paralelización.
El sesgo algorítmico: más allá del código
Los algoritmos no operan en el vacío; se alimentan de datos históricos y decisiones humanas. Si los datos de entrenamiento contienen desigualdades sociales, el algoritmo las aprenderá y las amplificará. Esto se conoce como sesgo algorítmico.
Un ejemplo clásico es la selección de personal automatizada. Si un algoritmo se entrena con datos de contrataciones de los últimos diez años en una industria dominada por hombres, el modelo puede aprender a penalizar implícitamente a las mujeres, asociando la palabra "mujer" con una menor probabilidad de éxito. El código puede ser matemáticamente perfecto, pero el resultado es socialmente sesgado.
Además, la opacidad de los modelos complejos, como las redes neuronales profundas, dificulta la auditoría. A menudo, la caja negra toma decisiones difíciles de explicar incluso para sus creadores. La transparencia en la recopilación de datos y la validación continua son necesarias para mitigar estos efectos, pero no los eliminan por completo. Reconocer que un algoritmo es una herramienta con defectos, y no una verdad absoluta, es el primer paso hacia su uso crítico.
Preguntas frecuentes
¿Todo el mundo usa algoritmos sin saberlo?
Sí. Cada vez que usas una aplicación de navegación para evitar el tráfico, ves recomendaciones en una serie de streaming o buscas una palabra en un diccionario, estás ejecutando o siendo procesado por un algoritmo. Están integrados en la rutina diaria de forma casi invisible.
¿Un algoritmo siempre da el mismo resultado?
Depende del tipo. Los algoritmos deterministas producen siempre el mismo resultado si las entradas son iguales. Sin embargo, los algoritmos aleatorios o probabilísticos pueden variar ligeramente su resultado o su tiempo de ejecución, aunque suelen acercarse a la solución óptima.
¿Cuál es la diferencia entre un dato y un algoritmo?
El dato es la materia prima (por ejemplo, una lista de números desordenados). El algoritmo es el proceso que transforma esos datos (por ejemplo, el método para ordenarlos de menor a mayor). Sin el algoritmo, los datos suelen ser información cruda y poco útil.
¿Los algoritmos toman decisiones por nosotros?
Los algoritmos procesan información para sugerir o seleccionar opciones, pero la decisión final suele depender de cómo se diseñó el algoritmo y qué datos se le alimentaron. Por ejemplo, un algoritmo de selección de personal puede filtrar currículos, pero la contratación final puede depender de un juicio humano o de la ponderación de variables definidas previamente.
¿Puede un algoritmo estar "equivocado"?
Sí. Un algoritmo puede ser correcto lógicamente pero producir resultados inesperados si los datos de entrada tienen errores (el famoso principio "basura dentro, basura fuera"). Además, puede tener sesgos si fue entrenado con datos históricos que reflejaban prejuicios humanos.
Resumen
Los algoritmos son procedimientos paso a paso para resolver problemas, fundamentales en la computación y las ciencias. Su estudio abarca desde su origen histórico en las matemáticas hasta su análisis de eficiencia mediante la notación asintótica. Conocer los tipos de algoritmos y sus limitaciones es esencial para aplicarlos correctamente en la vida real y en la tecnología actual.
Véase también
- Cómo funcionan los logaritmos
- Eliminación de Gauss-Jordan
- Geometría diferencial
- Ángulos suplementarios
- Lema de Schwarz
- Resta de vectores
- Cálculo y análisis matemático
- Qué son los logaritmos en matemáticas