【C/C++】208. 实现 Trie (前缀树、字典树模板)

775 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情


题目链接:208. 实现 Trie (前缀树)

题目描述

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

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

提示:

  • 1word.length,prefix.length20001 \leqslant word.length, prefix.length \leqslant 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 31043 * 10^4

示例 1:

输入
["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

整理题意

题目让我们实现 字典树Trie 发音类似 "try" 或者说 前缀树,是一种树形数据结构)包括实现以下方法:

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

解题思路分析

该题为字典树的模板题,也就是让我们实现数据结构 字典树,同时还要完成字典树的相关操作方法函数。

字典树上的每个节点包含两个字段:

  • 指向子节点的指针数组 children
  • 布尔字段 isEnd,表示该节点是否为字符串(单词)的结尾。

插入字符串: 从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:

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

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

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

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

重复以上步骤,直到返回空指针或查找完前缀或字符串的最后一个字符。若搜索到了字符串或前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的 isEnd 为真,则说明字典树中存在该字符串。

具体实现

插入单词

从根结点出发,沿着字符串的字符一直往下走,若某一字符不存在,则直接把它创建出来,继续走下去,走完了整个单词,标记最后的位置的 isEnd = true

查找单词

从根结点出发,沿着字符串的字符一直往下走,若某一字符不存在,则直接返回 false ,如果可以沿着字符串走完,判断最后一个节点位置的 isEnd 是否为真即可。

查找前缀

从根结点出发,沿着字符串的字符一直往下走,若某一字符不存在,则直接返回 false ,如果可以沿着字符串走完,则返回true

查找单词和查找前缀的唯一区别就在于是否判断最后一个节点位置的 isEnd

复杂度分析

  • 时间复杂度:初始化为 O(1)O(1),其余操作为 O(S)O(|S|),其中 S|S| 是每次插入或查询的字符串的长度。
  • 空间复杂度:O(TΣ)O(∣T∣⋅Σ),其中 T|T| 为所有插入字符串的长度之和,Σ\Sigma 为字符集的大小,本题 Σ=26\Sigma=26

代码实现

class Trie {
private:
    //定义字典树数据结构
    struct TreeNode {
        bool isEnd;
        TreeNode *children[26];
        TreeNode(){
            //创建节点时初始化
            isEnd = false;
            for(int i = 0; i < 26; i++) children[i] = NULL;
        }
    }*root;

public:
    Trie() {
        //初始化创建一个根节点
        root = new TreeNode();
    }
    //插入操作
    void insert(string word) {
        auto p = root;
        //从根节点出发
        for(auto c : word){
            int i = c - 'a';
            //没有就创建
            if(!p->children[i]) p->children[i] = new TreeNode();
            p = p->children[i];
        }
        //标记为一个单词结尾
        p->isEnd = true;
    }
    //查找单词
    bool search(string word) {
        auto p = root;
        //从根节点出发
        for(auto c : word){
            int i = c - 'a';
            //如果没有说明找不到
            if(!p->children[i]) return false;
            p = p->children[i];
        }
        //看当前标记是否为单词结尾
        return p->isEnd;
    }
    //查找前缀
    bool startsWith(string prefix) {
        auto p = root;
        //从根节点出发
        for(auto c : prefix){
            int i = c - 'a';
            //如果没有说明找不到
            if(!p->children[i]) return false;
            p = p->children[i];
        }
        //因为是前缀,所以无需判断是否为结尾
        return true;
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */

总结

  • 字典树 是一种数据结构而不是算法。
  • 该题为字典树的模板题,让我们实现字典树的各种操作,在之后遇到有关字典树的题可以直接套用该题的模板进行使用。
  • 测试结果:

208.png

结束语

时间是公平的,每个人的一天都是24小时。有的人把这24小时安排得松弛有度,而有的人却浑浑噩噩过完一天又一天。如何利用每天的时间,决定了你会成为怎样的人。别再羡慕别人的优秀,行动起来,成功或许并不难。新的一天,加油!