前缀树

98 阅读3分钟

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

前缀树

前缀树实现

前缀树的优点:

为什么我们还需要 Trie 树呢?尽管哈希表可以在 O(1) 时间内寻找键值,却无法高效的完成以下操作:

  • 找到具有同一前缀的全部键值。
  • 按词典序枚举字符串的数据集

Trie 树优于哈希表的另一个理由是,随着哈希表大小增加,会出现大量的冲突,时间复杂度可能增加到 O(n),其中 n 是插入的键的数量。与哈希表相比,Trie 树在存储多个具有相同前缀的键时可以使用较少的空间。此时 Trie 树只需要 O(m) 的时间复杂度,其中 m 为键长。而在平衡树中查找键值需要 O(mlogn) 时间复杂度。

 class Trie {
 public:
     /** Initialize your data structure here. */
     Trie():root_(new TreeNode) { }
 ​
     ~Trie() { destroy(root_); }
     
     /** Inserts a word into the trie. */
    
     void insert(const std::string& word) {
         if(word.empty()) return;
 ​
         TreeNode* curr = root_;
         for(const char& ch : word) { 
             size_t index = ch -'a';
             if(curr->branchs[index] == nullptr) 
             {
                 curr->branchs[index] = new TreeNode;
             }
             curr = curr->branchs[index];
         }
         curr->end = true; 
     }
     
     /** Returns if the word is in the trie. */
     bool search(const std::string& word) {
         if(word.empty()) return true; 
 ​
         TreeNode* curr = root_;
         for(const char& ch : word) { 
             int index = ch - 'a';
             if(curr->branchs[index] == nullptr) 
             {
                 return false;
             }
 ​
             curr = curr->branchs[index];
         }
 ​
         return curr->end;
     }
     
     /** Returns if there is any word in the trie that starts with the given prefix. */
     bool startsWith(const std::string& prefix) {
       if(prefix.empty()) return true;
 ​
       TreeNode* curr = root_;
       for(const char& ch : prefix) { 
         int index = ch - 'a';
         if(curr->branchs[index] == nullptr) 
         {
             return false;
         }
 ​
         curr = curr->branchs[index];
       }
 ​
       return true;
     }
 ​
 private:
     struct TreeNode { 
       bool end; // 是否是某个字符串的结尾标志
       std::array<TreeNode*, 26> branchs;
 ​
       TreeNode(bool end=false): end(end)
       { 
           branchs.fill(nullptr);
       }
     };
 ​
   void destroy(TreeNode* node) {
     if(node ==nullptr) return;
 ​
     for(TreeNode* entry : node->branchs)
     {
      destroy(entry); 
     } 
 ​
     delete node;
     node = nullptr;
   }
     
   TreeNode* root_;
 };
 ​

最大异或和子数组

 题目:给定一个数组,求子数组的最大异或和。其中,一个数组的异或和定义为数组中所有的数异或起来的结果。

常规解法

涉及子数组的题目,总体思路一样:以i结尾的子数组异或和,再将i0 < i <= n遍历一遍,求出所有位置的子数组异或和,最大值就在其中。但是如此的时间复杂度分析:

 以i为例:
     i-1~i
     i-2~2
     ...
     0 ~ i

需要求出i个子数组的异或和,每个子数组异或和时间复杂度为O(n),因为仅仅求一个位置i的子数组异或和就高达O(n^2)的复杂度。外层再将i从0 < i <= n遍历一遍,时间复杂度O(n^3)

 int maxXor(int* arr, int length) {
     int outside_xor =0;
     int max_=0;
 ​
     for(int i=0; i < length; ++i) { 
         outside_xor ^= arr[i];   // 记录以 i 位置结束的 0~i的数组异或和
         max_ = std::max(max_, outside_xor);
         // 求取以i位置结尾的所有子数组异或和
         for(int j=0; j < i; ++j) { 
             int inside_xor = 0;
             for(int k=0; k < j; ++k) { 
                 inside_xor ^= arr[k];                //  0~ j-1 位置的异或和 
                 int x = outside_xor ^ inside_xor;    // j ~ i 位置的异或和,其中 0 <= j <i,这就是以i结尾所有子数组异或和
                 if(max_ < x) max_ = x;
             }
         }
     }
     return max_;
 }

如果改进,降低算法的复杂度,可以使用一个数组记录下之前的一些重复计算的值。比如这题的所有以j结尾的0~j的子数组,都在重复计算,那么就是可以用一个数组来存储,时间复杂度可以降低为O(n^2)

 int maxXor(int* arr, int length) {
     int outside_xor =0;
     int max_=0;
     int xorsum[length]; // 以 i结束的 0~i的所有异或和
 ​
     for(int i=0; i < length; ++i) { 
         outside_xor ^= arr[i]; 
         max_ = std::max(max_, outside_xor);
         xorsum[i] = outside_xor;   // 存储起来
         for(int j=0; j < i; ++j) { 
             int x =  xorsum[j] ^ outside_xor; // 这里以 0~j区间的异或和,之前已经计算好,直接使用
             if(max_ < x) max_ = x;
         }
     }
     return max_;
 }

前缀树

将每个数字变成二进制用一个二叉树来表示,所有的二叉树都是左子节点是0,右子节点是1。整个树根节点不表示,只是串联左右子树。

 0 ~ 1的异或和
 0 ~ 2的异或和
 ...
 0 ~ i-1的异或和

在求0~i区间最大异或和子数组时,前面得各个位置信息都已经具备了。因此可以直接使用了:将 0 ~ i 前面的异或和生成前缀树,在求i位置arr[i]的时候,如下限制:

  • 第一位是符号位,应该该位置是0,表示正数
  • 除了符号位外,在前缀树中,能选择1就选择1。如果没有1,只能选0。

满足以上两个,这样才能保持最大。 说的可能不太好理解。

 比如:
  0 ~ 0的异或和是 0001 
  0 ~ 1的异或和是 0101
  0 ~ 2的异或和是 1011
  0 ~ 3的异或和是 1111
  求以位置3结尾的最大异或和子数组 
  
                         root 
                       /     \
                      0       1
                     / \     /
                    0   1   0
                   /    /    \
                  0    0      1
                  \     \      \
                   1     1      1

0~3位置的异或和1111来确定以位置3结尾的最大异或和子数组。

  • 第一位符号位是1:期望的是0,上面的前缀树中第二层存在 root - 1
  • 第二位是1,如果要让异或和最大,这位异或后的结果也是1,那么就期待0。前缀树中也存在 root - 1 - 0
  • 同理,第三位也期望是1,需要与0异或,但是此时不存在0,只能选择1
  • 同理,第四位也只能与1异或

最终的异或1011 ^ 1111 = 0100,即使4,也就是最大异或子数组区间:[2,3]。 代码及其解释如下:

 public class Solution { 
 ​
     public static class Node {
         public Node[] nexts = new Node[2]; // 两个节点,一个指向0,一个指向1,
     }
 ​
     public static class NumTrie {
         public Node head = new Node();
 ​
         // 用于生成前缀树
         public void add(int num) {
             Node cur = head;
             for (int move = 31; move >= 0; move--) {
                 // 从高位到低位 依次取出每一位的二进制数
                 int path = ((num >> move) & 1);
                 // cur.nexts[path] = cur.nexts[path] == null ? new Node() : cur.nexts[path];
                 if(cur.nexts[path] == null)
                     cur.nexts[path] = new Node();
                     
                 cur = cur.nexts[path];
             }
         }
 ​
         // 根据前缀树查询异或最大值
         public int maxXor(int num) {
             Node cur = head;
             int res = 0;
             for (int move = 31; move >= 0; move--) {
                 // 取出每位二进制数
                 int path = (num >> move) & 1;
                 // 符号位,需要和符号位保持一致,才能异或为0。非符号位期望是1 
                 // 非符号位置,需要异或的结果是1,因此 path ^1,最低为位肯定是1
                 int best = move == 31 ? path : (path ^ 1);
                 // 这里没有创建结点,就是查询,选择道路
                 best = cur.nexts[best] != null ? best : (best ^ 1); // 实际的值
                 res |= (path ^ best) << move; 
                 cur = cur.nexts[best];
             }
             return res;
         }
 ​
     }
 ​
     public static int maxXorSubarray(int[] arr) {
         if (arr == null || arr.length == 0) {
             return 0;
         }
         int max = Integer.MIN_VALUE;
         int eor = 0;
         NumTrie numTrie = new NumTrie();
         numTrie.add(0);
         for (int i = 0; i < arr.length; i++) {
             eor ^= arr[i]; //  0 ~ i 的异或和
             max = Math.max(max, numTrie.maxXor(eor)); // 求取
             numTrie.add(eor);
         }
         return max;
     }
 }

显而易见,时间复杂度是O(N)