Algoritmos

Un algoritmo es un conjunto ordenado y finito de operaciones que deben seguirse para resolver un problema.

Es un conjunto ordenado de operaciones, lo que quiere decir que es una cadena de instrucciones precisas que deben seguirse por orden.

Su objetivo es resolver un problema, lo que significa que tiene un objetivo delimitado.

Cuando escribimos un algoritmo, lo hacemos para que produzca un resultado. No se trata tan sólo de escribir un bonito conjunto de órdenes que no conduzcan a ninguna parte, sino que se hace racionalmente y con un objetivo determinado.

Búsqueda binaria

La búsqueda binaria es un algoritmo eficiente para encontrar un elemento en una lista ordenada de elementos. Funciona al dividir repetidamente a la mitad la porción de la lista que podría contener al elemento, hasta reducir las ubicaciones posibles a solo una.

Al describir un algoritmo para un ser humano, a menudo es suficiente una descripción incompleta. Algunos detalles pueden quedar fuera de una receta de un pastel; la receta supone que sabes cómo abrir el refrigerador para sacar los huevos y que sabes cómo romper los huevos. La gente puede saber intuitivamente cómo completar los detalles faltantes, pero los programas de computadora no. Es por eso que necesitamos describir completamente los algoritmos computacionales.

Para poder implementar un algoritmo en un lenguaje de programación, necesitarás entender un algoritmo hasta sus últimos detalles.

Notación asintótica

hemos analizado la búsqueda lineal y la búsqueda binaria al contar el número máximo de intentos que necesitamos hacer. Pero lo que en realidad queremos saber es cuánto tiempo tardan estos algoritmos. Estamos interesados en el tiempo, no solo en los intentos. Los tiempos de ejecución de la búsqueda lineal y la búsqueda binaria incluyen el tiempo necesario para hacer y revisar los intentos, pero hay más acerca de estos algoritmos.

El tiempo de ejecución de un algoritmo depende de cuánto tiempo le tome a una computadora ejecutar las líneas de código del algoritmo, y eso depende de la velocidad de la computadora, el lenguaje de programación y el compilador que traduce el programa del lenguaje de programación al código que se ejecuta directamente en la computadora, entre otros factores.

Pensemos más cuidadosamente acerca del tiempo de ejecución de un algoritmo. Podemos usar una combinación de dos ideas. Primero, necesitamos determinar cuánto tiempo se tarda el algoritmo, en términos del tamaño de su entrada. Esta idea tiene sentido intuitivamente, ¿o no? Ya vimos que el número máximo de intentos en una búsqueda lineal y una búsqueda binaria aumenta a medida que la longitud del arreglo aumenta. O piensa en un GPS. Si solo supiera acerca del sistema carretero del país, y no acerca de cada pequeño camino, sería capaz de encontrar rutas más rápido, ¿cierto? Así que pensamos acerca del tiempo de ejecución del algoritmo como una función del tamaño de su entrada.

La segunda idea es que debemos enfocarnos en qué tan rápido crece una función con el tamaño de la entrada. A esto lo llamamos la tasa de crecimiento del tiempo de ejecución. Para mantener las cosas manejables, tenemos que simplificar la función para extraer la parte más importante y dejar de lado las partes menos importantes. Por ejemplo, supón que un algoritmo, que corre con un entrada de tamaño nn, se tarda 6n^2 + 100n + 3006n2+100n+300 instrucciones de máquina. El término 6n^26n2 se vuelve más grande que el resto de los términos, 100 n + 300100n+300, una vez que nn se hace suficientemente grande, 20 en este caso. Aquí hay una gráfica que muestra los valores de 6n^26n2 y de 100n + 300100n+300 para valores de nn de 0 a 100:

6n^2 vs 100n+300

Diríamos que el tiempo de ejecución de este algoritmo crece como n^2n2, descartando el coeficiente 6 y los términos restantes 100n + 300100n+300. En realidad no importa qué coeficiente usemos; siempre que el tiempo de ejecución sea an^2 + bn + can2+bn+c, para algunos números a > 0a>0, bb, y cc, siempre habrá un valor de nn para el cual an^2an2 sea mayor que bn + cbn+c, y esta diferencia aumenta a medida que nn aumenta. Por ejemplo, aquí hay una gráfica que muestra valores de 0.6n^20.6n2 y de 1000n + 30001000n+3000, de modo que reducimos el coeficiente de n^2n2 por un factor de 10 y aumentamos las otras dos constantes por un factor de 10:

6n^2 vs 100n+300

El valor de nn para el cual 0.6n^20.6n2 se hace más grande que 1000n + 30001000n+3000aumentó, pero siempre habrá tal valor, sin importar las constantes.

Al descartar los términos menos significativos y los coeficientes constantes, podemos enfocarnos en la parte importante del tiempo de ejecución de un algoritmo (su tasa de crecimiento) sin involucrarnos en detalles que complican nuestro entendimiento. Cuando descartamos los coeficientes constantes y los términos menos significativos, usamos notación asintótica. Veremos tres formas: notación \ThetaΘ grande, notación O grande y notación \OmegaΩ grande.

Ordenamiento

Ordenar una lista de elementos en orden ascendente o descendente puede ayudarle a un ser humano o a una computadora a encontrar elementos rápidamente en esa lista, tal vez al usar un algoritmo como una búsqueda binaria. JavaScript tiene un método integrado de ordenamiento. Funciona en arreglos de números, o incluso en arreglos de cadenas

Ordenamiento por inserción

Existen muchas maneras distintas de ordenar. Conforme se ejecuta el ordenamiento por selección, el subarreglo al inicio del arreglo queda ordenado mientras que el subarreglo al final aún no lo está. El ordenamiento por selección busca en el subarreglo no ordenado el siguiente elemento que será incluido en el subarreglo ordenado.

Aquí está otra forma de pensar acerca del ordenamiento. Imagina que estás jugando un juego de cartas. Tienes las cartas en tu mano y las cartas están ordenadas. Tomas exactamente una nueva carta del mazo. La tienes que colocar en el sitio correcto de manera que las cartas en tu mano sigan estando ordenadas. En el ordenamiento por selección, cada elemento que agregas al subarreglo ordenado es mayor o igual que los elementos que ya están en el subarreglo ordenado. Pero en nuestro ejemplo de las cartas, la nueva carta podría ser menor que algunas de las cartas que ya tienes en la mano, así que vas una por una, comparando la nueva carta con cada una de las que ya tienes en la mano, hasta encontrar el lugar donde debe ser colocada. Insertas la nueva carta en el sitio correcto y, una vez más, tienes en la mano cartas completamente ordenadas. Entonces tomas otra carta del mazo y repites el mismo procedimiento. Luego otra carta, y otra, y así sucesivamente, hasta terminar con el mazo.