数据结构与算法之字典树-2

77 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第26天,点击查看活动详情

2.最大异或值

2.1题目链接

剑指 Offer II 067. 最大的异或

1707. 与数组中元素的最大异或值

143. 最大异或对

2.2题目详情

难度中等

给你一个整数数组 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 = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127

提示:

  • 1 <= nums.length <= 2 * 105
  • 0 <= nums[i] <= 231 - 1

2.3实现0-1字典树

2.3.1结点设计

首先创建一个类TireNode表示结点,里面的成员就是两个结点,分别对应0和1,直接使用数组表示即可。

java版本:

//结点类
class TireNode {
    public TireNode[] next = new TireNode[2];
    
    public TireNode() {}
}

c++版本:

//结点
class TireNode 
{
public:
    TireNode* next[2] = {nullptr};
    
    TireNode() {}
};

然后是0-1字典树本体,使用Tire类表示吧,里面有一个根结点表示就可以了,还有一个插入数字的方法insert,查询最大异或数方法query。 java版本:

//Tire类
class Tire {
    //根结点
    public TireNode root = new TireNode();
    
    
    //插入操作
    public void insert(int val) {
​
    }
    
    //查询操作 返回val异或最大时的另外一个操作数
    public int query(int val) {
​
    }
}

c++版本:

//Tire类
class Tire 
{
public:
    //根结点
    TireNode* root = new TireNode();
    
    //插入操作
    void insert(int val) 
    {
​
    }
    
    //查询操作 返回与val异或最大时的另外一个操作数
    int query(int val)
    {
​
    }
};

2.3.2插入操作

插入操作从本质上来说和字典树是一模一样的,并且0-1字典树本质上就是字典树,插入的方式有多种,你可以选择从数的最低位开始插,也可以从数的最高位进行插入。

所以我们来从题目入手,来选择合适的方式将目标数val的所有位插入到0-1字典树当中,题目要求一组数中两个数的最大异或数,我们可以这样考虑,从最高位开始,我们得到val某一位t,要想使的求出来的异或值最大,根据异或相同位0相异为1的性质,另一位数对应为要尽量与val不相同,因此优先考虑从字典树中找对应位相反的数u,如果u对应的结点存在,我们就往u这个方向搜索,不存在就退而求其次,去t方向的路径寻找,这样想的话,必须得从最高位开始搜索才能保证搜索到的异或操作数异或后结果是最大的,因此选择从高位到低位的方式建立0-1字典树。

由于题目保证为正数,我们不需要去考虑最高位,毕竟最高位表示符号位。

插入操作那就很简单了,先判断一下t对应的结点是否为空,为空则创建一个结点,继续向下搜索插入即可。

java实现代码:

    //插入操作
    public void insert(int val) {
        TireNode cur = root;
        
        for (int i = 30; i >= 0; --i) {
            //获取第i位的二进制数
            int u = (val >> i) & 1;
            
            //如果不存在则创建
            if (cur.next[u] == null) cur.next[u] = new TireNode();
            
            //更新cur
            cur = cur.next[u];
        }
    }

c++实现代码:

    //插入操作
    void insert(int val) 
    {
        TireNode* cur = root;
        
        for (int i = 30; i >= 0; --i)
        {
            //获取第i位的二进制
            int u = (val >> i) & 1;
            //如果对应的next元素为空,则新增
            if (cur->next[u] == nullptr) cur->next[u] = new TireNode();
            
            //更新cur
            cur = cur->next[u];
        }
    }

2.3.3查询最大的异或数

在介绍为什么要使用从高位存到低位的时候,介绍了一种思想,那就是尽量往val对应位不同的方向走,这样可以保证异或的结果最大,所以查询思路如下:

  • 获取val对应位t
  • 获取与t相反的位u=-t+1
  • 判断u方向路径是否存在,存在就往u方向走,否则继续沿着t方向走。
  • 每走一步就将结果积累起来并使用一个变量ret保存,最后返回ret
  • ret累计可以使用ret = ret * 2 + u / t或者使用位运算ret |= (u / t << i)

java实现代码:

    //查询操作 返回val异或最大时的另外一个操作数
    public int query(int val) {
        TireNode cur = root;
        //储存最大异或操作数的值
        int res = 0;
        for (int i = 30; i >= 0; --i) {
            //获取对应位的二进制
            int t = (val >> i) & 1;
            //与t不同的二进制
            int u = -t + 1;
            
            if (cur.next[u] != null) {
                //优先选择不同位
                res = res * 2 + u;
                cur = cur.next[u];
            } else {
                //下策选择相同位
                res = res * 2 + t;
                cur = cur.next[t];
            }
        }
        
        return res;
    }

c++实现代码:

    //查询操作 返回与val异或最大时的另外一个操作数
    int query(int val)
    {
        TireNode* cur = root;
        int res = 0;
        for (int i = 30; i >= 0; --i)
        {
            //获取第i位的二进制
            int t = (val >> i) & 1;
            //获取与t不同的二进制位
            int u = -t + 1;
            
            if (cur->next[u] != nullptr) 
            {
                //优先选择不同位
                res = res * 2 + u;
                cur = cur->next[u];
            } else 
            {
                //迫不得已选相同
                res = res * 2 + t;
                cur = cur->next[t];
            }
        }
        return res;
    }