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.
Su principal objetivo es establecer los límites prácticos de qué es lo que se puede hacer en una computadora y qué no.
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).
Hay algoritmos que sacrifican espacio para reducir el tiempo o viceversa, algunos ejemplos son:
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)
}
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
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
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
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"]
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"]
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
Algoritmos que generalmente tratan de resolver algo por fuerza bruta
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.
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/
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.
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
}
}
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.
Leer sobre cada nuevo avance que se logra en el estándar y la evolución de los motores de JavaScript.