Complejidad Computacional y JavaScript
Qué es Complejidad computacional?
Parte de la teoría de la computación que se centra en la clasificación de los problemas computacionales de acuerdo
con su dificultad y la relación entre diversas clases de complejidad.
Cuales son sus objetivos?
Su principal objetivo es establecer los límites prácticos de qué es lo que se puede hacer en una computadora y qué no.
Campos relacionados aplicables
Análisis de algoritmos
Determina la cantidad de recursos requeridos por un algoritmo en particular para resolver un problema, los recursos pueden ser analizados como tiempo (Uso de la CPU, uso de la red) y espacio (memoria o almacenamiento).
Compensación entre tiempo y espacio
Hay algoritmos que sacrifican espacio para reducir el tiempo o viceversa, algunos ejemplos son:
- Almacenamiento en tablas indexadas vs recalcular sobre la información original
- Compresión de datos vs datos planos
- Transmisión completa de datos vs caché
Teoría de la computabilidad
Analiza todos los posibles algoritmos que pudieran ser usados para resolver el mismo problema.
function fibonacciSimple (num) => {
let a = 1, b = 0, temp
while (num >= 0){
temp = a
a = a + b
b = temp
num--
}
return b
}
function fibonacciRecursive (num) {
if (num <= 1) return 1
return fibonacci(num - 1) + fibonacci(num - 2)
}
Clasificación de algoritmos
Big O notation
O(1) - Tiempo constante
La operación no depende del tamaño de los datos. Es orden perfecto, pero a la vez el menos frecuente.
const getLast = items => items[items.length-1]
const data = [1, 2, 3, 4, 5]
getLast(data) // returns 5
O(N) - Lineal
El tiempo de ejecución es directamente proporcional al tamaño de los datos. Crece en una línea recta.
const findItem = (items, match) => {
for (let i = 0, total = items.length; i < total; i++) {
if (items[i] === match) return i
}
return -1;
}
const data = ['a', 'b', 'c', 'd', 'e']
findItem(data, 'e') // returns 4
O(log n) - Logarítmica
Son algoritmos que dividen la información para realizar la operación deseada.
const binarySearch = (array, number) => {
let start = 0
let end = array.length - 1
while (start <= end) {
let mid = Math.floor((start + end) / 2)
if (array[mid] === number) return true
if (array[mid] < number) {
start = mid + 1
} else {
end = mid - 1
}
}
return false
}
function findNumber(array, number) {
if (binarySearch(array, number, 0, array.length-1)) {
return(`Number ${number} exist in the array`)
}
return(`Number ${number} not found!`)
}
const numberArray = [1, 3, 4, 2, 5, 7, 6, 8, 9]
findNumber(numberArray, 5) // Number 5 exist in the array
O(nlog n) - Logarítmica
Se trata de funciones similares a las anteriores, pero que dividen la ejecución en varios segmentos por cada elemento,volviendo a retornar la información tras la ejecución de cada segmento.
const quickSort = items => {
if (items.length < 2) {
return items
}
let pivot = items[0]
let left = []
let right = []
for (let i = 1, total = items.length; i < total; i++) {
if (items[i] < pivot) {
left.push(items[i])
} else {
right.push(items[i])
}
}
return [
...quickSort(left),
pivot,
...quickSort(right)
]
}
quickSort( ['q','a','z','w','s','x','e','d','c','r']) // ["a", "c", "d", "e", "q", "r", "s", "w", "x", "z"]
O(n2) - Cuadrática
Algoritmos que necesitan realizar una iteración por todos los elementos en cada uno de los elementos a procesar.
const bubbleSort = (inputArr) => {
const len = inputArr.length
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
if (inputArr[j] > inputArr[j + 1]) {
let tmp = inputArr[j]
inputArr[j] = inputArr[j + 1]
inputArr[j + 1] = tmp
}
}
}
return inputArr
}
bubbleSort( ['q','a','z','w','s','x','e','d','c','r'])
// ["a", "c", "d", "e", "q", "r", "s", "w", "x", "z"]
O(2^n): Exponencial
Algoritmos que duplican su complejidad con cada elemento añadido al procesamiento. Son algoritmos muy raros pues en condiciones normales no debería ser necesario hacer algo así.
function fibonacci (num) {
if (num <= 1) return 1
return fibonacci(num - 1) + fibonacci(num - 2)
}
fibonacci(10) // 89
O(n!) - Factorial
Algoritmos que generalmente tratan de resolver algo por fuerza bruta
Big O de forma gráfica
Analizando la complejidad computacional en JavaScript
Cada lenguaje de programación trata de resolver problemas computacionales de diferentes formas implementando diferentes paradigmas, la forma de JavaScript es particular.
Al ser un lenguaje scripting, de asignación dinámica de memoria y que usa el paradigma asíncrono (que en si mismo, es una evolución frente al modelo tradicional), genera nuevos retos en complejidad diferentes a los demás lenguajes y tiene así mismo fortalezas y debilidades debido a sus peculiaridades.
Métodos nativos del estándar de ECMAScript
La especificación de ECMAScript no especifica ni define la complejidad de los métodos a implementar, cada motor de JavaScript es libre de implementar sus propias funcionalidades mientras cumpla con el estándar, por lo tanto a la hora de analizar la complejidad de un método en específico habrá que remitirse al código del motor donde se desee analizar.
En el caso de V8, https://chromium.googlesource.com/v8/v8/
Innovaciones de V8 para reducir la complejidad de tiempo y espacio en JavaScript
- Irregexp en 2009
- Crankshaft (Compilador JIT) en 2010
- Incremental garbage collector en 2011
- Concurrent compilation en 2014
- Code caching and script streaming en 2015
- Orinoco (nuevo garbage collector) en 2016
- Ignition y Turbofan (nuevo pipeline de compilación y ejecución)
Limitaciones de tiempo en JavaScript
JavaScript es ejecutado en un solo hilo principal, deriva de forma natural a procedimientos asíncronos tareas que puedan llevar mucho tiempo y así soportar una concurrencia importante.
En la mayoría de los casos esto será suficiente, pero hay ciertas operaciones como el manejo de strings y operaciones matemáticas que no son procedimientos asíncronos, para esto hay herramientas para hacer multi-threading como un complemento.
Teoría de la computabilidad en JavaScript
const verifyUser = (username, password, callback) => {
dataBase.verifyUser(username, password, (error, userInfo) => {
if (error) return callback(error)
dataBase.getRoles(username, (error, roles) => {
if (error) return callback(error)
dataBase.logAccess(username, (error) => {
if (error) return callback(error)
callback(null, userInfo, roles)
})
})
})
}
const verifyUserSimple = async (username, password) => {
try {
const userInfo = await dataBase.verifyUser(username, password);
const rolesInfo = await dataBase.getRoles(userInfo);
const logStatus = await dataBase.logAccess(userInfo);
return userInfo;
} catch (e) {
//handle errors as needed
}
}
Cómo aplicarlo a mi código diario
No obsesionarse
La optimización temprana es la raíz de todos los males, no todo debe ser optimizado, sobre código inteligente siempre debe primar el código claro a no ser que sea una parte crítica del aplicativo que debe mandatoriamente ser optimizada.
Mantenerse actualizado
Leer sobre cada nuevo avance que se logra en el estándar y la evolución de los motores de JavaScript.
Usar ayudas para analizar el código
Utilizar recursos especializados y herramientas de medición
Recursos recomendados
Mil Gracias!!
Complejidad computacional y JavaScript
By Adrián Estrada
Complejidad computacional y JavaScript
Complejidad computacional y JavaScript, un análisis sobre el impacto de la teoría de la computación sobre el lenguaje de la web - Charla para Boyaconf 2019
- 1,114