← All Posts
DSA · Bit Manipulation· Part 4 of 32

Bitwise Operators

Bitwise operators work on individual bits of their operands. They are the fundamental building blocks of every bit manipulation technique. Understanding them deeply is essential before moving to the tricks and patterns in later posts.

Bitwise AND (&)

The AND operator compares each bit of two numbers. The result bit is 1 only if both input bits are 1.

ABA & B
000
010
100
111
  0b11001010   (202)
& 0b10101100   (172)
-----------
  0b10001000   (136)

// In C++:
int result = 202 & 172;  // 136

Mental model: AND is a filter. It keeps only the bits that are set in both operands. If you AND with a mask, you extract only the bits where the mask has 1s. Everything else becomes 0.

Key uses of AND: Checking if a bit is set (x & (1 << i)), clearing bits (x & ~mask), extracting bit fields ((x >> offset) & mask), testing even/odd (x & 1).

▶ Bitwise AND Operation

Watch each bit pair get ANDed together.

Bitwise OR (|)

The OR operator produces a 1 if either or both input bits are 1.

ABA | B
000
011
101
111
  0b11001010   (202)
| 0b10101100   (172)
-----------
  0b11101110   (238)

Mental model: OR is a combiner. It merges set bits from both operands. Useful for setting specific bits: flags |= NEW_FLAG.

Bitwise XOR (^)

XOR (exclusive OR) produces a 1 if the input bits are different.

ABA ^ B
000
011
101
110
  0b11001010   (202)
^ 0b10101100   (172)
-----------
  0b01100110   (102)

Mental model: XOR is a difference detector. It highlights where two values differ. XOR has remarkable properties that make it one of the most useful operators:

XOR is the star of bit manipulation. It is used for toggling bits, finding unique elements, swapping without a temporary, generating Gray codes, and countless interview problems. We dedicate an entire post to XOR patterns.

Bitwise NOT (~)

NOT is a unary operator that flips every bit: 0 becomes 1, 1 becomes 0.

uint8_t x = 0b11001010;   // 202
uint8_t y = ~x;           // 0b00110101 = 53

// For signed 32-bit: ~0 = -1 (all bits set = 0xFFFFFFFF)

Key identity: In two's complement, ~x = -(x+1). Equivalently, -x = ~x + 1.

Left Shift (<<)

Shifts all bits left by the specified number of positions. New bits on the right are filled with 0. Each left shift by 1 multiplies by 2.

uint8_t x = 0b00001010;   // 10
uint8_t y = x << 2;      // 0b00101000 = 40  (10 × 4)

// General: x << n  =  x × 2^n  (if no overflow)
Shift amount must be less than the bit width. x << 32 for a 32-bit integer is undefined behavior in C++.

Right Shift (>>)

Shifts all bits right. Each right shift by 1 divides by 2 (rounding toward zero for unsigned, implementation-defined for signed).

uint8_t x = 0b00101000;   // 40
uint8_t y = x >> 2;      // 0b00001010 = 10  (40 / 4)

There are two types of right shift:

// Arithmetic right shift (signed):
int8_t x = -8;            // 0b11111000
int8_t y = x >> 2;       // 0b11111110 = -2  (sign bit replicated)

// Logical right shift (unsigned):
uint8_t u = 0b11111000;   // 248
uint8_t v = u >> 2;       // 0b00111110 = 62  (zeros filled in)

Operator Precedence Trap

Bitwise operators have lower precedence than comparison operators in C++. This is a notorious source of bugs:

// BUG: this is parsed as x & (1 == 1), not (x & 1) == 1
if (x & 1 == 1) { ... }   // WRONG!

// FIX: always use parentheses with bitwise operators
if ((x & 1) == 1) { ... }  // CORRECT

// Precedence order (high to low):
// ~        (NOT, unary)
// << >>   (shifts)
// &        (AND)
// ^        (XOR)
// |        (OR)
Golden rule: Always parenthesize bitwise expressions. The precedence rules are counterintuitive and a constant source of bugs.

Compound Assignment Operators

x &= mask;    // x = x & mask
x |= mask;    // x = x | mask
x ^= mask;    // x = x ^ mask
x <<= n;      // x = x << n
x >>= n;      // x = x >> n

Key Identities

// AND identities:
x & 0         = 0          // AND with zero clears all bits
x & x         = x          // AND with self is identity
x & ~0        = x          // AND with all-ones is identity
x & (x - 1)   clears the lowest set bit

// OR identities:
x | 0         = x          // OR with zero is identity
x | x         = x          // OR with self is identity
x | ~0        = ~0         // OR with all-ones sets all bits

// XOR identities:
x ^ 0         = x          // XOR with zero is identity
x ^ x         = 0          // XOR with self cancels
x ^ ~0        = ~x         // XOR with all-ones flips all bits

// De Morgan's Laws (bitwise):
~(a & b)      = ~a | ~b
~(a | b)      = ~a & ~b

Summary

GCC/Clang Built-in Intrinsics (CP Essential)

Competitive programmers lean heavily on compiler intrinsics. They compile down to single CPU instructions (POPCNT, BSF/TZCNT, BSR/LZCNT) and are dramatically faster than a hand-written loop.

// All of these take an unsigned int. Behavior is UB if x == 0
// for __builtin_clz / __builtin_ctz, so guard against it.

__builtin_popcount(x)   // # of set bits  (e.g., popcount(0b1011) = 3)
__builtin_ctz(x)        // # of trailing zeros (index of lowest set bit)
__builtin_clz(x)        // # of leading zeros  (31 - floor(log2(x)) for 32-bit)
__builtin_parity(x)     // popcount(x) & 1  (1 if odd # of set bits)

// 64-bit versions: append 'll' (long long)
__builtin_popcountll(x)
__builtin_ctzll(x)
__builtin_clzll(x)
__builtin_parityll(x)

// Common idioms:
int highest_bit  = 31 - __builtin_clz(x);     // floor(log2(x))
int lowest_bit   = __builtin_ctz(x);          // position of lsb set
bool is_pow2     = x && !(x & (x - 1));       // (also __builtin_popcount(x)==1)
C++20 portable alternatives live in <bit>: std::popcount(x), std::countr_zero(x), std::countl_zero(x), std::has_single_bit(x), std::bit_width(x), std::bit_ceil(x), std::bit_floor(x). Unlike the GCC intrinsics, these are well-defined for x == 0 and work on any unsigned integral type.
#include <bit>
std::popcount(0u)         // 0  (defined, unlike __builtin_clz(0))
std::countr_zero(0b1000u) // 3
std::bit_width(5u)        // 3  (smallest k with 2^k > x, == floor(log2)+1)

Practice Problems

Work through these in order — they exercise everything on this page (truth tables, masks, XOR identities, popcount, parity).