GUIDE · Patterns
Bit Manipulation
operate on the binary representation directly; O(1) tricks and compact state.
At a glance
- WHAT
- operate on the binary representation directly; O(1) tricks and compact state.
- TELL
- "without using + / *", "appears once / twice / three times", "number of 1 bits", "power of two", "subsets of a small set", "XOR", "O(1) space" where counting would be natural.
- SAY
- bitmask, set / clear / toggle / test a bit, lowest set bit, popcount (Hamming weight), Brian Kernighan's algorithm, XOR cancellation, two's complement, arithmetic vs logical shift.
- BIG-O
- O(1) per op on 32-bit ints; O(number of set bits) for Kernighan; O(2^n * n) for subset enumeration.
- NOTE
- JS bitwise ops coerce to 32-BIT SIGNED ints. 1 << 31 === -2147483648; 2**31 | 0 is negative. For > 2^31 use BigInt (1n << 40n) or Math (Math.floor(x / 2**k)). >>> 0 reinterprets as unsigned 32.
The idea#
Picture a row of light switches, thirty-two of them, each on or off. That row is an integer. Bitwise operators in JavaScript work on 32-bit signed integers, so switch 31 is the sign, and 1 << 31 comes back negative. A wider row needs BigInt, or plain arithmetic with Math.floor.
Four moves cover the row. Test switch i by shifting it down to the end and masking with 1. Set it by OR-ing a 1 into that position. Clear it by AND-ing with the inverse of that 1. Toggle it by XOR-ing that 1. XOR is the useful one to feel in your hands: flip the same switch twice and it is back where it started. Two copies of a number cancel. One copy remains. x ^ x is 0, and x ^ 0 is x.
n & (n - 1) turns off the lowest switch that is on. Loop that and you count the on switches, one per set bit. That is Brian Kernighan's trick. n & -n isolates that same lowest switch. A power of two is a row with exactly one switch on: n > 0 and n & (n - 1) is 0.
Why it matters#
You reach for the switches when the problem says "without using plus or times," a value that appears once while everything else appears twice or three times, the number of 1 bits, a power of two, the subsets of a small set, or an XOR. You also reach for them when a visited-set would be natural and n is small enough to pack into the bits of one integer, about twenty or fewer.
Each operation on a 32-bit int is O(1). Kernighan's loop is proportional to the number of set bits. Enumerating subsets by walking every mask is O(2^n · n).
A small example#
function singleNumber(nums) { let acc = 0; for (const x of nums) acc ^= x; return acc;} const lone = singleNumber([4, 1, 2, 1, 2]);1 cancels with 1, 2 cancels with 2, and 4 has nothing to cancel with. lone is 4. The whole row of numbers collapses in one pass and constant extra space.
What the code below is doing#
The sheet opens with the operators and the idioms: AND, OR, XOR, NOT (which is -a - 1), left shift, arithmetic right shift that keeps the sign, and logical right shift >>> that fills with zeros. Then test, set, clear, toggle, clear-lowest, isolate-lowest, and the power-of-two test. The 32-bit trap is asserted: 1 << 31 is negative, >>> 0 reinterprets it as unsigned, and 1n << 40n is how you leave the 32-bit row.
Single number XORs the whole array. Pairs cancel, the loner remains, and negatives are fine. Single number II, where every other value appears three times, counts set bits per position modulo 3 and rebuilds the answer, using | 0 so the sign bit behaves. That is O(32n). Number of 1 bits is the Kernighan loop, n &= n - 1 once per set bit. Counting bits from 0 through n is a small dynamic-programming fill: bits[i] = bits[i >> 1] + (i & 1), or one plus the count after clearing the lowest set bit. Reverse bits shifts a result left, ORs in the lowest bit of n, and logically shifts n right, thirty-two times.
Missing number XORs every index from 0 through n with every value. The matched pairs cancel and the hole remains. Sum of two integers without + uses XOR for the sum-without-carry and (a & b) << 1 for the carry, repeating until the carry is 0, which finishes within 32 rounds. Subsets via bitmask walk every mask from 0 to 2^n - 1 and include nums[i] when bit i is set. The visited-set section packs a path into one integer: visited | (1 << i) marks i, and the mask equals (1 << n) - 1 when every bit is on. The last note is the guard for values past 2^31: bitwise ops truncate, so you switch to Math.floor or BigInt.
When you talk about it, say this: a row of switches, XOR cancels pairs, and remember the row is 32 bits wide unless you asked for bigger.
Everything below is the demonstration. Same idea, written as code you can run.
'use strict';const assert = require('node:assert/strict');Operators & idioms#
// a & b AND a | b OR a ^ b XOR ~a NOT (= -a - 1)// a << k shift left (x 2^k) a >> k arithmetic shift right (keeps sign) a >>> k logical shift right (zero-fill)// test bit i : (n >> i) & 1 set bit i : n | (1 << i)// clear bit i : n & ~(1 << i) toggle bit i: n ^ (1 << i)// n & (n - 1) : clears the LOWEST set bit n & -n : isolates the LOWEST set bit// power of two : n > 0 && (n & (n - 1)) === 0 x ^ x = 0, x ^ 0 = x => XOR cancels pairs// swap : a ^= b; b ^= a; a ^= b (don't; use destructuring) parity: popcount & 1const getBit = (n, i) => (n >> i) & 1;const setBit = (n, i) => n | (1 << i);const clearBit = (n, i) => n & ~(1 << i);const toggleBit = (n, i) => n ^ (1 << i);const isPowerOfTwo = (n) => n > 0 && (n & (n - 1)) === 0;Verified by 16 asserts
assert.equal(getBit(0b1010, 1), 1);assert.equal(getBit(0b1010, 0), 0);assert.equal(setBit(0b1010, 0), 0b1011);assert.equal(clearBit(0b1010, 3), 0b0010);assert.equal(toggleBit(0b1010, 1), 0b1000);assert.equal(0b1100 & (0b1100 - 1), 0b1000); // clears lowest set bitassert.equal(0b1100 & -0b1100, 0b0100); // isolates lowest set bitassert.equal(isPowerOfTwo(64), true);assert.equal(isPowerOfTwo(0), false);assert.equal(isPowerOfTwo(96), false);assert.equal(~5, -6);assert.equal(-8 >> 1, -4); // arithmeticassert.equal(-8 >>> 28, 15); // logical: 0xFFFFFFF8 >>> 28assert.equal(1 << 31, -2147483648); // the 32-bit trapassert.equal((1 << 31) >>> 0, 2147483648); // unsigned viewassert.equal(1n << 40n, 1099511627776n); // BigInt for wide shiftsSingle number (every other appears twice)#
XOR everything; pairs cancel
const singleNumber = (nums) => nums.reduce((a, x) => a ^ x, 0);Verified by 3 asserts
assert.equal(singleNumber([4, 1, 2, 1, 2]), 4);assert.equal(singleNumber([1]), 1);assert.equal(singleNumber([-3, 7, 7]), -3);Single number II (others appear three times) sketch#
per bit, count set bits mod 3; rebuild answer. Handles negatives via | 0
function singleNumberII(nums) { let out = 0; for (let i = 0; i < 32; i++) { let cnt = 0; for (const x of nums) cnt += (x >> i) & 1; if (cnt % 3) out |= 1 << i; } return out | 0; // force signed 32-bit interpretation}Verified by 3 asserts
assert.equal(singleNumberII([2, 2, 3, 2]), 3);assert.equal(singleNumberII([0, 1, 0, 1, 0, 1, 99]), 99);assert.equal(singleNumberII([-2, -2, -2, 5]), 5);Number of 1 bits (Hamming weight), Brian Kernighan#
loop n &= n - 1 once per set bit
function hammingWeight(n) { let count = 0; while (n !== 0) { n &= n - 1; count++; } return count;}Verified by 3 asserts
assert.equal(hammingWeight(0b1011), 3);assert.equal(hammingWeight(0), 0);assert.equal(hammingWeight(-1), 32); // all 32 bits set in two's complementCounting bits 0..n (DP)#
bits[i] = bits[i >> 1] + (i & 1); or bits[i & (i-1)] + 1
function countBits(n) { const bits = new Array(n + 1).fill(0); for (let i = 1; i <= n; i++) bits[i] = bits[i >> 1] + (i & 1); return bits;}Verified by 2 asserts
assert.deepEqual(countBits(5), [0, 1, 1, 2, 1, 2]);assert.deepEqual(countBits(0), [0]);Reverse bits (32-bit unsigned)#
shift result left, OR in lowest bit of n, shift n right (logical)
function reverseBits(n) { let out = 0; for (let i = 0; i < 32; i++) { out = (out << 1) | (n & 1); n >>>= 1; } return out >>> 0; // unsigned result}Verified by 3 asserts
assert.equal(reverseBits(0b00000010100101000001111010011100), 964176192);assert.equal(reverseBits(0), 0);assert.equal(reverseBits(1), 2147483648);Missing number via XOR#
XOR all indices 0..n and all values; the unmatched one survives
function missingNumber(nums) { let x = nums.length; for (let i = 0; i < nums.length; i++) x ^= i ^ nums[i]; return x;}Verified by 3 asserts
assert.equal(missingNumber([3, 0, 1]), 2);assert.equal(missingNumber([0]), 1);assert.equal(missingNumber([9, 6, 4, 2, 3, 5, 7, 0, 1]), 8);Sum of two integers without +#
sum = a ^ b (no carry); carry = (a & b) << 1; repeat until carry 0
function getSum(a, b) { while (b !== 0) { const carry = (a & b) << 1; a ^= b; b = carry; } return a;}Verified by 3 asserts
assert.equal(getSum(1, 2), 3);assert.equal(getSum(-2, 3), 1);assert.equal(getSum(0, 0), 0);Subsets via bitmask enumeration#
mask 0..2^n-1; bit i set => include nums[i]
function subsets(nums) { const n = nums.length, out = []; for (let mask = 0; mask < 1 << n; mask++) { const sub = []; for (let i = 0; i < n; i++) if (mask & (1 << i)) sub.push(nums[i]); out.push(sub); } return out;}// Iterate submasks of a mask: for (let s = mask; s > 0; s = (s - 1) & mask) ... (O(3^n) over all masks)Verified by 2 asserts
assert.deepEqual(subsets([1, 2, 3]), [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]);assert.deepEqual(subsets([]), [[]]);Bitmask as visited-set for small n (n <= ~20)#
visited | (1 << i); full when visited === (1 << n) - 1
// Example: count Hamiltonian paths from node 0 in a tiny graph via DFS over (node, mask). Memo key = node * 2^n + mask.function countHamiltonianPaths(adj) { const n = adj.length, full = (1 << n) - 1, memo = new Map(); const dfs = (u, mask) => { if (mask === full) return 1; const key = u * (1 << n) + mask; if (memo.has(key)) return memo.get(key); let total = 0; for (const v of adj[u]) if (!(mask & (1 << v))) total += dfs(v, mask | (1 << v)); memo.set(key, total); return total; }; return dfs(0, 1); // start at node 0, visited = {0}}const triangle = [[1, 2], [0, 2], [0, 1]];const path3 = [[1], [0, 2], [1]];Verified by 3 asserts
assert.equal(countHamiltonianPaths(triangle), 2); // 0-1-2, 0-2-1assert.equal(countHamiltonianPaths(path3), 1); // 0-1-2 onlyassert.equal(countHamiltonianPaths([[]]), 1); // single node: already full> 2^31 guard#
bitwise truncates; use Math.floor / BigInt for big values
Verified by 4 asserts
assert.equal((2 ** 32 + 5) | 0, 5); // truncated silentlyassert.equal(Math.floor((2 ** 40 + 7) / 2 ** 40), 1); // "shift" with Math for big numbersassert.equal(BigInt(2 ** 40) >> 38n, 4n);assert.equal(Number.MAX_SAFE_INTEGER, 2 ** 53 - 1); // beyond this, integers lose precision anywaySee also