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