Java中前缀树(Trie)的简介与使用 LeetCode 力扣 720 词典中最长的单词

Trie树是一种特殊的N叉树,又称字典树,单词查找树,键树等。一般用于字符串的储存,查找,比较,偶尔也会用于删除。除根节点外,Trie树的每一个节点包含一个字符,每个节点的子节点字符不重复(但不同节点可以有相同的子节点,比如表示字符串“aa”和“ba”)。从根节点到某一节点的路径为该节点对应的字符串,因此每条路径上的字符排列应该是唯一的。

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销,对字符串的查找通常效率高于哈希表。

画一棵前缀树:
Java中前缀树(Trie)的简介与使用 LeetCode 力扣 720 词典中最长的单词_第1张图片
可以看到前缀树的几个要点:

  1. 黄色的是根节点,通常不赋值,或赋特殊值。
  2. 节点所有的子节点都与该节点相关的字符串有着共同的前缀。
  3. 绿色的节点代表从根节点到该节点可形成存在的字符串,图中包含字符串"a",“d”,“kt”,“km”,“be”,“bed”,“bet”,“do”。而不以绿色节点为末端的路径和不以根节点开始的路径形成的字符串不存在。

如果要查询一个单词是否存在,我们只需要从根节点开始,沿着单词的每一个字符向前走。每个节点的子节点字符唯一,是不会走到岔路上的。走到最后一个字符时,只要看这个节点是否被标记为绿色就可以知道它是否存在。如果走着走着没有路了,也说明这个节点不存在。插入时,如果没有路则创造路,走到最后一个节点时,把这个节点标记为绿色,就完成了插入操作。这个过程所用时间为O(Length),Length为单词长度。

Java中前缀树(Trie)的简介与使用 LeetCode 力扣 720 词典中最长的单词_第2张图片

通常用数组或HashMap来储存前缀树。因为不知道树中存在哪些字符,因此需要为每个子节点声明大小为26的数组。数组中的查找速度更快,但当字符种类不多时,会造成空间的浪费。

class TrieNode {
    char ch;
    boolean valid;
    int[] children = new int[26];
    public TrieNode(char ch){
        this.ch = ch;
    }
}

另一种实现方法时使用HashMap:

class TrieNode {
    char ch;
    boolean valid;
    HashMap<Character, TrieNode> children = new HashMap();
    public TrieNode(char ch){
        this.ch = ch;
    }
}

children为TrieNode的子节点列表。键为字符,值为字符对应的节点。子节点的存取稍慢,但是因为可以按需插入map,能够节省空间,更加灵活。

除了节点值,子节点列表,在节点定义中还加入了一个布尔值,用来标记是否是有效的字符串(是否为绿色节点)。

前缀树的主要操作有插入和查找,构建前缀树的过程就是多次调用插入函数。

public class Trie {
    TrieNode root;
    Trie() {
        root = new TrieNode('0');
    }
    public void insert(String word) {
        TrieNode cur = root;
        for(char ch : word.toCharArray()) {
            cur.children.putIfAbsent(ch, new TrieNode(ch));
            cur = cur.children.get(ch);
        }
        cur.valid = true;
    }
}

类似地,实现查找功能:

	public boolean search(String word) {
        TrieNode cur = root;
        for(char ch : word.toCharArray()) {
            if(cur == null) break;
            cur = cur.children.get(ch);
        }
        return cur != null && cur.valid;
    }

LeetCode一道可以用前缀树解的题:

  1. 词典中最长的单词
    给出一个字符串数组words组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。若无答案,则返回空字符串。

示例 1:

输入:
words = [“w”,“wo”,“wor”,“worl”, “world”]
输出: “world”
解释:
单词"world"可由"w", “wo”, “wor”, 和 "worl"添加一个字母组成。

示例 2:

输入:
words = [“a”, “banana”, “app”, “appl”, “ap”, “apply”, “apple”]
输出: “apple”
解释:
“apply"和"apple"都能由词典中的单词组成。但是"apple"得字典序小于"apply”。

思路:
可以用给出的words数组中所有单词构建前缀树,再深搜遍历前缀树, 找到前缀都在树中的最长单词。

class Solution {    
    public String longestWord(String[] words) {
        Trie trie = new Trie();
        int index = 0;
        for(String word : words)
            trie.insert(word, ++index);
        return trie.bfs(trie.root, words);
    }
}

class TrieNode {
    char ch;
    boolean valid;
    int index;
    Map<Character, TrieNode> children = new HashMap<>();
    TrieNode(char ch) {
        this.ch = ch;
    }
}

public class Trie {
    TrieNode root;

    Trie() {
        root = new TrieNode('0');
    }

    public void insert(String word, int index) {
        TrieNode cur = root;
        for(char ch : word.toCharArray()) {
            cur.children.putIfAbsent(ch, new TrieNode(ch));
            cur = cur.children.get(ch);
        }
        cur.index = index;
    }

    public String bfs (TrieNode root, String[] words) {
        String ans = "";
        Queue<TrieNode> queue = new LinkedList<>();
        TrieNode cur = root;
        queue.offer(root);
        while(!queue.isEmpty()) {
            cur = queue.remove();
            for(TrieNode node : cur.children.values()) {
                if (node.index > 0) {
                    String str = words[node.index - 1];
                    if (str.length() > ans.length() ||
                            (str.length() == ans.length() && str.compareTo(ans) < 0))
                        ans = str;
                    queue.offer(node);
                }
            }            
        }
        return ans;
    }
}

你可能感兴趣的:(算法与数据结构,java,字符串,哈希表)