算法-字典树

字典树

  • 字典树
  • 面试
  • 例题
    • 实现字典树
    • [添加与搜索单词 - 数据结构设计](#添加与搜索单词 - 数据结构设计)
  • [720. 词典中最长的单词](#720. 词典中最长的单词)
  • [212. 单词搜索 II](#212. 单词搜索 II)
  • [648. 单词替换](#648. 单词替换)

字典树

节点定义:

cpp 复制代码
struct TrieNode {
    // 核心 1:路标数组
    // 大小为 26,代表 a-z。
    // 如果 children[0] 不为空,说明有通往 'a' 的路。
    TrieNode* children[26]; 

    // 核心 2:终点标记
    // true 表示从根走到这里,组成了一个完整的单词。
    bool isEnd; 

    TrieNode() {
        isEnd = false;
        // 初始化:刚开始所有路都不通
        for (int i = 0; i < 26; i++) children[i] = nullptr;
    }
};

距离:现在有一个空的 Trie,我们要插入字符串 "apple"。

  1. 站在 Root 节点
  2. 看第一个字 'a':检查 root->children['a'-'a'] (即索引0) 是否存在?
    不存在(nullptr)。
    动作:new 一个新节点,把路修好。
    移动:指针跳到这个新节点。
  3. 看第二个字 'p':检查当前节点的 children['p'-'a'] (索引15) 是否存在?
    不存在。动作:new 一个新节点。移动:指针跳到新节点。
  4. ...重复直到 'e'...
  5. 在 'e' 对应的节点上,打个勾 isEnd = true。
    插入逻辑
cpp 复制代码
void insert(string word) {
    TrieNode* node = root; // 1. 指针站在根部
    
    for (char c : word) {
        int index = c - 'a'; // 算出走哪条路 (0-25)
        
        // 2. 路没修?那就修一条
        if (node->children[index] == nullptr) {
            node->children[index] = new TrieNode();
        }
        
        // 3. 往前走一步
        node = node->children[index];
    }
    
    // 4. 走完所有字符,标记这里是终点
    node->isEnd = true;
}

查找逻辑(最关键)

  1. Trie 里有 "apple",我要查 "app" 存在吗?
    指针顺着 a -> p -> p 走得很顺畅。
    循环结束了,指针停在了第二个 'p' 的节点上。
    关键判断:此时检查该节点的 isEnd。
    因为我们只插入了 "apple",所以在第二个 'p' 处,isEnd 是 false。
    结论:返回 false。 "app" 只是前缀,不是完整单词。
  2. 场景 B:Trie 里有 "apple",我要查 "banana" 存在吗?
    第一个字母 'b'。
    检查 root 下的 children['b'-'a']。
    发现是 nullptr(路不通)。
    结论:直接返回 false。
cpp 复制代码
bool search(string word) {
    TrieNode* node = root;
    for (char c : word) {
        int index = c - 'a';
        
        // 路断了,直接判定不存在
        if (node->children[index] == nullptr) {
            return false;
        }
        
        node = node->children[index];
    }
    
    // 走到最后了,不仅要路通,还得必须是"终点"
    return node->isEnd; 
}

面试

这是 Trie 碾压哈希表的地方。

如果面试官问:"我要设计一个搜索引擎的自动补全功能,用户输入 'te',我要知道有没有以 'te' 开头的词。"

你用哈希表做不到(除非遍历整个哈希表)。但 Trie 只需要把上面的 search 代码改 一行:

cpp 复制代码
bool startsWith(string prefix) {
    TrieNode* node = root;
    for (char c : prefix) {
        int index = c - 'a';
        if (node->children[index] == nullptr) return false;
        node = node->children[index];
    }
    // 区别就在这!
    // 只要能走完路径,不管是不是终点,都说明前缀存在。
    return true; 
}

例题

实现字典树

cpp 复制代码
class Trie {
private:

    struct TrieNode{
        TrieNode* childen[26];
        bool isEnd;

        TrieNode(){
            isEnd = false;
            for(int i = 0;i<26;i++){
                childen[i] = nullptr;
            }
        }
    };
    TrieNode* root;
public:
    Trie() {
        root =  new TrieNode();
    }
    
    void insert(string word) {
        TrieNode* node = root;
        for(char c : word){
            int index = c-'a';
            if(node->childen[index] == nullptr){
                node->childen[index] = new TrieNode();
            }
            node = node->childen[index];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        TrieNode* node = root;
        for(char c:word){
            int index = c-'a';
            if(node->childen[index] == nullptr){
                return false;
            }
            node = node->childen[index];
        }
        return node->isEnd;
    }
    
    bool startsWith(string prefix) {
        TrieNode* node = root;
        for (char c : prefix) {
            int index = c - 'a';
            if (node->childen[index] == nullptr) {
                return false;
            }
            node = node->childen[index];
        }
        return true; // 只要能走完路径,就说明前缀存在
    }
};

添加与搜索单词 - 数据结构设计

添加与搜索单词 - 数据结构设计

和上一题一样先定义TrieNode节点,以及添加单词

不同的地方在于这题结合了dfs深度优先搜索

首先传入要搜索的词,当前搜索到 word 的第几个字符,当前处在 Trie 树的哪个节点

如果搜索到index == word.size()的时候,返回字母尾巴的isEnd(注意尾巴的isEnd都是true)

接下来定义一下此时的word 的第几个字符(c)

两个if判断如果是正常字符的话就看,此时节点的孩子中是不是nullptr,如果是的话就直接返回false

如果不是进入dfs递归循环

如果是'.'的话利用for循环遍历26个孩子,只要有一个不是nullptr就是可以继续

如果不是nullptr的话进入向下判断

cpp 复制代码
class WordDictionary {
private:
    struct TrieNode{
        TrieNode* childen[26];
        bool isEnd;

        TrieNode(){
            isEnd = false;
            for(int i = 0;i<26;i++){
                childen[i] = nullptr;
            }
        }
    };
    TrieNode* root;
    bool dfs(string word,int index, TrieNode* node){
        if(index == word.size()){
            return node->isEnd;
        }
        char c = word[index];

        if(c != '.'){
            int i = c-'a';
            if(node->childen[i] == nullptr) return false;
             
            return dfs(word,index+1,node->childen[i]);
        }else{
            for(int i = 0; i < 26; i++){
                if (node->childen[i] != nullptr){
                    if (dfs(word, index + 1, node->childen[i])){
                        return true;
                    }
                }
            }
            return false;
        }
    }
public:
    WordDictionary() {
        root = new TrieNode();
    }
    
    void addWord(string word) {
        TrieNode* node = root;
        for(char c:word){
            int index = c-'a';
            if(node->childen[index] == nullptr){
                node->childen[index] = new TrieNode();
            }
            node = node->childen[index];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        return dfs(word,0,root);
    }
};

720. 词典中最长的单词

720. 词典中最长的单词

常规的创建字典树节点结构体,但是新加了一个string word(此时输出结果单词)方便后续的结果替换

另外创建结果字符串(刚开始是空的)

插入也是常规插入,但是多了一个记录此时插入的word

最重要的dfs遍历,首先传入根节点开始dfs的逻辑

在dfs里面,首先判断新进入的节点是不是根节点,因为根节点是空的,我们需要跳过他,去找真正的第一个节点

然后开始判断这里node中word的长度是不是大于result的长度,如果大于就直接将结果替换

或者他们长度相同的情况下,判断此时node的长度是不是比result小,如果小也替换result

遍历26个子节点,如果他们是的isEnd是True(代表之前出现过)(体现原题目中的逐步添加)则继续往下走

cpp 复制代码
class Solution {
private:
    struct TrieNode{
        TrieNode* children[26];
        bool isEnd;
        string word;

        TrieNode(){
            isEnd = false;
            word = "";
            for(int i = 0;i<26;i++){
                children[i] = nullptr;
            }
        }
    };
    TrieNode* root;
    string result = "";
public:
    void insert(const string& w){
        TrieNode* node = root;
        for(char c:w){
            int index = c-'a';
            if(node->children[index] == nullptr){
                node->children[index] = new TrieNode();
            }
            node = node->children[index];
        }
        node->isEnd = true;
        node->word = w;
    }
    void dfs(TrieNode* node){
        if(node != root){
            if(node->word.size() > result.length() || (node->word.length() == result.length() && node->word < result)){
                result = node->word;
            }
        }

        for(int i = 0;i<26;i++){
            if(node->children[i]!=nullptr && node->children[i]->isEnd){
                dfs(node->children[i]);
            }
        }
    }
    string longestWord(vector<string>& words) {
        root = new TrieNode();
        for(const string& w:words){
            insert(w);
        }
        dfs(root);
        return result;
    }
};

212. 单词搜索 II

212. 单词搜索 II

常规插入和建树,但是这里不需要isEnd

判断是不是出棋盘了,出了就返回

然后记录此时的字符和index

看看下面有没有路,没路就返回

有路的话记录下一个节点为nextNode

看看下一个节点里面的word是不是空的(建树的时候在最后一个节点哪里安排上word,不是最有一个节点都是空)

不是空就把结果取出来,并把结果消除(放出其他的路也能拿到这个结果,不同路同一个结果)

回溯操作,标记这个格子,防止回路死循环

上下左右以此递归

然后复原

cpp 复制代码
class Solution {
private:
    struct TrieNode{
        TrieNode* children[26];
        
        string word;
        TrieNode(){
            
            word = "";
            for(int i = 0;i<26;i++){
                children[i] = nullptr;
            }

        }
    };
    TrieNode* root;
    vector<string> result;

    void insert(const string& w){
        TrieNode* node = root;
        
        for(char c: w){
            int index = c-'a';
            if(node->children[index] == nullptr){
                node->children[index] = new TrieNode();
            }
            node = node->children[index];
        }
        node->word = w;
    }

    void dfs(vector<vector<char>>& board,int i,int j,TrieNode* node){
        if(i<0||i>=board.size()||j<0||j>=board[0].size()||board[i][j]=='#'){
            return;
        }

        char c= board[i][j];
        int index = c-'a';
        if (node->children[index] == nullptr) {
            return;
        }
        TrieNode* nextNode = node->children[index];
        if(!nextNode->word.empty()){
            result.push_back(nextNode->word);
            nextNode->word = "";
        }
        board[i][j] = '#';
        dfs(board,i-1,j,nextNode);
        dfs(board,i+1,j,nextNode);
        dfs(board,i,j+1,nextNode);
        dfs(board,i,j-1,nextNode);
        board[i][j] = c;
    }
public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        root = new TrieNode();
        for(const string& w : words){
            insert(w);
        }
        int m = board.size();
        int n = board[0].size();
        for(int i = 0;i<m;i++){
            for(int j = 0;j<n;j++){
                dfs(board,i,j,root);
            }
        }
        return result;
    }
};

648. 单词替换

648. 单词替换

cpp 复制代码
class Solution {
private:
    struct TrieNode{
        TrieNode* children[26];
        bool isEnd;

        TrieNode(){
            isEnd = false;
            for(int i =0;i<26;i++){
                children[i] = nullptr;
            }
        }
    };
    TrieNode* root;
    void insert(const string& word){
        TrieNode* node = root;
        for(char c : word){
            int index = c-'a';
            if(node->children[index] == nullptr){
                node->children[index] = new TrieNode();
            }
            node = node->children[index];
        }
        node->isEnd = true;
    }
    string findRoot(const string& word){
        TrieNode* node = root;
        string prefix = "";

        for(char c:word){
            int index = c-'a';
            if(node->children[index] == nullptr){
                return word;
            }
            node = node->children[index];
            prefix += c;

            if(node->isEnd){
                return prefix;
            }
        }
        return word;
    }
public:
    string replaceWords(vector<string>& dictionary, string sentence) {
        root = new TrieNode();
        for(const string& w:dictionary){
            insert(w);
        }
        stringstream ss(sentence);
        string word;
        string result = "";
        while(ss>>word){
            if(!result.empty()){
                result += " ";
            }
            result += findRoot(word);
        }
        return result;
    }
};
相关推荐
Tansmjs2 小时前
C++编译期数据结构
开发语言·c++·算法
diediedei2 小时前
C++类型推导(auto/decltype)
开发语言·c++·算法
索荣荣2 小时前
Java动态代理实战:从原理到精通
java·开发语言
兩尛2 小时前
c++的数组和Java数组的不同
java·开发语言·c++
roman_日积跬步-终至千里2 小时前
【Java并发】多线程/并发问题集
java·开发语言
调皮连续波(rsp_tiaopige)2 小时前
毫米波雷达 : OpenRadar(Matlab版本)正式发布
开发语言·matlab
独断万古他化2 小时前
【算法通关】前缀和:从一维到二维、从和到积,核心思路与解题模板
算法·前缀和
loui robot2 小时前
规划与控制之局部路径规划算法local_planner
人工智能·算法·自动驾驶
格林威2 小时前
Baumer相机金属焊缝缺陷识别:提升焊接质量检测可靠性的 7 个关键技术,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·计算机视觉·视觉检测·堡盟相机