TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Problema estilo CTCI 8.7 para principiantes: lista cada permutación de un string con caracteres todos distintos. Backtracking con un array used, Java claro y un recorrido corto para abc.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
Tienes una palabra corta. Cada letra es distinta. ¿De cuántas formas puedes reordenar esas letras, y cómo listar cada arreglo sin repetir trabajo? Eso es Permutations without Dups: generar todos los órdenes de un string con caracteres distintos.
Este post es enseñanza original para principiantes en Java. Misma familia de "genera todas las permutaciones" en entrevista, no una copia de libro. Parte de la serie CTCI en Java. Recursión y programación dinámica, problema 8.7.
1. Analogía cotidiana
Imagina tres etiquetas distintas sobre la mesa: A, B, C. Quieres cada posible fila de personas con esas etiquetas.
- En el primer asiento puedes elegir cualquiera de las tres.
- En el segundo eliges cualquier etiqueta que siga en la mesa.
- El último asiento se queda con lo que queda.
Si lo dibujas como un árbol, el primer nivel tiene tres ramas, cada segundo nivel tiene dos, y las hojas son filas completas: ABC, ACB, BAC, BCA, CAB, CBA. Seis hojas, que es 3! = 6.
El truco de código es el mismo recorrido del árbol: elegir, recursar, deshacer la elección para que la siguiente rama vea la mesa limpia. Ese deshacer es backtracking.
2. Problema en palabras claras
Entrada: un string s cuyos caracteres son todos distintos (sin letras repetidas).
Salida: una lista con cada permutación distinta de s. El orden de la lista no importa salvo que el entrevistador pida salida ordenada.
Ejemplos:
| Entrada | Salida (cualquier orden) |
|---|---|
"abc" |
"abc", "acb", "bac", "bca", "cab", "cba" |
"ab" |
"ab", "ba" |
"a" |
"a" |
"" |
un string vacío (o una lista vacía; elige uno y cúmplelo) |
Aclara antes de codificar:
- ¿Caracteres únicos? (Sí para 8.7. El 8.8 cubre duplicados.)
- ¿Sensible a mayúsculas? (
'A'y'a'son distintos si ambos aparecen.) - ¿Devolver
List<String>o imprimir? (Devolver lista es más fácil de probar.) - ¿String vacío? (Una permutación vacía es un base case limpio.)
- ¿Mutar la entrada? (Mejor un char array o un builder para no tocar el string del llamador.)
3. Piensa primero
Cuenta primero
Con n caracteres únicos hay n! permutaciones. Con n = 10 ya pasas de tres millones. La entrevista quiere el generador, no materializar n enorme gratis.
Idea bruta (nómbrala y no la codifiques)
Generar cada orden de índices con bucles anidados o con Collections.shuffle hasta "tener suficientes." No escala, y shuffle no prueba completitud. Sáltalo cuando ya hayas nombrado el crecimiento factorial.
Idea recursiva limpia
Construye una respuesta parcial prefix. En cada paso:
- Si la longitud de
prefixesn, guarda una copia deprefixy vuelve. - Por cada carácter aún no usado, añádelo, recursa, luego quítalo (backtrack).
Necesitas saber qué caracteres están libres:
- Un
boolean[] usedde longitudn(índice en el string original), o - Un conjunto de caracteres restantes, o
- Swaps in-place en un char array (pon el elegido en el índice actual, recursa en el sufijo, swap de vuelta).
Los tres valen. El array used se explica bien en voz alta. Los swaps usan menos estructura extra. Abajo usamos used por claridad y luego una variante corta con swap.
Por qué importa "without dups"
Si el string tuviera dos letras iguales, el mismo árbol produciría strings duplicados. El problema 8.8 lo arregla saltando un carácter cuando coincide con un hermano anterior no usado. Aquí cada carácter es único, así que cada hoja es un string distinto. Sin lógica extra de skip.
Segunda forma clásica (opcional)
Otra vista de libro: toma las permutaciones del string sin el primer carácter e inserta ese carácter en cada índice de cada sub-permutación. Misma cuenta, otra recursión. Backtracking con un prefijo que crece suele ser más rápido de escribir bajo presión.
4. Solución en Java
Backtracking con array used
import java.util.ArrayList;
import java.util.List;
public class PermutationsWithoutDups {
public List<String> permutations(String s) {
List<String> result = new ArrayList<>();
if (s == null) {
return result;
}
boolean[] used = new boolean[s.length()];
backtrack(s, new StringBuilder(), used, result);
return result;
}
private void backtrack(String s, StringBuilder path,
boolean[] used, List<String> result) {
if (path.length() == s.length()) {
result.add(path.toString());
return;
}
for (int i = 0; i < s.length(); i++) {
if (used[i]) {
continue;
}
used[i] = true;
path.append(s.charAt(i));
backtrack(s, path, used, result);
path.deleteCharAt(path.length() - 1); // undo
used[i] = false; // undo
}
}
}
Recorrido para "abc":
- Path vacío. Prueba índice 0 (
a): path"a". - Desde
"a", pruebab→"ab", solo quedac→"abc"(guarda). Undoc, undob. - Desde
"a", pruebac→"ac", luegob→"acb"(guarda). Undo hasta vaciara. - Igual empezando por
b, luego porc. Seis strings guardados.
Uso mínimo:
List<String> perms = new PermutationsWithoutDups().permutations("abc");
// size 6; contains "abc", "acb", "bac", "bca", "cab", "cba"
Variante con swaps (misma idea)
public List<String> permutationsSwap(String s) {
List<String> result = new ArrayList<>();
if (s == null) {
return result;
}
char[] chars = s.toCharArray();
swapBacktrack(chars, 0, result);
return result;
}
private void swapBacktrack(char[] chars, int index, List<String> result) {
if (index == chars.length) {
result.add(new String(chars));
return;
}
for (int i = index; i < chars.length; i++) {
swap(chars, index, i);
swapBacktrack(chars, index + 1, result);
swap(chars, index, i); // restore
}
}
private void swap(char[] chars, int i, int j) {
char tmp = chars[i];
chars[i] = chars[j];
chars[j] = tmp;
}
En la profundidad index, el prefijo chars[0..index) está fijo. Pruebas cada carácter restante del sufijo intercambiándolo a index, recursas y vuelves a intercambiar. El mismo árbol factorial, sin boolean[].
Cualquiera de las dos formas vale en entrevista. Elige una, termínala y menciona la otra si sobra tiempo.
5. Tabla de complejidad
| Pieza | Notas de coste |
|---|---|
| Número de hojas | n! para entrada única de longitud n |
| Trabajo por hoja | O(n) para copiar el string terminado al resultado |
| Tiempo total | O(n · n!) para construir cada permutación |
| Profundidad de recursión | O(n) |
| Espacio extra (sin salida) | O(n) para path + flags used (o O(1) además del char array con swaps) |
| Espacio de salida | O(n · n!) para guardar cada string |
El tiempo es sensible a la salida. Tocás cada permutación que devolvés. No digas O(n) para la lista completa. Con n enorme, pueden pedir un iterador en streaming o "solo el conteo," otro producto.
6. Casos borde y errores comunes
Los entrevistadores tocan esto:
- Entrada null → lista vacía (o excepción; dilo).
- String vacío → un string vacío en la lista es un base case natural.
- Un solo carácter → lista de tamaño 1.
- Dos caracteres → dos strings; buen chequeo manual.
- Longitud 0 vs null → no los trates igual sin decirlo.
Errores comunes:
- Olvidar el undo. Si dejas
used[i] = trueo un char en el builder, las ramas siguientes pierden caracteres o crecen sin fin. - Mutar un
StringBuildercompartido al guardar. Siemprepath.toString()(unStringnuevo) antes deresult.add. - Asumir entrada o salida ordenada. Ninguna se exige salvo que lo pidan.
- Usar este código con letras duplicadas. Emitirás permutaciones repetidas. Eso es trabajo del 8.8.
- Bucles anidados fijos para un n concreto. Se rompe cuando cambian la longitud.
- Contar
n!en la cabeza y luego decir O(n²). Cuenta las hojas primero, luego el coste por hoja.
Autochequeo rápido: para "ab", espera exactamente ["ab", "ba"] (orden libre). Para "abc", tamaño 6 y sin strings repetidos.
7. Recap para contárselo a un amigo
Permutations without dups, versión entrevista:
- Los caracteres son únicos, así que cada camino completo del árbol es un string distinto.
- Hay
n!de ellos. - Construye un path. En cada paso elige un carácter no usado, recursa, luego deshaz.
- Cuando la longitud del path llega a
n, guarda una copia. used[]+StringBuilder, o swaps in-place en un char array: el mismo árbol.- Tiempo O(n · n!), espacio dominado por la lista de salida.
Si puedes dibujar las seis hojas de "abc", escribir el bucle elegir-recursar-deshacer sin olvidar el undo, y nombrar el tamaño factorial, dominas el problema 8.7.
Serie
- Guía: Guía de la serie CTCI
- Anterior: Towers of Hanoi
- Siguiente: Permutations with Dups
