trie 树(字典树)

前言

trie树,也称字典树。是一种支持查找、插入、删除字符串的数据结构。

他可以被看成一种自动机,只接受特定字符串。

接下来我们用 \(S\) 表示字符集,\(|S|\) 表示字符集。

算法复杂度

以下 \(s\) 表示插入、删除、查询的字符串。

时间复杂度

插入:\(O(|s|)\)
删除:\(O(|s|)\)
查询:\(O(|s|)\)

空间复杂度:\(O(\min(|S||s|n, |S|^{|s|}))\)

算法思想

如果直接用字符串数组来维护,则会发现查找和删除时间复杂度过高。

于是尝试模仿字典储存字符串的方法。

首先,字典会将单词按照字典序排序,然后对于每个首字母记录下第一个单词出现位置。

我们在查找单词时,先去查找单词首字母,然后在首字母出现的范围内再查找单词。

若在计算机上,则可以继续优化。我们不单可以用首字母,在用首字母确定范围后还可以在这个范围内再用第二个字母缩小范围。再而用第三个,以此类推。

如何实现?我们可以建立一颗树,动态加点。

对于根节点,不记录字符,接下来每个节点都表示一个字符。一个节点的儿子表示不同的字符。

这样,根节点到一个节点的路径可以组成一个字符串。

算法流程

接下来我们使用 \(d_u\) 代表节点 \(u\) 的深度。\(s_i\) 代表字符串第 \(i\) 位字符,\(|s|\) 代表字符串 \(s\) 的长度。

根节点深度为 \(0\)

插入

插入字符串 \(s\)

从根开始遍历,假设当前节点为 \(u\),先询问是否有表示 \(s_{d_i}\) 的子节点。

没有这个节点则创建一个,继续走。

直到执行完 \(|s|\) 次后,则走到了代表这个字符串的节点。

之后就是执行相关操作了。

查询

查询字符串 \(s\)

从根开始遍历,假设当前节点为 \(u\),先询问是否有表示 \(s_{d_i}\) 的子节点。

没有这个节点,就说明字典树中没有要查询的字符串 \(s\)

直到执行完 \(|s|\) 次后,则走到了代表这个字符串的节点。

删除

删除字符串 \(s\)

从根开始遍历,假设当前节点为 \(u\),先询问是否有表示 \(s_{d_i}\) 的子节点。

没有这个节点,就说明字典树中没有要查询的字符串 \(s\)

直到执行完 \(|s|\) 次后,则走到了代表这个字符串的节点。

然后删除节点相关信息。

算法伪代码

insert(s):
p = 0
n = size s
for i in [0, n]
	if son[p][s[i]] not exists
		tot = tot + 1
		son[p][s[i]] = tot
	p = son[p][s[i]]
end

query(s):
p = 0
n = size s
for i in [0, n]
	if son[p][s[i]] not exists
		unfind
	p = son[p][s[i]]
end

delete(s):
p = 0
n = size s
for i in [0, n]
	if son[p][s[i]] not exists
		unfind
	p = son[p][s[i]]
delete info
end

算法优化

我们可以发现这算法时间复杂度很好,但是空间太大了。

所以考虑用一下奇怪的优化

正常的话要储存基本的字符串我们要每个节点 \(128\) 个儿子,这样空间复杂度很大。

但是,我们可以考虑把一个字符拆分成 \(2\) 个一次插入,这样一个字符串长度就为 \(2|s|\),然后每个节点儿子个数就为 \(16\)

因为一般情况下都是 \(128|s|n\)\(128 ^ {|s|}\)

那原来空间为 \(128|s|n\),现在变为 \(16 \times 2 |s| n = 32|s|n\)

减少了 \(4\) 倍!当然你也可以考虑拆成4份 \(O(16|s|n)\)

posted on 2023-02-13 16:42  Evan_song  阅读(25)  评论(0)    收藏  举报