Trie树的介绍

84 阅读4分钟

Trie树是一种高效的存储和查找字符串集合的数据结构。

适用场景

  • 全是大写字母
  • 全是小写字母
  • 0和1

Trie树的形状

比如建立单词字典树,那么对于字符串abcdef,Trie树如下图所示:

image.png

然后添加字符串 bcd,如下图所示:

image.png 加入第三个字符串 acd,如下图所示:

image.png

image.png

  • 可以看到相同的结点只保存一次,那怎么判断单词是否存在呢,我们只需要在单词最后的字母节点上添加一个标记就能知道是否是单词的结尾,还可以添加一个计数。
  • 比如添加一个标记isEnd可以很方便的判断,或者标记count,通过count去判断是否有单词以此结尾。
  • 所以Trie树可以实现快速的查询单词是否出现过以及出现过的次数。

例题

Acwing 835. Trie字符串统计

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗10^4

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1
import java.util.*;
import java.lang.*;
import java.io.*;

public class Main{
    
    static TrieNode root = null;
    static {
        root = new Trie();
    }
    public static void main  (String [] args) throws IOException{
        int n;
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        n = Integer.parseInt(br.readLine());
        for(int i = 0 ; i < n;i++){
            String [] s = br.readLine().split(" ");
            if(s[0].equals("I") ){
                addKeyWord(s[1]);
            }else {
                bw.write(countKeyWord(s[1])+"\n");
            }
        }
        bw.flush();
        br.close();
        bw.close();
        
    } 
    
    public static void addKeyWord(String keyword){
        // 从根节点开始查找
        TrieNode tempNode = root;
        
        for(int i = 0 ; i < keyword.length();i++){
            Character c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);
            // 存在子节点的话直接使用即可,不存在结点需要创建子节点加入
            if(subNode == null) {
                subNode = new TrieNode();
                tempNode.addNode(c, subNode);
            }
            
            tempNode = subNode;
            // 如果是最终的位置isEnd为true,并且更新计数
            if(i == keyword.length() - 1) {
                tempNode.setKeyWordEnd(true);
                tempNode.setCount(tempNode.getCount() + 1);
            }
            
        }
        
    }
    
    public static int countKeyWord(String keyword){
        TrieNode tempNode = root;
        for(int i = 0 ; i < keyword.length();i++){
            Character c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);
            
            if(subNode == null) {
                return 0;
            }
            
            tempNode = subNode;
        }
        return tempNode.getCount();
        
    }
    
    
}
// Trie树的结点, 写算法的时候不必要去写这些get / set函数
class TrieNode{
    boolean isKeyWordEnd;
    int cnt;
    HashMap<Character, TrieNode> child;
    public TrieNode(){
        child = new HashMap<>();
        cnt = 0;
    }
    public boolean isKeyWordEnd(){
        return isKeyWordEnd;
    }
    
    public void setCount(int cnt){
        this.cnt = cnt;
    }
    public int getCount(){
        return cnt;
    }
    
    public void setKeyWordEnd(boolean isKeyWordEnd) {
        this.isKeyWordEnd = isKeyWordEnd;
    }
    public void addNode(Character c, TrieNode node){
        child.put(c, node);
    }
    public TrieNode getSubNode(Character c){
        return child.get(c);
    }
}

AcWing 143. 最大异或对

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,所有输入的字符串总长度不超过 10^5,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

接下来 N行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗10^4

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1
import java.util.*;
import java.lang.*;
import java.io.*;

public class Main{
    
    static TrieNode root = new TrieNode();
    
    public static void main  (String [] args) throws IOException{
        int n;
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        n = Integer.parseInt(br.readLine());
        
        String [] s = br.readLine().split(" ");
        for(int i = 0 ;  i < n;i++){
            addNum(Integer.parseInt(s[i]));
        }
        int res = 0;
        for(int i = 0; i < n;i++){
            res = Math.max(res, find(Integer.parseInt(s[i])));
        }
        System.out.println(res);
        bw.flush();
        br.close();
        bw.close();
        
    } 
    
    public static void addNum(int num){
        TrieNode tempNode = root;
        
        for(int i = 30; i >= 0 ; i--){
            int x = num >> i & 1;
            TrieNode subNode = tempNode.getSubNode(x);
            if(subNode == null) {
                subNode = new TrieNode();
                tempNode.addNode(x, subNode);
            }
            tempNode = subNode;
        }
        
    }
    
    public static int find(int num){
        TrieNode tempNode = root;
        int res = 0;
        for(int i = 30; i >= 0 ; i--){
            int x = (num >> i & 1) == 1 ? 0 : 1;
            TrieNode subNode = tempNode.getSubNode(x);
            
            if(subNode == null) {
                x = 1 - x;
                tempNode = tempNode.getSubNode(x);
            } else {
                res += 1 << i;
                tempNode = subNode;
            }
            
        }
        return res;
        
    }
    
    
}

class TrieNode{
    HashMap<Integer, TrieNode> child;
    public TrieNode(){
        child = new HashMap<>();
    }
    public void addNode(int c, TrieNode node){
        child.put(c, node);
    }
    public TrieNode getSubNode(int c){
        return child.get(c);
    }
}

如果要减少内存的消耗的话,可以用数组去模拟指针的方式实现。