前缀树Trie

前缀树Trie

1. Trie的结构

Trie前缀树主要用于存储字符串,它的查找速度主要和要查找的字符串的长度相关[O(w)]。

前缀树的每一个节点都包含三个元素:

  1. 在该路径上,添加字符串的过程中经过该节点的次数path
  2. 在该路径上,以该节点结尾的字符串的个数end
  3. 节点类型的后继数组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. 实现

  1. 节点

    static class TrieNode{
    
        int path;//该路径上,有多少字符串经过该节点
        int end;//该路径上,有多少字符串以该节点结尾
        TrieNode[] next;//每个结点都有26个next指针,分别用来表示字母表中的小写字母
    
        public TrieNode(){
            path = 0;
            end = 0;
            next = new TrieNode[26];
        }
    }
    
  2. 插入操作

    插入过程中,每插入一个节点,不管该节点是否已经存在,更新该节点的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++;
    }
    
  3. 查找该前缀树中是否有前缀字符串“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;
    }
    
  4. 查找该前缀树中是否有字符串“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;
    }
    
  5. 删除字符串“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^cb=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)。

  1. 现有i,0<i<n,遍历前缀树,从前缀树中找出一条路径,路径上的值与当前Eor(0, n)做异或运算,找出产生最大异或值的那条路径。假设这条路径代表Eor(0, n),那么根据异或运算的交换律,Eor(i+1, n)就是最大异或值。
  2. 如果前缀树中没有路径,能够当前Eor(0, n)做异或运算并产生最大的异或值,那么Eor(0, n)就是最大的异或值。

因此,就找到了。

如何从前缀树中找符合条件的那条路径?

前缀树中每条路径上都有32个数(0或1),第一个数代表符号位,0表示正数,1表示负数。

  1. 假如我们已经计算出从0异或到第n个数的结果Eor(0, n)。假设Eor(0, n)为正数,即第一位数字为0,那么从前缀数中找路径的时候,尽量找第一位也是0的,这样的话,两个值异或的结果也是正数,第一位确定以后,保证异或之后的结果,除了第1位,其他位尽量为1。只有这样,才能保证最后的异或结果为最大的正数。
  2. 假如我们已经计算出从0异或到第n个数的结果Eor(0, n)。假设Eor(0, n)为负数,即第一位数字为1,那么从前缀数中找路径的时候,尽量找第一位也是1的,这样的话,两个值异或的结果就会变为正数,第一位确定以后,保证异或之后的结果,除了第1位,其他位尽量为1。只有这样,才能保证最后的异或结果为最大的正数。
  3. 如果与前缀树中的每一个路径值的异或结果只能为负数,那么只要保证这个负数最大就可以了。除了第一位,让其他为尽量为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){

    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于前缀树Trie)来说,匹配包含某个字段的字符串可以通过以下步骤实现: 1. 从根节点开始,遍历字段中的每个字符。 2. 检查当前节点是否有一个子节点与当前字符匹配。如果有匹配的子节点,则移动到该子节点,并继续下一个字符的匹配。 3. 如果没有匹配的子节点,则表示没有与字段中的字符相匹配的路径,这时可以结束匹配过程。 4. 当遍历完字段中的所有字符时,检查当前节点是否为某个字符串的结尾。如果是,则表示该前缀树中存在匹配包含该字段的字符串。 下面是一个示例代码,演示如何使用前缀树匹配包含字段: ```python class TrieNode: def __init__(self): self.children = {} self.is_end_of_word = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word): node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.is_end_of_word = True def search(self, word): node = self.root for char in word: if char not in node.children: return False node = node.children[char] return node.is_end_of_word def starts_with(self, prefix): node = self.root for char in prefix: if char not in node.children: return False node = node.children[char] return True def contains_word(self, word): for i in range(len(word)): if self.starts_with(word[i:]): return True return False # 创建前缀树 trie = Trie() # 插入字符串 trie.insert("apple") trie.insert("banana") trie.insert("orange") # 检查是否包含字段 print(trie.contains_word("app")) # True print(trie.contains_word("nan")) # False print(trie.contains_word("rang")) # True ``` 在上述示例代码中,我们首先创建了一个前缀树对象 `trie`,并使用 `insert` 方法插入了三个字符串:"apple"、"banana" 和 "orange"。然后我们使用 `contains_word` 方法检查是否存在包含特定字段的字符串。在示例中,我们分别检查了 "app"、"nan" 和 "rang" 这三个字段,得到了相应的结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值