186 lines
7.8 KiB
YAML
186 lines
7.8 KiB
YAML
title: Number of 1 Bits
|
|
slug: number-of-1-bits
|
|
difficulty: easy
|
|
leetcode_id: 191
|
|
leetcode_url: https://leetcode.com/problems/number-of-1-bits/
|
|
categories:
|
|
- math
|
|
patterns:
|
|
- slug: bit-manipulation
|
|
is_optimal: true
|
|
|
|
function_signature: "def hamming_weight(n: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { n: 11 }
|
|
expected: 3
|
|
- input: { n: 128 }
|
|
expected: 1
|
|
- input: { n: 1 }
|
|
expected: 1
|
|
hidden:
|
|
- input: { n: 7 }
|
|
expected: 3
|
|
- input: { n: 255 }
|
|
expected: 8
|
|
- input: { n: 16 }
|
|
expected: 1
|
|
|
|
description: |
|
|
Given a positive integer `n`, write a function that returns the number of **set bits** in its binary representation (also known as the [Hamming weight](http://en.wikipedia.org/wiki/Hamming_weight)).
|
|
|
|
A *set bit* is a bit that has a value of `1`.
|
|
|
|
constraints: |
|
|
- `1 <= n <= 2^31 - 1`
|
|
|
|
examples:
|
|
- input: "n = 11"
|
|
output: "3"
|
|
explanation: "The binary representation of 11 is `1011`, which has three set bits."
|
|
- input: "n = 128"
|
|
output: "1"
|
|
explanation: "The binary representation of 128 is `10000000`, which has one set bit."
|
|
- input: "n = 2147483645"
|
|
output: "30"
|
|
explanation: "The binary representation has 30 set bits (all bits except the 2nd position are 1)."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine each bit in the binary representation as a light switch — either on (`1`) or off (`0`). Your task is simply to count how many switches are in the "on" position.
|
|
|
|
The naive approach would be to check each of the 32 bit positions one by one. But there's a clever trick using bit manipulation that lets you count *only* the `1` bits, skipping all the `0`s entirely.
|
|
|
|
The key insight is the operation `n & (n - 1)`. Subtracting `1` from a number flips all the bits from the rightmost `1` bit to the end. When you AND the original number with `n - 1`, the rightmost `1` bit gets cleared. For example:
|
|
- `12` in binary is `1100`
|
|
- `11` in binary is `1011`
|
|
- `12 & 11 = 1000` — the rightmost `1` bit is gone!
|
|
|
|
By repeatedly clearing the rightmost `1` bit and counting how many times we do this, we get the exact count of set bits. This approach is proportional to the number of `1` bits, not the total number of bits.
|
|
|
|
approach: |
|
|
We solve this using the **Brian Kernighan's Algorithm**:
|
|
|
|
**Step 1: Initialise the counter**
|
|
|
|
- `count`: Set to `0` to track the number of set bits
|
|
|
|
|
|
|
|
**Step 2: Loop while n is not zero**
|
|
|
|
- While `n > 0`:
|
|
- Apply `n = n & (n - 1)` — this clears the rightmost set bit
|
|
- Increment `count` by `1`
|
|
|
|
|
|
|
|
**Step 3: Return the count**
|
|
|
|
- Return `count` which now holds the number of `1` bits
|
|
|
|
|
|
|
|
This algorithm is elegant because it only iterates as many times as there are set bits. A number with just one `1` bit (like `128`) completes in one iteration, while a number with many `1` bits takes proportionally more.
|
|
|
|
common_pitfalls:
|
|
- title: Checking All 32 Bits
|
|
description: |
|
|
A common approach is to loop through all 32 bits and check each one:
|
|
|
|
```
|
|
for i in range(32):
|
|
if n & (1 << i):
|
|
count += 1
|
|
```
|
|
|
|
While this works and has O(1) time (constant 32 iterations), it's less efficient than the `n & (n - 1)` approach when there are few set bits.
|
|
|
|
For example, the number `128` has only one set bit. The bit-by-bit approach still checks all 32 positions, while Brian Kernighan's algorithm finishes in just one iteration.
|
|
wrong_approach: "Loop through all 32 bit positions"
|
|
correct_approach: "Use n & (n - 1) to clear bits one at a time"
|
|
|
|
- title: Using Right Shift with Signed Integers
|
|
description: |
|
|
In some languages, using `n >> 1` on a negative number (or treating the input as signed) can cause issues due to sign extension — the sign bit gets replicated.
|
|
|
|
Python handles arbitrary-precision integers, so this isn't an issue in Python. But in languages like C or Java, you'd want to use unsigned right shift (`>>>`) or work with unsigned types to avoid infinite loops with numbers that have the high bit set.
|
|
wrong_approach: "Using signed right shift on potentially negative values"
|
|
correct_approach: "Use unsigned operations or the n & (n - 1) trick"
|
|
|
|
- title: Forgetting Zero Has No Set Bits
|
|
description: |
|
|
While the problem states `n >= 1`, it's worth noting that `0` has zero set bits. If your algorithm starts with `count = 1` instead of `count = 0`, or doesn't handle the case where `n` is already zero, you could return incorrect results.
|
|
|
|
The `n & (n - 1)` approach handles this naturally — if `n = 0`, the loop never executes and returns `0`.
|
|
|
|
key_takeaways:
|
|
- "**Brian Kernighan's Algorithm**: `n & (n - 1)` clears the rightmost set bit — a fundamental bit manipulation trick"
|
|
- "**Efficiency proportional to set bits**: The algorithm runs in O(k) time where k is the number of `1` bits, not the total bit width"
|
|
- "**Hamming weight applications**: Counting set bits is used in error detection, cryptography, and similarity measures between binary strings"
|
|
- "**Follow-up optimisation**: For repeated calls, precompute a lookup table for bytes (256 entries) and process the number 8 bits at a time"
|
|
|
|
time_complexity: "O(k) where k is the number of set bits. Each iteration clears exactly one `1` bit, so we loop at most k times (maximum 31 for a 32-bit integer)."
|
|
space_complexity: "O(1). We only use a single counter variable regardless of the input size."
|
|
|
|
solutions:
|
|
- approach_name: Brian Kernighan's Algorithm
|
|
is_optimal: true
|
|
code: |
|
|
def hamming_weight(n: int) -> int:
|
|
# Count of set bits
|
|
count = 0
|
|
|
|
while n:
|
|
# n & (n - 1) clears the rightmost set bit
|
|
# This works because n - 1 flips all bits from
|
|
# the rightmost 1 to the end
|
|
n = n & (n - 1)
|
|
count += 1
|
|
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(k) — where k is the number of set bits (at most 31).
|
|
|
|
**Space Complexity:** O(1) — only a counter variable.
|
|
|
|
The trick `n & (n - 1)` exploits the binary representation: subtracting 1 flips the rightmost `1` and all bits after it, so AND-ing with the original clears exactly that bit. We count how many times we can do this before `n` becomes zero.
|
|
|
|
- approach_name: Bit-by-Bit Check
|
|
is_optimal: false
|
|
code: |
|
|
def hamming_weight(n: int) -> int:
|
|
count = 0
|
|
|
|
# Check each of the 32 bit positions
|
|
while n:
|
|
# Check if the last bit is 1
|
|
count += n & 1
|
|
# Right shift to check the next bit
|
|
n >>= 1
|
|
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — or O(32) for a 32-bit integer.
|
|
|
|
**Space Complexity:** O(1) — only a counter variable.
|
|
|
|
This approach checks each bit by AND-ing with `1` and right-shifting. It always processes all bits of the number, unlike Brian Kernighan's algorithm which only iterates once per set bit.
|
|
|
|
- approach_name: Built-in Population Count
|
|
is_optimal: false
|
|
code: |
|
|
def hamming_weight(n: int) -> int:
|
|
# Python's bit_count() method (Python 3.10+)
|
|
return n.bit_count()
|
|
|
|
# Alternative using bin() string manipulation
|
|
# return bin(n).count('1')
|
|
explanation: |
|
|
**Time Complexity:** O(1) — built-in operations are highly optimised, often using CPU popcount instructions.
|
|
|
|
**Space Complexity:** O(1) — no additional space.
|
|
|
|
Modern Python (3.10+) provides `int.bit_count()` which uses optimised CPU instructions when available. The `bin(n).count('1')` alternative creates a string, making it less efficient. While practical for production code, understanding the underlying algorithms is valuable for interviews.
|