JavaRush /Blog Java /Random-ES /Complejidad del algoritmo

Complejidad del algoritmo

Publicado en el grupo Random-ES
¡Hola! La conferencia de hoy será un poco diferente del resto. Se diferenciará en que sólo está indirectamente relacionado con Java. Complejidad del algoritmo - 1Sin embargo, este tema es muy importante para todo programador. Hablaremos de algoritmos . ¿Qué es un algoritmo? En términos simples, se trata de una determinada secuencia de acciones que se deben realizar para lograr el resultado deseado . A menudo utilizamos algoritmos en la vida cotidiana. Por ejemplo, cada mañana te enfrentas a una tarea: ir a la escuela o al trabajo y al mismo tiempo ser:
  • Vestido
  • Limpio
  • Bien alimentado
¿ Qué algoritmo te permitirá lograr este resultado?
  1. Despierta con un despertador.
  2. Date una ducha, lávate la cara.
  3. Preparar el desayuno, preparar café/té.
  4. Comer.
  5. Si no has planchado tu ropa desde la noche, plánchala.
  6. Vestirse.
  7. Sal de la casa.
Esta secuencia de acciones definitivamente le permitirá obtener el resultado deseado. En programación, el objetivo de nuestro trabajo es resolver problemas constantemente. Una parte importante de estas tareas se puede realizar utilizando algoritmos ya conocidos. Por ejemplo, se enfrenta a una tarea: ordenar una lista de 100 nombres en una matriz . Este problema es bastante sencillo, pero se puede solucionar de diferentes formas. Aquí hay una solución: Algoritmo para ordenar nombres alfabéticamente:
  1. Compre o descargue de Internet el “Diccionario de nombres personales rusos”, edición de 1966.
  2. Encuentre todos los nombres de nuestra lista en este diccionario.
  3. Anota en una hoja de papel en qué página del diccionario se encuentra el nombre.
  4. Ordena los nombres usando las notas en una hoja de papel.
¿Esta secuencia de acciones nos permitirá resolver nuestro problema? Sí, lo permitirá completamente. ¿ Será efectiva esta solución ? Difícilmente. Aquí llegamos a otra propiedad muy importante de los algoritmos: su eficiencia . El problema se puede solucionar de diferentes formas. Pero tanto en la programación como en la vida cotidiana, elegimos el método que nos resulte más eficaz. Si tu tarea es hacer un sándwich con mantequilla, puedes, por supuesto, empezar sembrando trigo y ordeñando una vaca. Pero esta será una solución ineficaz : llevará mucho tiempo y costará mucho dinero. Para resolver su problema simple, simplemente puede comprar pan y mantequilla. Y el algoritmo con trigo y vaca, aunque permite solucionar el problema, es demasiado complejo para aplicarlo en la práctica. Para evaluar la complejidad de los algoritmos en programación, se creó una notación especial llamada Big-O ("big O") . Big-O le permite estimar en qué medida depende el tiempo de ejecución de un algoritmo de los datos que se le pasan . Veamos el ejemplo más simple: la transferencia de datos. Imagine que necesita transmitir cierta información en forma de archivo a una larga distancia (por ejemplo, 5000 kilómetros). ¿Qué algoritmo será el más eficiente? Depende de los datos con los que tenga que trabajar. Por ejemplo, tenemos un archivo de audio de 10 megas de tamaño. Complejidad del algoritmo - 2En este caso, el algoritmo más eficaz sería transferir el archivo a través de Internet. ¡Solo te llevará un par de minutos! Entonces, repitamos nuestro algoritmo: "Si necesita transferir información en forma de archivos a una distancia de 5000 kilómetros, debe utilizar la transmisión de datos a través de Internet". Excelente. Ahora analicémoslo. ¿Resuelve nuestro problema? En general sí, lo soluciona por completo. Pero ¿qué puedes decir sobre su complejidad? Mmmm, aquí es donde las cosas se ponen interesantes. El hecho es que nuestro algoritmo depende en gran medida de los datos entrantes, es decir, del tamaño de los archivos. Ahora tenemos 10 megas y todo está bien. ¿Qué pasa si necesitamos transferir 500 megas? ¿20 gigas? ¿500 terabytes? ¿30 petabytes? ¿Nuestro algoritmo dejará de funcionar? No, todas estas cantidades de datos aún se pueden transferir. ¿Tardará más tiempo en completarse? ¡Sí, lo será! Ahora conocemos una característica importante de nuestro algoritmo: cuanto mayor sea el tamaño de los datos a transferir, más tardará el algoritmo en completarse . Pero nos gustaría entender con mayor precisión cómo es esta relación (entre el tamaño de los datos y el tiempo que lleva transferirlos). En nuestro caso, la complejidad del algoritmo será lineal.. "Lineal" significa que a medida que aumenta el volumen de datos, el tiempo de transmisión aumentará aproximadamente proporcionalmente. Si hay 2 veces más datos, tomará 2 veces más tiempo transferirlos. Si hay 10 veces más datos, el tiempo de transferencia aumentará 10 veces. Usando la notación Big-O, la complejidad de nuestro algoritmo se define como O(N) . Es mejor recordar esta notación para referencia futura; siempre se usa para algoritmos con complejidad lineal. Tenga en cuenta: no estamos hablando aquí en absoluto de varias cosas "variables": la velocidad de Internet, la potencia de nuestra computadora, etc. Al evaluar la complejidad de un algoritmo, esto simplemente no tiene sentido; de todos modos, no tenemos control sobre él. Big-O evalúa el algoritmo en sí, independientemente del "entorno" en el que tendrá que funcionar. Sigamos con nuestro ejemplo. Digamos que finalmente resulta que el tamaño de los archivos a transferir es de 800 terabytes. Si los transmitimos a través de Internet, el problema, por supuesto, se solucionará. Sólo hay un problema: la transmisión a través de un enlace moderno estándar (a 100 megabits por segundo) que la mayoría de nosotros utilizamos en nuestros hogares tardará aproximadamente 708 días. ¡Casi 2 años! :O Entonces, nuestro algoritmo claramente no es adecuado aquí. ¡Necesitamos alguna otra solución! ¡De repente, el gigante informático Amazon viene en nuestra ayuda! ¡Su servicio Amazon Snowmobile le permite cargar grandes cantidades de datos en unidades de almacenamiento móviles y entregarlas en camión a la dirección deseada! Complejidad del algoritmo - 3¡Así que tenemos un nuevo algoritmo! "Si necesita transferir información en forma de archivos a una distancia de 5.000 kilómetros y el proceso tardará más de 14 días cuando se transfiere a través de Internet, debe utilizar el transporte por camión de Amazon". La cifra de 14 días se eligió aquí al azar: digamos que este es el período máximo que podemos permitirnos. Analicemos nuestro algoritmo. ¿Qué pasa con la velocidad? Incluso si el camión viaja a sólo 50 km/h, recorrerá 5.000 kilómetros en sólo 100 horas. ¡Eso es poco más de cuatro días! Esto es mucho mejor que la opción de transmisión por Internet. ¿Qué pasa con la complejidad de este algoritmo? ¿Será también lineal, O(N)? No, no lo hará. Al fin y al cabo, al camión no le importa cuánto lo cargue: seguirá circulando aproximadamente a la misma velocidad y llegará a tiempo. Ya sea que tengamos 800 terabytes o 10 veces más datos, el camión llegará allí en 5 días. En otras palabras, el algoritmo para entregar datos por camión tiene una complejidad constante . "Constante" significa que no depende de los datos pasados ​​al algoritmo. Coloque una unidad flash de 1 GB en el camión y llegará en 5 días. Pon ahí discos con 800 terabytes de datos y te llegará en 5 días. Cuando se usa Big-O, la complejidad constante se denota como O(1) . Desde que nos familiarizamos con O(N) yO(1) , veamos ahora más ejemplos de “programadores” :) Digamos que se le proporciona una matriz de 100 números y la tarea es imprimir cada uno de ellos en la consola. Escribes un bucle regular forque realiza esta tarea.
int[] numbers = new int[100];
// ..заполняем массив числами

for (int i: numbers) {
   System.out.println(i);
}
¿Cuál es la complejidad del algoritmo escrito? Lineal, O(N). La cantidad de acciones que debe realizar el programa depende exactamente de cuántos números se le pasaron. Si hay 100 números en la matriz, habrá 100 acciones (salidas en la pantalla). Si hay 10,000 números en la matriz, será necesario realizar 10,000 acciones. ¿Se puede mejorar nuestro algoritmo? No. En cualquier caso tendremos que hacer N pasos por el array y realizar N salidas a la consola. Veamos otro ejemplo.
public static void main(String[] args) {

   LinkedList<Integer> numbers = new LinkedList<>();
   numbers.add(0, 20202);
   numbers.add(0, 123);
   numbers.add(0, 8283);
}
Tenemos uno vacío LinkedListen el que insertamos varios números. Necesitamos estimar la complejidad del algoritmo para insertar un solo número en LinkedListnuestro ejemplo y cómo depende de la cantidad de elementos en la lista. La respuesta es O(1): complejidad constante . ¿Por qué? Tenga en cuenta: cada vez que insertamos un número al principio de la lista. Además, como recordará, al insertar números en LinkedListelementos, no se desplazan a ninguna parte: los enlaces se redefinen (si de repente olvidó cómo funciona LinkedList, eche un vistazo a una de nuestras conferencias antiguas ). Si ahora el primer número de nuestra lista es el número х, e insertamos el número y al principio de la lista, todo lo que se necesita es:
x.previous  = y;
y.previous = null;
y.next = x;
Para esta redefinición de referencia, no nos importa cuántos números haya ahoraLinkedList : al menos uno, al menos mil millones. La complejidad del algoritmo será constante: O(1).

Complejidad logarítmica

¡No entrar en pánico! :) Si la palabra "logarítmico" te hace querer cerrar la conferencia y no seguir leyendo, espera un par de minutos. No habrá dificultades matemáticas aquí (hay muchas explicaciones de este tipo en otros lugares) y analizaremos todos los ejemplos "con los dedos". Imagina que tu tarea es encontrar un número específico en una matriz de 100 números. Más precisamente, compruebe si existe. Tan pronto como se encuentre el número requerido, se debe detener la búsqueda y en la consola debe aparecer la entrada "¡Se ha encontrado el número requerido!". Su índice en la matriz = ....” ¿Cómo resolverías un problema así? Aquí la solución es obvia: es necesario recorrer los elementos de la matriz uno por uno, comenzando por el primero (o el último) y comprobar si el número actual coincide con el deseado. En consecuencia, la cantidad de acciones depende directamente de la cantidad de elementos de la matriz. Si tenemos 100 números, entonces debemos ir al siguiente elemento 100 veces y verificar si el número coincide 100 veces. Si hay 1000 números, entonces habrá 1000 pasos de verificación. Esto es obviamente complejidad lineal, O(N) . Ahora agregaremos una aclaración a nuestro ejemplo: la matriz en la que necesita encontrar un número está ordenada en orden ascendente . ¿Esto cambia algo para nuestra tarea? Todavía podemos buscar el número deseado por fuerza bruta. Pero en su lugar podemos utilizar el conocido algoritmo de búsqueda binaria . Complejidad del algoritmo - 5En la fila superior de la imagen vemos una matriz ordenada. En él necesitamos encontrar el número 23. En lugar de iterar sobre los números, simplemente dividimos la matriz en 2 partes y verificamos el número promedio en la matriz. Encontramos el número que se encuentra en la celda 4 y lo verificamos (segunda fila en la imagen). Este número es 16 y estamos buscando 23. El número actual es menor. ¿Qué quiere decir esto? Que no es necesario comprobar todos los números anteriores (que se encuentran hasta el número 16) : ¡definitivamente serán menores que el que estamos buscando, porque nuestra matriz está ordenada! Continuamos la búsqueda entre los 5 elementos restantes. Prestar atención:Sólo hemos hecho una comprobación, pero ya hemos eliminado la mitad de las opciones posibles. Sólo nos quedan 5 elementos. Repetiremos nuestro paso: nuevamente dividimos la matriz restante entre 2 y nuevamente tomamos el elemento del medio (línea 3 en la figura). Este número es 56 y es mayor que el que estamos buscando. ¿Qué quiere decir esto? Que descartamos 3 opciones más: el número 56 en sí y dos números después (definitivamente son mayores que 23, porque la matriz está ordenada). Solo nos quedan 2 números para verificar (la última fila de la figura): números con índices de matriz 5 y 6. Verificamos el primero de ellos, y esto es lo que estábamos buscando: ¡el número 23! ¡Su índice = 5! Miremos los resultados de nuestro algoritmo y luego comprenderemos su complejidad. (Por cierto, ahora entiendes por qué se llama binario: su esencia es dividir constantemente los datos entre 2). ¡El resultado es impresionante! Si buscáramos el número deseado usando la búsqueda lineal, necesitaríamos 10 comprobaciones, ¡pero con la búsqueda binaria lo hicimos en 3! En el peor de los casos, serían 4 si en el último paso el número que necesitábamos fuera el segundo y no el primero. ¿Qué pasa con su complejidad? Este es un punto muy interesante :) El algoritmo de búsqueda binaria depende mucho menos del número de elementos de la matriz que el algoritmo de búsqueda lineal (es decir, enumeración simple). Con 10 elementos en la matriz, la búsqueda lineal necesitará un máximo de 10 comprobaciones y la búsqueda binaria necesitará un máximo de 4 comprobaciones. La diferencia es 2,5 veces. Pero para una matriz de 1000 elementos, la búsqueda lineal necesitará 1000 comprobaciones y la búsqueda binaria necesitará solo 10 . ¡La diferencia ya es 100 veces! Prestar atención:el número de elementos en la matriz aumentó 100 veces (de 10 a 1000), y el número de comprobaciones necesarias para la búsqueda binaria aumentó solo 2,5 veces, de 4 a 10. Si llegamos a 10.000 elementos , la diferencia es aún más impresionante: 10.000 comprobaciones de búsqueda lineal y un total de 14 comprobaciones de búsqueda binaria. Y nuevamente: el número de elementos aumentó 1000 veces (de 10 a 10000), pero el número de controles aumentó solo 3,5 veces (de 4 a 14). La complejidad del algoritmo de búsqueda binaria es logarítmica o, en notación O grande, O(log n) . ¿Por qué se llama así? Un logaritmo es la inversa de la exponenciación. El logaritmo binario se utiliza para calcular la potencia de 2. Por ejemplo, tenemos 10.000 elementos que debemos analizar mediante una búsqueda binaria. Complejidad del algoritmo - 6Ahora tienes una imagen ante tus ojos y sabes que para ello se requieren un máximo de 14 comprobaciones. Pero, ¿qué pasa si no hay ninguna imagen ante tus ojos y necesitas contar el número exacto de comprobaciones necesarias? Basta responder a una pregunta sencilla: ¿ a qué potencia se debe elevar el número 2 para que el resultado obtenido sea >= el número de elementos que se están comprobando? Para 10000 será la decimocuarta potencia. 2 elevado a la 13.ª potencia es demasiado pequeño (8192) Pero 2 elevado a la 14.ª potencia = 16384 , este número satisface nuestra condición (es >= el número de elementos de la matriz). Encontramos el logaritmo: 14. ¡Ésta es la cantidad de controles que necesitamos! :) Los algoritmos y su complejidad son un tema demasiado amplio para incluirlo en una sola conferencia. Pero saberlo es muy importante: en muchas entrevistas recibirás problemas algorítmicos. Para la teoría, puedo recomendarte varios libros. Un buen lugar para comenzar es " Algoritmos de Grocking ": aunque los ejemplos del libro están escritos en Python, el lenguaje y los ejemplos del libro son muy simples. La mejor opción para un principiante y además tiene un volumen pequeño. Lectura más seria: libros de Robert Laforet y Robert Sedgwick . Ambos están escritos en Java, lo que te facilitará un poco el aprendizaje. Después de todo, ¡estás bastante familiarizado con este idioma! :) Para estudiantes con buena formación matemática, la mejor opción sería el libro de Thomas Corman . ¡Pero no quedará satisfecho sólo con la teoría! “Saber” != “poder” Puedes practicar la resolución de problemas de algoritmos en HackerRank y Leetcode . Los problemas de allí se utilizan a menudo incluso durante las entrevistas en Google y Facebook, por lo que definitivamente no te aburrirás :) Para reforzar el material de la conferencia, te aconsejo que veas un excelente video sobre Big-O en YouTube. ¡Nos vemos en las próximas conferencias! :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION