405. Convert a Number to Hexadecimal
Problem Description
This problem asks you to convert a 32-bit integer into its hexadecimal representation as a string.
The key requirements are:
-
Input: A 32-bit integer
num
(can be positive, negative, or zero) -
Output: A string representing the hexadecimal value of the number
-
Special handling for negative numbers: Use two's complement representation. This means negative numbers should be treated as their unsigned 32-bit equivalent. For example,
-1
in 32-bit two's complement is represented as0xFFFFFFFF
, which should return"ffffffff"
. -
Format requirements:
- All letters must be lowercase (
a-f
, notA-F
) - No leading zeros (except when the number itself is
0
, which should return"0"
) - Cannot use built-in library methods that directly convert to hexadecimal
- All letters must be lowercase (
The solution works by:
- Handling the special case of
0
directly - For other numbers, iterating through 8 hexadecimal digits (32 bits ÷ 4 bits per hex digit = 8 digits)
- Extracting each 4-bit group using bit shifting and masking with
0xF
(binary1111
) - Converting each 4-bit value (0-15) to its corresponding hex character (
0-9
ora-f
) - Building the result string while skipping leading zeros
The bit manipulation (num >> (4 * i)) & 0xF
extracts the i-th hexadecimal digit from the right, where each hex digit represents 4 bits of the original number.
Intuition
The key insight is recognizing that hexadecimal is base-16, where each digit represents exactly 4 bits of binary data. This natural alignment between hexadecimal and binary makes bit manipulation the perfect tool for this conversion.
Think about how we normally extract digits from a decimal number - we might use division and modulo operations. For hexadecimal, we can do something similar but with bit operations since we're working in powers of 2.
Since each hex digit represents 4 bits, a 32-bit integer contains exactly 8 hex digits (32 ÷ 4 = 8
). We can extract each group of 4 bits one at a time by:
- Shifting the bits to position the desired 4-bit group at the rightmost position
- Masking with
0xF
(binary1111
) to isolate just those 4 bits
For example, to get the second hex digit from the right, we shift right by 4 bits and mask: (num >> 4) & 0xF
.
The beauty of this approach is that it naturally handles negative numbers correctly. In two's complement representation, negative numbers have their high bits set to 1. When we treat the 32-bit pattern as unsigned and extract 4-bit chunks, we automatically get the correct hexadecimal representation without any special negative number handling.
Starting from the most significant digit (leftmost) and working right allows us to easily skip leading zeros - we simply don't start adding characters to our result until we encounter the first non-zero digit. This is why the loop goes from i = 7
down to 0
, processing from the most significant hex digit to the least significant.
The mapping from the extracted 4-bit value (0-15) to hex characters is straightforward using a lookup string '0123456789abcdef'
where the index corresponds to the decimal value.
Solution Approach
Let's walk through the implementation step by step:
1. Handle the base case:
if num == 0: return '0'
If the input is 0, we immediately return '0'
to avoid complications with leading zero handling.
2. Set up the hex character mapping:
chars = '0123456789abcdef'
This string serves as a lookup table where index i
gives us the hexadecimal character for value i
(0-15).
3. Initialize the result list:
s = []
We use a list to build our result string efficiently, as string concatenation in Python creates new string objects each time.
4. Extract hex digits from most significant to least significant:
for i in range(7, -1, -1):
x = (num >> (4 * i)) & 0xF
- We iterate from
i = 7
down toi = 0
, processing 8 hex digits total - For each iteration,
4 * i
calculates how many bits to shift right - When
i = 7
: shift right by 28 bits to get the most significant hex digit - When
i = 0
: shift right by 0 bits to get the least significant hex digit - The
& 0xF
operation masks the result to keep only the lowest 4 bits (values 0-15)
5. Skip leading zeros and build the result:
if s or x != 0: s.append(chars[x])
- The condition
s or x != 0
ensures we:- Skip leading zeros (when
s
is empty andx
is 0) - Include all digits once we've found the first non-zero digit (when
s
is non-empty) - This elegantly handles the leading zero requirement
- Skip leading zeros (when
6. Return the final string:
return ''.join(s)
Join all collected hex characters into a single string.
Example walkthrough with num = 26
:
- Binary:
00000000 00000000 00000000 00011010
- Iteration
i=7
:(26 >> 28) & 0xF = 0
→ skip (leading zero) - Iteration
i=6
:(26 >> 24) & 0xF = 0
→ skip (leading zero) - ...continue skipping zeros...
- Iteration
i=1
:(26 >> 4) & 0xF = 1
→ append'1'
- Iteration
i=0
:(26 >> 0) & 0xF = 10
→ append'a'
- Result:
"1a"
For negative numbers like num = -1
:
- In 32-bit two's complement:
11111111 11111111 11111111 11111111
- Each 4-bit group equals
1111
(15 in decimal, 'f' in hex) - Result:
"ffffffff"
The algorithm runs in O(1) time complexity since we always process exactly 8 hex digits, and O(1) space complexity for the result string.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through converting num = 170
to hexadecimal using our bit manipulation approach.
Step 1: Binary representation
170
in binary:00000000 00000000 00000000 10101010
- Expected hex result:
"aa"
(since10101010
=0xAA
)
Step 2: Initialize
chars = '0123456789abcdef'
(our lookup table)s = []
(empty result list)
Step 3: Extract each hex digit (from most to least significant)
-
i = 7:
(170 >> 28) & 0xF
- Shift:
170 >> 28
=0
- Mask:
0 & 0xF
=0
- Action: Skip (leading zero,
s
is empty)
- Shift:
-
i = 6:
(170 >> 24) & 0xF
- Shift:
170 >> 24
=0
- Mask:
0 & 0xF
=0
- Action: Skip (leading zero,
s
is empty)
- Shift:
-
i = 5, 4, 3, 2: All produce
0
- Action: Skip all (leading zeros)
-
i = 1:
(170 >> 4) & 0xF
- Shift:
170 >> 4
=10
(shifts out rightmost 4 bits:1010
) - Mask:
10 & 0xF
=10
(decimal) - Action: Append
chars[10]
='a'
tos
s = ['a']
- Shift:
-
i = 0:
(170 >> 0) & 0xF
- Shift:
170 >> 0
=170
(no shift) - Mask:
170 & 0xF
=10
(keeps only last 4 bits:1010
) - Action: Append
chars[10]
='a'
tos
s = ['a', 'a']
- Shift:
Step 4: Join and return
''.join(s)
="aa"
The algorithm correctly extracts the two hex digits A
and A
(lowercase as required) from the binary representation, skipping all leading zeros to produce the final result "aa"
.
Solution Implementation
1class Solution:
2 def toHex(self, num: int) -> str:
3 """
4 Convert a 32-bit integer to its hexadecimal representation.
5 Negative numbers are represented using two's complement.
6
7 Args:
8 num: Integer to convert (-2^31 <= num <= 2^31 - 1)
9
10 Returns:
11 Hexadecimal string representation of the number
12 """
13 # Handle the special case of zero
14 if num == 0:
15 return '0'
16
17 # Define hexadecimal characters mapping
18 hex_chars = '0123456789abcdef'
19
20 # List to store hexadecimal digits
21 result = []
22
23 # Process 8 hexadecimal digits (32 bits / 4 bits per hex digit = 8 digits)
24 # Start from the most significant digit
25 for position in range(7, -1, -1):
26 # Extract 4 bits at the current position
27 # Shift right by (4 * position) bits and mask with 0xF to get 4 bits
28 hex_digit_value = (num >> (4 * position)) & 0xF
29
30 # Only append non-zero digits or if we've already started building the result
31 # This avoids leading zeros
32 if result or hex_digit_value != 0:
33 result.append(hex_chars[hex_digit_value])
34
35 # Join all hexadecimal digits into a string
36 return ''.join(result)
37
1class Solution {
2 /**
3 * Converts a 32-bit integer to its hexadecimal representation.
4 * Handles both positive and negative numbers (negative numbers use two's complement).
5 *
6 * @param num The integer to convert to hexadecimal
7 * @return The hexadecimal string representation
8 */
9 public String toHex(int num) {
10 // Handle special case: zero
11 if (num == 0) {
12 return "0";
13 }
14
15 // StringBuilder to accumulate hexadecimal digits
16 StringBuilder hexBuilder = new StringBuilder();
17
18 // Process the number 4 bits at a time (one hex digit = 4 bits)
19 while (num != 0) {
20 // Extract the rightmost 4 bits (values 0-15)
21 int hexDigit = num & 0xF; // 0xF = 15 in binary: 1111
22
23 // Convert to appropriate character
24 if (hexDigit < 10) {
25 // For values 0-9, append the digit directly
26 hexBuilder.append(hexDigit);
27 } else {
28 // For values 10-15, convert to 'a'-'f'
29 // 10 -> 'a', 11 -> 'b', ..., 15 -> 'f'
30 char hexChar = (char) (hexDigit - 10 + 'a');
31 hexBuilder.append(hexChar);
32 }
33
34 // Logical right shift by 4 bits to process next hex digit
35 // Using >>> ensures zero-fill for negative numbers
36 num >>>= 4;
37 }
38
39 // Reverse the string since we built it from least to most significant digit
40 return hexBuilder.reverse().toString();
41 }
42}
43
1class Solution {
2public:
3 string toHex(int num) {
4 // Handle special case: 0 converts to "0"
5 if (num == 0) {
6 return "0";
7 }
8
9 string hexResult = "";
10
11 // Process 8 nibbles (4 bits each) from most significant to least significant
12 // 32-bit integer = 8 nibbles × 4 bits
13 for (int nibbleIndex = 7; nibbleIndex >= 0; --nibbleIndex) {
14 // Extract the current nibble by shifting right and masking with 0xF (15 in decimal)
15 // Shift amount: nibbleIndex * 4 positions to bring target nibble to rightmost position
16 int nibbleValue = (num >> (4 * nibbleIndex)) & 0xF;
17
18 // Skip leading zeros (only add to result if we've started building the string
19 // or current nibble is non-zero)
20 if (hexResult.size() > 0 || nibbleValue != 0) {
21 // Convert nibble value to hex character
22 // 0-9 maps to '0'-'9', 10-15 maps to 'a'-'f'
23 char hexChar;
24 if (nibbleValue < 10) {
25 hexChar = static_cast<char>(nibbleValue + '0');
26 } else {
27 hexChar = static_cast<char>(nibbleValue - 10 + 'a');
28 }
29
30 hexResult += hexChar;
31 }
32 }
33
34 return hexResult;
35 }
36};
37
1function toHex(num: number): string {
2 // Handle special case: 0 converts to "0"
3 if (num === 0) {
4 return "0";
5 }
6
7 let hexResult: string = "";
8
9 // Process 8 nibbles (4 bits each) from most significant to least significant
10 // 32-bit integer = 8 nibbles × 4 bits
11 for (let nibbleIndex = 7; nibbleIndex >= 0; nibbleIndex--) {
12 // Extract the current nibble by shifting right and masking with 0xF (15 in decimal)
13 // Shift amount: nibbleIndex * 4 positions to bring target nibble to rightmost position
14 // Use >>> for unsigned right shift to handle negative numbers correctly
15 const nibbleValue: number = (num >>> (4 * nibbleIndex)) & 0xF;
16
17 // Skip leading zeros (only add to result if we've started building the string
18 // or current nibble is non-zero)
19 if (hexResult.length > 0 || nibbleValue !== 0) {
20 // Convert nibble value to hex character
21 // 0-9 maps to '0'-'9', 10-15 maps to 'a'-'f'
22 let hexChar: string;
23 if (nibbleValue < 10) {
24 hexChar = String.fromCharCode(nibbleValue + '0'.charCodeAt(0));
25 } else {
26 hexChar = String.fromCharCode(nibbleValue - 10 + 'a'.charCodeAt(0));
27 }
28
29 hexResult += hexChar;
30 }
31 }
32
33 return hexResult;
34}
35
Time and Space Complexity
Time Complexity: O(1)
The algorithm iterates through a fixed loop of 8 iterations (from i = 7
to i = 0
). In each iteration, it performs constant-time operations:
- Right shift operation:
num >> (4 * i)
-O(1)
- Bitwise AND operation:
& 0xF
-O(1)
- Array/string indexing:
chars[x]
-O(1)
- List append operation:
s.append()
-O(1)
amortized
Since the loop runs exactly 8 times regardless of the input value, the overall time complexity is O(8)
= O(1)
.
Space Complexity: O(1)
The algorithm uses:
- A fixed string
chars
of length 16 -O(1)
- A list
s
that stores at most 8 characters (since we process 8 hexadecimal digits) -O(1)
- A few integer variables (
i
,x
) -O(1)
The maximum space used is bounded by a constant (the output string can have at most 8 characters for a 32-bit integer), making the space complexity O(1)
.
Common Pitfalls
1. Incorrect Handling of Negative Numbers in Different Programming Languages
One of the most common pitfalls occurs when implementing this solution in languages that handle integer bit operations differently, particularly with negative numbers.
The Problem: In Python 3, integers have arbitrary precision, meaning negative numbers don't automatically wrap around to their 32-bit two's complement representation. When you perform bit operations on negative numbers in Python, they maintain their sign through an infinite series of leading 1s.
For example, in Python:
-1 >> 4
results in-1
(not0x0FFFFFFF
as you might expect)- The bit pattern extends infinitely:
...11111111111111111111111111111111
Why This Breaks:
# Incorrect approach that fails for negative numbers
def toHex_wrong(num):
if num == 0:
return '0'
hex_chars = '0123456789abcdef'
result = []
# This will create an infinite loop for negative numbers!
while num != 0:
result.append(hex_chars[num & 0xF])
num >>= 4 # For negative numbers, this never becomes 0
return ''.join(reversed(result))
The Solution: The provided solution cleverly avoids this issue by:
- Using a fixed loop of exactly 8 iterations (not a while loop)
- Relying on Python's automatic handling of the
& 0xF
operation, which correctly extracts 4 bits regardless of sign
However, if you need to explicitly handle the two's complement conversion, you can:
# Alternative approach: Convert negative to unsigned 32-bit if num < 0: num = (1 << 32) + num # Convert to unsigned 32-bit representation
2. Off-by-One Errors in Bit Shifting
The Problem: It's easy to make mistakes with the bit shift calculations, especially when determining which bits to extract.
Common Mistake:
# Wrong: This extracts bits in the wrong order
for i in range(8): # Going from 0 to 7
x = (num >> (4 * i)) & 0xF # Extracts from least to most significant
if result or x != 0:
result.append(hex_chars[x])
# This would produce "a1" instead of "1a" for num=26
The Solution: Always remember that for proper hexadecimal representation:
- We need to extract from most significant to least significant digit
- Use
range(7, -1, -1)
to iterate from 7 down to 0 - The shift amount should be
4 * i
where largeri
values correspond to more significant digits
3. Forgetting Edge Cases
The Problem: Failing to handle special cases properly, particularly:
- Zero (returning empty string or "00000000")
- Maximum negative number (-2^31)
- Small positive numbers (returning with leading zeros)
The Solution: Always include explicit handling:
# Correct: Handle zero explicitly if num == 0: return '0' # Correct: Skip leading zeros but include all digits once we start if result or hex_digit_value != 0: result.append(hex_chars[hex_digit_value])
Which of the following uses divide and conquer strategy?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!