0Pricing
DSA Interview Prep · 课时

位计数、缺失数字与位反转

使用 DP 和最低有效置位技巧计算 0..n 的位计数,通过 XOR 找出缺失数字,并反转 32 位整数的所有位。

位计数、缺失数字与位反转 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。

位计数问题概览

位计数问题(LeetCode 338)要求:给定 n,返回一个大小为 n+1 的数组 ans,其中 ans[i] 是 i 中 1 位的数量。朴素方法需要 O(n log n) 时间,即分别统计每个数字中的位。DP 方法利用 i 与其一半或最低置位位之间的关系,将时间复杂度降为 O(n)。

DP 由两个关键观察驱动:(1) i >> 1 会去掉最低位,因此 bits[i] = bits[i >> 1] + (i & 1)。(2) 清除最低置位位:bits[i] = bits[i & (i-1)] + 1。两种方法的时间复杂度都是 O(n),空间复杂度都是 O(n)(用于输出数组)。

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

为何 DP 递推式成立

对于the 右移递推式 dp[i] = dp[i >> 1] + (i & 1):除以 2(右移)会移除最后一位。如果最后一位是 1,计数增加 1;如果是 0,则不变。因此,bits[i] = bits[i // 2] + (i mod 2)。

对于最低置位位递推式 dp[i] = dp[i & (i-1)] + 1:i & (i-1) 会清除最右侧的 1 位,因此它比 i 少一个置位位。所以,计数就是该缩小后数值的计数加 1。两种递推式都按 i 递增的顺序处理,因此较小的子问题总会先得到解决。

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

缺失数字:XOR 与求和方法

缺失数字问题(LeetCode 268)给定一个包含 n 个互不相同数字的数组,这些数字属于 [0, n],其中恰好缺少一个数字。XOR 方法:将索引 0..n 与数组中的所有值进行 XOR。成对的值会相互抵消,最后留下缺失数字。求和方法:expected = n*(n+1)//2,返回 expected - sum(nums)。

两种方法的时间复杂度都是 O(n),空间复杂度都是 O(1)。在使用固定宽度整数的语言中,XOR 方法更加稳健,因为它可以避免潜在的溢出。在这种语言中,两种方法都能正常工作,因为整数具有任意精度。

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

反转 32 位整数中的位

位反转问题(LeetCode 190)要求您反转一个 32 位无符号整数的二进制表示。迭代方法是:从输入值的右向左处理 32 个位,并将它们从左向右放入输出值。每次迭代都要使用 n & 1 提取最右侧的位,将输出值左移以腾出空间,使用 OR 运算放入该位,然后将 n 右移。

经过 32 次迭代后,输出整数将包含 n 的全部 32 个位,只是顺序相反。该方法的复杂度为 O(32) = O(1)(每次调用),对于使用 8 位块的重复调用,配合缓存后摊销复杂度为 O(1)。

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

位反转:分治法

一种更快的 O(log 32) = O(1) 方法使用分治交换来反转位。首先交换相邻位,然后交换相邻的 2 位组,再交换 4 位组,以此类推。每一层交换都使用掩码分离交替的位组,并通过移位将它们交错合并。经过 5 次交换后,全部 32 个位都会被反转。

无论输入是什么,该方法都只使用 O(1) 个固定操作,因此用于硬件实现。掩码是常量:0x55555555(交替的 01 模式)、0x33333333(交替的 0011)、0x0f0f0f0f(交替的 00001111)等。

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

1 位数量(汉明重量)

1 位数量问题(LeetCode 191)要求计算一个无符号整数的汉明重量(1 位计数)。有三种权衡不同的方法:朴素循环(O(32))、克尼汉方法(O(k),其中 k = 置位位的数量),以及内置的 n.bit_count()(3.10 及更高版本)。

面试中通常推荐克尼汉方法,因为它展示了对 n & (n-1) 技巧的理解。每次迭代都会移除最低置位位,因此循环恰好运行与 1 位数量相同的次数——对于稀疏整数,这比完整扫描 32 位快得多。

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

连续位计数:前缀方法

有时您需要快速统计范围 [l, r] 中的 1 位数量。为 0..n 建立一个置位位前缀和:prefix[i] = prefix[i-1] + bin(i).count('1')。然后,范围 [l, r] 的计数为 prefix[r] - prefix[l-1]。预处理需要 O(n) 时间,之后即可用 O(1) 时间回答范围查询。

这种方法可以推广到范围上的任何基于位的聚合。例如,要统计 [l, r] 中具有偶数个置位位的数字,可以使用相同的前缀技术,只需更换累加函数。

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

负数的位反转

在这种语言中,整数带符号且位宽任意。对于 LeetCode 问题中的位反转,我们必须将输入视为32 位无符号整数。处理前使用 & 0xFFFFFFFF 对输入进行掩码,以确保只考虑 32 个位。输出也应是一个无符号的 32 位整数(非负数)。

如果给定的整数可能为负数(按照二进制补码的含义),请先对其应用 & 0xFFFFFFFF,得到无符号的 32 位表示,然后再进行反转。结果始终是介于 0 和 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:位计数模式

位计数问题揭示了位 DP 的一个通用模式:如果您知道较小版本 i 的答案,就可以通过一次常数时间的位操作计算 i 的答案。该模式还可以推广到其他位计数问题,例如统计 [0, n] 中恰好有 k 个置位位的数字(使用二进制枚举),或者求每个数字的最大二次幂因子。

另一个有用的观察是:在每个二次幂区间内,i 的置位位数量会呈现重复模式。区间 [2^k, 2^(k+1) - 1] 的模式与 [0, 2^k - 1] 相同,只是每个值都增加了 1,因为在这个范围内第 k 位始终为置位状态。

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

综合练习:结合三种方法

许多面试题会将位计数、缺失数字逻辑和位反转结合在一个问题中。例如:给定一个数组,其中的元素是 n 位整数且缺少一个值,找出缺失的值。或者:给定一个位计数数据流,重建缺失的整数。这些问题要求您识别应使用哪种子技术。

请练习建立思维导图:如果问题提到查找缺失元素,请考虑 XOR 或求和。如果问题要求“高效地统计 1 的数量”,请考虑克尼汉方法或 DP。如果问题要求“反转位”,请考虑迭代法或分治法。这三种方法是面试中位操作的核心工具。

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

位反转缓存

对于重复执行的位反转调用(例如在硬件模拟中),请为 8 位块缓存结果。由于每个字节只能取 256 个值,因此可以预先计算 0-255 中每个值的反转字节。要反转一个 32 位整数,请将其拆分为四个 8 位块,分别反转,然后按相反顺序重新组合。

这样,每次调用只需进行四次表查找和位操作——批量处理时比 32 次迭代的循环快得多。缓存只需使用 O(256 × 8) 时间构建一次,之后的所有调用都可以用 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}')

快速检查

检查您对本课数据结构与算法——编程面试准备相关概念的理解。

课程回顾

本课介绍了:位计数使用 DP,通过 dp[i] = dp[i >> 1] + (i & 1) 或 dp[i] = dp[i & (i-1)] + 1,在 O(n) 时间内完成,缺失数字可以通过将所有索引与所有值进行 XOR,或使用算术求和公式,在 O(n)/O(1) 时间和空间复杂度下解决,以及32 位反转可以通过 O(32) 的迭代方法,或使用分治掩码技术完成。接下来我们将学习单调栈,从递增与递减不变量以及下一个更大元素查询开始。

常见问题解答

「位计数、缺失数字与位反转」课时是免费的吗?

是的 — 「位计数、缺失数字与位反转」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。

「位计数、缺失数字与位反转」这节课中我会学到什么?

使用 DP 和最低有效置位技巧计算 0..n 的位计数,通过 XOR 找出缺失数字,并反转 32 位整数的所有位。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DSA Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「位计数、缺失数字与位反转」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DSA Interview Prep 课中编写并运行代码吗?

能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 位运算符:AND、OR、XOR、NOT 与移位
  2. Single Number 与 XOR 性质
  3. 位掩码:设置、清除、翻转与检查
  4. 位计数、缺失数字与位反转
← 返回 DSA Interview Prep