【LeetCode】208.实现Trie(前缀树)(java+c++,两种方法实现,详细图解)

题目

链接

image-20200627231617460

分析

解题思路

这是一种叫做 单词查找树 的结构。它由字符串键中所有的字符构造而成,允许使用被查找键中的字符进行查找。其中包括插入、查找、删除,寻找前缀等操作。接下来先介绍Trie树的基本性质。

先看下面的例子:

208_1.png

现在有5个word,分别为by,by,hello,heat,the。所构成的TrieTree如图所示,其中包含一个根节点,值为空,跟几点所连接的是每个word的第一个字符,每个字符按照同样的方式生成与之连接的字符的TrieTree,在每个word的最末处,表示该word出现了几次。例如:“b”处为0,表示"b"这个单词没有出现过。“y”处为2,表示“by”这个单词出现了两次。

单词查找树之所以有这样的,是由于我们对于其结点的特殊定义。单词查找树的每一个节点都包含了下一个可能出现的字符的链接。从根节点开始,首先经过的是word的首字母所对应的链接,在下一个节点中沿着第二个字符所对应的链接继续前进,在第二个节点中沿着第三个字符所对应的链接向前,这样到达最后一个字符所指向的节点就结束。接下来我们来看对其节点的定义。

1. TrieNode

定义单词查找树的结点为:

public class TrieNode{
    public int path;
    public int end;
    public HashMap<Character, TrieNode> next;

    public TrieNode(){
        path = 0;
        end = 0;
        next = new HashMap<>();
    }
}
  • path:表示当前节点所能链接到其他节点的个数(在删除操作中会用到)
  • end:表示以当前节点为结尾的单词的个数
  • nextHashMap<Character, TrieNode>类型,表示当前节点能链接到的所有节点。

这里next同样可以使用字符数组来表示,例如字符的范围是小写字母,可以以'a'~'z'字符作为索引,这样相比起哈希表来会提高查找速度,但每一个点都含有一个值和26个链接,这样会多出好多空节点,如图所示:

208_2.png
该图片来自网络

2.insert操作

如同链表的生成过程一样,从根节点开始,如果根节点所连接的节点中没有当前字符,则生成一个值为当前字符的新节点,插入其中;如果有当前字符,则则继续进行匹配,并在过程中对每一个匹配到的节点path进行计数,重复上述过程,直到插完最后一个字符,并在最后一个字符的节点end进行计数,表示已经该单词的插入操作已经完成。

public void insert(String word){
    if(word == null || word.equals(""))  return ;
    TrieNode node = root;
    for(int i = 0; i<word.length(); i++){
        char ch = word.charAt(i);
        if(!node.next.containsKey(ch)) {
            node.next.put(ch,new TrieNode());
        }
        node = node.next.get(ch);
        node.path++;
    }
    node.end++;
}
2.search操作

同insert操作基本相同,由于我这里使用的是Hashmap进行的节点存储,故如果在匹配的过程中没能匹配到,则表示搜索失败,匹配到后最后一个字符时,如果当前end值不为零,则表示匹配成功。

public boolean search(String word){
    if(word == null || word.equals("")) return false;
    TrieNode node = root;
    for(int i = 0; i<word.length(); i++){
        char ch = word.charAt(i);
        if(!node.next.containsKey(ch)) return false;
        node = node.next.get(ch);
    }
    if(node.end == 0) return false;
    return true;
}
3.startwith操作

同search操作基本相同,只是这里判断到最后一个字符的时候,不需要判断end值。因为这里只需要检查前缀是否存在。

public boolean startsWith(String word){
    if(word == null || word.equals("")) return false;
    TrieNode node = root;
    for(int i = 0; i<word.length(); i++){
        char ch = word.charAt(i);
        if(!node.next.containsKey(ch)) return false;
        node = node.next.get(ch);
    }
    return true;
}
4.delete操作

delete操作同上述操作大致相同,这里需要使用到path变量,回忆一下,path:表示当前节点所能链接到其他节点的个数。还以五个单词为例,例如删除’the’,当匹配到‘t’时,由于进行path–操作后,此时判断path为0,过可直接将当前节点置空,后面的数据则不用再去遍历即可。java中的垃圾回收机制会处理其他的被断开的节点,如果在C++中,则需要全部匹配,之后调用析构函数去操作。

public void delete(String word){
    if(word == null || word.equals("") || !search(word)) return ;
    TrieNode node = root;
    for (int i = 0; i < word.length(); i++) {
        char ch = word.charAt(i);
        if(--node.next.get(ch).path == 0){
            node.next.remove(ch);
            return;
        }
        node = node.next.get(ch);
    }
    node.end--;
}

刚才的列出了insert、search、startwith、delete四种操作,由于本题没有要求delete操作,故在最终代码中不体现,但delete操作还是要掌握的。

代码

java实现 map

public class Trie {
    public class TrieNode{
    public int path;
    public int end;
    public HashMap<Character, TrieNode> next;

    public TrieNode(){
        path = 0;
        end = 0;
        next = new HashMap<>();
    }
}

    private TrieNode root;
    public Trie(){
        root = new TrieNode();
    }

    public void insert(String word){
        if(word == null || word.equals(""))  return ;
        TrieNode node = root;
        for(int i = 0; i<word.length(); i++){
            char ch = word.charAt(i);
            if(!node.next.containsKey(ch)) {
                node.next.put(ch,new TrieNode());
            }
            node = node.next.get(ch);
            node.path++;
        }
        node.end++;
    }

    public boolean search(String word){
        if(word == null || word.equals("")) return false;
        TrieNode node = root;
        for(int i = 0; i<word.length(); i++){
            char ch = word.charAt(i);
            if(!node.next.containsKey(ch)) return false;
            node = node.next.get(ch);
        }
        if(node.end == 0) return false;
        return true;
    }
    public boolean startsWith(String word){
        if(word == null || word.equals("")) return false;
        TrieNode node = root;
        for(int i = 0; i<word.length(); i++){
            char ch = word.charAt(i);
            if(!node.next.containsKey(ch)) return false;
            node = node.next.get(ch);
        }
        return true;
    }
}

/**
 * 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);
 */

C++实现,仅供参考。

class Trie {
public:
    class TrieNode {
    public:
        int path;   // 当前节点所能链接到其他节点的个数,该成员可以没有
        int end;    // 以当前节点为结尾的单词的个数
        unordered_map<char, TrieNode*> next;    // 当前节点能链接到的所有节点
        
        TrieNode () {
            path = 0;
            end = 0;
            next = unordered_map<char, TrieNode*> {};
        }
    };

    TrieNode *root;
    Trie() {
        root = new TrieNode();
    }
    
    void insert(string word) {
        if (word.empty())
            return ;
        TrieNode *node = root;
        for (int i = 0; i < word.length(); i++) {
            char ch = word[i];
            if (node->next.count(ch) == 0) {
                node->next[ch] = new TrieNode();
            }
            node = node->next[ch];
            node->path++;
        }
        node->end++;
    }
    
    bool search(string word) {
        if (word.empty())
            return false;
        TrieNode *node = root;
        for (int i = 0; i < word.length(); i++) {
            char ch = word[i];
            if (node->next.count(ch) == 0)
                return false;
            node = node->next[ch];
        }
        if (node->end == 0) // 若不成立,说明此时word仅是这条分支上的prefix,因此在startWith方法中,把这个判断去掉即可
            return false;
        return true;
    }
    
    bool startsWith(string prefix) {
        if (prefix.empty()) 
            return false;
        TrieNode *node = root;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix[i];
            if (node->next.count(ch) == 0) 
                return false;
            node = node->next[ch];
        }
        return true;
    }
};

java索引实现

class Trie {
    private final int ALPHABET_SIZE = 26;
    private Trie[] children = new Trie[ALPHABET_SIZE];
    boolean isEndOfWord = false;
    public Trie() {}
    
    public void insert(String word) {
        Trie tmp = this;
        for (char i : word.toCharArray()) {
            if (tmp.children[i-'a'] == null) {
                tmp.children[i-'a'] = new Trie();
            }
            tmp = tmp.children[i-'a'];
        }
        tmp.isEndOfWord = true;
    }
    
    public boolean search(String word) {
        Trie tmp = this;
        for (char i : word.toCharArray()) {
            if (tmp.children[i-'a'] == null) {
                return false;
            }
            tmp = tmp.children[i-'a'];
        }
        return tmp.isEndOfWord ? true : false;
    }
    
    public boolean startsWith(String prefix) {
        Trie tmp = this;
        for (char i : prefix.toCharArray()) {
            if (tmp.children[i-'a'] == null) {
                return false;
            }
            tmp = tmp.children[i-'a'];
        }
        return true;
    }
}
原文地址:https://www.cnblogs.com/hzcya1995/p/13308018.html