Imagine you're spray-painting a t-shirt. You cut out a stencil โ maybe a star shape โ and place it over the fabric. When you spray, only the star gets painted. The rest of the shirt is protected.
A bit mask is exactly that stencil. It's a pattern of bits that selects which bits of a number you want to modify, and which bits you want to leave alone.
Let's meet the 5 operations you can do with a mask. These are the spray cans in your toolkit:
Operation
Expression
Analogy
Set bit to 1
n | (1<<j)
"Paint this spot"
Clear bit to 0
n & ~(1<<j)
"Scrape this spot clean"
Toggle bit
n ^ (1<<j)
"Flip the switch"
Check bit
(n>>j) & 1
"Peek under the stencil"
Isolate lowest 1
n & -n
"Find the rightmost 1"
Tip
๐ก The Key Insight: A mask is just a regular number that you AND, OR, XOR, or NOT with your target. The bits set to 1 in the mask are the bits you're operating on. Bits set to 0 in the mask are left alone. That's all there is to it!
1. Number Complement โ Flip Every Bit
Problem: Given a positive integer, flip every bit in its binary representation and return the result.
For n = 5 (binary 101), the complement is 010 = 2.
Note
๐ญ Real-World Analogy: Imagine you have a row of lamps, each either on (1) or off (0). The complement is like having a light switch panel that flips ALL the lamps at once โ every lamp that was on turns off, and every lamp that was off turns on. But here's the catch: you only want to flip the lamps that are actually in your display, not all 32 lamps in the store! So first you figure out how many lamps you have (bit length), build a mask of that many lamps all set to ON, then XOR with your display.
๐ค Let's Think About It
How do you flip a bit? You XOR with 1! Every bit XOR'd with 1 flips: 0โ1, 1โ0.
So we need a mask of all 1s that's the same length as our number. For 5 (101), we need 111. The trick: find the bit-length, then compute (1 << bitLength) - 1.
๐ Edge Cases to Consider
n = 0: The complement should be 1 (since 0 has 1 bit, 0 ^ 1 = 1). But Math.log2(0) is -Infinity. Must handle n=0 as a special case.
n = power of two: e.g., 8 (1000). bit length = 4. mask = (1 << 4) - 1 = 15 (1111). 8 ^ 15 = 7 (0111). Works!
Large numbers (near 2^31): JavaScript bitwise ops truncate to 32-bit. (1 << 31) overflows. Use BigInt for larger values.
Brute Force โ Build Mask Bit by Bit
function bitwiseComplement(n) {
if (n === 0) return 1;
// Find bit-length
let bitLength = 0;
let temp = n;
while (temp > 0) {
temp >>= 1;
bitLength++;
}
// Build mask of all 1s
let mask = 0;
for (let i = 0; i < bitLength; i++) {
mask = (mask << 1) | 1; // 0 โ 1 โ 11 โ 111
}
return n ^ mask; // XOR flips every bit
}
bitwiseComplement(5); // 2 (101 ^ 111 = 010)
bitwiseComplement(10); // 5 (1010 ^ 1111 = 0101)
bitwiseComplement(0); // 1
๐ Real-Time Thinking โ What Do I Know?
"I need to flip every bit of n. Flipping a bit is XOR with 1. So I need a mask of all 1s with the same bit length as n. How do I build that mask? If I find the bit length (say 3 for n=5, which is 101), then (1 << 3) = 1000, and (1 << 3) - 1 = 0111 = 111. That's my mask! Then n ^ mask flips everything. Let me check: 5 (101) ^ 7 (111) = 010 = 2. That's correct. The formula is: mask = (1 << bitLength) - 1."
Power of two: e.g., 8 (1000) โ 7 (0111). The mask must not clip the leading 1.
Large numbers: JS bitwise works on 32-bit signed integers, so this works for integers up to 2ยณยน-1.
Tip
โก Interview Tip: The mask (1 << k) - 1 gives you k bits all set to 1. This is the SINGLE most useful mask-building formula. Memorize it!
PROBLEMNumber Complement
Given a positive integer, return its complement โ flip all bits in its binary representation.
Loading playground...
PROBLEMComplement of Base 10 Integer
Same complement problem, framed as a base-10 integer. Every non-negative integer N has a complement in binary.
Loading playground...
2. Binary Gap & Alternating Bits
Problem: Given a positive integer, find the longest sequence of consecutive zeros between two 1s in its binary representation.
Note
๐ Real-World Analogy: Binary gap is like measuring the distance between two trees in a park. You walk along the path (the binary digits from right to left). Every time you see a tree (a 1-bit), you note its position. The gap is the distance between consecutive trees. If there's only one tree, there's no gap โ you need at least two trees to have a gap between them!
For n = 9 (1001), there are two 1s at positions 0 and 3. The gap between them is 3 - 0 = 3.
The Algorithm โ Track Position of Last 1
We scan bits from right to left. When we find a 1, we check how far it is from theprevious 1 we saw. The maximum distance is our answer.
function binaryGap(n) {
let lastPos = -1; // position of the last 1 we saw
let maxGap = 0;
for (let pos = 0; n > 0; pos++) {
if (n & 1) { // current bit is 1
if (lastPos !== -1) {
maxGap = Math.max(maxGap, pos - lastPos);
}
lastPos = pos;
}
n >>= 1; // move to next bit
}
return maxGap;
}
binaryGap(9); // 3 (1001)
binaryGap(1041); // 5 (10000010001)
binaryGap(6); // 1 (110)
binaryGap(8); // 0 (1000) โ only one 1
binaryGap(1); // 0 (1) โ only one 1
Dry Run โ 9 (1001) Step by Step
Step
n
LSB
lastPos
Action
1
1001
1
-1 โ 0
First 1, just record position 0
2
0100
0
0
Skip zero
3
0010
0
0
Skip zero
4
0001
1
0 โ 3
Second 1! gap = 3 - 0 = 3. maxGap = 3 โจ
Alternating Bits โ The XOR Shift Trick
Problem: Check if a number's binary representation has alternating bits (no two adjacent bits are the same).
Here's the magic: n ^ (n >> 1) puts a 1 wherever adjacent bits differ. If ALL adjacent bits differ, the result is all 1s!
๐ Real-Time Thinking โ What Do I Know?
"I need to check if adjacent bits alternate. If I XOR n with n >> 1, I get 1 wherever adjacent bits differ. If ALL adjacent bits differ, the result is ALL 1s. How do I check if a number is all 1s? Well, if x is all 1s (like 111), then x+1 = 1000, and x & (x+1) = 0. That's exactly like the power-of-two check but inverted! So: n ^ (n>>1) gives the difference pattern. Then (x & (x+1)) === 0 means it's all 1s. Let me verify with 5 (101): 5 ^ 2 = 7 (111). 7 & 8 = 0. Yes, alternating!"
function hasAlternatingBits(n) {
// XOR n with itself shifted right by 1
// If bits alternate, every adjacent pair differs โ all 1s
const x = n ^ (n >> 1);
// Check if x is all 1s: (x & (x + 1)) === 0
// e.g., 5 (101) ^ 2 (10) = 7 (111)
// 7 & (7+1) = 7 & 8 = 0 โ
return (x & (x + 1)) === 0;
}
hasAlternatingBits(5); // true 101
hasAlternatingBits(7); // false 111 (adjacent 1s)
hasAlternatingBits(10); // true 1010
hasAlternatingBits(11); // false 1011
Key Insight
๐ The Intuition:n ^ (n>>1) is a "difference detector" for adjacent bits. When bits alternate, every adjacent pair is different, so the result is all 1s. The (x & (x+1)) === 0 check is a standard way to test if a number is all 1s (like checking power of two, but inverted).
PROBLEMBinary Gap
Find the longest sequence of zeros between two 1s in a positive integer's binary representation.
Loading playground...
PROBLEMBinary Number with Alternating Bits
Check whether a positive integer has alternating bits โ no two adjacent bits are the same.
Loading playground...
3. Base Conversion โ Hexadecimal
Problem: Convert a 32-bit integer to its hexadecimal representation. Handle negative numbers via two's complement.
๐ค Bits to Hex: 4 Bits at a Time
Hexadecimal is base-16. Each hex digit represents exactly 4 bits. So we can extract 4 bits at a time using a mask of 0xF (which is 1111in binary), map them to a character, and repeat!
function toHex(num) {
if (num === 0) return "0";
const hexChars = "0123456789abcdef";
let result = "";
// >>> 0 converts to unsigned 32-bit (handles negatives!)
let n = num >>> 0;
while (n > 0) {
const digit = n & 0xf; // extract lowest 4 bits
result = hexChars[digit] + result;
n >>>= 4; // shift right by 4 bits
}
return result;
}
toHex(26); // "1a"
toHex(255); // "ff"
toHex(-1); // "ffffffff" (two's complement!)
toHex(0); // "0"
Dry Run โ 26 to Hex
Step
n
n & 0xF
Hex Digit
n >>> 4
1
26 (11010)
10 (1010)
"a"
1
2
1 (1)
1 (0001)
"1"
0
Result: "1" + "a" = "1a" ๐ฏ
Warning
โ ๏ธ The >>> 0 Trick: In JavaScript, bitwise operators work on 32-bit signed integers. Using >>> 0 (unsigned right shift by 0) converts the number to its unsigned 32-bit representation, which is how two's complement handles negative numbers. Without this, toHex(-1) would loop forever!
Tip
โก Pattern: The mask 0xF (binary 1111) isolates the lowest 4 bits. This is the "hex mask." For octal (3 bits), use 0x7. For binary (1 bit), use 0x1.mask = (1 << bitsPerDigit) - 1 is the universal formula.
PROBLEMConvert a Number to Hexadecimal
Given a 32-bit integer, return its hexadecimal representation. Handle negative numbers with two's complement.
Loading playground...
4. Add Binary Strings โ Column Addition with Carry
Problem: Given two binary strings, return their sum as a binary stringwithout converting them to integers.
Note
๐งฎ Real-World Analogy: Remember adding numbers in elementary school? You line them up, add column by column, and carry the overflow to the next column. Binary addition is EXACTLY the same โ except overflow happens at 2 instead of 10. 1 + 1 = 0 with carry 1. It's like having a two-finger abacus where each column can only hold 0 or 1 before it overflows!
๐ค The Old-School Way
Remember adding numbers in elementary school? Line them up, add column by column, carry the overflow to the next column. Binary is EXACTLY the same โ except overflow happens at 2 instead of 10.
Brute Force โ Parse Then Add
function addBinaryBrute(a, b) {
// Convert to integers, add, convert back
// PROBLEM: JavaScript loses precision for large numbers!
return (parseInt(a, 2) + parseInt(b, 2)).toString(2);
}
// Works for small strings:
addBinaryBrute("11", "1"); // "100"
// Fails for large strings โ precision loss!
Warning
โ ๏ธ Browser Precision Trap:parseInt can only handle 53-bit integers. For binary strings longer than 53 bits, you get precision loss. The column addition approach works for any length!
โจ The Column Addition Solution
function addBinary(a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let result = [];
while (i >= 0 || j >= 0 || carry > 0) {
const bitA = i >= 0 ? parseInt(a[i], 10) : 0;
const bitB = j >= 0 ? parseInt(b[j], 10) : 0;
const sum = bitA + bitB + carry;
result.push(sum % 2); // sum bit
carry = Math.floor(sum / 2); // carry to next column
i--;
j--;
}
return result.reverse().join("");
}
addBinary("11", "1"); // "100"
addBinary("1010", "1011"); // "10101"
addBinary("0", "0"); // "0"
addBinary("1111", "1"); // "10000"
Edge Cases & Carry Ripples
Single carry: "1" + "1" = "10" (carry adds one bit)
Ripple carry: "1" + "111" = "1000" (carry propagates through all bits)
Equal length: "1010" + "0101" = "1111" (no carry at all)
Result longer: "111" + "111" = "1110" (result is 1 bit longer than inputs)
Key Insight
๐ Key Formula: In binary, each column is: sum = a + b + carry. The output bit is sum % 2 and the new carry isMath.floor(sum / 2). This is literally how CPUs add numbers!
PROBLEMAdd Binary
Given two binary strings, return their sum as a binary string. Handle carry propagation correctly without converting to integers.
Loading playground...
5. Binary Watch โ Enumeration with Popcount
Problem: A binary watch displays time using LEDs. The hour (0โ11) uses 4 LEDs and the minute (0โ59) uses 6 LEDs. Given a number of LEDs that are turned on, list all possible times.
๐ Real-Time Thinking โ What Do I Know?
"I need to find all times where exactly k LEDs are lit. Hours: 0-11 (4 bits). Minutes: 0-59 (6 bits). Total combinations: 12 ร 60 = 720 โ that's tiny! I can brute force this by iterating over ALL hours and minutes and counting set bits. For each hour h and minute m, if popcount(h) + popcount(m) === k, I add the time to the result. Brute force IS the optimal solution here because the search space is so small."
๐ค The Combinatorial Approach
We need to find all pairs of (hour, minute) where the total number of 1-bits equalsturnedOn. The brute-force way: check EVERY possible hour (0โ11) and EVERY possible minute (0โ59), count their set bits, and collect valid pairs.
That's only 12 ร 60 = 720 combinations โ tiny! So brute force is the optimal solution here.
function readBinaryWatch(turnedOn) {
const result = [];
for (let h = 0; h < 12; h++) {
for (let m = 0; m < 60; m++) {
// Count set bits in hour + minute
const bits = popcount(h) + popcount(m);
if (bits === turnedOn) {
const time = h + ":" + m.toString().padStart(2, "0");
result.push(time);
}
}
}
return result;
}
// Brian Kernighan's algorithm โ count set bits
function popcount(n) {
let count = 0;
while (n) {
n &= n - 1; // clear the lowest set bit
count++;
}
return count;
}
readBinaryWatch(1);
// ["0:01","0:02","0:04","0:08","0:16","0:32",
// "1:00","2:00","4:00","8:00"]
readBinaryWatch(9); // [] โ max 10 LEDs (4+6), can't have 9
Why Popcount Matters
The popcount (n & (n - 1)) is itself a bit masking trick! Each iteration clears the lowest set bit and increments the counter. For a number with k set bits, it runs k times instead of checking all 32 bit positions.
Tip
โก The Pattern: "Enumeration by popcount" โ whenever you need to generate combinations where exactly k items are selected from n, consider iterating over the value range and filtering by popcount. It's simple, fast, and the brute force is often optimal when the range is small.
PROBLEMBinary Watch
A binary watch has 4 LEDs for hours (0-11) and 6 LEDs for minutes (0-59). Given turnedOn LEDs, return all possible times. Use popcount to filter.
Loading playground...
6. Common Masking Tricks โ Complete Reference
These tricks appear in dozens of LeetCode problems. Memorize them.They're the building blocks of every bit manipulation solution.
Set, Clear, Toggle, Check
// Check if k-th bit is set
(n >> k) & 1
// or: (n & (1 << k)) !== 0
// Set k-th bit to 1
n | (1 << k)
// Clear k-th bit to 0
n & ~(1 << k)
// Toggle k-th bit
n ^ (1 << k)
Power of Two Check
function isPowerOfTwo(n) {
return n > 0 && (n & (n - 1)) === 0;
}
// Why? Powers of two have exactly ONE set bit.
// n & (n-1) clears the lowest set bit.
// If that makes n zero, there was only one set bit!
isPowerOfTwo(1); // true (2โฐ)
isPowerOfTwo(16); // true (2โด)
isPowerOfTwo(6); // false (110 โ two set bits)
Isolate Lowest Set Bit
// n & -n isolates the rightmost 1 bit
// Why? -n = ~n + 1 (two's complement)
// The +1 flips trailing 0s and the lowest 1,
// ~ flips higher bits โ AND isolates the lowest 1
lowestSetBit(12); // 4 (1100 โ 0100)
lowestSetBit(10); // 2 (1010 โ 0010)
lowestSetBit(7); // 1 (111 โ 001)
โก Pro Tip: All these tricks follow the same recipe:build the right mask, then apply AND/OR/XOR/NOT. The mask selects which bits you're operating on, and the operator determines what you do to them.
๐ฏ Interview Cheat Sheet
Key Insight
Q1: How do you flip every bit in a number? XOR with a mask of all 1s of the same bit-length: n ^ mask. Build mask via (1 << bitLength) - 1.
Key Insight
Q2: How do you find the longest binary gap? Scan bits right to left. Track the position of the last 1. When you find a new 1, compute pos - lastPos and update the max. Strip trailing zeros first.
Key Insight
Q3: How do you check for alternating bits? n ^ (n >> 1) produces all 1s if bits alternate. Check with (x & (x + 1)) === 0.
Key Insight
Q4: How do you convert to hex by hand? Extract lowest 4 bits with n & 0xF, map to hex char, shift right by 4 (n >>>= 4), repeat. For negatives, use >>> 0 for two's complement.
Key Insight
Q5: How do you add binary strings safely? Column addition from right to left. sum = a + b + carry, result bit = sum % 2, carry = Math.floor(sum / 2). Don't use parseInt for long strings โ precision loss!
Key Insight
Q6: How do you enumerate fixed-count combinations? Iterate over the value range and filter by popcount (n &= n - 1in a loop). This is the "enumeration by popcount" pattern.
Key Insight
Q7: What are the 5 essential masking tricks? (1) Check: (n>>k) & 1. (2) Set: n | (1<<k). (3) Clear: n & ~(1<<k). (4) Toggle: n ^ (1<<k). (5) Clear lowest: n & (n-1).
๐ Key Takeaways
Bit masking = using stencils. A mask selects which bits you operate on; the operator determines what you do.
Number complement: Build a mask of all 1s with (1 << bitLength) - 1, then XOR.
Binary gap: Track lastPos of each 1. The max distance between consecutive 1s is your answer.
Alternating bits:n ^ (n>>1) detects adjacent differences. All 1s means alternating.
Hex conversion: Extract 4 bits at a time with 0xF mask. >>> 0 handles negatives.
Add binary: Column addition with carry. sum % 2 for bit, Math.floor(sum/2) for carry.
Binary watch: Brute force 720 combinations, filter by popcount. Sometimes brute force IS the optimal answer.
n & (n-1) clears the lowest set bit โ foundation of popcount and power-of-2 checks.
n & -n isolates the lowest set bit โ two's complement magic.
Practice! Head to the Playground below and solve these problems. The only way masking clicks is by doing it ๐ฅ
Note
๐ฎ What's Next? Now that you've mastered bit masking, check out AND/OR Range Patternsfor range-based bit tricks!