树_构建多叉树_41 . 实现Trie(前缀树)

本节目标:

1 . 多叉树型结构扩展

2 . 加深哈希思想与熟练运用


题目介绍

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

请你实现 Trie 类:

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

提示:

  • 1 <= word.length, prefix.length <= 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 3 * 104

代码片段:

cpp 复制代码
class Trie {
public:
    Trie() {
        
    }
    
    void insert(string word) {
        
    }
    
    bool search(string word) {
        
    }
    
    bool startsWith(string prefix) {
        
    }
};

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

解析

1 . 仔细读题:能了解何为"前缀树" ------ 给你一个字符串,能利用"前缀树"这个结构返回存在树里的完整字符串------常用于 自动填充 和 拼写检查

2 . 再看接口设计:初始化Trie() 插入单词insert() 搜索单词search() 通过前缀查找单词startsWith()

3 . 总的来说:就是上层用户传来前缀作为键,返回值

哈希

1 . 对于已知键,要快速得到对应的值;我们应该形成条件反射------哈希

2 . 但是哈希也有两种结构 : unordered_set 、 unordered_map

3 . 那此时再问自己:我们要存储的是否是一对关系呢 ?

答:其实没必要,这里的接口都是返回bool 或 void

换句话说:我们只需要快速搜索目标字符串是否存在,那么首当其冲的结构是unordered_set

4 . 那哈希表中每个格子存的,就是std::string

cpp 复制代码
class Trie {
public:
    Trie() {
        // stl容器(此处的hash)会默认调用它自己的默认构造。此处代码不必添加什么
    }
    
    void insert(string word) {
        
    }
    
    bool search(string word) {
        
    }
    
    bool startsWith(string prefix) {
        
    }
private:
        std::unordered_set<std::string> hash;// 结构
};

5 . insert接口里,要求在Trie里插入指定字符串------能吃现成就吃

cpp 复制代码
void insert(string word) {
        if(!hash.count(word)) // 检查word是否是已经存在?如果不存在,则插入word
        {
           hash.insert(word);     
        }
    }

6 . search接口同样,不就是搜索指定字符串是否在哈希表里存在? count接口

cpp 复制代码
bool search(string word) {
        return hash.count(word) == 1; // unordered_set的count接口,若查到了返回1 ;没查到返回0
    }

7 . 给定前缀,要求查找------ 现在无法直接拿着prefix直接查找;但我们可以遍历哈希表里存储的所有字符串,然后一个一个比对前缀

8 . 怎么比对,这是关键

a) 我们遍历哈希表时,拿到的元素是string -> 那就试试想字符串相关接口

b) 有接口能在当前字符串查找前缀嘛? 查找前缀不知道,反正有在当前字符串查找一个子串的接口 find

c) 至于是否是前缀,就要看find的返回值是否是0(find的返回值:

那代码跃然纸上:

cpp 复制代码
    bool startsWith(string prefix) {
        for(const string& word:hash)
        {
            if(word.find(prefix) == 0)
                return true;
        }
        return false;
    }

完整代码:

cpp 复制代码
class Trie {
public:
    Trie() {
        
    }
    ~Trie()
    {
        
    }
    void insert(string word) {
        if(!hash.count(word))
            hash.insert(word);
    }
    
    bool search(string word) {
        return hash.count(word) == 1;
    }
    
    bool startsWith(string prefix) {
        for(const string& word:hash)
        {
            if(word.find(prefix) == 0)
                return true;
        }
        return false;
    }
private:
        std::unordered_set<string> hash;
};

树型结构

1 . 此方法为本章核心目标

2 . 我们实现的当真是前缀树否? 非也,只是个带了前缀搜索的哈希表(苦笑)

3 . 还记得树得天独厚的结构:比如二叉树查找时间复杂度O(logN)

4 . 我们要想办法实现一个树结构+前缀搜索功能------>前缀树
1 . 谈到"树",不免想到树结点 、递归 、前中后序等等回忆

2 . 首先是结构:

树 : 一棵树往往是给出它的根节点代表 一棵树

细看,树 : 由根节点和孩子结点们组成

3 . 此处的结点如何设计?(缺乏想象力,那就先画一点

如图:这是一棵树,它的根节点是'a',它的孩子有36个

a) 如果找单词:app ; 那么首先找到root的 a字符所在树,即找到图上这棵

b) 接下来找p , 顺着a(当前root为a )找到p字符所在树,即红色p那棵

4 . 相信你有点启示,但不够清晰;接下来代码会有点陡,但看懂了就会加深多叉树构建能力

cpp 复制代码
// 结点结构
struct TrieNode
{
    TrieNode()
    :_children(26,nullptr) // 初始化为26个空槽位
    {}
    ~TrieNode() // 析构:成员变量为含指针,手动析构->暗含递归
    {
        for(auto* child:_children)
        {
            delete child;
        }
        _isEnd = false;
    }
    std::vector<TrieNode*> _children; // 该结点存的是孩子们,只会有26种情况------所以大小为26,存的是孩子结点的指针
}

5 . 是有点抽象------这是在干嘛:Trie 树的结点本身并不直接存储字符,字符是通过「结点在数组中的索引位置」来隐含表示的

a) 即 0 下标表示 'a'

b) 而 _children[0] 则是'a'这棵树,即找到'a'的孩子们

c) 整棵树你想象出来,应该是:本结点有26个槽位(vector大小),每个非空的槽位都会指向一个新的、同样有 26 个槽位的节点

i) TrieNode本质是不是一个大小为26的数组? 是的

ii) 那26个槽位中其中一个元素存储的是什么 ?是TrieNode的地址

iii) 所以其中一个槽位能指向新的 26 个槽位
6 . 有结点,那就组织起来成为树:

cpp 复制代码
class Trie {
public:
    using TrieNode = struct TrieNode;//取别名,少打点字
    Trie() {
        _root = new TrieNode();// 初始化
    }
    ~Trie()
    {
        delete _root;
    }
    void insert(string word) {
        
    }
    
    bool search(string word) {
        
    }
    
    bool startsWith(string prefix) {
        
    }
    private:
        TrieNode* _root; // 用该树的根节点表示整棵树
};

7 . 试着完成insert。设想:遍历word的每个字母然后一个一个存入前缀树------因为TrieNode没必要单独开一个char存储当前结点的字符,所以直接用槽位的下标表示。

cpp 复制代码
    void insert(string word) {
        for(auto& c : word)
        {
            TrieNode* pcur = _root;// 不直接操作_root
            int idx = c - 'a';//获取到本次字母的下标
            if(pcur->_children[idx])
            {
                // 如果pcur->_children[idx]有效,意味着该字母存过
            }
            else
            {
                // 如果pcur->_children[idx]无效,意味着该字母没存过
                // 没存过 ? 那就存;存该字母,就是"存下标"->让该下标有效->让它有存放孩子的槽位

            }
        }
    }
cpp 复制代码
    void insert(string word) {
        TrieNode* pcur = _root
        for(auto& c : word)
        {
            int idx = c - 'a';//获取到本次字母的下标
            if(!pcur->_children[idx])
            {
                pcur->_children[idx] = new TrieNode();
            }
            pcur = pcur->_child[idx];// 接下来的字母就应该存在它的槽位中,pcur自然要更新
        }
    }

8 . 有个bug需注意

1)我们在后面树的时候,如果插入apple ,app 两个单词后

2)搜索app时,我们该怎么区分apple 和 app呢 ?

3)给结点再加一个标志字段 : _isEnd 用于搜索目标单词时判断是否结尾

i. 如果搜索app ,此时遍历到p最后一个字母,然后再判断该结点是否结束字母即可

ii. 如果搜索apple ,此时遍历到e最后一个字母,然后再判断该结点是否结束字母即可

所以改后的 结点结构 + insert :

cpp 复制代码
struct TrieNode
{
    TrieNode()
    :_children(26,nullptr),
    _isEnd(false)
    {}
    ~TrieNode() // 析构:成员变量为含指针,手动析构->暗含递归
    {
        for(auto* child:_children)
        {
            delete child;
        }
        _isEnd = false;
    }
    std::vector<TrieNode*> _children;
    bool _isEnd;// 添加字段
}
class Trie {
public:
    using TrieNode = struct TrieNode;
    Trie() {
        _root = new TrieNode();
    }
    
    void insert(string word) {
        TrieNode* pcur = _root
        for(auto& c : word)
        {
            int idx = c - 'a';//获取到本次字母的下标
            if(!pcur->_children[idx])
            {
                pcur->_children[idx] = new TrieNode();
            }
            pcur = pcur->_child[idx];
        }
        pcur->_isEnd = true;// 插入完毕,记得给最后一个结点修改标志位
    }

9 . 有了insert , 那search就是照猫画虎:

思路:遍历给定的word,在树里挨个搜索:

a)如果遇到某个字母在树里不存在,那么返回false

b) 全部存在,但需要最后判断是否以最后一个字母结尾

cpp 复制代码
    bool search(string word) {
        TrieNode* pcur = _root;
        for(auto& c:word)
        {
            int idx = c - 'a';
            if(!pcur->_children[idx])
                return false;
            pcur = pcur->_children[idx];
        }
        return pcur->_isEnd == true;
    }

10 . startsWith 则是需要查找是否存在给定prefix的单词

11 . 放在树型结构里,和search的思路很像------唯一区别是我们只要求这个prefix存在即可,并不要求它是一个单词

cpp 复制代码
    bool startsWith(string prefix) {
        TrieNode* pcur = _root;
        for(auto& c:prefix)
        {
            int idx = c - 'a';
            if(!pcur->_children[idx])
                return false;
            pcur = pcur->_children[idx];
        }
        return true;
    }

整合代码:

cpp 复制代码
struct TrieNode
{
    TrieNode()
    :_children(26,nullptr),
    _isEnd(false)
    {}
    ~TrieNode() // 析构:成员变量为含指针,手动析构->暗含递归
    {
        for(auto* child:_children)
        {
            delete child;
        }
        _isEnd = false;
    }
    std::vector<TrieNode*> _children;
    bool _isEnd;
};
class Trie {
public:
    using TrieNode = struct TrieNode;
    Trie() {
        _root = new TrieNode();
    }
    ~Trie()
    {
        delete _root;
    }
    void insert(string word) {
        TrieNode* pcur = _root;
        for(auto& c : word)
        {
            int idx = c - 'a';//获取到本次字母的下标
            if(!pcur->_children[idx])
            {
                pcur->_children[idx] = new TrieNode();
            }
            pcur = pcur->_children[idx];
        }
        pcur->_isEnd = true;
    }
    
    bool search(string word) {
        TrieNode* pcur = _root;
        for(auto& c:word)
        {
            int idx = c - 'a';
            if(!pcur->_children[idx])
                return false;
            pcur = pcur->_children[idx];
        }
        return pcur->_isEnd == true;
    }
    
    bool startsWith(string prefix) {
        TrieNode* pcur = _root;
        for(auto& c:prefix)
        {
            int idx = c - 'a';
            if(!pcur->_children[idx])
                return false;
            pcur = pcur->_children[idx];
        }
        return true;
    }
    private:
        TrieNode* _root;
};

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

总结以及完整参考代码

cpp 复制代码
class Trie {
public:
    Trie() {
        
    }
    ~Trie()
    {
        
    }
    void insert(string word) {
        if(!hash.count(word))
            hash.insert(word);
    }
    
    bool search(string word) {
        return hash.count(word) == 1;
    }
    
    bool startsWith(string prefix) {
        for(const string& word:hash)
        {
            if(word.find(prefix) == 0)
                return true;
        }
        return false;
    }
private:
        std::unordered_set<string> hash;
};
cpp 复制代码
struct TrieNode
{
    TrieNode()
    :_children(26,nullptr),
    _isEnd(false)
    {}
    ~TrieNode() // 析构:成员变量为含指针,手动析构->暗含递归
    {
        for(auto* child:_children)
        {
            delete child;
        }
        _isEnd = false;
    }
    std::vector<TrieNode*> _children;
    bool _isEnd;
};
class Trie {
public:
    using TrieNode = struct TrieNode;
    Trie() {
        _root = new TrieNode();
    }
    ~Trie()
    {
        delete _root;
    }
    void insert(string word) {
        TrieNode* pcur = _root;
        for(auto& c : word)
        {
            int idx = c - 'a';//获取到本次字母的下标
            if(!pcur->_children[idx])
            {
                pcur->_children[idx] = new TrieNode();
            }
            pcur = pcur->_children[idx];
        }
        pcur->_isEnd = true;
    }
    
    bool search(string word) {
        TrieNode* pcur = _root;
        for(auto& c:word)
        {
            int idx = c - 'a';
            if(!pcur->_children[idx])
                return false;
            pcur = pcur->_children[idx];
        }
        return pcur->_isEnd == true;
    }
    
    bool startsWith(string prefix) {
        TrieNode* pcur = _root;
        for(auto& c:prefix)
        {
            int idx = c - 'a';
            if(!pcur->_children[idx])
                return false;
            pcur = pcur->_children[idx];
        }
        return true;
    }
    private:
        TrieNode* _root;
};

/**
 * 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);
 */
相关推荐
寻寻觅觅☆5 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc5 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
偷吃的耗子5 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
l1t5 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
2013编程爱好者6 小时前
【C++】树的基础
数据结构·二叉树··二叉树的遍历
NEXT066 小时前
二叉搜索树(BST)
前端·数据结构·面试
化学在逃硬闯CS6 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar1236 小时前
C++使用format
开发语言·c++·算法
码说AI7 小时前
python快速绘制走势图对比曲线
开发语言·python