0Pricing
Coding Interview Prep · Leçon

Compter les bits, nombre manquant et inversion des bits

Calculez les nombres de bits pour 0..n avec DP et l’astuce du bit de poids faible défini, trouvez un nombre manquant avec XOR et inversez les bits d’un entier sur 32 bits.

Compter les bits, nombre manquant et inversion des bits est une leçon Coding Interview Prep gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Coding Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Coding Interview Prep comprend 4 leçons au total.

Présentation du problème de comptage des bits

Le problème de comptage des bits (LeetCode 338) demande, étant donné n, de renvoyer un tableau ans de taille n+1 dans lequel ans[i] est le nombre de bits à 1 de i. L'approche naïve prend O(n log n) : compter individuellement les bits de chaque nombre. L'approche par DP prend O(n) en exploitant the relation entre i et sa moitié ou son bit positionné de poids faible.

Deux observations clés alimentent the DP : (1) i >> 1 supprime le bit de poids faible, donc bits[i] = bits[i >> 1] + (i & 1). (2) Effacement du bit positionné de poids faible : bits[i] = bits[i & (i-1)] + 1. Les deux méthodes donnent un temps d'exécution de O(n) et un espace de O(n) pour le tableau de sortie.

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))

Pourquoi les récurrences de la DP fonctionnent

Pour la récurrence par décalage à droite dp[i] = dp[i >> 1] + (i & 1) : la division par 2, c'est-à-dire le décalage à droite, supprime le dernier bit. Si ce dernier bit valait 1, le compteur augmente de 1 ; s'il valait 0, il ne change pas. Ainsi, bits[i] = bits[i // 2] + (i modulo 2).

Pour la récurrence fondée sur le bit positionné de poids faible dp[i] = dp[i & (i-1)] + 1 : i & (i-1) efface le bit 1 le plus à droite, et possède donc un bit positionné de moins que i. Le compteur est donc égal au compteur de cette valeur réduite, plus 1. Les deux récurrences traitent i dans l'ordre croissant, de sorte que les sous-problèmes plus petits sont toujours résolus en premier.

# 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)

Nombre manquant : approches par XOR et par somme

Le problème du nombre manquant (LeetCode 268) fournit un tableau de n nombres distincts dans [0, n], dont exactly un est manquant. L'approche par XOR consiste à effectuer un XOR entre tous les indices de 0 à n et toutes les valeurs du tableau. Les paires s'annulent et il ne reste que le nombre manquant. L'approche par somme utilise expected = n*(n+1)//2, puis renvoie expected - sum(nums).

Les deux approches prennent O(n) en temps et O(1) en espace. L'approche par XOR est plus robuste dans les langages utilisant des entiers de largeur fixe, car elle évite un éventuel dépassement de capacité. En Python, les deux fonctionnent correctement puisque les entiers ont une précision arbitraire.

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)}')

Inversion des bits d'un entier de 32 bits

Le problème d'inversion des bits (LeetCode 190) vous demande d'inverser la représentation binaire d'un entier non signé de 32 bits. L'approche itérative consiste à traiter chacun des 32 bits de l'entrée de droite à gauche, en les plaçant de gauche à droite dans la sortie. À chaque itération, extrayez le bit le plus à droite avec n & 1, décalez la sortie vers la gauche pour créer de la place, effectuez un OR avec le bit, puis décalez n vers la droite.

Après 32 itérations, l'entier de sortie contient les 32 bits de n dans l'ordre inversé. Cela prend O(32) = O(1) par appel, ou O(1) amorti avec une mise en cache pour les appels répétés sur des blocs 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))  # 1

Inversion des bits : diviser pour régner

Une approche plus rapide en O(log 32) = O(1) inverse les bits à l'aide d'un échange par diviser pour régner. Commencez par échanger les bits adjacents, puis les groupes adjacents de 2 bits, puis les groupes de 4 bits, et ainsi de suite. À chaque niveau d'échange, utilisez des masques pour séparer les groupes alternés et des décalages pour les entrelacer. Après 5 échanges, les 32 bits sont inversés.

Cette approche utilise un nombre fixe d'opérations en O(1), quelle que soit l'entrée, et elle est utilisée dans les implémentations matérielles. Les masques sont des constantes : 0x55555555 (motif alterné 01), 0x33333333 (motif alterné 0011), 0x0f0f0f0f (motif alterné 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}')

Nombre de bits à 1 (poids de Hamming)

Le problème du nombre de bits à 1 (LeetCode 191) demande le poids de Hamming, c'est-à-dire le nombre de bits à 1, d'un entier non signé. Trois approches offrent des compromis différents : la boucle naïve (O(32)), la méthode de Brian Kernighan (O(k), où k est le nombre de bits positionnés) et la méthode intégrée de Python n.bit_count() (3.10 ou version ultérieure).

La méthode de Brian Kernighan est privilégiée lors des entretiens, car elle démontre la compréhension de l'astuce n & (n-1). Chaque itération supprime le bit positionné de poids faible, de sorte que la boucle s'exécute exactement autant de fois qu'il y a de bits à 1 — bien plus rapidement qu'un parcours complet des 32 bits pour les entiers peu denses.

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}')

Somme de bits consécutifs : approche par préfixes

Vous devez parfois compter rapidement les bits à 1 dans une plage [l, r]. Construisez une somme préfixe des bits positionnés pour 0..n : prefix[i] = prefix[i-1] + bin(i).count('1'). Le nombre de bits à 1 dans la plage [l, r] est alors prefix[r] - prefix[l-1]. Cela permet d'effectuer des requêtes sur une plage en O(1) après un prétraitement en O(n).

Cette technique se généralise à tout agrégat fondé sur les bits dans une plage. Par exemple, pour compter les nombres de [l, r] ayant un nombre pair de bits positionnés, utilisez la même technique de préfixes avec une fonction d'accumulation différente.

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)}')

Inversion des bits pour les nombres négatifs

En Python, les entiers sont signés et leur largeur est arbitraire. Lors de l'inversion des bits pour le problème de LeetCode, nous devons traiter l'entrée comme un entier non signé de 32 bits. Appliquez le masque & 0xFFFFFFFF à l'entrée avant le traitement afin de ne prendre en compte que 32 bits. La sortie doit également être un entier non signé de 32 bits, donc non négatif.

Si l'on vous fournit un entier Python potentiellement négatif, au sens du complément à deux, appliquez d'abord & 0xFFFFFFFF pour obtenir sa représentation non signée sur 32 bits, puis inversez ses bits. Le résultat est toujours un entier non négatif compris entre 0 et 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}')   # 0x7fffffff

DP de manipulation des bits : motifs de comptage

Le problème du comptage des bits révèle un schéma général de la DP sur les bits : si vous connaissez la réponse pour une version plus petite de i, vous pouvez la calculer pour i à l'aide d'une opération sur les bits en temps constant. Ce schéma se généralise à d'autres problèmes de comptage des bits, comme compter les nombres ayant exactement k bits positionnés dans [0, n] à l'aide d'une énumération binaire, ou trouver la plus grande puissance de deux qui divise chaque nombre.

Autre observation utile : le nombre de bits positionnés de i suit un motif répétitif dans chaque intervalle correspondant à une puissance de deux. Le motif pour [2^k, 2^(k+1) - 1] est identique à celui de [0, 2^k - 1], chaque valeur étant augmentée de 1, car le bit k est toujours positionné dans cet intervalle.

# 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]}')

Combiner les trois approches : exercice intégré

De nombreux problèmes d'entretien combinent le comptage des bits, la logique des nombres manquants et l'inversion des bits en une seule question. Par exemple : étant donné un tableau dont les éléments sont des entiers sur n bits et dont l'un est manquant, trouvez la valeur manquante. Autre exemple : étant donné un flux de nombres de bits, reconstituez l'entier manquant. Ces problèmes exigent de reconnaître quelle sous-technique appliquer.

Entraînez-vous à établir une carte mentale : si un problème mentionne la recherche d'éléments manquants, pensez au XOR ou à la somme. S'il demande de « compter efficacement les bits à 1 », pensez à Kernighan ou à la DP. S'il demande d'« inverser les bits », pensez à l'approche itérative ou à celle de diviser pour régner. Ce sont les trois outils fondamentaux de la manipulation des bits lors des entretiens.

# 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}')

Mise en cache des bits pour leur inversion

Pour les appels répétés à l'inversion des bits, par exemple dans une simulation matérielle, mettez en cache les résultats pour des blocs de 8 bits. Comme chaque octet ne peut prendre que 256 valeurs, précalculez l'octet inversé pour chaque valeur de 0 à 255. Pour inverser un entier de 32 bits, divisez-le en quatre blocs de 8 bits, inversez chacun d'eux, puis réassemblez-les dans l'ordre inverse.

Cela ramène chaque appel à quatre accès à une table et à des opérations sur les bits — bien plus rapide qu'une boucle de 32 itérations pour le traitement en masse. Le cache est construit une seule fois en O(256 × 8), puis réutilisé pour tous les appels suivants 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}')

Vérification rapide

Évaluez votre compréhension des concepts de structures de données et d'algorithmes — préparation aux entretiens de programmation — abordés dans cette leçon.

Récapitulatif de la leçon

Dans cette leçon, vous avez appris que le comptage des bits utilise la DP avec dp[i] = dp[i >> 1] + (i & 1) ou dp[i] = dp[i & (i-1)] + 1, pour un temps d'exécution de O(n), que le nombre manquant se résout en O(n)/O(1) en effectuant un XOR entre tous les indices et toutes les valeurs, ou en utilisant la formule de somme arithmétique, et que l'inversion de 32 bits s'effectue de manière itérative en O(32) ou avec la technique de masques par diviser pour régner. Nous allons ensuite étudier les piles monotones, en commençant par l'invariant croissant ou décroissant et les requêtes sur l'élément supérieur suivant.

Questions Fréquemment Posées

La leçon « Compter les bits, nombre manquant et inversion des bits » est-elle gratuite ?

Oui — le texte complet de « Compter les bits, nombre manquant et inversion des bits » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Coding Interview Prep, passe à CoddyKit PRO. Le cours Coding Interview Prep comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Compter les bits, nombre manquant et inversion des bits » ?

Calculez les nombres de bits pour 0..n avec DP et l’astuce du bit de poids faible défini, trouvez un nombre manquant avec XOR et inversez les bits d’un entier sur 32 bits. Tu pratiques Coding Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Coding Interview Prep ?

Aucune expérience préalable n'est requise. Coding Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Compter les bits, nombre manquant et inversion des bits » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Coding Interview Prep ?

Oui. Chaque leçon Coding Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Opérateurs bit à bit : AND, OR, XOR, NOT et décalages
  2. Nombre unique et propriétés de XOR
  3. Masques binaires : définir, effacer, inverser et vérifier
  4. Compter les bits, nombre manquant et inversion des bits
← Retour à Coding Interview Prep