Facebook Pixel

421. Maximum XOR of Two Numbers in an Array

MediumBit ManipulationTrieArrayHash Table
Leetcode Link

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 0
  • children[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 using x >> i & 1
    • x >> i shifts the number right by i 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 bit v ^ 1 exists in the Trie
    • If v = 0, then v ^ 1 = 1 (we want to find 1)
    • If v = 1, then v ^ 1 = 0 (we want to find 0)
  • If the opposite bit exists:
    • We set the corresponding bit in our answer using ans |= 1 << i
    • We traverse to the opposite branch
  • 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)
  1. First, we build the Trie by inserting all numbers from the array
  2. Then, for each number in the array, we search for its maximum XOR partner
  3. 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 Evaluator

Example 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 called n times, and each insertion traverses through 31 bits (from bit position 30 to 0), performing constant-time operations at each level. This gives us O(n * 31) = O(n * L).

  • Finding maximum XOR: The search method is called n times (once for each number), and each search traverses through 31 bits, performing constant-time operations at each level. This also gives us O(n * 31) = O(n * L).

  • Total time complexity: O(n * L) + O(n * L) = O(n * L), which simplifies to O(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 is O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More