[字典树、位运算] 剑指 Offer II 067. 最大的异或

210 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

每日刷题 2022.08.17

题目

  • 给定一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < 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 <= 2 * 10^5
  • 0 <= nums[i] <= 2^31 - 1

解题思路

  • 根据题意可知:需要找个整个nums数组中的两个数异或的最大值,且下标需要满足0<=i<=j<n.
  • 查看数据的范围,数组长度最大是:2 * 10 ^ 5,如果暴力求解的话,双层for循环对于每一个数都需要遍历一遍数组中的另一个数,那么时间复杂度就是:4 * 10 ^ 10,显然会超时。因此暴力模拟的方式被排除了。那么想想如何优化呢?

字典树

  • 可以将nums中的每一个数拆分成31位,将每一个按位存储在字典树中。因为需要获得两个数异或的最大值,异或:相异为1,相同为0。由此可见最高位的1应该被尽可能的保留,因为其能使最终的结果更大。那么就将nums数组中的每一个数值,从高位到低位依次存储在字典树tree中。
  • 接着在遍历nums数组中的每个数值(记为:cur),将cur的转换成二进制,且需要从高位往低位进行遍历,依次去字典树中查找与cur当前的二进制位不同的数值。
  • 例如(字典树中选择的时候尽可能的满足当前位能取到的最大值):cur当前位为1,那么字典树中对应的位需要找0;如果字典树不存在0,那么就只能找和自己相同的1
  • 同样的如果cur当前位为0,那么字典树中对应的位需要找1;如果字典树中不存在1,那么就只能找和自己相同的0
  • 还存在一个问题?如何记录当前的两个数的异或值呢?
    • 在遍历cur的每一位在字典树中查找值时,初始化一个变量res,每次在字典树中找到与之匹配的位时,就将当前的res左移1位,加上当前的位数值即可。
    • 最终将数组中的每一个数值计算出的自身最大异或值,取一个最大的返回即可。

最近学习的常用的位运算

  • 使用数值将一个字符串中出现过的所有字符都记录下来。
// 巧妙的使用一个cur数值记录下字符串中出现过的所有字符
let bit = one.charCodeAt() -'a'.charCodeAt();
// 将1左移bit位,然后与cur或,就可以使cur记录下来one代表的字符
// 或运算:只要有1个为真,即为真,两个都为0,才为0;
cur |= (1 << bit);
  • 如下图所示:得到g表示的二进制后,与0原先的值相或,就可以将字符串中所有出现的字符统计到一个二进制数组中。(只有出现的字符,才会为12f9a41fe13397901bf9f1ecf2ee3f39.jpg

  • 将十进制的数值获取其每一个二进制位

// cur表示一个十进制数值,将cur移动几位(30位),依次获取每一位的值
// &1 一定不能丢,这是获取二进制下的最后一位的值
cur = (cur >> i) & 1;
cur = cur >> 1; // 整除,就不需要写成cur = Math.floor(cur / 2);

AC代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var findMaximumXOR = function(nums) {
  // 因为是存在顺序的,所以要按照顺序来插入字典树中
  let n = nums.length, maxx = 0, root = {};
  for(let i = 0; i < n; i++) {
    maxx = Math.max(maxx, check(nums[i]));
    addTrie(nums[i]);
  }
  return maxx;
  // 将当前的节点插入到字典树中
  function addTrie(cur) {
    let node = root;
    for(let i = 30; i >= 0; i--) {
      let bit = (cur >> i) & 1;
      if(!node[bit]) node[bit] = {};
      node = node[bit];
    }
  }
  // 判断当前节点的最大异或和
  function check(cur) {
    // 如果字典数为空
    let node = root, res = 0;
    if(Object.keys(node).length === 0) return res;
    // if(root === {}) return res;
    for(let i = 30; i >= 0; i--) {
      // console.log(cur >> i, (cur >> i) & 1)
      // &1表示取最后一位
      let bit = (cur >> i) & 1;
      if(node[1] && (bit ^ 1 === 1)) {
        // 相异为1,选择另一条路走
        // bit 为 0
        res = (res << 1) + 1;
        node = node[1];
      } else if(node[0] && (bit ^ 0 === 1)) {
        // 相同为0,bit = 1
        res = (res << 1) + 1;
        node = node[0];
      } else {
        // 其他的情况,例如:bit为1,但是不存在0或者bit为0,但是不存在1
        // 就只能选择和自己相同的
        res = res << 1;
        node = node[bit];
      }
    }
    return res;
  }
};