Counting Bits
Table of Contents + β
Counting Bits takes a single-number trick and stretches it across many numbers. You could count the ones for each number on its own. But there is a smarter path. You reuse answers you already found. That reuse is dynamic programming, and this problem is a clean way to learn it.
π― The Problem
You get a number n. You build a list of bit counts for every number from 0 to n. The count of 1 bits in a number is called its bit count.
The rules:
- The list has
n + 1slots, one for each number from0ton. - Position
iholds the number of1bits in the numberi. - Return the finished list.
Let us say n is 5. We look at every number from 0 to 5. The result is the list of their bit counts.
Input: n = 5Output: [0, 1, 1, 2, 1, 2]
Explanation:0 -> 000 -> 0 ones1 -> 001 -> 1 one2 -> 010 -> 1 one3 -> 011 -> 2 ones4 -> 100 -> 1 one5 -> 101 -> 2 onesSo we want one answer per number, packed into a list.
Here is the binary of each number from 0 to 5. The bit counts on the right are what we collect.
π’ Approach 1: Count Each Number From Scratch (Brute Force)
The idea in one line: count the 1 bits of every number on its own.
The idea:
- Loop every number from
0ton. - For each one, run a small inner loop that counts its
1bits. - Use the
n & (n - 1)trick, which clears the lowest1bit each pass.
How it works:
- Each clear removes one
1bit and adds one to that numberβs count. - Store the count at position
i.
Why it is weak:
- Every number starts counting from zero again.
- The inner loop runs once per
1bit, for every number. - Total work grows with
ntimes the bit width.
Here is the count-from-scratch code:
def count_bits(n): answer = [] for value in range(n + 1): count = 0 x = value while x: count += x & 1 x >>= 1 answer.append(count) return answerβ‘ Approach 2: Dynamic Programming Reuse (Best)
The idea in one line: solve each number once, then reuse that answer for bigger numbers.
The idea:
- This is dynamic programming, where we reuse finished answers instead of recomputing them.
- We go in order from small to large, so smaller answers are always ready.
How it works:
i & (i - 1)clears the lowest1bit ofi, giving a smaller number.- That smaller number is already counted.
ihas exactly one extra1bit, the one we cleared.- So
result[i] = result[i & (i - 1)] + 1.
Why it is fast:
- Each number costs one single step.
- No inner loop at all.
- The whole list is built in one clean pass.
Here is a picture of the recurrence for a few numbers. Each one points to the smaller number it reuses.
Steps to Solve
- Make a list of size
n + 1filled with zeros. - Set position
0to0, since zero has no1bits. - Loop
ifrom1up ton. - Set
result[i]toresult[i & (i - 1)] + 1. - Each step reuses an already-counted smaller number.
- Return the finished list.
This Python version builds a list with the dynamic programming recurrence.
def count_bits(n): result = [0] * (n + 1) # all start at zero for i in range(1, n + 1): result[i] = result[i & (i - 1)] + 1 # reuse smaller answer return result
print(count_bits(5))The output of the above code will be:
[0, 1, 1, 2, 1, 2]Let us go through the Python version line by line and see why the formula holds.
def count_bits(n): result = [0] * (n + 1) for i in range(1, n + 1): result[i] = result[i & (i - 1)] + 1 return resultThe line result = [0] * (n + 1) makes a list with one slot for every number from 0 to n. We fill it with zeros. Position 0 is already correct, because the number zero has no 1 bits.
The line for i in range(1, n + 1): walks from 1 up to n. We start at 1 because 0 is already done. We go in order so that smaller answers are always ready before we need them.
The line result[i] = result[i & (i - 1)] + 1 is the whole solution. The piece i & (i - 1) clears the lowest 1 bit of i, giving a smaller number we have already counted. We add 1 because i has exactly one more 1 bit than that smaller number. So we reuse old work and add one.
The line return result hands back the full list of bit counts.
β±οΈ Time and Space Complexity
The count-each-number way runs a small loop for every number, so its time is about O(n times bit width). The dynamic programming way does just one step per number, because it reuses a smaller answer. So it is a clean O(n). Both store the output list, so memory is O(n). The reuse is what removes the inner loop and brings the time down.
| Approach | Time Complexity | Space Complexity |
|---|---|---|
| Count each number | O(n log n) | O(n) |
| Dynamic programming reuse | O(n) | O(n) |
Tip
The recurrence result[i] = result[i & (i - 1)] + 1 is the line to remember. Say why it works. The number i & (i - 1) is i with one fewer 1 bit, so adding one gives the count for i.
π§© Key Takeaways
- β The goal is a list of 1-bit counts for every number from 0 to n.
- β The slow way counts each number on its own from scratch.
- β The fast way reuses smaller answers, which is dynamic programming.
- β
The formula is
result[i] = result[i & (i - 1)] + 1. - β Reuse turns the work into a clean O(n) pass.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does the Counting Bits problem return?
Why: It returns a list where position i holds the number of 1 bits in the number i.
- 2
What does i & (i - 1) give you?
Why: It clears the lowest set bit, giving a smaller number whose count we already know.
- 3
Why is the dynamic programming version faster?
Why: Each number's count is found in one step by reusing the count of a smaller number.
- 4
What is the time complexity of the dynamic programming approach?
Why: Each of the n numbers is handled in a single step, so the total time is O(n).