LeetCode 剑指 Offer II 前缀树(上) 专题总结

  • 📚 博客主页:⭐️这是一只小逸白的博客鸭~⭐️
  • 👉 欢迎 关注❤️点赞👍收藏⭐️评论📝
  • 😜 小逸白正在备战实习,经常更新面试题LeetCode题解,欢迎志同道合的朋友互相交流~
  • 💙 若有问题请指正,记得关注哦,感谢~

往期文章 :

第一题了解前缀树比较简单,后面两题应用就稍微难一点,代码量都挺高的,但思路很简单,希望别劝退新手

062. 实现前缀树

题目:

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

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

示例:

输入
inputs = [“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”]
inputs = [[], [“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

提示:

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

思路:

Trie,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:

  • 指向子节点的指针数组 children。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children[0] 对应小写字母 achildren[1] 对应小写字母 b,…,children[25] 对应小写字母 z
  • 布尔字段 isEnd,表示该节点是否为字符串的结尾。

我习惯用next替代children,因为比较好写吧,这题相当于介绍前缀树,所以代码也很简单,不懂的可以先百度前缀树,了解了再来看,无非就是把二叉树替换成26叉树,然后一个bool判断是否到达结尾,然后掌握插入和查询方法。

class Trie {
private:
    // 前缀树, 相当于26叉树
    vector<Trie*> next;
    // 判断是否为单词终点
    bool isWrod;

    // 因为search,startsWith方法都需要查询,所以提取出查询方法
    // 直接返回查询最后的点
    Trie* searchPrefix(string prefix) {
        Trie* node = this;
        for(char ch : prefix) {
            int index = ch - 'a';
            if(node->next[index] != nullptr) {
                node = node->next[index];
            }else {
                return nullptr;
            }
        }
        return node;
    }
public:
    
    Trie():next(26),isWrod(false) {}
    
    void insert(string word) {
    int count = 0;
        // 指向当前类,每个类都有26个分支
        Trie* node = this;
        for(char ch : word) {
            int index = ch - 'a';
            if(node->next[index] == nullptr) {
                // 创建父类节点的index节点,此时index也有26个分支
                node->next[index] = new Trie();
            }
            // 将父类指向index节点
            node = node->next[index];
        }
        // 创建完当前node指向最后一个单词,设为单词终点
        node->isWrod = true;
    }
    
    // 不为空 and 最后一个点的isWrod为true 代表word存在
    bool search(string word) {
        Trie* node = this->searchPrefix(word);
        return node != nullptr && node->isWrod;
    }
    // 不为空即查到了前缀
    bool startsWith(string prefix) {
        Trie* node = this->searchPrefix(prefix);
        return node != nullptr;
    }
};

063. 替换单词

题目:

在英语中,有一个叫做 词根(root) 的概念,它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。
现在,给定一个由许多词根组成的词典和一个句子,需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。
需要输出替换之后的句子。

示例:

输入:dictionary = [“cat”,“bat”,“rat”], sentence = “the cattle was rattled by the battery”
输出:“the cat was rat by the bat”

提示:

  • 1 <= dictionary.length <= 1000
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写字母组成。
  • 1 <= sentence.length <= 10^6
  • sentence 仅由小写字母和空格组成。
  • sentence 中单词的总量在范围 [1, 1000] 内。
  • sentence 中每个单词的长度在范围 [1, 1000] 内。
  • sentence 中单词之间由一个空格隔开。
  • sentence 没有前导或尾随空格。

思路:

  1. 将词根加入到前缀树中,insert方法跟上一题一样
  2. 分割字符串,将单词保存到数组中
  3. 处理字符串,就是找到词根就截取单词的词根部分,否则直接添加单词
    prefixLen方法跟上题查询方法很像,本来查到结尾返回true,现在返回词根长度,查不到结尾就返回0,方便判断是否查到词根
class Trie {
private:
    bool isWord;
    vector<Trie*> next;
public:
    Trie():next(26,nullptr),isWord(false){};
    // 跟上一题一样
    void insert(const string& word) {
        Trie* node = this;
        for(char ch : word) {
            int index = ch - 'a';
            if(node->next[index] == nullptr) {
                node->next[index] = new Trie();
            }
            node = node->next[index];
        }
        node->isWord = true;
    }
    // 相对于上一题的search稍作修改
    int prefixLen(const string& word) {
        Trie* node = this;
        // 返回前缀的长度
        int len = 0;
        for(char ch : word) {
            int index = ch - 'a';
            // 没有这个前缀
            if(node->next[index] == nullptr) {
                return 0;
            }
            node = node->next[index];
            len++;
            // 找到这个前缀并返回这个前缀的长度
            if(node->isWord) 
                return len;
        }
        return 0;
    }
};

class Solution {
public:
    string replaceWords(vector<string>& dictionary, string sentence) {
        Trie* root = new Trie();
        // 将词根加入前缀树
        for(auto& word : dictionary) {
            root->insert(word);
        }

        // 分割字符串
        vector<string> words{""};
        for(char ch : sentence) {
            if(ch == ' ') {
                words.push_back("");
            }else {
                words.back().push_back(ch);
            }
        }
        // 处理字符串
        string res;
        for(auto& word : words) {
            int len = root->prefixLen(word);
            // 没找到直接添加
            if(len == 0){
                res += word + " ";
            }else {
                // 找到前缀只添加前缀部分
                res += word.substr(0, len) + " ";
            }
        }
        res.pop_back();
        return res;
    }
};

064. 神奇的字典

题目:

设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于已构建的神奇字典中。
实现 MagicDictionary 类:
MagicDictionary() 初始化对象
void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同
bool search(String searchWord) 给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成

示例:

输入
inputs = [“MagicDictionary”, “buildDict”, “search”, “search”, “search”, “search”]
inputs = [[], [[“hello”, “leetcode”]], [“hello”], [“hhllo”], [“hell”], [“leetcoded”]]
输出
[null, null, false, true, false, false]
解释
MagicDictionary magicDictionary = new MagicDictionary();
magicDictionary.buildDict([“hello”, “leetcode”]);
magicDictionary.search(“hello”); // 返回 False
magicDictionary.search(“hhllo”); // 将第二个 ‘h’ 替换为 ‘e’ 可以匹配 “hello” ,所以返回 True
magicDictionary.search(“hell”); // 返回 False
magicDictionary.search(“leetcoded”); // 返回 False

提示:

  • 1 <= dictionary.length <= 100
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写英文字母组成
  • dictionary 中的所有字符串 互不相同
  • 1 <= searchWord.length <= 100
  • searchWord 仅由小写英文字母组成
  • buildDict 仅在 search 之前调用一次
  • 最多调用 100search

思路:

前缀树 + dfs
初始化插入方法跟前两题一样
查询方法:

  • 如果 达到字典单词的末尾 and 查询单词的末尾 and 修改次数 ==1 代表查询成功
  • 遍历26个可能的字母,如果有一个true 直接返回true

以后函数传参记得将字符串这种类型的能加上引用&就加上
效率差了十倍,内存差了6倍,恐怖如斯
因为不加引用,dfs调用函数时会再创建一个形参string费时间空间,加引用的话直接取传递string参数,就不用再创建

在这里插入图片描述
在这里插入图片描述

class Trie {
public:
    vector<Trie*> next;
    bool isWord;
    Trie():next(26,nullptr),isWord(false){}
    void insert(const string& word) {
        Trie* node = this;
        for(auto& ch : word) {
            int index = ch - 'a';
            if(node->next[index] == nullptr) {
                node->next[index] = new Trie();
            }
            node = node->next[index];
        }
        node->isWord = true;
    }
};

class MagicDictionary {
private:
    Trie* root;
    bool dfs(Trie* root, string& word, int index, int edit) {
        if(root == nullptr || edit > 1) return false;
        //如果root正好在字典字符串结尾,且index刚好到达字符串末尾,且只被修改过一个字符
        if(root->isWord && index == word.size() && edit == 1) {
            return true;
        }
        if(index >= word.size())
            return false;

        //必须都遍历,否则如果 hello, hallo  查询hello直接查询完,就不会返回true
        for(int i = 0; i < 26; i++) {
            //如果子节点存在,edit不变;否则,edit加1;
            int nextEdit = i == word[index] - 'a'? edit : edit + 1;
            // 遍历26种,直到返回true
            if(dfs(root->next[i], word, index + 1, nextEdit))
                return true;
        }
        return false;
    }

public:
    /** Initialize your data structure here. */
    MagicDictionary() {
        root = new Trie();
    }
    
    void buildDict(vector<string> dictionary) {
        for(auto& word : dictionary) {
            root->insert(word);
        }
    }
    
    bool search(string searchWord) {
        return dfs(root, searchWord, 0, 0);
    }
};

  • 15
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小逸白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值