Bitwise trie (maximum XOR)
In one line
Store numbers in a trie by their bits from most significant to least, then for each number greedily walk toward the opposite bit at every level to find the partner that gives the largest XOR, in O(bits) per query.
What it is
A bitwise trie (also called a binary trie) is a trie whose alphabet is just 0 and 1. Each number is inserted as a fixed-length path of its bits, starting from the most significant bit. For 32-bit integers every path has the same depth, so each insert and query takes a constant 32 (or 31) steps.
Its main interview use is maximising XOR. XOR gives 1 where two bits differ, and a 1 in a higher bit is worth more than all lower bits combined (2^4 = 16 is more than 8 + 4 + 2 + 1 = 15). So to get the biggest XOR with x, you want the opposite of x's bit at the highest position possible, then the next, and so on. The trie lets you make that greedy choice one level at a time.
Why interviewers ask
'Maximum XOR of Two Numbers in an Array' (LeetCode 421) and its variants are common in harder interviews and online assessments. A brute force check of all pairs is O(n^2); the bitwise trie makes it O(n x B) with B bits. Interviewers want to see that you can justify the greedy choice (higher bits dominate), handle the empty-trie edge case, and extend the idea to subarray XOR or queries with limits.
How it works
- 1Pick a bit width B large enough for the inputs, such as 31 for non-negative 32-bit integers.
- 2Insert a number: for i from B-1 down to 0, take bit b = (x >> i) & 1 and follow or create child b.
- 3Query the best partner for x: at each level, compute the bit you want, the opposite of x's bit. If that child exists, go there and set bit i of the result to 1. Otherwise follow the same bit, and that result bit stays 0.
- 4Because a higher bit outweighs all lower ones together, choosing the opposite bit whenever possible, from the top down, always gives the maximum XOR.
- 5For the whole array, insert each number and query it against the trie, keeping the best answer. Inserting before querying guarantees the trie is never empty (a number XOR itself is just 0).
Worked example · nums = [3, 10, 5, 25, 2, 8], using 5 bits
- In binary: 3 = 00011, 10 = 01010, 5 = 00101, 25 = 11001, 2 = 00010, 8 = 01000.
- After inserting 3, 10 and 5, query 25 = 11001 (after inserting it too).
- Bit 4: 25 has 1, want 0. 3, 10 and 5 start with 0, so take it. Result = 10000 (16).
- Bit 3: 25 has 1, want 0. Under '0', 3 (00011) and 5 (00101) have 0 here. Take it. Result = 11000 (24).
- Bit 2: 25 has 0, want 1. Only 5 (00101) has 1 here. Take it. Result = 11100 (28). Only 5 remains on this path.
- Bit 1: 25 has 0, want 1, but 5 has 0: follow 0, result bit stays 0. Bit 0: 25 has 1, want 0, but 5 has 1: follow 1. Final result 11100 = 28.
- Check: 25 XOR 5 = 11001 XOR 00101 = 11100 = 28. Queries for 2 and 8 later find nothing better, so the answer is 28.
Picture it
It is like a game of 'be as different as possible' played one yes/no question at a time, with the most important question asked first. At each question you pick the opposite answer if anyone gave it, because winning an earlier question beats winning all the later ones combined.
Reference code
def find_maximum_xor(nums, bits=31):
root = {}
def insert(x):
node = root
for i in range(bits - 1, -1, -1): # most significant bit first
b = (x >> i) & 1
node = node.setdefault(b, {})
def best_xor(x):
node, result = root, 0
for i in range(bits - 1, -1, -1):
b = (x >> i) & 1
want = 1 - b # opposite bit makes this XOR bit 1
if want in node:
result |= 1 << i
node = node[want]
else:
node = node[b] # always exists: x itself is stored
return result
best = 0
for x in nums:
insert(x) # insert first so the trie is never empty
best = max(best, best_xor(x))
return bestEach number becomes a 31-level path of bits, highest first. best_xor prefers the opposite bit at every level, setting that result bit to 1 when possible. Inserting x before querying keeps the trie non-empty and guarantees node[b] exists. Time O(n x 31), space O(n x 31) nodes in the worst case.
Key patterns
Maximum XOR of two numbers
When: Find max a XOR b over all pairs (LeetCode 421)
How: Insert each number, query it greedily for the opposite bit, track the best.
Maximum XOR of a subarray
When: Find the subarray with the largest XOR
How: XOR of nums[i..j] = prefix[j+1] XOR prefix[i]; insert prefix XORs (starting with 0) and query each new prefix.
Queries with a limit
When: For each query (x, m), max x XOR a over a <= m (LeetCode 1707)
How: Sort numbers and queries by limit; insert numbers up to m before answering each query offline.
Count pairs with XOR below k
When: Count pairs with a XOR b < k (LeetCode 1803 style)
How: Store a count on each node; walk with x and k's bits, adding the count of the branch that keeps the XOR smaller than k.
Real-world use
Internet routing (longest prefix match)
Routers store IP prefixes in binary tries over the address bits (Patricia tries in BSD-style kernels, the level-compressed fib_trie in Linux) and walk the destination address bit by bit to find the most specific route.
Distributed hash tables
Kademlia, used by the BitTorrent mainline DHT and IPFS, measures distance between node IDs with XOR and organises its routing table by shared bit prefixes, which is the same binary-trie view of IDs.
Firewalls and IP allow lists
Systems that check whether an address falls inside many CIDR blocks store the blocks in a binary trie so each check costs at most 32 steps for IPv4 (128 for IPv6).
Competitive programming and online assessments
Maximum XOR pair, maximum subarray XOR and offline XOR queries are standard problems solved with a bitwise trie because they turn O(n^2) into O(n x 32).
In a project
You are building an IP reputation service that must decide, for every incoming request, whether its IPv4 address falls in any of 500,000 blocked CIDR ranges, at 50,000 requests per second.
- Insert each CIDR block as a path of its first prefix-length bits into a binary trie, and mark the node where the prefix ends as blocked.
- For each request, walk the address bits from the most significant bit and stop as soon as you reach a blocked node (or a missing child).
- Compress single-child chains (Patricia / radix style) to cut memory and the number of steps.
- Rebuild the trie when the blocklist changes and swap it in atomically.
Impact: Each lookup takes at most 32 steps no matter how many ranges there are, compared with checking 500,000 ranges one by one, so the check adds microseconds rather than milliseconds to each request.
Trade-offs
Use it when
- You need the maximum (or a bound on) XOR between a number and a set
- You need longest-prefix matching over fixed-width binary keys such as IP addresses
- n is large enough that O(n^2) pair checks are too slow
Avoid it when
- n is tiny: checking all pairs is simpler and fast enough
- Memory is tight: up to B nodes per number can be a lot; a sort-and-prefix-hash approach for max XOR uses less memory
- Numbers can be negative in a language without fixed-width integers (Python): offset them or mask to a fixed width first
Cost: Insert and query O(B) each, where B is the bit width (31 or 32). Whole array: O(n x B) time, O(n x B) nodes in the worst case.
Common mistakes
- Inserting bits from least significant first, which breaks the greedy argument because low bits are decided before high ones.
- Using a different number of bits for different numbers (for example, bin(x) without padding), so paths do not line up.
- Querying before inserting anything, then dereferencing a missing child on an empty trie.
- Wanting the same bit instead of the opposite bit; that minimises XOR rather than maximising it.
- Forgetting the prefix XOR of 0 when solving maximum subarray XOR, which misses subarrays that start at index 0.
Interview questions
Quick revision
- Binary trie: alphabet {0, 1}, fixed depth B (31 for non-negative ints).
- Insert from the most significant bit down: b = (x >> i) & 1.
- Max XOR: at each level go to the opposite bit if it exists, set result bit i.
- Greedy is correct because 2^i > sum of all lower bits.
- Max pair XOR: O(n x B) time instead of O(n^2).
- Subarray XOR: use prefix XORs, insert 0 first.
- Same idea powers longest-prefix match in IP routing.
Check yourself
1. To maximise x XOR y, at each level of the trie you prefer the child that:
2. Why must the bits be inserted from most significant to least significant?
3. What is 25 XOR 5?
4. For n numbers of 31 bits, what is the time to find the maximum pair XOR with a bitwise trie?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.