前缀树

作者: zyyupup | 来源:发表于2020-08-11 13:56 被阅读0次

1.什么是前缀树

前缀树是N叉树的一种特殊形式。通常来说,一个前缀树是用来存储字符串的。前缀树的每一个节点代表一个字符串(前缀)。每一个节点会有多个子节点,通往不同子节点的路径上有着不同的字符。子节点代表的字符串是由节点本身的原始字符串,以及通往该子节点路径上所有的字符组成的。

下面是前缀树的一个例子


image.png

在上图示例中,我们在节点中标记的值是该节点对应表示的字符串。例如,我们从根节点开始,选择第二条路径 'b',然后选择它的第一个子节点 'a',接下来继续选择子节点 'd',我们最终会到达叶节点 "bad"。节点的值是由从根节点开始,与其经过的路径中的字符按顺序形成的。

值得注意的是,根节点表示空字符串。

前缀树的一个重要的特性是,节点所有的后代都与该节点相关的字符串有着共同的前缀。这就是前缀树名称的由来。

我们再来看这个例子。例如,以节点 "b" 为根的子树中的节点表示的字符串,都具有共同的前缀 "b"。反之亦然,具有公共前缀 "b" 的字符串,全部位于以 "b" 为根的子树中,并且具有不同前缀的字符串来自不同的分支。

前缀树有着广泛的应用,例如自动补全,拼写检查等等。我们将在后面的章节中介绍实际应用场景。

2 如何表示一个前缀树?

前缀树的特别之处在于字符和子节点之间的对应关系。有许多不同的表示前缀树节点的方法,这里我们只介绍其中的两种方法。

2.1 数组

第一种方法是用数组存储子节点。

例如,如果我们只存储含有字母 a 到 z 的字符串,我们可以在每个节点中声明一个大小为26的数组来存储其子节点。对于特定字符 c,我们可以使用 c - 'a' 作为索引来查找数组中相应的子节点。

// change this value to adapt to different cases
#define N 26

struct TrieNode {
    TrieNode* children[N];
    
    // you might need some extra values according to different cases
};

/** Usage:
 *  Initialization: TrieNode root = new TrieNode();
 *  Return a specific child node with char c: (root->children)[c - 'a']
 */

访问子节点十分快捷。访问一个特定的子节点比较容易,因为在大多数情况下,我们很容易将一个字符转换为索引。但并非所有的子节点都需要这样的操作,所以这可能会导致空间的浪费。

#include <iostream>
#include <map>
#include <vector>
#define N 26
using namespace std;
class Trie{
    private:
        struct Node{
            Node* next[N];
            bool is_w = false;//是否是一个完整的单词
        };
        Node* trie;
    public:
        Trie(){
            trie = new Node();
        }
        /*insert word*/
        void insert(const string & str){
            Node* cur = trie;
            for(int i = 0;i < str.size();i++){
                if(cur->next[str[i] - 'a'] == nullptr)
                    cur->next[str[i] - 'a']= new Node();
                cur = cur->next[str[i] - 'a'];
            }
            cur->is_w = true;
        }
        bool search(const string & str) {
            Node* cur = trie;
            for(int i = 0;i < str.size();i++){
                if(cur->next[str[i] - 'a'] != nullptr)
                    cur = cur->next[str[i] - 'a'];
                else
                    return false;
            
            }
            if(cur->is_w) return true;
            else return false;
        }
};

int main(){
    Trie t;
    t.insert("apple");
    t.insert("banana");
    cout << t.search("apple") << endl;
    cout << t.search("banana") << endl;
    return 0;
}

2.2 Map

第二种方法是使用 Hashmap 来存储子节点。
我们可以在每个节点中声明一个Hashmap。Hashmap的键是字符,值是相对应的子节点。

struct TrieNode {
    unordered_map<char, TrieNode*> children;
    
    // you might need some extra values according to different cases
};

/** Usage:
 *  Initialization: TrieNode root = new TrieNode();
 *  Return a specific child node with char c: (root->children)[c]
 */

通过相应的字符来访问特定的子节点更为容易。但它可能比使用数组稍慢一些。但是,由于我们只存储我们需要的子节点,因此节省了空间。这个方法也更加灵活,因为我们不受到固定长度和固定范围的限制。

我们已经提到过如何表示前缀树中的子节点。除此之外,我们也需要用到一些其他的值。

例如,我们知道,前缀树的每个节点表示一个字符串,但并不是所有由前缀树表示的字符串都是有意义的。如果我们只想在前缀树中存储单词,那么我们可能需要在每个节点中声明一个布尔值(Boolean)作为标志,来表明该节点所表示的字符串是否为一个单词。

3 leetcode题目最长公共前缀

思路非常简单,由于是寻找最长前缀,所以只需要插入第一个字符串,后续字符串分别进行匹配即可,更优化一点可以先遍历字符数组,找到最短的那个字符串,再进行匹配。

class Solution {
public:
    struct Node{
        Node* next[26];
        bool isStr;
    };
    void insertTrie(Node* root,const string &str){
        if(root == nullptr) return;
        for(int i = 0;i < str.size();i++){
                if(root->next[str[i] - 'a'] == nullptr){
                    root->next[str[i] - 'a'] = new Node();
                }
                root = root->next[str[i] - 'a'];
        }
        root->isStr = true;
    }
    int searchCommonPrefix(Node* root, const string& str){
        if(root == nullptr) return 0;
        int maxCommon = 0;
        for(int i = 0;i < str.size();i++){
            if(root->next[str[i] - 'a'] == nullptr)
                return maxCommon;
            else{
                maxCommon++;
                root = root->next[str[i] - 'a'];
            }
        }
        return maxCommon;
    }
    string longestCommonPrefix(vector<string>& strs) {
        string result = "";
        if(strs.size() == 0) return result;
        if(strs.size() == 1) return strs[0];
        Node* trie = new Node();
        insertTrie(trie,strs[0]);
        int maxCommon = strs[0].size();
        for (int i = 1;i < strs.size();i++){
            maxCommon = maxCommon > searchCommonPrefix(trie,strs[i]) ? searchCommonPrefix(trie,strs[i]) : maxCommon;
            if(maxCommon == 0) return result;
        }

        for(int i = 0;i <maxCommon;i++){
            result += strs[0][i];
        }
        return result;
    }
};

字典树参考:https://www.cnblogs.com/vincent1997/p/11237389.html

相关文章

  • 前缀树(字典树/Trie)Java实现和应用

    摘要: 前缀树,字典树,插入查询逻辑,Java实现,时间复杂度分析 前缀树介绍 Trie树又被称为前缀树、字典树,...

  • 数据结构基础--前缀树&&后缀树

    本文只是自己的笔记,并不具备过多的指导意义。 前缀树 何为前缀树 前缀树又名字典树,单词查找树,Trie树,是一种...

  • 前缀树

    前缀树又名Tries树、字典树、单词查找树等,常用于快速检索,大量字符串的排序和统计等。 三个基本性质 根节点不包...

  • 前缀树

    前缀树原理

  • 前缀树

    概念: 简述:又名单词查找树,tries树,一种多路树形结构,常用来操作字符串(但不限于字符串),和hash效率有...

  • 前缀树

    题目 给定一个字符串数组,其中不含有重复字符串,判断是否有字符串是另一个字符串的前缀 思路 实现前缀树即可,判断是...

  • 前缀树

    1.什么是前缀树 前缀树是N叉树的一种特殊形式。通常来说,一个前缀树是用来存储字符串的。前缀树的每一个节点代表一个...

  • 前缀树

    最近看代码,发现了一个敏感词检测是用前缀树写的,看起来速度蛮快,毕竟是拿空间换时间,LOG倍速。但是缺点也很明显,...

  • 前缀树

  • 前缀树

    问题 已知一个字符串数组words=['accommodate','accompany','accord','ac...

网友评论

      本文标题:前缀树

      本文链接:https://www.haomeiwen.com/subject/wvpsdktx.html