Search…
DSA Foundations · Part 3

Bits, bytes, and bitwise tricks

In this series (8 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

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