{x}
blog image

Counting Bits

Given an integer n, return an array ans of length n + 1 such that for each i (0 <= i <= n), ans[i] is the number of 1's in the binary representation of i.

 

Example 1:

Input: n = 2
Output: [0,1,1]
Explanation:
0 --> 0
1 --> 1
2 --> 10

Example 2:

Input: n = 5
Output: [0,1,1,2,1,2]
Explanation:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101

 

Constraints:

  • 0 <= n <= 105

 

Follow up:

  • It is very easy to come up with a solution with a runtime of O(n log n). Can you do it in linear time O(n) and possibly in a single pass?
  • Can you do it without using any built-in function (i.e., like __builtin_popcount in C++)?

Solution Explanation and Code for Counting Bits

The problem asks to count the number of 1s in the binary representation of each integer from 0 to n. We'll explore two approaches: a straightforward method using built-in functions and a more optimized dynamic programming approach.

Approach 1: Using Built-in Functions (O(n log n) Time Complexity)

This approach leverages built-in functions to count set bits (1s) in the binary representation of each integer. The time complexity is dominated by the bit counting operation, which in many implementations takes O(log n) time for an integer n because it iterates through the bits of the number. Since this is done for n+1 numbers, the overall time complexity becomes O(n log n). The space complexity is O(n) due to the output array.

Code:

The code is straightforward across different languages. It iterates through numbers 0 to n, counts the set bits using the appropriate built-in function for each language, and stores the result in an array.

  • Python: i.bit_count()
  • Java: Integer.bitCount(i)
  • C++: __builtin_popcount(i)
  • Go: bits.OnesCount(uint(i)) (requires importing math/bits)
  • TypeScript: A custom bitCount function (shown below) is used since there's no direct built-in equivalent in JavaScript. This custom function is O(log n).

TypeScript bitCount function:

function bitCount(n: number): number {
    let count = 0;
    while (n) {
        n &= n - 1; // Clears the least significant set bit
        ++count;
    }
    return count;
}

Approach 2: Dynamic Programming (O(n) Time Complexity)

This approach utilizes dynamic programming to achieve linear time complexity. The core idea is based on the observation that the number of 1s in the binary representation of an integer i is related to the number of 1s in the binary representation of i/2 (integer division).

If i is even, then its binary representation is the same as i/2 with an extra 0 at the end. Therefore, the number of 1s in i is the same as in i/2.

If i is odd, then its binary representation is the same as i/2 with an extra 1 at the end. Therefore, the number of 1s in i is one more than in i/2.

This relationship allows us to build up the solution iteratively:

Code:

The code efficiently calculates the count of set bits using the above dynamic programming strategy. The time complexity is O(n) because we iterate through the numbers from 1 to n exactly once, and each operation within the loop is constant time. The space complexity remains O(n).

class Solution:
    def countBits(self, n: int) -> List[int]:
        ans = [0] * (n + 1)
        for i in range(1, n + 1):
            ans[i] = ans[i & (i - 1)] + 1 # The core DP step: i & (i-1) clears the least significant bit
        return ans
 

The Java, C++, Go, and TypeScript versions are almost identical, differing only in syntax. The i & (i - 1) operation efficiently removes the least significant set bit from i.

Time and Space Complexity Summary:

| Approach | Time Complexity | Space Complexity | |-----------------|-----------------|------------------| | Built-in Functions | O(n log n) | O(n) | | Dynamic Programming | O(n) | O(n) |

The dynamic programming approach is significantly more efficient for large values of n. The built-in functions approach is easier to read and understand, but less efficient. Choose the approach based on your priorities (code clarity vs. performance).