Contar bits, número ausente e invertir bits
Calcule el número de bits para 0..n mediante PD y el truco del bit menos significativo establecido, encuentre un número ausente con XOR e invierta los bits de un entero de 32 bits.
Contar bits, número ausente e invertir bits es una lección gratuita de DSA Interview Prep en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DSA Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DSA Interview Prep incluye 4 lecciones en total.
Descripción general del problema de contar bits
El problema Counting Bits (LeetCode 338) plantea lo siguiente: dado n, devuelva un array ans de tamaño n+1, donde ans[i] es la cantidad de bits iguales a 1 en i. El enfoque ingenuo es O(n log n), ya que cuenta los bits de cada número por separado. El enfoque de DP es O(n), porque aprovecha la relación entre i y su mitad o su bit establecido menos significativo.
Dos observaciones clave sustentan la DP: (1) i >> 1 elimina el bit menos significativo, por lo que bits[i] = bits[i >> 1] + (i & 1). (2) Al borrar el bit establecido menos significativo: bits[i] = bits[i & (i-1)] + 1. Ambos enfoques ofrecen un tiempo O(n) y un espacio O(n) para el array de salida.
def count_bits_v1(n):
# O(n log n): naive individual count
return [bin(i).count('1') for i in range(n + 1)]
def count_bits_dp(n):
# O(n): DP using right shift
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1) # i >> 1 drops last bit
return dp
def count_bits_dp2(n):
# O(n): DP using lowest-set-bit trick
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i & (i - 1)] + 1 # i & (i-1) clears lowest set bit
return dp
n = 10
print('Naive:', count_bits_v1(n))
print('DP v1:', count_bits_dp(n))
print('DP v2:', count_bits_dp2(n))Por qué funcionan las recurrencias de la DP
Para la recurrencia de desplazamiento a la derecha dp[i] = dp[i >> 1] + (i & 1): dividir entre 2 mediante un desplazamiento a la derecha elimina el último bit. Si el último bit era 1, el conteo aumenta en 1; si era 0, no cambia. Por tanto, bits[i] = bits[i // 2] + (i mod 2).
Para la recurrencia del bit establecido menos significativo dp[i] = dp[i & (i-1)] + 1: i & (i-1) borra el bit 1 situado más a la derecha, por lo que tiene un bit establecido menos que i. Por tanto, el conteo es el conteo de ese valor reducido más 1. Ambas recurrencias procesan i en orden creciente, de modo que los subproblemas más pequeños siempre se resuelven primero.
# Trace both recurrences for i = 0..8
print('i | i>>1 | i&1 | dp[i>>1]+(i&1) | i&(i-1) | 1+dp[i&(i-1)]')
print('-' * 60)
dp = [0] * 9
for i in range(1, 9):
# Right shift method
v1 = dp[i >> 1] + (i & 1)
# Lowest set bit method
v2 = dp[i & (i - 1)] + 1
dp[i] = v1 # either works
print(f'{i:2d} ({bin(i)[2:]:4s}) | {i>>1:2d} | {i&1} | {v1} | {i&(i-1):2d} | {v2}')
print('\nFinal dp:', dp)Número que falta: enfoques de XOR y de suma
El problema Missing Number (LeetCode 268) proporciona un array de n números distintos en [0, n], con exactamente uno ausente. El enfoque con XOR: aplique XOR a todos los índices de 0..n y a todos los valores del array. Los pares se cancelan y queda el número que falta. El enfoque con suma: expected = n*(n+1)//2; devuelva expected - sum(nums).
Ambos enfoques tienen un tiempo O(n) y un espacio O(1). El enfoque con XOR es más robusto en lenguajes con enteros de ancho fijo, ya que evita posibles desbordamientos. En Python, ambos funcionan correctamente porque los enteros tienen precisión arbitraria.
def missing_xor(nums):
n = len(nums)
result = n
for i, val in enumerate(nums):
result ^= i ^ val # each index i cancels its matching value
return result
def missing_sum(nums):
n = len(nums)
return n * (n + 1) // 2 - sum(nums)
test_cases = [
[3, 0, 1], # missing 2
[0, 1], # missing 2
[9,6,4,2,3,5,7,0,1], # missing 8
[0], # missing 1
]
for nums in test_cases:
print(f'{nums} => XOR={missing_xor(nums)}, Sum={missing_sum(nums)}')Invertir los bits de un entero de 32 bits
El problema Reverse Bits (LeetCode 190) le pide invertir la representación binaria de un entero sin signo de 32 bits. El enfoque iterativo procesa cada uno de los 32 bits de la entrada, de derecha a izquierda, y los coloca en la salida de izquierda a derecha. En cada iteración, extraiga el bit situado más a la derecha con n & 1, desplace la salida a la izquierda para hacer sitio, aplique OR al bit y, después, desplace n a la derecha.
Después de 32 iteraciones, el entero de salida contiene los 32 bits de n en orden inverso. Esto es O(32) = O(1) por llamada, o O(1) amortizado con almacenamiento en caché para llamadas repetidas sobre bloques de 8 bits.
def reverse_bits(n):
result = 0
for _ in range(32):
result = (result << 1) | (n & 1) # shift result left, OR in rightmost bit
n >>= 1 # move to next bit
return result
# Test with known values
print(reverse_bits(0b00000010100101000001111010011100)) # 964176192
print(reverse_bits(0b11111111111111111111111111111101)) # 3221225471
print(reverse_bits(0)) # 0
print(reverse_bits(1)) # 2147483648 (bit 0 goes to bit 31)
print(reverse_bits(0b10000000000000000000000000000000)) # 1Invertir bits: divide y vencerás
Un enfoque más rápido, O(log 32) = O(1), invierte los bits mediante un intercambio de divide y vencerás. Primero intercambia los bits adyacentes; después, los grupos adyacentes de 2 bits; luego, los grupos de 4 bits; y así sucesivamente. Cada nivel de intercambio utiliza máscaras para separar grupos alternos y desplazamientos para entrelazarlos. Después de 5 intercambios, los 32 bits quedan invertidos.
Este enfoque utiliza un número fijo de operaciones O(1), independientemente de la entrada, y se usa en implementaciones de hardware. Las máscaras son constantes: 0x55555555 (patrón alterno 01), 0x33333333 (patrón alterno 0011), 0x0f0f0f0f (patrón alterno 00001111), etc.
def reverse_bits_dc(n):
# Treat n as 32-bit unsigned
n &= 0xFFFFFFFF
# Swap adjacent bits
n = ((n & 0x55555555) << 1) | ((n >> 1) & 0x55555555)
# Swap adjacent 2-bit groups
n = ((n & 0x33333333) << 2) | ((n >> 2) & 0x33333333)
# Swap adjacent 4-bit groups
n = ((n & 0x0f0f0f0f) << 4) | ((n >> 4) & 0x0f0f0f0f)
# Swap adjacent bytes
n = ((n & 0x00ff00ff) << 8) | ((n >> 8) & 0x00ff00ff)
# Swap adjacent 16-bit halves
n = ((n & 0x0000ffff) << 16) | ((n >> 16) & 0x0000ffff)
return n & 0xFFFFFFFF
# Verify against iterative version
def reverse_bits_iter(n):
result = 0
for _ in range(32):
result = (result << 1) | (n & 1); n >>= 1
return result
for test in [0b10110100, 0b11111111, 0, 1, 0xDEADBEEF]:
assert reverse_bits_dc(test) == reverse_bits_iter(test)
print(f'{test:#010x} reversed: {reverse_bits_dc(test):#010x}')Cantidad de bits 1 (peso de Hamming)
El problema Number of 1 Bits (LeetCode 191) pide calcular el peso de Hamming (popcount) de un entero sin signo. Hay tres enfoques con distintas ventajas y desventajas: un bucle ingenuo (O(32)), el método de Brian Kernighan (O(k), donde k = cantidad de bits establecidos) y el método integrado de Python n.bit_count() (3.10 o posterior).
El método de Brian Kernighan es el preferido en las entrevistas porque demuestra que comprende el truco n & (n-1). Cada iteración elimina el bit establecido menos significativo, por lo que el bucle se ejecuta exactamente tantas veces como bits 1 haya; esto es mucho más rápido que recorrer los 32 bits completos en enteros dispersos.
def hamming_weight_naive(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
def hamming_weight_kernighan(n):
count = 0
while n:
n &= n - 1 # clear lowest set bit
count += 1
return count
# Python 3.10+
# def hamming_weight_builtin(n): return n.bit_count()
for n in [0, 1, 11, 128, 255, 0xDEADBEEF]:
naive = hamming_weight_naive(n)
kern = hamming_weight_kernighan(n)
bits = bin(n).count('1')
print(f'{n:#012b} ({n:10d}): naive={naive}, kern={kern}, bin={bits}')Suma de bits consecutivos: enfoque de suma de prefijos
A veces necesita contar rápidamente los bits 1 de un intervalo [l, r]. Construya una suma de prefijos de bits establecidos para 0..n: prefix[i] = prefix[i-1] + bin(i).count('1'). Después, el conteo del intervalo [l, r] es prefix[r] - prefix[l-1]. Esto permite realizar consultas de intervalos en O(1) después de un preprocesamiento O(n).
Esta técnica se generaliza a cualquier agregado basado en bits sobre un intervalo. Por ejemplo, para contar los números de [l, r] que tienen una cantidad par de bits establecidos, se utiliza la misma técnica de prefijos, pero con una función de acumulación diferente.
def build_bit_prefix(n):
prefix = [0] * (n + 2)
for i in range(1, n + 1):
prefix[i] = prefix[i - 1] + bin(i).count('1')
return prefix
def count_bits_range(prefix, l, r):
return prefix[r] - prefix[l - 1]
# Build prefix for 0..15
prefix = build_bit_prefix(15)
print('Prefix sums (set bit counts up to i):')
for i in range(16):
print(f' i={i:2d} ({bin(i)[2:]:4s}): bits={bin(i).count("1")}, prefix={prefix[i]}')
# Range queries
print(f'\nSet bits in [5, 10]: {count_bits_range(prefix, 5, 10)}')
print(f'Set bits in [1, 15]: {count_bits_range(prefix, 1, 15)}')Invertir bits de números negativos
En Python, los enteros tienen signo y un ancho arbitrario. Al invertir bits para el problema de LeetCode, debemos tratar la entrada como un entero sin signo de 32 bits. Aplique la máscara & 0xFFFFFFFF a la entrada antes de procesarla para asegurarse de que solo se consideren 32 bits. La salida también debe ser un entero sin signo de 32 bits, es decir, no negativo.
Si recibe un entero de Python que puede ser negativo, en el sentido de complemento a dos, aplique primero & 0xFFFFFFFF para obtener la representación sin signo de 32 bits y después inviértala. El resultado siempre es un entero no negativo entre 0 y 2^32 - 1.
def reverse_bits_signed_safe(n):
n &= 0xFFFFFFFF # treat as 32-bit unsigned
result = 0
for _ in range(32):
result = (result << 1) | (n & 1)
n >>= 1
return result & 0xFFFFFFFF
# Python treats -1 as all 1s in two's complement
print(f'-1 as 32-bit unsigned: {-1 & 0xFFFFFFFF:#010x}') # 0xffffffff
print(f'Reversed: {reverse_bits_signed_safe(-1):#010x}') # 0xffffffff (all 1s reversed = all 1s)
# -2 in 32-bit = 0xFFFFFFFE = 11...10
print(f'-2 as 32-bit unsigned: {-2 & 0xFFFFFFFF:#010x}') # 0xfffffffe
print(f'Reversed: {reverse_bits_signed_safe(-2):#010x}') # 0x7fffffffDP de manipulación de bits: patrones de conteo de bits
El problema de contar bits revela un patrón general de la DP de bits: si conoce la respuesta para una versión más pequeña de i, puede calcularla para i mediante una operación de bits de tiempo constante. Este patrón se generaliza a otros problemas de conteo de bits, como contar los números con exactamente k bits establecidos en [0, n] mediante enumeración binaria, o encontrar la mayor potencia de dos que divide cada número.
Otra observación útil es que la cantidad de bits establecidos de i sigue un patrón repetitivo dentro de cada intervalo de potencias de dos. El patrón para [2^k, 2^(k+1) - 1] es igual al de [0, 2^k - 1], con cada valor incrementado en 1, porque el bit k siempre está establecido en este intervalo.
# Visualise the repeating pattern
def show_bit_pattern(n):
bits = [bin(i).count('1') for i in range(n + 1)]
print('i | bits | pattern')
for i, b in enumerate(bits):
block = i.bit_length() - 1 if i > 0 else 0
print(f'{i:2d} ({bin(i)[2:]:4s}) | {b} | block {block}')
return bits
bits = show_bit_pattern(15)
# Verify the pattern: bits[i] = bits[i - highest_power] + 1 for i >= 2^k
print('\nVerify pattern:')
for i in range(1, 16):
highest_pow = 1 << (i.bit_length() - 1)
if highest_pow < i:
prev_i = i - highest_pow
print(f'bits[{i}] = bits[{prev_i}] + 1 = {bits[prev_i]} + 1 = {bits[i]}')Combinación de las tres técnicas: ejercicio integrado
Muchos problemas de entrevistas combinan el conteo de bits, la lógica de números que faltan y la inversión de bits en una sola pregunta. Por ejemplo: dado un array cuyos elementos son enteros de n bits y en el que falta uno, encuentre el valor que falta. O bien: dada una secuencia de conteos de bits, reconstruya el entero que falta. Estos problemas requieren reconocer qué subtécnica se aplica.
Practique la creación de un mapa mental: si un problema menciona encontrar elementos que faltan, piense en XOR o en la suma. Si dice «cuente los 1 de forma eficiente», piense en Kernighan o en DP. Si dice «invierta los bits», piense en el enfoque iterativo o en divide y vencerás. Estas son las tres herramientas fundamentales de la manipulación de bits en las entrevistas.
# Integrated exercise: given bit-count array, find the missing number
# arr[i] = number of 1 bits in i, for all i in 0..n except one
# Reconstruct the missing number
def find_missing_from_bit_counts(bit_counts, n):
# Rebuild full count array
full = [bin(i).count('1') for i in range(n + 1)]
# Find which index is missing by comparing
for i, count in enumerate(bit_counts):
if full[i] != count:
return i - 1 # the entry before the mismatch is missing
return n # last element missing
# Simpler: use XOR on indices matching bit counts
# (This is simplified for illustration)
bits = [0,1,1,2,1,2,2,3,0,1] # bit counts for 0..9 with 8 missing
# Normal: [0,1,1,2,1,2,2,3,1,2]
# Missing is index 8
full = [bin(i).count('1') for i in range(10)]
missing_idx = None
for i in range(10):
if i >= len(bits) or bits[i] != full[i]:
missing_idx = i
break
print(f'Missing number: {missing_idx}')Almacenamiento en caché de bits para invertir bits
Para llamadas repetidas a la inversión de bits, por ejemplo, en una simulación de hardware, almacene en caché los resultados para bloques de 8 bits. Como cada byte solo puede tener 256 valores, precalcule el byte invertido para cada valor de 0 a 255. Para invertir un entero de 32 bits, divídalo en cuatro bloques de 8 bits, invierta cada uno y vuelva a ensamblarlos en orden inverso.
Esto reduce cada llamada a cuatro consultas de tabla y operaciones de bits, mucho más rápidas que un bucle de 32 iteraciones para el procesamiento masivo. La caché se construye una vez en un tiempo O(256 × 8) y se reutiliza para todas las llamadas posteriores en O(1).
# Build 8-bit reverse cache
def build_reverse_byte_cache():
cache = [0] * 256
for i in range(256):
n, result = i, 0
for _ in range(8):
result = (result << 1) | (n & 1)
n >>= 1
cache[i] = result
return cache
cache = build_reverse_byte_cache()
def reverse_bits_cached(n):
return (cache[n & 0xFF] << 24 |
cache[(n >> 8) & 0xFF] << 16 |
cache[(n >> 16) & 0xFF] << 8 |
cache[(n >> 24) & 0xFF])
# Test
for test in [0b10110100, 0b11111111, 0x12345678]:
cached = reverse_bits_cached(test)
# Reference: iterative
n, result = test, 0
for _ in range(32): result = (result << 1) | (n & 1); n >>= 1
assert cached == result
print(f'{test:#010x} => {cached:#010x}')Comprobación rápida
Compruebe sus conocimientos sobre los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.
Resumen de la lección
En esta lección ha aprendido que el conteo de bits utiliza DP con dp[i] = dp[i >> 1] + (i & 1) o dp[i] = dp[i & (i-1)] + 1 para obtener un tiempo O(n); que el número que falta se resuelve en O(n)/O(1) aplicando XOR a todos los índices y todos los valores, o mediante la fórmula de la suma aritmética; y que la inversión de 32 bits se realiza de forma iterativa en O(32) o mediante la técnica de máscaras de divide y vencerás. A continuación exploraremos las pilas monótonas, empezando por la invariante creciente frente a decreciente y las consultas del siguiente elemento mayor.
Preguntas frecuentes
¿La lección «Contar bits, número ausente e invertir bits» es gratis?
Sí — el texto completo de «Contar bits, número ausente e invertir bits» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DSA Interview Prep, actualiza a CoddyKit PRO. El curso de DSA Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Contar bits, número ausente e invertir bits»?
Calcule el número de bits para 0..n mediante PD y el truco del bit menos significativo establecido, encuentre un número ausente con XOR e invierta los bits de un entero de 32 bits. Practicas DSA Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar DSA Interview Prep?
No se requiere experiencia previa. DSA Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.
¿Cuánto tiempo toma la lección «Contar bits, número ausente e invertir bits»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de DSA Interview Prep?
Sí. Cada lección de DSA Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Operadores bit a bit: AND, OR, XOR, NOT y desplazamientos
- Single Number y propiedades de XOR
- Máscaras de bits: establecer, borrar, alternar y comprobar
- Contar bits, número ausente e invertir bits