Trie (Prefix Tree) 前缀树

1. 什么是Trie?

Trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
Trie可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的。
《Trie (Prefix Tree) 前缀树》

2. Trie的应用

(1) 自动补全
例如,你在百度搜索的输入框中,输入一个单词的前半部分,它能够自动补全出可能的单词结果。
(2) 拼写检查
例如,在word中输入一个拼写错误的单词, 它能够自动检测出来。
(3) IP路由表
在IP路由表中进行路由匹配时, 要按照最长匹配前缀的原则进行匹配。
(4) T9预测文本
在大多手机输入法中, 都会用9格的那种输入法. 这个输入法能够根据用户在9格上的输入,自动匹配出可能的单词。
(5) 填单词游戏
相信大多数人都玩过那种在横竖的格子里填单词的游戏。

3. Trie的相关操作

(1)Trie的结点结构

  • 根结点一般为空
  • 每个结点最多有R个孩子,每个孩子分别对应字母表数据集中的一个字母(通常情况下,R为26,字母表为26个英文字母 ).
  • 每个结点都有一个boolean类型的域, 代表这个结点是否是一个key的末尾.
class TrieNode {

    // R links to node children
    private TrieNode[] links;

    private final int R = 26;

    private boolean isEnd;

    public TrieNode() {
        links = new TrieNode[R];
    }

    public boolean containsKey(char ch) {
        return links[ch -'a'] != null;
    }
    public TrieNode get(char ch) {
        return links[ch -'a'];
    }
    public void put(char ch, TrieNode node) {
        links[ch -'a'] = node;
    }
    public void setEnd() {
        isEnd = true;
    }
    public boolean isEnd() {
        return isEnd;
    }
}

(2) 向trie中插入一个关键字

基本思想:
从根结点开始, 向下依次寻找当前结点的link中, 是否有与关键字key中相应位置的字母相同的link;

  • 如果有:沿着该link到下一层继续寻找;
  • 如果没有: 则新建一个node, 插入当前结点的link中,然后沿link到下一层继续寻找;

向下遍历到key的末尾时, 修改该结点的boolean指示值,表示是关键字末尾.

class Trie {
    private TrieNode root;

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

    // Inserts a word into the trie.
    public void insert(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            char currentChar = word.charAt(i);
            if (!node.containsKey(currentChar)) {
                node.put(currentChar, new TrieNode());
            }
            node = node.get(currentChar);
        }
        node.setEnd();
    }
}

时间复杂度O(m), m是关键字字符串的长度; 空间复杂度也为O(m), m是关键字字符串的长度.

(3) 在trie中查找一个关键字

基本思想:
从根结点开始, 根据关键字中的字母,沿着不同的link向下搜寻, 依次比较当前节点的link中是否有和关键字相应字母相同的link.

  • 如果有,则继续到下一层搜寻
  • 如果没有, 说明已经到一个单词的末尾. 此时看关键字是否遍历到了末尾,如果到了末尾的话, 说明匹配成功; 如果没有到末尾,说明只匹配到了关键字的一个前缀,匹配失败.
class Trie {
    ...

    // search a prefix or whole key in trie and
    // returns the node where search ends
    private TrieNode searchPrefix(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
           char curLetter = word.charAt(i);
           if (node.containsKey(curLetter)) {
               node = node.get(curLetter);
           } else {
               return null;
           }
        }
        return node;
    }

    // Returns if the word is in the trie.
    public boolean search(String word) {
       TrieNode node = searchPrefix(word);
       return node != null && node.isEnd();
    }
}

时间复杂度O(m), m是关键字字符串长度;空间复杂度O(1).

(4) 在trie中查找一个关键字是否是前缀

这个操作和(3)在trie中查找一个关键字很相似, 不同的是, 这里的关键字不必是某个单词的末尾, 只需是前缀即可.

class Trie {
    ...

    // Returns if there is any word in the trie
    // that starts with the given prefix.
    public boolean startsWith(String prefix) {
        TrieNode node = searchPrefix(prefix);
        return node != null;
    }
}

时间复杂度O(m), m是关键字字符串长度;空间复杂度O(1).

4. Trie的优点

还有其他几种数据结构,如平衡树和哈希表,它们可以在字符串数据集中搜索一个单词。那为什么我们还需要trie?
虽然哈希表在查找某个关键字时有O(1)的时间复杂度,但以下操作效率不高:

  • 用共同的前缀查找所有的键。
  • 以字典顺序列举字符串数据集。

trie优于哈希表的另一个原因是,随着哈希表的大小增加,会有很多哈希冲突,搜索时间复杂度可能会恶化到 O(n ),其中n是插入的f关键字的数量。当存储具有相同前缀的多个关键字时,Trie可以使用比哈希表少的空间。在这种情况下,使用trie只有O(m )时间复杂度,其中m是关键字长度。而在平衡树中查找关键字的时间复杂度为 O(m l o g n )。

参考资料:
1. https://en.wikipedia.org/wiki/Trie
2. https://leetcode.com/articles/implement-trie-prefix-tree/

    原文作者:Trie树
    原文地址: https://blog.csdn.net/l947069962/article/details/77650918
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞