当前位置:   article > 正文

【算法学习】前缀树Trie_前缀树算法

前缀树算法

Tire(前缀树)

一、定义:

  • Trie(发音类似 “try”)或者说前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键

  • 其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。

  • Trie结构

    • 与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

其中红色节点表示一个单词的结尾。

  • Tire字典树的查找速度主要和它的元素(字符串)的长度相关O(w)。

二、插入字符串

public void insert(String word)

我们从字典树的根开始查找前缀,对于当前字符对应的子节点,有两种情况:

  • 子节点存在:沿着指针移动到子节点,继续寻找下一个字符。
  • 子节点不存在:创建一个新的子节点,记录在children数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。

重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾

三、查找前缀

public Trie searchPrefix(String prefix)

我们从字典树的根开始查找前缀,对于当前字符对应的子节点,有两种情况:

  • 子节点存在:沿着指针移动到子节点,继续搜索下一个字符。
  • 子节点不存在:说明字典树中不包含该前缀,返回null空指针。

重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。

  • 若搜索到了前缀的末尾,说明该字典树中存在该前缀
  • 另外,若前缀末尾对应的节点isEnd==true,则说明字典树中存在该字符串

三、应用场景:

1、Trie树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构,这一数据结构有相当多的应用情景,例如自动补完和拼写检查

2、在纯算法领域,前缀树算是一种比较常用的数据结构,但如果是在工程中,不考虑前缀匹配的话,基本上使用hash就可以满足要求了。

  • 但考虑前缀匹配的话,工程上一般也不会使用Tire。(工程领域Tire的应用面不广)
    • 字符集大小不好确定(如下的题目只考虑了26个字母,字符集的大小限制在较小的26以内),因此可以使用Trie,但在工程中一般会兼容各种字符集,一旦字符集很大,Tire将会带来很大的空间浪费。
    • 另外,对于个别的超长字符Tire会进一步变深,如果这时Tire是存储在硬盘中的,Tire结构过深带来的影响是多次随机IO,随机IO成本极高。
    • Tire的特殊结构,也会在工程上为分布式存储带来困难。
  • 工程上例如【联想输入】、【模糊匹配】、【全文检索】的典型场景主要是通过ES(ElasticSearch)解决的,而ES的实现主要是依靠【倒排索引】。

—>例如:Leetcode「208. 实现 Trie (前缀树)」

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word)如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix)如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。

示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]

输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
class Trie {
    private Trie[] children;//指向子节点的指针数组children
    private boolean isEnd;//表明该节点是否为字符串的结尾

    public Trie() {
        children=new Trie[26];//于本题数组长度为26,即小写英文字母的数量
        isEnd=false;//初始化为false
    }
    
    public void insert(String word) {
        Trie node = this;
        for(int i=0;i<word.length();i++){
            char ch=word.charAt(i);
            int index=ch-'a';
            //子节点不存在,创建一个新的子节点,记录在children数组的对应位置上
            if(node.children[index]==null){
                node.children[index]=new Trie();
            }
            //沿着指针移动到子节点,继续搜索下一个字符
            node=node.children[index];
        }
        node.isEnd=true;
    }
    
    public boolean search(String word) {
        Trie node = searchPrefix(word);
        return node!=null&&node.isEnd;
    }
    
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix)!=null;
    }
    
    public Trie searchPrefix(String prefix){
        Trie node = this;
        for(int i=0;i<prefix.length();i++){
            char ch=prefix.charAt(i);
            int index=ch-'a';
            //子节点不存在,说明字典树中不包含该前缀,返回空指针
            if(node.children[index]==null){
                return null;
            }
            //沿着指针移动到子节点
            node=node.children[index];
        }
        return node;
    }
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

复杂度分析:

  • 时间:初始化为O(1),其余操作为O(|S|),其中|S|为每次插入/查询的字符串长度。

  • 空间:O(∣T∣⋅Σ),其中 ∣T∣ 为所有插入字符串的长度之和,Σ为字符集的大小,本题 Σ=26。

四、扩展

实际需求中不可能只有26个字母,也许有大小写,也许有其他的符号,因此我们需要使用其他的数据结构。

第二版:

class Node{
	public boolean isWord;
	public Map<Character,Node> next;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 此时暂时只考虑英文字符不考虑其他语言(中文等)。

1、根据第二版我们设计的Trie:

public class Trie {
    private class Node{
        public boolean isWord;
        public TreeMap<Character,Node> next;

        public Node(boolean isWord){
            this.isWord = isWord;
            this.next = new TreeMap<>();
        }

        public Node(){
            this(false);
        }
    }

    private Node root;
    private int size;

    public Trie(){
        this.root = new Node();
        this.size = 0;
    }

	/**
	 * 返回Trie节点数
	 */
    public int getSize(){
        return this.size;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

2、Trie的添加

向字典树中添加节点的步骤如图:

//添加单词
public void add(String word){
    Node curr = root;
    for(int i=0;i<word.length();i++){
        char c = word.charAt(i);
        if(curr.next.get(c)==null){
            curr.next.put(c,new Node());//没有该节点,直接添加新的
        }
        curr = curr.next.get(c);//有该节点,移动curr,继续查找下一节点
    }
    if(!curr.isWord){//如果之前已经存在该单词了size不能加1
        curr.isWord = true;
        size++;//Trie的节点数
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3、Trie的查找单词:

//查询单词
public boolean contains(String word){
    Node curr = root;
    for(int i=0;i<word.length();i++){
        char c = word.charAt(i);
        if(curr.next.get(c)==null){
            return false;//无下一个值的节点,直接返回false
        }
        curr = curr.next.get(c);
    }
    return curr.isWord;//单词到达末尾,根据isWord标志判断
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4、判断前缀:

//查询在Trie中是否有单词以prefix为前缀
public boolean isPrefix(String prefix){
    Node curr = root;
    for(int i=0;i<prefix.length();i++){
        char c = prefix.charAt(i);
        if(curr.next.get(c)==null){
            return false;
        }
        curr = curr.next.get(c);
    }
    return true;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

5、删除单词

删除节点有以下四种情况:

  • 情况一:逐一将节点去除即可;
  • 情况二:直接将isWord置为false即可;
  • 情况三:逐一删除节点,如果待删除节点是另外一个单词结尾则停止删除;
  • 情况四:逐一删除节点,但是如果待删除节点还有其他孩子节点则停止删除。
//删除单词
public void delete(String word){
    Stack<Node> preNodes = new Stack<>();
    //删除前查找是否存在这个单词,存在才能删除
    if(!contains(word)){
        return;//不存在该单词,直接返回
    }
    Node curr = root;
    for(int i=0;i<word.length();i++){
        char c = word.charAt(i);
        preNodes.push(curr);//添加前驱节点
        curr = curr.next.get(c);
    }
    if(curr.next.size()==0){//到了单词末尾,节点是叶子节点
        for(int i=word.length()-1;i>=0;i--){
            char c = word.charAt(i);
            Node pre = preNodes.pop();//获得前驱节点
            //判断是否为其他单词的结尾,是则停止删除节点【情况3】
            //判断待删除节点是否还有其他孩子,有则停止删除节点【情况4】
       if((i!=word.length()-1&&pre.next.get(c).isWord)||pre.next.get(c).next.size()!=0){
                break;
            }
            pre.next.remove(c);//删除节点【情况1】
        }
    }else{
        curr.isWord = false;//情况2
    }
    size--;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

上面实现的Trie中,我们是使用TreeMap来保存节点的所有子节点,也可以用HashMap来保存,效率会更高:

public Node(boolean isWord){
            this.isWord = isWord;
            this.next = new HashMap<>();
        }
  • 1
  • 2
  • 3
  • 4
  • Trie的查询效率非常高,但对空间的消耗还是很大的,是典型的空间换时间。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/727772
推荐阅读
  

闽ICP备14008679号