TechLead
Principiante
15 min
Guía completa

Notación Big O

Comprende el análisis de complejidad de tiempo y espacio para algoritmos

¿Qué es la notación Big O?

La notación Big O es una notación matemática usada para describir el rendimiento o la complejidad de un algoritmo. En particular, describe el peor caso y nos ayuda a entender cómo crecen el tiempo de ejecución o los requisitos de memoria a medida que aumenta el tamaño de la entrada.

Big O nos da una comprensión de alto nivel de la complejidad temporal o espacial de un algoritmo, permitiéndonos comparar enfoques y elegir el más eficiente.

¿Por qué es importante Big O?

  • 📊 Predicción de rendimiento: Entiende cómo escala tu algoritmo con entradas más grandes
  • ⚖️ Comparación de algoritmos: Compara soluciones de forma objetiva
  • 🎯 Esencial en entrevistas: Clave en entrevistas técnicas de empresas top
  • 💡 Decisiones de diseño: Elige estructuras de datos y algoritmos con criterio

Complejidades temporales comunes

Estas son las complejidades Big O más comunes, de mejor a peor:

O(1) - Tiempo constante

El algoritmo tarda lo mismo sin importar el tamaño de la entrada.

// Example: Array access by index
function getFirstElement(arr) {
  return arr[0]; // Always takes the same time
}

// Example: Hash table lookup
const map = new Map([['key', 'value']]);
map.get('key'); // O(1) lookup

O(log n) - Tiempo logarítmico

El tiempo crece de forma logarítmica. ¡Muy eficiente!

// Example: Binary search
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  
  return -1;
}

// With each iteration, we cut the search space in half
// For 1000 items, only ~10 comparisons needed!

O(n) - Tiempo lineal

El tiempo crece linealmente con el tamaño de la entrada.

// Example: Finding max in unsorted array
function findMax(arr) {
  let max = arr[0];
  
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) {
      max = arr[i];
    }
  }
  
  return max;
}

// Must check every element once

O(n log n) - Tiempo linealítmico

Común en algoritmos de ordenamiento eficientes como merge sort y quicksort.

// Example: Merge Sort
function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }
  
  return result.concat(left.slice(i)).concat(right.slice(j));
}

O(n²) - Tiempo cuadrático

Común con bucles anidados. Se vuelve lento con entradas grandes.

// Example: Bubble Sort
function bubbleSort(arr) {
  const n = arr.length;
  
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  
  return arr;
}

// For 1000 items, this makes ~1,000,000 comparisons!

O(2ⁿ) - Tiempo exponencial

Muy lento. Se duplica con cada entrada adicional. ¡Evitar si es posible!

// Example: Naive Fibonacci (inefficient)
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Exponential growth - fibonacci(50) would take years!
// This is why we need dynamic programming

Tabla de complejidad Big O

Complejidad Nombre n=10 n=100 n=1000
O(1) Constante 1 1 1
O(log n) Logarítmica 3 7 10
O(n) Lineal 10 100 1,000
O(n log n) Linealítmica 30 700 10,000
O(n²) Cuadrática 100 10,000 1,000,000
O(2ⁿ) Exponencial 1,024 1.27e+30 💀 Imposible

Complejidad espacial

Big O también describe la complejidad de espacio (memoria). Mide cuánta memoria adicional necesita un algoritmo.

Espacio O(1) - Constante

// Uses fixed amount of space
function sum(a, b) {
  return a + b; // No extra space
}

Espacio O(n) - Lineal

// Creates new array
function double(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(arr[i] * 2);
  }
  return result;
}

Big O, Big Omega y Big Theta

Big O (O)

Cota superior - Peor caso. La más usada.

"Este algoritmo nunca será más lento que..."

Big Omega (Ω)

Cota inferior - Mejor caso.

"Este algoritmo será al menos así de rápido..."

Big Theta (Θ)

Cota ajustada - Caso promedio, tanto superior como inferior.

"Este algoritmo siempre estará alrededor de..."

Reglas para calcular Big O

1. Eliminar constantes

O(2n) → O(n), O(500) → O(1)

// This is O(n), not O(2n)
function example(arr) {
  for (let i = 0; i < arr.length; i++) { } // O(n)
  for (let i = 0; i < arr.length; i++) { } // O(n)
  // Total: O(n + n) = O(2n) = O(n)
}

2. Eliminar términos no dominantes

O(n² + n) → O(n²), O(n + log n) → O(n)

// This is O(n²), not O(n² + n)
function example(arr) {
  for (let i = 0; i < arr.length; i++) { } // O(n)
  
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length; j++) { } // O(n²)
  }
  // Total: O(n + n²) = O(n²)
}

3. Entradas diferentes → variables diferentes

// This is O(a + b), not O(n)
function example(arr1, arr2) {
  for (let i = 0; i < arr1.length; i++) { } // O(a)
  for (let i = 0; i < arr2.length; i++) { } // O(b)
  // Total: O(a + b)
}

💡 Puntos clave

  • ✓ Big O describe el peor caso a medida que crece la entrada
  • ✓ Enfócate en el término dominante y elimina constantes
  • ✓ O(1) es mejor, O(2ⁿ) es peor; busca O(log n) u O(n)
  • ✓ Bucles anidados suelen significar O(n²) o peor
  • ✓ Divide y vencerás suele lograr O(log n) u O(n log n)
  • ✓ Considera complejidad temporal y espacial en tus soluciones