TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Problema estilo CTCI 5.4 para principiantes: dado un int positivo, encuentra el siguiente valor mayor y el menor que conserven el mismo número de bits 1. Cuenta ceros y unos finales, voltea un bit y reordena el resto.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
Tienes una bolsa de dígitos binarios con un número fijo de 1s. Puedes reordenarlos, pero no inventas unos extra ni tiras ninguno. Entre todos los números que puedes formar así, ¿cuál queda justo por encima del valor actual y cuál justo por debajo? Eso es Next Number: mismo popcount, vecinos más cercanos en la recta de enteros.
Este post es enseñanza original para principiantes en Java. Misma familia de preguntas de manipulación de bits en entrevista, no una copia de libro. Parte de la serie CTCI en Java. Capítulo 5, manipulación de bits.
1. Analogía cotidiana
Piensa en una fila de interruptores. Algunos están ON (1), otros OFF (0). La regla de este puzzle: cada patrón legal debe conservar exactamente el mismo conteo de interruptores ON.
- El patrón siguiente mayor es el entero más pequeño mayor que el actual que aún tenga el mismo número de ONs.
- El patrón siguiente menor es el entero más grande menor que el actual con el mismo conteo de ONs.
La fuerza bruta probaría n+1, n+2, ... y contaría bits cada vez. Sirve para demos chicas. En entrevista quieren una construcción directa con bits: encontrar el sitio correcto para voltear un bit y empaquetar el resto de unos según la dirección.
2. Problema en palabras simples
Entrada: un int n positivo (en entrevistas, trátarlo como patrón de 32 bits en complemento a dos; enfócate en no negativos salvo que digan lo contrario).
Salida:
getNext(n): el número más pequeño mayor quencon el mismo número de bits1, o un centinela (por ejemplo-1) si no existe en el ancho de palabra.getPrev(n): el número más grande menor quencon el mismo número de bits1, o un centinela si no existe.
Mismo número de bits 1 significa mismo popcount: Integer.bitCount(result) == Integer.bitCount(n).
Ejemplos:
| n (binario) | Unos | Siguiente mayor | Siguiente menor |
|---|---|---|---|
11011001111100 (13948) |
9 | 11011010001111 (13967) |
(existe; ver idea del recorrido abajo) |
10110 (22) |
3 | 11001 (25) |
10101 (21) |
10011100 (156) |
4 | 10100011 (163) |
10011010 (154) |
1 |
1 | 10 (2) |
ninguno (devolver -1) |
| solo unos en los k bits bajos, nada libre más alto | k | puede no existir si ningún cero puede subir | a menudo aún existe si hay ceros arriba |
Aclara antes de codear:
- ¿Solo positivos, o 32 bits completos con bit de signo? (Empieza con positivos; menciona 31 como bit alto práctico para
intpositivo.) - ¿Si no hay next/prev? (
-1o excepción; elige uno y manténlo.) - ¿Se permite
n == 0? (Cero unos: solo cero tiene cero unos. Ni next ni prev.) - ¿Ambas respuestas en un método, o dos helpers?
3. Piensa primero
Bruta (bien como calentamiento)
next = n + 1
while bitCount(next) != bitCount(n): next++
Misma idea hacia abajo para prev. Correcto para n pequeños. En el peor caso puedes caminar lejos, y en una palabra fija hay que parar al desbordar. Suelen querer trabajo de bits O(1) o O(tamaño de palabra).
Idea para el siguiente mayor
Quieres el menor aumento que preserve el conteo de 1s.
Eso significa:
- Encuentra el cero no final más a la derecha: el
0más bajo que tiene al menos un1a su derecha. Llamapa su índice. - Voltea ese
0a1. El número crece y temporalmente tienes un1de más. - Limpia todos los bits por debajo de
p. - Devuelve los unos que "debes" en las posiciones más a la derecha bajo
p, pero soloc1 - 1(ya gastaste un flip en el 1 extra enp). Eso minimiza el valor bajop.
Cómo hallar p sin escanear al azar:
c0= conteo de0s finales (desde el bit 0 hacia arriba).c1= conteo de1s después de esos ceros (una racha de unos).- Entonces
p = c0 + c1. El bitpes el cero justo a la izquierda de esa racha de unos.
Idea para el siguiente menor
Espejo:
- Cuenta los
1s finales (c1), luego los ceros encima (c0). - La posición
p = c0 + c1es el uno no final más a la derecha. - Baja ese
1a0(achica el número) y limpia los bits de abajo. - Coloca
c1 + 1unos con el empaquetado estándar: bloque de(c1 + 1)unos desplazado por(c0 - 1).
Si no hay cero por encima de los unos bajos (patrón tipo 000...00111), no puedes bajar con el mismo conteo.
Atajos aritméticos (mismos conteos)
Cuando tienes c0 y c1:
- Siguiente mayor:
n + (1 << c0) + (1 << (c1 - 1)) - 1 - Siguiente menor:
n - (1 << c1) - (1 << (c0 - 1)) + 1
Mismos resultados que voltear y reempacar. Buena segunda implementación después de explicar el dibujo de bits.
4. Solución en Java
getNext: siguiente mayor con el mismo conteo de bits
/**
* Smallest number greater than n with the same number of 1 bits.
* Returns -1 if none exists within a 32-bit positive pattern.
*/
int getNext(int n) {
if (n <= 0) {
return -1;
}
int c = n;
int c0 = 0; // trailing zeros
int c1 = 0; // ones right after those zeros
// count trailing zeros
while ((c & 1) == 0 && c != 0) {
c0++;
c >>>= 1;
}
// count ones after that
while ((c & 1) == 1) {
c1++;
c >>>= 1;
}
// no larger number with same 1-count in 32-bit space
// (e.g. 111...11000...0 with no non-trailing zero to flip)
if (c0 + c1 == 31 || c0 + c1 == 0) {
return -1;
}
int p = c0 + c1; // position of rightmost non-trailing zero
// Flip the zero at p to one.
n |= (1 << p);
// Clear all bits to the right of p.
n &= ~((1 << p) - 1);
// Insert (c1 - 1) ones on the right.
n |= (1 << (c1 - 1)) - 1;
return n;
}
Gemelo aritmético:
int getNextArithmetic(int n) {
if (n <= 0) {
return -1;
}
int c = n;
int c0 = 0;
int c1 = 0;
while ((c & 1) == 0 && c != 0) {
c0++;
c >>>= 1;
}
while ((c & 1) == 1) {
c1++;
c >>>= 1;
}
if (c0 + c1 == 31 || c0 + c1 == 0 || c1 == 0) {
return -1;
}
return n + (1 << c0) + (1 << (c1 - 1)) - 1;
}
getPrev: siguiente menor con el mismo conteo de bits
/**
* Largest number less than n with the same number of 1 bits.
* Returns -1 if none exists.
*/
int getPrev(int n) {
if (n <= 0) {
return -1;
}
int c = n;
int c0 = 0; // zeros after the trailing ones
int c1 = 0; // trailing ones
// count trailing ones
while ((c & 1) == 1) {
c1++;
c >>>= 1;
}
if (c == 0) {
// pattern like 00...00111: no smaller with same ones
return -1;
}
// count zeros after those ones
while ((c & 1) == 0 && c != 0) {
c0++;
c >>>= 1;
}
int p = c0 + c1; // rightmost non-trailing one
// Clear bits from p down through 0.
n &= (-1 << (p + 1)); // same as ~0 << (p + 1)
// Sequence of (c1 + 1) ones.
int mask = (1 << (c1 + 1)) - 1;
// Place that block as far right as allowed: leave (c0 - 1) zeros at the bottom.
n |= mask << (c0 - 1);
return n;
}
Gemelo aritmético:
int getPrevArithmetic(int n) {
if (n <= 0) {
return -1;
}
int c = n;
int c0 = 0;
int c1 = 0;
while ((c & 1) == 1) {
c1++;
c >>>= 1;
}
if (c == 0) {
return -1;
}
while ((c & 1) == 0 && c != 0) {
c0++;
c >>>= 1;
}
if (c0 == 0) {
return -1;
}
return n - (1 << c1) - (1 << (c0 - 1)) + 1;
}
Usa >>> (desplazamiento sin signo) al recorrer c para que un 1 alto (bit de signo) no deje el bucle colgado con >> aritmético. Con entradas positivas de entrevista, cualquiera vale; >>> es mejor hábito.
5. Recorridos
Siguiente mayor: 13948
n = 11011001111100
trailing zeros: 00 → c0 = 2
then ones: 11111 → c1 = 5
p = 7 (0-based from the right)
Flip bit 7: 11011011111100
Clear below 7: 11011010000000
Add c1-1 = 4 ones on the right:
11011010001111 = 13967
Comprueba: ambos tienen nueve 1s, y nada entre 13948 y 13967 tiene nueve 1s.
Siguiente menor: 156 (10011100)
n = 10011100
trailing ones: none → c1 = 0
then zeros: 00 → c0 = 2
next bit is 1, so p = 2
Clear from bit 2 down: 10011000
mask = (c1 + 1) ones = 1
shift by (c0 - 1) = 1: 10011010 = 154
Cuatro unos cada uno. 155 tiene cinco, así que 154 es el vecino.
Caso chico: 22 (10110)
| Dirección | Conteos | Resultado binario | Decimal |
|---|---|---|---|
| next | c0=1, c1=2, p=3 | 11001 |
25 |
| prev | c1=0, c0=1, p=1 | 10101 |
21 |
6. Complejidad, bordes, tips de entrevista
| Tema | Respuesta |
|---|---|
| Tiempo | O(b) para contar rachas, b = tamaño de palabra (32). Flip y máscaras son O(1). |
| Espacio extra | O(1) |
| Alternativa bruta | O(gap) incrementos; el gap puede ser grande |
| Sin next | Patrones sin cero no final que voltear (guarda con c0 + c1) |
| Sin prev | Todos los unos solo abajo (c == 0 tras contar unos finales) |
n = 0 |
Solo cero tiene cero unos; -1 en ambos |
| Bit de signo | Prefiere >>> al escanear; quédate en rango positivo en entrevistas |
Bugs comunes:
- Usar
>>aritmético sobre un intermedio negativo al practicar casos más anchos. - Off-by-one: insertar
c1unos en vez dec1 - 1tras voltear hacia arriba. - Olvidar limpiar debajo de
pantes de insertar unos (bits viejos corrompen el conteo). - Decir que no hay solución sin mirar la estructura de rachas finales.
- Confundir "siguiente mayor por valor" con "siguiente al rotar bits". Este problema es orden de enteros, no rotación.
Cómo contarlo:
- Reformula: mismo popcount, mayor más cercano y menor más cercano.
- Dibuja una cadena de bits. Marca ceros finales, luego unos, luego la posición del flip.
- Flip, limpia a la derecha, reempaca unos.
- Espejo para prev.
- Opcional: muestra que la forma aritmética coincide en tu ejemplo.
7. Explícaselo a un amigo
Next Number (problema 5.4) pide: desde un int positivo, el siguiente mayor y el siguiente menor con el mismo número de bits 1.
- Siguiente mayor: cuenta ceros finales (
c0) y luego unos (c1). Voltea el cero enp = c0 + c1. Limpia bajop. Ponc1 - 1unos a la derecha. - Siguiente menor: cuenta unos finales (
c1) y luego ceros (c0). Baja el uno enp = c0 + c1limpiando depa 0. Colocac1 + 1unos desplazados porc0 - 1. - Aritmética:
n + (1<<c0) + (1<<(c1-1)) - 1yn - (1<<c1) - (1<<(c0-1)) + 1cuando ya tienes los conteos. - Centinela cuando el patrón no tiene hueco (sin cero no final para next, sin uno no final para prev).
- Prefiere desplazamientos sin signo al recorrer bits.
Si puedes pasar 13948 a 13967 a mano y explicar por qué los unos se asientan a la derecha tras el flip, dominas el 5.4.
Serie
- Guía: Guía de la serie CTCI
- Anterior: Flip Bit to Win
- Siguiente: Debugger
