← 返回题目列表

数组中两个数的最大异或值如何求?(LeetCode 421)

高频 中等 第 12 / 26 题 更新于 2026/07/30
位运算异或前缀树贪心

简化版

最大异或值要让高位尽量为 1,因为高位贡献更大。常见解法是把所有数的二进制位插入 0/1 前缀树,然后对每个数从高位到低位贪心找相反位:当前位是 0 就优先找 1,当前位是 1 就优先找 0。每个数查一次,32 位整数下时间 O(32n),空间 O(32n)。

详细版

class Node {
    Node[] child = new Node[2];
}

int findMaximumXOR(int[] nums) {
    Node root = new Node();
    for (int x : nums) {
        Node cur = root;
        for (int b = 31; b >= 0; b--) {
            int bit = (x >>> b) & 1;
            if (cur.child[bit] == null) cur.child[bit] = new Node();
            cur = cur.child[bit];
        }
    }

    int ans = 0;
    for (int x : nums) {
        Node cur = root;
        int val = 0;
        for (int b = 31; b >= 0; b--) {
            int bit = (x >>> b) & 1;
            int want = bit ^ 1;
            if (cur.child[want] != null) {
                val |= (1 << b);
                cur = cur.child[want];
            } else {
                cur = cur.child[bit];
            }
        }
        ans = Math.max(ans, val);
    }
    return ans;
}
  • 异或某一位为 1 的条件是两个数该位不同。
  • 为了最大化数值,必须优先保证更高位为 1。
  • 0/1 前缀树能快速判断某个高位前缀下是否存在相反位。
  • 固定 32 位整数下时间 O(n),展开写为 O(32n),空间 O(32n)。

完整版教学

一、为什么不能直接枚举所有数对

暴力会枚举所有 (i,j),计算 nums[i] ^ nums[j] 并取最大值,时间 O(n^2)。当 n=20000 时,数对约 2 亿个,很容易超时。优化的切入点不是减少异或本身,而是避免盲目试所有搭档。

异或值的大小由高位优先决定。例如第 30 位为 1 的结果,一定大于第 30 位为 0、低位全为 1 的结果。因此寻找最大异或时要从高位开始贪心。

二、最大异或的贪心依据

某一位上,0 ^ 1 = 11 ^ 0 = 1,相同才是 0。对一个固定数字 x,如果当前位是 0,就希望另一个数字当前位是 1;如果当前位是 1,就希望另一个数字当前位是 0。

x 当前位 bit = 0 -> want = 1
x 当前位 bit = 1 -> want = 0
want = bit ^ 1

由于从高位到低位处理,只要更高位已经能变成 1,就不应该为了低位牺牲高位。这就是贪心正确的关键。

三、为什么需要 0/1 前缀树

如果每一位都想找相反位,必须知道数组中是否存在某个数拥有对应的二进制前缀。0/1 前缀树把每个数字按二进制从高位到低位插入,每条根到叶的路径代表一个数字。

nums = [3, 10, 5]
用 4 位表示:
3  = 0011
10 = 1010
5  = 0101

root
├─0
│ ├─0 -> 3 的前缀
│ └─1 -> 5 的前缀
└─1 -> 10 的前缀

查询某个 x 时,从根开始优先走相反位;如果相反位不存在,才走相同位。这样每一步都在当前可行集合里让当前位尽量为 1。

记忆钩子:最大 XOR 不是找“数值最大的另一个数”,而是从最高位开始找“和我最不一样的前缀”。

四、用具体例子走查询过程

nums=[3,10,5,25,2,8],经典答案是 5 ^ 25 = 28

5  = 00101
25 = 11001
XOR= 11100 = 28

5 查询时,高位开始尽量找相反位:

5 的位:      0 0 1 0 1
希望搭档位:  1 1 0 1 0
25 的位:     1 1 0 0 1
实际 XOR:    1 1 1 0 0

不是每一位都一定能找到相反位,但高位能找到时优先拿下。最终形成的 11100 已经比很多低位更优的组合更大。

五、前缀哈希的另一种写法

除了前缀树,也可以按位贪心加哈希集合。每轮假设当前高位答案可以变成 candidate = ans | (1 << bit),把所有数的高位前缀放入集合,检查是否存在两个前缀异或出 candidate

int ans = 0, mask = 0;
for (int b = 31; b >= 0; b--) {
    mask |= (1 << b);
    Set<Integer> prefixes = new HashSet<>();
    for (int x : nums) prefixes.add(x & mask);
    int candidate = ans | (1 << b);
    for (int p : prefixes) {
        if (prefixes.contains(p ^ candidate)) {
            ans = candidate;
            break;
        }
    }
}

这个写法空间也是 O(n),思路更数学;前缀树写法更直观,适合讲“逐位找相反位”。

解法核心判断时间复杂度适合场景
暴力枚举直接算每个数对的 XORO(n^2)只适合小数据或验证答案
0/1 前缀树对每个数逐位找相反分支O(32n)讲解最直观,支持动态插入
哈希前缀假设当前答案位可为 1,检查前缀是否存在O(32n)代码较短,适合熟悉异或等式时使用

六、复杂度和实现细节

前缀树插入每个数需要扫描 32 位,查询每个数也扫描 32 位,所以时间 O(64n),简化为 O(n)。空间上最坏每个数字贡献 32 个节点,因此 O(32n)。

实现细节里最容易错的是位宽和右移。Java 中建议使用 >>> 取位,表达无符号位模式;如果输入都非负,>> 多数情况下也能工作,但 >>> 更稳。若题目限定数字小于 2^31,从 bit 30 开始也可以;从 31 开始更通用。

七、常见误区与追问

  • 误区:选择数组中最大的两个数异或。 最大数值不等于最大异或,高位是否相反才关键。
  • 误区:从低位开始贪心。 低位贡献小,不能为了低位 1 牺牲高位 1。
  • 误区:前缀树查询时只看当前位,不维护路径可行性。 每一步都必须沿着真实存在的路径走,否则拼出的搭档可能不存在。
  • 追问:为什么贪心是正确的? 因为二进制数按高位决定大小,高位能置 1 时任何低位选择都无法弥补放弃高位的损失。
  • 追问:前缀树和哈希前缀法哪个更好? 前缀树直观且一次建树多次查询;哈希前缀法代码短,但正确性解释更抽象。
  • 追问:如果是动态插入和查询怎么办? 前缀树更合适,可以插入新数后立即查询最大异或搭档。

八、加强记忆

最大异或值的核心是“高位优先找相反”。把数字放进 0/1 前缀树,查询时当前位为 0 就优先走 1,当前位为 1 就优先走 0;走到相反位时把答案对应位设为 1。不要被“最大数字”误导,真正要最大化的是从最高位开始的差异前缀。