LeetCode 421 - Maximum XOR of Two Numbers in an Array
The problem gives us an array of non-negative integers called nums. We must choose two indices i and j such that 0 <= i <= j < n, then compute: The goal is to return the largest XOR value that can be produced from any pair in the array.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, Bit Manipulation, Trie
Solution
LeetCode 421 - Maximum XOR of Two Numbers in an Array
Problem Understanding
The problem gives us an array of non-negative integers called nums. We must choose two indices i and j such that 0 <= i <= j < n, then compute:
nums[i] XOR nums[j]
The goal is to return the largest XOR value that can be produced from any pair in the array.
The XOR operation compares bits position by position:
0 XOR 0 = 01 XOR 1 = 00 XOR 1 = 11 XOR 0 = 1
This means XOR becomes large when two numbers differ at high bit positions. For example:
5 = 00101
25 = 11001
XOR= 11100 = 28
The input array can contain up to 2 * 10^5 numbers, and each number can be as large as 2^31 - 1. These constraints are important because they immediately rule out slow quadratic solutions for large inputs.
A naive solution that checks every pair would require:
O(n^2)
comparisons, which becomes too expensive when n = 200000.
The problem guarantees:
- At least one number exists in the array
- All numbers are non-negative
- Values fit within 31 bits
Important edge cases include arrays with only one element, arrays where all numbers are identical, arrays containing zero, and arrays where the maximum XOR depends on differing high-order bits rather than low-order bits.
Approaches
Brute Force Approach
The most direct solution is to try every possible pair of numbers.
For every index i, we iterate through every index j >= i, compute:
nums[i] XOR nums[j]
and track the maximum result seen so far.
This works because it exhaustively checks all valid combinations, so the largest XOR value cannot be missed.
However, the time complexity is:
O(n^2)
With 200000 elements, this would require roughly:
200000^2 = 40,000,000,000
operations in the worst case, which is far too slow.
Optimal Approach, Bitwise Trie
The key insight is that XOR becomes large when corresponding bits differ.
To maximize XOR for a number, we want to find another number whose bits are opposite whenever possible.
For example, if the current bit is:
1, we prefer to pair it with00, we prefer to pair it with1
Since numbers are represented in binary, we can process them bit by bit from the most significant bit down to the least significant bit.
A Trie, also called a prefix tree, is ideal for this because it stores numbers according to their binary representation. Each level of the Trie corresponds to one bit position.
When searching for the best XOR partner for a number:
- At each bit position, we greedily try to move to the opposite bit
- If the opposite bit exists, we gain a
1in the XOR result at that position - Otherwise, we move to the same bit
This greedy choice works because higher bits contribute more to the final numeric value than lower bits.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Checks every pair directly |
| Optimal Trie | O(n * 31) | O(n * 31) | Uses binary Trie for efficient XOR maximization |
Algorithm Walkthrough
Step 1: Build a Binary Trie
We create a Trie where each node has two possible children:
01
Each number is inserted bit by bit from the 31st bit down to the 0th bit.
We use 31 bits because the constraints guarantee values fit within signed 32-bit integers.
For example, the number 5 becomes:
000...00101
Each bit determines which child we move to in the Trie.
Step 2: Insert Every Number
As we process the array, we insert each number into the Trie.
This organizes numbers by their binary prefixes, allowing efficient bitwise searches later.
Step 3: Search for the Best XOR Partner
For each number, we traverse the Trie again.
At each bit position:
- Extract the current bit
- Compute the opposite bit
- Try moving to the opposite bit first
- If it exists, set the corresponding XOR bit to
1 - Otherwise move to the same bit
This greedy strategy maximizes higher-order bits first, which guarantees the maximum numeric XOR value.
Step 4: Track the Maximum XOR
For every number, we compute its best possible XOR value against numbers already in the Trie.
We continuously update the global maximum.
Step 5: Return the Final Result
After processing all numbers, the stored maximum is the answer.
Why it works
The algorithm works because XOR is maximized when bits differ at the highest possible positions. The Trie allows us to efficiently search for the best complementary bit at every level. Since we process bits from most significant to least significant, greedy choices at higher positions always dominate any later choices. Therefore, the constructed XOR value is optimal for each number, and taking the maximum across all numbers produces the correct final answer.
Python Solution
from typing import List
class TrieNode:
def __init__(self):
self.children = {}
class Solution:
def findMaximumXOR(self, nums: List[int]) -> int:
root = TrieNode()
def insert(num: int) -> None:
node = root
for bit_position in range(31, -1, -1):
bit = (num >> bit_position) & 1
if bit not in node.children:
node.children[bit] = TrieNode()
node = node.children[bit]
def find_best_xor(num: int) -> int:
node = root
current_xor = 0
for bit_position in range(31, -1, -1):
bit = (num >> bit_position) & 1
opposite_bit = 1 - bit
if opposite_bit in node.children:
current_xor |= (1 << bit_position)
node = node.children[opposite_bit]
else:
node = node.children[bit]
return current_xor
for num in nums:
insert(num)
maximum_xor = 0
for num in nums:
maximum_xor = max(maximum_xor, find_best_xor(num))
return maximum_xor
The solution begins by defining a TrieNode class. Each node stores child pointers for binary digits 0 and 1.
The insert function places a number into the Trie one bit at a time. We iterate from bit 31 down to 0, extracting each bit using:
(num >> bit_position) & 1
If the corresponding child node does not exist, we create it.
The find_best_xor function performs the greedy XOR search. At every bit position, we attempt to move toward the opposite bit because opposite bits produce 1 in XOR. When such a branch exists, we update the current XOR value using:
current_xor |= (1 << bit_position)
This sets the current bit in the XOR result.
If the opposite branch does not exist, we move along the matching bit branch instead.
After building the Trie with all numbers, we query the Trie for every number and keep the maximum XOR encountered.
Go Solution
package main
type TrieNode struct {
children [2]*TrieNode
}
func insert(root *TrieNode, num int) {
node := root
for bitPosition := 31; bitPosition >= 0; bitPosition-- {
bit := (num >> bitPosition) & 1
if node.children[bit] == nil {
node.children[bit] = &TrieNode{}
}
node = node.children[bit]
}
}
func findBestXOR(root *TrieNode, num int) int {
node := root
currentXOR := 0
for bitPosition := 31; bitPosition >= 0; bitPosition-- {
bit := (num >> bitPosition) & 1
oppositeBit := 1 - bit
if node.children[oppositeBit] != nil {
currentXOR |= (1 << bitPosition)
node = node.children[oppositeBit]
} else {
node = node.children[bit]
}
}
return currentXOR
}
func findMaximumXOR(nums []int) int {
root := &TrieNode{}
for _, num := range nums {
insert(root, num)
}
maximumXOR := 0
for _, num := range nums {
current := findBestXOR(root, num)
if current > maximumXOR {
maximumXOR = current
}
}
return maximumXOR
}
The Go implementation follows the same algorithmic structure as the Python version. Instead of using dictionaries for child nodes, Go uses a fixed-size array:
children [2]*TrieNode
This is more memory efficient and faster because there are only two possible branches.
Go integers safely support the required bit operations under the given constraints. Nil pointer checks replace Python dictionary membership checks.
Worked Examples
Example 1
Input:
nums = [3,10,5,25,2,8]
Binary Representations
| Number | Binary |
|---|---|
| 3 | 00011 |
| 10 | 01010 |
| 5 | 00101 |
| 25 | 11001 |
| 2 | 00010 |
| 8 | 01000 |
Searching for Best XOR
When evaluating 5:
| Bit Position | Current Bit | Preferred Bit | Exists? | XOR Bit Set? |
|---|---|---|---|---|
| 4 | 0 | 1 | Yes | Yes |
| 3 | 0 | 1 | Yes | Yes |
| 2 | 1 | 0 | Yes | Yes |
| 1 | 0 | 1 | No | No |
| 0 | 1 | 0 | No | No |
The resulting XOR becomes:
11100 = 28
This corresponds to:
5 XOR 25 = 28
No larger XOR exists in the array.
Example 2
Input:
nums = [14,70,53,83,49,91,36,80,92,51,66,70]
Key Observation
The maximum XOR is:
127
Binary form:
127 = 1111111
This means the chosen pair differs at every bit position within the relevant range.
During Trie traversal, the algorithm repeatedly finds opposite bits at high positions, producing the maximum possible XOR value.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * 31) | Each number is inserted and queried across 32 bits |
| Space | O(n * 31) | Trie may contain up to 32 nodes per number |
Since 31 is constant, the practical complexity is linear with respect to the number of elements.
The Trie depth never exceeds 32 levels because integers are bounded by 32-bit representation. Therefore each insertion and lookup runs in constant time relative to bit width.
Test Cases
from typing import List
class TrieNode:
def __init__(self):
self.children = {}
class Solution:
def findMaximumXOR(self, nums: List[int]) -> int:
root = TrieNode()
def insert(num: int) -> None:
node = root
for bit_position in range(31, -1, -1):
bit = (num >> bit_position) & 1
if bit not in node.children:
node.children[bit] = TrieNode()
node = node.children[bit]
def find_best_xor(num: int) -> int:
node = root
current_xor = 0
for bit_position in range(31, -1, -1):
bit = (num >> bit_position) & 1
opposite_bit = 1 - bit
if opposite_bit in node.children:
current_xor |= (1 << bit_position)
node = node.children[opposite_bit]
else:
node = node.children[bit]
return current_xor
for num in nums:
insert(num)
maximum_xor = 0
for num in nums:
maximum_xor = max(maximum_xor, find_best_xor(num))
return maximum_xor
solution = Solution()
assert solution.findMaximumXOR([3,10,5,25,2,8]) == 28 # provided example 1
assert solution.findMaximumXOR([14,70,53,83,49,91,36,80,92,51,66,70]) == 127 # provided example 2
assert solution.findMaximumXOR([0]) == 0 # single element
assert solution.findMaximumXOR([2,4]) == 6 # simple XOR
assert solution.findMaximumXOR([8,10,2]) == 10 # different bit patterns
assert solution.findMaximumXOR([7,7,7]) == 0 # all identical numbers
assert solution.findMaximumXOR([0,1]) == 1 # includes zero
assert solution.findMaximumXOR([1,2,4,8,16]) == 24 # powers of two
assert solution.findMaximumXOR([5,25]) == 28 # direct optimal pair
assert solution.findMaximumXOR([2147483647,0]) == 2147483647 # max 31-bit value
| Test | Why |
|---|---|
[3,10,5,25,2,8] |
Validates standard example |
[14,70,53,...] |
Validates large XOR across many values |
[0] |
Tests single-element input |
[2,4] |
Tests simple two-number XOR |
[8,10,2] |
Tests mixed bit patterns |
[7,7,7] |
Tests identical values |
[0,1] |
Ensures zero handled correctly |
[1,2,4,8,16] |
Tests powers of two |
[5,25] |
Tests direct optimal pair |
[2147483647,0] |
Tests upper constraint boundary |
Edge Cases
Single Element Array
If the array contains only one number, the only valid pair is the number with itself:
x XOR x = 0
A buggy implementation might incorrectly assume two distinct numbers are required. This solution handles the case naturally because the Trie contains the same number being queried.
All Numbers Identical
Consider:
[7,7,7]
Every XOR result equals zero because identical bits always produce zero in XOR.
Some implementations incorrectly attempt to force differing paths in the Trie even when none exist. This implementation safely falls back to the matching bit branch whenever the opposite branch is unavailable.
Numbers with Large High Bits
Consider:
[2147483647, 0]
The maximum XOR comes entirely from high-order bits.
A common mistake is iterating through too few bit positions. This implementation processes all 32 relevant bits from 31 down to 0, ensuring large values are handled correctly.
Arrays Containing Zero
Zero is important because:
0 XOR x = x
Sometimes the maximum XOR includes zero paired with a large value.
The Trie correctly stores zero as a path consisting entirely of 0 bits, allowing comparisons against any other number without special handling.