Search…
DSA Foundations · Part 3

Bits, bytes, and bitwise tricks

In this series (15 parts)
  1. How computers store data
  2. Big-O and algorithm analysis
  3. Bits, bytes, and bitwise tricks
  4. Arrays: fixed-size and indexing
  5. Dynamic arrays: how lists grow
  6. 2D arrays and matrices
  7. Strings and character encoding
  8. String manipulation patterns
  9. Two-pointer technique
  10. Sliding window technique
  11. Prefix sums and difference arrays
  12. Fast and slow pointers
  13. Singly linked lists
  14. Doubly & circular linked lists
  15. Stacks (LIFO)

You already know from how computers store data that everything in memory is binary. This post goes one level deeper: the actual operations CPUs perform directly on those bits, and the tricks built on top of them. Bitwise operations are O(1) — the fastest thing a computer can do — which is why they show up constantly in performance-critical code, flags/permissions systems, hashing, and interview problems.


Number systems: binary, decimal, hex

Decimal Binary Hex
0 0000 0x0
5 0101 0x5
10 1010 0xA
15 1111 0xF
16 10000 0x10
255 11111111 0xFF

Hex is popular because each hex digit maps exactly to 4 bits — a byte is always exactly 2 hex digits (0x00–0xFF).

Converting decimal to binary: repeatedly divide by 2 and read the remainders bottom-up. Converting binary to decimal: sum bit × 2^position for every set bit, counting positions from the right starting at 0.

13 in binary:
13 / 2 = 6 remainder 1
 6 / 2 = 3 remainder 0
 3 / 2 = 1 remainder 1
 1 / 2 = 0 remainder 1
Read remainders bottom-up: 1101

Check: 1×8 + 1×4 + 0×2 + 1×1 = 8+4+0+1 = 13 ✓

The bitwise operators

Each operator works bit by bit, independently, on corresponding positions of two numbers.

A B A AND B A OR B A XOR B
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0

AND is true only when both bits are 1. OR is true when at least one is 1. XOR is true when exactly one is 1 (they differ).

Try it yourself — flip bits in A and B below and switch operations to see the result update live:

Click any bit to flip it, then choose an operation to see the result update live.
  • AND (&) — masking: force certain bits to 0 while keeping others. x & 1 isolates the last bit (checks even/odd).
  • OR (|) — setting bits: force certain bits to 1 without disturbing others.
  • XOR (^) — toggling: flip specific bits; also the basis of “find the odd one out” and swapping without a temp variable.
  • NOT (~) — flips every bit (in two’s complement, ~x equals -x - 1).
  • Left shift (<<) — shifts bits left, filling with 0s on the right; equivalent to multiplying by 2^k.
  • Right shift (>>) — shifts bits right; equivalent to dividing by 2^k (integer division, rounding toward negative infinity for signed numbers in most languages).
x = 0b1010    # 10
y = 0b0110    # 6

print(bin(x & y))   # 0b0010 → 2   (AND)
print(bin(x | y))   # 0b1110 → 14  (OR)
print(bin(x ^ y))   # 0b1100 → 12  (XOR)
print(bin(x << 1))  # 0b10100 → 20 (x * 2)
print(bin(x >> 1))  # 0b0101 → 5   (x / 2)

Negative numbers: two’s complement

Computers don’t store a “minus sign” — negative integers use two’s complement: to negate a number, flip all bits and add 1. The leftmost bit acts as a sign indicator (1 = negative) but is not simply “worth −1”; the whole pattern is reinterpreted.

Click any bit to flip it, then choose an operation to see the result update live.
# 8-bit two's complement
 5 = 00000101
-5 = 11111011   # flip all bits of 5 (11111010), then add 1

# Why this works: 00000101 + 11111011 = 100000000
# The 9th bit overflows off an 8-bit register, leaving 00000000 = 0
# So 11111011 behaves exactly like -5 under addition.

Why two’s complement and not “sign + magnitude”? Because addition/subtraction circuitry doesn’t need special-casing for signs — 5 + (-5) is just binary addition that happens to overflow to zero. It also gives exactly one representation of 0 (sign+magnitude has both +0 and -0).

Bit width Unsigned range Signed (two's complement) range
8-bit 0 to 255 -128 to 127
16-bit 0 to 65,535 -32,768 to 32,767
32-bit 0 to 4,294,967,295 -2,147,483,648 to 2,147,483,647

Classic bit tricks

These come up constantly — memorize the pattern, not just the code:

n = 42

# Check if bit i is set
def is_set(n, i):
    return (n >> i) & 1 == 1

# Set bit i to 1
def set_bit(n, i):
    return n | (1 << i)

# Clear bit i (force to 0)
def clear_bit(n, i):
    return n & ~(1 << i)

# Toggle bit i
def toggle_bit(n, i):
    return n ^ (1 << i)

# Check if n is a power of two (exactly one bit set)
def is_power_of_two(n):
    return n > 0 and (n & (n - 1)) == 0
    # n-1 flips the lowest set bit and everything below it to 1;
    # ANDing with n clears that lowest bit. If n had only one bit
    # set, the result is 0.

# Count set bits (population count / Hamming weight)
def count_set_bits(n):
    count = 0
    while n:
        n &= n - 1   # clears the lowest set bit each iteration
        count += 1
    return count       # runs once per SET bit, not once per bit — faster than checking all 32/64 bits

# Get the lowest set bit (useful in Fenwick trees, later in this series)
def lowest_set_bit(n):
    return n & (-n)   # -n is two's complement negation

# Swap two variables without a temp (XOR swap — mostly a curiosity today,
# real code should just use a temp variable or tuple unpacking, but the
# trick shows how XOR is its own inverse)
def xor_swap(a, b):
    a ^= b
    b ^= a   # b = original a
    a ^= b   # a = original b
    return a, b

Why n & (n - 1) clears the lowest set bit: subtracting 1 flips the lowest set bit to 0 and every bit below it (which were 0) to 1. ANDing with the original n keeps everything above the lowest set bit unchanged, zeroes the lowest set bit itself (since n had 1 there but n-1 has 0), and zeroes everything below (since n had 0s there).

n     = 0b10110100
n - 1 = 0b10110011   ← lowest set bit (the 4s place) flipped to 0, bits below flipped to 1
n & (n-1) = 0b10110000   ← lowest set bit cleared

Where bit manipulation shows up later in this series

# Fenwick tree (Binary Indexed Tree) — uses lowbit = i & (-i)
# Union-Find — sometimes packs rank + parent into one int
# Bitmask DP — represents "which items are chosen" as a bitmask
# Hash functions — rely on XOR and shifts for good bit distribution
subset_mask = 0b0000
subset_mask |= (1 << 3)   # include item 3 in the subset
// Flags/permissions systems (Unix file permissions, feature flags)
#define READ    (1 << 0)   // 0b001
#define WRITE   (1 << 1)   // 0b010
#define EXECUTE (1 << 2)   // 0b100

int perms = READ | WRITE;          // grant read+write
int can_write = perms & WRITE;     // check write permission

Key takeaways

  1. Bitwise ops are O(1) — they operate on a fixed number of bits regardless of the “value” of the number.
  2. AND masks, OR sets, XOR toggles, shifts multiply/divide by powers of two.
  3. Two’s complement lets negative numbers use ordinary binary addition circuitry — negate by flipping all bits and adding 1.
  4. n & (n-1) clears the lowest set bit — the basis of power-of-two checks and efficient set-bit counting.
  5. n & (-n) isolates the lowest set bit — this exact trick powers Fenwick trees later in this series.
  6. These tricks aren’t just interview trivia — they underlie hashing, flag/permission systems, and several data structures coming up later (Fenwick trees, bitmask DP, Bloom filters).

Practice problems


What’s next

  • Arrays: fixed-size and indexing — putting the memory-access formula into practice
  • Dynamic arrays — how list.append() uses the amortized doubling from the previous post (coming soon)
Start typing to search across all content
navigate Enter open Esc close