前缀树Trie
1. Trie的结构
Trie前缀树主要用于存储字符串,它的查找速度主要和要查找的字符串的长度相关[O(w)]。
前缀树的每一个节点都包含三个元素:
- 在该路径上,添加字符串的过程中经过该节点的次数
path
- 在该路径上,以该节点结尾的字符串的个数
end
- 节点类型的后继数组next[26],数组长度为26(假设字符串只由26个小写英文字母组成)
在以上结构中,用路径上的值来表示字符串。
如果要在根节点上插入字符m,则根节点的后继数组的第13个元素next[12]变为非空。'm' - 'a' = 12
如上图所示,要在空前缀树中插入字符串“abc”,要插入a,则第一个节点的后继数组元素next[0] ('a'-'a'=0
)非空,其他数组元素为空,然后next[0]作为第二个节点,其中这两个节点之间的路径代表字符a。接着要插入b,则第二个节点的后继数组元素next[1] ('b'-'a'=1
)非空,其他数组元素为空。该数组元素next[1]作为第三个节点,以此类推。
2. 实现
-
节点
static class TrieNode{ int path;//该路径上,有多少字符串经过该节点 int end;//该路径上,有多少字符串以该节点结尾 TrieNode[] next;//每个结点都有26个next指针,分别用来表示字母表中的小写字母 public TrieNode(){ path = 0; end = 0; next = new TrieNode[26]; } }
-
插入操作
插入过程中,每插入一个节点,不管该节点是否已经存在,更新该节点的path值
插入完毕后,更新字符串最后一个元素节点的end值
public void insert(String words){ if(words == null){ return; } char[] arr = words.toCharArray(); int len = arr.length; TrieNode cur = root; int index = 0; for(int i = 0; i < len; i++){ index = arr[i] - 'a'; if(cur.next[index] == null){ cur.next[index] = new TrieNode(); } cur = cur.next[index]; cur.path++; } cur.end++; }
-
查找该前缀树中是否有前缀字符串“xxx”
判断是否有前缀字符串“xxx”,只需要判断是否有路径包含字符串“xxx”
public boolean startsWith(String prefix){ if(prefix == null){ return false; } char[] arr = prefix.toCharArray(); int len = arr.length; TrieNode cur = root; int index = 0; for(int i = 0; i < len; i++){ index = arr[i] - 'a'; if(cur.next[index] == null){ return false; } cur = cur.next[index]; } return true; }
-
查找该前缀树中是否有字符串“xxx”
判断是否有字符串“xxx”,不仅需要判断是否有路径包含字符串“xxx”,而且字符串“xxx”的最后一个节点的end值不为0。
判断过程中,判断到特定字符串的最后一个元素,如果最后一个元素的end值等于0,说明该前缀树中没有字符串”xxx“。表明”xxx“是某个更长字符串的前缀。
public boolean search(String words){ if(words == null){ return false; } char[] arr = words.toCharArray(); int len = arr.length; TrieNode cur = root; int index = 0; for(int i = 0; i < len; i++){ if(i == len - 1){ index = arr[i] - 'a'; return cur.next[index] != null && cur.next[index].end != 0; }else{ index = arr[i] - 'a'; if(cur.next[index] == null){ return false; } cur = cur.next[index]; } } return true; }
-
删除字符串“xxx”
删除字符串“xxx”,如果最后一个字符的节点之后没有字符,之间删除该节点。如果还有其他节点,记得要更新end值。
public void delete(String words){ if(!search(words)){ return; } char[] arr = words.toCharArray(); int len = arr.length; TrieNode cur = root; int index = 0; for(int i = 0; i < len; i++){ index = arr[i] - 'a'; if(cur.next[index].path > 1){ cur.next[index].path--; cur = cur.next[index]; }else{ cur.next[index] = null; return; } } cur.end--;//这一句不要忘了 }
3. leetcode208
题目: 实现一个 Trie (前缀树),包含
insert
,search
, 和startsWith
这三个操作。
上面已经实现,不再赘述。
4. 子数组的最大异或和
异或运算满足结合律与交换律
如
a^b=c
,则a=b^c
,b=a^c
动态规划:O(N^2)
思路:可以使用暴力递归,但是我们有更好的解法,利用异或运算的交换律。
如果一直从0异或到i,则已知0到i上的异或结果,记为Eor(0, i),接着再进行异或运算,异或到n,则知道从0到n上的异或结果Eor(0, n)。已知这两个结果,则可以知道从i+1位置上到n位置上的异或结果,即为Eor(i + 1, n) = Eor(0, i) ^ Eor(0, n)
。
前缀树解法:O(N)
https://www.jianshu.com/p/060b6a2949c2
可以使用前缀树来记录每次异或的结果。存储结构如下:
- 由于数组中存放的是int型的数,int型为4字节,32位。将值转换成二进制在存到前缀树中,所以前缀树中只会有0和1。前缀树中每个节点最多有两个孩子,0和1。
- 前缀树不是使用节点记录值,而是使用边,所以前缀树的高度为33。
- 每次异或的结果要么是正数,要么是负数,所以,前缀树中的根节点的两个孩子,0代表此路径为正数,1代表此路径为负数。
如何找从0到n上最大的异或值,从0异或到n,则已知结果Eor(0, n)。
- 现有i,0<i<n,遍历前缀树,从前缀树中找出一条路径,路径上的值与当前Eor(0, n)做异或运算,找出产生最大异或值的那条路径。假设这条路径代表Eor(0, n),那么根据异或运算的交换律,Eor(i+1, n)就是最大异或值。
- 如果前缀树中没有路径,能够当前Eor(0, n)做异或运算并产生最大的异或值,那么Eor(0, n)就是最大的异或值。
因此,就找到了。
如何从前缀树中找符合条件的那条路径?
前缀树中每条路径上都有32个数(0或1),第一个数代表符号位,0表示正数,1表示负数。
- 假如我们已经计算出从0异或到第n个数的结果Eor(0, n)。假设Eor(0, n)为正数,即第一位数字为0,那么从前缀数中找路径的时候,尽量找第一位也是0的,这样的话,两个值异或的结果也是正数,第一位确定以后,保证异或之后的结果,除了第1位,其他位尽量为1。只有这样,才能保证最后的异或结果为最大的正数。
- 假如我们已经计算出从0异或到第n个数的结果Eor(0, n)。假设Eor(0, n)为负数,即第一位数字为1,那么从前缀数中找路径的时候,尽量找第一位也是1的,这样的话,两个值异或的结果就会变为正数,第一位确定以后,保证异或之后的结果,除了第1位,其他位尽量为1。只有这样,才能保证最后的异或结果为最大的正数。
- 如果与前缀树中的每一个路径值的异或结果只能为负数,那么只要保证这个负数最大就可以了。除了第一位,让其他为尽量为1。如-1的二进制表示为
111111111111111111111111
,32个1。高位的1越多,负数最大,越接近-1。正数也是,高位1越多,值越大。
代码
class Solution_MaxEor{
//前缀树中的节点
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;
//int类型一共32位
for(int move=31;move>=0;move--)
{
int path=((num>>move)&1);//当前位数
cur.nexts[path]=cur.nexts[path]==null?new Node():cur.nexts[path];
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,path^path=0,即0^0=0,1^1=0
//如果不是符号位,尽量使异或之后的符号位为1,path^path^1=0^1=1
//期待选的数位:如果path=1,期待选0,如果path=0,期待选1
int best=move==31?path:(path^1);
//如果前缀树中有符合条件的位,继续向下延伸
//如果前缀树中没有符合条件的位,只能选择另一个位。
//实际选的数位:如果path=1,期待选0,但是路径进行到此处,只有1,那也只能选1
best=cur.nexts[best]!=null?best:(best^1);
//最终的异或结果从高位32位逐步添加(path^best)
//整个for循环遍历完,res的32位也填充完了,就是当前的最大异或值
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的异或和
//0-i之间子数组最大的异或和
max = Math.max(max, numTrie.maxXor(eor));
numTrie.add(eor);
}
return max;
}
public static void main(String[] args){
}
}