204 lines
9.3 KiB
YAML
204 lines
9.3 KiB
YAML
title: Reverse Bits
|
|
slug: reverse-bits
|
|
difficulty: easy
|
|
leetcode_id: 190
|
|
leetcode_url: https://leetcode.com/problems/reverse-bits/
|
|
categories:
|
|
- math
|
|
patterns:
|
|
- two-pointers
|
|
|
|
function_signature: "def reverse_bits(n: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { n: 43261596 }
|
|
expected: 964176192
|
|
- input: { n: 4294967293 }
|
|
expected: 3221225471
|
|
- input: { n: 1 }
|
|
expected: 2147483648
|
|
hidden:
|
|
- input: { n: 0 }
|
|
expected: 0
|
|
- input: { n: 2 }
|
|
expected: 1073741824
|
|
- input: { n: 4294967295 }
|
|
expected: 4294967295
|
|
|
|
description: |
|
|
Reverse bits of a given 32 bits unsigned integer.
|
|
|
|
**Note:**
|
|
|
|
- In some languages, such as Java, there is no unsigned integer type. In this case, both input and output will be given as a signed integer type. They should not affect your implementation, as the integer's internal binary representation is the same, whether it is signed or unsigned.
|
|
- In Java, the compiler represents the signed integers using 2's complement notation. Therefore, in **Example 2**, the input represents the signed integer `-3` and the output represents the signed integer `-1073741825`.
|
|
|
|
constraints: |
|
|
- The input must be a **binary string** of length `32`
|
|
|
|
examples:
|
|
- input: "n = 00000010100101000001111010011100"
|
|
output: "964176192 (00111001011110000010100101000000)"
|
|
explanation: "The input binary string 00000010100101000001111010011100 represents the unsigned integer 43261596, so return 964176192 which its binary representation is 00111001011110000010100101000000."
|
|
- input: "n = 11111111111111111111111111111101"
|
|
output: "3221225471 (10111111111111111111111111111111)"
|
|
explanation: "The input binary string 11111111111111111111111111111101 represents the unsigned integer 4294967293, so return 3221225471 which its binary representation is 10111111111111111111111111111111."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you have a string of 32 characters and you want to reverse it. The same concept applies to binary digits (bits) — we need to flip the order so the first bit becomes the last, the second becomes second-to-last, and so on.
|
|
|
|
Think of it like reading a word backwards: "hello" becomes "olleh". For bits, we're doing the same thing but with 1s and 0s.
|
|
|
|
The key insight is that we can build the reversed number **bit by bit**. As we extract each bit from the right side of the input (the least significant bit), we place it on the left side of our result (making it the most significant bit so far). By doing this 32 times, we've effectively reversed all bits.
|
|
|
|
Another way to visualise this: imagine two pointers — one at the rightmost bit of the input, one at the leftmost position of the output. We copy bits from input to output while moving both pointers towards each other.
|
|
|
|
approach: |
|
|
We solve this using a **Bit-by-Bit Extraction** approach:
|
|
|
|
**Step 1: Initialise the result**
|
|
|
|
- `result`: Set to `0` — this will hold our reversed bits
|
|
|
|
|
|
|
|
**Step 2: Process all 32 bits**
|
|
|
|
- Loop exactly 32 times (since we're working with 32-bit integers)
|
|
- In each iteration:
|
|
- **Left-shift the result** by 1 position to make room for the next bit
|
|
- **Extract the rightmost bit** of `n` using `n & 1`
|
|
- **Add this bit** to the result using OR: `result | (n & 1)`
|
|
- **Right-shift `n`** by 1 to move to the next bit
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- After 32 iterations, `result` contains all bits in reversed order
|
|
- Return `result`
|
|
|
|
|
|
|
|
This works because left-shifting the result before adding each new bit ensures that earlier-extracted bits (from the right of `n`) end up on the left of the result.
|
|
|
|
common_pitfalls:
|
|
- title: Processing Only Until n Becomes Zero
|
|
description: |
|
|
A tempting optimisation is to stop the loop when `n` becomes zero, since all remaining bits are 0.
|
|
|
|
However, this is **incorrect** because those trailing zeros in the input become leading zeros in the output, which affects the numerical value. For example, if the input has 4 significant bits, stopping early would only reverse those 4 bits instead of all 32.
|
|
|
|
Always process exactly 32 bits to ensure correctness.
|
|
wrong_approach: "while n > 0: process bits"
|
|
correct_approach: "for i in range(32): process bits"
|
|
|
|
- title: Forgetting the Unsigned Nature
|
|
description: |
|
|
In languages like Python, integers can be arbitrarily large, but we're specifically dealing with **32-bit unsigned integers**.
|
|
|
|
The result should be treated as unsigned, which means values can range from `0` to `2^32 - 1`. In some languages, you may need to handle sign extension or use unsigned types explicitly.
|
|
|
|
In Python, this isn't an issue since integers don't overflow, but be aware when translating to other languages.
|
|
wrong_approach: "Treating result as signed integer in languages with fixed-width types"
|
|
correct_approach: "Use unsigned integer types or handle masking appropriately"
|
|
|
|
- title: Confusing Bit Order
|
|
description: |
|
|
It's easy to get confused about which direction to shift. Remember:
|
|
|
|
- **Right-shift `n`** to access bits from right to left (LSB first)
|
|
- **Left-shift `result`** to place bits from left to right (making extracted bits the MSB)
|
|
|
|
If you shift in the wrong direction, you'll get incorrect results or infinite loops.
|
|
wrong_approach: "Left-shifting n or right-shifting result"
|
|
correct_approach: "Right-shift n to extract, left-shift result to place"
|
|
|
|
key_takeaways:
|
|
- "**Bit extraction pattern**: Use `n & 1` to get the rightmost bit, then right-shift to move to the next bit"
|
|
- "**Building numbers bit by bit**: Left-shift the result before adding each new bit to place bits in the correct position"
|
|
- "**Fixed iteration count**: When working with fixed-width integers, always process all bits — don't optimise by stopping early"
|
|
- "**Reversing as two-pointer analogy**: Think of it as two pointers moving in opposite directions — extracting from right, placing on left"
|
|
|
|
time_complexity: "O(1). We always perform exactly 32 iterations, regardless of the input value."
|
|
space_complexity: "O(1). We only use a single variable (`result`) to store the reversed bits."
|
|
|
|
solutions:
|
|
- approach_name: Bit-by-Bit Extraction
|
|
is_optimal: true
|
|
code: |
|
|
def reverse_bits(n: int) -> int:
|
|
result = 0
|
|
|
|
# Process all 32 bits
|
|
for _ in range(32):
|
|
# Make room for the next bit in result
|
|
result <<= 1
|
|
|
|
# Extract rightmost bit of n and add to result
|
|
result |= (n & 1)
|
|
|
|
# Move to the next bit in n
|
|
n >>= 1
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(1) — Fixed 32 iterations regardless of input.
|
|
|
|
**Space Complexity:** O(1) — Only one variable used.
|
|
|
|
We iterate through all 32 bits, extracting each bit from the right side of the input and placing it on the left side of the result. The left-shift before each addition ensures proper bit positioning.
|
|
|
|
- approach_name: Divide and Conquer (Byte Swap)
|
|
is_optimal: false
|
|
code: |
|
|
def reverse_bits(n: int) -> int:
|
|
# Swap adjacent single bits
|
|
n = ((n & 0x55555555) << 1) | ((n >> 1) & 0x55555555)
|
|
# Swap adjacent 2-bit pairs
|
|
n = ((n & 0x33333333) << 2) | ((n >> 2) & 0x33333333)
|
|
# Swap adjacent 4-bit nibbles
|
|
n = ((n & 0x0F0F0F0F) << 4) | ((n >> 4) & 0x0F0F0F0F)
|
|
# Swap adjacent bytes
|
|
n = ((n & 0x00FF00FF) << 8) | ((n >> 8) & 0x00FF00FF)
|
|
# Swap 2-byte halves
|
|
n = (n << 16) | (n >> 16)
|
|
|
|
# Mask to 32 bits (needed for Python)
|
|
return n & 0xFFFFFFFF
|
|
explanation: |
|
|
**Time Complexity:** O(1) — Fixed number of operations.
|
|
|
|
**Space Complexity:** O(1) — In-place bit manipulation.
|
|
|
|
This approach uses divide and conquer to reverse bits in log(32) = 5 steps. We first swap adjacent single bits, then adjacent pairs, then nibbles (4 bits), then bytes, and finally the two 16-bit halves.
|
|
|
|
While this has the same asymptotic complexity, it uses fewer operations (5 vs 32) and is more efficient in practice. The magic constants are bit masks that select alternating groups of bits.
|
|
|
|
- approach_name: Lookup Table (Follow-up Optimisation)
|
|
is_optimal: false
|
|
code: |
|
|
# Precompute reversed values for all bytes (0-255)
|
|
REVERSE_BYTE = [0] * 256
|
|
for i in range(256):
|
|
REVERSE_BYTE[i] = int(f'{i:08b}'[::-1], 2)
|
|
|
|
def reverse_bits(n: int) -> int:
|
|
# Reverse each byte and place in opposite position
|
|
return (
|
|
REVERSE_BYTE[n & 0xFF] << 24 |
|
|
REVERSE_BYTE[(n >> 8) & 0xFF] << 16 |
|
|
REVERSE_BYTE[(n >> 16) & 0xFF] << 8 |
|
|
REVERSE_BYTE[(n >> 24) & 0xFF]
|
|
)
|
|
explanation: |
|
|
**Time Complexity:** O(1) — Only 4 lookups and bit operations.
|
|
|
|
**Space Complexity:** O(1) — The lookup table is fixed at 256 entries.
|
|
|
|
This approach precomputes the reversed value of every possible byte (0-255). To reverse a 32-bit integer, we split it into 4 bytes, look up each reversed byte, and place them in reverse order.
|
|
|
|
This is ideal for the follow-up question about repeated calls — the precomputation is done once, and each call becomes extremely fast with just 4 table lookups.
|