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,031