数据结构之前缀树(Trie)

本文介绍了前缀树(Trie),它是用于字符串查询、统计、排序的数据结构。通过合并相同前缀字符串的链形成子树,还探讨了字符串插入Trie时遇到的问题及解决办法,如用val[]数组记录结点权值,最后给出了只有小写字母的Trie代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前缀树,又叫字典树,主要用于字符串(不限于字符串)查询、统计、排序的一种数据结构

比如,给定n个字符串,进行m次查询,每次查询给定一个字符串 t,问t 是否存在于那给定的n个字符串里

这里,我们用到了前缀树,即将每个字符串看作一条链,把拥有相同前缀的字符串的链的相同前缀给合并,形成一棵棵子树。如给定三个字符串his,her,hit,得到前缀树:

上图很清晰了,在右边的树里面从根出发,一直到叶子结点,可以找到his,hit,her三个单词

那么问题来了,如果再来一个字符串it,怎么插入到这棵Trie里?显然这是无法接到根节点h下面的,因为it第一个字母不是h~~

那么我们可以令一个空结点为根,所有子树都从这个空结点出发,即:

这时又有了一个新的问题,it插进Trie了,如果再有一个its怎么办?直接在it后面加一个s,那么不就把it覆盖掉了吗?

这个好解决,只需要使用一个val[]数组记录每一个结点的权值即可,从根开始,向下查找,找到某个结点i,val[i]==0表示当前得到的字符串只是一个前缀,并不在我们之前插入到Trie里的众多字符串(后面称之为字典)里,不是0则说明这个字符串在字典里。额,我懒地画图了,反正这玩意儿也好理解。

然后就是实现了:使用0表示根节点(就是那个空结点)然后1,2,3......依次表示下面的结点,第一个插入到Trie里的为1(即id为1),第二个插入到Trie里的为2(即id为2),以此类推。使用二维数组ch[id][son]来表示Trie,ch[i][j]表示的是id为i对应结点的j儿子的id。至于ch[i][j]那个j怎么表示字符,就看情况而定了,比如如果只会出现小写字母,那就用0,1,2,....,25分别表示a,b,c...,z即可。

给出只有小写字母的的Trie代码:

struct Trie
{
    int ch[maxnode][sigma_size];//maxnode是结点总数,sigma_size是字母表的大小(即可能出现多少种字符,0~sigma_size与字符一一对应)
    int val[maxnode];
    int sz;//结点总数
    Trie(){ sz = 1; memset(ch[0], 0, sizeof ch[0]);}//起始只有根节点
    int idx(char c) {   return c-'a';} // 字符c的编号

    // 插入字符串s,附加信息为v(例如字符串的权值等),v非0,因为0表示本结点不是单词结点
    void insert(char* s, int v)
    {
        int u = 0, n = strlen(s);
        for(int i = 0; i < n; ++ i)
        {
            int c = idx(s[i]);
            if(!ch[u][c])//结点不存在
            {
                memset(ch[sz], 0, sizeof ch[sz]);
                val[sz] = 0;
                ch[u][c] = sz ++;
            }
            u = ch[u][c];//往下走
        }
        val[u] = v;//字符串的最后一个字符的附加信息为v
    }

    // 查询字符串s,如果存在返回附加值,否则返回0
    int find(char* s)
    {
        int u = 0, n = strlen(s);
        for(int i = 0; i < n; ++ i)
        {
            int c = idx(s[i]);
            if(!ch[u][c])   return 0;
            u = ch[u][c];
        }
        return val[u];
    }
    
};

 

好的,以下是使用 Kotlin 实现 Trie 的代码: ```kotlin class Trie() { private val root = TrieNode() fun insert(word: String) { var cur = root for (c in word) { if (!cur.children.containsKey(c)) { cur.children[c] = TrieNode() } cur = cur.children[c]!! } cur.isEnd = true } fun search(word: String): Boolean { var cur = root for (c in word) { if (!cur.children.containsKey(c)) { return false } cur = cur.children[c]!! } return cur.isEnd } fun startsWith(prefix: String): Boolean { var cur = root for (c in prefix) { if (!cur.children.containsKey(c)) { return false } cur = cur.children[c]!! } return true } } class TrieNode { val children = mutableMapOf<Char, TrieNode>() var isEnd = false } ``` 在上面的代码中,我们定义了 `Trie` 类和 `TrieNode` 类,其中 `TrieNode` 表示前缀的节点,包含一个表示是否为单词结尾的布尔值和一个 `children` 字典,用于存储子节点。`Trie` 类中包含 `insert`、`search` 和 `startsWith` 三个方,分别用于插入、查找和判断是否存在前缀。在 `insert` 方中,我们遍历字符串的每个字符,如果当前节点的子节点中没有该字符,则新建一个 `TrieNode` 并加入子节点中。在遍历结束后,将最后一个节点标记为单词结尾。在 `search` 和 `startsWith` 方中,我们同样遍历字符串的每个字符,如果当前节点的子节点中没有该字符,则直接返回 false。如果遍历结束后,当前节点是单词结尾,则说明该单词存在,返回 true;否则说明该单词不存在,返回 false。在 `startsWith` 方中,我们不需要判断当前节点是否为单词结尾,只需判断该前缀是否存在即可。 希望能对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值