1. Problem Statement
Write a function that takes the binary representation of an unsigned integer and returns the number of '1' bits it has (also known as the Hamming weight).
Input: n = 00000000000000000000000000001011
Output: 3 (The input has three '1' bits)
2. The Mental Model: The "Bit Mask" Intuition
Every integer is stored in memory as a sequence of 32 or 64 binary digits (0 or 1).
To count the '1's, we can use a Scanner (a mask).
- Check the last digit:
n & 1. If the result is 1, we found a set bit. - Move the scanner: Shift the entire number to the right by 1 bit:
n >>> 1. - Repeat until the number becomes zero.
However, there is a much faster way used by senior engineers called Brian Kernighan’s Algorithm.
3. Visual Execution (Brian Kernighan’s Magic)
Instead of checking every bit (32 steps), we can jump directly from one '1' bit to the next.
The Rule: n & (n - 1) always unsets the rightmost set bit.
graph TD
Start[n = 11 (1011)] --> Step1[n & 10 (1010) = 10 (1010)]
Step1 --> Step2[10 & 9 (1001) = 8 (1000)]
Step2 --> Step3[8 & 7 (0111) = 0 (0000)]
Step3 --> Done[Count: 3 steps!]
4. Java Implementation (Staff-Tier Optimized)
public int hammingWeight(int n) {
int count = 0;
// Brian Kernighan's Algorithm
while (n != 0) {
// This magic operation unsets the rightmost '1' bit
n = n & (n - 1);
count++;
}
return count;
}
5. Verbal Interview Script (Staff Tier)
Interviewer: "How do you count set bits, and why is your approach better than a standard loop?"
You: "A standard approach would be to iterate through all 32 bits using a bitmask and shifting. While correct, it always takes 32 iterations regardless of the input. I prefer Brian Kernighan’s Algorithm, which performs the operation n & (n - 1). This specific bitwise operation clears the rightmost set bit in a single step. Therefore, the number of iterations is strictly equal to the number of set bits (the Hamming weight), not the total number of bits. For a sparse integer like $2^{31}$, my code finishes in 1 iteration instead of 32. This provides a significant constant-time optimization for bit-heavy applications."
6. Staff-Level Follow-Ups
Follow-up 1: "How does the JVM handle signed vs unsigned shifts?"
- The Answer: "Java uses
>>for arithmetic right shift (preserves the sign bit) and>>>for logical right shift (fills with zeros regardless of sign). For this problem, using>>>is critical if we were to use the shifting approach, as it ensures that negative numbers don't lead to infinite loops by filling the left side with 1s."
Follow-up 2: "What if you need to call this function billions of times per second?"
- The Answer: "I would use a Pre-calculated Lookup Table. I'd split the 32-bit integer into four 8-bit segments. I would pre-compute the Hamming weight for all 256 possible 8-bit values and store them in an array. The final count would then be
table[seg1] + table[seg2] + table[seg3] + table[seg4]. This reduces the work to 4 array lookups and 3 additions, which is extremely fast and cache-friendly."
7. Performance Nuances (The Java Perspective)
- Integer.bitCount(): In production, I would use the built-in
Integer.bitCount(n). This method is often mapped to a Hardware-level CPU instruction (likePOPCNTon x86), which performs the entire count in a single CPU cycle. - Primitive Specialization: Always use
intorlongfor bitwise operations. Never useIntegerobjects as the constant boxing and unboxing will destroy the performance gains of bit manipulation.
6. Staff-Level Verbal Masterclass (Communication)
Interviewer: "How would you defend this specific implementation in a production review?"
You: "In a mission-critical environment, I prioritize the Big-O efficiency of the primary data path, but I also focus on the Predictability of the system. In this implementation, I chose a state-based dynamic programming approach. While a recursive solution is more readable, I would strictly monitor the stack depth. If this were to handle skewed inputs, I would immediately transition to an explicit stack on the heap to avoid a StackOverflowError. From a memory perspective, I leverage localized objects to ensure that we minimize the garbage collection pauses (Stop-the-world) that typically plague high-throughput Java applications."
7. Global Scale & Distributed Pivot
When a problem like this is moved from a single machine to a global distributed architecture, the constraints change fundamentally.
- Data Partitioning: We would shard the input space using Consistent Hashing. This ensures that even if our dataset grows to petabytes, any single query only hits a small subset of our cluster, maintaining logarithmic lookup times.
- State Consistency: For problems involving state updates (like DP or Caching), we would use a Distributed Consensus protocol like Raft or Paxos to ensure that all replicas agree on the final state, even in the event of a network partition (The P in CAP theorem).
8. Performance Nuances (The Staff Perspective)
- Cache Locality: Accessing a 2D matrix in row-major order (reading
[i][j]then[i][j+1]) is significantly faster than column-major order in modern CPUs due to L1/L2 cache pre-fetching. I always structure my loops to align with how the memory is physically laid out. - Autoboxing and Generics: In Java, using
List<Integer>instead ofint[]can be 3x slower due to the overhead of object headers and constant wrapping. For the most performance-sensitive sections of this algorithm, I advocate for primitive specialized structures.