421. Maximum XOR of Two Numbers in an Array
Problem Description
You are given an integer array nums
. Your task is to find the maximum XOR (exclusive OR) value that can be obtained by selecting any two elements from the array.
Specifically, you need to calculate nums[i] XOR nums[j]
for all valid pairs where 0 <= i <= j < n
(where n
is the length of the array), and return the maximum value among all these XOR operations.
The XOR operation between two numbers compares their binary representations bit by bit - it returns 1 when the bits are different and 0 when they are the same.
For example, if nums = [3, 10, 5, 25, 2, 8]
, you would compute XOR for all possible pairs:
3 XOR 3 = 0
3 XOR 10 = 9
3 XOR 5 = 6
- ... and so on
The maximum XOR value from all these pairs would be your answer.
The solution uses a Trie (prefix tree) data structure to efficiently find the maximum XOR. The Trie stores the binary representation of each number, allowing us to greedily select the opposite bit at each position when searching for the number that would produce the maximum XOR with a given number. This approach processes each bit from the most significant to the least significant (bits 30 to 0 for 32-bit integers), trying to maximize the XOR result by choosing the opposite bit whenever possible.
Intuition
To maximize the XOR between two numbers, we need to understand what makes XOR large. XOR returns 1 when bits are different and 0 when they're the same. Therefore, to maximize the XOR result, we want to find pairs of numbers whose bits differ as much as possible, especially in the higher-order (more significant) bit positions since they contribute more to the final value.
The key insight is that for any given number, we want to find another number in the array that has opposite bits in as many positions as possible, starting from the most significant bit. For instance, if we have a number whose binary representation starts with 101...
, we'd ideally want to find a number that starts with 010...
to maximize the XOR.
A brute force approach would compare every pair, taking O(nΒ²)
time. However, we can optimize this using a Trie structure to store binary representations of all numbers. The Trie allows us to make greedy choices bit by bit.
Here's the clever part: when we insert numbers into the Trie, we're essentially creating paths based on their binary representation. Each node has at most two children (0 or 1). When searching for the maximum XOR partner for a number, we traverse the Trie and at each level (representing each bit position from high to low), we try to go down the opposite branch if it exists. If the current bit is 0, we prefer to go down the 1-branch, and vice versa.
For example, if we're at a bit position where our number has a 1, and the Trie has a branch for 0 at that position, we take it because 1 XOR 0 = 1
, contributing to a larger result. If the opposite branch doesn't exist, we have no choice but to follow the same bit branch, resulting in 1 XOR 1 = 0
or 0 XOR 0 = 0
for that position.
This greedy approach works because we process bits from most significant to least significant, and maximizing higher-order bits always leads to a larger number regardless of the lower-order bits. The time complexity becomes O(n * 31)
for insertion and searching, where 31 is the number of bits we consider for each integer.
Solution Approach
The solution implements a binary Trie (prefix tree) to efficiently find the maximum XOR value. Let's walk through the implementation step by step:
Trie Data Structure
The Trie
class has a children
attribute that stores exactly two possible children (for bits 0 and 1):
children[0]
: represents the path when the current bit is 0children[1]
: represents the path when the current bit is 1
Insert Operation
The insert
method adds a number to the Trie by processing its binary representation:
def insert(self, x: int):
node = self
for i in range(30, -1, -1):
v = x >> i & 1
if node.children[v] is None:
node.children[v] = Trie()
node = node.children[v]
- We iterate from bit position 30 down to 0 (covering 31 bits total)
- For each bit position
i
, we extract the bit usingx >> i & 1
x >> i
shifts the number right byi
positions& 1
extracts the least significant bit
- If the child node for this bit doesn't exist, we create it
- We then move to that child node and continue
Search Operation
The search
method finds the maximum XOR value for a given number against all numbers in the Trie:
def search(self, x: int) -> int:
node = self
ans = 0
for i in range(30, -1, -1):
v = x >> i & 1
if node.children[v ^ 1]:
ans |= 1 << i
node = node.children[v ^ 1]
else:
node = node.children[v]
return ans
- Again, we process bits from position 30 down to 0
- For each bit
v
of the input number, we check if the opposite bitv ^ 1
exists in the Trie- If
v = 0
, thenv ^ 1 = 1
(we want to find 1) - If
v = 1
, thenv ^ 1 = 0
(we want to find 0)
- If
- If the opposite bit exists:
- We set the corresponding bit in our answer using
ans |= 1 << i
- We traverse to the opposite branch
- We set the corresponding bit in our answer using
- If the opposite bit doesn't exist:
- We're forced to take the same bit path (contributing 0 to XOR)
- We traverse to the available branch
Main Solution
The findMaximumXOR
method orchestrates the entire process:
def findMaximumXOR(self, nums: List[int]) -> int:
trie = Trie()
for x in nums:
trie.insert(x)
return max(trie.search(x) for x in nums)
- First, we build the Trie by inserting all numbers from the array
- Then, for each number in the array, we search for its maximum XOR partner
- We return the maximum XOR value found across all numbers
The time complexity is O(n * 31)
for both building the Trie and searching, where n
is the number of elements. The space complexity is O(n * 31)
in the worst case for storing the Trie structure.
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 a small example with nums = [3, 10, 5]
to illustrate how the Trie-based solution finds the maximum XOR.
Step 1: Convert numbers to binary (5-bit representation for simplicity)
- 3 = 00011
- 10 = 01010
- 5 = 00101
Step 2: Build the Trie by inserting all numbers
Inserting 3 (00011):
root | [0] | [0] | [0] | [1] | [1]
Inserting 10 (01010):
root / \ [0] [0] | | [0] [1] | | [0] [0] | | [1] [1] | | [1] [0]
Inserting 5 (00101):
root / \ [0] [0] | | [0] [1] | / \ [0] [0] [1] | | | [1] [1] [0] | | | [1] [0] [1]
Step 3: Search for maximum XOR for each number
Searching for 3 (00011):
- Bit 4: Current bit is 0, want 1 β 1 doesn't exist, must take 0 β XOR bit = 0
- Bit 3: Current bit is 0, want 1 β 1 doesn't exist, must take 0 β XOR bit = 0
- Bit 2: Current bit is 0, want 1 β 1 exists! Take it β XOR bit = 1
- Bit 1: Current bit is 1, want 0 β 0 exists! Take it β XOR bit = 1
- Bit 0: Current bit is 1, want 0 β 0 doesn't exist, must take 1 β XOR bit = 0
Result: 00110 = 6 (this is 3 XOR 5)
Searching for 10 (01010):
- Bit 4: Current bit is 0, want 1 β 1 doesn't exist, must take 0 β XOR bit = 0
- Bit 3: Current bit is 1, want 0 β 0 exists! Take it β XOR bit = 1
- Bit 2: Current bit is 0, want 1 β 1 exists! Take it β XOR bit = 1
- Bit 1: Current bit is 1, want 0 β 0 exists! Take it β XOR bit = 1
- Bit 0: Current bit is 0, want 1 β 1 exists! Take it β XOR bit = 1
Result: 01111 = 15 (this is 10 XOR 5)
Searching for 5 (00101):
- Bit 4: Current bit is 0, want 1 β 1 doesn't exist, must take 0 β XOR bit = 0
- Bit 3: Current bit is 0, want 1 β 1 doesn't exist, must take 0 β XOR bit = 0
- Bit 2: Current bit is 1, want 0 β 0 exists! Take it β XOR bit = 1
- Bit 1: Current bit is 0, want 1 β 1 exists! Take it β XOR bit = 1
- Bit 0: Current bit is 1, want 0 β 0 exists! Take it β XOR bit = 1
Result: 00111 = 7 (this is 5 XOR 3 or 5 XOR 10? Actually checking: 5 XOR 10 = 15, 5 XOR 3 = 6, so this path gives us 5 XOR 10 = 15)
Step 4: Return maximum Maximum XOR = max(6, 15, 15) = 15
The answer is 15, which corresponds to 10 XOR 5.
Solution Implementation
1from typing import List, Optional
2
3
4class Trie:
5 """Binary Trie for storing integers and finding maximum XOR."""
6
7 __slots__ = ("children",)
8
9 def __init__(self):
10 # Each node has two children: 0 and 1 for binary representation
11 self.children: List[Optional['Trie']] = [None, None]
12
13 def insert(self, num: int) -> None:
14 """
15 Insert a number into the trie by its binary representation.
16
17 Args:
18 num: The integer to insert into the trie
19 """
20 current_node = self
21
22 # Process bits from most significant to least significant (31 bits for int32)
23 for bit_position in range(30, -1, -1):
24 # Extract the bit at current position (0 or 1)
25 bit_value = (num >> bit_position) & 1
26
27 # Create new node if path doesn't exist
28 if current_node.children[bit_value] is None:
29 current_node.children[bit_value] = Trie()
30
31 # Move to the next node
32 current_node = current_node.children[bit_value]
33
34 def search(self, num: int) -> int:
35 """
36 Find the maximum XOR value for the given number against all stored numbers.
37
38 Args:
39 num: The integer to find maximum XOR for
40
41 Returns:
42 The maximum XOR value possible with stored numbers
43 """
44 current_node = self
45 max_xor = 0
46
47 # Process bits from most significant to least significant
48 for bit_position in range(30, -1, -1):
49 # Extract the bit at current position
50 bit_value = (num >> bit_position) & 1
51
52 # Try to go opposite direction for maximum XOR
53 # XOR is maximized when bits are different (0^1=1, 1^0=1)
54 opposite_bit = bit_value ^ 1
55
56 if current_node.children[opposite_bit] is not None:
57 # If opposite bit path exists, take it and set this bit in result
58 max_xor |= (1 << bit_position)
59 current_node = current_node.children[opposite_bit]
60 else:
61 # Otherwise, follow the same bit path
62 current_node = current_node.children[bit_value]
63
64 return max_xor
65
66
67class Solution:
68 def findMaximumXOR(self, nums: List[int]) -> int:
69 """
70 Find the maximum XOR of two numbers in the array.
71
72 Args:
73 nums: List of non-negative integers
74
75 Returns:
76 Maximum XOR value of any two numbers in the array
77 """
78 # Build trie with all numbers
79 trie = Trie()
80 for num in nums:
81 trie.insert(num)
82
83 # Find maximum XOR for each number against all others
84 return max(trie.search(num) for num in nums)
85
1/**
2 * Binary Trie data structure for finding maximum XOR value
3 * Each node has at most 2 children (0 and 1) representing binary digits
4 */
5class Trie {
6 // Array to store child nodes: children[0] for bit 0, children[1] for bit 1
7 private Trie[] children = new Trie[2];
8
9 /**
10 * Constructor for Trie node
11 */
12 public Trie() {
13 }
14
15 /**
16 * Inserts a number into the trie by its binary representation
17 * @param x The number to insert
18 */
19 public void insert(int x) {
20 Trie currentNode = this;
21
22 // Process each bit from most significant (30th) to least significant (0th)
23 // Using 30 as starting point since we're dealing with 31-bit positive integers
24 for (int bitPosition = 30; bitPosition >= 0; --bitPosition) {
25 // Extract the bit at current position (0 or 1)
26 int bitValue = (x >> bitPosition) & 1;
27
28 // Create new node if path doesn't exist
29 if (currentNode.children[bitValue] == null) {
30 currentNode.children[bitValue] = new Trie();
31 }
32
33 // Move to the child node
34 currentNode = currentNode.children[bitValue];
35 }
36 }
37
38 /**
39 * Searches for the maximum XOR value with the given number
40 * @param x The number to find maximum XOR with
41 * @return The maximum XOR value possible with x and any number in the trie
42 */
43 public int search(int x) {
44 Trie currentNode = this;
45 int maxXorValue = 0;
46
47 // Process each bit from most significant to least significant
48 for (int bitPosition = 30; bitPosition >= 0; --bitPosition) {
49 // Extract the bit at current position
50 int bitValue = (x >> bitPosition) & 1;
51
52 // Try to go opposite direction for maximum XOR
53 // XOR is maximized when bits are different (0^1=1, 1^0=1)
54 int oppositeBit = bitValue ^ 1;
55
56 if (currentNode.children[oppositeBit] != null) {
57 // Opposite bit exists, add 1 to this bit position in result
58 maxXorValue |= (1 << bitPosition);
59 currentNode = currentNode.children[oppositeBit];
60 } else {
61 // Opposite bit doesn't exist, follow the same bit path
62 currentNode = currentNode.children[bitValue];
63 }
64 }
65
66 return maxXorValue;
67 }
68}
69
70/**
71 * Solution class for LeetCode problem: Maximum XOR of Two Numbers in an Array
72 */
73class Solution {
74 /**
75 * Finds the maximum XOR of any two numbers in the array
76 * @param nums Array of non-negative integers
77 * @return Maximum XOR value of any two numbers in the array
78 */
79 public int findMaximumXOR(int[] nums) {
80 // Initialize trie data structure
81 Trie trie = new Trie();
82 int maximumXor = 0;
83
84 // For each number in the array
85 for (int currentNum : nums) {
86 // Insert current number into trie
87 trie.insert(currentNum);
88
89 // Find maximum XOR with current number and all previously inserted numbers
90 // Update global maximum if a larger XOR is found
91 maximumXor = Math.max(maximumXor, trie.search(currentNum));
92 }
93
94 return maximumXor;
95 }
96}
97
1class Trie {
2public:
3 // Binary trie with two children (0 and 1)
4 Trie* children[2];
5
6 // Constructor initializes both children to nullptr
7 Trie() : children{nullptr, nullptr} {}
8
9 // Insert a number into the trie by its binary representation
10 void insert(int num) {
11 Trie* currentNode = this;
12
13 // Process bits from most significant (bit 30) to least significant (bit 0)
14 for (int bitPosition = 30; bitPosition >= 0; --bitPosition) {
15 // Extract the bit at current position (0 or 1)
16 int bitValue = (num >> bitPosition) & 1;
17
18 // Create new node if path doesn't exist
19 if (!currentNode->children[bitValue]) {
20 currentNode->children[bitValue] = new Trie();
21 }
22
23 // Move to the next node
24 currentNode = currentNode->children[bitValue];
25 }
26 }
27
28 // Find maximum XOR value with given number among all inserted numbers
29 int search(int num) {
30 Trie* currentNode = this;
31 int maxXorValue = 0;
32
33 // Process bits from most significant to least significant
34 for (int bitPosition = 30; bitPosition >= 0; --bitPosition) {
35 // Extract the bit at current position
36 int bitValue = (num >> bitPosition) & 1;
37
38 // Try to go opposite direction (XOR with 1 gives opposite bit)
39 // This maximizes the XOR result
40 if (currentNode->children[bitValue ^ 1]) {
41 // Set the corresponding bit in result
42 maxXorValue |= (1 << bitPosition);
43 // Move to the opposite bit path
44 currentNode = currentNode->children[bitValue ^ 1];
45 } else {
46 // If opposite path doesn't exist, follow the same bit path
47 currentNode = currentNode->children[bitValue];
48 }
49 }
50
51 return maxXorValue;
52 }
53};
54
55class Solution {
56public:
57 // Find the maximum XOR of any two numbers in the array
58 int findMaximumXOR(vector<int>& nums) {
59 // Initialize the trie data structure
60 Trie* trie = new Trie();
61 int maxXorResult = 0;
62
63 // For each number, insert it into trie and find max XOR with existing numbers
64 for (int currentNum : nums) {
65 trie->insert(currentNum);
66 // Update maximum XOR value found so far
67 maxXorResult = max(maxXorResult, trie->search(currentNum));
68 }
69
70 return maxXorResult;
71 }
72};
73
1// Binary trie node with two children (0 and 1)
2interface TrieNode {
3 children: [TrieNode | null, TrieNode | null];
4}
5
6// Create a new trie node with both children initialized to null
7function createTrieNode(): TrieNode {
8 return {
9 children: [null, null]
10 };
11}
12
13// Insert a number into the trie by its binary representation
14function insert(root: TrieNode, num: number): void {
15 let currentNode: TrieNode = root;
16
17 // Process bits from most significant (bit 30) to least significant (bit 0)
18 for (let bitPosition = 30; bitPosition >= 0; bitPosition--) {
19 // Extract the bit at current position (0 or 1)
20 const bitValue: number = (num >> bitPosition) & 1;
21
22 // Create new node if path doesn't exist
23 if (!currentNode.children[bitValue]) {
24 currentNode.children[bitValue] = createTrieNode();
25 }
26
27 // Move to the next node
28 currentNode = currentNode.children[bitValue]!;
29 }
30}
31
32// Find maximum XOR value with given number among all inserted numbers
33function search(root: TrieNode, num: number): number {
34 let currentNode: TrieNode = root;
35 let maxXorValue: number = 0;
36
37 // Process bits from most significant to least significant
38 for (let bitPosition = 30; bitPosition >= 0; bitPosition--) {
39 // Extract the bit at current position
40 const bitValue: number = (num >> bitPosition) & 1;
41
42 // Try to go opposite direction (XOR with 1 gives opposite bit)
43 // This maximizes the XOR result
44 if (currentNode.children[bitValue ^ 1]) {
45 // Set the corresponding bit in result
46 maxXorValue |= (1 << bitPosition);
47 // Move to the opposite bit path
48 currentNode = currentNode.children[bitValue ^ 1]!;
49 } else {
50 // If opposite path doesn't exist, follow the same bit path
51 currentNode = currentNode.children[bitValue]!;
52 }
53 }
54
55 return maxXorValue;
56}
57
58// Find the maximum XOR of any two numbers in the array
59function findMaximumXOR(nums: number[]): number {
60 // Initialize the trie data structure
61 const trie: TrieNode = createTrieNode();
62 let maxXorResult: number = 0;
63
64 // For each number, insert it into trie and find max XOR with existing numbers
65 for (const currentNum of nums) {
66 insert(trie, currentNum);
67 // Update maximum XOR value found so far
68 maxXorResult = Math.max(maxXorResult, search(trie, currentNum));
69 }
70
71 return maxXorResult;
72}
73
Time and Space Complexity
Time Complexity: O(n * L)
where n
is the number of elements in the input array and L
is the number of bits (31 bits for integers up to 2^31).
-
Building the Trie: The
insert
method is calledn
times, and each insertion traverses through 31 bits (from bit position 30 to 0), performing constant-time operations at each level. This gives usO(n * 31) = O(n * L)
. -
Finding maximum XOR: The
search
method is calledn
times (once for each number), and each search traverses through 31 bits, performing constant-time operations at each level. This also gives usO(n * 31) = O(n * L)
. -
Total time complexity:
O(n * L) + O(n * L) = O(n * L)
, which simplifies toO(31n) = O(n)
since 31 is a constant.
Space Complexity: O(n * L)
where n
is the number of elements and L
is the number of bits (31 bits).
-
In the worst case, each number in the array could have a completely unique bit pattern, requiring a unique path in the Trie.
-
Each number requires up to 31 nodes to be stored (one for each bit position from 30 to 0).
-
With
n
numbers and potentially 31 nodes per number, the space complexity isO(n * 31) = O(n * L)
. -
Since 31 is a constant, this can be simplified to
O(n)
in terms of the input size.
Common Pitfalls
1. Incorrect Bit Range Handling
Pitfall: A common mistake is using an incorrect range for bit positions. Developers might use range(31, -1, -1)
thinking they need to process all 32 bits, but this can cause issues when dealing with negative numbers or when the constraint specifies non-negative integers only.
Why it happens: The problem states we're dealing with non-negative integers, and the maximum value in typical constraints is often less than 2^31. Using bit position 31 would include the sign bit in signed integer representation, which can lead to unexpected behavior.
Solution: The code correctly uses range(30, -1, -1)
to process 31 bits (positions 0-30), which is sufficient for positive integers up to 2^31 - 1.
2. Forgetting to Handle Missing Opposite Bit Path
Pitfall: When searching for the maximum XOR, forgetting to handle the case where the opposite bit path doesn't exist in the Trie:
# WRONG - This will crash with NoneType error
def search(self, num: int) -> int:
current_node = self
max_xor = 0
for bit_position in range(30, -1, -1):
bit_value = (num >> bit_position) & 1
opposite_bit = bit_value ^ 1
# Always trying to go opposite without checking
max_xor |= (1 << bit_position)
current_node = current_node.children[opposite_bit] # Can be None!
return max_xor
Solution: Always check if the opposite path exists before traversing:
if current_node.children[opposite_bit] is not None: max_xor |= (1 << bit_position) current_node = current_node.children[opposite_bit] else: current_node = current_node.children[bit_value]
3. Incorrect XOR Result Accumulation
Pitfall: Incorrectly updating the XOR result when the opposite bit is found:
# WRONG - Adding instead of OR-ing the bit
max_xor += (1 << bit_position)
# WRONG - Always setting the bit regardless of path taken
for bit_position in range(30, -1, -1):
bit_value = (num >> bit_position) & 1
max_xor |= (1 << bit_position) # Wrong! Only set when opposite bit found
Solution: Only set the bit in the result when we successfully take the opposite path:
if current_node.children[opposite_bit] is not None: max_xor |= (1 << bit_position) # Set bit only when XOR would be 1
4. Edge Case: Single Element Array
Pitfall: Not considering that with a single element, the only valid pair is the element with itself, which always gives XOR = 0:
# Potential issue if not handled properly nums = [5] # XOR of 5 with itself is 0, not 5
Solution: The current implementation handles this correctly because:
- The Trie will contain only one number
- When searching, it will follow the exact same path, resulting in XOR = 0
- The logic naturally handles this without special casing
5. Memory Optimization Oversight
Pitfall: Using a full class with unnecessary overhead for Trie nodes:
# Less efficient
class Trie:
def __init__(self):
self.children = [None, None]
self.is_end = False # Not needed for this problem!
self.value = None # Not needed!
Solution: The implementation correctly uses __slots__
and only stores the essential children
array:
class Trie:
__slots__ = ("children",) # Memory optimization
def __init__(self):
self.children = [None, None] # Only what's needed
Which of the following uses divide and conquer strategy?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
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!