Complejidad

⚡ Tiempo Constante
Veamos nuestro primer ejemplo.
Este método recibe un array de enteros e imprime el primer elemento en consola.
👉 No importa si el array tiene 1 elemento o un millón: siempre se ejecuta una sola operación.
public class Main {
public void log(int[] numbers) {
// O(1)
System.out.println(numbers[0]);
}
}⏱️ Complejidad del algoritmo
Aquí solo realizamos una operación.
Eso significa que este método se ejecuta en tiempo constante, denotado como:
\[ O(1) \]
⚡ Lo importante es que no depende del tamaño de la entrada. Incluso con millones de elementos, siempre accedemos al índice 0.
🔄 ¿Y si duplicamos la línea?
public class Main {
public void log(int[] numbers) {
// O(2)
System.out.println(numbers[0]);
System.out.println(numbers[0]);
}
}Ahora tenemos dos operaciones constantes. Matemáticamente sería:
\[ O(2) \]
Pero en notación Big-O simplificamos siempre a:
\[ O(1) \]
Porque lo que importa no es el número exacto de operaciones, sino cómo crece el tiempo de ejecución a medida que crece la entrada.
Este método siempre se ejecuta en tiempo constante sin importar el tamaño del array.
Esto es lo que llamamos complejidad O(1).
📏 Complejidad Lineal
En este caso tenemos un bucle que recorre todos los elementos de un array e imprime cada uno en consola.
Aquí sí importa el tamaño de la entrada.
public class Main {
public void log(int[] numbers) {
for (int i = 0; i < numbers.length; i++)
System.out.println(numbers[i]);
}
}⏱️ Complejidad
Si el array tiene 1 elemento, hacemos 1 impresión.
Si tiene 1 millón de elementos, hacemos 1 millón de impresiones.
Por lo tanto, el coste del algoritmo crece linealmente con el tamaño de la entrada:
\[ O(n) \]
🔄 Variantes del bucle
Podemos usar:
fortradicional,for-each,while,do-while.
El resultado será el mismo:
\[ O(n) \]
⚡ ¿Y si añadimos operaciones extra?
public class Main {
public void log(int[] numbers) {
System.out.println(); // O(1)
for (int number: numbers) // O(n)
System.out.println(number);
System.out.println(); // O(1)
}
}Este método tiene:
\[ O(1) + O(n) + O(1) = O(n+2) \]
Pero en notación Big-O omitimos las constantes:
\[ O(n+2) ;;\equiv;; O(n) \]
🔁 Dos bucles consecutivos
public class Main {
public void log(int[] numbers) {
for (int number: numbers) // O(n)
System.out.println(number);
for (int number: numbers) // O(n)
System.out.println(number);
}
}Aquí tenemos:
\[ O(n) + O(n) = O(2n) \equiv O(n) \]
El crecimiento sigue siendo lineal.
🔀 Dos entradas diferentes
public class Main {
public void log(int[] numbers, String[] names) {
for (int number: numbers) // O(n)
System.out.println(number);
for (String name: names) // O(m)
System.out.println(name);
}
}En este caso hay dos entradas distintas:
n= tamaño del array de números,m= tamaño del array de nombres.
Por lo tanto:
\[ O(n) + O(m) \]
El tiempo de ejecución crece linealmente con el tamaño de la entrada (o entradas).
Por eso decimos que este algoritmo tiene complejidad \[O(n)\].
🌀 Bucles Anidados
Hasta ahora vimos que un bucle simple tiene complejidad: \[ O(n) \]
Pero, ¿qué pasa si tenemos un bucle dentro de otro?
public class Main {
public void log(int[] numbers) {
for (int first : numbers) // O(n)
for (int second : numbers) // O(n)
System.out.println(first + ", " + second);
}
}⏱️ Complejidad
- El primer bucle recorre los \(n\) elementos → \(O(n)\).
- En cada iteración, el segundo bucle también recorre los \(n\) elementos → \(O(n)\).
Por lo tanto:
\[ O(n) \times O(n) = O(n^2) \]
Esto significa que el tiempo de ejecución crece cuadráticamente con el tamaño de la entrada.
🔄 Añadiendo un bucle externo
public class Main {
public void log(int[] numbers) {
for (int number : numbers) // O(n)
System.out.println(number);
for (int first : numbers) // O(n)
for (int second : numbers) // O(n)
System.out.println(first + ", " + second);
}
}Aquí tenemos:
\[ O(n) + O(n^2) \]
Pero como \(n^2\) crece más rápido que \(n\), simplificamos:
\[ O(n + n^2) \equiv O(n^2) \]
🔼 Añadiendo aún más bucles
Si anidamos tres bucles:
public class Main {
public void log(int[] numbers) {
for (int first : numbers)
for (int second : numbers)
for (int third : numbers)
System.out.println(first + ", " + second + ", " + third);
}
}La complejidad ahora es:
\[ O(n) \times O(n) \times O(n) = O(n^3) \]
📈 Crecimiento
\(O(n)\) ➲ crece de forma lineal.
\(O(n^2)\) ➲ crece de forma cuadrática, mucho más rápido.
\(O(n^3)\) ➲ crece cúbicamente, aún más costoso.

Por ejemplo:
| Tamaño de entrada (n) | Operaciones O(n) | Operaciones O(n^2) | Operaciones O(n^3) |
|---|---|---|---|
| 10 | 10 | 100 | 1,000 |
| 100 | 100 | 10,000 | 1,000,000 |
| 1000 | 1000 | 1,000,000 | 1,000,000,000 |
Cada bucle anidado multiplica el coste del algoritmo. Por eso:
- Un bucle → \(O(n)\)
- Dos bucles anidados → \(O(n^2)\)
- Tres bucles anidados → \(O(n^3)\)
Y así sucesivamente 🚀
📉 Complejidad Logarítmica
Otra velocidad de crecimiento que debemos conocer es la logarítmica, representada como:
\[ O(\log n) \]
Mientras que la complejidad lineal (\(O(n)\)) crece en proporción directa al tamaño de la entrada,
la complejidad logarítmica crece rápidamente al inicio pero después se aplana, volviéndose mucho más lenta en su crecimiento.

🚀 Escalabilidad
Con búsqueda lineal (\(O(n)\)), si tenemos 1 millón de elementos, en el peor caso inspeccionamos los 1 millón.
Con búsqueda binaria (\(O(\log n)\)), en 1 millón de elementos necesitamos como máximo:
\[ \log_2(1{,}000{,}000) \approx 19 \]
¡Solo 19 pasos para encontrar la respuesta!
Los algoritmos logarítmicos son mucho más eficientes que los lineales y cuadráticos.
Cada paso reduce el problema a la mitad, haciendo que el crecimiento sea extremadamente lento y escalable.
💾 Complejidad Espacial
Ejemplo 1: espacio constante \(O(1)\)
En este método greet, solo declaramos una variable de bucle independiente del tamaño de la entrada.
Así que, aunque tengamos 10 o un millón de elementos, el espacio adicional será siempre el mismo:
public class Main {
public void greet(String[] names) {
// O(1) space
for (int i = 0; i < names.length; i++) {
System.out.println("Hi " + names[i]);
}
}
}Idea clave: El espacio requerido no crece con el tamaño de la entrada, es constante.
Ejemplo 2: espacio lineal \(O(n)\)
Ahora supongamos que declaramos un nuevo array llamado copy, de la misma longitud que names.
public class Main {
public void greet(String[] names) {
// O(n) space
String[] copy = new String[names.length];
for (int i = 0; i < names.length; i++) {
System.out.println("Hi " + names[i]);
}
}
}Aquí la memoria adicional sí depende del tamaño de la entrada: si names tiene 1.000 elementos, copy también ocupará espacio para 1.000 elementos.
Por eso decimos que la complejidad espacial es \(O(n)\).
Nota importante
Cuando hablamos de complejidad espacial, no contamos el tamaño de la entrada (pues ya existe). Solo medimos el espacio extra que nuestro algoritmo necesita para ejecutarse.
⚡ En este curso nos enfocaremos en la complejidad temporal, ya que suele ser más crítica. Pero recuerda siempre reflexionar sobre la memoria, sobre todo cuando trabajes en dispositivos con recursos limitados.