Part VI · More Patterns Pattern 19 4 problems

Bit Manipulation

An integer is a row of switches. Three tricks cover most interview questions: XOR cancels pairs, x & (x - 1) turns off the lowest switch, and a mask is a set you can test in one step.

Bit questions look like puzzles, but they reward a short list of facts rather than cleverness. Learn the operators, learn the three tricks, and learn the one Python-specific trap: Python integers have no fixed width, so negative numbers need a 32-bit mask before they behave like the numbers in a C or Java answer.

Contents

  1. When to use
  2. Core idea
  3. Operator cheat list
  4. Negatives and the 32-bit mask
  5. The templates
  6. Common mistakes
  7. Single Number
  8. Number of 1 Bits
  9. Counting Bits
  10. Sum of Two Integers
  11. Recap

When to use

The trigger. The question mentions bits, binary, powers of two, or XOR, or it bans the obvious tool: “without extra space”, “without +”, “in O(1) memory”. A small n (20 or less) with “every subset” is the other tell: each subset fits in one integer.

Core idea

Three facts do most of the work.
XOR cancels pairs 5 1 0 1 3 0 1 1 5 1 0 1 = 3 0 1 1 the two 5s cancel to 0 x & (x - 1) drops the lowest 1 x 1 0 1 1 0 x - 1 1 0 1 0 1 x & (x-1) 1 0 1 0 0 amber: bits that - 1 flipped red: the lowest 1, now cleared a mask is a set d c b a 0 1 0 1 mask = 0b0101 = {a, c} union |, intersection & test: (mask >> i) & 1
Figure 19.1 — Pairs cancel under XOR, x & (x - 1) clears one bit, and a mask is a set of positions.

Reading the figure. Each square is one bit, highest bit on the left. Left: the red rows are a pair, and they cancel, so only the blue 3 survives. Middle: amber bits are the ones that subtracting 1 flipped. AND keeps neither, so the red bit is cleared. Right: bit i on means item i is in the set.

Two quick consequences are worth having ready. x & (x - 1) == 0 for a positive x means x is a power of two, because it had exactly one set bit. And x & -x isolates the lowest set bit, which is the move behind Fenwick trees.

Operator cheat list

Precedence. Shifts bind tighter than &, which binds tighter than ^, which binds tighter than |. Comparisons bind looser than all of them. So x & 1 == 0 works in Python, unlike in C. Add the brackets anyway. The reader should not have to know.

Negatives and the 32-bit mask

In C or Java an int is 32 bits, and -1 is thirty-two 1s. In Python an integer has no width. -1 behaves as if it had infinitely many 1s to the left. Any loop that shifts a negative number right until it reaches zero never ends, because -1 >> 1 is still -1.

The fix is & 0xFFFFFFFF. 0xFFFFFFFF is thirty-two 1 bits. AND-ing with it keeps the low 32 bits and drops the infinite sign bits, so -1 & 0xFFFFFFFF is 4294967295, the same bit pattern Java would hold. Now the value is a finite, non-negative number and loops end. To turn a 32-bit pattern back into a signed Python int, check bit 31. If it is set, the number was negative: ~(x ^ 0xFFFFFFFF) restores it.
MASK = 0xFFFFFFFF          # 32 one-bits: keeps only the low 32 bits
MAX_INT = 0x7FFFFFFF       # 31 one-bits: the largest positive signed 32-bit value

pattern = -1 & MASK        # 4294967295: thirty-two 1s, as Java stores -1
signed = pattern if pattern <= MAX_INT else ~(pattern ^ MASK)   # back to -1

The templates

Template A — XOR to cancel pairs
def xor_all(values: list[int]) -> int:
    """XOR of every value. Pairs cancel, so odd-count values survive.

    Example:
        >>> xor_all([3, 5, 3])
        5
    """
    # 0 is the XOR identity: 0 ^ x == x, so starting here changes nothing.
    acc = 0

    # Invariant: acc is the XOR of every value before this pass.
    for value in values:
        acc ^= value              # a second copy of value cancels the first

    return acc
Template B — walk the set bits with x & (x - 1)
def set_bit_positions(x: int) -> list[int]:
    """Positions of the 1 bits in a non-negative x, lowest first.

    Example:
        >>> set_bit_positions(0b10110)
        [1, 2, 4]
    """
    positions: list[int] = []

    # Invariant: x holds the set bits not yet reported. Each pass removes one.
    # The loop stops at 0: no set bits left.
    while x != 0:
        lowest = x & -x                       # -x flips all bits above the lowest 1
        # bit_length() - 1: a number with one set bit at position p has length p + 1
        positions.append(lowest.bit_length() - 1)
        x &= x - 1                            # - 1 turns the lowest 1 into 0
    return positions
x lowest = x & -x positions 4 3 2 1 0 pass 1 1 0 1 1 0 0 0 0 1 0 bit 1: [1] pass 2 1 0 1 0 0 0 0 1 0 0 bit 2: [1, 2] pass 3 1 0 0 0 0 1 0 0 0 0 bit 4: [1, 2, 4] x &= x - 1 removes the amber bit. After pass 3, x = 0 and the loop stops.
Figure 19.2 — The loop runs once per set bit: three passes for the three 1s in 10110.

Reading the figure. Each row is one pass of the while loop. The small numbers on top are bit positions. The amber bit is the lowest 1, which x & -x isolates. Its position joins the green list. Then x &= x - 1 clears it, giving the next row.

The loop runs once per set bit, not once per bit position. On a sparse number that is much faster than checking all 32 positions.

Template C — masks as sets: visit every subset
def subsets_by_mask(items: list[str]) -> list[list[str]]:
    """Every subset of items. Bit i of mask means items[i] is in.

    Example:
        >>> subsets_by_mask(["a", "b"])
        [[], ['a'], ['b'], ['a', 'b']]
    """
    n = len(items)
    result: list[list[str]] = []

    # 1 << n is 2 ** n: one mask per subset, from 0 (empty) to 2 ** n - 1 (all).
    for mask in range(1 << n):
        # (mask >> i) & 1 reads bit i. The 1 keeps only that one bit.
        result.append([items[i] for i in range(n) if (mask >> i) & 1])

    return result
items = [a, b, c]. Bit 0 is a, bit 1 is b, bit 2 is c. mask = 0 c b a 0 0 0 {} mask = 1 c b a 0 0 1 {a} mask = 2 c b a 0 1 0 {b} mask = 3 c b a 0 1 1 {a, b} mask = 4 c b a 1 0 0 {c} mask = 5 c b a 1 0 1 {a, c} mask = 6 c b a 1 1 0 {b, c} mask = 7 c b a 1 1 1 {a, b, c}
Figure 19.3 — Counting from 0 to 2**n - 1 visits every subset exactly once.

Reading the figure. Each card is one value of mask. The letters above the bits say which item each bit stands for. Green bits are on, so those items are in the subset on the right. Notice that the masks are just the numbers 0 to 7 in order, and together they cover all 8 subsets.

This is the iterative twin of backtracking subsets. It only works while 2 ** n is small, so about n ≤ 20.

Common mistakes

The problems

1. Single Number Easy

Problem

Every value in a non-empty array appears exactly twice, except one value that appears once. Find it in O(n) time and O(1) extra space.

The idea

A hash set works but costs O(n) space. XOR is the O(1) answer. XOR is commutative and associative, so the order does not matter. Each pair meets itself and becomes 0. The single value XOR 0 is itself.

Solution

def single_number(nums: list[int]) -> int:
    """The one value that appears once when every other appears twice.

    Args:
        nums: Non-empty integers. Exactly one value appears once.

    Returns:
        The value that appears once.

    Example:
        >>> single_number([4, 1, 2, 1, 2])
        4
        >>> single_number([-3, 7, 7])
        -3
    """
    # Start at 0: XOR with 0 changes nothing, so 0 is the neutral start.
    result = 0

    # Invariant: result is the XOR of every number seen so far. Pairs seen
    # twice have cancelled to 0, so result holds only the unpaired ones.
    for value in nums:
        result ^= value

    return result

Walkthrough

nums = [4, 1, 2, 1, 2], in binary:

step 4 2 1 result after the step start 0 0 0 ^ 4 1 0 0 4 goes in ^ 1 1 0 1 1 goes in ^ 2 1 1 1 2 goes in ^ 1 1 1 0 second 1 cancels the first ^ 2 1 0 0 second 2 cancels: answer 4
Figure 19.4 — Each repeated value switches its bit on and then off, so only 4 is left.

Reading the figure. Each row is result after one XOR, with place values 4, 2, 1 on top. Blue squares are 1 bits. A red square is a bit that a second copy just switched back to 0. The green row is the answer.

TimeO(n)SpaceO(1)

Edge cases to raise

Say this out loud: “XOR is order-free and every pair cancels to zero, so XOR-ing the whole array leaves only the single value. One pass, one integer of space.”

2. Number of 1 Bits Easy

Problem

Return the number of 1 bits in the 32-bit binary form of an integer. This count is also called the Hamming weight.

The idea

Checking all 32 positions works. The better answer uses n & (n - 1), which clears one set bit per pass. The loop runs exactly as many times as there are 1s. Mask the input with 0xFFFFFFFF first, so a negative input counts its 32-bit pattern and the loop still ends.

Solution

def hamming_weight(n: int) -> int:
    """Number of 1 bits in the 32-bit two's complement form of n.

    Args:
        n: Any integer. Negatives are read as their 32-bit pattern.

    Returns:
        The count of set bits, from 0 to 32.

    Example:
        >>> hamming_weight(11)
        3
        >>> hamming_weight(-1)
        32
    """
    # 0xFFFFFFFF is thirty-two 1s. AND keeps the low 32 bits and drops
    # Python's infinite sign bits, so -1 becomes 32 ones and the loop ends.
    n &= 0xFFFFFFFF
    count = 0                     # no set bits counted yet

    # Invariant: count + (set bits left in n) == the answer.
    # The loop stops at 0: no set bits left.
    while n != 0:
        n &= n - 1                # - 1 flips the lowest 1 to 0, AND clears it
        count += 1                # one bit removed, so one more counted

    return count

Walkthrough

n = 11, binary 1011:

pass 1: count 1 n 1 0 1 1 n - 1 1 0 1 0 n & (n-1) 1 0 1 0 pass 2: count 2 n 1 0 1 0 n - 1 1 0 0 1 n & (n-1) 1 0 0 0 pass 3: count 3 n 1 0 0 0 n - 1 0 1 1 1 n & (n-1) 0 0 0 0 Each pass clears the amber bit, shown red after the AND. n = 0 after pass 3, so the loop stops. 1011 has three 1s.
Figure 19.5 — n & (n - 1) clears exactly one 1 per pass, so the pass count is the answer.

Reading the figure. Each block is one pass. The amber bit is the lowest 1 of n. Subtracting 1 flips it and every 0 below it. After the AND, that bit is the red square, and the green row is the new n. Three passes means three 1 bits.

TimeO(set bits), at most 32SpaceO(1)

Edge cases to raise

Say this out loud: “n & (n - 1) clears the lowest set bit, so the loop runs once per 1 bit. I mask with 0xFFFFFFFF first, because Python ints have no width and a negative would never reach zero.”

3. Counting Bits Easy

Problem

Given n, return a list ans of length n + 1 where ans[i] is the number of 1 bits in i. Aim for O(n), not O(n log n).

The idea

This is dynamic programming on bits. Shifting i right by one drops its last bit and gives i >> 1, a smaller number whose count is already known. The dropped bit is i & 1. So ans[i] = ans[i >> 1] + (i & 1). Each cell costs O(1).

Solution

def count_bits(n: int) -> list[int]:
    """Set-bit counts for every integer from 0 to n.

    Args:
        n: The largest integer to count, n >= 0.

    Returns:
        A list of length n + 1. Entry i is the number of 1 bits in i.

    Example:
        >>> count_bits(5)
        [0, 1, 1, 2, 1, 2]
    """
    # n + 1 slots so index n exists: slots 0..n. Slot 0 stays 0, the base
    # case, because 0 has no set bits.
    ones = [0] * (n + 1)

    # Start at 1: slot 0 is the base case. n + 1 so n itself is included.
    # Invariant: ones[0..i-1] are final. i >> 1 < i, so it is ready.
    for i in range(1, n + 1):
        # i >> 1 drops the last bit (shift by 1 = halve). i & 1 is that
        # dropped bit: 1 if i is odd, 0 if even.
        ones[i] = ones[i >> 1] + (i & 1)

    return ones

Walkthrough

i binary ones[i] 0 0 0 1 1 1 2 10 1 3 11 2 4 100 1 5 101 2 ones[i] = ones[i >> 1] + (i & 1). Amber: the last bit, i & 1.
Figure 19.6 — Each count reuses the count of i >> 1 and adds the one bit that the shift dropped.

Reading the figure. Each column is one i. Blue arrows point from i back to i >> 1, a smaller cell that is already filled. The amber digit is the last bit, the one the shift drops. So each green count is the count at the arrow tip plus the amber digit.

TimeO(n)SpaceO(n) for the output

Edge cases to raise

Say this out loud: “Shifting right by one gives a smaller number I have already solved, and the bit I dropped is i & 1. So each entry is one lookup plus one bit. That makes it a DP over the integers.”

4. Sum of Two Integers Medium

Problem

Return a + b without using the + or - operators. Inputs are 32-bit signed integers, and negatives must work.

The idea

Binary addition splits into two parts. a ^ b is the sum of each column ignoring carries. (a & b) << 1 is the carries, each moved one column left. Add those two the same way, and repeat until no carry is left. In C this ends in at most 32 rounds, because a carry falls off the top. In Python it does not fall off, so a negative input loops forever. The fix is to mask every step with 0xFFFFFFFF to fake a 32-bit register, then turn the result back into a signed int.

Solution

def get_sum(a: int, b: int) -> int:
    """a + b using only bit operations, with 32-bit signed wraparound.

    Args:
        a: A 32-bit signed integer.
        b: A 32-bit signed integer.

    Returns:
        Their sum as a signed integer.

    Example:
        >>> get_sum(1, 2)
        3
        >>> get_sum(-2, 3)
        1
        >>> get_sum(-5, -7)
        -12
    """
    mask = 0xFFFFFFFF             # 32 one-bits: keeps only the low 32 bits
    max_int = 0x7FFFFFFF          # 31 one-bits: largest positive signed 32-bit

    # Drop Python's infinite sign bits. Negatives become 32-bit patterns.
    a &= mask
    b &= mask

    # Invariant: a + b (as 32-bit numbers) equals the true answer.
    # b is the carry. It moves 1 column left each pass, so after at most
    # 32 passes the mask pushes it out and it becomes 0.
    while b != 0:
        # Columns where both are 1 make a carry. << 1 moves it to the next
        # column. & mask drops a carry out of bit 31, like real hardware.
        carry = ((a & b) << 1) & mask
        a = (a ^ b) & mask        # column sums with no carry
        b = carry                 # the carries still to add

    # Bit 31 clear: a is already the right non-negative value.
    if a <= max_int:
        return a

    # Bit 31 set: a is a negative number's pattern. a ^ mask flips the low
    # 32 bits. ~ then flips every bit, so the low 32 come back and the
    # infinite upper bits become 1s, which is how Python stores a negative.
    return ~(a ^ mask)

Walkthrough

a = 5 (0101), b = 3 (0011):

a = a ^ b (sum, no carry) b = (a & b) << 1 (carry) start 0 1 0 1 0 0 1 1 5 + 3 pass 1 0 1 1 0 0 0 1 0 carry moves left pass 2 0 1 0 0 0 1 0 0 pass 3 0 0 0 0 1 0 0 0 pass 4 1 0 0 0 0 0 0 0 b = 0: answer 8
Figure 19.7 — The carry walks one column left each pass until it is 0, and a holds the sum.

Reading the figure. Each row shows a and b after one pass. Blue bits are the sum so far, without carries. Amber bits are the carries, already moved one column left. Watch the single amber 1 climb from column 1 to column 3. When b is all zeros, the green a is the answer.

TimeO(32) = O(1)SpaceO(1)

Edge cases to raise

Say this out loud: “XOR adds each column without carries, and AND shifted left gives the carries. I repeat until the carry is zero. Python ints never overflow, so I mask to 32 bits each step and convert back with ~(a ^ mask) if bit 31 is set.”

Recap

The six things to carry forward

Where this goes next

Pattern 20, K-way Merge and Two Heaps, goes back to heaps. It covers the two heap moves the top-K page only names: merging many sorted lists, and keeping a running median.


← 18 — Trie 20 — K-way Merge and Two Heaps →