Binary representation
Every integer in a computer is stored as a fixed sequence of bits (0s and 1s).
Unsigned integers: value = sum of 2^i for each bit i that is set.
13 = 8 + 4 + 1
= 2³ + 2² + 2⁰
= 1101 in binary
Two’s complement (signed integers): The most significant bit (MSB) is the sign bit — 0 means non-negative, 1 means negative. To negate any number: flip all bits, then add 1.
5 in 8-bit: 00000101
-5 in 8-bit: 11111011 (flip: 11111010, add 1: 11111011)
Key consequences you must know cold:
-1is all 1-bits:11111111 11111111 11111111 11111111(32-bit)~n = -(n + 1)— flipping all bits ofngives-n - 1(two’s complement identity)- For 32-bit signed integers: range is
-2,147,483,648to2,147,483,647 - JavaScript bitwise operations convert all operands to 32-bit signed integers before operating
Bitwise operators
AND (&)
Both bits must be 1 for the result bit to be 1. Used to mask (extract) specific bits.
// Check if bit i is set
function isBitSet(n: number, i: number): boolean {
return (n & (1 << i)) !== 0
}
// Isolate the lower k bits
function lowerKBits(n: number, k: number): number {
return n & ((1 << k) - 1)
}
console.log(6 & 3) // 110 & 011 = 010 = 2
console.log(12 & 10) // 1100 & 1010 = 1000 = 8
OR (|)
At least one bit must be 1. Used to set specific bits.
function setBit(n: number, i: number): number {
return n | (1 << i)
}
console.log(6 | 3) // 110 | 011 = 111 = 7
console.log(12 | 10) // 1100 | 1010 = 1110 = 14
XOR (^)
Bits differ → 1. Bits same → 0. Key algebraic properties:
a ^ a = 0(self-inverse: XOR with itself cancels out)a ^ 0 = a(identity: XOR with 0 is a no-op)- Commutative and associative
function toggleBit(n: number, i: number): number {
return n ^ (1 << i)
}
console.log(6 ^ 3) // 110 ^ 011 = 101 = 5
console.log(5 ^ 5) // 0 (self-cancelling)
console.log(5 ^ 0) // 5 (identity)
console.log(7 ^ 3 ^ 3) // 7 (3 cancels itself)
XOR is the foundation of three important tricks: finding the unique element among duplicates, swapping without a temp, and checking bit differences.
NOT (~)
Flips all bits. In two’s complement: ~n === -(n + 1).
console.log(~5) // -6
console.log(~0) // -1
console.log(~(-1)) // 0
Left shift (<<)
Shifts bits left by the given amount, filling with 0s on the right. Each position left multiplies by 2.
console.log(1 << 3) // 8 (1 × 2³)
console.log(3 << 2) // 12 (3 × 4)
console.log(1 << 31) // -2147483648 ← sets the sign bit in 32-bit signed!
Arithmetic right shift (>>)
Shifts bits right, preserving the sign bit (fills left with the sign bit). Each position right divides by 2 (truncated).
console.log(8 >> 1) // 4 (positive: fills with 0s)
console.log(-8 >> 1) // -4 (negative: fills with 1s, sign preserved)
console.log(7 >> 1) // 3 (truncates toward negative infinity)
Unsigned right shift (>>>)
JavaScript-specific: shifts right and fills with 0s regardless of sign. Treats the value as unsigned.
console.log(-1 >>> 0) // 4294967295 (all 32 bits set, unsigned interpretation)
console.log(-8 >>> 1) // 2147483644 (not -4!)
console.log(8 >>> 1) // 4 (same as >> for positive numbers)
Use >>> 0 to convert a potentially negative 32-bit value to its unsigned equivalent.
Essential tricks
These patterns appear constantly — know them by heart.
// 1. Check if n is a power of 2
// Powers of 2 have exactly one set bit: 1000...0
// n-1 flips all lower bits: 0111...1
// AND of the two is always 0
function isPowerOfTwo(n: number): boolean {
return n > 0 && (n & (n - 1)) === 0
}
// 2. Isolate the lowest set bit
// -n in two's complement flips all bits then adds 1,
// which preserves only the lowest set bit
function lowestSetBit(n: number): number {
return n & (-n)
}
// 3. Clear the lowest set bit — the heart of Brian Kernighan's algorithm
// n-1 turns the lowest set bit to 0 and flips all lower bits to 1
// AND with n clears exactly that one bit
function clearLowestSetBit(n: number): number {
return n & (n - 1)
}
// 4. Count set bits — Brian Kernighan's algorithm
// Each iteration clears one set bit. Runs in O(k) where k = set bit count.
// Faster than looping over all 32 positions when bits are sparse.
function countSetBits(n: number): number {
let count = 0
while (n !== 0) {
n = n & (n - 1)
count++
}
return count
}
// 5. Swap two numbers without a temporary variable
function swapBits(a: number, b: number): [number, number] {
a ^= b // a = a XOR b
b ^= a // b = b XOR (a XOR b) = original a
a ^= b // a = (a XOR b) XOR original a = original b
return [a, b]
}
// 6. Sign of a number (without branching)
function sign(n: number): number {
return 1 | (n >> 31) // 1 for positive/zero, -1 for negative
}
Single Number — LC 136
Every element in the array appears exactly twice except for one. Find the single element in O(n) time and O(1) space.
Trick: XOR all elements. Duplicate pairs cancel each other out (a ^ a = 0), leaving only the unique element.
function singleNumber(nums: number[]): number {
return nums.reduce((xor, n) => xor ^ n, 0)
}
// [2, 2, 1] → 0 ^ 2 ^ 2 ^ 1 = 0 ^ 0 ^ 1 = 1
// [4, 1, 2, 1, 2] → 0 ^ 4 ^ 1 ^ 2 ^ 1 ^ 2 = 4
Time: O(n). Space: O(1). No hash map needed.
Single Number II — LC 137
Every element appears exactly three times except for one. Find the single element.
Trick: For each bit position, count how many numbers have that bit set. If the count mod 3 is 1, the unique number has that bit set. If 0, it does not.
function singleNumberII(nums: number[]): number {
let result = 0
for (let i = 0; i < 32; i++) {
let bitSum = 0
for (const num of nums) {
bitSum += (num >> i) & 1
}
if (bitSum % 3 === 1) {
result |= (1 << i)
}
}
return result | 0 // force 32-bit signed interpretation
}
Optimized O(1) space — bit automaton approach:
Track which bits have been seen once (ones) and twice (twos). When a bit is seen a third time, it drops out of both.
function singleNumberIIOptimal(nums: number[]): number {
let ones = 0, twos = 0
for (const num of nums) {
ones = (ones ^ num) & ~twos // add to ones only if not already in twos
twos = (twos ^ num) & ~ones // add to twos only if not already in ones
}
return ones // ones holds bits seen exactly once
}
Time: O(n). Space: O(1).
Number of 1 Bits — LC 191
Count the number of set bits (Hamming weight) in a 32-bit unsigned integer.
function hammingWeight(n: number): number {
n = n >>> 0 // treat as unsigned 32-bit to handle negative input safely
let count = 0
while (n !== 0) {
n = n & (n - 1) // clear the lowest set bit
count++
}
return count
}
// Trace for n = 11 (binary 1011):
// 1011 → 1010 → 1000 → 0000 (3 iterations = 3 set bits)
Time: O(k) where k = number of set bits. Space: O(1).
Note: The naive approach loops over all 32 bit positions — O(32). Brian Kernighan’s algorithm only iterates over set bits, making it faster for sparse integers.
Counting Bits — LC 338
Return an array ans where ans[i] = number of set bits in i, for all 0 <= i <= n.
Trick: i >> 1 is i with its last bit removed (already computed). Adding i & 1 accounts for the last bit.
function countBits(n: number): number[] {
const dp = new Array(n + 1).fill(0)
for (let i = 1; i <= n; i++) {
dp[i] = dp[i >> 1] + (i & 1)
// ^^^^^^^^^^^ ^^^^^^^^
// bits in i/2 last bit of i (0 if even, 1 if odd)
}
return dp
}
// n = 5: [0, 1, 1, 2, 1, 2]
// i=4 (100): dp[2] + 0 = 1 + 0 = 1 ✓
// i=5 (101): dp[2] + 1 = 1 + 1 = 2 ✓
Time: O(n). Space: O(n). This is DP with a bit trick. The naive approach calling hammingWeight(i) for each i would be O(n × 32) = O(n log n).
Reverse Bits — LC 190
Reverse the bits of a 32-bit unsigned integer.
function reverseBits(n: number): number {
let result = 0
for (let i = 0; i < 32; i++) {
result = (result << 1) | (n & 1) // shift result left, append LSB of n
n >>>= 1 // unsigned right shift n (zero-fills MSB)
}
return result >>> 0 // ensure unsigned 32-bit result
}
// For n = 43261596 (00000010100101000001111010011100):
// Result = 964176192 (00111001011110000010100101000000)
Time: O(32) = O(1). Space: O(1).
result >>> 0 is critical: without it, if bit 31 of result is set, JavaScript’s signed integer representation returns a negative number, which is wrong for an unsigned reverse.
Missing Number — LC 268
Given an array containing n distinct numbers in the range [0, n], find the one that’s missing.
function missingNumber(nums: number[]): number {
const n = nums.length
// XOR all indices 0..n with all values in nums
// Each present number cancels with its index; only the missing number survives
let xor = n
for (let i = 0; i < n; i++) {
xor ^= i ^ nums[i]
}
return xor
}
// nums = [3, 0, 1], n = 3
// xor = 3 ^ (0^3) ^ (1^0) ^ (2^1) = 3^3^0^1^2^1 = 0^0^2 = 2 ✓
Alternative: math formula n*(n+1)/2 - sum(nums). XOR avoids potential overflow for very large n.
Sum of Two Integers — LC 371
Add two integers without using + or -.
Trick: XOR gives the sum without carries. AND shifted left gives the carry bits. Repeat until there are no more carries.
function getSum(a: number, b: number): number {
while (b !== 0) {
const carry = (a & b) << 1 // positions where both bits are 1 produce a carry
a = a ^ b // sum without carry
b = carry // carry propagates to next position
}
return a
}
// 3 + 5:
// Round 1: carry = (011 & 101) << 1 = 001 << 1 = 010
// a = 011 ^ 101 = 110
// b = 010
// Round 2: carry = (110 & 010) << 1 = 010 << 1 = 100
// a = 110 ^ 010 = 100
// b = 100
// Round 3: carry = (100 & 100) << 1 = 1000
// a = 100 ^ 100 = 000
// b = 1000
// Round 4: carry = (000 & 1000) << 1 = 0
// a = 000 ^ 1000 = 1000 = 8 ✓
Time: O(1) — at most 32 iterations. Space: O(1).
Handles negative numbers correctly: two’s complement arithmetic works the same way for XOR/AND operations, so the algorithm is correct for all 32-bit integers.
Maximum XOR of Two Numbers in an Array — LC 421
Find the maximum XOR of any two numbers in nums.
Trie approach: Insert all numbers into a binary trie (MSB first). For each number, greedily walk the trie choosing the opposite bit at each level — opposite bits XOR to 1, maximizing the result.
class TrieNode {
children: (TrieNode | null)[] = [null, null]
}
function findMaximumXOR(nums: number[]): number {
const root = new TrieNode()
function insert(num: number): void {
let node = root
for (let i = 31; i >= 0; i--) {
const bit = (num >> i) & 1
if (!node.children[bit]) node.children[bit] = new TrieNode()
node = node.children[bit]!
}
}
// For each number, find the max XOR achievable against the trie
function query(num: number): number {
let node = root, xor = 0
for (let i = 31; i >= 0; i--) {
const bit = (num >> i) & 1
const want = 1 - bit // prefer opposite bit to get a 1 in XOR result
if (node.children[want]) {
xor |= (1 << i)
node = node.children[want]!
} else {
node = node.children[bit]!
}
}
return xor
}
for (const num of nums) insert(num)
return nums.reduce((max, num) => Math.max(max, query(num)), 0)
}
Time: O(n × 32) = O(n). Space: O(n × 32) for the trie.
Bitmask DP
Use an integer as a compact representation of a set: bit i is 1 if element i is in the set, 0 otherwise. State space becomes 2^n subsets.
When to use: n is small (typically n ≤ 20 or n ≤ 25). Problems over all subsets: assignment problems, TSP variants, minimum cost covering all states.
Essential bitmask operations:
const n = 4
// All elements included: (1 << n) - 1
const fullMask = (1 << n) - 1 // 1111 = 15
// Check if element i is in mask
const has = (mask: number, i: number) => (mask & (1 << i)) !== 0
// Add element i to mask
const add = (mask: number, i: number) => mask | (1 << i)
// Remove element i from mask
const remove = (mask: number, i: number) => mask & ~(1 << i)
// Iterate over all subsets of mask (excluding empty set)
function iterateSubsets(mask: number): void {
for (let sub = mask; sub > 0; sub = (sub - 1) & mask) {
// process subset `sub`
}
}
// Popcount (number of set bits)
function popcount(mask: number): number {
let count = 0
let m = mask
while (m !== 0) { m &= m - 1; count++ }
return count
}
TSP-style example — Traveling Salesman Problem:
// dp[mask][i] = min cost to visit exactly the cities in mask, ending at city i
function tsp(cost: number[][]): number {
const n = cost.length
const full = (1 << n) - 1
const INF = Infinity
const dp: number[][] = Array.from({ length: 1 << n }, () =>
new Array(n).fill(INF)
)
dp[1][0] = 0 // start at city 0, mask = 0001
for (let mask = 1; mask <= full; mask++) {
for (let u = 0; u < n; u++) {
if (!(mask & (1 << u))) continue // u not in current mask
if (dp[mask][u] === INF) continue
for (let v = 0; v < n; v++) {
if (mask & (1 << v)) continue // v already visited
const nextMask = mask | (1 << v)
dp[nextMask][v] = Math.min(dp[nextMask][v], dp[mask][u] + cost[u][v])
}
}
}
// Return to starting city 0
let ans = INF
for (let u = 1; u < n; u++) {
if (dp[full][u] !== INF) ans = Math.min(ans, dp[full][u] + cost[u][0])
}
return ans
}
Time: O(2^n × n²). Space: O(2^n × n). Feasible for n ≤ 20.
JavaScript gotcha: 32-bit signed integers
JavaScript’s bitwise operators always convert operands to 32-bit signed integers before operating and return a 32-bit signed integer. This causes surprises.
// Shift amount is taken mod 32 — shifting by 32 is a no-op!
console.log(1 << 32) // 1 (not 0)
console.log(1 << 31) // -2147483648 (sets sign bit → negative)
// >>> 0 converts to unsigned 32-bit
console.log(-1 >>> 0) // 4294967295
console.log(0x80000000 | 0) // -2147483648 (MSB set = negative in signed)
console.log(0x80000000 >>> 0) // 2147483648 (correct unsigned value)
// XOR with all 1s flips every bit (equivalent to bitwise NOT for unsigned)
const unsignedFlip = (n: number) => (n ^ 0xFFFFFFFF) >>> 0
// Safe popcount for potentially negative inputs
function hammingWeightSafe(n: number): number {
n = n >>> 0 // reinterpret as unsigned 32-bit before counting
let count = 0
while (n !== 0) { n = n & (n - 1); count++ }
return count
}
Rules of thumb:
- Use
>>> 0any time you need unsigned 32-bit semantics - Avoid
1 << 31in arithmetic — it produces a negative 32-bit signed integer - Bit shifts with amounts ≥ 32 silently wrap (shift amount
% 32) - Use
BigIntwhen you genuinely need more than 31 usable bits
Complexity and use-case reference
| Problem | Time | Space | Key Trick |
|---|---|---|---|
| Single Number (LC 136) | O(n) | O(1) | XOR cancels pairs |
| Single Number II (LC 137) | O(n) | O(1) | Bit count mod 3 |
| Number of 1 Bits (LC 191) | O(k) | O(1) | Brian Kernighan |
| Counting Bits (LC 338) | O(n) | O(n) | DP: dp[i >> 1] + (i & 1) |
| Reverse Bits (LC 190) | O(1) | O(1) | Shift and OR, 32 iterations |
| Missing Number (LC 268) | O(n) | O(1) | XOR index with value |
| Sum of Two Integers (LC 371) | O(1) | O(1) | XOR sum + AND carry |
| Maximum XOR (LC 421) | O(n) | O(n) | Greedy trie traversal |
| Bitmask DP (TSP etc.) | O(2^n × n²) | O(2^n × n) | Integer as subset |
| Power of 2 check | O(1) | O(1) | n & (n - 1) === 0 |
| Isolate lowest set bit | O(1) | O(1) | n & (-n) |
| Clear lowest set bit | O(1) | O(1) | n & (n - 1) |