给你一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n 。
进阶:你可以在 O(n) 的时间解决这个问题吗?
示例 1:
输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28.
示例 2:
输入:nums = [0]
输出:0
示例 3:
输入:nums = [2,4]
输出:6
示例 4:
输入:nums = [8,10,2]
输出:10
示例 5:
输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127
提示:
- 1 <= nums.length <= 20000
- 0 <= nums[i] <= 2^31 - 1
方法一:位运算 + 哈希表
1、想象一个二维表,每一个方格存储0或1,每一行代表一个数的二进制表示,左侧代表高位,右侧为低位。
2、nums[i] <= 2^31 - 1, 所以最多需要31位来表示一个数
3、这 31 个二进制位从高位到低位依次编号为30,29,28…3, 2, 1, 0 。我们从最高位第 30 个二进制位开始,依次确定 x 的每一位是 0 还是 1;
4、由于我们需要找出最大的 x,因此在枚举每一位时,我们先判断 x 的这一位是否能取到 1。如果能,我们取这一位为 1,否则我们取这一位为 0。
class Solution {
public:
/**
想象一个二维表,每一行代表一个数的二进制
*/
int findMaximumXOR(vector<int>& nums) {
int n = nums.size();
//x代表最终结果中当前二进制位左侧的部分,不包含当前位
int x = 0;
for(int k=30; k>=0; k--){
//当前二进制位是k+1位
unordered_set<int> set;
//存储所有数当前二进制位的左半部分,包括当前位,
for(int num : nums){
set.insert(num >> k);
}
//假设x_next代表最终结果中当前二进制位及其左侧的二进制部分
//这里 x_next = x * 2 + 1;假设当前位存在异或为1的情况
int x_next = x * 2 + 1;
bool found = false;
for(int num : nums){
// a ^ b = c, 则 a ^ c = b;
//如果存在两个数异或的结果为x_next,那么x_next的假设成立,当前二进制位为1
if(set.count(x_next ^ (num >> k))){
found = true;
break;
}
}
//假设成立,当前位为1
if(found) {
x = x_next;
}//否则,当前位为0
else {
x = x_next - 1;
}
}
return x;
}
};
举例:[3,10,5,25,2,8]
这个例子,最大的数是25,占5位,所以就从k=4模拟算法过程。
- k=4时, 此时set集合为(0, 1),x=0, x_next = 1; 因为 set集合中存在 1, 使得 x_next ^ 0 = 1, 此时x为1,也就是说最后结果的最高位为1,此时,可将最后结果的二进制表示为1 0 0 0 0
- k=3时,此时set集合由前两列的二进制得到,为(0, 1, 3),x = 1, x_next = 3, 此时,有x_next ^ 0 = 3,( 或者说 x_next ^ 3 = 0 ), 那么 x = x_next = 3; x 的二进制表示11,所以当前二进制位 可以为1,最终结果的二进制表示为 1 1 0 0 0,至此已经确定了前两位。
- k = 2时, 此时set集合由前三列二进制得到,为(0,2,1, 6),x = 3, x_next = 7, 此时,有x_next ^ 6 = 1(或者说 x_next ^ 1 = 6), x = x_next = 7. x的二进制表示 111,所以当前位可以为1,最终结果的二进制表示为 1 1 1 0 0
- k = 1 。。。。
- k = 0 。。。。
方法二:前缀树
1、我们也可以将数组中的元素看成长度为 31 的字符串,字符串中只包含 0 和 1。如果我们将字符串放入字典树中,左子树存放0, 右子树存放1。那么在字典树中查询一个字符串的过程,恰好就是从高位开始确定每一个二进制位的过程。
2、我们可以从字典树的根节点开始进行遍历,如果当前位为0,那么我们应当往表示 1 的子节点走,这样 0 ^ 1 = 1,如果不存在表示 1 的子节点,那么我们只能往表示 0 的子节点走。
3、同样道理,如果当前位为1,那么我们应当往表示 0 的子节点走,这样 0 ^ 1 = 1,如果不存在表示 0 的子节点,那么我们只能往表示 1 的子节点走。
两种方法思想上是一致的。
struct Trie {
// 左子树指向表示 0 的子节点
Trie* left = nullptr;
// 右子树指向表示 1 的子节点
Trie* right = nullptr;
Trie() {}
};
//构建一个树,将所有数对应的二进制添加进去
class Solution {
private:
Trie* root =new Trie(); //根节点
public:
void insert(int num){
Trie* cur = root;
for(int k=30; k>=0; k--){
//二进制位
int bit = (num >> k) & 1;
//如果是0,需要插入到左子节点上
if(bit == 0){
//构造左子节点
if(cur->left == nullptr){
cur->left = new Trie();
}
cur = cur->left;
}
如果是1,需要插入到右子节点上
else{
if(!cur->right){
cur->right = new Trie();
}
cur = cur->right;
}
}
}
int query(int num){
Trie* cur = root;
int x = 0;
for(int k=30; k>=0; k--){
int bit = (num >> k) & 1;
//当前位为0,找右子树的1,0 ^ 1 = 1;
if(bit == 0){
//如果右子树存在
if(cur->right){
cur = cur->right;
x = x * 2 + 1;
}//否则,左子树
else{
cur = cur->left;
x = x * 2;
}
}//当前位为1,找左子树的0,1 ^ 0 = 1;
else{
if(cur->left){
cur = cur->left;
x = x * 2 + 1;
}
else{
cur = cur->right;
x = x * 2;
}
}
}
return x;
}
int findMaximumXOR(vector<int>& nums) {
int n = nums.size();
int res = 0;
for(int i=1; i<n; i++){
insert(nums[i-1]);
res = max(res, query(nums[i]));
}
return res;
}
};