【令人心动的算法】字典树
本文最后更新于100 天前,其中的信息可能已经过时,如有错误请发送邮件到727189340@qq.com

一个常见的字符串匹配问题是:在 $n$ 个字符串中,查找某个字符串。如果使用暴力法,需要逐位匹配每个字符串,复杂度为 $O(nm)$,其中 $m$ 为字符串的平均长度,这样的效率显然太低。

借用现实生活的经验,我们其实无需按照这 $n$ 个字符串的顺序一个一个看下来,而是先找到第一个字母,再看第二个字母……查找任意一个单词,查找次数都只要这个单词的长度,而和单词的总数量无关。

这就是 字典树(Trie,Retrieval Tree 的缩写),又译为前缀树,它模拟了这个过程,就像是翻阅字典一样。字典树是一种多叉树,如英文字母的字典树是 $26$ 叉树,数字的字典树是 $10$ 叉树。

字典树是其他很多算法和数据结构的基础。

字典树的构造

特征

以只含小写字母的字符串为例,对于具有相同前缀的单词,它们的每一位父节点都是一样的:也就是说, 多个单词存储时共用相同的前缀(Prefix)。为了区分同一条链上的不同字符,可以在节点上设置一个标志,标记这个节点是否是一个单词的末尾。

性质

  • 根节点不包含字符,除根节点之外的每一个节点都包含一个字符。
  • 从根节点到某个节点,路径上所经过的字符连接起来,就是这个节点对应的字符串。
  • 一个完整的单词并不是存储在某个节点上,而是存储在一条链上。
  • 一个节点的所有子节点都具有相同的前缀。

定义数据结构

struct TrieNode
{
    <Type> data;
    bool is_end_of_word; // 标记是否是单词的末尾
    TrieNode * children[SIZE]; // 指向多个子节点
};

复杂度分析

字典树具有较好的时间复杂度:插入和查找一个单词的复杂度都是 $O(m)$,其中 $m$ 为待插入/查询的字符串长度,与整棵树的大小无关。

但就像线段树一样,它的空间复杂度较差:每个节点都需要设置 SIZE 个子节点,而大多数并不会被用到。而常用的更容易编码的静态数组存储将会浪费更多的空间。

像 BFS 一样,字典树是一种 空间换时间 的数据结构。所有基于字典树的数据结构和算法都具有相似的特征。

应用

  • 字符串检索
  • 词频统计
  • 字典序排序:插入时,在树的平级按字母表的顺序插入。字典序建好之后,再利用先序遍历,就得到了字典序的排序。
  • 前缀匹配:对于相同的前缀筛选单词

模板代码

字典树模板题:P2580 于是他错误的点名开始了

观察题目,发现这是一道普通的字符串匹配问题,有两种解决方式:STL map 和 字典树。

STL map

STL map 进行插入和查询的复杂度为 $O(\log_2 n)$,比字典树慢一些。

#include <iostream>
#include <map>

using namespace std;

int read()
{
    int x = 0, f = 1;
    char ch = 0;
    while (!isdigit(ch))
    {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch))
    {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return x * f;
}

int n, m;
string name;

map<string, int> student;

int main()
{
    n = read();
    while (n--)
    {
        cin >> name;
        student[name] = 1;
    }
    m = read();
    while (m--)
    {
        cin >> name;
        if (student[name] == 1)
        {
            printf("OK\n");
            student[name] = 2;
        }
        else if (student[name] == 2)
            printf("REPEAT\n");
        else
            printf("WRONG\n");
    }
    return 0;
}

字典树

STL map 进行插入和查询的复杂度为 $O(\log_2 n)$,比字典树慢一些。

用静态数组存储

首先,用结构体数组 t[] 存储字典树的节点。每个节点都有 $26$ 个子节点,即 $26$ 个小写字母。

在某个节点上,如果 t[now].son[v - 'a] != 0,就表示这个节点存储了一个字符 v,并且让 now 指向下一个字符的存储位置,now 是用 cnt 累加的一个存储位置。特别地,用 t[0] 表示字符串的起点,即第 $1$ 个字符。

操作

字典树有插入和查询两个主要操作

插入操作

假设存储字符串 "ab"。首先插入第一个字符 'a',假设此时 cnt = 5,那么令 t[0].son['a' - 'a'] = now = cnt = 5,表示存储了字符 'a',并且下一个字符存储在 t[5],也就是 'b' 要存储在 t[5].son['b' - 'a']

如果还需要再加入一个字符串 "ac",那么首先查询到 'a'已经存储,并且 t[0].son['a' - 'a'] = 5,然后再存储 'c',即 t[5].son['c' - 'a'] 上。

查询操作

例如,查询字符串 "abc" 是否存在,就先检查 t[0].son['a' - 'a'] = now 是否等于 $0$,如果不为 $0$,就说明 'a' 存在,进一步检查字母 'b'……以此类推。

模板代码

#include <iostream>
#include <cstring>

using namespace std;

int read()
{
    int x = 0, f = 1;
    char ch = 0;
    while (!isdigit(ch))
    {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch))
    {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return x * f;
}

const int maxN = 1e6;

struct node
{
    bool repeat; // 这个前缀是否重复
    int son[26]; // 26个英文字母作为子节点
    int sum;     // 前缀出现的次数
    bool isEnd;  // 是否为单词的结尾
} t[maxN];

int cnt = 1; // 当前新分配的存储位置
int n, m;

char s[55];

void insert(char *s)
{
    int now = 0;
    for (int i = 0; i < s[i]; i++)
    {
        int ch = s[i] - 'a';
        if (t[now].son[ch] == 0)    // 这个字符还没有存储过
            t[now].son[ch] = cnt++; // 把当前的位置分配给它
        now = t[now].son[ch];       // 沿着字典树向下走
        t[now].sum++;               // 统计这个前缀出现过多少次
        if (i == strlen(s) - 1)
            t[now].isEnd = true;
    }
}

int find(char *s)
{
    int now = 0;
    for (int i = 0; i < s[i]; i++)
    {
        int ch = s[i] - 'a';
        if (t[now].son[ch] == 0)
            return 3;
        now = t[now].son[ch];
    }
    if (t[now].isEnd == false)
        return 3;
    if (t[now].sum == 0) // 这个前缀没有出现过
        return 3;
    if (t[now].repeat == false)
    {
        t[now].repeat = true;
        return 1;
    }
    return 2;
}

int main()
{
    n = read();
    while (n--)
    {
        scanf("%s", s);
        insert(s);
    }
    m = read();
    while (m--)
    {
        scanf("%s", s);
        int r = find(s);
        if (r == 1)
            printf("OK\n");
        else if (r == 2)
            printf("REPEAT\n");
        else
            printf("WRONG\n");
    }
    return 0;
}

作者

觉得有帮助的话可以投喂煮啵,助力煮啵过上点外卖不用神卷的生活

本文作者:Flaw_Owl
看到这里的朋友们,作者十分感谢!如果你看到任何写得不清楚或是错误之处,或者是有什么希望看到的课程和内容笔记,欢迎在评论区留言!评论区可以单纯的发表文字,也可以使用 Markdown 语法,你可以参照 Markdown 官方教程发出带有格式的评论。

版权说明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0协议,转载请注明文章地址及作者哦
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇