Conteggio dei bit, numero mancante e inversione dei bit
Calcoli il numero di bit impostati per 0..n usando la DP e il metodo del bit impostato meno significativo, trovi un numero mancante tramite XOR e inverta i bit di un intero a 32 bit.
Conteggio dei bit, numero mancante e inversione dei bit è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.
Panoramica del problema Counting Bits
Il problema Counting Bits (LeetCode 338) richiede, dato n, di restituire un array ans di dimensione n+1 in cui ans[i] è il numero di bit a 1 in i. L'approccio ingenuo è O(n log n): contare i bit di ogni numero individualmente. L'approccio DP è O(n), perché sfrutta la relazione tra i e la sua metà o il suo bit meno significativo impostato.
La DP si basa su due osservazioni fondamentali: (1) i >> 1 elimina il bit meno significativo, quindi bits[i] = bits[i >> 1] + (i & 1). (2) Cancellando il bit meno significativo impostato: bits[i] = bits[i & (i-1)] + 1. Entrambi gli approcci richiedono tempo O(n) e spazio O(n) per l'array di output.
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))Perché funzionano le ricorrenze della DP
Per la ricorrenza dello shift a destra dp[i] = dp[i >> 1] + (i & 1): dividere per 2, effettuando uno shift a destra, rimuove l'ultimo bit. Se l'ultimo bit era 1, il conteggio aumenta di 1; se era 0, non cambia. Quindi bits[i] = bits[i // 2] + (i mod 2).
Per la ricorrenza basata sul bit meno significativo impostato dp[i] = dp[i & (i-1)] + 1: i & (i-1) cancella il bit a 1 più a destra, quindi ha un bit impostato in meno rispetto a i. Il conteggio è dunque quello del valore ridotto, più 1. Entrambe le ricorrenze elaborano i in ordine crescente, così i sottoproblemi più piccoli vengono sempre risolti per primi.
# 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)Numero mancante: approcci XOR e somma
Il problema Missing Number (LeetCode 268) fornisce un array di n numeri distinti nell'intervallo [0, n], con esattamente un numero mancante. L'approccio XOR: applicare XOR a tutti gli indici da 0 a n e a tutti i valori dell'array. Le coppie si annullano, lasciando il numero mancante. L'approccio della somma: expected = n*(n+1)//2, quindi restituire expected - sum(nums).
Entrambi gli approcci richiedono tempo O(n) e spazio O(1). L'approccio XOR è più robusto nei linguaggi con interi a larghezza fissa, perché evita un possibile overflow. In Python, entrambi funzionano senza problemi, poiché gli interi hanno precisione 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)}')Inversione dei bit di un intero a 32 bit
Il problema Reverse Bits (LeetCode 190) richiede di invertire la rappresentazione binaria di un intero senza segno a 32 bit. L'approccio iterativo consiste nell'elaborare ciascuno dei 32 bit dell'input da destra a sinistra e nel collocarli da sinistra a destra nell'output. A ogni iterazione si estrae il bit più a destra con n & 1, si sposta l'output a sinistra per fare spazio, si inserisce il bit con OR e infine si sposta n a destra.
Dopo 32 iterazioni, l'intero di output contiene tutti i 32 bit di n in ordine inverso. Si tratta di O(32) = O(1) per chiamata, oppure di O(1) ammortizzato con il caching per chiamate ripetute su blocchi da 8 bit.
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)) # 1Inversione dei bit: divide et impera
Un approccio più rapido O(log 32) = O(1) inverte i bit usando uno scambio divide et impera. Prima si scambiano i bit adiacenti, poi i gruppi adiacenti di 2 bit, quindi i gruppi di 4 bit e così via. Ogni livello di scambio usa maschere per separare i gruppi alternati e shift per intercalarli. Dopo 5 scambi, tutti i 32 bit sono invertiti.
Questo approccio usa un numero fisso di operazioni O(1), indipendentemente dall'input, ed è utilizzato nelle implementazioni hardware. Le maschere sono costanti: 0x55555555 (pattern alternato 01), 0x33333333 (pattern alternato 0011), 0x0f0f0f0f (pattern alternato 00001111) e così via.
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}')Numero di bit a 1 (peso di Hamming)
Il problema Number of 1 Bits (LeetCode 191) richiede il peso di Hamming (popcount) di un intero senza segno. Esistono tre approcci, con compromessi diversi: ciclo ingenuo (O(32)), metodo di Brian Kernighan (O(k), dove k = numero di bit impostati) e funzione integrata di Python n.bit_count() (3.10+).
Il metodo di Brian Kernighan è preferibile nei colloqui perché dimostra la comprensione del trucco n & (n-1). A ogni iterazione rimuove il bit meno significativo impostato, quindi il ciclo viene eseguito esattamente tante volte quanti sono i bit a 1: è molto più veloce di una scansione completa a 32 bit per gli interi sparsi.
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}')Somma di bit consecutivi: approccio con prefissi
A volte è necessario contare rapidamente i bit a 1 in un intervallo [l, r]. Costruisca una somma prefissa dei bit impostati per 0..n: prefix[i] = prefix[i-1] + bin(i).count('1'). Il conteggio per l'intervallo [l, r] è quindi prefix[r] - prefix[l-1]. In questo modo, dopo una preelaborazione O(n), è possibile eseguire query sugli intervalli in O(1).
Questa tecnica si generalizza a qualsiasi aggregato basato sui bit su un intervallo. Ad esempio, per contare i numeri in [l, r] con un numero pari di bit impostati si usa la stessa tecnica dei prefissi, ma con una funzione di accumulazione diversa.
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)}')Inversione dei bit per numeri negativi
In Python, gli interi sono con segno e hanno larghezza arbitraria. Quando si invertono i bit per il problema di LeetCode, è necessario trattare l'input come un intero senza segno a 32 bit. Applichi la maschera & 0xFFFFFFFF all'input prima dell'elaborazione, per assicurarsi di considerare solo 32 bit. Anche l'output deve essere un intero senza segno a 32 bit, quindi non negativo.
Se Le viene fornito un intero Python che può essere negativo, in senso complemento a due, applichi prima & 0xFFFFFFFF per ottenere la rappresentazione senza segno a 32 bit, quindi lo inverta. Il risultato è sempre un intero non negativo compreso tra 0 e 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 sulla manipolazione dei bit: pattern del conteggio dei bit
Il problema del conteggio dei bit mostra un pattern generale per la DP sui bit: se conosce la risposta per una versione più piccola di i, può calcolarla per i usando un'operazione sui bit a tempo costante. Questo pattern si generalizza ad altri problemi di conteggio dei bit, come contare i numeri con esattamente k bit impostati nell'intervallo [0, n] usando l'enumerazione binaria, oppure trovare la potenza di due più alta che divide ciascun numero.
Un'altra osservazione utile è che il conteggio dei bit impostati di i segue un pattern ripetuto all'interno di ogni intervallo definito da una potenza di due. Il pattern per [2^k, 2^(k+1) - 1] è uguale a quello per [0, 2^k - 1], con ogni valore incrementato di 1, perché il bit k è sempre impostato in questo intervallo.
# 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]}')Combinare tutti e tre: esercizio integrato
Molti problemi da colloquio combinano il conteggio dei bit, la logica dei numeri mancanti e l'inversione dei bit in un'unica domanda. Ad esempio: dato un array i cui elementi sono interi a n bit e in cui ne manca uno, trovare il valore mancante. Oppure: dato un flusso di conteggi dei bit, ricostruire l'intero mancante. Questi problemi richiedono di riconoscere quale sottotecnica applicare.
Si eserciti a costruire una mappa mentale: se un problema parla di trovare elementi mancanti, pensi a XOR o alla somma. Se chiede di «contare gli 1 in modo efficiente», pensi a Kernighan o alla DP. Se chiede di «invertire i bit», pensi all'approccio iterativo o a quello divide et impera. Questi sono i tre strumenti fondamentali della manipolazione dei bit nei colloqui.
# 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}')Caching per l'inversione dei bit
Per chiamate ripetute all'inversione dei bit, ad esempio in una simulazione hardware, memorizzi nella cache i risultati per blocchi da 8 bit. Poiché ogni byte può assumere solo 256 valori, precalcoli il byte invertito per ogni valore da 0 a 255. Per invertire un intero a 32 bit, lo suddivida in quattro blocchi da 8 bit, inverta ciascuno di essi e li riassembli nell'ordine inverso.
In questo modo ogni chiamata si riduce a quattro accessi a una tabella e ad alcune operazioni sui bit, molto più rapide di un ciclo da 32 iterazioni per l'elaborazione in blocco. La cache viene creata una volta in O(256 × 8) e riutilizzata per tutte le chiamate successive in 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}')Verifica rapida
Metta alla prova la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep affrontati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: il conteggio dei bit usa la DP con dp[i] = dp[i >> 1] + (i & 1) oppure dp[i] = dp[i & (i-1)] + 1, con tempo O(n); il numero mancante si trova in O(n)/O(1) applicando XOR a tutti gli indici e a tutti i valori oppure usando la formula della somma aritmetica; inoltre, l'inversione di 32 bit si esegue iterativamente in O(32) oppure con la tecnica delle maschere divide et impera. Prossimamente esploreremo gli stack monotoni, iniziando dall'invariante crescente rispetto a quella decrescente e dalle query del successivo elemento maggiore.
Domande Frequenti
La lezione «Conteggio dei bit, numero mancante e inversione dei bit» è gratuita?
Sì — il testo completo di «Conteggio dei bit, numero mancante e inversione dei bit» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.
Cosa imparerò in «Conteggio dei bit, numero mancante e inversione dei bit»?
Calcoli il numero di bit impostati per 0..n usando la DP e il metodo del bit impostato meno significativo, trovi un numero mancante tramite XOR e inverta i bit di un intero a 32 bit. Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare DSA Interview Prep?
Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Conteggio dei bit, numero mancante e inversione dei bit»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?
Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Operatori bitwise: AND, OR, XOR, NOT e shift
- Single Number e proprietà di XOR
- Maschere di bit: impostare, azzerare, invertire e verificare
- Conteggio dei bit, numero mancante e inversione dei bit